Merge branch 'master' into auto-open-fixes

This commit is contained in:
Marshall Lochbaum 2016-08-19 22:50:26 -04:00
commit 08b348be50
154 changed files with 4332 additions and 2281 deletions

View File

@ -1 +0,0 @@
qutebrowser/3rdparty/pdfjs/*

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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'+

View File

@ -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 &gt; ::-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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -1,3 +1,3 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
check-manifest==0.31
check-manifest==0.32

View File

@ -2,4 +2,4 @@
codecov==2.0.5
coverage==4.2
requests==2.10.0
requests==2.11.1

View File

@ -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

View File

@ -1,2 +1,2 @@
pip==8.1.2
setuptools==25.1.6
setuptools==25.2.0

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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),

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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."""

View 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)

View File

@ -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)

View File

@ -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)

View File

@ -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):

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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)))

View File

@ -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)

View 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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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
"""

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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 :'),
]

View File

@ -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

View File

@ -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)

View 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}]

View File

@ -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();
})();

View File

@ -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;
})();

View File

@ -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;
})();

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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))

View File

@ -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:

View File

@ -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."""

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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)

View File

@ -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

View File

@ -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):

View File

@ -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]

View File

@ -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)

View File

@ -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.

View File

@ -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

View File

@ -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()),
'',

View File

@ -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',

View File

@ -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

View File

@ -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():

View File

@ -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]:

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -1,6 +1,9 @@
<!DOCTYPE html>
<!-- target: hello.txt -->
<!--
target: hello.txt
qtwebengine_todo: Doesn't seem to work?
-->
<html>
<head>

View File

@ -1,6 +1,9 @@
<!DOCTYPE html>
<!-- target: hello.txt -->
<!--
target: hello.txt
qtwebengine_todo: Doesn't seem to work?
-->
<html>
<head>

View File

@ -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>

View File

@ -6,6 +6,6 @@
<title>Simple input</title>
</head>
<body>
<form><input></input></form>
<form><input id="qute-input"></input></form>
</body>
</html>

View File

@ -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>

View 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>

View File

@ -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;

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View 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>

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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