Merge branch 'master' into auto-open-fixes
This commit is contained in:
commit
08b348be50
@ -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,10 +28,37 @@ 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.
|
||||
- New `:insert-text` command to insert a given text into a field on the page,
|
||||
which replaces `:paste-primary` together with the `{primary}` replacement.
|
||||
- New `:window-only` command to close all other windows.
|
||||
- New `prev-category` and `next-category` arguments to `:completion-item-focus`
|
||||
to focus the previous/next category in the completion (bound to `<Ctrl-Tab>`
|
||||
and `<Ctrl-Shift-Tab>` by default).
|
||||
- New `:click-element` command to fake a click on a element.
|
||||
- New `:debug-log-filter` command to change console log filtering on-the-fly.
|
||||
- New `:debug-log-level` command to change the console loglevel on-the-fly.
|
||||
|
||||
Changed
|
||||
~~~~~~~
|
||||
|
||||
- Hints are now drawn natively in Qt instead of using web elements. This has a
|
||||
few implications for users:
|
||||
* The `hints -> opacity` setting does not exist anymore, but you can use
|
||||
`rgba(r, g, b, alpha)` colors instead for `colors -> hints.bg`.
|
||||
* The `hints -> font` setting is not affected by
|
||||
`fonts -> web-family-fixed` anymore. Thus, a transformer got added to
|
||||
change `Monospace` to `${_monospace}`.
|
||||
* Gradients in hint colors can now be configured by using `qlineargradient`
|
||||
and friends instead of `-webkit-gradient`. The most common cases get
|
||||
migrated automatically, but if you drastically changed the defaults,
|
||||
you'll need to manually adjust your config.
|
||||
* Styling hints by styling `qutehint` elements in `user-stylesheet` was
|
||||
never officially supported and does not work anymore.
|
||||
* Hints are now not affected by the page's stylesheet or zoom anymore.
|
||||
- `:bookmark-add` now has a `--toggle` flag which deletes the bookmark if it
|
||||
already exists.
|
||||
- `:bookmark-load` now has a `--delete` flag which deletes the bookmark after
|
||||
@ -51,6 +78,37 @@ Changed
|
||||
`: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, or `first-opened` to use the first (oldest) available 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.
|
||||
- `:navigate` now takes a count for `up`/`increment`/`decrement`.
|
||||
- The `hints -> auto-follow` setting now can be set to
|
||||
`always`/`full-match`/`unique-match`/`never` to more precisely control when
|
||||
hints should be followed automatically.
|
||||
- Counts can now be used with special keybindings (e.g. with modifiers).
|
||||
This was already implemented for v0.7.0 originally, but got reverted because
|
||||
it caused some issues and then never re-applied.
|
||||
- Sending a command to an existing instance (via "qutebrowser :reload") now
|
||||
doesn't mark it as urgent anymore.
|
||||
- `tabs -> title-format` now treats an empty string as valid.
|
||||
- Bindings for `:`, `/` and `?` are now configured explicitly and not hardcoded
|
||||
anymore.
|
||||
|
||||
Deprecated
|
||||
~~~~~~~~~~
|
||||
|
||||
- The `:paste` command got deprecated as `:open` with `{clipboard}` and
|
||||
`{primary}` can be used instead.
|
||||
- The `:paste-primary` command got deprecated as `:insert-text {primary}` can
|
||||
be used instead.
|
||||
|
||||
Removed
|
||||
~~~~~~~
|
||||
@ -59,6 +117,20 @@ Removed
|
||||
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.
|
||||
- The `hints -> opacity` setting - see the "Changed" section for details.
|
||||
|
||||
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
|
||||
- `:undo` now doesn't undo tabs "closed" by `:tab-detach` anymore.
|
||||
- Fixed an issue with hint chars not being cleared correctly when leaving hint
|
||||
mode.
|
||||
- `:tab-detach` now fails correctly when there's only one tab open.
|
||||
|
||||
v0.8.3 (unreleased)
|
||||
-------------------
|
||||
@ -69,6 +141,7 @@ 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.
|
||||
- Fixed crash when using hints on Void Linux.
|
||||
|
||||
v0.8.2
|
||||
------
|
||||
@ -78,7 +151,6 @@ Fixed
|
||||
|
||||
- Fixed `general -> private-browsing` not being set correctly until a restart
|
||||
(which caused e.g. local storage to be enabled).
|
||||
- Fixed crash when using hints with JS disabled in some rare circumstances.
|
||||
- When hinting input fields (`:t`), also consider input elements without a type.
|
||||
- Fixed crash when opening an invalid URL with a percent-encoded and a real @ in it
|
||||
- Fixed default `;o` and `;O` bindings
|
||||
@ -92,6 +164,7 @@ Fixed
|
||||
- Fixed crash when cancelling a download after doing `:prompt-open-download`
|
||||
- Fixed crash when writing a download to disk fails with
|
||||
`:prompt-open-download`.
|
||||
- Fixed `:restart` deleting the basedir when it was given with `--basedir`.
|
||||
|
||||
v0.8.1
|
||||
------
|
||||
@ -198,7 +271,6 @@ Changed
|
||||
- `:navigate` now clears the URL fragment
|
||||
- `:completion-item-del` (`Ctrl-D`) can now be used in `:buffer` completion to
|
||||
close a tab
|
||||
- Counts can now be used with special keybindings (e.g. with modifiers)
|
||||
- Various SSL ciphers are now disabled by default. With recent Qt/OpenSSL
|
||||
versions those already all are disabled, but with older versions they might
|
||||
not be.
|
||||
|
@ -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
|
||||
|
@ -142,15 +142,15 @@ Contributors, sorted by the number of commits in descending order:
|
||||
* Florian Bruhin
|
||||
* Daniel Schadt
|
||||
* Ryan Roden-Corrent
|
||||
* Jakub Klinkovský
|
||||
* Antoni Boucher
|
||||
* Lamar Pavel
|
||||
* Jan Verbeek
|
||||
* Bruno Oliveira
|
||||
* Alexander Cogneau
|
||||
* Marshall Lochbaum
|
||||
* Felix Van der Jeugt
|
||||
* Jakub Klinkovský
|
||||
* Martin Tournoij
|
||||
* Jan Verbeek
|
||||
* Raphael Pierzina
|
||||
* Joel Torstensson
|
||||
* Patric Schmitz
|
||||
@ -166,21 +166,24 @@ Contributors, sorted by the number of commits in descending order:
|
||||
* Thorsten Wißmann
|
||||
* Austin Anderson
|
||||
* Jimmy
|
||||
* Niklas Haas
|
||||
* Alexey "Averrin" Nabrodov
|
||||
* avk
|
||||
* ZDarian
|
||||
* Milan Svoboda
|
||||
* John ShaggyTwoDope Jenkins
|
||||
* nanjekyejoannah
|
||||
* Peter Vilim
|
||||
* Clayton Craft
|
||||
* nanjekyejoannah
|
||||
* Oliver Caldwell
|
||||
* Jonas Schürmann
|
||||
* error800
|
||||
* Michael Hoang
|
||||
* Liam BEGUIN
|
||||
* skinnay
|
||||
* Zach-Button
|
||||
* Tomasz Kramkowski
|
||||
* Peter Rice
|
||||
* Ismail S
|
||||
* Halfwit
|
||||
* David Vogt
|
||||
@ -196,6 +199,7 @@ Contributors, sorted by the number of commits in descending order:
|
||||
* Brian Jackson
|
||||
* sbinix
|
||||
* neeasade
|
||||
* knaggita
|
||||
* jnphilipp
|
||||
* Tobias Patzl
|
||||
* Stefan Tatschner
|
||||
@ -204,6 +208,7 @@ Contributors, sorted by the number of commits in descending order:
|
||||
* Panashe M. Fundira
|
||||
* Link
|
||||
* Larry Hynes
|
||||
* Julian Weigt
|
||||
* Johannes Altmanninger
|
||||
* Jeremy Kaplan
|
||||
* Ismail
|
||||
@ -220,7 +225,6 @@ Contributors, sorted by the number of commits in descending order:
|
||||
* zwarag
|
||||
* xd1le
|
||||
* oniondreams
|
||||
* knaggita
|
||||
* issue
|
||||
* haxwithaxe
|
||||
* evan
|
||||
@ -234,6 +238,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,5 +1,21 @@
|
||||
// DO NOT EDIT THIS FILE DIRECTLY!
|
||||
// It is autogenerated from docstrings by running:
|
||||
// $ python3 scripts/dev/src2asciidoc.py
|
||||
|
||||
= Commands
|
||||
|
||||
In qutebrowser, all keybindings are mapped to commands.
|
||||
|
||||
Some commands are hidden, which means they don't show up in the command
|
||||
completion when pressing `:`, as they're typically not useful to run by hand.
|
||||
|
||||
In the commandline, there are also some variables you can use:
|
||||
|
||||
- `{url}` expands to the URL of the current page
|
||||
- `{url:pretty}` expands to the URL in decoded format
|
||||
- `{clipboard}` expands to the clipboard contents
|
||||
- `{primary}` expands to the primary selection contents
|
||||
|
||||
== Normal commands
|
||||
.Quick reference
|
||||
[options="header",width="75%",cols="25%,75%"]
|
||||
@ -28,13 +44,13 @@
|
||||
|<<hint,hint>>|Start hinting.
|
||||
|<<history-clear,history-clear>>|Clear all browsing history.
|
||||
|<<home,home>>|Open main startpage in current tab.
|
||||
|<<insert-text,insert-text>>|Insert text at cursor position.
|
||||
|<<inspector,inspector>>|Toggle the web inspector.
|
||||
|<<jseval,jseval>>|Evaluate a JavaScript string.
|
||||
|<<later,later>>|Execute a command after some time.
|
||||
|<<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.
|
||||
@ -65,6 +81,7 @@
|
||||
|<<unbind,unbind>>|Unbind a keychain.
|
||||
|<<undo,undo>>|Re-open a closed tab (optionally skipping [count] closed tabs).
|
||||
|<<view-source,view-source>>|Show the source of the current page.
|
||||
|<<window-only,window-only>>|Close all windows except for the current one.
|
||||
|<<wq,wq>>|Save open pages and quit.
|
||||
|<<yank,yank>>|Yank something to the clipboard or primary selection.
|
||||
|<<zoom,zoom>>|Set the zoom level for the current tab.
|
||||
@ -320,12 +337,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 +393,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.
|
||||
@ -390,6 +416,18 @@ Note this only clears the global history (e.g. `~/.local/share/qutebrowser/histo
|
||||
=== home
|
||||
Open main startpage in current tab.
|
||||
|
||||
[[insert-text]]
|
||||
=== insert-text
|
||||
Syntax: +:insert-text 'text'+
|
||||
|
||||
Insert text at cursor position.
|
||||
|
||||
==== positional arguments
|
||||
* +'text'+: The text to insert.
|
||||
|
||||
==== note
|
||||
* This command does not split arguments after the last argument and handles quotes literally.
|
||||
|
||||
[[inspector]]
|
||||
=== inspector
|
||||
Toggle the web inspector.
|
||||
@ -468,12 +506,18 @@ This tries to automatically click on typical _Previous Page_ or _Next Page_ link
|
||||
* +*-b*+, +*--bg*+: Open in a background tab.
|
||||
* +*-w*+, +*--window*+: Open in a new window.
|
||||
|
||||
==== count
|
||||
For `increment` and `decrement`, the number to change the URL by. For `up`, the number of levels to go up in the URL.
|
||||
|
||||
|
||||
[[open]]
|
||||
=== open
|
||||
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.
|
||||
|
||||
@ -490,20 +534,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']+
|
||||
@ -844,6 +874,10 @@ Re-open a closed tab (optionally skipping [count] closed tabs).
|
||||
=== view-source
|
||||
Show the source of the current page.
|
||||
|
||||
[[window-only]]
|
||||
=== window-only
|
||||
Close all windows except for the current one.
|
||||
|
||||
[[wq]]
|
||||
=== wq
|
||||
Syntax: +:wq ['name']+
|
||||
@ -910,6 +944,7 @@ How many steps to zoom out.
|
||||
|==============
|
||||
|Command|Description
|
||||
|<<clear-keychain,clear-keychain>>|Clear the currently entered key chain.
|
||||
|<<click-element,click-element>>|Click the element matching the given filter.
|
||||
|<<command-accept,command-accept>>|Execute the command currently in the commandline.
|
||||
|<<command-history-next,command-history-next>>|Go forward in the commandline history.
|
||||
|<<command-history-prev,command-history-prev>>|Go back in the commandline history.
|
||||
@ -940,7 +975,6 @@ How many steps to zoom out.
|
||||
|<<move-to-start-of-next-block,move-to-start-of-next-block>>|Move the cursor or selection to the start of next block.
|
||||
|<<move-to-start-of-prev-block,move-to-start-of-prev-block>>|Move the cursor or selection to the start of previous block.
|
||||
|<<open-editor,open-editor>>|Open an external editor with the currently selected form field.
|
||||
|<<paste-primary,paste-primary>>|Paste the primary selection at cursor position.
|
||||
|<<prompt-accept,prompt-accept>>|Accept the current prompt.
|
||||
|<<prompt-no,prompt-no>>|Answer no to a yes/no prompt.
|
||||
|<<prompt-open-download,prompt-open-download>>|Immediately open a download.
|
||||
@ -974,6 +1008,22 @@ How many steps to zoom out.
|
||||
=== clear-keychain
|
||||
Clear the currently entered key chain.
|
||||
|
||||
[[click-element]]
|
||||
=== click-element
|
||||
Syntax: +:click-element [*--target* 'target'] 'filter' 'value'+
|
||||
|
||||
Click the element matching the given filter.
|
||||
|
||||
The given filter needs to result in exactly one element, otherwise, an error is shown.
|
||||
|
||||
==== positional arguments
|
||||
* +'filter'+: How to filter the elements. id: Get an element based on its ID.
|
||||
|
||||
* +'value'+: The value to filter for.
|
||||
|
||||
==== optional arguments
|
||||
* +*-t*+, +*--target*+: How to open the clicked element (normal/tab/tab-bg/window).
|
||||
|
||||
[[command-accept]]
|
||||
=== command-accept
|
||||
Execute the command currently in the commandline.
|
||||
@ -997,7 +1047,7 @@ Syntax: +:completion-item-focus 'which'+
|
||||
Shift the focus of the completion menu to another item.
|
||||
|
||||
==== positional arguments
|
||||
* +'which'+: 'next' or 'prev'
|
||||
* +'which'+: 'next', 'prev', 'next-category', or 'prev-category'.
|
||||
|
||||
[[drop-selection]]
|
||||
=== drop-selection
|
||||
@ -1169,10 +1219,6 @@ 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.
|
||||
|
||||
[[paste-primary]]
|
||||
=== paste-primary
|
||||
Paste the primary selection at cursor position.
|
||||
|
||||
[[prompt-accept]]
|
||||
=== prompt-accept
|
||||
Accept the current prompt.
|
||||
@ -1403,6 +1449,8 @@ These commands are mainly intended for debugging. They are hidden if qutebrowser
|
||||
|<<debug-crash,debug-crash>>|Crash for debugging purposes.
|
||||
|<<debug-dump-page,debug-dump-page>>|Dump the current page's content to a file.
|
||||
|<<debug-log-capacity,debug-log-capacity>>|Change the number of log lines to be stored in RAM.
|
||||
|<<debug-log-filter,debug-log-filter>>|Change the log filter for console logging.
|
||||
|<<debug-log-level,debug-log-level>>|Change the log level for console logging.
|
||||
|<<debug-pyeval,debug-pyeval>>|Evaluate a python string and display the results as a web page.
|
||||
|<<debug-set-fake-clipboard,debug-set-fake-clipboard>>|Put data into the fake clipboard and enable logging, used for tests.
|
||||
|<<debug-trace,debug-trace>>|Trace executed code via hunter.
|
||||
@ -1454,6 +1502,24 @@ Change the number of log lines to be stored in RAM.
|
||||
==== positional arguments
|
||||
* +'capacity'+: Number of lines for the log.
|
||||
|
||||
[[debug-log-filter]]
|
||||
=== debug-log-filter
|
||||
Syntax: +:debug-log-filter 'filters'+
|
||||
|
||||
Change the log filter for console logging.
|
||||
|
||||
==== positional arguments
|
||||
* +'filters'+: A comma separated list of logger names.
|
||||
|
||||
[[debug-log-level]]
|
||||
=== debug-log-level
|
||||
Syntax: +:debug-log-level 'level'+
|
||||
|
||||
Change the log level for console logging.
|
||||
|
||||
==== positional arguments
|
||||
* +'level'+: The log level to set.
|
||||
|
||||
[[debug-pyeval]]
|
||||
=== debug-pyeval
|
||||
Syntax: +:debug-pyeval [*--quiet*] 's'+
|
||||
|
@ -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
|
||||
@ -174,18 +178,18 @@
|
||||
|==============
|
||||
|Setting|Description
|
||||
|<<hints-border,border>>|CSS border value for hints.
|
||||
|<<hints-opacity,opacity>>|Opacity for hints.
|
||||
|<<hints-mode,mode>>|Mode to use for hints.
|
||||
|<<hints-chars,chars>>|Chars used for hint strings.
|
||||
|<<hints-min-chars,min-chars>>|Minimum number of chars used for hint strings.
|
||||
|<<hints-scatter,scatter>>|Whether to scatter hint key chains (like Vimium) or not (like dwb). Ignored for number hints.
|
||||
|<<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,auto-follow>>|Controls when a hint can be automatically followed without the user pressing Enter.
|
||||
|<<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''
|
||||
@ -243,7 +247,7 @@
|
||||
|<<colors-tabs.indicator.error,tabs.indicator.error>>|Color for the tab indicator on errors..
|
||||
|<<colors-tabs.indicator.system,tabs.indicator.system>>|Color gradient interpolation system for the tab indicator.
|
||||
|<<colors-hints.fg,hints.fg>>|Font color for hints.
|
||||
|<<colors-hints.bg,hints.bg>>|Background color for hints.
|
||||
|<<colors-hints.bg,hints.bg>>|Background color for hints. Note that you can use a `rgba(...)` value for transparency.
|
||||
|<<colors-hints.fg.match,hints.fg.match>>|Font color for the matched part of hints.
|
||||
|<<colors-downloads.bg.bar,downloads.bg.bar>>|Background color for the download bar.
|
||||
|<<colors-downloads.fg.start,downloads.fg.start>>|Color gradient start for download text.
|
||||
@ -443,6 +447,19 @@ 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:
|
||||
|
||||
* +first-opened+: Open new tabs in the first (oldest) opened window.
|
||||
* +last-opened+: Open new tabs in the last (newest) 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.
|
||||
@ -586,7 +601,7 @@ This setting is only available with the QtWebKit backend.
|
||||
=== user-stylesheet
|
||||
User stylesheet to use (absolute filename, filename relative to the config directory or CSS string). Will expand environment variables.
|
||||
|
||||
Default: +pass:[::-webkit-scrollbar { width: 0px; height: 0px; }]+
|
||||
Default: +pass:[html > ::-webkit-scrollbar { width: 0px; height: 0px; }]+
|
||||
|
||||
This setting is only available with the QtWebKit backend.
|
||||
|
||||
@ -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()
|
||||
@ -1596,12 +1600,6 @@ CSS border value for hints.
|
||||
|
||||
Default: +pass:[1px solid #E3BE23]+
|
||||
|
||||
[[hints-opacity]]
|
||||
=== opacity
|
||||
Opacity for hints.
|
||||
|
||||
Default: +pass:[0.7]+
|
||||
|
||||
[[hints-mode]]
|
||||
=== mode
|
||||
Mode to use for hints.
|
||||
@ -1656,14 +1654,16 @@ Default: +pass:[/usr/share/dict/words]+
|
||||
|
||||
[[hints-auto-follow]]
|
||||
=== auto-follow
|
||||
Follow a hint immediately when the hint text is completely matched.
|
||||
Controls when a hint can be automatically followed without the user pressing Enter.
|
||||
|
||||
Valid values:
|
||||
|
||||
* +true+
|
||||
* +false+
|
||||
* +always+: Auto-follow whenever there is only a single hint on a page.
|
||||
* +unique-match+: Auto-follow whenever there is a unique non-empty match in either the hint string (word mode) or filter (number mode).
|
||||
* +full-match+: Follow the hint when the user typed the whole hint (letter, word or number mode) or the element's text (only in number mode).
|
||||
* +never+: The user will always need to press Enter to follow a hint.
|
||||
|
||||
Default: +pass:[true]+
|
||||
Default: +pass:[unique-match]+
|
||||
|
||||
[[hints-auto-follow-timeout]]
|
||||
=== auto-follow-timeout
|
||||
@ -1694,6 +1694,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.
|
||||
@ -2034,9 +2045,9 @@ Default: +pass:[black]+
|
||||
|
||||
[[colors-hints.bg]]
|
||||
=== hints.bg
|
||||
Background color for hints.
|
||||
Background color for hints. Note that you can use a `rgba(...)` value for transparency.
|
||||
|
||||
Default: +pass:[-webkit-gradient(linear, left top, left bottom, color-stop(0%,#FFF785), color-stop(100%,#FFC542))]+
|
||||
Default: +pass:[qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 rgba(255, 247, 133, 0.8), stop:1 rgba(255, 197, 66, 0.8))]+
|
||||
|
||||
[[colors-hints.fg.match]]
|
||||
=== hints.fg.match
|
||||
@ -2183,7 +2194,7 @@ Default: +pass:[8pt ${_monospace}]+
|
||||
=== hints
|
||||
Font used for the hints.
|
||||
|
||||
Default: +pass:[bold 13px Monospace]+
|
||||
Default: +pass:[bold 13px ${_monospace}]+
|
||||
|
||||
[[fonts-debug-console]]
|
||||
=== debug-console
|
||||
|
@ -3,18 +3,15 @@ MAINTAINER Florian Bruhin <me@the-compiler.org>
|
||||
|
||||
RUN echo 'Server = http://mirror.de.leaseweb.net/archlinux/$repo/os/$arch' > /etc/pacman.d/mirrorlist
|
||||
RUN pacman-key --init && pacman-key --populate archlinux && pacman -Sy --noconfirm archlinux-keyring
|
||||
RUN pacman -S --noconfirm pacman && pacman-db-upgrade
|
||||
|
||||
RUN pacman -Suyy --noconfirm
|
||||
RUN pacman-db-upgrade
|
||||
|
||||
RUN pacman -S --noconfirm \
|
||||
RUN pacman -Suyy --noconfirm \
|
||||
git \
|
||||
python-tox \
|
||||
qt5-base \
|
||||
qt5-webkit \
|
||||
python-pyqt5 \
|
||||
xorg-xinit \
|
||||
herbstluftwm \
|
||||
xorg-server-xvfb
|
||||
|
||||
RUN echo 'en_US.UTF-8 UTF-8' > /etc/locale.gen && locale-gen
|
||||
@ -23,13 +20,9 @@ RUN useradd user && mkdir /home/user && chown -R user:users /home/user
|
||||
USER user
|
||||
WORKDIR /home/user
|
||||
|
||||
ENV DISPLAY=:0
|
||||
ENV LC_ALL=en_US.UTF-8
|
||||
ENV LANG=en_US.UTF-8
|
||||
|
||||
CMD Xvfb -screen 0 800x600x24 :0 & \
|
||||
sleep 2 && \
|
||||
herbstluftwm & \
|
||||
git clone /outside qutebrowser.git && \
|
||||
CMD git clone /outside qutebrowser.git && \
|
||||
cd qutebrowser.git && \
|
||||
tox -e py35
|
||||
|
@ -11,10 +11,10 @@ RUN apt-get -y update && \
|
||||
python-tox \
|
||||
python3-sip \
|
||||
xvfb \
|
||||
xauth \
|
||||
git \
|
||||
python3-setuptools \
|
||||
wget \
|
||||
herbstluftwm \
|
||||
locales \
|
||||
libjs-pdf
|
||||
RUN echo 'en_US.UTF-8 UTF-8' > /etc/locale.gen && locale-gen
|
||||
@ -23,13 +23,9 @@ RUN useradd user && mkdir /home/user && chown -R user:users /home/user
|
||||
USER user
|
||||
WORKDIR /home/user
|
||||
|
||||
ENV DISPLAY=:0
|
||||
ENV LC_ALL=en_US.UTF-8
|
||||
ENV LANG=en_US.UTF-8
|
||||
|
||||
CMD Xvfb -screen 0 800x600x24 :0 & \
|
||||
sleep 2 && \
|
||||
herbstluftwm & \
|
||||
git clone /outside qutebrowser.git && \
|
||||
CMD git clone /outside qutebrowser.git && \
|
||||
cd qutebrowser.git && \
|
||||
tox -e py34
|
||||
|
@ -14,7 +14,6 @@ RUN apt-get -y update && \
|
||||
git \
|
||||
python3-setuptools \
|
||||
wget \
|
||||
herbstluftwm \
|
||||
language-pack-en \
|
||||
libjs-pdf \
|
||||
dbus
|
||||
@ -25,13 +24,9 @@ RUN useradd user && mkdir /home/user && chown -R user:users /home/user
|
||||
USER user
|
||||
WORKDIR /home/user
|
||||
|
||||
ENV DISPLAY=:0
|
||||
ENV LC_ALL=en_US.UTF-8
|
||||
ENV LANG=en_US.UTF-8
|
||||
|
||||
CMD Xvfb -screen 0 800x600x24 :0 & \
|
||||
sleep 2 && \
|
||||
herbstluftwm & \
|
||||
git clone /outside qutebrowser.git && \
|
||||
CMD git clone /outside qutebrowser.git && \
|
||||
cd qutebrowser.git && \
|
||||
tox -e py35
|
||||
|
@ -1,3 +1,3 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
check-manifest==0.31
|
||||
check-manifest==0.32
|
||||
|
@ -2,4 +2,4 @@
|
||||
|
||||
codecov==2.0.5
|
||||
coverage==4.2
|
||||
requests==2.10.0
|
||||
requests==2.11.1
|
||||
|
@ -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.8
|
||||
six==1.10.0
|
||||
|
@ -1,2 +1,2 @@
|
||||
pip==8.1.2
|
||||
setuptools==25.1.6
|
||||
setuptools==25.2.0
|
||||
|
@ -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.1
|
||||
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.1
|
||||
six==1.10.0
|
||||
uritemplate.py==0.3.0
|
||||
uritemplate.py==1.0.0
|
||||
wrapt==1.10.8
|
||||
|
@ -42,4 +42,8 @@ git+https://github.com/pallets/jinja.git
|
||||
git+https://github.com/pallets/markupsafe.git
|
||||
hg+http://bitbucket.org/birkenfeld/pygments-main
|
||||
hg+https://bitbucket.org/fdik/pypeg
|
||||
hg+https://bitbucket.org/xi/pyyaml
|
||||
|
||||
# Fails to build:
|
||||
# gcc: error: ext/_yaml.c: No such file or directory
|
||||
# hg+https://bitbucket.org/xi/pyyaml
|
||||
PyYAML==3.11
|
||||
|
@ -2,11 +2,12 @@
|
||||
|
||||
beautifulsoup4==4.5.1
|
||||
CherryPy==7.1.0
|
||||
click==6.6
|
||||
coverage==4.2
|
||||
decorator==4.0.10
|
||||
Flask==0.10.1 # rq.filter: < 0.11.0
|
||||
Flask==0.11.1
|
||||
glob2==0.4.1
|
||||
httpbin==0.4.1
|
||||
httpbin==0.5.0
|
||||
hypothesis==3.4.2
|
||||
itsdangerous==0.24
|
||||
# Jinja2==2.8
|
||||
@ -23,11 +24,11 @@ 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
|
||||
|
@ -1,7 +1,7 @@
|
||||
beautifulsoup4
|
||||
CherryPy
|
||||
coverage
|
||||
Flask==0.10.1
|
||||
Flask
|
||||
httpbin
|
||||
hypothesis
|
||||
pytest
|
||||
@ -19,5 +19,4 @@ pytest-warnings
|
||||
pytest-xvfb
|
||||
vulture
|
||||
|
||||
#@ filter: Flask < 0.11.0
|
||||
#@ ignore: Jinja2, MarkupSafe
|
||||
|
@ -15,6 +15,8 @@ markers =
|
||||
xfail_norun: xfail the test with out running it
|
||||
ci: Tests which should only run on CI.
|
||||
flaky_once: Try to rerun this test once if it fails
|
||||
qtwebengine_todo: Features still missing with QtWebEngine
|
||||
qtwebengine_skip: Tests not applicable with QtWebEngine
|
||||
qt_log_level_fail = WARNING
|
||||
qt_log_ignore =
|
||||
^SpellCheck: .*
|
||||
@ -35,4 +37,5 @@ qt_log_ignore =
|
||||
^QObject::connect: Cannot connect \(null\)::stateChanged\(QNetworkSession::State\) to QNetworkReplyHttpImpl::_q_networkSessionStateChanged\(QNetworkSession::State\)
|
||||
^QXcbClipboard: Cannot transfer data, no data available
|
||||
^load glyph failed
|
||||
^Error when parsing the netrc file
|
||||
xfail_strict = true
|
||||
|
@ -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)
|
||||
|
||||
@ -551,8 +539,9 @@ class Quitter:
|
||||
argdict['session'] = session
|
||||
argdict['override_restore'] = False
|
||||
# Ensure :restart works with --temp-basedir
|
||||
argdict['temp_basedir'] = False
|
||||
argdict['temp_basedir_restarted'] = True
|
||||
if self._args.temp_basedir:
|
||||
argdict['temp_basedir'] = False
|
||||
argdict['temp_basedir_restarted'] = True
|
||||
|
||||
# Dump the data
|
||||
data = json.dumps(argdict)
|
||||
|
@ -21,14 +21,16 @@
|
||||
|
||||
import itertools
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QUrl, QObject, QPoint, QSizeF
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QUrl, QObject, QSizeF
|
||||
from PyQt5.QtGui import QIcon
|
||||
from PyQt5.QtWidgets import QWidget
|
||||
from PyQt5.QtWidgets import QWidget, QApplication
|
||||
|
||||
from qutebrowser.keyinput import modeman
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import utils, objreg, usertypes, message, log, qtutils
|
||||
from qutebrowser.utils import (utils, objreg, usertypes, message, log, qtutils,
|
||||
urlutils)
|
||||
from qutebrowser.misc import miscwidgets
|
||||
from qutebrowser.browser import mouse, hints
|
||||
|
||||
|
||||
tab_id_gen = itertools.count(0)
|
||||
@ -67,14 +69,22 @@ class TabData:
|
||||
load.
|
||||
inspector: The QWebInspector used for this webview.
|
||||
viewing_source: Set if we're currently showing a source view.
|
||||
open_target: How the next clicked link should be opened.
|
||||
override_target: Override for open_target for fake clicks (like hints).
|
||||
"""
|
||||
|
||||
__slots__ = ['keep_icon', 'viewing_source', 'inspector']
|
||||
|
||||
def __init__(self):
|
||||
self.keep_icon = False
|
||||
self.viewing_source = False
|
||||
self.inspector = None
|
||||
self.open_target = usertypes.ClickTarget.normal
|
||||
self.override_target = None
|
||||
|
||||
def combined_target(self):
|
||||
if self.override_target is not None:
|
||||
return self.override_target
|
||||
else:
|
||||
return self.open_target
|
||||
|
||||
|
||||
class AbstractPrinting:
|
||||
@ -219,19 +229,6 @@ class AbstractZoom(QObject):
|
||||
default_zoom = config.get('ui', 'default-zoom')
|
||||
self._set_factor_internal(float(default_zoom) / 100)
|
||||
|
||||
@pyqtSlot(QPoint)
|
||||
def _on_mouse_wheel_zoom(self, delta):
|
||||
"""Handle zooming via mousewheel requested by the web view."""
|
||||
divider = config.get('input', 'mouse-zoom-divider')
|
||||
factor = self.factor() + delta.y() / divider
|
||||
if factor < 0:
|
||||
return
|
||||
perc = int(100 * factor)
|
||||
message.info(self._win_id, "Zoom level: {}%".format(perc))
|
||||
self._neighborlist.fuzzyval = perc
|
||||
self._set_factor_internal(factor)
|
||||
self._default_zoom_changed = True
|
||||
|
||||
|
||||
class AbstractCaret(QObject):
|
||||
|
||||
@ -418,6 +415,55 @@ class AbstractHistory:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class AbstractElements:
|
||||
|
||||
"""Finding and handling of elements on the page."""
|
||||
|
||||
def __init__(self, tab):
|
||||
self._widget = None
|
||||
self._tab = tab
|
||||
|
||||
def find_css(self, selector, callback, *, only_visible=False):
|
||||
"""Find all HTML elements matching a given selector async.
|
||||
|
||||
Args:
|
||||
callback: The callback to be called when the search finished.
|
||||
selector: The CSS selector to search for.
|
||||
only_visible: Only show elements which are visible on screen.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def find_id(self, elem_id, callback):
|
||||
"""Find the HTML element with the given ID async.
|
||||
|
||||
Args:
|
||||
callback: The callback to be called when the search finished.
|
||||
elem_id: The ID to search for.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def find_focused(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 find_at_pos(self, pos, callback):
|
||||
"""Find the element at the given position async.
|
||||
|
||||
This is also called "hit test" elsewhere.
|
||||
|
||||
Args:
|
||||
pos: The QPoint to get the element for.
|
||||
callback: The callback to be called when the search finished.
|
||||
Called with a WebEngineElement or None.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class AbstractTab(QWidget):
|
||||
|
||||
"""A wrapper over the given widget to hide its API and expose another one.
|
||||
@ -455,6 +501,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
|
||||
@ -474,14 +521,23 @@ class AbstractTab(QWidget):
|
||||
# self.zoom = AbstractZoom(win_id=win_id)
|
||||
# self.search = AbstractSearch(parent=self)
|
||||
# self.printing = AbstractPrinting()
|
||||
# self.elements = AbstractElements(self)
|
||||
|
||||
self.data = TabData()
|
||||
self._layout = miscwidgets.WrapperLayout(self)
|
||||
self._widget = None
|
||||
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
|
||||
|
||||
# FIXME:qtwebengine Should this be public api via self.hints?
|
||||
# Also, should we get it out of objreg?
|
||||
hintmanager = hints.HintManager(win_id, self.tab_id, parent=self)
|
||||
objreg.register('hintmanager', hintmanager, scope='tab',
|
||||
window=self.win_id, tab=self.tab_id)
|
||||
|
||||
def _set_widget(self, widget):
|
||||
# pylint: disable=protected-access
|
||||
self._widget = widget
|
||||
@ -492,7 +548,11 @@ class AbstractTab(QWidget):
|
||||
self.zoom._widget = widget
|
||||
self.search._widget = widget
|
||||
self.printing._widget = widget
|
||||
widget.mouse_wheel_zoom.connect(self.zoom._on_mouse_wheel_zoom)
|
||||
self.elements._widget = widget
|
||||
self._install_event_filter()
|
||||
|
||||
def _install_event_filter(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def _set_load_status(self, val):
|
||||
"""Setter for load_status."""
|
||||
@ -502,6 +562,61 @@ class AbstractTab(QWidget):
|
||||
self._load_status = val
|
||||
self.load_status_changed.emit(val.name)
|
||||
|
||||
def _event_target(self):
|
||||
"""Return the widget events should be sent to."""
|
||||
raise NotImplementedError
|
||||
|
||||
def send_event(self, evt, *, postpone=False):
|
||||
"""Send the given event to the underlying widget.
|
||||
|
||||
Args:
|
||||
postpone: Postpone the event to be handled later instead of
|
||||
immediately. Using this might cause crashes in Qt.
|
||||
"""
|
||||
recipient = self._event_target()
|
||||
if postpone:
|
||||
QApplication.postEvent(recipient, evt)
|
||||
else:
|
||||
QApplication.sendEvent(recipient, evt)
|
||||
|
||||
@pyqtSlot(QUrl)
|
||||
def _on_link_clicked(self, url):
|
||||
log.webview.debug("link clicked: url {}, override target {}, "
|
||||
"open_target {}".format(
|
||||
url.toDisplayString(),
|
||||
self.data.override_target,
|
||||
self.data.open_target))
|
||||
|
||||
if not url.isValid():
|
||||
msg = urlutils.get_errstring(url, "Invalid link clicked")
|
||||
message.error(self.win_id, msg)
|
||||
self.data.open_target = usertypes.ClickTarget.normal
|
||||
return False
|
||||
|
||||
target = self.data.combined_target()
|
||||
|
||||
if target == usertypes.ClickTarget.normal:
|
||||
return
|
||||
elif target == usertypes.ClickTarget.tab:
|
||||
win_id = self.win_id
|
||||
bg_tab = False
|
||||
elif target == usertypes.ClickTarget.tab_bg:
|
||||
win_id = self.win_id
|
||||
bg_tab = True
|
||||
elif target == usertypes.ClickTarget.window:
|
||||
from qutebrowser.mainwindow import mainwindow
|
||||
window = mainwindow.MainWindow()
|
||||
window.show()
|
||||
win_id = window.win_id
|
||||
bg_tab = False
|
||||
else:
|
||||
raise ValueError("Invalid ClickTarget {}".format(target))
|
||||
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=win_id)
|
||||
tabbed_browser.tabopen(url, background=bg_tab)
|
||||
self.data.open_target = usertypes.ClickTarget.normal
|
||||
|
||||
@pyqtSlot(QUrl)
|
||||
def _on_url_changed(self, url):
|
||||
"""Update title when URL has changed and no title is available."""
|
||||
@ -533,6 +648,11 @@ 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."""
|
||||
raise NotImplementedError
|
||||
|
||||
@pyqtSlot(int)
|
||||
def _on_load_progress(self, perc):
|
||||
self._progress = perc
|
||||
@ -542,7 +662,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):
|
||||
@ -583,14 +703,6 @@ class AbstractTab(QWidget):
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def run_js_blocking(self, code):
|
||||
"""Run javascript and block.
|
||||
|
||||
This returns the result to the caller. Its use should be avoided when
|
||||
possible as it runs a local event loop for QtWebEngine.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def shutdown(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@ -603,25 +715,6 @@ class AbstractTab(QWidget):
|
||||
def set_html(self, html, base_url):
|
||||
raise NotImplementedError
|
||||
|
||||
def find_all_elements(self, selector, callback, *, only_visible=False):
|
||||
"""Find all HTML elements matching a given selector async.
|
||||
|
||||
Args:
|
||||
callback: The callback to be called when the search finished.
|
||||
selector: The CSS selector to search for.
|
||||
only_visible: Only show elements which are visible on screen.
|
||||
"""
|
||||
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),
|
||||
|
@ -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')
|
||||
@ -384,10 +424,12 @@ class CommandDispatcher:
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window')
|
||||
def tab_detach(self):
|
||||
"""Detach the current tab to its own window."""
|
||||
if self._count() < 2:
|
||||
raise cmdexc.CommandError("Cannot detach one tab.")
|
||||
url = self._current_url()
|
||||
self._open(url, window=True)
|
||||
cur_widget = self._current_widget()
|
||||
self._tabbed_browser.close_tab(cur_widget)
|
||||
self._tabbed_browser.close_tab(cur_widget, add_undo=False)
|
||||
|
||||
def _back_forward(self, tab, bg, window, count, forward):
|
||||
"""Helper function for :back/:forward."""
|
||||
@ -442,7 +484,8 @@ class CommandDispatcher:
|
||||
@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):
|
||||
@cmdutils.argument('count', count=True)
|
||||
def navigate(self, where: str, tab=False, bg=False, window=False, count=1):
|
||||
"""Open typical prev/next links or navigate using the URL path.
|
||||
|
||||
This tries to automatically click on typical _Previous Page_ or
|
||||
@ -462,6 +505,8 @@ class CommandDispatcher:
|
||||
tab: Open in a new tab.
|
||||
bg: Open in a background tab.
|
||||
window: Open in a new window.
|
||||
count: For `increment` and `decrement`, the number to change the
|
||||
URL by. For `up`, the number of levels to go up in the URL.
|
||||
"""
|
||||
# save the pre-jump position in the special ' mark
|
||||
self.set_mark("'")
|
||||
@ -486,7 +531,7 @@ class CommandDispatcher:
|
||||
handler(browsertab=widget, win_id=self._win_id, baseurl=url,
|
||||
tab=tab, background=bg, window=window)
|
||||
elif where in ['up', 'increment', 'decrement']:
|
||||
new_url = handlers[where](url)
|
||||
new_url = handlers[where](url, count)
|
||||
self._open(new_url, tab, bg, window)
|
||||
else: # pragma: no cover
|
||||
raise ValueError("Got called with invalid value {} for "
|
||||
@ -796,7 +841,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.
|
||||
|
||||
@ -810,15 +856,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(
|
||||
@ -1185,14 +1228,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):
|
||||
@ -1314,7 +1356,11 @@ class CommandDispatcher:
|
||||
formatter = pygments.formatters.HtmlFormatter(full=True,
|
||||
linenos='table')
|
||||
highlighted = pygments.highlight(source, lexer, formatter)
|
||||
current_url = self._current_url()
|
||||
try:
|
||||
current_url = self._current_url()
|
||||
except cmdexc.CommandError as e:
|
||||
message.error(self._win_id, str(e))
|
||||
return
|
||||
new_tab = self._tabbed_browser.tabopen(explicit=True)
|
||||
new_tab.set_html(highlighted, current_url)
|
||||
new_tab.data.viewing_source = True
|
||||
@ -1424,8 +1470,7 @@ class CommandDispatcher:
|
||||
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.
|
||||
|
||||
@ -1433,7 +1478,7 @@ class CommandDispatcher:
|
||||
`general -> editor` config option.
|
||||
"""
|
||||
tab = self._current_widget()
|
||||
tab.find_focus_element(self._open_editor_cb)
|
||||
tab.elements.find_focused(self._open_editor_cb)
|
||||
|
||||
def on_editing_finished(self, elem, text):
|
||||
"""Write the editor text into the form field and clean up tempfile.
|
||||
@ -1450,10 +1495,25 @@ class CommandDispatcher:
|
||||
raise cmdexc.CommandError("Element vanished while editing!")
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher',
|
||||
deprecated="Use :insert-text {primary}",
|
||||
modes=[KeyMode.insert], hide=True, scope='window',
|
||||
needs_js=True, backend=usertypes.Backend.QtWebKit)
|
||||
def paste_primary(self):
|
||||
"""Paste the primary selection at cursor position."""
|
||||
try:
|
||||
self.insert_text(utils.get_clipboard(selection=True))
|
||||
except utils.SelectionUnsupportedError:
|
||||
self.insert_text(utils.get_clipboard())
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', maxsplit=0,
|
||||
scope='window', needs_js=True,
|
||||
backend=usertypes.Backend.QtWebKit)
|
||||
def insert_text(self, text):
|
||||
"""Insert text at cursor position.
|
||||
|
||||
Args:
|
||||
text: The text to insert.
|
||||
"""
|
||||
# FIXME:qtwebengine have a proper API for this
|
||||
tab = self._current_widget()
|
||||
page = tab._widget.page() # pylint: disable=protected-access
|
||||
@ -1463,20 +1523,57 @@ class CommandDispatcher:
|
||||
raise cmdexc.CommandError("No element focused!")
|
||||
if not elem.is_editable(strict=True):
|
||||
raise cmdexc.CommandError("Focused element is not editable!")
|
||||
|
||||
try:
|
||||
sel = utils.get_clipboard(selection=True)
|
||||
except utils.SelectionUnsupportedError:
|
||||
sel = utils.get_clipboard()
|
||||
|
||||
log.misc.debug("Pasting primary selection into element {}".format(
|
||||
log.misc.debug("Inserting text into element {}".format(
|
||||
elem.debug_text()))
|
||||
elem.run_js_async("""
|
||||
var sel = '{}';
|
||||
var text = '{}';
|
||||
var event = document.createEvent('TextEvent');
|
||||
event.initTextEvent('textInput', true, true, null, sel);
|
||||
event.initTextEvent('textInput', true, true, null, text);
|
||||
this.dispatchEvent(event);
|
||||
""".format(javascript.string_escape(sel)))
|
||||
""".format(javascript.string_escape(text)))
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window',
|
||||
hide=True)
|
||||
@cmdutils.argument('filter_', choices=['id'])
|
||||
def click_element(self, filter_: str, value, *,
|
||||
target: usertypes.ClickTarget=
|
||||
usertypes.ClickTarget.normal):
|
||||
"""Click the element matching the given filter.
|
||||
|
||||
The given filter needs to result in exactly one element, otherwise, an
|
||||
error is shown.
|
||||
|
||||
Args:
|
||||
filter_: How to filter the elements.
|
||||
id: Get an element based on its ID.
|
||||
value: The value to filter for.
|
||||
target: How to open the clicked element (normal/tab/tab-bg/window).
|
||||
"""
|
||||
tab = self._current_widget()
|
||||
|
||||
def single_cb(elem):
|
||||
"""Click a single element."""
|
||||
if elem is None:
|
||||
message.error(self._win_id, "No element found!")
|
||||
return
|
||||
elem.click(target)
|
||||
|
||||
# def multiple_cb(elems):
|
||||
# """Click multiple elements (with only one expected)."""
|
||||
# if not elems:
|
||||
# message.error(self._win_id, "No element found!")
|
||||
# return
|
||||
# elif len(elems) != 1:
|
||||
# message.error(self._win_id, "{} elements found!".format(
|
||||
# len(elems)))
|
||||
# return
|
||||
# elems[0].click(target)
|
||||
|
||||
handlers = {
|
||||
'id': (tab.elements.find_id, single_cb),
|
||||
}
|
||||
handler, callback = handlers[filter_]
|
||||
handler(value, callback)
|
||||
|
||||
def _search_cb(self, found, *, tab, old_scroll_pos, options, text, prev):
|
||||
"""Callback called from search/search_next/search_prev.
|
||||
@ -1853,20 +1950,20 @@ class CommandDispatcher:
|
||||
keyinfo.modifiers, keyinfo.text)
|
||||
|
||||
if global_:
|
||||
receiver = QApplication.focusWindow()
|
||||
if receiver is None:
|
||||
window = QApplication.focusWindow()
|
||||
if window is None:
|
||||
raise cmdexc.CommandError("No focused window!")
|
||||
QApplication.sendEvent(window, press_event)
|
||||
QApplication.sendEvent(window, release_event)
|
||||
else:
|
||||
try:
|
||||
tab = objreg.get('tab', scope='tab', tab='current')
|
||||
except objreg.RegistryUnavailableError:
|
||||
raise cmdexc.CommandError("No focused webview!")
|
||||
# pylint: disable=protected-access
|
||||
receiver = tab._widget
|
||||
# pylint: enable=protected-access
|
||||
|
||||
QApplication.postEvent(receiver, press_event)
|
||||
QApplication.postEvent(receiver, release_event)
|
||||
tab = self._current_widget()
|
||||
tab.send_event(press_event)
|
||||
tab.send_event(release_event)
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window',
|
||||
debug=True)
|
||||
|
@ -23,23 +23,19 @@ import collections
|
||||
import functools
|
||||
import math
|
||||
import re
|
||||
import html
|
||||
from string import ascii_lowercase
|
||||
|
||||
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QObject, QEvent, Qt, QUrl,
|
||||
QTimer)
|
||||
from PyQt5.QtGui import QMouseEvent
|
||||
from PyQt5.QtWebKitWidgets import QWebPage
|
||||
from PyQt5.QtCore import pyqtSlot, QObject, Qt, QUrl
|
||||
from PyQt5.QtWidgets import QLabel
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.config import config, style
|
||||
from qutebrowser.keyinput import modeman, modeparsers
|
||||
from qutebrowser.browser import webelem
|
||||
from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners
|
||||
from qutebrowser.utils import usertypes, log, qtutils, message, objreg, utils
|
||||
|
||||
|
||||
ElemTuple = collections.namedtuple('ElemTuple', ['elem', 'label'])
|
||||
|
||||
|
||||
Target = usertypes.enum('Target', ['normal', 'current', 'tab', 'tab_fg',
|
||||
'tab_bg', 'window', 'yank', 'yank_primary',
|
||||
'run', 'fill', 'hover', 'download',
|
||||
@ -57,15 +53,91 @@ def on_mode_entered(mode, win_id):
|
||||
modeman.maybe_leave(win_id, usertypes.KeyMode.hint, 'insert mode')
|
||||
|
||||
|
||||
class HintLabel(QLabel):
|
||||
|
||||
"""A label for a link.
|
||||
|
||||
Attributes:
|
||||
elem: The element this label belongs to.
|
||||
_context: The current hinting context.
|
||||
"""
|
||||
|
||||
STYLESHEET = """
|
||||
QLabel {
|
||||
background-color: {{ color['hints.bg'] }};
|
||||
color: {{ color['hints.fg'] }};
|
||||
font: {{ font['hints'] }};
|
||||
border: {{ config.get('hints', 'border') }};
|
||||
padding-left: -3px;
|
||||
padding-right: -3px;
|
||||
}
|
||||
"""
|
||||
|
||||
def __init__(self, elem, context):
|
||||
super().__init__(parent=context.tab)
|
||||
self._context = context
|
||||
self.elem = elem
|
||||
|
||||
self.setAttribute(Qt.WA_StyledBackground, True)
|
||||
style.set_register_stylesheet(self)
|
||||
|
||||
self._context.tab.contents_size_changed.connect(self._move_to_elem)
|
||||
self._move_to_elem()
|
||||
self.show()
|
||||
|
||||
def __repr__(self):
|
||||
try:
|
||||
text = self.text()
|
||||
except RuntimeError:
|
||||
text = '<deleted>'
|
||||
return utils.get_repr(self, elem=self.elem, text=text)
|
||||
|
||||
def update_text(self, matched, unmatched):
|
||||
"""Set the text for the hint.
|
||||
|
||||
Args:
|
||||
matched: The part of the text which was typed.
|
||||
unmatched: The part of the text which was not typed yet.
|
||||
"""
|
||||
if (config.get('hints', 'uppercase') and
|
||||
self._context.hint_mode == 'letter'):
|
||||
matched = html.escape(matched.upper())
|
||||
unmatched = html.escape(unmatched.upper())
|
||||
else:
|
||||
matched = html.escape(matched)
|
||||
unmatched = html.escape(unmatched)
|
||||
|
||||
match_color = html.escape(config.get('colors', 'hints.fg.match'))
|
||||
self.setText('<font color="{}">{}</font>{}'.format(
|
||||
match_color, matched, unmatched))
|
||||
self.adjustSize()
|
||||
|
||||
@pyqtSlot()
|
||||
def _move_to_elem(self):
|
||||
"""Reposition the label to its element."""
|
||||
if not self.elem.has_frame():
|
||||
# This sometimes happens for some reason...
|
||||
log.hints.debug("Frame for {!r} vanished!".format(self))
|
||||
self.hide()
|
||||
return
|
||||
no_js = config.get('hints', 'find-implementation') != 'javascript'
|
||||
rect = self.elem.rect_on_view(no_js=no_js)
|
||||
self.move(rect.x(), rect.y())
|
||||
|
||||
def cleanup(self):
|
||||
"""Clean up this element and hide it."""
|
||||
self.hide()
|
||||
self.deleteLater()
|
||||
|
||||
|
||||
class HintContext:
|
||||
|
||||
"""Context namespace used for hinting.
|
||||
|
||||
Attributes:
|
||||
frames: The QWebFrames to use.
|
||||
all_elems: A list of all (elem, label) namedtuples ever created.
|
||||
elems: A mapping from key strings to (elem, label) namedtuples.
|
||||
May contain less elements than `all_elems` due to filtering.
|
||||
all_labels: A list of all HintLabel objects ever created.
|
||||
labels: A mapping from key strings to HintLabel objects.
|
||||
May contain less elements than `all_labels` due to filtering.
|
||||
baseurl: The URL of the current page.
|
||||
target: What to do with the opened links.
|
||||
normal/current/tab/tab_fg/tab_bg/window: Get passed to
|
||||
@ -79,21 +151,23 @@ class HintContext:
|
||||
to_follow: The link to follow when enter is pressed.
|
||||
args: Custom arguments for userscript/spawn
|
||||
rapid: Whether to do rapid hinting.
|
||||
filterstr: Used to save the filter string for restoring in rapid mode.
|
||||
tab: The WebTab object we started hinting in.
|
||||
group: The group of web elements to hint.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.all_elems = []
|
||||
self.elems = {}
|
||||
self.all_labels = []
|
||||
self.labels = {}
|
||||
self.target = None
|
||||
self.baseurl = None
|
||||
self.to_follow = None
|
||||
self.rapid = False
|
||||
self.frames = []
|
||||
self.filterstr = None
|
||||
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."""
|
||||
@ -104,24 +178,11 @@ class HintContext:
|
||||
return args
|
||||
|
||||
|
||||
class HintActions(QObject):
|
||||
class HintActions:
|
||||
|
||||
"""Actions which can be done after selecting a hint.
|
||||
"""Actions which can be done after selecting a hint."""
|
||||
|
||||
Signals:
|
||||
mouse_event: Mouse event to be posted in the web view.
|
||||
arg: A QMouseEvent
|
||||
start_hinting: Emitted when hinting starts, before a link is clicked.
|
||||
arg: The ClickTarget to use.
|
||||
stop_hinting: Emitted after a link was clicked.
|
||||
"""
|
||||
|
||||
mouse_event = pyqtSignal('QMouseEvent')
|
||||
start_hinting = pyqtSignal(usertypes.ClickTarget)
|
||||
stop_hinting = pyqtSignal()
|
||||
|
||||
def __init__(self, win_id, parent=None):
|
||||
super().__init__(parent)
|
||||
def __init__(self, win_id):
|
||||
self._win_id = win_id
|
||||
|
||||
def click(self, elem, context):
|
||||
@ -144,54 +205,19 @@ class HintActions(QObject):
|
||||
else:
|
||||
target_mapping[Target.tab] = usertypes.ClickTarget.tab
|
||||
|
||||
# Click the center of the largest square fitting into the top/left
|
||||
# corner of the rectangle, this will help if part of the <a> element
|
||||
# is hidden behind other elements
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/1005
|
||||
rect = elem.rect_on_view()
|
||||
if rect.width() > rect.height():
|
||||
rect.setWidth(rect.height())
|
||||
else:
|
||||
rect.setHeight(rect.width())
|
||||
pos = rect.center()
|
||||
|
||||
action = "Hovering" if context.target == Target.hover else "Clicking"
|
||||
log.hints.debug("{} on '{}' at position {}".format(
|
||||
action, elem.debug_text(), pos))
|
||||
|
||||
self.start_hinting.emit(target_mapping[context.target])
|
||||
if context.target in [Target.tab, Target.tab_fg, Target.tab_bg,
|
||||
Target.window]:
|
||||
modifiers = Qt.ControlModifier
|
||||
else:
|
||||
modifiers = Qt.NoModifier
|
||||
events = [
|
||||
QMouseEvent(QEvent.MouseMove, pos, Qt.NoButton, Qt.NoButton,
|
||||
Qt.NoModifier),
|
||||
]
|
||||
if context.target != Target.hover:
|
||||
events += [
|
||||
QMouseEvent(QEvent.MouseButtonPress, pos, Qt.LeftButton,
|
||||
Qt.LeftButton, modifiers),
|
||||
QMouseEvent(QEvent.MouseButtonRelease, pos, Qt.LeftButton,
|
||||
Qt.NoButton, modifiers),
|
||||
]
|
||||
|
||||
if context.target in [Target.normal, Target.current]:
|
||||
# Set the pre-jump mark ', so we can jump back here after following
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=self._win_id)
|
||||
tabbed_browser.set_mark("'")
|
||||
|
||||
if context.target == Target.current:
|
||||
if context.target == Target.hover:
|
||||
elem.hover()
|
||||
elif context.target == Target.current:
|
||||
elem.remove_blank_target()
|
||||
for evt in events:
|
||||
self.mouse_event.emit(evt)
|
||||
if elem.is_text_input() and elem.is_editable():
|
||||
QTimer.singleShot(0, functools.partial(
|
||||
elem.frame().page().triggerAction,
|
||||
QWebPage.MoveToEndOfDocument))
|
||||
QTimer.singleShot(0, self.stop_hinting.emit)
|
||||
elem.click(target_mapping[context.target])
|
||||
else:
|
||||
elem.click(target_mapping[context.target])
|
||||
|
||||
def yank(self, url, context):
|
||||
"""Yank an element to the clipboard or primary selection.
|
||||
@ -234,11 +260,8 @@ class HintActions(QObject):
|
||||
args = context.get_args(urlstr)
|
||||
text = ' '.join(args)
|
||||
if text[0] not in modeparsers.STARTCHARS:
|
||||
message.error(self._win_id,
|
||||
"Invalid command text '{}'.".format(text),
|
||||
immediately=True)
|
||||
else:
|
||||
message.set_cmd_text(self._win_id, text)
|
||||
raise HintingError("Invalid command text '{}'.".format(text))
|
||||
message.set_cmd_text(self._win_id, text)
|
||||
|
||||
def download(self, elem, context):
|
||||
"""Download a hint URL.
|
||||
@ -249,16 +272,20 @@ class HintActions(QObject):
|
||||
"""
|
||||
url = elem.resolve_url(context.baseurl)
|
||||
if url is None:
|
||||
raise HintingError
|
||||
raise HintingError("No suitable link found for this element.")
|
||||
if context.rapid:
|
||||
prompt = False
|
||||
else:
|
||||
prompt = None
|
||||
|
||||
# FIXME:qtwebengine get a proper API for this
|
||||
# pylint: disable=protected-access
|
||||
page = elem._elem.webFrame().page()
|
||||
# pylint: enable=protected-access
|
||||
|
||||
download_manager = objreg.get('download-manager', scope='window',
|
||||
window=self._win_id)
|
||||
download_manager.get(url, page=elem.frame().page(),
|
||||
prompt_download_directory=prompt)
|
||||
download_manager.get(url, page=page, prompt_download_directory=prompt)
|
||||
|
||||
def call_userscript(self, elem, context):
|
||||
"""Call a userscript from a hint.
|
||||
@ -282,7 +309,7 @@ class HintActions(QObject):
|
||||
userscripts.run_async(context.tab, cmd, *args, win_id=self._win_id,
|
||||
env=env)
|
||||
except userscripts.UnsupportedError as e:
|
||||
message.error(self._win_id, str(e), immediately=True)
|
||||
raise HintingError(str(e))
|
||||
|
||||
def spawn(self, url, context):
|
||||
"""Spawn a simple command from a hint.
|
||||
@ -308,7 +335,6 @@ class HintManager(QObject):
|
||||
_context: The HintContext for the current invocation.
|
||||
_win_id: The window ID this HintManager is associated with.
|
||||
_tab_id: The tab ID this HintManager is associated with.
|
||||
_filterstr: Used to save the filter string for restoring in rapid mode.
|
||||
|
||||
Signals:
|
||||
See HintActions
|
||||
@ -331,23 +357,15 @@ class HintManager(QObject):
|
||||
Target.spawn: "Spawn command via hint",
|
||||
}
|
||||
|
||||
mouse_event = pyqtSignal('QMouseEvent')
|
||||
start_hinting = pyqtSignal(usertypes.ClickTarget)
|
||||
stop_hinting = pyqtSignal()
|
||||
|
||||
def __init__(self, win_id, tab_id, parent=None):
|
||||
"""Constructor."""
|
||||
super().__init__(parent)
|
||||
self._win_id = win_id
|
||||
self._tab_id = tab_id
|
||||
self._context = None
|
||||
self._filterstr = None
|
||||
self._word_hinter = WordHinter()
|
||||
|
||||
self._actions = HintActions(win_id)
|
||||
self._actions.start_hinting.connect(self.start_hinting)
|
||||
self._actions.stop_hinting.connect(self.stop_hinting)
|
||||
self._actions.mouse_event.connect(self.mouse_event)
|
||||
|
||||
mode_manager = objreg.get('mode-manager', scope='window',
|
||||
window=win_id)
|
||||
@ -363,24 +381,14 @@ class HintManager(QObject):
|
||||
|
||||
def _cleanup(self):
|
||||
"""Clean up after hinting."""
|
||||
try:
|
||||
self._context.tab.contents_size_changed.disconnect(
|
||||
self.on_contents_size_changed)
|
||||
except TypeError:
|
||||
# For some reason, this can fail sometimes...
|
||||
pass
|
||||
for label in self._context.all_labels:
|
||||
label.cleanup()
|
||||
|
||||
for elem in self._context.all_elems:
|
||||
try:
|
||||
elem.label.remove_from_document()
|
||||
except webelem.Error:
|
||||
pass
|
||||
text = self._get_text()
|
||||
message_bridge = objreg.get('message-bridge', scope='window',
|
||||
window=self._win_id)
|
||||
message_bridge.maybe_reset_text(text)
|
||||
self._context = None
|
||||
self._filterstr = None
|
||||
|
||||
def _hint_strings(self, elems):
|
||||
"""Calculate the hint strings for elems.
|
||||
@ -393,7 +401,9 @@ class HintManager(QObject):
|
||||
Return:
|
||||
A list of hint strings, in the same order as the elements.
|
||||
"""
|
||||
hint_mode = config.get('hints', 'mode')
|
||||
if not elems:
|
||||
return []
|
||||
hint_mode = self._context.hint_mode
|
||||
if hint_mode == 'word':
|
||||
try:
|
||||
return self._word_hinter.hint(elems)
|
||||
@ -513,92 +523,6 @@ class HintManager(QObject):
|
||||
hintstr.insert(0, chars[0])
|
||||
return ''.join(hintstr)
|
||||
|
||||
def _is_hidden(self, elem):
|
||||
"""Check if the element is hidden via display=none."""
|
||||
display = elem.style_property('display', strategy='inline')
|
||||
return display == 'none'
|
||||
|
||||
def _show_elem(self, elem):
|
||||
"""Show a given element."""
|
||||
elem.set_style_property('display', 'inline !important')
|
||||
|
||||
def _hide_elem(self, elem):
|
||||
"""Hide a given element."""
|
||||
elem.set_style_property('display', 'none !important')
|
||||
|
||||
def _set_style_properties(self, elem, label):
|
||||
"""Set the hint CSS on the element given.
|
||||
|
||||
Args:
|
||||
elem: The QWebElement to set the style attributes for.
|
||||
label: The label QWebElement.
|
||||
"""
|
||||
attrs = [
|
||||
('display', 'inline !important'),
|
||||
('z-index', '{} !important'.format(int(2 ** 32 / 2 - 1))),
|
||||
('pointer-events', 'none !important'),
|
||||
('position', 'fixed !important'),
|
||||
('color', config.get('colors', 'hints.fg') + ' !important'),
|
||||
('background', config.get('colors', 'hints.bg') + ' !important'),
|
||||
('font', config.get('fonts', 'hints') + ' !important'),
|
||||
('border', config.get('hints', 'border') + ' !important'),
|
||||
('opacity', str(config.get('hints', 'opacity')) + ' !important'),
|
||||
]
|
||||
|
||||
# Make text uppercase if set in config
|
||||
if (config.get('hints', 'uppercase') and
|
||||
config.get('hints', 'mode') == 'letter'):
|
||||
attrs.append(('text-transform', 'uppercase !important'))
|
||||
else:
|
||||
attrs.append(('text-transform', 'none !important'))
|
||||
|
||||
for k, v in attrs:
|
||||
label.set_style_property(k, v)
|
||||
self._set_style_position(elem, label)
|
||||
|
||||
def _set_style_position(self, elem, label):
|
||||
"""Set the CSS position of the label element.
|
||||
|
||||
Args:
|
||||
elem: The QWebElement to set the style attributes for.
|
||||
label: The label QWebElement.
|
||||
"""
|
||||
no_js = config.get('hints', 'find-implementation') != 'javascript'
|
||||
rect = elem.rect_on_view(adjust_zoom=False, no_js=no_js)
|
||||
left = rect.x()
|
||||
top = rect.y()
|
||||
log.hints.vdebug("Drawing label '{!r}' at {}/{} for element '{!r}' "
|
||||
"(no_js: {})".format(label, left, top, elem, no_js))
|
||||
label.set_style_property('left', '{}px !important'.format(left))
|
||||
label.set_style_property('top', '{}px !important'.format(top))
|
||||
|
||||
def _draw_label(self, elem, string):
|
||||
"""Draw a hint label over an element.
|
||||
|
||||
Args:
|
||||
elem: The QWebElement to use.
|
||||
string: The hint string to print.
|
||||
|
||||
Return:
|
||||
The newly created label element
|
||||
"""
|
||||
doc = elem.document_element()
|
||||
body = doc.find_first('body')
|
||||
if body is None:
|
||||
parent = doc
|
||||
else:
|
||||
parent = body
|
||||
label = parent.create_inside('span')
|
||||
label['class'] = 'qutehint'
|
||||
self._set_style_properties(elem, label)
|
||||
label.set_text(string)
|
||||
return label
|
||||
|
||||
def _show_url_error(self):
|
||||
"""Show an error because no link was found."""
|
||||
message.error(self._win_id, "No suitable link found for this element.",
|
||||
immediately=True)
|
||||
|
||||
def _check_args(self, target, *args):
|
||||
"""Check the arguments passed to start() and raise if they're wrong.
|
||||
|
||||
@ -629,45 +553,57 @@ class HintManager(QObject):
|
||||
# Do multi-word matching
|
||||
return all(word in elemstr for word in filterstr.split())
|
||||
|
||||
def _filter_matches_exactly(self, filterstr, elemstr):
|
||||
"""Return True if `filterstr` exactly matches `elemstr`."""
|
||||
# Empty string and None never match
|
||||
if not filterstr:
|
||||
return False
|
||||
filterstr = filterstr.casefold()
|
||||
elemstr = elemstr.casefold()
|
||||
return filterstr == elemstr
|
||||
|
||||
def _start_cb(self, elems):
|
||||
"""Initialize the elements and labels based on the context set."""
|
||||
filterfunc = webelem.FILTERS.get(self._context.group, lambda e: True)
|
||||
elems = [e for e in elems if filterfunc(e)]
|
||||
if not elems:
|
||||
raise cmdexc.CommandError("No elements found.")
|
||||
message.error(self._win_id, "No elements found.", immediately=True)
|
||||
return
|
||||
strings = self._hint_strings(elems)
|
||||
log.hints.debug("hints: {}".format(', '.join(strings)))
|
||||
for e, string in zip(elems, strings):
|
||||
label = self._draw_label(e, string)
|
||||
elem = ElemTuple(e, label)
|
||||
self._context.all_elems.append(elem)
|
||||
self._context.elems[string] = elem
|
||||
|
||||
for elem, string in zip(elems, strings):
|
||||
label = HintLabel(elem, self._context)
|
||||
label.update_text('', string)
|
||||
self._context.all_labels.append(label)
|
||||
self._context.labels[string] = label
|
||||
|
||||
keyparsers = objreg.get('keyparsers', scope='window',
|
||||
window=self._win_id)
|
||||
keyparser = keyparsers[usertypes.KeyMode.hint]
|
||||
keyparser.update_bindings(strings)
|
||||
|
||||
self._context.tab.contents_size_changed.connect(
|
||||
self.on_contents_size_changed)
|
||||
message_bridge = objreg.get('message-bridge', scope='window',
|
||||
window=self._win_id)
|
||||
message_bridge.set_text(self._get_text())
|
||||
modeman.enter(self._win_id, usertypes.KeyMode.hint,
|
||||
'HintManager.start')
|
||||
|
||||
# to make auto-follow == 'always' work
|
||||
self._handle_auto_follow()
|
||||
|
||||
@cmdutils.register(instance='hintmanager', scope='tab', name='hint',
|
||||
star_args_optional=True, maxsplit=2,
|
||||
backend=usertypes.Backend.QtWebKit)
|
||||
star_args_optional=True, maxsplit=2)
|
||||
@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.
|
||||
@ -694,6 +630,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.
|
||||
@ -714,6 +657,12 @@ class HintManager(QObject):
|
||||
tab = tabbed_browser.currentWidget()
|
||||
if tab is None:
|
||||
raise cmdexc.CommandError("No WebView available yet!")
|
||||
if (tab.backend == usertypes.Backend.QtWebEngine and
|
||||
target == Target.download):
|
||||
message.error(self._win_id, "The download target is not available "
|
||||
"yet with QtWebEngine.", immediately=True)
|
||||
return
|
||||
|
||||
mode_manager = objreg.get('mode-manager', scope='window',
|
||||
window=self._win_id)
|
||||
if mode_manager.mode == usertypes.KeyMode.hint:
|
||||
@ -732,93 +681,88 @@ 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:
|
||||
raise cmdexc.CommandError("No URL set for this page yet!")
|
||||
self._context.tab = tab
|
||||
self._context.args = args
|
||||
self._context.group = group
|
||||
selector = webelem.SELECTORS[self._context.group]
|
||||
self._context.tab.find_all_elements(selector, self._start_cb,
|
||||
self._context.tab.elements.find_css(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_auto_follow(self, keystr="", filterstr="", visible=None):
|
||||
"""Handle the auto-follow option."""
|
||||
if visible is None:
|
||||
visible = {string: label
|
||||
for string, label in self._context.labels.items()
|
||||
if label.isVisible()}
|
||||
|
||||
if len(visible) != 1:
|
||||
return
|
||||
|
||||
auto_follow = config.get('hints', 'auto-follow')
|
||||
|
||||
if auto_follow == "always":
|
||||
follow = True
|
||||
elif auto_follow == "unique-match":
|
||||
follow = keystr or filterstr
|
||||
elif auto_follow == "full-match":
|
||||
elemstr = str(list(visible.values())[0].elem)
|
||||
filter_match = self._filter_matches_exactly(filterstr, elemstr)
|
||||
follow = (keystr in visible) or filter_match
|
||||
else:
|
||||
follow = False
|
||||
# save the keystr of the only one visible hint to be picked up
|
||||
# later by self.follow_hint
|
||||
self._context.to_follow = list(visible.keys())[0]
|
||||
|
||||
if follow:
|
||||
# apply auto-follow-timeout
|
||||
timeout = config.get('hints', 'auto-follow-timeout')
|
||||
keyparsers = objreg.get('keyparsers', scope='window',
|
||||
window=self._win_id)
|
||||
normal_parser = keyparsers[usertypes.KeyMode.normal]
|
||||
normal_parser.set_inhibited_timeout(timeout)
|
||||
# unpacking gets us the first (and only) key in the dict.
|
||||
self._fire(*visible)
|
||||
|
||||
def handle_partial_key(self, keystr):
|
||||
"""Handle a new partial keypress."""
|
||||
log.hints.debug("Handling new keystring: '{}'".format(keystr))
|
||||
for string, elem in self._context.elems.items():
|
||||
for string, label in self._context.labels.items():
|
||||
try:
|
||||
if string.startswith(keystr):
|
||||
matched = string[:len(keystr)]
|
||||
rest = string[len(keystr):]
|
||||
match_color = config.get('colors', 'hints.fg.match')
|
||||
elem.label.set_inner_xml(
|
||||
'<font color="{}">{}</font>{}'.format(
|
||||
match_color, matched, rest))
|
||||
if self._is_hidden(elem.label):
|
||||
# hidden element which matches again -> show it
|
||||
self._show_elem(elem.label)
|
||||
label.update_text(matched, rest)
|
||||
# Show label again if it was hidden before
|
||||
label.show()
|
||||
else:
|
||||
# element doesn't match anymore -> hide it
|
||||
self._hide_elem(elem.label)
|
||||
# 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')):
|
||||
label.hide()
|
||||
except webelem.Error:
|
||||
pass
|
||||
|
||||
def _filter_number_hints(self):
|
||||
"""Apply filters for numbered hints and renumber them.
|
||||
|
||||
Return:
|
||||
Elements which are still visible
|
||||
"""
|
||||
# renumber filtered hints
|
||||
elems = []
|
||||
for e in self._context.all_elems:
|
||||
try:
|
||||
if not self._is_hidden(e.label):
|
||||
elems.append(e)
|
||||
except webelem.Error:
|
||||
pass
|
||||
if not elems:
|
||||
# Whoops, filtered all hints
|
||||
modeman.leave(self._win_id, usertypes.KeyMode.hint,
|
||||
'all filtered')
|
||||
return {}
|
||||
|
||||
strings = self._hint_strings(elems)
|
||||
self._context.elems = {}
|
||||
for elem, string in zip(elems, strings):
|
||||
elem.label.set_inner_xml(string)
|
||||
self._context.elems[string] = elem
|
||||
keyparsers = objreg.get('keyparsers', scope='window',
|
||||
window=self._win_id)
|
||||
keyparser = keyparsers[usertypes.KeyMode.hint]
|
||||
keyparser.update_bindings(strings, preserve_filter=True)
|
||||
|
||||
return self._context.elems
|
||||
|
||||
def _filter_non_number_hints(self):
|
||||
"""Apply filters for letter/word hints.
|
||||
|
||||
Return:
|
||||
Elements which are still visible
|
||||
"""
|
||||
visible = {}
|
||||
for string, elem in self._context.elems.items():
|
||||
try:
|
||||
if not self._is_hidden(elem.label):
|
||||
visible[string] = elem
|
||||
except webelem.Error:
|
||||
pass
|
||||
if not visible:
|
||||
# Whoops, filtered all hints
|
||||
modeman.leave(self._win_id, usertypes.KeyMode.hint,
|
||||
'all filtered')
|
||||
return visible
|
||||
self._handle_auto_follow(keystr=keystr)
|
||||
|
||||
def filter_hints(self, filterstr):
|
||||
"""Filter displayed hints according to a text.
|
||||
@ -830,51 +774,54 @@ class HintManager(QObject):
|
||||
and `self._filterstr` are None, all hints are shown.
|
||||
"""
|
||||
if filterstr is None:
|
||||
filterstr = self._filterstr
|
||||
filterstr = self._context.filterstr
|
||||
else:
|
||||
self._filterstr = filterstr
|
||||
self._context.filterstr = filterstr
|
||||
|
||||
for elem in self._context.all_elems:
|
||||
visible = []
|
||||
for label in self._context.all_labels:
|
||||
try:
|
||||
if self._filter_matches(filterstr, str(elem.elem)):
|
||||
if self._is_hidden(elem.label):
|
||||
# hidden element which matches again -> show it
|
||||
self._show_elem(elem.label)
|
||||
if self._filter_matches(filterstr, str(label.elem)):
|
||||
visible.append(label)
|
||||
# Show label again if it was hidden before
|
||||
label.show()
|
||||
else:
|
||||
# element doesn't match anymore -> hide it
|
||||
self._hide_elem(elem.label)
|
||||
label.hide()
|
||||
except webelem.Error:
|
||||
pass
|
||||
|
||||
if config.get('hints', 'mode') == 'number':
|
||||
visible = self._filter_number_hints()
|
||||
else:
|
||||
visible = self._filter_non_number_hints()
|
||||
if not visible:
|
||||
# Whoops, filtered all hints
|
||||
modeman.leave(self._win_id, usertypes.KeyMode.hint,
|
||||
'all filtered')
|
||||
return
|
||||
|
||||
if (len(visible) == 1 and
|
||||
config.get('hints', 'auto-follow') and
|
||||
filterstr is not None):
|
||||
# apply auto-follow-timeout
|
||||
timeout = config.get('hints', 'auto-follow-timeout')
|
||||
if self._context.hint_mode == 'number':
|
||||
# renumber filtered hints
|
||||
strings = self._hint_strings(visible)
|
||||
self._context.labels = {}
|
||||
for label, string in zip(visible, strings):
|
||||
label.update_text('', string)
|
||||
self._context.labels[string] = label
|
||||
keyparsers = objreg.get('keyparsers', scope='window',
|
||||
window=self._win_id)
|
||||
normal_parser = keyparsers[usertypes.KeyMode.normal]
|
||||
normal_parser.set_inhibited_timeout(timeout)
|
||||
# unpacking gets us the first (and only) key in the dict.
|
||||
self.fire(*visible)
|
||||
keyparser = keyparsers[usertypes.KeyMode.hint]
|
||||
keyparser.update_bindings(strings, preserve_filter=True)
|
||||
|
||||
def fire(self, keystr, force=False):
|
||||
# Note: filter_hints can be called with non-None filterstr only
|
||||
# when number mode is active
|
||||
if filterstr is not None:
|
||||
# pass self._context.labels as the dict of visible hints
|
||||
self._handle_auto_follow(filterstr=filterstr,
|
||||
visible=self._context.labels)
|
||||
|
||||
def _fire(self, keystr):
|
||||
"""Fire a completed hint.
|
||||
|
||||
Args:
|
||||
keystr: The keychain string to follow.
|
||||
force: When True, follow even when auto-follow is false.
|
||||
"""
|
||||
if not (force or config.get('hints', 'auto-follow')):
|
||||
self.handle_partial_key(keystr)
|
||||
self._context.to_follow = keystr
|
||||
return
|
||||
|
||||
# Handlers which take a QWebElement
|
||||
elem_handlers = {
|
||||
Target.normal: self._actions.click,
|
||||
@ -896,9 +843,9 @@ class HintManager(QObject):
|
||||
Target.fill: self._actions.preset_cmd_text,
|
||||
Target.spawn: self._actions.spawn,
|
||||
}
|
||||
elem = self._context.elems[keystr].elem
|
||||
elem = self._context.labels[keystr].elem
|
||||
|
||||
if elem.frame() is None:
|
||||
if not elem.has_frame():
|
||||
message.error(self._win_id,
|
||||
"This element has no webframe.",
|
||||
immediately=True)
|
||||
@ -910,7 +857,9 @@ class HintManager(QObject):
|
||||
elif self._context.target in url_handlers:
|
||||
url = elem.resolve_url(self._context.baseurl)
|
||||
if url is None:
|
||||
self._show_url_error()
|
||||
message.error(self._win_id,
|
||||
"No suitable link found for this element.",
|
||||
immediately=True)
|
||||
return
|
||||
handler = functools.partial(url_handlers[self._context.target],
|
||||
url, self._context)
|
||||
@ -924,13 +873,13 @@ class HintManager(QObject):
|
||||
# Reset filtering
|
||||
self.filter_hints(None)
|
||||
# Undo keystring highlighting
|
||||
for string, elem in self._context.elems.items():
|
||||
elem.label.set_inner_xml(string)
|
||||
for string, label in self._context.labels.items():
|
||||
label.update_text('', string)
|
||||
|
||||
try:
|
||||
handler()
|
||||
except HintingError:
|
||||
self._show_url_error()
|
||||
except HintingError as e:
|
||||
message.error(self._win_id, str(e), immediately=True)
|
||||
|
||||
@cmdutils.register(instance='hintmanager', scope='tab', hide=True,
|
||||
modes=[usertypes.KeyMode.hint])
|
||||
@ -945,23 +894,9 @@ class HintManager(QObject):
|
||||
raise cmdexc.CommandError("No hint to follow")
|
||||
else:
|
||||
keystring = self._context.to_follow
|
||||
elif keystring not in self._context.elems:
|
||||
elif keystring not in self._context.labels:
|
||||
raise cmdexc.CommandError("No hint {}!".format(keystring))
|
||||
self.fire(keystring, force=True)
|
||||
|
||||
@pyqtSlot()
|
||||
def on_contents_size_changed(self):
|
||||
"""Reposition hints if contents size changed."""
|
||||
log.hints.debug("Contents size changed...!")
|
||||
for e in self._context.all_elems:
|
||||
try:
|
||||
if e.elem.frame() is None:
|
||||
# This sometimes happens for some reason...
|
||||
e.label.remove_from_document()
|
||||
continue
|
||||
self._set_style_position(e.elem, e.label)
|
||||
except webelem.Error:
|
||||
pass
|
||||
self._fire(keystring)
|
||||
|
||||
@pyqtSlot(usertypes.KeyMode)
|
||||
def on_mode_left(self, mode):
|
||||
@ -1021,6 +956,7 @@ 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,
|
||||
@ -1029,7 +965,9 @@ class WordHinter:
|
||||
extractable_attrs = collections.defaultdict(list, {
|
||||
"img": ["alt", "title", "src"],
|
||||
"a": ["title", "href", "text"],
|
||||
"input": ["name"]
|
||||
"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."""
|
||||
|
||||
|
227
qutebrowser/browser/mouse.py
Normal file
227
qutebrowser/browser/mouse.py
Normal file
@ -0,0 +1,227 @@
|
||||
# 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, usertypes
|
||||
from qutebrowser.keyinput import modeman
|
||||
|
||||
|
||||
from PyQt5.QtCore import QObject, QEvent, Qt, QTimer
|
||||
|
||||
|
||||
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
|
||||
|
||||
Attributes:
|
||||
_filter: The event filter to install.
|
||||
_widget: The widget expected to send out childEvents.
|
||||
"""
|
||||
|
||||
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.
|
||||
|
||||
Attributes:
|
||||
_tab: The browsertab object this filter is installed on.
|
||||
_handlers: A dict of handler functions for the handled events.
|
||||
_ignore_wheel_event: Whether to ignore the next wheelEvent.
|
||||
_check_insertmode_on_release: Whether an insertmode check should be
|
||||
done when the mouse is released.
|
||||
"""
|
||||
|
||||
def __init__(self, tab, parent=None):
|
||||
super().__init__(parent)
|
||||
self._tab = tab
|
||||
self._handlers = {
|
||||
QEvent.MouseButtonPress: self._handle_mouse_press,
|
||||
QEvent.MouseButtonRelease: self._handle_mouse_release,
|
||||
QEvent.Wheel: self._handle_wheel,
|
||||
QEvent.ContextMenu: self._handle_context_menu,
|
||||
}
|
||||
self._ignore_wheel_event = False
|
||||
self._check_insertmode_on_release = False
|
||||
|
||||
def _handle_mouse_press(self, 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
|
||||
|
||||
self._ignore_wheel_event = True
|
||||
self._mousepress_opentarget(e)
|
||||
self._tab.elements.find_at_pos(e.pos(), self._mousepress_insertmode_cb)
|
||||
|
||||
return False
|
||||
|
||||
def _handle_mouse_release(self, _e):
|
||||
"""Handle releasing of a mouse button."""
|
||||
# We want to make sure we check the focus element after the WebView is
|
||||
# updated completely.
|
||||
QTimer.singleShot(0, self._mouserelease_insertmode)
|
||||
return False
|
||||
|
||||
def _handle_wheel(self, e):
|
||||
"""Zoom on Ctrl-Mousewheel.
|
||||
|
||||
Args:
|
||||
e: The QWheelEvent.
|
||||
"""
|
||||
if self._ignore_wheel_event:
|
||||
# See https://github.com/The-Compiler/qutebrowser/issues/395
|
||||
self._ignore_wheel_event = False
|
||||
return True
|
||||
|
||||
if e.modifiers() & Qt.ControlModifier:
|
||||
divider = config.get('input', 'mouse-zoom-divider')
|
||||
factor = self._tab.zoom.factor() + (e.angleDelta().y() / divider)
|
||||
if factor < 0:
|
||||
return False
|
||||
perc = int(100 * factor)
|
||||
message.info(self._tab.win_id, "Zoom level: {}%".format(perc))
|
||||
self._tab.zoom.set_factor(factor)
|
||||
|
||||
return False
|
||||
|
||||
def _handle_context_menu(self, _e):
|
||||
"""Suppress context menus if rocker gestures are turned on."""
|
||||
return config.get('input', 'rocker-gestures')
|
||||
|
||||
def _mousepress_insertmode_cb(self, elem):
|
||||
"""Check if the clicked element is editable."""
|
||||
if elem is None:
|
||||
# Something didn't work out, let's find the focus element after
|
||||
# a mouse release.
|
||||
log.mouse.debug("Got None element, scheduling check on "
|
||||
"mouse release")
|
||||
self._check_insertmode_on_release = True
|
||||
return
|
||||
|
||||
if elem.is_editable():
|
||||
log.mouse.debug("Clicked editable element!")
|
||||
modeman.enter(self._tab.win_id, usertypes.KeyMode.insert,
|
||||
'click', only_if_normal=True)
|
||||
else:
|
||||
log.mouse.debug("Clicked non-editable element!")
|
||||
if config.get('input', 'auto-leave-insert-mode'):
|
||||
modeman.maybe_leave(self._tab.win_id,
|
||||
usertypes.KeyMode.insert,
|
||||
'click')
|
||||
|
||||
def _mouserelease_insertmode(self):
|
||||
"""If we have an insertmode check scheduled, handle it."""
|
||||
if not self._check_insertmode_on_release:
|
||||
return
|
||||
self._check_insertmode_on_release = False
|
||||
|
||||
def mouserelease_insertmode_cb(elem):
|
||||
"""Callback which gets called from JS."""
|
||||
if elem is None:
|
||||
log.mouse.debug("Element vanished!")
|
||||
return
|
||||
|
||||
if elem.is_editable():
|
||||
log.mouse.debug("Clicked editable element (delayed)!")
|
||||
modeman.enter(self._tab.win_id, usertypes.KeyMode.insert,
|
||||
'click-delayed', only_if_normal=True)
|
||||
else:
|
||||
log.mouse.debug("Clicked non-editable element (delayed)!")
|
||||
if config.get('input', 'auto-leave-insert-mode'):
|
||||
modeman.maybe_leave(self._tab.win_id,
|
||||
usertypes.KeyMode.insert,
|
||||
'click-delayed')
|
||||
|
||||
self._tab.elements.find_focused(mouserelease_insertmode_cb)
|
||||
|
||||
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 _mousepress_opentarget(self, e):
|
||||
"""Set the open target when something was clicked.
|
||||
|
||||
Args:
|
||||
e: The QMouseEvent.
|
||||
"""
|
||||
if e.button() == Qt.MidButton or e.modifiers() & Qt.ControlModifier:
|
||||
background_tabs = config.get('tabs', 'background-tabs')
|
||||
if e.modifiers() & Qt.ShiftModifier:
|
||||
background_tabs = not background_tabs
|
||||
if background_tabs:
|
||||
target = usertypes.ClickTarget.tab_bg
|
||||
else:
|
||||
target = usertypes.ClickTarget.tab
|
||||
self._tab.data.open_target = target
|
||||
log.mouse.debug("Ctrl/Middle click, setting target: {}".format(
|
||||
target))
|
||||
else:
|
||||
self._tab.data.open_target = usertypes.ClickTarget.normal
|
||||
log.mouse.debug("Normal click, setting normal target")
|
||||
|
||||
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](event)
|
@ -17,7 +17,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Implementation of :navigate"""
|
||||
"""Implementation of :navigate."""
|
||||
|
||||
import posixpath
|
||||
|
||||
@ -31,11 +31,12 @@ class Error(Exception):
|
||||
"""Raised when the navigation can't be done."""
|
||||
|
||||
|
||||
def incdec(url, inc_or_dec):
|
||||
def incdec(url, count, inc_or_dec):
|
||||
"""Helper method for :navigate when `where' is increment/decrement.
|
||||
|
||||
Args:
|
||||
url: The current url.
|
||||
count: How much to increment or decrement by.
|
||||
inc_or_dec: Either 'increment' or 'decrement'.
|
||||
tab: Whether to open the link in a new tab.
|
||||
background: Open the link in a new background tab.
|
||||
@ -43,23 +44,26 @@ def incdec(url, inc_or_dec):
|
||||
"""
|
||||
segments = set(config.get('general', 'url-incdec-segments'))
|
||||
try:
|
||||
new_url = urlutils.incdec_number(url, inc_or_dec, segments=segments)
|
||||
new_url = urlutils.incdec_number(url, inc_or_dec, count,
|
||||
segments=segments)
|
||||
except urlutils.IncDecError as error:
|
||||
raise Error(error.msg)
|
||||
return new_url
|
||||
|
||||
|
||||
def path_up(url):
|
||||
def path_up(url, count):
|
||||
"""Helper method for :navigate when `where' is up.
|
||||
|
||||
Args:
|
||||
url: The current url.
|
||||
count: The number of levels to go up in the url.
|
||||
"""
|
||||
path = url.path()
|
||||
if not path or path == '/':
|
||||
raise Error("Can't go up!")
|
||||
new_path = posixpath.join(path, posixpath.pardir)
|
||||
url.setPath(new_path)
|
||||
for _i in range(0, min(count, path.count('/'))):
|
||||
path = posixpath.join(path, posixpath.pardir)
|
||||
url.setPath(path)
|
||||
return url
|
||||
|
||||
|
||||
@ -137,4 +141,4 @@ def prevnext(*, browsertab, win_id, baseurl, prev=False,
|
||||
|
||||
selector = ', '.join([webelem.SELECTORS[webelem.Group.links],
|
||||
webelem.SELECTORS[webelem.Group.prevnext]])
|
||||
browsertab.find_all_elements(selector, _prevnext_cb)
|
||||
browsertab.elements.find_css(selector, _prevnext_cb)
|
||||
|
@ -29,7 +29,8 @@ Module attributes:
|
||||
|
||||
import collections.abc
|
||||
|
||||
from PyQt5.QtCore import QUrl
|
||||
from PyQt5.QtCore import QUrl, Qt, QEvent, QTimer
|
||||
from PyQt5.QtGui import QMouseEvent
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import log, usertypes, utils, qtutils
|
||||
@ -73,7 +74,14 @@ class Error(Exception):
|
||||
|
||||
class AbstractWebElement(collections.abc.MutableMapping):
|
||||
|
||||
"""A wrapper around QtWebKit/QtWebEngine web element."""
|
||||
"""A wrapper around QtWebKit/QtWebEngine web element.
|
||||
|
||||
Attributes:
|
||||
tab: The tab associated with this element.
|
||||
"""
|
||||
|
||||
def __init__(self, tab):
|
||||
self._tab = tab
|
||||
|
||||
def __eq__(self, other):
|
||||
raise NotImplementedError
|
||||
@ -103,30 +111,14 @@ class AbstractWebElement(collections.abc.MutableMapping):
|
||||
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?
|
||||
def has_frame(self):
|
||||
"""Check if this element has a valid frame attached."""
|
||||
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
|
||||
@ -166,21 +158,6 @@ class AbstractWebElement(collections.abc.MutableMapping):
|
||||
# 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?
|
||||
@ -191,8 +168,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
|
||||
# FIXME:qtwebengine get rid of this?
|
||||
raise NotImplementedError
|
||||
|
||||
def rect_on_view(self, *, elem_geometry=None, adjust_zoom=True,
|
||||
no_js=False):
|
||||
def rect_on_view(self, *, elem_geometry=None, no_js=False):
|
||||
"""Get the geometry of the element relative to the webview.
|
||||
|
||||
Uses the getClientRects() JavaScript method to obtain the collection of
|
||||
@ -208,8 +184,6 @@ class AbstractWebElement(collections.abc.MutableMapping):
|
||||
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
|
||||
@ -367,3 +341,63 @@ class AbstractWebElement(collections.abc.MutableMapping):
|
||||
url = baseurl.resolved(url)
|
||||
qtutils.ensure_valid(url)
|
||||
return url
|
||||
|
||||
def _mouse_pos(self):
|
||||
"""Get the position to click/hover."""
|
||||
# Click the center of the largest square fitting into the top/left
|
||||
# corner of the rectangle, this will help if part of the <a> element
|
||||
# is hidden behind other elements
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/1005
|
||||
rect = self.rect_on_view()
|
||||
if rect.width() > rect.height():
|
||||
rect.setWidth(rect.height())
|
||||
else:
|
||||
rect.setHeight(rect.width())
|
||||
return rect.center()
|
||||
|
||||
def click(self, click_target):
|
||||
"""Simulate a click on the element."""
|
||||
# FIXME:qtwebengine do we need this?
|
||||
# self._widget.setFocus()
|
||||
self._tab.data.override_target = click_target
|
||||
|
||||
pos = self._mouse_pos()
|
||||
|
||||
log.hints.debug("Sending fake click to '{}' at position {} with "
|
||||
"target {}".format(self.debug_text(), pos,
|
||||
click_target))
|
||||
|
||||
if click_target in [usertypes.ClickTarget.tab,
|
||||
usertypes.ClickTarget.tab_bg,
|
||||
usertypes.ClickTarget.window]:
|
||||
modifiers = Qt.ControlModifier
|
||||
else:
|
||||
modifiers = Qt.NoModifier
|
||||
|
||||
events = [
|
||||
QMouseEvent(QEvent.MouseMove, pos, Qt.NoButton, Qt.NoButton,
|
||||
Qt.NoModifier),
|
||||
QMouseEvent(QEvent.MouseButtonPress, pos, Qt.LeftButton,
|
||||
Qt.LeftButton, modifiers),
|
||||
QMouseEvent(QEvent.MouseButtonRelease, pos, Qt.LeftButton,
|
||||
Qt.NoButton, modifiers),
|
||||
]
|
||||
|
||||
for evt in events:
|
||||
# For some reason, postpone=True is needed here to *not* cause
|
||||
# segfaults in misc.feature because of :fake-key later...
|
||||
self._tab.send_event(evt, postpone=True)
|
||||
|
||||
def after_click():
|
||||
"""Move cursor to end and reset override_target after clicking."""
|
||||
if self.is_text_input() and self.is_editable():
|
||||
self._tab.caret.move_to_end_of_document()
|
||||
self._tab.data.override_target = None
|
||||
QTimer.singleShot(0, after_click)
|
||||
|
||||
def hover(self):
|
||||
"""Simulate a mouse hover over the element."""
|
||||
pos = self._mouse_pos()
|
||||
event = QMouseEvent(QEvent.MouseMove, pos, Qt.NoButton, Qt.NoButton,
|
||||
Qt.NoModifier)
|
||||
self._tab.send_event(event)
|
||||
|
@ -24,7 +24,7 @@
|
||||
|
||||
from PyQt5.QtCore import QRect
|
||||
|
||||
from qutebrowser.utils import log
|
||||
from qutebrowser.utils import log, javascript
|
||||
from qutebrowser.browser import webelem
|
||||
|
||||
|
||||
@ -32,7 +32,8 @@ class WebEngineElement(webelem.AbstractWebElement):
|
||||
|
||||
"""A web element for QtWebEngine, using JS under the hood."""
|
||||
|
||||
def __init__(self, js_dict):
|
||||
def __init__(self, js_dict, tab):
|
||||
super().__init__(tab)
|
||||
self._id = js_dict['id']
|
||||
self._js_dict = js_dict
|
||||
|
||||
@ -57,26 +58,13 @@ class WebEngineElement(webelem.AbstractWebElement):
|
||||
def __len__(self):
|
||||
return len(self._js_dict['attributes'])
|
||||
|
||||
def frame(self):
|
||||
log.stub()
|
||||
return None
|
||||
def has_frame(self):
|
||||
return True
|
||||
|
||||
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 ''
|
||||
@ -91,7 +79,7 @@ class WebEngineElement(webelem.AbstractWebElement):
|
||||
|
||||
The returned name will always be lower-case.
|
||||
"""
|
||||
return self._js_dict['tag_name']
|
||||
return self._js_dict['tag_name'].lower()
|
||||
|
||||
def outer_xml(self):
|
||||
"""Get the full HTML representation of this element."""
|
||||
@ -117,22 +105,8 @@ class WebEngineElement(webelem.AbstractWebElement):
|
||||
content-editable.
|
||||
"""
|
||||
# FIXME:qtwebengine what to do about use_js with WebEngine?
|
||||
log.stub()
|
||||
|
||||
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()
|
||||
js_code = javascript.assemble('webelem', 'set_text', self._id, text)
|
||||
self._tab.run_js_async(js_code)
|
||||
|
||||
def run_js_async(self, code, callback=None):
|
||||
"""Run the given JS snippet async on the element."""
|
||||
@ -145,15 +119,9 @@ class WebEngineElement(webelem.AbstractWebElement):
|
||||
log.stub()
|
||||
return None
|
||||
|
||||
def rect_on_view(self, *, elem_geometry=None, adjust_zoom=True,
|
||||
no_js=False):
|
||||
def rect_on_view(self, *, elem_geometry=None, 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
|
||||
@ -162,11 +130,35 @@ class WebEngineElement(webelem.AbstractWebElement):
|
||||
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()
|
||||
rects = self._js_dict['rects']
|
||||
for rect in rects:
|
||||
# FIXME:qtwebengine
|
||||
# width = rect.get("width", 0)
|
||||
# height = rect.get("height", 0)
|
||||
width = rect['width']
|
||||
height = rect['height']
|
||||
if width > 1 and height > 1:
|
||||
# Fix coordinates according to zoom level
|
||||
# We're not checking for zoom-text-only here as that doesn't
|
||||
# exist for QtWebEngine.
|
||||
zoom = self._tab.zoom.factor()
|
||||
rect["left"] *= zoom
|
||||
rect["top"] *= zoom
|
||||
width *= zoom
|
||||
height *= zoom
|
||||
rect = QRect(rect["left"], rect["top"], width, height)
|
||||
# FIXME:qtwebengine
|
||||
# frame = self._elem.webFrame()
|
||||
# while frame is not None:
|
||||
# # Translate to parent frames' position (scroll position
|
||||
# # is taken care of inside getClientRects)
|
||||
# rect.translate(frame.geometry().topLeft())
|
||||
# frame = frame.parentFrame()
|
||||
return rect
|
||||
log.webview.debug("Couldn't find rectangle for {!r} ({})".format(
|
||||
self, rects))
|
||||
return QRect()
|
||||
|
||||
def is_visible(self, mainframe):
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -24,16 +24,15 @@
|
||||
|
||||
import functools
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, Qt, QEvent, QPoint
|
||||
from PyQt5.QtCore import pyqtSlot, Qt, QEvent, QPoint, QUrl
|
||||
from PyQt5.QtGui import QKeyEvent, QIcon
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
# pylint: disable=no-name-in-module,import-error,useless-suppression
|
||||
from PyQt5.QtWebEngineWidgets import QWebEnginePage, QWebEngineScript
|
||||
# pylint: enable=no-name-in-module,import-error,useless-suppression
|
||||
|
||||
from qutebrowser.browser import browsertab
|
||||
from qutebrowser.browser import browsertab, mouse
|
||||
from qutebrowser.browser.webengine import webview, webengineelem
|
||||
from qutebrowser.utils import usertypes, qtutils, log, javascript
|
||||
from qutebrowser.utils import usertypes, qtutils, log, javascript, utils
|
||||
|
||||
|
||||
class WebEnginePrinting(browsertab.AbstractPrinting):
|
||||
@ -43,7 +42,7 @@ class WebEnginePrinting(browsertab.AbstractPrinting):
|
||||
def check_pdf_support(self):
|
||||
if not hasattr(self._widget.page(), 'printToPdf'):
|
||||
raise browsertab.WebTabError(
|
||||
"Printing to PDF is unsupported with QtWebEngine on Qt > 5.7")
|
||||
"Printing to PDF is unsupported with QtWebEngine on Qt < 5.7")
|
||||
|
||||
def check_printer_support(self):
|
||||
raise browsertab.WebTabError(
|
||||
@ -182,36 +181,35 @@ class WebEngineScroller(browsertab.AbstractScroller):
|
||||
|
||||
"""QtWebEngine implementations related to scrolling."""
|
||||
|
||||
# FIXME:qtwebengine
|
||||
# using stuff here with a big count/argument causes memory leaks and hangs
|
||||
|
||||
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.
|
||||
press_evt = QKeyEvent(QEvent.KeyPress, key, Qt.NoModifier, 0, 0, 0)
|
||||
release_evt = QKeyEvent(QEvent.KeyRelease, key, Qt.NoModifier, 0, 0, 0)
|
||||
recipient = self._widget.focusProxy()
|
||||
for _ in range(count):
|
||||
# If we get a segfault here, we might want to try sendEvent
|
||||
# instead.
|
||||
QApplication.postEvent(recipient, press_evt)
|
||||
QApplication.postEvent(recipient, release_evt)
|
||||
self._tab.send_event(press_evt)
|
||||
self._tab.send_event(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
|
||||
@ -222,8 +220,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
|
||||
@ -232,18 +230,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):
|
||||
@ -317,13 +315,70 @@ class WebEngineZoom(browsertab.AbstractZoom):
|
||||
return self._widget.zoomFactor()
|
||||
|
||||
|
||||
class WebEngineElements(browsertab.AbstractElements):
|
||||
|
||||
"""QtWebEngine implemementations related to elements on the page."""
|
||||
|
||||
def _js_cb_multiple(self, callback, js_elems):
|
||||
"""Handle found elements coming from JS and call the real callback.
|
||||
|
||||
Args:
|
||||
callback: The callback to call with the found elements.
|
||||
js_elems: The elements serialized from javascript.
|
||||
"""
|
||||
elems = []
|
||||
for js_elem in js_elems:
|
||||
elem = webengineelem.WebEngineElement(js_elem, tab=self._tab)
|
||||
elems.append(elem)
|
||||
callback(elems)
|
||||
|
||||
def _js_cb_single(self, callback, js_elem):
|
||||
"""Handle a found focus elem coming from JS and call the real callback.
|
||||
|
||||
Args:
|
||||
callback: The callback to call with the found element.
|
||||
Called with a WebEngineElement or None.
|
||||
js_elem: The element serialized from javascript.
|
||||
"""
|
||||
log.webview.debug("Got element from JS: {!r}".format(js_elem))
|
||||
if js_elem is None:
|
||||
callback(None)
|
||||
else:
|
||||
elem = webengineelem.WebEngineElement(js_elem, tab=self._tab)
|
||||
callback(elem)
|
||||
|
||||
def find_css(self, selector, callback, *, only_visible=False):
|
||||
js_code = javascript.assemble('webelem', 'find_all', selector,
|
||||
only_visible)
|
||||
js_cb = functools.partial(self._js_cb_multiple, callback)
|
||||
self._tab.run_js_async(js_code, js_cb)
|
||||
|
||||
def find_id(self, elem_id, callback):
|
||||
js_code = javascript.assemble('webelem', 'element_by_id', elem_id)
|
||||
js_cb = functools.partial(self._js_cb_single, callback)
|
||||
self._tab.run_js_async(js_code, js_cb)
|
||||
|
||||
def find_focused(self, callback):
|
||||
js_code = javascript.assemble('webelem', 'focus_element')
|
||||
js_cb = functools.partial(self._js_cb_single, callback)
|
||||
self._tab.run_js_async(js_code, js_cb)
|
||||
|
||||
def find_at_pos(self, pos, callback):
|
||||
assert pos.x() >= 0
|
||||
assert pos.y() >= 0
|
||||
js_code = javascript.assemble('webelem', 'element_at_pos',
|
||||
pos.x(), pos.y())
|
||||
js_cb = functools.partial(self._js_cb_single, callback)
|
||||
self._tab.run_js_async(js_code, js_cb)
|
||||
|
||||
|
||||
class WebEngineTab(browsertab.AbstractTab):
|
||||
|
||||
"""A QtWebEngine tab in the browser."""
|
||||
|
||||
def __init__(self, win_id, mode_manager, parent=None):
|
||||
super().__init__(win_id)
|
||||
widget = webview.WebEngineView()
|
||||
widget = webview.WebEngineView(tabdata=self.data)
|
||||
self.history = WebEngineHistory(self)
|
||||
self.scroller = WebEngineScroller(self, parent=self)
|
||||
self.caret = WebEngineCaret(win_id=win_id, mode_manager=mode_manager,
|
||||
@ -331,16 +386,54 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
self.zoom = WebEngineZoom(win_id=win_id, parent=self)
|
||||
self.search = WebEngineSearch(parent=self)
|
||||
self.printing = WebEnginePrinting()
|
||||
self.elements = WebEngineElements(self)
|
||||
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:
|
||||
@ -362,23 +455,6 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
else:
|
||||
self._widget.page().runJavaScript(code, callback)
|
||||
|
||||
def run_js_blocking(self, code):
|
||||
unset = object()
|
||||
loop = qtutils.EventLoop()
|
||||
js_ret = unset
|
||||
|
||||
def js_cb(val):
|
||||
"""Handle return value from JS and stop blocking."""
|
||||
nonlocal js_ret
|
||||
js_ret = val
|
||||
loop.quit()
|
||||
|
||||
self.run_js_async(code, js_cb)
|
||||
loop.exec_() # blocks until loop.quit() in js_cb
|
||||
assert js_ret is not unset
|
||||
|
||||
return js_ret
|
||||
|
||||
def shutdown(self):
|
||||
log.stub()
|
||||
|
||||
@ -413,42 +489,19 @@ 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.
|
||||
@pyqtSlot()
|
||||
def _on_history_trigger(self):
|
||||
url = self.url()
|
||||
requested_url = self.url(requested=True)
|
||||
|
||||
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)
|
||||
elems.append(elem)
|
||||
callback(elems)
|
||||
# Don't save the title if it's generated from the URL
|
||||
title = self.title()
|
||||
title_url = QUrl(url)
|
||||
title_url.setScheme('')
|
||||
if title == title_url.toDisplayString(QUrl.RemoveScheme).strip('/'):
|
||||
title = ""
|
||||
|
||||
def find_all_elements(self, selector, callback, *, only_visible=False):
|
||||
js_code = javascript.assemble('webelem', 'find_all_elements', 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:
|
||||
callback(webengineelem.WebEngineElement(js_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)
|
||||
self.add_history_item.emit(url, requested_url, title)
|
||||
|
||||
def _connect_signals(self):
|
||||
view = self._widget
|
||||
@ -457,10 +510,12 @@ 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.loadFinished.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)
|
||||
page.certificate_error.connect(self._on_ssl_errors)
|
||||
page.link_clicked.connect(self._on_link_clicked)
|
||||
try:
|
||||
view.iconChanged.connect(self.icon_changed)
|
||||
except AttributeError:
|
||||
@ -469,3 +524,6 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
page.contentsSizeChanged.connect(self.contents_size_changed)
|
||||
except AttributeError:
|
||||
log.stub('contentsSizeChanged, on Qt < 5.7')
|
||||
|
||||
def _event_target(self):
|
||||
return self._widget.focusProxy()
|
||||
|
@ -20,43 +20,39 @@
|
||||
"""The main browser widget for QtWebEngine."""
|
||||
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, Qt, QPoint
|
||||
from PyQt5.QtCore import pyqtSignal, QUrl
|
||||
# pylint: disable=no-name-in-module,import-error,useless-suppression
|
||||
from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage
|
||||
# pylint: enable=no-name-in-module,import-error,useless-suppression
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import log
|
||||
from qutebrowser.utils import log, debug, usertypes
|
||||
|
||||
|
||||
class WebEngineView(QWebEngineView):
|
||||
|
||||
"""Custom QWebEngineView subclass with qutebrowser-specific features."""
|
||||
|
||||
mouse_wheel_zoom = pyqtSignal(QPoint)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
def __init__(self, tabdata, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setPage(WebEnginePage(self))
|
||||
|
||||
def wheelEvent(self, e):
|
||||
"""Zoom on Ctrl-Mousewheel.
|
||||
|
||||
Args:
|
||||
e: The QWheelEvent.
|
||||
"""
|
||||
if e.modifiers() & Qt.ControlModifier:
|
||||
e.accept()
|
||||
self.mouse_wheel_zoom.emit(e.angleDelta())
|
||||
else:
|
||||
super().wheelEvent(e)
|
||||
self.setPage(WebEnginePage(tabdata, parent=self))
|
||||
|
||||
|
||||
class WebEnginePage(QWebEnginePage):
|
||||
|
||||
"""Custom QWebEnginePage subclass with qutebrowser-specific features."""
|
||||
"""Custom QWebEnginePage subclass with qutebrowser-specific features.
|
||||
|
||||
Signals:
|
||||
certificate_error: FIXME:qtwebengine
|
||||
link_clicked: Emitted when a link was clicked on a page.
|
||||
"""
|
||||
|
||||
certificate_error = pyqtSignal()
|
||||
link_clicked = pyqtSignal(QUrl)
|
||||
|
||||
def __init__(self, tabdata, parent=None):
|
||||
super().__init__(parent)
|
||||
self._tabdata = tabdata
|
||||
|
||||
def certificateError(self, error):
|
||||
self.certificate_error.emit()
|
||||
@ -82,3 +78,31 @@ class WebEnginePage(QWebEnginePage):
|
||||
"""Handle new windows via JS."""
|
||||
log.stub()
|
||||
return None
|
||||
|
||||
def acceptNavigationRequest(self,
|
||||
url: QUrl,
|
||||
typ: QWebEnginePage.NavigationType,
|
||||
is_main_frame: bool):
|
||||
"""Override acceptNavigationRequest to handle clicked links.
|
||||
|
||||
Setting linkDelegationPolicy to DelegateAllLinks and using a slot bound
|
||||
to linkClicked won't work correctly, because when in a frameset, we
|
||||
have no idea in which frame the link should be opened.
|
||||
|
||||
Checks if it should open it in a tab (middle-click or control) or not,
|
||||
and then conditionally opens the URL. Opening it in a new tab/window
|
||||
is handled in the slot connected to link_clicked.
|
||||
"""
|
||||
target = self._tabdata.combined_target()
|
||||
log.webview.debug("navigation request: url {}, type {}, "
|
||||
"target {}, is_main_frame {}".format(
|
||||
url.toDisplayString(),
|
||||
debug.qenum_key(QWebEnginePage, typ),
|
||||
target, is_main_frame))
|
||||
|
||||
if typ != QWebEnginePage.NavigationTypeLinkClicked:
|
||||
return True
|
||||
|
||||
self.link_clicked.emit(url)
|
||||
|
||||
return url.isValid() and target == usertypes.ClickTarget.normal
|
||||
|
@ -271,7 +271,7 @@ class _Downloader:
|
||||
elements = web_frame.findAllElements('link, script, img')
|
||||
|
||||
for element in elements:
|
||||
element = webkitelem.WebKitElement(element)
|
||||
element = webkitelem.WebKitElement(element, tab=self.tab)
|
||||
# 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 = webkitelem.WebKitElement(style)
|
||||
style = webkitelem.WebKitElement(style, tab=self.tab)
|
||||
# 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 = webkitelem.WebKitElement(element)
|
||||
element = webkitelem.WebKitElement(element, tab=self.tab)
|
||||
style = element['style']
|
||||
for element_url in _get_css_imports(style, inline=True):
|
||||
self._fetch_url(web_url.resolved(QUrl(element_url)))
|
||||
|
@ -38,7 +38,8 @@ class WebKitElement(webelem.AbstractWebElement):
|
||||
|
||||
"""A wrapper around a QWebElement."""
|
||||
|
||||
def __init__(self, elem):
|
||||
def __init__(self, elem, tab):
|
||||
super().__init__(tab)
|
||||
if isinstance(elem, self.__class__):
|
||||
raise TypeError("Trying to wrap a wrapper!")
|
||||
if elem.isNull():
|
||||
@ -83,36 +84,14 @@ class WebKitElement(webelem.AbstractWebElement):
|
||||
if self._elem.isNull():
|
||||
raise IsNullError('Element {} vanished!'.format(self._elem))
|
||||
|
||||
def frame(self):
|
||||
def has_frame(self):
|
||||
self._check_vanished()
|
||||
return self._elem.webFrame()
|
||||
return self._elem.webFrame() is not None
|
||||
|
||||
def geometry(self):
|
||||
self._check_vanished()
|
||||
return self._elem.geometry()
|
||||
|
||||
def document_element(self):
|
||||
self._check_vanished()
|
||||
elem = self._elem.webFrame().documentElement()
|
||||
return WebKitElement(elem)
|
||||
|
||||
def create_inside(self, tagname):
|
||||
# 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
|
||||
# then use lastChild() to get a reference to it.
|
||||
# See: http://stackoverflow.com/q/7364852/2085149
|
||||
self._check_vanished()
|
||||
self._elem.appendInside('<{}></{}>'.format(tagname, tagname))
|
||||
return WebKitElement(self._elem.lastChild())
|
||||
|
||||
def find_first(self, selector):
|
||||
self._check_vanished()
|
||||
elem = self._elem.findFirst(selector)
|
||||
if elem.isNull():
|
||||
return None
|
||||
return WebKitElement(elem)
|
||||
|
||||
def style_property(self, name, *, strategy):
|
||||
self._check_vanished()
|
||||
strategies = {
|
||||
@ -156,18 +135,6 @@ class WebKitElement(webelem.AbstractWebElement):
|
||||
text = javascript.string_escape(text)
|
||||
self._elem.evaluateJavaScript("this.value='{}'".format(text))
|
||||
|
||||
def set_inner_xml(self, xml):
|
||||
self._check_vanished()
|
||||
self._elem.setInnerXml(xml)
|
||||
|
||||
def remove_from_document(self):
|
||||
self._check_vanished()
|
||||
self._elem.removeFromDocument()
|
||||
|
||||
def set_style_property(self, name, value):
|
||||
self._check_vanished()
|
||||
return self._elem.setStyleProperty(name, value)
|
||||
|
||||
def run_js_async(self, code, callback=None):
|
||||
"""Run the given JS snippet async on the element."""
|
||||
self._check_vanished()
|
||||
@ -180,9 +147,9 @@ class WebKitElement(webelem.AbstractWebElement):
|
||||
elem = self._elem.parent()
|
||||
if elem is None:
|
||||
return None
|
||||
return WebKitElement(elem)
|
||||
return WebKitElement(elem, tab=self._tab)
|
||||
|
||||
def _rect_on_view_js(self, adjust_zoom):
|
||||
def _rect_on_view_js(self):
|
||||
"""Javascript implementation for rect_on_view."""
|
||||
# FIXME:qtwebengine maybe we can reuse this?
|
||||
rects = self._elem.evaluateJavaScript("this.getClientRects()")
|
||||
@ -203,7 +170,7 @@ class WebKitElement(webelem.AbstractWebElement):
|
||||
if width > 1 and height > 1:
|
||||
# fix coordinates according to zoom level
|
||||
zoom = self._elem.webFrame().zoomFactor()
|
||||
if not config.get('ui', 'zoom-text-only') and adjust_zoom:
|
||||
if not config.get('ui', 'zoom-text-only'):
|
||||
rect["left"] *= zoom
|
||||
rect["top"] *= zoom
|
||||
width *= zoom
|
||||
@ -231,18 +198,9 @@ class WebKitElement(webelem.AbstractWebElement):
|
||||
rect.translate(frame.geometry().topLeft())
|
||||
rect.translate(frame.scrollPosition() * -1)
|
||||
frame = frame.parentFrame()
|
||||
# We deliberately always adjust the zoom here, even with
|
||||
# adjust_zoom=False
|
||||
if elem_geometry is None:
|
||||
zoom = self._elem.webFrame().zoomFactor()
|
||||
if not config.get('ui', 'zoom-text-only'):
|
||||
rect.moveTo(rect.left() / zoom, rect.top() / zoom)
|
||||
rect.setWidth(rect.width() / zoom)
|
||||
rect.setHeight(rect.height() / zoom)
|
||||
return rect
|
||||
|
||||
def rect_on_view(self, *, elem_geometry=None, adjust_zoom=True,
|
||||
no_js=False):
|
||||
def rect_on_view(self, *, elem_geometry=None, no_js=False):
|
||||
"""Get the geometry of the element relative to the webview.
|
||||
|
||||
Uses the getClientRects() JavaScript method to obtain the collection of
|
||||
@ -258,18 +216,14 @@ class WebKitElement(webelem.AbstractWebElement):
|
||||
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
|
||||
"""
|
||||
# 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
|
||||
# accurate
|
||||
if elem_geometry is None and not no_js:
|
||||
rect = self._rect_on_view_js(adjust_zoom)
|
||||
rect = self._rect_on_view_js()
|
||||
if rect is not None:
|
||||
return rect
|
||||
|
||||
@ -349,5 +303,6 @@ def focus_elem(frame):
|
||||
Args:
|
||||
frame: The QWebFrame to search in.
|
||||
"""
|
||||
# FIXME:qtwebengine get rid of this
|
||||
elem = frame.findFirstElement('*:focus')
|
||||
return WebKitElement(elem)
|
||||
return WebKitElement(elem, tab=None)
|
||||
|
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
|
||||
|
||||
|
||||
|
@ -32,7 +32,7 @@ from PyQt5.QtPrintSupport import QPrinter
|
||||
|
||||
from qutebrowser.browser import browsertab
|
||||
from qutebrowser.browser.webkit import webview, tabhistory, webkitelem
|
||||
from qutebrowser.utils import qtutils, objreg, usertypes, utils
|
||||
from qutebrowser.utils import qtutils, objreg, usertypes, utils, log
|
||||
|
||||
|
||||
class WebKitPrinting(browsertab.AbstractPrinting):
|
||||
@ -315,7 +315,7 @@ class WebKitCaret(browsertab.AbstractCaret):
|
||||
if QWebSettings.globalSettings().testAttribute(
|
||||
QWebSettings.JavascriptEnabled):
|
||||
if tab:
|
||||
self._widget.page().open_target = usertypes.ClickTarget.tab
|
||||
self._tab.data.open_target = usertypes.ClickTarget.tab
|
||||
self._tab.run_js_async(
|
||||
'window.getSelection().anchorNode.parentNode.click()')
|
||||
else:
|
||||
@ -491,6 +491,85 @@ class WebKitHistory(browsertab.AbstractHistory):
|
||||
self._tab.scroller.to_point, cur_data['scroll-pos']))
|
||||
|
||||
|
||||
class WebKitElements(browsertab.AbstractElements):
|
||||
|
||||
"""QtWebKit implemementations related to elements on the page."""
|
||||
|
||||
def find_css(self, selector, callback, *, only_visible=False):
|
||||
mainframe = self._widget.page().mainFrame()
|
||||
if mainframe is None:
|
||||
raise browsertab.WebTabError("No frame focused!")
|
||||
|
||||
elems = []
|
||||
frames = webkitelem.get_child_frames(mainframe)
|
||||
for f in frames:
|
||||
for elem in f.findAllElements(selector):
|
||||
elems.append(webkitelem.WebKitElement(elem, tab=self._tab))
|
||||
|
||||
if only_visible:
|
||||
elems = [e for e in elems if e.is_visible(mainframe)]
|
||||
|
||||
callback(elems)
|
||||
|
||||
def find_id(self, elem_id, callback):
|
||||
def find_id_cb(elems):
|
||||
if not elems:
|
||||
callback(None)
|
||||
else:
|
||||
callback(elems[0])
|
||||
self.find_css('#' + elem_id, find_id_cb)
|
||||
|
||||
def find_focused(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, tab=self._tab))
|
||||
|
||||
def find_at_pos(self, pos, callback):
|
||||
assert pos.x() >= 0
|
||||
assert pos.y() >= 0
|
||||
frame = self._widget.page().frameAt(pos)
|
||||
if frame is None:
|
||||
# This happens when we click inside the webview, but not actually
|
||||
# on the QWebPage - for example when clicking the scrollbar
|
||||
# sometimes.
|
||||
log.webview.debug("Hit test at {} but frame is None!".format(pos))
|
||||
callback(None)
|
||||
return
|
||||
|
||||
# You'd think we have to subtract frame.geometry().topLeft() from the
|
||||
# position, but it seems QWebFrame::hitTestContent wants a position
|
||||
# relative to the QWebView, not to the frame. This makes no sense to
|
||||
# me, but it works this way.
|
||||
hitresult = frame.hitTestContent(pos)
|
||||
if hitresult.isNull():
|
||||
# For some reason, the whole hit result can be null sometimes (e.g.
|
||||
# on doodle menu links). If this is the case, we schedule a check
|
||||
# later (in mouseReleaseEvent) which uses webkitelem.focus_elem.
|
||||
log.webview.debug("Hit test result is null!")
|
||||
callback(None)
|
||||
return
|
||||
|
||||
try:
|
||||
elem = webkitelem.WebKitElement(hitresult.element(), tab=self._tab)
|
||||
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
|
||||
# later (in mouseReleaseEvent) which uses webelem.focus_elem.
|
||||
log.webview.debug("Hit test result element is null!")
|
||||
callback(None)
|
||||
return
|
||||
|
||||
callback(elem)
|
||||
|
||||
|
||||
class WebKitTab(browsertab.AbstractTab):
|
||||
|
||||
"""A QtWebKit tab in the browser."""
|
||||
@ -505,17 +584,25 @@ class WebKitTab(browsertab.AbstractTab):
|
||||
self.zoom = WebKitZoom(win_id=win_id, parent=self)
|
||||
self.search = WebKitSearch(parent=self)
|
||||
self.printing = WebKitPrinting()
|
||||
self.elements = WebKitElements(self)
|
||||
self._set_widget(widget)
|
||||
self._connect_signals()
|
||||
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()
|
||||
@ -525,13 +612,10 @@ class WebKitTab(browsertab.AbstractTab):
|
||||
callback(frame.toHtml())
|
||||
|
||||
def run_js_async(self, code, callback=None):
|
||||
result = self.run_js_blocking(code)
|
||||
result = self._widget.page().mainFrame().evaluateJavaScript(code)
|
||||
if callback is not None:
|
||||
callback(result)
|
||||
|
||||
def run_js_blocking(self, code):
|
||||
return self._widget.page().mainFrame().evaluateJavaScript(code)
|
||||
|
||||
def icon(self):
|
||||
return self._widget.icon()
|
||||
|
||||
@ -555,37 +639,15 @@ class WebKitTab(browsertab.AbstractTab):
|
||||
nam = self._widget.page().networkAccessManager()
|
||||
nam.clear_all_ssl_errors()
|
||||
|
||||
@pyqtSlot()
|
||||
def _on_history_trigger(self):
|
||||
url = self.url()
|
||||
requested_url = self.url(requested=True)
|
||||
self.add_history_item.emit(url, requested_url, self.title())
|
||||
|
||||
def set_html(self, html, base_url):
|
||||
self._widget.setHtml(html, base_url)
|
||||
|
||||
def find_all_elements(self, selector, callback, *, only_visible=False):
|
||||
mainframe = self._widget.page().mainFrame()
|
||||
if mainframe is None:
|
||||
raise browsertab.WebTabError("No frame focused!")
|
||||
|
||||
elems = []
|
||||
frames = webkitelem.get_child_frames(mainframe)
|
||||
for f in frames:
|
||||
for elem in f.findAllElements(selector):
|
||||
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.
|
||||
@ -630,3 +692,8 @@ 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)
|
||||
page.link_clicked.connect(self._on_link_clicked)
|
||||
|
||||
def _event_target(self):
|
||||
return self._widget
|
||||
|
@ -26,14 +26,14 @@ from PyQt5.QtGui import QDesktopServices
|
||||
from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest
|
||||
from PyQt5.QtWidgets import QFileDialog
|
||||
from PyQt5.QtPrintSupport import QPrintDialog
|
||||
from PyQt5.QtWebKitWidgets import QWebPage
|
||||
from PyQt5.QtWebKitWidgets import QWebPage, QWebFrame
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.browser import pdfjs
|
||||
from qutebrowser.browser.webkit import http
|
||||
from qutebrowser.browser.webkit.network import networkmanager
|
||||
from qutebrowser.utils import (message, usertypes, log, jinja, qtutils, utils,
|
||||
objreg, debug, urlutils)
|
||||
objreg, debug)
|
||||
|
||||
|
||||
class BrowserPage(QWebPage):
|
||||
@ -42,27 +42,28 @@ class BrowserPage(QWebPage):
|
||||
|
||||
Attributes:
|
||||
error_occurred: Whether an error occurred while loading.
|
||||
open_target: Where to open the next navigation request.
|
||||
("normal", "tab", "tab_bg")
|
||||
_hint_target: Override for open_target while hinting, or None.
|
||||
_extension_handlers: Mapping of QWebPage extensions to their handlers.
|
||||
_networkmanager: The NetworkManager used.
|
||||
_win_id: The window ID this BrowserPage is associated with.
|
||||
_ignore_load_started: Whether to ignore the next loadStarted signal.
|
||||
_is_shutting_down: Whether the page is currently shutting down.
|
||||
_tabdata: The TabData object of the tab this page is in.
|
||||
|
||||
Signals:
|
||||
shutting_down: Emitted when the page is currently shutting down.
|
||||
reloading: Emitted before a web page reloads.
|
||||
arg: The URL which gets reloaded.
|
||||
link_clicked: Emitted when a link was clicked on a page.
|
||||
"""
|
||||
|
||||
shutting_down = pyqtSignal()
|
||||
reloading = pyqtSignal(QUrl)
|
||||
link_clicked = pyqtSignal(QUrl)
|
||||
|
||||
def __init__(self, win_id, tab_id, parent=None):
|
||||
def __init__(self, win_id, tab_id, tabdata, parent=None):
|
||||
super().__init__(parent)
|
||||
self._win_id = win_id
|
||||
self._tabdata = tabdata
|
||||
self._is_shutting_down = False
|
||||
self._extension_handlers = {
|
||||
QWebPage.ErrorPageExtension: self._handle_errorpage,
|
||||
@ -70,8 +71,6 @@ class BrowserPage(QWebPage):
|
||||
}
|
||||
self._ignore_load_started = False
|
||||
self.error_occurred = False
|
||||
self.open_target = usertypes.ClickTarget.normal
|
||||
self._hint_target = None
|
||||
self._networkmanager = networkmanager.NetworkManager(
|
||||
win_id, tab_id, self)
|
||||
self.setNetworkAccessManager(self._networkmanager)
|
||||
@ -422,22 +421,6 @@ class BrowserPage(QWebPage):
|
||||
if 'scroll-pos' in data and frame.scrollPosition() == QPoint(0, 0):
|
||||
frame.setScrollPosition(data['scroll-pos'])
|
||||
|
||||
@pyqtSlot(usertypes.ClickTarget)
|
||||
def on_start_hinting(self, hint_target):
|
||||
"""Emitted before a hinting-click takes place.
|
||||
|
||||
Args:
|
||||
hint_target: A ClickTarget member to set self._hint_target to.
|
||||
"""
|
||||
log.webview.debug("Setting force target to {}".format(hint_target))
|
||||
self._hint_target = hint_target
|
||||
|
||||
@pyqtSlot()
|
||||
def on_stop_hinting(self):
|
||||
"""Emitted when hinting is finished."""
|
||||
log.webview.debug("Finishing hinting.")
|
||||
self._hint_target = None
|
||||
|
||||
def userAgentForUrl(self, url):
|
||||
"""Override QWebPage::userAgentForUrl to customize the user agent."""
|
||||
ua = config.get('network', 'user-agent')
|
||||
@ -531,7 +514,10 @@ class BrowserPage(QWebPage):
|
||||
answer = True
|
||||
return answer
|
||||
|
||||
def acceptNavigationRequest(self, _frame, request, typ):
|
||||
def acceptNavigationRequest(self,
|
||||
_frame: QWebFrame,
|
||||
request: QNetworkRequest,
|
||||
typ: QWebPage.NavigationType):
|
||||
"""Override acceptNavigationRequest to handle clicked links.
|
||||
|
||||
Setting linkDelegationPolicy to DelegateAllLinks and using a slot bound
|
||||
@ -539,48 +525,23 @@ class BrowserPage(QWebPage):
|
||||
have no idea in which frame the link should be opened.
|
||||
|
||||
Checks if it should open it in a tab (middle-click or control) or not,
|
||||
and then opens the URL.
|
||||
|
||||
Args:
|
||||
_frame: QWebFrame (target frame)
|
||||
request: QNetworkRequest
|
||||
typ: QWebPage::NavigationType
|
||||
and then conditionally opens the URL. Opening it in a new tab/window
|
||||
is handled in the slot connected to link_clicked.
|
||||
"""
|
||||
url = request.url()
|
||||
urlstr = url.toDisplayString()
|
||||
target = self._tabdata.combined_target()
|
||||
log.webview.debug("navigation request: url {}, type {}, "
|
||||
"target {}".format(
|
||||
url.toDisplayString(),
|
||||
debug.qenum_key(QWebPage, typ),
|
||||
target))
|
||||
|
||||
if typ == QWebPage.NavigationTypeReload:
|
||||
self.reloading.emit(url)
|
||||
if typ != QWebPage.NavigationTypeLinkClicked:
|
||||
return True
|
||||
if not url.isValid():
|
||||
msg = urlutils.get_errstring(url, "Invalid link clicked")
|
||||
message.error(self._win_id, msg)
|
||||
self.open_target = usertypes.ClickTarget.normal
|
||||
return False
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=self._win_id)
|
||||
log.webview.debug("acceptNavigationRequest, url {}, type {}, hint "
|
||||
"target {}, open_target {}".format(
|
||||
urlstr, debug.qenum_key(QWebPage, typ),
|
||||
self._hint_target, self.open_target))
|
||||
if self._hint_target is not None:
|
||||
target = self._hint_target
|
||||
else:
|
||||
target = self.open_target
|
||||
self.open_target = usertypes.ClickTarget.normal
|
||||
if target == usertypes.ClickTarget.tab:
|
||||
tabbed_browser.tabopen(url, False)
|
||||
return False
|
||||
elif target == usertypes.ClickTarget.tab_bg:
|
||||
tabbed_browser.tabopen(url, True)
|
||||
return False
|
||||
elif target == usertypes.ClickTarget.window:
|
||||
from qutebrowser.mainwindow import mainwindow
|
||||
window = mainwindow.MainWindow()
|
||||
window.show()
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window=window.win_id)
|
||||
tabbed_browser.tabopen(url, False)
|
||||
return False
|
||||
else:
|
||||
elif typ != QWebPage.NavigationTypeLinkClicked:
|
||||
return True
|
||||
|
||||
self.link_clicked.emit(url)
|
||||
|
||||
return url.isValid() and target == usertypes.ClickTarget.normal
|
||||
|
@ -21,16 +21,15 @@
|
||||
|
||||
import sys
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QTimer, QUrl, QPoint
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QUrl
|
||||
from PyQt5.QtGui import QPalette
|
||||
from PyQt5.QtWidgets import QApplication, QStyleFactory
|
||||
from PyQt5.QtWidgets import QStyleFactory
|
||||
from PyQt5.QtWebKit import QWebSettings
|
||||
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.browser import hints
|
||||
from qutebrowser.utils import log, usertypes, utils, qtutils, objreg
|
||||
from qutebrowser.browser.webkit import webpage, webkitelem
|
||||
|
||||
|
||||
@ -45,24 +44,16 @@ class WebView(QWebView):
|
||||
win_id: The window ID of the view.
|
||||
_tab_id: The tab ID of the view.
|
||||
_old_scroll_pos: The old scroll position.
|
||||
_check_insertmode: If True, in mouseReleaseEvent we should check if we
|
||||
need to enter/leave insert mode.
|
||||
_ignore_wheel_event: Ignore the next wheel event.
|
||||
See https://github.com/The-Compiler/qutebrowser/issues/395
|
||||
|
||||
Signals:
|
||||
scroll_pos_changed: Scroll percentage of current tab changed.
|
||||
arg 1: x-position in %.
|
||||
arg 2: y-position in %.
|
||||
mouse_wheel_zoom: Emitted when the page should be zoomed because the
|
||||
mousewheel was used with ctrl.
|
||||
arg 1: The angle delta of the wheel event (QPoint)
|
||||
shutting_down: Emitted when the view is shutting down.
|
||||
"""
|
||||
|
||||
scroll_pos_changed = pyqtSignal(int, int)
|
||||
shutting_down = pyqtSignal()
|
||||
mouse_wheel_zoom = pyqtSignal(QPoint)
|
||||
|
||||
def __init__(self, win_id, tab_id, tab, parent=None):
|
||||
super().__init__(parent)
|
||||
@ -70,51 +61,32 @@ class WebView(QWebView):
|
||||
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-42948
|
||||
# See https://github.com/The-Compiler/qutebrowser/issues/462
|
||||
self.setStyle(QStyleFactory.create('Fusion'))
|
||||
# FIXME:qtwebengine this is only used to set the zoom factor from
|
||||
# the QWebPage - we should get rid of it somehow (signals?)
|
||||
self.tab = tab
|
||||
self.win_id = win_id
|
||||
self._check_insertmode = False
|
||||
self.scroll_pos = (-1, -1)
|
||||
self._old_scroll_pos = (-1, -1)
|
||||
self._ignore_wheel_event = False
|
||||
self._set_bg_color()
|
||||
self._tab_id = tab_id
|
||||
|
||||
page = self._init_page()
|
||||
hintmanager = hints.HintManager(win_id, self._tab_id, self)
|
||||
hintmanager.mouse_event.connect(self.on_mouse_event)
|
||||
hintmanager.start_hinting.connect(page.on_start_hinting)
|
||||
hintmanager.stop_hinting.connect(page.on_stop_hinting)
|
||||
objreg.register('hintmanager', hintmanager, scope='tab', window=win_id,
|
||||
tab=tab_id)
|
||||
self._init_page(tab.data)
|
||||
mode_manager = objreg.get('mode-manager', scope='window',
|
||||
window=win_id)
|
||||
mode_manager.entered.connect(self.on_mode_entered)
|
||||
mode_manager.left.connect(self.on_mode_left)
|
||||
if config.get('input', 'rocker-gestures'):
|
||||
self.setContextMenuPolicy(Qt.PreventContextMenu)
|
||||
objreg.get('config').changed.connect(self.on_config_changed)
|
||||
objreg.get('config').changed.connect(self._set_bg_color)
|
||||
|
||||
@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, tabdata):
|
||||
"""Initialize the QWebPage used by this view.
|
||||
|
||||
def _init_page(self):
|
||||
"""Initialize the QWebPage used by this view."""
|
||||
page = webpage.BrowserPage(self.win_id, self._tab_id, self)
|
||||
Args:
|
||||
tabdata: The TabData object for this tab.
|
||||
"""
|
||||
page = webpage.BrowserPage(self.win_id, self._tab_id, tabdata,
|
||||
parent=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):
|
||||
url = utils.elide(self.url().toDisplayString(QUrl.EncodeUnicode), 100)
|
||||
@ -133,6 +105,7 @@ class WebView(QWebView):
|
||||
# deleted
|
||||
pass
|
||||
|
||||
@config.change_filter('colors', 'webpage.bg')
|
||||
def _set_bg_color(self):
|
||||
"""Set the webpage background color as configured."""
|
||||
col = config.get('colors', 'webpage.bg')
|
||||
@ -142,126 +115,6 @@ class WebView(QWebView):
|
||||
palette.setColor(QPalette.Base, col)
|
||||
self.setPalette(palette)
|
||||
|
||||
@pyqtSlot(str, str)
|
||||
def on_config_changed(self, section, option):
|
||||
"""Update rocker gestures/background color."""
|
||||
if section == 'input' and option == 'rocker-gestures':
|
||||
if config.get('input', 'rocker-gestures'):
|
||||
self.setContextMenuPolicy(Qt.PreventContextMenu)
|
||||
else:
|
||||
self.setContextMenuPolicy(Qt.DefaultContextMenu)
|
||||
elif section == 'colors' and option == 'webpage.bg':
|
||||
self._set_bg_color()
|
||||
|
||||
def _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.
|
||||
|
||||
Args:
|
||||
e: The QMouseEvent.
|
||||
"""
|
||||
pos = e.pos()
|
||||
frame = self.page().frameAt(pos)
|
||||
if frame is None:
|
||||
# This happens when we click inside the webview, but not actually
|
||||
# on the QWebPage - for example when clicking the scrollbar
|
||||
# sometimes.
|
||||
log.mouse.debug("Clicked at {} but frame is None!".format(pos))
|
||||
return
|
||||
# You'd think we have to subtract frame.geometry().topLeft() from the
|
||||
# position, but it seems QWebFrame::hitTestContent wants a position
|
||||
# relative to the QWebView, not to the frame. This makes no sense to
|
||||
# me, but it works this way.
|
||||
hitresult = frame.hitTestContent(pos)
|
||||
if hitresult.isNull():
|
||||
# For some reason, the whole hit result can be null sometimes (e.g.
|
||||
# on doodle menu links). If this is the case, we schedule a check
|
||||
# later (in mouseReleaseEvent) which uses webkitelem.focus_elem.
|
||||
log.mouse.debug("Hitresult is null!")
|
||||
self._check_insertmode = True
|
||||
return
|
||||
try:
|
||||
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
|
||||
# later (in mouseReleaseEvent) which uses webelem.focus_elem.
|
||||
log.mouse.debug("Hitresult element is null!")
|
||||
self._check_insertmode = True
|
||||
return
|
||||
if ((hitresult.isContentEditable() and elem.is_writable()) or
|
||||
elem.is_editable()):
|
||||
log.mouse.debug("Clicked editable element!")
|
||||
modeman.enter(self.win_id, usertypes.KeyMode.insert, 'click',
|
||||
only_if_normal=True)
|
||||
else:
|
||||
log.mouse.debug("Clicked non-editable element!")
|
||||
if config.get('input', 'auto-leave-insert-mode'):
|
||||
modeman.maybe_leave(self.win_id, usertypes.KeyMode.insert,
|
||||
'click')
|
||||
|
||||
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 = webkitelem.focus_elem(self.page().currentFrame())
|
||||
except (webkitelem.IsNullError, RuntimeError):
|
||||
log.mouse.debug("Element/page vanished!")
|
||||
return
|
||||
if elem.is_editable():
|
||||
log.mouse.debug("Clicked editable element (delayed)!")
|
||||
modeman.enter(self.win_id, usertypes.KeyMode.insert,
|
||||
'click-delayed', only_if_normal=True)
|
||||
else:
|
||||
log.mouse.debug("Clicked non-editable element (delayed)!")
|
||||
if config.get('input', 'auto-leave-insert-mode'):
|
||||
modeman.maybe_leave(self.win_id, usertypes.KeyMode.insert,
|
||||
'click-delayed')
|
||||
|
||||
def _mousepress_opentarget(self, e):
|
||||
"""Set the open target when something was clicked.
|
||||
|
||||
Args:
|
||||
e: The QMouseEvent.
|
||||
"""
|
||||
if e.button() == Qt.MidButton or e.modifiers() & Qt.ControlModifier:
|
||||
background_tabs = config.get('tabs', 'background-tabs')
|
||||
if e.modifiers() & Qt.ShiftModifier:
|
||||
background_tabs = not background_tabs
|
||||
if background_tabs:
|
||||
target = usertypes.ClickTarget.tab_bg
|
||||
else:
|
||||
target = usertypes.ClickTarget.tab
|
||||
self.page().open_target = target
|
||||
log.mouse.debug("Middle click, setting target: {}".format(target))
|
||||
else:
|
||||
self.page().open_target = usertypes.ClickTarget.normal
|
||||
log.mouse.debug("Normal click, setting normal target")
|
||||
|
||||
def shutdown(self):
|
||||
"""Shut down the webview."""
|
||||
self.shutting_down.emit()
|
||||
@ -297,13 +150,6 @@ class WebView(QWebView):
|
||||
bridge = objreg.get('js-bridge')
|
||||
frame.addToJavaScriptWindowObject('qute', bridge)
|
||||
|
||||
@pyqtSlot('QMouseEvent')
|
||||
def on_mouse_event(self, evt):
|
||||
"""Post a new mouse event from a hintmanager."""
|
||||
log.modes.debug("Hint triggered, focusing {!r}".format(self))
|
||||
self.setFocus()
|
||||
QApplication.postEvent(self, evt)
|
||||
|
||||
@pyqtSlot()
|
||||
def on_load_finished(self):
|
||||
"""Handle a finished page load.
|
||||
@ -407,59 +253,9 @@ class WebView(QWebView):
|
||||
# Let superclass handle the event
|
||||
super().paintEvent(e)
|
||||
|
||||
def mousePressEvent(self, e):
|
||||
"""Extend QWidget::mousePressEvent().
|
||||
|
||||
This does the following things:
|
||||
- Check if a link was clicked with the middle button or Ctrl and
|
||||
set the page's open_target attribute accordingly.
|
||||
- Emit the editable_elem_selected signal if an editable element was
|
||||
clicked.
|
||||
|
||||
Args:
|
||||
e: The arrived event.
|
||||
|
||||
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
|
||||
super().mousePressEvent(e)
|
||||
|
||||
def mouseReleaseEvent(self, e):
|
||||
"""Extend mouseReleaseEvent to enter insert mode if needed."""
|
||||
super().mouseReleaseEvent(e)
|
||||
# We want to make sure we check the focus element after the WebView is
|
||||
# updated completely.
|
||||
QTimer.singleShot(0, self.mouserelease_insertmode)
|
||||
|
||||
def contextMenuEvent(self, e):
|
||||
"""Save a reference to the context menu so we can close it."""
|
||||
menu = self.page().createStandardContextMenu()
|
||||
self.shutting_down.connect(menu.close)
|
||||
modeman.instance(self.win_id).entered.connect(menu.close)
|
||||
menu.exec_(e.globalPos())
|
||||
|
||||
def wheelEvent(self, e):
|
||||
"""Zoom on Ctrl-Mousewheel.
|
||||
|
||||
Args:
|
||||
e: The QWheelEvent.
|
||||
"""
|
||||
if self._ignore_wheel_event:
|
||||
self._ignore_wheel_event = False
|
||||
# See https://github.com/The-Compiler/qutebrowser/issues/395
|
||||
return
|
||||
if e.modifiers() & Qt.ControlModifier:
|
||||
e.accept()
|
||||
self.mouse_wheel_zoom.emit(e.angleDelta())
|
||||
else:
|
||||
super().wheelEvent(e)
|
||||
|
@ -17,4 +17,15 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Utilities and classes regarding to commands."""
|
||||
"""In qutebrowser, all keybindings are mapped to commands.
|
||||
|
||||
Some commands are hidden, which means they don't show up in the command
|
||||
completion when pressing `:`, as they're typically not useful to run by hand.
|
||||
|
||||
In the commandline, there are also some variables you can use:
|
||||
|
||||
- `{url}` expands to the URL of the current page
|
||||
- `{url:pretty}` expands to the URL in decoded format
|
||||
- `{clipboard}` expands to the clipboard contents
|
||||
- `{primary}` expands to the primary selection contents
|
||||
"""
|
||||
|
@ -21,12 +21,13 @@
|
||||
|
||||
import collections
|
||||
import traceback
|
||||
import re
|
||||
|
||||
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 +50,35 @@ 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 any('{url}' in arg for arg in arglist):
|
||||
url = _current_url(tabbed_browser).toString(QUrl.FullyEncoded |
|
||||
QUrl.RemovePassword)
|
||||
if any('{url:pretty}' in arg for arg in arglist):
|
||||
pretty_url = _current_url(tabbed_browser).toString(QUrl.RemovePassword)
|
||||
for arg in arglist:
|
||||
if '{url}' in arg:
|
||||
args.append(arg.replace('{url}', url))
|
||||
elif '{url:pretty}' in arg:
|
||||
args.append(arg.replace('{url:pretty}', pretty_url))
|
||||
else:
|
||||
args.append(arg)
|
||||
|
||||
def repl_cb(matchobj):
|
||||
"""Return replacement for given match."""
|
||||
var = matchobj.group("var")
|
||||
if var not in values:
|
||||
values[var] = variables[var]()
|
||||
return values[var]
|
||||
repl_pattern = re.compile("{(?P<var>" + "|".join(variables.keys()) + ")}")
|
||||
|
||||
try:
|
||||
for arg in arglist:
|
||||
# using re.sub with callback function replaces all variables in a
|
||||
# single pass and avoids expansion of nested variables (e.g.
|
||||
# "{url}" from clipboard is not expanded)
|
||||
args.append(repl_pattern.sub(repl_cb, arg))
|
||||
except utils.ClipboardError as e:
|
||||
raise cmdexc.CommandError(e)
|
||||
return args
|
||||
|
||||
|
||||
|
@ -180,20 +180,58 @@ class CompletionView(QTreeView):
|
||||
# Item is a real item, not a category header -> success
|
||||
return idx
|
||||
|
||||
def _next_category_idx(self, upwards):
|
||||
"""Get the index of the previous/next category.
|
||||
|
||||
Args:
|
||||
upwards: Get previous item, not next.
|
||||
|
||||
Return:
|
||||
A QModelIndex.
|
||||
"""
|
||||
idx = self.selectionModel().currentIndex()
|
||||
if not idx.isValid():
|
||||
return self._next_idx(upwards).sibling(0, 0)
|
||||
idx = idx.parent()
|
||||
direction = -1 if upwards else 1
|
||||
while True:
|
||||
idx = idx.sibling(idx.row() + direction, 0)
|
||||
if not idx.isValid() and upwards:
|
||||
# wrap around to the first item of the last category
|
||||
return self.model().last_item().sibling(0, 0)
|
||||
elif not idx.isValid() and not upwards:
|
||||
# wrap around to the first item of the first category
|
||||
idx = self.model().first_item()
|
||||
self.scrollTo(idx.parent())
|
||||
return idx
|
||||
elif idx.isValid() and idx.child(0, 0).isValid():
|
||||
# scroll to ensure the category is visible
|
||||
self.scrollTo(idx)
|
||||
return idx.child(0, 0)
|
||||
|
||||
@cmdutils.register(instance='completion', hide=True,
|
||||
modes=[usertypes.KeyMode.command], scope='window')
|
||||
@cmdutils.argument('which', choices=['next', 'prev'])
|
||||
@cmdutils.argument('which', choices=['next', 'prev', 'next-category',
|
||||
'prev-category'])
|
||||
def completion_item_focus(self, which):
|
||||
"""Shift the focus of the completion menu to another item.
|
||||
|
||||
Args:
|
||||
which: 'next' or 'prev'
|
||||
which: 'next', 'prev', 'next-category', or 'prev-category'.
|
||||
"""
|
||||
if not self._active:
|
||||
return
|
||||
selmodel = self.selectionModel()
|
||||
|
||||
idx = self._next_idx(which == 'prev')
|
||||
if which == 'next':
|
||||
idx = self._next_idx(upwards=False)
|
||||
elif which == 'prev':
|
||||
idx = self._next_idx(upwards=True)
|
||||
elif which == 'next-category':
|
||||
idx = self._next_category_idx(upwards=False)
|
||||
elif which == 'prev-category':
|
||||
idx = self._next_category_idx(upwards=True)
|
||||
|
||||
if not idx.isValid():
|
||||
return
|
||||
|
||||
|
@ -29,7 +29,7 @@ import functools
|
||||
|
||||
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 = {}
|
||||
@ -114,6 +114,13 @@ def init_session_completion():
|
||||
_instances[usertypes.Completion.sessions] = model
|
||||
|
||||
|
||||
def _init_bind_completion():
|
||||
"""Initialize the command completion model."""
|
||||
log.completion.debug("Initializing bind completion.")
|
||||
model = miscmodels.BindCompletionModel()
|
||||
_instances[usertypes.Completion.bind] = model
|
||||
|
||||
|
||||
INITIALIZERS = {
|
||||
usertypes.Completion.command: _init_command_completion,
|
||||
usertypes.Completion.helptopic: _init_helptopic_completion,
|
||||
@ -125,6 +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.bind: _init_bind_completion,
|
||||
}
|
||||
|
||||
|
||||
@ -155,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')
|
||||
@ -176,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',
|
||||
@ -259,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
|
||||
|
@ -24,6 +24,7 @@ are fundamentally different. This is why nothing inherits from configparser,
|
||||
but we borrow some methods and classes from there where it makes sense.
|
||||
"""
|
||||
|
||||
import re
|
||||
import os
|
||||
import sys
|
||||
import os.path
|
||||
@ -34,9 +35,11 @@ import collections
|
||||
import collections.abc
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, QObject, QUrl, QSettings
|
||||
from PyQt5.QtGui import QColor
|
||||
|
||||
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)
|
||||
@ -285,6 +288,50 @@ def _transform_position(val):
|
||||
return val
|
||||
|
||||
|
||||
def _transform_hint_color(val):
|
||||
"""Transformer for hint colors."""
|
||||
log.config.debug("Transforming hint value {}".format(val))
|
||||
|
||||
def to_rgba(qcolor):
|
||||
"""Convert a QColor to a rgba() value."""
|
||||
return 'rgba({}, {}, {}, 0.8)'.format(qcolor.red(), qcolor.green(),
|
||||
qcolor.blue())
|
||||
|
||||
if val.startswith('-webkit-gradient'):
|
||||
pattern = re.compile(r'-webkit-gradient\(linear, left top, '
|
||||
r'left bottom, '
|
||||
r'color-stop\(0%, *([^)]*)\), '
|
||||
r'color-stop\(100%, *([^)]*)\)\)')
|
||||
|
||||
match = pattern.fullmatch(val)
|
||||
if match:
|
||||
log.config.debug('Color groups: {}'.format(match.groups()))
|
||||
start_color = QColor(match.group(1))
|
||||
stop_color = QColor(match.group(2))
|
||||
if not start_color.isValid() or not stop_color.isValid():
|
||||
return None
|
||||
|
||||
return ('qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 {}, '
|
||||
'stop:1 {})'.format(to_rgba(start_color),
|
||||
to_rgba(stop_color)))
|
||||
else:
|
||||
return None
|
||||
elif val.startswith('-'): # Custom CSS stuff?
|
||||
return None
|
||||
else: # Already transformed or a named color.
|
||||
return val
|
||||
|
||||
|
||||
def _transform_hint_font(val):
|
||||
"""Transformer for fonts -> hints."""
|
||||
match = re.fullmatch(r'(.*\d+p[xt]) Monospace', val)
|
||||
if match:
|
||||
# Close enough to the old default:
|
||||
return match.group(1) + ' ${_monospace}'
|
||||
else:
|
||||
return val
|
||||
|
||||
|
||||
class ConfigManager(QObject):
|
||||
|
||||
"""Configuration manager for qutebrowser.
|
||||
@ -350,7 +397,9 @@ class ConfigManager(QObject):
|
||||
('tabs', 'auto-hide'),
|
||||
('tabs', 'hide-always'),
|
||||
('ui', 'display-statusbar-messages'),
|
||||
('ui', 'hide-mouse-cursor'),
|
||||
('general', 'wrap-search'),
|
||||
('hints', 'opacity'),
|
||||
('completion', 'auto-open'),
|
||||
]
|
||||
CHANGED_OPTIONS = {
|
||||
@ -364,6 +413,12 @@ class ConfigManager(QObject):
|
||||
_get_value_transformer({'false': 'none', 'true': 'debug'}),
|
||||
('ui', 'keyhint-blacklist'):
|
||||
_get_value_transformer({'false': '*', 'true': ''}),
|
||||
('hints', 'auto-follow'):
|
||||
_get_value_transformer({'false': 'never', 'true': 'unique-match'}),
|
||||
('colors', 'hints.bg'): _transform_hint_color,
|
||||
('colors', 'hints.fg'): _transform_hint_color,
|
||||
('colors', 'hints.fg.match'): _transform_hint_color,
|
||||
('fonts', 'hints'): _transform_hint_font,
|
||||
('completion', 'show'):
|
||||
_get_value_transformer({'false': 'never', 'true': 'always'}),
|
||||
}
|
||||
@ -524,7 +579,15 @@ class ConfigManager(QObject):
|
||||
k = self.RENAMED_OPTIONS[sectname, k]
|
||||
if (sectname, k) in self.CHANGED_OPTIONS:
|
||||
func = self.CHANGED_OPTIONS[(sectname, k)]
|
||||
v = func(v)
|
||||
new_v = func(v)
|
||||
if new_v is None:
|
||||
exc = configexc.ValidationError(
|
||||
v, "Could not automatically migrate the given value")
|
||||
exc.section = sectname
|
||||
exc.option = k
|
||||
raise exc
|
||||
|
||||
v = new_v
|
||||
|
||||
try:
|
||||
self.set('conf', sectname, k, v, validate=False)
|
||||
|
@ -227,13 +227,27 @@ 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(
|
||||
('first-opened', "Open new tabs in the first (oldest) "
|
||||
"opened window."),
|
||||
('last-opened', "Open new tabs in the last (newest) "
|
||||
"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',
|
||||
@ -302,7 +316,8 @@ def data(readonly=False):
|
||||
|
||||
('user-stylesheet',
|
||||
SettingValue(typ.UserStyleSheet(none_ok=True),
|
||||
'::-webkit-scrollbar { width: 0px; height: 0px; }',
|
||||
'html > ::-webkit-scrollbar { width: 0px; '
|
||||
'height: 0px; }',
|
||||
backends=[usertypes.Backend.QtWebKit]),
|
||||
"User stylesheet to use (absolute filename, filename relative to "
|
||||
"the config directory or CSS string). Will expand environment "
|
||||
@ -346,10 +361,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()"),
|
||||
@ -640,7 +651,8 @@ def data(readonly=False):
|
||||
('title-format',
|
||||
SettingValue(typ.FormatString(
|
||||
fields=['perc', 'perc_raw', 'title', 'title_sep', 'index',
|
||||
'id', 'scroll_pos', 'host']), '{index}: {title}'),
|
||||
'id', 'scroll_pos', 'host'], none_ok=True),
|
||||
'{index}: {title}'),
|
||||
"The format to use for the tab title. The following placeholders "
|
||||
"are defined:\n\n"
|
||||
"* `{perc}`: The percentage as a string like `[10%]`.\n"
|
||||
@ -887,10 +899,6 @@ def data(readonly=False):
|
||||
SettingValue(typ.String(), '1px solid #E3BE23'),
|
||||
"CSS border value for hints."),
|
||||
|
||||
('opacity',
|
||||
SettingValue(typ.Float(minval=0.0, maxval=1.0), '0.7'),
|
||||
"Opacity for hints."),
|
||||
|
||||
('mode',
|
||||
SettingValue(typ.String(
|
||||
valid_values=typ.ValidValues(
|
||||
@ -928,9 +936,21 @@ def data(readonly=False):
|
||||
"The dictionary file to be used by the word hints."),
|
||||
|
||||
('auto-follow',
|
||||
SettingValue(typ.Bool(), 'true'),
|
||||
"Follow a hint immediately when the hint text is completely "
|
||||
"matched."),
|
||||
SettingValue(typ.String(
|
||||
valid_values=typ.ValidValues(
|
||||
('always', "Auto-follow whenever there is only a single "
|
||||
"hint on a page."),
|
||||
('unique-match', "Auto-follow whenever there is a unique "
|
||||
"non-empty match in either the hint string (word mode) "
|
||||
"or filter (number mode)."),
|
||||
('full-match', "Follow the hint when the user typed the "
|
||||
"whole hint (letter, word or number mode) or the "
|
||||
"element's text (only in number mode)."),
|
||||
('never', "The user will always need to press Enter to "
|
||||
"follow a hint."),
|
||||
)), 'unique-match'),
|
||||
"Controls when a hint can be automatically followed without the "
|
||||
"user pressing Enter."),
|
||||
|
||||
('auto-follow-timeout',
|
||||
SettingValue(typ.Int(), '0'),
|
||||
@ -957,6 +977,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
|
||||
)),
|
||||
|
||||
@ -1183,18 +1207,18 @@ def data(readonly=False):
|
||||
"Color gradient interpolation system for the tab indicator."),
|
||||
|
||||
('hints.fg',
|
||||
SettingValue(typ.CssColor(), 'black'),
|
||||
SettingValue(typ.QssColor(), 'black'),
|
||||
"Font color for hints."),
|
||||
|
||||
('hints.bg',
|
||||
SettingValue(
|
||||
typ.CssColor(), '-webkit-gradient(linear, left top, '
|
||||
'left bottom, color-stop(0%,#FFF785), '
|
||||
'color-stop(100%,#FFC542))'),
|
||||
"Background color for hints."),
|
||||
SettingValue(typ.QssColor(), 'qlineargradient(x1:0, y1:0, x2:0, '
|
||||
'y2:1, stop:0 rgba(255, 247, 133, 0.8), '
|
||||
'stop:1 rgba(255, 197, 66, 0.8))'),
|
||||
"Background color for hints. Note that you can use a `rgba(...)` "
|
||||
"value for transparency."),
|
||||
|
||||
('hints.fg.match',
|
||||
SettingValue(typ.CssColor(), 'green'),
|
||||
SettingValue(typ.QssColor(), 'green'),
|
||||
"Font color for the matched part of hints."),
|
||||
|
||||
('downloads.bg.bar',
|
||||
@ -1267,7 +1291,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',
|
||||
@ -1283,7 +1307,7 @@ def data(readonly=False):
|
||||
"Font used for the downloadbar."),
|
||||
|
||||
('hints',
|
||||
SettingValue(typ.Font(), 'bold 13px Monospace'),
|
||||
SettingValue(typ.Font(), 'bold 13px ${_monospace}'),
|
||||
"Font used for the hints."),
|
||||
|
||||
('debug-console',
|
||||
@ -1441,7 +1465,7 @@ RETURN_KEYS = ['<Return>', '<Ctrl-M>', '<Ctrl-J>', '<Shift-Return>', '<Enter>',
|
||||
|
||||
KEY_DATA = collections.OrderedDict([
|
||||
('!normal', collections.OrderedDict([
|
||||
('clear-keychain ;; leave-mode', ['<Escape>', '<Ctrl-[>']),
|
||||
('leave-mode', ['<Escape>', '<Ctrl-[>']),
|
||||
])),
|
||||
|
||||
('normal', collections.OrderedDict([
|
||||
@ -1454,6 +1478,9 @@ KEY_DATA = collections.OrderedDict([
|
||||
('set-cmd-text :open -b -i {url:pretty}', ['xO']),
|
||||
('set-cmd-text -s :open -w', ['wo']),
|
||||
('set-cmd-text :open -w {url:pretty}', ['wO']),
|
||||
('set-cmd-text /', ['/']),
|
||||
('set-cmd-text ?', ['?']),
|
||||
('set-cmd-text :', [':']),
|
||||
('open -t', ['ga', '<Ctrl-T>']),
|
||||
('open -w', ['<Ctrl-N>']),
|
||||
('tab-close', ['d', '<Ctrl-W>']),
|
||||
@ -1512,12 +1539,12 @@ KEY_DATA = collections.OrderedDict([
|
||||
('yank domain -s', ['yD']),
|
||||
('yank pretty-url', ['yp']),
|
||||
('yank pretty-url -s', ['yP']),
|
||||
('paste', ['pp']),
|
||||
('paste -s', ['pP']),
|
||||
('paste -t', ['Pp']),
|
||||
('paste -ts', ['PP']),
|
||||
('paste -w', ['wp']),
|
||||
('paste -ws', ['wP']),
|
||||
('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']),
|
||||
@ -1574,7 +1601,7 @@ KEY_DATA = collections.OrderedDict([
|
||||
|
||||
('insert', collections.OrderedDict([
|
||||
('open-editor', ['<Ctrl-E>']),
|
||||
('paste-primary', ['<Shift-Ins>']),
|
||||
('insert-text {primary}', ['<Shift-Ins>']),
|
||||
])),
|
||||
|
||||
('hint', collections.OrderedDict([
|
||||
@ -1591,6 +1618,8 @@ KEY_DATA = collections.OrderedDict([
|
||||
('command-history-next', ['<Ctrl-N>']),
|
||||
('completion-item-focus prev', ['<Shift-Tab>', '<Up>']),
|
||||
('completion-item-focus next', ['<Tab>', '<Down>']),
|
||||
('completion-item-focus next-category', ['<Ctrl-Tab>']),
|
||||
('completion-item-focus prev-category', ['<Ctrl-Shift-Tab>']),
|
||||
('completion-item-del', ['<Ctrl-D>']),
|
||||
('command-accept', RETURN_KEYS),
|
||||
])),
|
||||
@ -1672,7 +1701,7 @@ CHANGED_KEY_COMMANDS = [
|
||||
(re.compile(r'^scroll ([-\d]+ [-\d]+)$'), r'scroll-px \1'),
|
||||
|
||||
(re.compile(r'^search *;; *clear-keychain$'), r'clear-keychain ;; search'),
|
||||
(re.compile(r'^leave-mode$'), r'clear-keychain ;; leave-mode'),
|
||||
(re.compile(r'^clear-keychain *;; *leave-mode$'), r'leave-mode'),
|
||||
|
||||
(re.compile(r'^download-remove --all$'), r'download-clear'),
|
||||
|
||||
@ -1687,6 +1716,23 @@ CHANGED_KEY_COMMANDS = [
|
||||
(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 -s$'), r'open -- {primary}'),
|
||||
(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'),
|
||||
|
||||
(re.compile(r'^open {clipboard}$'), r'open -- {clipboard}'),
|
||||
(re.compile(r'^open -([twb]) {clipboard}$'), r'open -\1 -- {clipboard}'),
|
||||
(re.compile(r'^open {primary}$'), r'open -- {primary}'),
|
||||
(re.compile(r'^open -([twb]) {primary}$'), r'open -\1 -- {primary}'),
|
||||
|
||||
(re.compile(r'^paste-primary$'), r'insert-text {primary}'),
|
||||
|
||||
(re.compile(r'^set-cmd-text -s :search$'), r'set-cmd-text /'),
|
||||
(re.compile(r'^set-cmd-text -s :search -r$'), r'set-cmd-text ?'),
|
||||
(re.compile(r'^set-cmd-text -s :$'), r'set-cmd-text :'),
|
||||
]
|
||||
|
@ -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):
|
||||
@ -1477,35 +1477,35 @@ class UserAgent(BaseType):
|
||||
def complete(self):
|
||||
"""Complete a list of common user agents."""
|
||||
out = [
|
||||
('Mozilla/5.0 (Windows NT 6.1; WOW64; rv:41.0) Gecko/20100101 '
|
||||
'Firefox/41.0',
|
||||
"Firefox 41.0 Win7 64-bit"),
|
||||
('Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:41.0) '
|
||||
'Gecko/20100101 Firefox/41.0',
|
||||
"Firefox 41.0 MacOSX"),
|
||||
('Mozilla/5.0 (X11; Linux x86_64; rv:41.0) Gecko/20100101 '
|
||||
'Firefox/41.0',
|
||||
"Firefox 41.0 Linux"),
|
||||
('Mozilla/5.0 (Windows NT 6.1; WOW64; rv:47.0) Gecko/20100101 '
|
||||
'Firefox/47.0',
|
||||
"Firefox Generic Win7"),
|
||||
('Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:47.0) '
|
||||
'Gecko/20100101 Firefox/47.0',
|
||||
"Firefox Generic MacOSX"),
|
||||
('Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:47.0) Gecko/20100101 '
|
||||
'Firefox/47.0',
|
||||
"Firefox Generic Linux"),
|
||||
|
||||
('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_1) '
|
||||
'AppleWebKit/601.2.7 (KHTML, like Gecko) Version/9.0.1 '
|
||||
'Safari/601.2.7',
|
||||
"Safari Generic MacOSX"),
|
||||
('Mozilla/5.0 (iPad; CPU OS 9_1 like Mac OS X) '
|
||||
('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) '
|
||||
'AppleWebKit/601.7.7 (KHTML, like Gecko) Version/9.1.2 '
|
||||
'Safari/601.7.7',
|
||||
"Safari Generic MacOSX"),
|
||||
('Mozilla/5.0 (iPad; CPU OS 9_3_2 like Mac OS X) '
|
||||
'AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 '
|
||||
'Mobile/13B143 Safari/601.1',
|
||||
"Mobile Safari Generic iOS"),
|
||||
'Mobile/13F69 Safari/601.1',
|
||||
"Mobile Safari 9.0 iOS"),
|
||||
|
||||
('Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, '
|
||||
'like Gecko) Chrome/46.0.2490.80 Safari/537.36',
|
||||
"Chrome 46.0 Win7 64-bit"),
|
||||
('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) '
|
||||
'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.80 '
|
||||
('Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 '
|
||||
'(KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36',
|
||||
"Chrome Generic Win10"),
|
||||
('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_5) '
|
||||
'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 '
|
||||
'Safari/537.36',
|
||||
"Chrome 46.0 MacOSX"),
|
||||
('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, '
|
||||
'like Gecko) Chrome/46.0.2490.80 Safari/537.36',
|
||||
"Chrome 46.0 Linux"),
|
||||
"Chrome Generic MacOSX"),
|
||||
('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 '
|
||||
'(KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36',
|
||||
"Chrome Generic Linux"),
|
||||
|
||||
('Mozilla/5.0 (compatible; Googlebot/2.1; '
|
||||
'+http://www.google.com/bot.html',
|
||||
@ -1517,7 +1517,7 @@ class UserAgent(BaseType):
|
||||
|
||||
('Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like '
|
||||
'Gecko',
|
||||
"IE 11.0 for Desktop Win7 64-bit")
|
||||
"IE 11.0 for Desktop Win7 64-bit")
|
||||
]
|
||||
return out
|
||||
|
||||
|
@ -153,7 +153,7 @@ class KeyConfigParser(QObject):
|
||||
@cmdutils.register(instance='key-config', maxsplit=1, no_cmd_split=True,
|
||||
no_replace_variables=True)
|
||||
@cmdutils.argument('win_id', win_id=True)
|
||||
@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):
|
||||
@ -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)
|
||||
|
38
qutebrowser/javascript/.eslintrc.yaml
Normal file
38
qutebrowser/javascript/.eslintrc.yaml
Normal file
@ -0,0 +1,38 @@
|
||||
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", "k", "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"
|
||||
sort-keys: "off"
|
||||
no-warning-comments: "off"
|
||||
max-len: ["error", {"ignoreUrls": true}]
|
@ -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,84 @@
|
||||
|
||||
"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() {
|
||||
// FIXME:qtwebengine integrate this with other window._qutebrowser code?
|
||||
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,74 @@
|
||||
* 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 - window.innerWidth) / 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 - window.innerHeight) / 100 * y;
|
||||
}
|
||||
|
||||
function _qutebrowser_scroll_pos() {
|
||||
var elem = document.documentElement;
|
||||
var dx = (elem.scrollWidth - elem.clientWidth);
|
||||
var dy = (elem.scrollHeight - elem.clientHeight);
|
||||
/*
|
||||
console.log(JSON.stringify({
|
||||
"x": x,
|
||||
"window.scrollX": window.scrollX,
|
||||
"window.innerWidth": window.innerWidth,
|
||||
"elem.scrollWidth": elem.scrollWidth,
|
||||
"x_px": x_px,
|
||||
"y": y,
|
||||
"window.scrollY": window.scrollY,
|
||||
"window.innerHeight": window.innerHeight,
|
||||
"elem.scrollHeight": elem.scrollHeight,
|
||||
"y_px": y_px,
|
||||
}));
|
||||
*/
|
||||
|
||||
var perc_x, perc_y;
|
||||
window.scroll(x_px, y_px);
|
||||
};
|
||||
|
||||
if (dx === 0) {
|
||||
perc_x = 0;
|
||||
} else {
|
||||
perc_x = 100 / dx * window.scrollX;
|
||||
}
|
||||
funcs.delta_page = function(x, y) {
|
||||
var dx = window.innerWidth * x;
|
||||
var dy = window.innerHeight * y;
|
||||
window.scrollBy(dx, dy);
|
||||
};
|
||||
|
||||
if (dy === 0) {
|
||||
perc_y = 0;
|
||||
} else {
|
||||
perc_y = 100 / dy * window.scrollY;
|
||||
}
|
||||
funcs.pos = function() {
|
||||
var elem = document.documentElement;
|
||||
var dx = elem.scrollWidth - window.innerWidth;
|
||||
var dy = elem.scrollHeight - window.innerHeight;
|
||||
var perc_x, perc_y;
|
||||
|
||||
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 (dx === 0) {
|
||||
perc_x = 0;
|
||||
} else {
|
||||
perc_x = 100 / dx * window.scrollX;
|
||||
}
|
||||
|
||||
// console.log(JSON.stringify(pos));
|
||||
return pos;
|
||||
}
|
||||
if (dy === 0) {
|
||||
perc_y = 0;
|
||||
} else {
|
||||
perc_y = 100 / dy * window.scrollY;
|
||||
}
|
||||
|
||||
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;
|
||||
})();
|
||||
|
@ -17,60 +17,135 @@
|
||||
* along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
document._qutebrowser_elements = [];
|
||||
window._qutebrowser.webelem = (function() {
|
||||
var funcs = {};
|
||||
var elements = [];
|
||||
|
||||
function serialize_elem(elem) {
|
||||
if (!elem) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function _qutebrowser_serialize_elem(elem, id) {
|
||||
var out = {
|
||||
"id": id,
|
||||
"text": elem.text,
|
||||
"tag_name": elem.tagName,
|
||||
"outer_xml": elem.outerHTML
|
||||
var id = elements.length;
|
||||
elements[id] = elem;
|
||||
|
||||
var out = {
|
||||
"id": id,
|
||||
"text": elem.text,
|
||||
"tag_name": elem.tagName,
|
||||
"outer_xml": elem.outerHTML,
|
||||
"rects": [], // Gets filled up later
|
||||
};
|
||||
|
||||
var attributes = {};
|
||||
for (var i = 0; i < elem.attributes.length; ++i) {
|
||||
var attr = elem.attributes[i];
|
||||
attributes[attr.name] = attr.value;
|
||||
}
|
||||
out.attributes = attributes;
|
||||
|
||||
var client_rects = elem.getClientRects();
|
||||
for (var k = 0; k < client_rects.length; ++k) {
|
||||
var rect = client_rects[k];
|
||||
out.rects.push({
|
||||
"top": rect.top,
|
||||
"right": rect.right,
|
||||
"bottom": rect.bottom,
|
||||
"left": rect.left,
|
||||
"height": rect.height,
|
||||
"width": rect.width,
|
||||
});
|
||||
}
|
||||
|
||||
// console.log(JSON.stringify(out));
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
function is_visible(elem) {
|
||||
// FIXME:qtwebengine Handle frames and iframes
|
||||
|
||||
// Adopted from vimperator:
|
||||
// https://github.com/vimperator/vimperator-labs/blob/vimperator-3.14.0/common/content/hints.js#L259-L285
|
||||
// FIXME:qtwebengine we might need something more sophisticated like
|
||||
// the cVim implementation here?
|
||||
// https://github.com/1995eaton/chromium-vim/blob/1.2.85/content_scripts/dom.js#L74-L134
|
||||
|
||||
var win = elem.ownerDocument.defaultView;
|
||||
var rect = elem.getBoundingClientRect();
|
||||
|
||||
if (!rect ||
|
||||
rect.top > window.innerHeight ||
|
||||
rect.bottom < 0 ||
|
||||
rect.left > window.innerWidth ||
|
||||
rect.right < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
rect = elem.getClientRects()[0];
|
||||
if (!rect) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var style = win.getComputedStyle(elem, null);
|
||||
// FIXME:qtwebengine do we need this <area> handling?
|
||||
// visibility and display style are misleading for area tags and they
|
||||
// get "display: none" by default.
|
||||
// See https://github.com/vimperator/vimperator-labs/issues/236
|
||||
if (elem.nodeName.toLowerCase() !== "area" && (
|
||||
style.getPropertyValue("visibility") !== "visible" ||
|
||||
style.getPropertyValue("display") === "none")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
funcs.find_all = function(selector, only_visible) {
|
||||
var elems = document.querySelectorAll(selector);
|
||||
var out = [];
|
||||
|
||||
for (var i = 0; i < elems.length; ++i) {
|
||||
if (!only_visible || is_visible(elems[i])) {
|
||||
out.push(serialize_elem(elems[i]));
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
};
|
||||
|
||||
var attributes = {};
|
||||
for (var i = 0; i < elem.attributes.length; ++i) {
|
||||
attr = elem.attributes[i];
|
||||
attributes[attr.name] = attr.value;
|
||||
}
|
||||
out["attributes"] = attributes;
|
||||
funcs.focus_element = function() {
|
||||
var elem = document.activeElement;
|
||||
|
||||
// console.log(JSON.stringify(out));
|
||||
if (!elem || elem === document.body) {
|
||||
// "When there is no selection, the active element is the page's
|
||||
// <body> or null."
|
||||
return null;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
return serialize_elem(elem);
|
||||
};
|
||||
|
||||
funcs.set_text = function(id, text) {
|
||||
elements[id].value = text;
|
||||
};
|
||||
|
||||
function _qutebrowser_find_all_elements(selector) {
|
||||
var elems = document.querySelectorAll(selector);
|
||||
var out = [];
|
||||
var id = document._qutebrowser_elements.length;
|
||||
funcs.element_at_pos = function(x, y) {
|
||||
// FIXME:qtwebengine
|
||||
// If the element at the specified point belongs to another document
|
||||
// (for example, an iframe's subdocument), the subdocument's parent
|
||||
// element is returned (the iframe itself).
|
||||
|
||||
for (var i = 0; i < elems.length; ++i) {
|
||||
var elem = elems[i];
|
||||
out.push(_qutebrowser_serialize_elem(elem, id));
|
||||
document._qutebrowser_elements[id] = elem;
|
||||
id++;
|
||||
}
|
||||
var elem = document.elementFromPoint(x, y);
|
||||
return serialize_elem(elem);
|
||||
};
|
||||
|
||||
return out;
|
||||
}
|
||||
funcs.element_by_id = function(id) {
|
||||
var elem = document.getElementById(id);
|
||||
return serialize_elem(elem);
|
||||
};
|
||||
|
||||
|
||||
function _qutebrowser_focus_element() {
|
||||
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 = document._qutebrowser_elements.length;
|
||||
return _qutebrowser_serialize_elem(elem, id);
|
||||
}
|
||||
|
||||
|
||||
function _qutebrowser_get_element(id) {
|
||||
return document._qutebrowser_elements[id];
|
||||
}
|
||||
return funcs;
|
||||
})();
|
||||
|
@ -131,7 +131,9 @@ class BaseKeyParser(QObject):
|
||||
except KeyError:
|
||||
self._debug_log("No special binding found for {}.".format(binding))
|
||||
return False
|
||||
self.execute(cmdstr, self.Type.special)
|
||||
count, _command = self._split_count()
|
||||
self.execute(cmdstr, self.Type.special, count)
|
||||
self.clear_keystring()
|
||||
return True
|
||||
|
||||
def _split_count(self):
|
||||
@ -193,7 +195,7 @@ class BaseKeyParser(QObject):
|
||||
if match == self.Match.definitive:
|
||||
self._debug_log("Definitive match for '{}'.".format(
|
||||
self._keystring))
|
||||
self._keystring = ''
|
||||
self.clear_keystring()
|
||||
self.execute(binding, self.Type.chain, count)
|
||||
elif match == self.Match.ambiguous:
|
||||
self._debug_log("Ambiguous match for '{}'.".format(
|
||||
@ -205,7 +207,7 @@ class BaseKeyParser(QObject):
|
||||
elif match == self.Match.none:
|
||||
self._debug_log("Giving up with '{}', no matches".format(
|
||||
self._keystring))
|
||||
self._keystring = ''
|
||||
self.clear_keystring()
|
||||
else:
|
||||
raise AssertionError("Invalid match value {!r}".format(match))
|
||||
return match
|
||||
@ -271,7 +273,7 @@ class BaseKeyParser(QObject):
|
||||
time = config.get('input', 'timeout')
|
||||
if time == 0:
|
||||
# execute immediately
|
||||
self._keystring = ''
|
||||
self.clear_keystring()
|
||||
self.execute(binding, self.Type.chain, count)
|
||||
else:
|
||||
# execute in `time' ms
|
||||
@ -289,8 +291,7 @@ class BaseKeyParser(QObject):
|
||||
command/count: As if passed to self.execute()
|
||||
"""
|
||||
self._debug_log("Executing delayed command now!")
|
||||
self._keystring = ''
|
||||
self.keystring_updated.emit(self._keystring)
|
||||
self.clear_keystring()
|
||||
self.execute(command, self.Type.chain, count)
|
||||
|
||||
def handle(self, e):
|
||||
@ -307,7 +308,9 @@ class BaseKeyParser(QObject):
|
||||
if handled or not self._supports_chains:
|
||||
return handled
|
||||
match = self._handle_single_key(e)
|
||||
self.keystring_updated.emit(self._keystring)
|
||||
# don't emit twice if the keystring was cleared in self.clear_keystring
|
||||
if self._keystring:
|
||||
self.keystring_updated.emit(self._keystring)
|
||||
return match != self.Match.none
|
||||
|
||||
def read_config(self, modename=None):
|
||||
@ -366,6 +369,8 @@ class BaseKeyParser(QObject):
|
||||
|
||||
def clear_keystring(self):
|
||||
"""Clear the currently entered key sequence."""
|
||||
self._debug_log("discarding keystring '{}'.".format(self._keystring))
|
||||
self._keystring = ''
|
||||
self.keystring_updated.emit(self._keystring)
|
||||
if self._keystring:
|
||||
self._debug_log("discarding keystring '{}'.".format(
|
||||
self._keystring))
|
||||
self._keystring = ''
|
||||
self.keystring_updated.emit(self._keystring)
|
||||
|
@ -282,6 +282,9 @@ class ModeManager(QObject):
|
||||
raise NotInModeError("Not in mode {}!".format(mode))
|
||||
log.modes.debug("Leaving mode {}{}".format(
|
||||
mode, '' if reason is None else ' (reason: {})'.format(reason)))
|
||||
# leaving a mode implies clearing keychain, see
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/1805
|
||||
self.clear_keychain()
|
||||
self.mode = usertypes.KeyMode.normal
|
||||
self.left.emit(mode, self.mode, self._win_id)
|
||||
|
||||
|
@ -25,7 +25,6 @@ Module attributes:
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, Qt
|
||||
|
||||
from qutebrowser.utils import message
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.keyinput import keyparser
|
||||
from qutebrowser.utils import usertypes, log, objreg, utils
|
||||
@ -70,9 +69,6 @@ class NormalKeyParser(keyparser.CommandKeyParser):
|
||||
self._debug_log("Ignoring key '{}', because the normal mode is "
|
||||
"currently inhibited.".format(txt))
|
||||
return self.Match.none
|
||||
if not self._keystring and any(txt == c for c in STARTCHARS):
|
||||
message.set_cmd_text(self._win_id, txt)
|
||||
return self.Match.definitive
|
||||
match = super()._handle_single_key(e)
|
||||
if match == self.Match.partial:
|
||||
timeout = config.get('input', 'partial-timeout')
|
||||
@ -189,7 +185,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)
|
||||
@ -231,7 +227,7 @@ class HintKeyParser(keyparser.CommandKeyParser):
|
||||
if keytype == self.Type.chain:
|
||||
hintmanager = objreg.get('hintmanager', scope='tab',
|
||||
window=self._win_id, tab='current')
|
||||
hintmanager.fire(cmdstr)
|
||||
hintmanager.handle_partial_key(cmdstr)
|
||||
else:
|
||||
# execute as command
|
||||
super().execute(cmdstr, keytype, count)
|
||||
|
@ -54,38 +54,62 @@ def get_window(via_ipc, force_window=False, force_tab=False,
|
||||
"""
|
||||
if force_window and force_tab:
|
||||
raise ValueError("force_window and force_tab are mutually exclusive!")
|
||||
|
||||
if not via_ipc:
|
||||
# Initial main window
|
||||
return 0
|
||||
window_to_raise = None
|
||||
|
||||
open_target = config.get('general', 'new-instance-open-target')
|
||||
|
||||
# Apply any target overrides, ordered by precedence
|
||||
if force_target is not None:
|
||||
open_target = force_target
|
||||
else:
|
||||
open_target = config.get('general', 'new-instance-open-target')
|
||||
if (open_target == 'window' or force_window) and not force_tab:
|
||||
if force_window:
|
||||
open_target = 'window'
|
||||
if force_tab and open_target == 'window':
|
||||
# Command sent via IPC
|
||||
open_target = 'tab-silent'
|
||||
|
||||
window = None
|
||||
raise_window = False
|
||||
|
||||
# Try to find the existing tab target if opening in a tab
|
||||
if open_target != 'window':
|
||||
window = get_target_window()
|
||||
raise_window = open_target not in ['tab-silent', 'tab-bg-silent']
|
||||
|
||||
# Otherwise, or if no window was found, create a new one
|
||||
if window is None:
|
||||
window = MainWindow()
|
||||
window.show()
|
||||
win_id = window.win_id
|
||||
window_to_raise = window
|
||||
else:
|
||||
try:
|
||||
window = objreg.last_window()
|
||||
except objreg.NoWindow:
|
||||
# There is no window left, so we open a new one
|
||||
window = MainWindow()
|
||||
window.show()
|
||||
win_id = window.win_id
|
||||
window_to_raise = window
|
||||
win_id = window.win_id
|
||||
if open_target not in ['tab-silent', 'tab-bg-silent']:
|
||||
window_to_raise = window
|
||||
if window_to_raise is not None:
|
||||
window_to_raise.setWindowState(
|
||||
window.windowState() & ~Qt.WindowMinimized | Qt.WindowActive)
|
||||
window_to_raise.raise_()
|
||||
window_to_raise.activateWindow()
|
||||
QApplication.instance().alert(window_to_raise)
|
||||
return win_id
|
||||
raise_window = True
|
||||
|
||||
if raise_window:
|
||||
window.setWindowState(window.windowState() & ~Qt.WindowMinimized)
|
||||
window.setWindowState(window.windowState() | Qt.WindowActive)
|
||||
window.raise_()
|
||||
window.activateWindow()
|
||||
QApplication.instance().alert(window)
|
||||
|
||||
return window.win_id
|
||||
|
||||
|
||||
def get_target_window():
|
||||
"""Get the target window for new tabs, or None if none exist."""
|
||||
try:
|
||||
win_mode = config.get('general', 'new-instance-open-target.window')
|
||||
if win_mode == 'last-focused':
|
||||
return objreg.last_focused_window()
|
||||
elif win_mode == 'first-opened':
|
||||
return objreg.window_by_index(0)
|
||||
elif win_mode == 'last-opened':
|
||||
return objreg.window_by_index(-1)
|
||||
elif win_mode == 'last-visible':
|
||||
return objreg.last_visible_window()
|
||||
else:
|
||||
raise ValueError("Invalid win_mode {}".format(win_mode))
|
||||
except objreg.NoWindow:
|
||||
return None
|
||||
|
||||
|
||||
class MainWindow(QWidget):
|
||||
@ -175,9 +199,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 +478,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))
|
||||
|
@ -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.
|
||||
@ -216,11 +217,12 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
for tab in self.widgets():
|
||||
self._remove_tab(tab)
|
||||
|
||||
def close_tab(self, tab):
|
||||
def close_tab(self, tab, *, add_undo=True):
|
||||
"""Close a tab.
|
||||
|
||||
Args:
|
||||
tab: The QWebView to be closed.
|
||||
add_undo: Whether the tab close can be undone.
|
||||
"""
|
||||
last_close = config.get('tabs', 'last-close')
|
||||
count = self.count()
|
||||
@ -228,7 +230,7 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
if last_close == 'ignore' and count == 1:
|
||||
return
|
||||
|
||||
self._remove_tab(tab)
|
||||
self._remove_tab(tab, add_undo=add_undo)
|
||||
|
||||
if count == 1: # We just closed the last tab above.
|
||||
if last_close == 'close':
|
||||
@ -242,11 +244,12 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
url = config.get('general', 'default-page')
|
||||
self.openurl(url, newtab=True)
|
||||
|
||||
def _remove_tab(self, tab):
|
||||
def _remove_tab(self, tab, *, add_undo=True):
|
||||
"""Remove a tab from the tab list and delete it properly.
|
||||
|
||||
Args:
|
||||
tab: The QWebView to be closed.
|
||||
add_undo: Whether the tab close can be undone.
|
||||
"""
|
||||
idx = self.indexOf(tab)
|
||||
if idx == -1:
|
||||
@ -260,8 +263,9 @@ class TabbedBrowser(tabwidget.TabWidget):
|
||||
window=self._win_id)
|
||||
if tab.url().isValid():
|
||||
history_data = tab.history.serialize()
|
||||
entry = UndoEntry(tab.url(), history_data)
|
||||
self._undo_stack.append(entry)
|
||||
if add_undo:
|
||||
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
|
||||
# (target="_blank" with a download, see [1]), so we silently ignore
|
||||
@ -297,13 +301,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 +346,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 +362,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 +381,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:
|
||||
|
@ -66,6 +66,9 @@ class TabWidget(QTabWidget):
|
||||
@config.change_filter('tabs')
|
||||
def init_config(self):
|
||||
"""Initialize attributes based on the config."""
|
||||
if self is None: # pragma: no cover
|
||||
# WORKAROUND for PyQt 5.2
|
||||
return
|
||||
tabbar = self.tabBar()
|
||||
self.setMovable(config.get('tabs', 'movable'))
|
||||
self.setTabsClosable(False)
|
||||
@ -103,7 +106,8 @@ class TabWidget(QTabWidget):
|
||||
fields['index'] = idx + 1
|
||||
|
||||
fmt = config.get('tabs', 'title-format')
|
||||
self.tabBar().setTabText(idx, fmt.format(**fields))
|
||||
title = '' if fmt is None else fmt.format(**fields)
|
||||
self.tabBar().setTabText(idx, title)
|
||||
|
||||
def get_tab_fields(self, idx):
|
||||
"""Get the tab field data."""
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -22,6 +22,7 @@
|
||||
import functools
|
||||
import types
|
||||
import traceback
|
||||
import logging
|
||||
|
||||
try:
|
||||
import hunter
|
||||
@ -247,3 +248,40 @@ def log_capacity(capacity: int):
|
||||
raise cmdexc.CommandError("Can't set a negative log capacity!")
|
||||
else:
|
||||
log.ram_handler.change_log_capacity(capacity)
|
||||
|
||||
|
||||
@cmdutils.register(debug=True)
|
||||
@cmdutils.argument('level', choices=sorted(
|
||||
(level.lower() for level in log.LOG_LEVELS),
|
||||
key=lambda e: log.LOG_LEVELS[e.upper()]))
|
||||
def debug_log_level(level: str):
|
||||
"""Change the log level for console logging.
|
||||
|
||||
Args:
|
||||
level: The log level to set.
|
||||
"""
|
||||
log.console_handler.setLevel(log.LOG_LEVELS[level.upper()])
|
||||
|
||||
|
||||
@cmdutils.register(debug=True)
|
||||
def debug_log_filter(filters: str):
|
||||
"""Change the log filter for console logging.
|
||||
|
||||
Args:
|
||||
filters: A comma separated list of logger names.
|
||||
"""
|
||||
if set(filters.split(',')).issubset(log.LOGGER_NAMES):
|
||||
log.console_filter.names = filters.split(',')
|
||||
else:
|
||||
raise cmdexc.CommandError("filters: Invalid value {} - expected one "
|
||||
"of: {}".format(filters,
|
||||
', '.join(log.LOGGER_NAMES)))
|
||||
|
||||
|
||||
@cmdutils.register()
|
||||
@cmdutils.argument('current_win_id', win_id=True)
|
||||
def window_only(current_win_id):
|
||||
"""Close all windows except for the current one."""
|
||||
for win_id, window in objreg.window_registry.items():
|
||||
if win_id != current_win_id:
|
||||
window.close()
|
||||
|
@ -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,6 +52,8 @@ def _convert_js_arg(arg):
|
||||
return 'undefined'
|
||||
elif isinstance(arg, str):
|
||||
return '"{}"'.format(string_escape(arg))
|
||||
elif isinstance(arg, bool):
|
||||
return str(arg).lower()
|
||||
elif isinstance(arg, (int, float)):
|
||||
return str(arg)
|
||||
else:
|
||||
@ -62,11 +61,12 @@ def _convert_js_arg(arg):
|
||||
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
|
||||
|
@ -87,6 +87,15 @@ LOG_LEVELS = {
|
||||
'CRITICAL': logging.CRITICAL,
|
||||
}
|
||||
|
||||
LOGGER_NAMES = [
|
||||
'statusbar', 'completion', 'init', 'url',
|
||||
'destroy', 'modes', 'webview', 'misc',
|
||||
'mouse', 'procs', 'hints', 'keyboard',
|
||||
'commands', 'signals', 'downloads',
|
||||
'js', 'qt', 'rfc6266', 'ipc', 'shlexer',
|
||||
'save', 'message', 'config', 'sessions'
|
||||
]
|
||||
|
||||
|
||||
def vdebug(self, msg, *args, **kwargs):
|
||||
"""Log with a VDEBUG level.
|
||||
@ -131,6 +140,8 @@ sessions = logging.getLogger('sessions')
|
||||
|
||||
|
||||
ram_handler = None
|
||||
console_handler = None
|
||||
console_filter = None
|
||||
|
||||
|
||||
def stub(suffix=''):
|
||||
@ -149,6 +160,7 @@ class CriticalQtWarning(Exception):
|
||||
|
||||
def init_log(args):
|
||||
"""Init loggers based on the argparse namespace passed."""
|
||||
global console
|
||||
level = args.loglevel.upper()
|
||||
try:
|
||||
numeric_level = getattr(logging, level)
|
||||
@ -161,9 +173,11 @@ def init_log(args):
|
||||
console, ram = _init_handlers(numeric_level, args.color, args.force_color,
|
||||
args.json_logging, args.loglines)
|
||||
root = logging.getLogger()
|
||||
global console_filter
|
||||
if console is not None:
|
||||
if args.logfilter is not None:
|
||||
console.addFilter(LogFilter(args.logfilter.split(',')))
|
||||
console_filter = LogFilter(args.logfilter.split(','))
|
||||
console.addFilter(console_filter)
|
||||
root.addHandler(console)
|
||||
if ram is not None:
|
||||
root.addHandler(ram)
|
||||
@ -175,6 +189,10 @@ def init_log(args):
|
||||
_log_inited = True
|
||||
|
||||
|
||||
def change(filters):
|
||||
console.addFilter(LogFilter(filters.split(',')))
|
||||
|
||||
|
||||
def _init_py_warnings():
|
||||
"""Initialize Python warning handling."""
|
||||
warnings.simplefilter('default')
|
||||
@ -210,6 +228,7 @@ def _init_handlers(level, color, force_color, json_logging, ram_capacity):
|
||||
json_logging: Output log lines in JSON (this disables all colors).
|
||||
"""
|
||||
global ram_handler
|
||||
global console_handler
|
||||
console_fmt, ram_fmt, html_fmt, use_colorama = _init_formatters(
|
||||
level, color, force_color, json_logging)
|
||||
|
||||
@ -448,16 +467,16 @@ class LogFilter(logging.Filter):
|
||||
|
||||
def __init__(self, names):
|
||||
super().__init__()
|
||||
self._names = names
|
||||
self.names = names
|
||||
|
||||
def filter(self, record):
|
||||
"""Determine if the specified record is to be logged."""
|
||||
if self._names is None:
|
||||
if self.names is None:
|
||||
return True
|
||||
if record.levelno > logging.DEBUG:
|
||||
# More important than DEBUG, so we won't filter at all
|
||||
return True
|
||||
for name in self._names:
|
||||
for name in self.names:
|
||||
if record.name == name:
|
||||
return True
|
||||
elif not record.name.startswith(name):
|
||||
|
@ -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,10 +285,26 @@ def dump_objects():
|
||||
return lines
|
||||
|
||||
|
||||
def last_window():
|
||||
"""Get the last opened window object."""
|
||||
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 window_by_index(-1)
|
||||
|
||||
|
||||
def window_by_index(idx):
|
||||
"""Get the Nth opened window object."""
|
||||
if not window_registry:
|
||||
raise NoWindow()
|
||||
else:
|
||||
key = sorted(window_registry)[-1]
|
||||
key = sorted(window_registry)[idx]
|
||||
return window_registry[key]
|
||||
|
@ -499,7 +499,7 @@ class IncDecError(Exception):
|
||||
return '{}: {}'.format(self.msg, self.url.toString())
|
||||
|
||||
|
||||
def _get_incdec_value(match, incdec, url):
|
||||
def _get_incdec_value(match, incdec, url, count):
|
||||
"""Get an incremented/decremented URL based on a URL match."""
|
||||
pre, zeroes, number, post = match.groups()
|
||||
# This should always succeed because we match \d+
|
||||
@ -507,9 +507,9 @@ def _get_incdec_value(match, incdec, url):
|
||||
if incdec == 'decrement':
|
||||
if val <= 0:
|
||||
raise IncDecError("Can't decrement {}!".format(val), url)
|
||||
val -= 1
|
||||
val -= count
|
||||
elif incdec == 'increment':
|
||||
val += 1
|
||||
val += count
|
||||
else:
|
||||
raise ValueError("Invalid value {} for indec!".format(incdec))
|
||||
if zeroes:
|
||||
@ -521,12 +521,13 @@ def _get_incdec_value(match, incdec, url):
|
||||
return ''.join([pre, zeroes, str(val), post])
|
||||
|
||||
|
||||
def incdec_number(url, incdec, segments=None):
|
||||
def incdec_number(url, incdec, count=1, segments=None):
|
||||
"""Find a number in the url and increment or decrement it.
|
||||
|
||||
Args:
|
||||
url: The current url
|
||||
incdec: Either 'increment' or 'decrement'
|
||||
count: The number to increment or decrement by
|
||||
segments: A set of URL segments to search. Valid segments are:
|
||||
'host', 'path', 'query', 'anchor'.
|
||||
Default: {'path', 'query'}
|
||||
@ -566,7 +567,7 @@ def incdec_number(url, incdec, segments=None):
|
||||
if not match:
|
||||
continue
|
||||
|
||||
setter(_get_incdec_value(match, incdec, url))
|
||||
setter(_get_incdec_value(match, incdec, url, count))
|
||||
return url
|
||||
|
||||
raise IncDecError("No number found in URL!", url)
|
||||
|
@ -226,7 +226,8 @@ PromptMode = enum('PromptMode', ['yesno', 'text', 'user_pwd', 'alert',
|
||||
|
||||
|
||||
# Where to open a clicked link.
|
||||
ClickTarget = enum('ClickTarget', ['normal', 'tab', 'tab_bg', 'window'])
|
||||
ClickTarget = enum('ClickTarget', ['normal', 'tab', 'tab_bg', 'window',
|
||||
'hover'])
|
||||
|
||||
|
||||
# Key input modes
|
||||
@ -238,7 +239,8 @@ KeyMode = enum('KeyMode', ['normal', 'hint', 'command', 'yesno', 'prompt',
|
||||
# Available command completions
|
||||
Completion = enum('Completion', ['command', 'section', 'option', 'value',
|
||||
'helptopic', 'quickmark_by_name',
|
||||
'bookmark_by_url', 'url', 'tab', 'sessions'])
|
||||
'bookmark_by_url', 'url', 'tab', 'sessions',
|
||||
'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',
|
||||
|
@ -56,21 +56,19 @@ def whitelist_generator():
|
||||
yield 'qutebrowser.mainwindow.statusbar.url.UrlText.urltype'
|
||||
|
||||
# Not used yet, but soon (or when debugging)
|
||||
yield 'qutebrowser.config.configtypes.Regex'
|
||||
yield 'qutebrowser.utils.debug.log_events'
|
||||
yield 'qutebrowser.utils.debug.log_signals'
|
||||
yield 'qutebrowser.utils.debug.qflags_key'
|
||||
yield 'qutebrowser.utils.qtutils.QtOSError.qt_errno'
|
||||
yield 'qutebrowser.utils.usertypes.NeighborList.firstitem'
|
||||
yield 'scripts.utils.bg_colors'
|
||||
yield 'scripts.utils.print_subtitle'
|
||||
yield 'qutebrowser.browser.webelem.AbstractWebElement.style_property'
|
||||
yield 'qutebrowser.config.configtypes.Float'
|
||||
|
||||
# Qt attributes
|
||||
yield 'PyQt5.QtWebKit.QWebPage.ErrorPageExtensionReturn().baseUrl'
|
||||
yield 'PyQt5.QtWebKit.QWebPage.ErrorPageExtensionReturn().content'
|
||||
yield 'PyQt5.QtWebKit.QWebPage.ErrorPageExtensionReturn().encoding'
|
||||
yield 'PyQt5.QtWebKit.QWebPage.ErrorPageExtensionReturn().fileNames'
|
||||
yield 'PyQt5.QtGui.QAbstractTextDocumentLayout.PaintContext().clip'
|
||||
yield 'PyQt5.QtWidgets.QStyleOptionViewItem.backgroundColor'
|
||||
|
||||
# qute:... handlers
|
||||
@ -81,9 +79,7 @@ def whitelist_generator():
|
||||
yield ('qutebrowser.completion.models.sortfilter.CompletionFilterModel().'
|
||||
'lessThan')
|
||||
yield 'qutebrowser.utils.jinja.Loader.get_source'
|
||||
yield 'qutebrowser.utils.log.VDEBUG'
|
||||
yield 'qutebrowser.utils.log.QtWarningFilter.filter'
|
||||
yield 'logging.LogRecord.log_color'
|
||||
yield 'qutebrowser.browser.pdfjs.is_available'
|
||||
# vulture doesn't notice the hasattr() and thus thinks netrc_used is unused
|
||||
# in NetworkManager.on_authentication_required
|
||||
|
@ -37,11 +37,18 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir,
|
||||
# We import qutebrowser.app so all @cmdutils-register decorators are run.
|
||||
import qutebrowser.app
|
||||
from scripts import asciidoc2html, utils
|
||||
from qutebrowser import qutebrowser
|
||||
from qutebrowser import qutebrowser, commands
|
||||
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):
|
||||
|
||||
@ -312,18 +319,22 @@ def _format_action(action):
|
||||
def generate_commands(filename):
|
||||
"""Generate the complete commands section."""
|
||||
with _open_file(filename) as f:
|
||||
f.write("= Commands\n")
|
||||
f.write(FILE_HEADER)
|
||||
f.write("= Commands\n\n")
|
||||
f.write(commands.__doc__)
|
||||
normal_cmds = []
|
||||
hidden_cmds = []
|
||||
debug_cmds = []
|
||||
for name, cmd in cmdutils.cmd_dict.items():
|
||||
if name in cmdutils.aliases:
|
||||
continue
|
||||
if cmd.deprecated:
|
||||
continue
|
||||
if cmd.hide:
|
||||
hidden_cmds.append((name, cmd))
|
||||
elif cmd.debug:
|
||||
debug_cmds.append((name, cmd))
|
||||
elif not cmd.deprecated:
|
||||
else:
|
||||
normal_cmds.append((name, cmd))
|
||||
normal_cmds.sort()
|
||||
hidden_cmds.sort()
|
||||
@ -395,6 +406,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():
|
||||
|
@ -105,7 +105,7 @@ def main():
|
||||
tab = " "
|
||||
print(tab + "def complete(self):")
|
||||
print((2 * tab) + "\"\"\"Complete a list of common user agents.\"\"\"")
|
||||
print((2 * tab) + "%sout = [")
|
||||
print((2 * tab) + "out = [")
|
||||
|
||||
for browser in ["Firefox", "Safari", "Chrome", "Obscure"]:
|
||||
for it in filtered[browser]:
|
||||
|
@ -184,13 +184,12 @@ def pytest_sessionfinish(exitstatus):
|
||||
|
||||
|
||||
if not getattr(sys, 'frozen', False):
|
||||
def pytest_bdd_apply_tag(tag, function):
|
||||
def _get_version_tag(tag):
|
||||
"""Handle tags like pyqt>=5.3.1 for BDD tests.
|
||||
|
||||
This transforms e.g. pyqt>=5.3.1 into an appropriate @pytest.mark.skip
|
||||
marker, and falls back to pytest-bdd's implementation for all other
|
||||
casesinto an appropriate @pytest.mark.skip marker, and falls back to
|
||||
pytest-bdd's implementation for all other cases
|
||||
"""
|
||||
version_re = re.compile(r"""
|
||||
(?P<package>qt|pyqt)
|
||||
@ -200,7 +199,6 @@ if not getattr(sys, 'frozen', False):
|
||||
|
||||
match = version_re.match(tag)
|
||||
if not match:
|
||||
# Use normal tag mapping
|
||||
return None
|
||||
|
||||
operators = {
|
||||
@ -217,15 +215,37 @@ if not getattr(sys, 'frozen', False):
|
||||
version = match.group('version')
|
||||
|
||||
if package == 'qt':
|
||||
mark = pytest.mark.skipif(qtutils.version_check(version, op),
|
||||
return pytest.mark.skipif(qtutils.version_check(version, op),
|
||||
reason='Needs ' + tag)
|
||||
elif package == 'pyqt':
|
||||
major, minor, patch = [int(e) for e in version.split('.')]
|
||||
hex_version = (major << 16) | (minor << 8) | patch
|
||||
mark = pytest.mark.skipif(not op(PYQT_VERSION, hex_version),
|
||||
return pytest.mark.skipif(not op(PYQT_VERSION, hex_version),
|
||||
reason='Needs ' + tag)
|
||||
else:
|
||||
raise ValueError("Invalid package {!r}".format(package))
|
||||
|
||||
mark(function)
|
||||
return True
|
||||
def _get_qtwebengine_tag(tag):
|
||||
"""Handle a @qtwebengine_* tag."""
|
||||
pytest_marks = {
|
||||
'qtwebengine_todo': pytest.mark.qtwebengine_todo,
|
||||
'qtwebengine_skip': pytest.mark.qtwebengine_skip,
|
||||
}
|
||||
if not any(tag.startswith(t + ':') for t in pytest_marks):
|
||||
return None
|
||||
name, desc = tag.split(':', maxsplit=1)
|
||||
return pytest_marks[name](desc)
|
||||
|
||||
def pytest_bdd_apply_tag(tag, function):
|
||||
"""Handle custom tags for BDD tests.
|
||||
|
||||
This tries various functions, and if none knows how to handle this tag,
|
||||
it returns None so it falls back to pytest-bdd's implementation.
|
||||
"""
|
||||
funcs = [_get_version_tag, _get_qtwebengine_tag]
|
||||
for func in funcs:
|
||||
mark = func(tag)
|
||||
if mark is not None:
|
||||
mark(function)
|
||||
return True
|
||||
return None
|
||||
|
@ -3,9 +3,11 @@
|
||||
<title>quteprocess.click_element test</title>
|
||||
</head>
|
||||
<body>
|
||||
<span onclick='console.log("click_element clicked")'>Test Element</span>
|
||||
<span id='test' onclick='console.log("click_element clicked")'>Test Element</span>
|
||||
<span onclick='console.log("click_element special chars")'>"Don't", he shouted</span>
|
||||
<span>Duplicate</span>
|
||||
<span>Duplicate</span>
|
||||
<form><input id='qute-input'></input></form>
|
||||
<a href="/data/hello.txt" id='link'>link</a>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -5,7 +5,7 @@
|
||||
<body>
|
||||
<p>Using <code>:prompt-open-download</code> with a file that has a loooooong filename</p>
|
||||
<p>
|
||||
<a href="/response-headers?Content-Disposition=download;%20filename%2A%3DUTF-8%27%27I%2527ve%2520actually%2520felt%2520slightly%2520uncomfortable%2520at%2520TED%2520for%2520the%2520last%2520two%2520days%252C%2520because%2520there%2527s%2520a%2520lot%2520of%2520vision%2520going%2520on%252C%2520right%253F%2520And%2520I%2520am%2520not%2520a%2520visionary.%2520I%2520do%2520not%2520have%2520a%2520five-year%2520plan.%2520I%2527m%2520an%2520engineer.%2520And%2520I%2520think%2520it%2527s%2520really%2520--%2520I%2520mean%2520--%2520I%2527m%2520perfectly%2520happy%2520with%2520all%2520the%2520people%2520who%2520are%2520walking%2520around%2520and%2520just%2520staring%2520at%2520the%2520clouds%2520and%2520looking%2520at%2520the%2520stars%2520and%2520saying%252C%2520%2522I%2520want%2520to%2520go%2520there.%2522%2520But%2520I%2527m%2520looking%2520at%2520the%2520ground%252C%2520and%2520I%2520want%2520to%2520fix%2520the%2520pothole%2520that%2527s%2520right%2520in%2520front%2520of%2520me%2520before%2520I%2520fall%2520in.%2520This%2520is%2520the%2520kind%2520of%2520person%2520I%2520am.txt">
|
||||
<a href="/response-headers?Content-Disposition=download;%20filename%2A%3DUTF-8%27%27I%2527ve%2520actually%2520felt%2520slightly%2520uncomfortable%2520at%2520TED%2520for%2520the%2520last%2520two%2520days%252C%2520because%2520there%2527s%2520a%2520lot%2520of%2520vision%2520going%2520on%252C%2520right%253F%2520And%2520I%2520am%2520not%2520a%2520visionary.%2520I%2520do%2520not%2520have%2520a%2520five-year%2520plan.%2520I%2527m%2520an%2520engineer.%2520And%2520I%2520think%2520it%2527s%2520really%2520--%2520I%2520mean%2520--%2520I%2527m%2520perfectly%2520happy%2520with%2520all%2520the%2520people%2520who%2520are%2520walking%2520around%2520and%2520just%2520staring%2520at%2520the%2520clouds%2520and%2520looking%2520at%2520the%2520stars%2520and%2520saying%252C%2520%2522I%2520want%2520to%2520go%2520there.%2522%2520But%2520I%2527m%2520looking%2520at%2520the%2520ground%252C%2520and%2520I%2520want%2520to%2520fix%2520the%2520pothole%2520that%2527s%2520right%2520in%2520front%2520of%2520me%2520before%2520I%2520fall%2520in.%2520This%2520is%2520the%2520kind%2520of%2520person%2520I%2520am.txt" id="long-link">
|
||||
Download me!
|
||||
</a>
|
||||
</p>
|
||||
|
@ -12,6 +12,6 @@
|
||||
</head>
|
||||
<body>
|
||||
<textarea id="qute-textarea"></textarea>
|
||||
<input type="button" onclick="log_text()" value="Log text">
|
||||
<input type="button" id="qute-button" onclick="log_text()" value="Log text">
|
||||
</body>
|
||||
</html>
|
||||
|
@ -8,6 +8,6 @@
|
||||
<title>Simple link</title>
|
||||
</head>
|
||||
<body>
|
||||
<a href="/data/hello.txt">Follow me!</a>
|
||||
<a href="/data/hello.txt" id="link">Follow me!</a>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,6 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<!-- target: hello.txt -->
|
||||
<!--
|
||||
target: hello.txt
|
||||
qtwebengine_todo: Doesn't seem to work?
|
||||
-->
|
||||
|
||||
<html>
|
||||
<head>
|
||||
|
@ -1,6 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<!-- target: hello.txt -->
|
||||
<!--
|
||||
target: hello.txt
|
||||
qtwebengine_todo: Doesn't seem to work?
|
||||
-->
|
||||
|
||||
<html>
|
||||
<head>
|
||||
|
@ -6,6 +6,6 @@
|
||||
<title>Scrolling inside an iframe</title>
|
||||
</head>
|
||||
<body>
|
||||
<iframe style="margin: 50px;" src="/data/scroll.html"></iframe>
|
||||
<iframe style="margin: 50px;" src="/data/scroll/simple.html"></iframe>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -6,6 +6,6 @@
|
||||
<title>Simple input</title>
|
||||
</head>
|
||||
<body>
|
||||
<form><input></input></form>
|
||||
<form><input id="qute-input"></input></form>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -5,6 +5,6 @@
|
||||
<title>A link to use hints on</title>
|
||||
</head>
|
||||
<body>
|
||||
<a href="/data/hello.txt" target="_blank">Follow me!</a>
|
||||
<a href="/data/hello.txt" target="_blank" id="link">Follow me!</a>
|
||||
</body>
|
||||
</html>
|
||||
|
24
tests/end2end/data/hints/short_dict.html
Normal file
24
tests/end2end/data/hints/short_dict.html
Normal file
@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Many links</title>
|
||||
</head>
|
||||
<body>
|
||||
<a href="/data/numbers/1.txt">1</a>
|
||||
<a href="/data/numbers/2.txt">2</a>
|
||||
<a href="/data/numbers/3.txt">3</a>
|
||||
<a href="/data/numbers/4.txt">4</a>
|
||||
<a href="/data/numbers/5.txt">5</a>
|
||||
<a href="/data/numbers/6.txt">6</a>
|
||||
<a href="/data/numbers/7.txt">7</a>
|
||||
<a href="/data/numbers/8.txt">8</a>
|
||||
<a href="/data/numbers/9.txt">9</a>
|
||||
<a href="/data/numbers/10.txt">10</a>
|
||||
<a href="/data/numbers/11.txt">11</a>
|
||||
<a href="/data/numbers/12.txt">12</a>
|
||||
<a href="/data/numbers/13.txt">13</a>
|
||||
<a href="/data/numbers/14.txt">14</a>
|
||||
</body>
|
||||
</html>
|
@ -2,8 +2,8 @@
|
||||
<html>
|
||||
<body>
|
||||
|
||||
<button onclick="openWin()">Open "myWindow"</button>
|
||||
<button onclick="closeWin()">Close "myWindow"</button>
|
||||
<button onclick="openWin()" id="open-button">Open "myWindow"</button>
|
||||
<button onclick="closeWin()" id="close-button">Close "myWindow"</button>
|
||||
|
||||
<script>
|
||||
var myWindow;
|
||||
|
@ -7,7 +7,7 @@
|
||||
<body>
|
||||
<h1 id="top">Top</h1>
|
||||
<a href="#top">Top</a>
|
||||
<a href="#bottom">Bottom</a>
|
||||
<a href="#bottom" id="bottom">Bottom</a>
|
||||
<div style="height: 3000px; width: 3000px;">Holy Grail</div>
|
||||
<div style="height: 3000px; width: 3000px;">Waldo</div>
|
||||
<div style="height: 3000px; width: 3000px;">Holy Grail</div>
|
||||
|
@ -33,6 +33,6 @@
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<input type="button" onclick="get_location()" value="Get position">
|
||||
<input type="button" onclick="get_location()" value="Get position" id="button">
|
||||
</body>
|
||||
</html>
|
||||
|
@ -10,6 +10,6 @@
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<input type="button" onclick="do_alert()" value="Show alert">
|
||||
<input type="button" onclick="do_alert()" value="Show alert" id="button">
|
||||
</body>
|
||||
</html>
|
||||
|
@ -10,6 +10,6 @@
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<input type="button" onclick="prompter()" value="Show prompt">
|
||||
<input type="button" onclick="prompter()" value="Show prompt" id="button">
|
||||
</body>
|
||||
</html>
|
||||
|
@ -10,6 +10,6 @@
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<input type="button" onclick="prompter()" value="Show prompt">
|
||||
<input type="button" onclick="prompter()" value="Show prompt" id="button">
|
||||
</body>
|
||||
</html>
|
||||
|
@ -34,6 +34,6 @@
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<input type="button" onclick="get_notification_permission()" value="Get notification permission">
|
||||
<input type="button" onclick="get_notification_permission()" value="Get notification permission" id="button">
|
||||
</body>
|
||||
</html>
|
||||
|
214
tests/end2end/data/scroll/no_doctype.html
Normal file
214
tests/end2end/data/scroll/no_doctype.html
Normal file
@ -0,0 +1,214 @@
|
||||
<!-- This is the same as scroll.html but without <!DOCTYPE html> -->
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Scrolling without doctype</title>
|
||||
</head>
|
||||
<body>
|
||||
<pre>
|
||||
0
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
6
|
||||
7
|
||||
8
|
||||
9
|
||||
10
|
||||
11
|
||||
12
|
||||
13
|
||||
14
|
||||
15
|
||||
16
|
||||
17
|
||||
18
|
||||
19
|
||||
20
|
||||
21
|
||||
22
|
||||
23
|
||||
24
|
||||
25
|
||||
26
|
||||
27
|
||||
28
|
||||
29
|
||||
30
|
||||
31
|
||||
32
|
||||
33
|
||||
34
|
||||
35
|
||||
36
|
||||
37
|
||||
38
|
||||
39
|
||||
40
|
||||
41
|
||||
42
|
||||
43
|
||||
44
|
||||
45
|
||||
46
|
||||
47
|
||||
48
|
||||
49
|
||||
50
|
||||
51
|
||||
52
|
||||
53
|
||||
54
|
||||
55
|
||||
56
|
||||
57
|
||||
58
|
||||
59
|
||||
60
|
||||
61
|
||||
62
|
||||
63
|
||||
64
|
||||
65
|
||||
66
|
||||
67
|
||||
68
|
||||
69
|
||||
70
|
||||
71
|
||||
72
|
||||
73
|
||||
74
|
||||
75
|
||||
76
|
||||
77
|
||||
78
|
||||
79
|
||||
80
|
||||
81
|
||||
82
|
||||
83
|
||||
84
|
||||
85
|
||||
86
|
||||
87
|
||||
88
|
||||
89
|
||||
90
|
||||
91
|
||||
92
|
||||
93
|
||||
94
|
||||
95
|
||||
96
|
||||
97
|
||||
98
|
||||
99
|
||||
100
|
||||
101
|
||||
102
|
||||
103
|
||||
104
|
||||
105
|
||||
106
|
||||
107
|
||||
108
|
||||
109
|
||||
110
|
||||
111
|
||||
112
|
||||
113
|
||||
114
|
||||
115
|
||||
116
|
||||
117
|
||||
118
|
||||
119
|
||||
120
|
||||
121
|
||||
122
|
||||
123
|
||||
124
|
||||
125
|
||||
126
|
||||
127
|
||||
128
|
||||
129
|
||||
130
|
||||
131
|
||||
132
|
||||
133
|
||||
134
|
||||
135
|
||||
136
|
||||
137
|
||||
138
|
||||
139
|
||||
140
|
||||
141
|
||||
142
|
||||
143
|
||||
144
|
||||
145
|
||||
146
|
||||
147
|
||||
148
|
||||
149
|
||||
150
|
||||
151
|
||||
152
|
||||
153
|
||||
154
|
||||
155
|
||||
156
|
||||
157
|
||||
158
|
||||
159
|
||||
160
|
||||
161
|
||||
162
|
||||
163
|
||||
164
|
||||
165
|
||||
166
|
||||
167
|
||||
168
|
||||
169
|
||||
170
|
||||
171
|
||||
172
|
||||
173
|
||||
174
|
||||
175
|
||||
176
|
||||
177
|
||||
178
|
||||
179
|
||||
180
|
||||
181
|
||||
182
|
||||
183
|
||||
184
|
||||
185
|
||||
186
|
||||
187
|
||||
188
|
||||
189
|
||||
190
|
||||
191
|
||||
192
|
||||
193
|
||||
194
|
||||
195
|
||||
196
|
||||
197
|
||||
198
|
||||
199
|
||||
This is a very long line so this page can be scrolled horizontally. Did you think this line would end here already? Nah, it does not. But now it will. Or will it? I think it's not long enough yet. But depending on your screen size, this is not even enough yet, so I'm typing some more gibberish here. Hopefully that helps. I'm glad if it did, I'm always happy to help. Really, you're welcome. Okay, okay, can I stop now?
|
||||
</pre>
|
||||
<a href="/data/hello2.txt">next</a> link to test the --top-navigate argument for :scroll-page.
|
||||
<a href="/data/hello3.txt">prev</a> link to test the --bottom-navigate argument for :scroll-page.
|
||||
</body>
|
||||
</html>
|
@ -11,12 +11,7 @@ Feature: Going back and forward.
|
||||
And I run :forward
|
||||
And I wait until data/backforward/2.txt is loaded
|
||||
And I reload
|
||||
Then the requests should be:
|
||||
data/backforward/1.txt
|
||||
data/backforward/2.txt
|
||||
data/backforward/1.txt
|
||||
data/backforward/2.txt
|
||||
And the session should look like:
|
||||
Then the session should look like:
|
||||
windows:
|
||||
- tabs:
|
||||
- history:
|
||||
@ -24,6 +19,7 @@ Feature: Going back and forward.
|
||||
- active: true
|
||||
url: http://localhost:*/data/backforward/2.txt
|
||||
|
||||
@qtwebengine_todo: FIXME why is this broken?
|
||||
Scenario: Going back in a new tab
|
||||
Given I open data/backforward/1.txt
|
||||
When I open data/backforward/2.txt
|
||||
@ -92,6 +88,7 @@ Feature: Going back and forward.
|
||||
- url: http://localhost:*/data/backforward/2.txt
|
||||
- url: http://localhost:*/data/backforward/3.txt
|
||||
|
||||
@qtwebengine_skip: Causes 'Ignoring invalid URL being added to history' sometimes?
|
||||
Scenario: Going back too much with count.
|
||||
Given I open data/backforward/1.txt
|
||||
When I open data/backforward/2.txt
|
||||
@ -107,8 +104,9 @@ Feature: Going back and forward.
|
||||
Then the error "At beginning of history." should be shown
|
||||
And the message "Still alive!" should be shown
|
||||
|
||||
@qtwebengine_skip: flaky for some reason?
|
||||
Scenario: Going back in a new window
|
||||
Given I have a fresh instance
|
||||
Given I clean up open tabs
|
||||
When I open data/backforward/1.txt
|
||||
And I open data/backforward/2.txt
|
||||
And I run :back -w
|
||||
@ -140,6 +138,7 @@ Feature: Going back and forward.
|
||||
When I run :forward
|
||||
Then the error "At end of history." should be shown
|
||||
|
||||
@qtwebengine_skip: Causes 'Ignoring invalid URL being added to history' sometimes?
|
||||
Scenario: Going forward too much with count.
|
||||
Given I open data/backforward/1.txt
|
||||
When I open data/backforward/2.txt
|
||||
|
@ -35,6 +35,27 @@ from qutebrowser.utils import log
|
||||
from helpers import utils
|
||||
|
||||
|
||||
def pytest_collection_modifyitems(config, items):
|
||||
"""Apply @qtwebengine_* markers."""
|
||||
webengine = config.getoption('--qute-bdd-webengine')
|
||||
|
||||
markers = {
|
||||
'qtwebengine_todo': ('QtWebEngine TODO', pytest.mark.xfail),
|
||||
'qtwebengine_skip': ('Skipped with QtWebEngine', pytest.mark.skipif),
|
||||
}
|
||||
|
||||
for item in items:
|
||||
for name, (prefix, pytest_mark) in markers.items():
|
||||
marker = item.get_marker(name)
|
||||
if marker:
|
||||
if marker.args:
|
||||
text = '{}: {}'.format(prefix, marker.args[0])
|
||||
else:
|
||||
text = prefix
|
||||
item.add_marker(pytest_mark(webengine, reason=text,
|
||||
**marker.kwargs))
|
||||
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_makereport(item, call):
|
||||
"""Add a BDD section to the test output."""
|
||||
@ -139,6 +160,15 @@ def fresh_instance(quteproc):
|
||||
quteproc.start()
|
||||
|
||||
|
||||
@bdd.given("I clean up open tabs")
|
||||
def clean_open_tabs(quteproc):
|
||||
"""Clean up open windows and tabs."""
|
||||
quteproc.set_setting('tabs', 'last-close', 'blank')
|
||||
quteproc.send_cmd(':window-only')
|
||||
quteproc.send_cmd(':tab-only')
|
||||
quteproc.send_cmd(':tab-close')
|
||||
|
||||
|
||||
## When
|
||||
|
||||
|
||||
@ -147,15 +177,18 @@ def open_path(quteproc, path):
|
||||
"""Open a URL.
|
||||
|
||||
If used like "When I open ... in a new tab", the URL is opened in a new
|
||||
tab. With "... in a new window", it's opened in a new window.
|
||||
tab. With "... in a new window", it's opened in a new window. With
|
||||
"... as a URL", it's opened according to new-instance-open-target.
|
||||
"""
|
||||
new_tab = False
|
||||
new_window = False
|
||||
as_url = False
|
||||
wait = True
|
||||
|
||||
new_tab_suffix = ' in a new tab'
|
||||
new_window_suffix = ' in a new window'
|
||||
do_not_wait_suffix = ' without waiting'
|
||||
as_url_suffix = ' as a URL'
|
||||
|
||||
if path.endswith(new_tab_suffix):
|
||||
path = path[:-len(new_tab_suffix)]
|
||||
@ -163,12 +196,16 @@ def open_path(quteproc, path):
|
||||
elif path.endswith(new_window_suffix):
|
||||
path = path[:-len(new_window_suffix)]
|
||||
new_window = True
|
||||
elif path.endswith(as_url_suffix):
|
||||
path = path[:-len(as_url_suffix)]
|
||||
as_url = True
|
||||
|
||||
if path.endswith(do_not_wait_suffix):
|
||||
path = path[:-len(do_not_wait_suffix)]
|
||||
wait = False
|
||||
|
||||
quteproc.open_path(path, new_tab=new_tab, new_window=new_window, wait=wait)
|
||||
quteproc.open_path(path, new_tab=new_tab, new_window=new_window,
|
||||
as_url=as_url, wait=wait)
|
||||
|
||||
|
||||
@bdd.when(bdd.parsers.parse("I set {sect} -> {opt} to {value}"))
|
||||
@ -282,6 +319,20 @@ def fill_clipboard_multiline(quteproc, httpbin, what, content):
|
||||
fill_clipboard(quteproc, httpbin, what, textwrap.dedent(content))
|
||||
|
||||
|
||||
@bdd.when(bdd.parsers.parse('I hint with args "{args}"'))
|
||||
def hint(quteproc, args):
|
||||
quteproc.send_cmd(':hint {}'.format(args))
|
||||
quteproc.wait_for(message='hints: *')
|
||||
|
||||
|
||||
@bdd.when(bdd.parsers.parse('I hint with args "{args}" and follow {letter}'))
|
||||
def hint_and_follow(quteproc, args, letter):
|
||||
args = args.replace('(testdata)', utils.abs_datapath())
|
||||
quteproc.send_cmd(':hint {}'.format(args))
|
||||
quteproc.wait_for(message='hints: *')
|
||||
quteproc.send_cmd(':follow-hint {}'.format(letter))
|
||||
|
||||
|
||||
## Then
|
||||
|
||||
|
||||
@ -373,12 +424,16 @@ def javascript_message_not_logged(quteproc, message):
|
||||
|
||||
|
||||
@bdd.then(bdd.parsers.parse("The session should look like:\n{expected}"))
|
||||
def compare_session(quteproc, expected):
|
||||
def compare_session(request, quteproc, expected):
|
||||
"""Compare the current sessions against the given template.
|
||||
|
||||
partial_compare is used, which means only the keys/values listed will be
|
||||
compared.
|
||||
"""
|
||||
if request.config.getoption('--qute-bdd-webengine'):
|
||||
# pylint: disable=no-member
|
||||
pytest.xfail(reason="QtWebEngine TODO: Sessions are not implemented")
|
||||
# pylint: enable=no-member
|
||||
quteproc.compare_session(expected)
|
||||
|
||||
|
||||
@ -442,13 +497,17 @@ def check_contents_json(quteproc, text):
|
||||
|
||||
|
||||
@bdd.then(bdd.parsers.parse("the following tabs should be open:\n{tabs}"))
|
||||
def check_open_tabs(quteproc, tabs):
|
||||
def check_open_tabs(quteproc, request, tabs):
|
||||
"""Check the list of open tabs in the session.
|
||||
|
||||
This is a lightweight alternative for "The session should look like: ...".
|
||||
|
||||
It expects a list of URLs, with an optional "(active)" suffix.
|
||||
"""
|
||||
if request.config.getoption('--qute-bdd-webengine'):
|
||||
# pylint: disable=no-member
|
||||
pytest.xfail(reason="QtWebEngine TODO: Sessions are not implemented")
|
||||
# pylint: enable=no-member
|
||||
session = quteproc.get_session()
|
||||
active_suffix = ' (active)'
|
||||
tabs = tabs.splitlines()
|
||||
@ -497,13 +556,17 @@ def should_quit(qtbot, quteproc):
|
||||
|
||||
def _get_scroll_values(quteproc):
|
||||
data = quteproc.get_session()
|
||||
pos = data['windows'][0]['tabs'][0]['history'][0]['scroll-pos']
|
||||
pos = data['windows'][0]['tabs'][0]['history'][-1]['scroll-pos']
|
||||
return (pos['x'], pos['y'])
|
||||
|
||||
|
||||
@bdd.then(bdd.parsers.re(r"the page should be scrolled "
|
||||
r"(?P<direction>horizontally|vertically)"))
|
||||
def check_scrolled(quteproc, direction):
|
||||
def check_scrolled(request, quteproc, direction):
|
||||
if request.config.getoption('--qute-bdd-webengine'):
|
||||
# pylint: disable=no-member
|
||||
pytest.xfail(reason="QtWebEngine TODO: Sessions are not implemented")
|
||||
# pylint: enable=no-member
|
||||
x, y = _get_scroll_values(quteproc)
|
||||
if direction == 'horizontally':
|
||||
assert x != 0
|
||||
@ -514,7 +577,11 @@ def check_scrolled(quteproc, direction):
|
||||
|
||||
|
||||
@bdd.then("the page should not be scrolled")
|
||||
def check_not_scrolled(quteproc):
|
||||
def check_not_scrolled(request, quteproc):
|
||||
if request.config.getoption('--qute-bdd-webengine'):
|
||||
# pylint: disable=no-member
|
||||
pytest.xfail(reason="QtWebEngine TODO: Sessions are not implemented")
|
||||
# pylint: enable=no-member
|
||||
x, y = _get_scroll_values(quteproc)
|
||||
assert x == 0
|
||||
assert y == 0
|
||||
|
@ -7,8 +7,7 @@ Feature: Downloading things from a website.
|
||||
Scenario: Downloading which redirects with closed tab (issue 889)
|
||||
When I set tabs -> last-close to blank
|
||||
And I open data/downloads/issue889.html
|
||||
And I run :hint links download
|
||||
And I run :follow-hint a
|
||||
And I hint with args "links download" and follow a
|
||||
And I run :tab-close
|
||||
And I wait for "* Handling redirect" in the log
|
||||
Then no crash should happen
|
||||
@ -16,8 +15,7 @@ Feature: Downloading things from a website.
|
||||
Scenario: Downloading with error in closed tab (issue 889)
|
||||
When I set tabs -> last-close to blank
|
||||
And I open data/downloads/issue889.html
|
||||
And I run :hint links download
|
||||
And I run :follow-hint s
|
||||
And I hint with args "links download" and follow s
|
||||
And I run :tab-close
|
||||
And I wait for the error "Download error: * - server replied: NOT FOUND"
|
||||
And I run :download-retry
|
||||
@ -28,8 +26,7 @@ Feature: Downloading things from a website.
|
||||
When I set completion -> download-path-suggestion to filename
|
||||
And I set storage -> prompt-download-directory to true
|
||||
And I open data/downloads/issue1243.html
|
||||
And I run :hint links download
|
||||
And I run :follow-hint a
|
||||
And I hint with args "links download" and follow a
|
||||
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default='qutebrowser-download' mode=<PromptMode.download: 5> text='Save file to:'>, *" in the log
|
||||
Then the error "Download error: No handler found for qute://!" should be shown
|
||||
|
||||
@ -37,8 +34,7 @@ Feature: Downloading things from a website.
|
||||
When I set completion -> download-path-suggestion to filename
|
||||
And I set storage -> prompt-download-directory to true
|
||||
And I open data/downloads/issue1214.html
|
||||
And I run :hint links download
|
||||
And I run :follow-hint a
|
||||
And I hint with args "links download" and follow a
|
||||
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default='binary blob' mode=<PromptMode.download: 5> text='Save file to:'>, *" in the log
|
||||
And I run :leave-mode
|
||||
Then no crash should happen
|
||||
@ -201,8 +197,7 @@ Feature: Downloading things from a website.
|
||||
Scenario: Directly open a download with a very long filename
|
||||
When I set storage -> prompt-download-directory to true
|
||||
And I open data/downloads/issue1725.html
|
||||
And I run :hint
|
||||
And I run :follow-hint a
|
||||
And I run :click-element id long-link
|
||||
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default=* mode=<PromptMode.download: 5> text='Save file to:'>, *" in the log
|
||||
And I directly open the download
|
||||
And I wait until the download is finished
|
||||
|
@ -61,11 +61,9 @@ Feature: Opening external editors
|
||||
Scenario: Spawning an editor successfully
|
||||
When I set up a fake editor returning "foobar"
|
||||
And I open data/editor.html
|
||||
And I run :hint all
|
||||
And I run :follow-hint a
|
||||
And I run :click-element id qute-textarea
|
||||
And I wait for "Clicked editable element!" in the log
|
||||
And I run :open-editor
|
||||
And I wait for "Read back: foobar" in the log
|
||||
And I run :hint all
|
||||
And I run :follow-hint s
|
||||
And I run :click-element id qute-button
|
||||
Then the javascript message "text: foobar" should be logged
|
||||
|
@ -6,100 +6,91 @@ Feature: Using hints
|
||||
|
||||
Scenario: Using :follow-hint with an invalid index.
|
||||
When I open data/hints/html/simple.html
|
||||
And I run :hint links normal
|
||||
And I run :follow-hint xyz
|
||||
And I hint with args "links normal" and follow xyz
|
||||
Then the error "No hint xyz!" should be shown
|
||||
|
||||
### Opening in current or new tab
|
||||
|
||||
@qtwebengine_todo: createWindow is not implemented yet
|
||||
Scenario: Following a hint and force to open in current tab.
|
||||
When I open data/hints/link_blank.html
|
||||
And I run :hint links current
|
||||
And I run :follow-hint a
|
||||
And I hint with args "links current" and follow a
|
||||
And I wait until data/hello.txt is loaded
|
||||
Then the following tabs should be open:
|
||||
- data/hello.txt (active)
|
||||
|
||||
@qtwebengine_todo: createWindow is not implemented yet
|
||||
Scenario: Following a hint and allow to open in new tab.
|
||||
When I open data/hints/link_blank.html
|
||||
And I run :hint links normal
|
||||
And I run :follow-hint a
|
||||
And I hint with args "links normal" and follow a
|
||||
And I wait until data/hello.txt is loaded
|
||||
Then the following tabs should be open:
|
||||
- data/hints/link_blank.html
|
||||
- data/hello.txt (active)
|
||||
|
||||
@qtwebengine_todo: createWindow is not implemented yet
|
||||
Scenario: Following a hint to link with sub-element and force to open in current tab.
|
||||
When I open data/hints/link_span.html
|
||||
And I run :tab-close
|
||||
And I run :hint links current
|
||||
And I run :follow-hint a
|
||||
And I hint with args "links current" and follow a
|
||||
And I wait until data/hello.txt is loaded
|
||||
Then the following tabs should be open:
|
||||
- data/hello.txt (active)
|
||||
|
||||
Scenario: Entering and leaving hinting mode (issue 1464)
|
||||
When I open data/hints/html/simple.html
|
||||
And I run :hint
|
||||
And I run :fake-key -g <Esc>
|
||||
Then no crash should happen
|
||||
When I open data/hints/html/simple.html
|
||||
And I hint with args "all"
|
||||
And I run :fake-key -g <Esc>
|
||||
Then no crash should happen
|
||||
|
||||
Scenario: Using :hint spawn with flags and -- (issue 797)
|
||||
When I open data/hints/html/simple.html
|
||||
And I run :hint -- all spawn -v echo
|
||||
And I run :follow-hint a
|
||||
And I hint with args "-- all spawn -v echo" and follow a
|
||||
Then the message "Command exited successfully." should be shown
|
||||
|
||||
Scenario: Using :hint spawn with flags (issue 797)
|
||||
When I open data/hints/html/simple.html
|
||||
And I run :hint all spawn -v echo
|
||||
And I run :follow-hint a
|
||||
And I hint with args "all spawn -v echo" and follow a
|
||||
Then the message "Command exited successfully." should be shown
|
||||
|
||||
Scenario: Using :hint spawn with flags and --rapid (issue 797)
|
||||
When I open data/hints/html/simple.html
|
||||
And I run :hint --rapid all spawn -v echo
|
||||
And I run :follow-hint a
|
||||
And I hint with args "--rapid all spawn -v echo" and follow a
|
||||
Then the message "Command exited successfully." should be shown
|
||||
|
||||
@posix
|
||||
Scenario: Using :hint spawn with flags passed to the command (issue 797)
|
||||
When I open data/hints/html/simple.html
|
||||
And I run :hint --rapid all spawn -v echo -e foo
|
||||
And I run :follow-hint a
|
||||
And I hint with args "--rapid all spawn -v echo -e foo" and follow a
|
||||
Then the message "Command exited successfully." should be shown
|
||||
|
||||
Scenario: Using :hint run
|
||||
When I open data/hints/html/simple.html
|
||||
And I run :hint all run message-info {hint-url}
|
||||
And I run :follow-hint a
|
||||
And I hint with args "all run message-info {hint-url}" and follow a
|
||||
Then the message "http://localhost:(port)/data/hello.txt" should be shown
|
||||
|
||||
Scenario: Using :hint fill
|
||||
When I open data/hints/html/simple.html
|
||||
And I run :hint all fill :message-info {hint-url}
|
||||
And I run :follow-hint a
|
||||
And I hint with args "all fill :message-info {hint-url}" and follow a
|
||||
And I press the key "<Enter>"
|
||||
Then the message "http://localhost:(port)/data/hello.txt" should be shown
|
||||
|
||||
@posix
|
||||
Scenario: Using :hint userscript
|
||||
When I open data/hints/html/simple.html
|
||||
And I run :hint all userscript (testdata)/userscripts/echo_hint_text
|
||||
And I run :follow-hint a
|
||||
And I hint with args "all userscript (testdata)/userscripts/echo_hint_text" and follow a
|
||||
Then the message "Follow me!" should be shown
|
||||
|
||||
Scenario: Yanking to primary selection without it being supported (#1336)
|
||||
When selection is not supported
|
||||
And I run :debug-set-fake-clipboard
|
||||
And I open data/hints/html/simple.html
|
||||
And I run :hint links yank-primary
|
||||
And I run :follow-hint a
|
||||
And I hint with args "links yank-primary" and follow a
|
||||
Then the clipboard should contain "http://localhost:(port)/data/hello.txt"
|
||||
|
||||
Scenario: Using hint --rapid to hit multiple buttons
|
||||
When I open data/hints/buttons.html
|
||||
And I run :hint --rapid
|
||||
And I hint with args "--rapid"
|
||||
And I run :follow-hint s
|
||||
And I run :follow-hint d
|
||||
And I run :follow-hint f
|
||||
@ -109,20 +100,17 @@ Feature: Using hints
|
||||
|
||||
Scenario: Using :hint run with a URL containing spaces
|
||||
When I open data/hints/html/with_spaces.html
|
||||
And I run :hint all run message-info {hint-url}
|
||||
And I run :follow-hint a
|
||||
And I hint with args "all run message-info {hint-url}" and follow a
|
||||
Then the message "http://localhost:(port)/data/hello.txt" should be shown
|
||||
|
||||
Scenario: Clicking an invalid link
|
||||
When I open data/invalid_link.html
|
||||
And I run :hint all
|
||||
And I run :follow-hint a
|
||||
And I hint with args "all" and follow a
|
||||
Then the error "Invalid link clicked - *" should be shown
|
||||
|
||||
Scenario: Hinting inputs without type
|
||||
When I open data/hints/input.html
|
||||
And I run :hint inputs
|
||||
And I run :follow-hint a
|
||||
And I hint with args "inputs" and follow a
|
||||
And I wait for "Entering mode KeyMode.insert (reason: click)" in the log
|
||||
And I run :leave-mode
|
||||
# The actual check is already done above
|
||||
@ -130,33 +118,31 @@ Feature: Using hints
|
||||
|
||||
### iframes
|
||||
|
||||
@qtwebengine_todo: Hinting in iframes is not implemented yet
|
||||
Scenario: Using :follow-hint inside an iframe
|
||||
When I open data/hints/iframe.html
|
||||
And I run :hint links normal
|
||||
And I run :follow-hint a
|
||||
Then "acceptNavigationRequest, url http://localhost:*/data/hello.txt, type NavigationTypeLinkClicked, *" should be logged
|
||||
And I hint with args "links normal" and follow a
|
||||
Then "navigation request: url http://localhost:*/data/hello.txt, type NavigationTypeLinkClicked, *" should be logged
|
||||
|
||||
### FIXME currenly skipped, see https://github.com/The-Compiler/qutebrowser/issues/1525
|
||||
@xfail_norun
|
||||
Scenario: Using :follow-hint inside a scrolled iframe
|
||||
When I open data/hints/iframe_scroll.html
|
||||
And I run :hint all normal
|
||||
And I run :follow-hint a
|
||||
And I hint with args "all normal" and follow a
|
||||
And I run :scroll bottom
|
||||
And I run :hint links normal
|
||||
And I run :follow-hint a
|
||||
Then "acceptNavigationRequest, url http://localhost:*/data/hello2.txt, type NavigationTypeLinkClicked, *" should be logged
|
||||
And I hint wht args "links normal" and follow a
|
||||
Then "navigation request: url http://localhost:*/data/hello2.txt, type NavigationTypeLinkClicked, *" should be logged
|
||||
|
||||
@qtwebengine_todo: createWindow is not implemented yet
|
||||
Scenario: Opening a link inside a specific iframe
|
||||
When I open data/hints/iframe_target.html
|
||||
And I run :hint links normal
|
||||
And I run :follow-hint a
|
||||
Then "acceptNavigationRequest, url http://localhost:*/data/hello.txt, type NavigationTypeLinkClicked, *" should be logged
|
||||
And I hint with args "links normal" and follow a
|
||||
Then "navigation request: url http://localhost:*/data/hello.txt, type NavigationTypeLinkClicked, *" should be logged
|
||||
|
||||
@qtwebengine_todo: createWindow is not implemented yet
|
||||
Scenario: Opening a link with specific target frame in a new tab
|
||||
When I open data/hints/iframe_target.html
|
||||
And I run :hint links tab
|
||||
And I run :follow-hint a
|
||||
And I hint with args "links tab" and follow a
|
||||
And I wait until data/hello.txt is loaded
|
||||
Then the following tabs should be open:
|
||||
- data/hints/iframe_target.html
|
||||
@ -169,7 +155,7 @@ Feature: Using hints
|
||||
And I set hints -> mode to number
|
||||
And I run :bind --force , message-error "This should not happen"
|
||||
And I open data/hints/html/simple.html
|
||||
And I run :hint all
|
||||
And I hint with args "all"
|
||||
And I press the key "f"
|
||||
And I wait until data/hello.txt is loaded
|
||||
And I press the key ","
|
||||
@ -182,28 +168,39 @@ Feature: Using hints
|
||||
And I set hints -> mode to number
|
||||
And I run :bind --force , message-info "Keypress worked!"
|
||||
And I open data/hints/html/simple.html
|
||||
And I run :hint all
|
||||
And I hint with args "all"
|
||||
And I press the key "f"
|
||||
And I wait until data/hello.txt is loaded
|
||||
And I press the key ","
|
||||
Then the message "Keypress worked!" should be shown
|
||||
|
||||
### Word hints
|
||||
|
||||
Scenario: Hinting with a too short dictionary
|
||||
When I open data/hints/short_dict.html
|
||||
And I set hints -> mode to word
|
||||
# Test letter fallback
|
||||
And I hint with args "all" and follow d
|
||||
Then the error "Not enough words in the dictionary." should be shown
|
||||
And data/numbers/5.txt should be loaded
|
||||
|
||||
### Number hint mode
|
||||
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/308
|
||||
Scenario: Renumbering hints when filtering
|
||||
When I open data/hints/number.html
|
||||
And I set hints -> mode to number
|
||||
And I run :hint all
|
||||
And I hint with args "all"
|
||||
And I press the key "s"
|
||||
And I run :follow-hint 1
|
||||
Then data/numbers/7.txt should be loaded
|
||||
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/576
|
||||
@qtwebengine_skip: Flaky for some reason
|
||||
Scenario: Keeping hint filter in rapid mode
|
||||
When I open data/hints/number.html
|
||||
And I set hints -> mode to number
|
||||
And I run :hint all tab-bg --rapid
|
||||
And I hint with args "all tab-bg --rapid"
|
||||
And I press the key "t"
|
||||
And I run :follow-hint 0
|
||||
And I run :follow-hint 1
|
||||
@ -214,7 +211,7 @@ Feature: Using hints
|
||||
Scenario: Keeping hints filter when using backspace
|
||||
When I open data/hints/issue1186.html
|
||||
And I set hints -> mode to number
|
||||
And I run :hint all
|
||||
And I hint with args "all"
|
||||
And I press the key "x"
|
||||
And I press the key "0"
|
||||
And I press the key "<Backspace>"
|
||||
@ -225,9 +222,9 @@ Feature: Using hints
|
||||
Scenario: Multi-word matching
|
||||
When I open data/hints/number.html
|
||||
And I set hints -> mode to number
|
||||
And I set hints -> auto-follow to true
|
||||
And I set hints -> auto-follow to unique-match
|
||||
And I set hints -> auto-follow-timeout to 0
|
||||
And I run :hint all
|
||||
And I hint with args "all"
|
||||
And I press the keys "ten pos"
|
||||
Then data/numbers/11.txt should be loaded
|
||||
|
||||
@ -235,15 +232,14 @@ Feature: Using hints
|
||||
When I open data/hints/number.html
|
||||
And I set hints -> mode to number
|
||||
And I set hints -> scatter to true
|
||||
And I run :hint all
|
||||
And I run :follow-hint 00
|
||||
And I hint with args "all" and follow 00
|
||||
Then data/numbers/1.txt should be loaded
|
||||
|
||||
# https://github.com/The-Compiler/qutebrowser/issues/1559
|
||||
Scenario: Filtering all hints in number mode
|
||||
When I open data/hints/number.html
|
||||
And I set hints -> mode to number
|
||||
And I run :hint all
|
||||
And I hint with args "all"
|
||||
And I press the key "2"
|
||||
And I wait for "Leaving mode KeyMode.hint (reason: all filtered)" in the log
|
||||
Then no crash should happen
|
||||
@ -252,8 +248,145 @@ Feature: Using hints
|
||||
Scenario: Using rapid number hinting twice
|
||||
When I open data/hints/number.html
|
||||
And I set hints -> mode to number
|
||||
And I run :hint --rapid
|
||||
And I hint with args "--rapid"
|
||||
And I run :leave-mode
|
||||
And I run :hint --rapid
|
||||
And I run :follow-hint 00
|
||||
And I hint with args "--rapid" and follow 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 hint with args "--mode number all"
|
||||
And I press the key "s"
|
||||
And I run :follow-hint 1
|
||||
Then data/numbers/7.txt should be loaded
|
||||
|
||||
### auto-follow option
|
||||
|
||||
Scenario: Using hints -> auto-follow == 'always' in letter mode
|
||||
When I open data/hints/html/simple.html
|
||||
And I set hints -> mode to letter
|
||||
And I set hints -> auto-follow to always
|
||||
And I hint with args "all"
|
||||
Then data/hello.txt should be loaded
|
||||
|
||||
# unique-match is actually the same as full-match in letter mode
|
||||
Scenario: Using hints -> auto-follow == 'unique-match' in letter mode
|
||||
When I open data/hints/html/simple.html
|
||||
And I set hints -> mode to letter
|
||||
And I set hints -> auto-follow to unique-match
|
||||
And I hint with args "all"
|
||||
And I press the key "a"
|
||||
Then data/hello.txt should be loaded
|
||||
|
||||
Scenario: Using hints -> auto-follow == 'full-match' in letter mode
|
||||
When I open data/hints/html/simple.html
|
||||
And I set hints -> mode to letter
|
||||
And I set hints -> auto-follow to full-match
|
||||
And I hint with args "all"
|
||||
And I press the key "a"
|
||||
Then data/hello.txt should be loaded
|
||||
|
||||
Scenario: Using hints -> auto-follow == 'never' without Enter in letter mode
|
||||
When I open data/hints/html/simple.html
|
||||
And I set hints -> mode to letter
|
||||
And I set hints -> auto-follow to never
|
||||
And I hint with args "all"
|
||||
And I press the key "a"
|
||||
Then "Leaving mode KeyMode.hint (reason: followed)" should not be logged
|
||||
|
||||
Scenario: Using hints -> auto-follow == 'never' in letter mode
|
||||
When I open data/hints/html/simple.html
|
||||
And I set hints -> mode to letter
|
||||
And I set hints -> auto-follow to never
|
||||
And I hint with args "all"
|
||||
And I press the key "a"
|
||||
And I press the key "<Enter>"
|
||||
Then data/hello.txt should be loaded
|
||||
|
||||
Scenario: Using hints -> auto-follow == 'always' in number mode
|
||||
When I open data/hints/html/simple.html
|
||||
And I set hints -> mode to number
|
||||
And I set hints -> auto-follow to always
|
||||
And I hint with args "all"
|
||||
Then data/hello.txt should be loaded
|
||||
|
||||
Scenario: Using hints -> auto-follow == 'unique-match' in number mode
|
||||
When I open data/hints/html/simple.html
|
||||
And I set hints -> mode to number
|
||||
And I set hints -> auto-follow to unique-match
|
||||
And I hint with args "all"
|
||||
And I press the key "f"
|
||||
Then data/hello.txt should be loaded
|
||||
|
||||
Scenario: Using hints -> auto-follow == 'full-match' in number mode
|
||||
When I open data/hints/html/simple.html
|
||||
And I set hints -> mode to number
|
||||
And I set hints -> auto-follow to full-match
|
||||
And I hint with args "all"
|
||||
# this actually presses the keys one by one
|
||||
And I press the key "follow me!"
|
||||
Then data/hello.txt should be loaded
|
||||
|
||||
Scenario: Using hints -> auto-follow == 'never' without Enter in number mode
|
||||
When I open data/hints/html/simple.html
|
||||
And I set hints -> mode to number
|
||||
And I set hints -> auto-follow to never
|
||||
And I hint with args "all"
|
||||
# this actually presses the keys one by one
|
||||
And I press the key "follow me!"
|
||||
Then "Leaving mode KeyMode.hint (reason: followed)" should not be logged
|
||||
|
||||
Scenario: Using hints -> auto-follow == 'never' in number mode
|
||||
When I open data/hints/html/simple.html
|
||||
And I set hints -> mode to number
|
||||
And I set hints -> auto-follow to never
|
||||
And I hint with args "all"
|
||||
# this actually presses the keys one by one
|
||||
And I press the key "follow me!"
|
||||
And I press the key "<Enter>"
|
||||
Then data/hello.txt should be loaded
|
||||
|
||||
Scenario: Using hints -> auto-follow == 'always' in word mode
|
||||
When I open data/hints/html/simple.html
|
||||
And I set hints -> mode to word
|
||||
And I set hints -> auto-follow to always
|
||||
And I hint with args "all"
|
||||
Then data/hello.txt should be loaded
|
||||
|
||||
Scenario: Using hints -> auto-follow == 'unique-match' in word mode
|
||||
When I open data/hints/html/simple.html
|
||||
And I set hints -> mode to word
|
||||
And I set hints -> auto-follow to unique-match
|
||||
And I hint with args "all"
|
||||
# the link gets "hello" as the hint
|
||||
And I press the key "h"
|
||||
Then data/hello.txt should be loaded
|
||||
|
||||
Scenario: Using hints -> auto-follow == 'full-match' in word mode
|
||||
When I open data/hints/html/simple.html
|
||||
And I set hints -> mode to word
|
||||
And I set hints -> auto-follow to full-match
|
||||
And I hint with args "all"
|
||||
# this actually presses the keys one by one
|
||||
And I press the key "hello"
|
||||
Then data/hello.txt should be loaded
|
||||
|
||||
Scenario: Using hints -> auto-follow == 'never' without Enter in word mode
|
||||
When I open data/hints/html/simple.html
|
||||
And I set hints -> mode to word
|
||||
And I set hints -> auto-follow to never
|
||||
And I hint with args "all"
|
||||
# this actually presses the keys one by one
|
||||
And I press the key "hello"
|
||||
Then "Leaving mode KeyMode.hint (reason: followed)" should not be logged
|
||||
|
||||
Scenario: Using hints -> auto-follow == 'never' in word mode
|
||||
When I open data/hints/html/simple.html
|
||||
And I set hints -> mode to word
|
||||
And I set hints -> auto-follow to never
|
||||
And I hint with args "all"
|
||||
# this actually presses the keys one by one
|
||||
And I press the key "hello"
|
||||
And I press the key "<Enter>"
|
||||
Then data/hello.txt should be loaded
|
||||
|
@ -34,13 +34,14 @@ Feature: Page history
|
||||
Then the history file should contain:
|
||||
http://localhost:(port)/data/%C3%A4%C3%B6%C3%BC.html Chäschüechli
|
||||
|
||||
@flaky_once
|
||||
@flaky_once @qtwebengine_todo: Error page message is not implemented
|
||||
Scenario: History with an error
|
||||
When I run :open file:///does/not/exist
|
||||
And I wait for "Error while loading file:///does/not/exist: Error opening /does/not/exist: *" in the log
|
||||
Then the history file should contain:
|
||||
file:///does/not/exist Error loading page: file:///does/not/exist
|
||||
|
||||
@qtwebengine_todo: Error page message is not implemented
|
||||
Scenario: History with a 404
|
||||
When I open status/404 without waiting
|
||||
And I wait for "Error while loading http://localhost:*/status/404: NOT FOUND" in the log
|
||||
@ -54,6 +55,7 @@ Feature: Page history
|
||||
|
||||
## Bugs
|
||||
|
||||
@qtwebengine_skip
|
||||
Scenario: Opening a valid URL which turns out invalid
|
||||
When I set general -> auto-search to true
|
||||
And I run :open http://foo%40bar@baz
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user