Compare commits

...

535 Commits

Author SHA1 Message Date
Florian Bruhin
113675a0b5 Release v0.8.2 2016-08-02 18:33:59 +02:00
Florian Bruhin
c4d8a767f9 Update changelog for 0.8.2 2016-08-02 18:33:36 +02:00
Florian Bruhin
de3867fe95 Bump up filename length limit to 50
The usual limit seems to be 255 bytes, so even when assuming 5-byte
UTF-8 chars for every letter, 50 should be fine.

http://serverfault.com/questions/9546/filename-length-limits-on-linux/9548#9548
2016-08-02 16:17:05 +02:00
Daniel Schadt
6ac3940264 open-download: don't crash on download cancel
Fixes #1728.
2016-08-02 16:17:05 +02:00
Daniel Schadt
5e9eafd5a3 open-download: force encoding for filename
Fixes #1726.
2016-08-02 16:17:05 +02:00
Daniel Schadt
5d4b9e815c open-download: make sure the name is not too long
Fixes #1725.

Make sure that the temporary filename is not too long by restricting the
suggested part to 20 characters.
2016-08-02 16:17:05 +02:00
Daniel Schadt
3eba7fc314 downloads: don't crash on OSError in open-download
Fixes the crash in #1725, but does not provide a solution. The browser
won't crash, but the file won't be downloaded and opened either.
2016-08-02 16:17:05 +02:00
Florian Bruhin
11d7486f97 freeze.py: Copy plugin folders on Windows
This makes HTML5 video work.
Fixes #1068.
2016-08-02 15:56:40 +02:00
Florian Bruhin
8e7a1d3d97 Use HTML content for localstorage test
JS logging is disabled by QtWebKit in private browsing mode
2016-08-02 15:18:58 +02:00
Florian Bruhin
98704c0471 Fix deleting of quickmarks with ctrl-d 2016-08-02 14:58:12 +02:00
Florian Bruhin
c2bf595b79 Don't use QSignalSpy in IPC test
Fixes #1727.

For another testcase in the same file we still need to use it until
pytest-qt has a MultiSignalBlocker.args.
2016-08-02 14:26:11 +02:00
Florian Bruhin
f73f3a2001 Tunnel private-browsing to QtWebKit correctly 2016-08-02 14:24:34 +02:00
Florian Bruhin
afde5bbc79 Fix using a relative path with --basedir 2016-08-02 14:23:38 +02:00
Florian Bruhin
66a76a4504 travis: Remove testing on Ubuntu Wily 2016-08-02 11:20:12 +02:00
Florian Bruhin
feb73f06c5 Fix ;o/;O default bindings 2016-08-02 10:52:45 +02:00
Florian Bruhin
e32fbe9013 QtWebEngine: Fix crash when closing/reopening tabs 2016-08-02 10:52:24 +02:00
Florian Bruhin
776a16bf65 Fix crash when opening http://foo%40bar@baz 2016-08-02 10:51:54 +02:00
Florian Bruhin
225c860452 Fix <input /> test in test_webelem 2016-08-02 10:48:18 +02:00
Florian Bruhin
f982402526 Consider input elements without type for hinting 2016-08-02 10:46:23 +02:00
Florian Bruhin
6a6e7ecb38 travis: Switch bot to #qutebrowser-dev 2016-07-27 13:05:09 +02:00
Florian Bruhin
c94ed93f13 build_release: Fix call_tox with no python on Win 2016-07-27 12:36:43 +02:00
Florian Bruhin
95d1721f01 Release v0.8.1 2016-07-27 12:31:30 +02:00
Florian Bruhin
410be07f54 Hide harfbuzz warning if frozen 2016-07-27 12:15:37 +02:00
Florian Bruhin
01de52c23a Improve error message on OS X without QtWebEngine 2016-07-27 12:13:45 +02:00
Florian Bruhin
a84807ed05 Handle empty command in CommandRunner.parse_all
Sicne we now call self._get_alias there, we also need to make sure it's
not an empty string before that.

Introduced in #1577. Fixes #1690.
2016-07-27 11:23:30 +02:00
Florian Bruhin
2795ae9478 Update build scripts from master 2016-07-26 17:01:08 +02:00
Florian Bruhin
614794a62a link_pyqt: Use PyQt5.QtCore to find PyQt5 path
For some reason on OS X, PyQt5.__file__ does not exist as it's a
namespace package.
2016-07-26 13:59:25 +02:00
Florian Bruhin
b12c984846 Release v0.8.0 2016-07-26 13:30:41 +02:00
Florian Bruhin
3801960c61 tests: Add v0.8.0 to old_configs 2016-07-26 13:30:41 +02:00
Florian Bruhin
07e0ae5584 Un-hide --backend argument 2016-07-26 13:30:41 +02:00
Florian Bruhin
3ccb691e9f tab API: Rename scroll to scroller
The scroll attribute did overwrite QWidget.scroll which is unfortunate.
2016-07-26 13:19:07 +02:00
Florian Bruhin
878fa26247 Use real slots for QtWebKit signals
Otherwise we can get this when immediately quitting:

Traceback (most recent call last):
  File ".../qutebrowser/browser/webkit/webkittab.py", line 580, in <lambda>
    not self._widget.page().error_occurred))
RuntimeError: wrapped C/C++ object of type WebView has been deleted
2016-07-26 12:55:40 +02:00
Florian Bruhin
214641301c Improve test_smoke
There's currently an error on exit which doesn't get caught with
--nowindow and not with ":later 500 quit".

We also need to check the output as there's an additional segfault when
that happens...
2016-07-26 12:51:12 +02:00
Florian Bruhin
7f9af096cd Clean up changelog 2016-07-26 12:34:02 +02:00
Florian Bruhin
99ea20d0d1 Merge branch 'paretje-bash' 2016-07-26 12:14:42 +02:00
Florian Bruhin
9bd48c4277 Merge branch 'bash' of https://github.com/paretje/qutebrowser into paretje-bash 2016-07-26 12:13:15 +02:00
Florian Bruhin
3ea6d4c527 Add KeyConfigStub.get_reverse_bindings_for 2016-07-26 12:06:42 +02:00
Kevin Velghe
0f07198271 Don't pass 2 arguments to shebang
On most platforms (according to shellcheck), you can't pass two
arguments in a shebang. I.e. on Debian you get:
/usr/bin/env: ‘bash -e’: No such file or directory
2016-07-26 11:48:26 +02:00
Florian Bruhin
49977a32c4 Remove unused imports 2016-07-26 11:09:24 +02:00
Florian Bruhin
70d6f90f08 Display key hint for :prompt-download-open
This also splits up _display_question into one method per mode.
2016-07-26 11:08:03 +02:00
Florian Bruhin
d70f3a0417 Merge branch 'Kingdread-open-download' 2016-07-26 10:54:59 +02:00
Florian Bruhin
d5cf8ef894 Update docs 2016-07-26 10:54:45 +02:00
Florian Bruhin
36b0054238 Add keyconf.get_reverse_bindings_for 2016-07-26 10:50:50 +02:00
Florian Bruhin
f4f6a3dac1 Merge branch 'open-download' of https://github.com/Kingdread/qutebrowser into Kingdread-open-download 2016-07-26 10:32:34 +02:00
Florian Bruhin
e4d896401d Merge branch 'mlochbaum-mark-del-no-args' 2016-07-26 08:39:11 +02:00
Florian Bruhin
da64db853e Update docs 2016-07-26 08:36:16 +02:00
Florian Bruhin
dab17b801e Merge branch 'mark-del-no-args' of https://github.com/mlochbaum/qutebrowser into mlochbaum-mark-del-no-args 2016-07-26 08:33:17 +02:00
Florian Bruhin
ded733c674 test requirements: Update CherryPy to 7.1.0
Implement systemd's socket activation mechanism for CherryPy servers,
based on work sponsored by Endless Computers.

Socket Activation allows one to setup a system so that systemd will sit
on a port and start services 'on demand' (a little bit like inetd and
xinetd used to do).
2016-07-25 22:59:28 +02:00
Marshall Lochbaum
028e7239ed Add more number files 2016-07-25 15:37:02 -04:00
Florian Bruhin
2751e57958 Update OS X install instructions
Closes #1656
2016-07-25 19:41:29 +02:00
Florian Bruhin
c618983b3d flake8 requirements: Update flake8-tuple to 0.2.12
flake8>=3.0.0 compatibility
2016-07-25 18:07:41 +02:00
Florian Bruhin
b8ea3d3c39 flake8 requirements: Pin flake8 properly 2016-07-25 18:07:31 +02:00
Florian Bruhin
3efb41f743 flake8 requirements: Filter to flake8 < 3.0 2016-07-25 16:39:30 +02:00
Florian Bruhin
9f45a27a2a test requirements: Update CherryPy to 7.0.0
7.0.0
-----

Removed the long-deprecated backward compatibility for
legacy config keys in the engine. Use the config for the
namespaced-plugins instead:

 - autoreload_on -> autoreload.on
 - autoreload_frequency -> autoreload.frequency
 - autoreload_match -> autoreload.match
 - reload_files -> autoreload.files
 - deadlock_poll_frequency -> timeout_monitor.frequency

6.2.1
-----

Fix KeyError in Bus.publish when signal handlers set in config.
2016-07-25 13:06:32 +02:00
Florian Bruhin
1564563662 Don't enable warnings if log was never inited
Otherwise, anything importing qtutils (which uses ignore_py_warnings
on module level) would enable warnings. This means pylint showed its own
warnings because of qute_pylint.config.
2016-07-25 13:03:58 +02:00
Florian Bruhin
c3f53312af Fix 'is not' string comparison 2016-07-23 19:51:13 +02:00
Florian Bruhin
e887d9a381 Remove useless super-calls 2016-07-23 18:39:27 +02:00
Marshall Lochbaum
3ffb726b98 Merge branch 'master' into mark-del-no-args 2016-07-23 12:12:19 -04:00
Marshall Lochbaum
f52c7f13d3 Update numbers in urlmarks test 2016-07-23 12:05:26 -04:00
Marshall Lochbaum
9eeda62adf Use quickmark-add rather than quickmark-save in test 2016-07-23 12:05:26 -04:00
Marshall Lochbaum
1781d0fba3 Use DoesNotExistError rather than CommandError in get_by_qurl 2016-07-23 12:05:21 -04:00
Marshall Lochbaum
9758b52d91 Assume _current_url is valid (remove try/except) 2016-07-23 12:05:21 -04:00
Marshall Lochbaum
02731743c0 Use qtutils.ensure_valid instead of testing isValid in get_by_qurl 2016-07-23 12:05:11 -04:00
Florian Bruhin
04fdce9058 Close file stream in qute_pylint.modeline 2016-07-23 17:00:05 +02:00
Marshall Lochbaum
cba25d2bbb Remove quickmark_del and bookmark_del from the urlmark classes (use delete instead) 2016-07-23 10:55:57 -04:00
Florian Bruhin
3d4a01ef4c Merge branch 'winged-issue_1033_bookmark_display' 2016-07-23 15:48:41 +02:00
Florian Bruhin
19077c8b47 Update docs 2016-07-23 15:47:31 +02:00
Florian Bruhin
91b754d6ea Merge branch 'issue_1033_bookmark_display' of https://github.com/winged/qutebrowser into winged-issue_1033_bookmark_display 2016-07-23 15:42:33 +02:00
David Vogt
1f320b8686 Various fixes after code review.
* Move documentation changes of bookmark / quickmarks to docstrings, as the
  asciidoc is autogenerated from those
* Fix some whitespaces in the BDD test cases
* Improved docstring in qute_bookmarks handler
2016-07-23 14:50:28 +02:00
Florian Bruhin
02a06732ff Also ignore ImportWarning for pkg_resources import 2016-07-23 14:15:39 +02:00
Florian Bruhin
78fd614237 Merge branch 'ganwell-issue_1670_tests_fail_due_to_SSL_error' 2016-07-23 14:03:08 +02:00
David Vogt
11bf5c8809 Minor cleanups. Whitespace / indentation CSS.
There were some CSS classes that were not used in the bookmarks page.
Also, fixes a small indentation / code alignment issue
2016-07-23 13:06:56 +02:00
Florian Bruhin
d1cc542835 Update authors 2016-07-23 13:05:55 +02:00
Florian Bruhin
40a3e24b05 Ignore warning when importing pkg_resources
On Debian Jessie we get a PendingDeprecationWarning which we now see
since the log is init'ed earlier.
2016-07-23 13:04:53 +02:00
Florian Bruhin
64f208486e Add log.ignore_py_warnings() 2016-07-23 13:04:45 +02:00
Florian Bruhin
36bb5cf285 Merge branch 'issue_1670_tests_fail_due_to_SSL_error' of https://github.com/ganwell/qutebrowser into ganwell-issue_1670_tests_fail_due_to_SSL_error 2016-07-23 12:46:16 +02:00
David Vogt
8bbfcc01be Sort bookmarks / quickmarks by title or name respectively. 2016-07-23 12:32:05 +02:00
David Vogt
12e7b4f244 Extend command documentation regarding bookmarks page
Add links to the qute:bookmarks page in the documentation for `:bookmark-add`
and `:quickmark-add` commands.
2016-07-23 12:27:31 +02:00
David Vogt
85be6565fc Implement feature request #1033: Bookmark display
There is a new page now, qute:bookmarks that will display all bookmarks and
quickmarks. It's still missing a search / filter feature, but you can use
the built-in search / navigation just as easily for now.
2016-07-23 12:09:49 +02:00
Jean-Louis Fuchs
f040fd5a6d Ignore missing SSLv3 messages from Qt 2016-07-23 10:01:56 +00:00
Jean-Louis Fuchs
ef01566621 Initialize qt logging to qutebrowser as early as possible 2016-07-23 09:54:13 +00:00
Florian Bruhin
76eab7617b Remove @pyqtSlot for functions and non-QObjects
Fixes #1669
2016-07-23 11:42:50 +02:00
Marshall Lochbaum
e9660ad676 Fix incorrect number in test 2016-07-21 22:45:36 -04:00
Marshall Lochbaum
ff682606ab Add tests for bookmark-del and quickmark-del with no arguments 2016-07-21 22:34:10 -04:00
Marshall Lochbaum
19949101c6 Make quickmark_del with no argument delete the current page's mark (fixes #1661) 2016-07-21 22:32:04 -04:00
Marshall Lochbaum
83005bc072 Make bookmark_del with no argument delete the current page's mark 2016-07-21 21:14:54 -04:00
Florian Bruhin
63e466f019 Remove @pyqtSlot for some non-QObject classes
Not sure if I got all, but at least I got the ones which fail
immediately on start.

See #1669.
2016-07-21 14:56:12 +02:00
Florian Bruhin
ac2553794c flake8: Ignore all dotfiles 2016-07-21 14:55:50 +02:00
Florian Bruhin
aabee4828e Merge branch 'rcorre-cut_test_clutter' 2016-07-20 16:19:49 +02:00
Florian Bruhin
a1c4e6e2cf Rename shadowed tmpdir variable 2016-07-20 16:13:46 +02:00
Florian Bruhin
5f2dc53d94 tox: Set distshare = {toxworkdir}
This is kind uf unorthodox and the "distshare" setting seems to be
deprecated, but we don't get a ~/.tox this way.

See #1637
2016-07-20 16:02:14 +02:00
Florian Bruhin
c4fb43df58 pylint: Set persistent=n
See #1637
2016-07-20 15:51:57 +02:00
Florian Bruhin
c7eec246d3 Merge branch 'cut_test_clutter' of https://github.com/rcorre/qutebrowser into rcorre-cut_test_clutter 2016-07-20 15:28:58 +02:00
Florian Bruhin
bf06f4a4d7 tests: Use dedicated logger for message mock
The message mock might handle a message during pytest-qt's processEvents
during test setup. If that happens, depending on the fixture order,
pytest-caplog might not be set up first, which is why the
self._caplog.at_level call can fail:

  File "c:\projects\qutebrowser\qutebrowser\misc\guiprocess.py", line 105, in on_finished
    immediately=True)
  File "C:\projects\qutebrowser\tests\helpers\messagemock.py", line 71, in _handle_error
    self._handle(Level.error, *args, **kwargs)
  File "C:\projects\qutebrowser\tests\helpers\messagemock.py", line 65, in _handle
    with self._caplog.at_level(log_level):  # needed so we don't fail
  File "C:\projects\qutebrowser\.tox\py34\lib\site-packages\pytest_catchlog.py", line 232, in at_level
    obj = logger and logging.getLogger(logger) or self.handler
  File "C:\projects\qutebrowser\.tox\py34\lib\site-packages\pytest_catchlog.py", line 186, in handler
    return self._item.catch_log_handler
  AttributeError: 'Function' object has no attribute 'catch_log_handler'

Full stack:

    c:\projects\qutebrowser-git\.tox\py34\lib\site-packages\pytestqt\plugin.py(100)pytest_runtest_setup()
  -> _process_events()
    c:\projects\qutebrowser-git\.tox\py34\lib\site-packages\pytestqt\plugin.py(140)_process_events()
  -> app.processEvents()
    c:\projects\qutebrowser-git\qutebrowser\misc\guiprocess.py(94)on_error()
  -> self._what, msg), immediately=True)
    c:\projects\qutebrowser-git\tests\helpers\messagemock.py(71)_handle_error()
  -> self._handle(Level.error, *args, **kwargs)
    c:\projects\qutebrowser-git\tests\helpers\messagemock.py(65)_handle()
  -> with self._caplog.at_level(log_level):  # needed so we don't fail
    c:\projects\qutebrowser-git\.tox\py34\lib\site-packages\pytest_catchlog.py(235)at_level()
  -> obj = logger and logging.getLogger(logger) or self.handler
  > c:\projects\qutebrowser-git\.tox\py34\lib\site-packages\pytest_catchlog.py(189)handler()->None

This should fix broken AppVeyor builds.

Fixes #1662.
2016-07-20 14:23:09 +02:00
Florian Bruhin
811361dbbe test requirements: Update beautifulsoup4 to 4.5.0
* Beautiful Soup is no longer compatible with Python 2.6. This
  actually happened a few releases ago, but it's now official.

* Beautiful Soup will now work with versions of html5lib greater than
  0.99999999.

* If a search against each individual value of a multi-valued
  attribute fails, the search will be run one final time against the
  complete attribute value considered as a single string. That is, if
  a tag has class="foo bar" and neither "foo" nor "bar" matches, but
  "foo bar" does, the tag is now considered a match.

  This happened in previous versions, but only when the value being
  searched for was a string. Now it also works when that value is
  a regular expression, a list of strings, etc.

* Fixed a bug that deranged the tree when a whitespace element was
  reparented into a tag that contained an identical whitespace
  element.

* Added support for CSS selector values that contain quoted spaces,
  such as tag[style="display: foo"].

* Corrected handling of XML processing instructions.

* Corrected an encoding error that happened when a BeautifulSoup
  object was copied.

* The contents of <textarea> tags will no longer be modified when the
  tree is prettified.

* When a BeautifulSoup object is pickled but its tree builder cannot
  be pickled, its .builder attribute is set to None instead of being
  destroyed. This avoids a performance problem once the object is
  unpickled.

* Specify the file and line number when warning about a
  BeautifulSoup object being instantiated without a parser being
  specified.

* The `limit` argument to `select()` now works correctly, though it's
  not implemented very efficiently.

* Fixed a Python 3 ByteWarning when a URL was passed in as though it
  were markup. Thanks to James Salter for a patch and
  test.

* We don't run the check for a filename passed in as markup if the
  'filename' contains a less-than character; the less-than character
  indicates it's most likely a very small document.
2016-07-20 14:07:01 +02:00
Florian Bruhin
dfbd31f35f Use /usr/bin/env shebang for bash userscripts
See #1665
2016-07-20 11:06:35 +02:00
Florian Bruhin
6130226a23 doc/stacktrace: Add qt5-webengine-debug 2016-07-20 09:40:09 +02:00
Florian Bruhin
d271c31edf pylint requirements: Update pylint to 1.6.4
* Recurse into all the ancestors when checking if an object is an exception

  Since we were going only into the first level, we weren't inferring
  when a class used a metaclass which defined a base Exception class
  for the aforementioned class.
2016-07-19 19:22:25 +02:00
Florian Bruhin
4161f4d6eb test requirements: Update CherryPy to 6.2.0
* Added tool to automatically convert request params based on type
  annotations (primarily in Python 3). For example:

    @cherrypy.tools.params()
    def resource(self, limit: int):
        assert isinstance(limit, int)
2016-07-19 09:36:51 +02:00
Florian Bruhin
70f14329e3 pylint requirements: Update pylint to 1.6.3
* Do not crash when inferring uninferable exception types for docparams
  extension
2016-07-19 09:24:51 +02:00
Ryan Roden-Corrent
48dbf505ce Limit config_tmpdir use in test_configtypes.
Only use the fixture in the test class that tries to access the config
dir (TestFileAndUserStyleSheet) rather than the whole test.
2016-07-18 21:49:37 -04:00
Ryan Roden-Corrent
7b3406a3e4 Use monkeypatch.setenv instead of os.putenv.
This ensures the environment is modified only for the test using the
fixture rather than for the whole test run.
2016-07-18 21:39:35 -04:00
Florian Bruhin
ea8e1f1131 Merge branch 'lahwaacz-#1657' 2016-07-18 14:31:20 +02:00
Florian Bruhin
8a290bf9b2 Add a test for #1657/#1658 2016-07-18 14:31:01 +02:00
Jakub Klinkovský
202883fc03 fix #1657
The _filterstr attribute was not cleaned up properly and persisted
between hintings. In this case, it was set to something representing the
Escape key.
2016-07-17 17:36:35 +02:00
Florian Bruhin
d867a789c2 Re-enable crash reports for QtWebEngine 2016-07-17 14:27:01 +02:00
Ryan Roden-Corrent
7d36847f77 Prevent test_tab from creating user data dir.
This is another case (like test_qt_javascript) that needs redirection
of XDG_DATA_HOME to prevent Qt from creating ~/.share/local/qute_test.
2016-07-17 07:07:05 -04:00
Florian Bruhin
f597b052c6 test requirements: Update CherryPy to 6.1.1
Fix issue where autoreload fails when the host interpreter for CherryPy
was launched using ``python -m``.
2016-07-17 12:00:54 +02:00
Ryan Roden-Corrent
5ae9d985b1 Prevent lingering object from test_config.
Using the config_tmdpir fixture across all tests in this module caused
a lingering LineParser to make test_debug fail.
I still don't know why, but scoping the config_tmpdir fixture to only
the test class that was creating ~/.config/qute_test fixes the issue,
and still prevents creation of a user tempdir.
2016-07-16 06:47:12 -04:00
Ryan Roden-Corrent
f589e44700 Don't write to user datadir in test_qt_javascript.
This was more complicated than the other data/config/cachedir test
fixes, as QtWebEngine was accessing the datadir directly (and bypassing
standdarddir.data).

This means the tmpdir_data stub is not enough, we need to set
XDG_DATA_HOME to redirect access.
2016-07-15 13:46:50 -04:00
Ryan Roden-Corrent
d9b546701e Prevent creation of user dirs on several tests.
Use the config_tempdir and data_tempdir fixtures for several tests that
were creation ~/.config/qute_test or ~/.local/share/qute_test.
2016-07-15 13:46:50 -04:00
Ryan Roden-Corrent
a6695ea1be Prevent test_adblock from creating real config dir.
Don't create ~/.config/qute_test by mocking out standdarddir.config for
all tests in this module.

This adds config_tmpdir to fixtures.py and moves temp_datadir from
test_adblock to fixtures.py as it will be needed more broadly.
2016-07-15 13:46:50 -04:00
Ryan Roden-Corrent
9c9b367887 Completely prevent tests from creating cachedir.
Attempting to fix the test on windows caused it to create the cachedir
again. The call to init(None) was unnecessary.
2016-07-15 13:46:50 -04:00
Ryan Roden-Corrent
cee5d6b97f Use fake_args in test_standarddir. 2016-07-15 13:46:50 -04:00
Ryan Roden-Corrent
34583d1565 Fix standarddir test on Windows.
Was broken by 48a2cad while trying to prevent creation of qute_test in
non-temp locations.
2016-07-15 13:46:50 -04:00
Ryan Roden-Corrent
1f71520bb2 Prevent tests from creating cachedir tag.
Running test_standarddir would pollute the user's home with
`~/.cache/qute_test`.

The `no_cachedir_tag` fixture was supposed to prevent this, but was not
working because [usefixtures does not work on fixtures]
(https://github.com/pytest-dev/pytest/issues/1014).

This fixes the fixture to actually prevent cachedir creation, but
applies it to tests individually (or by class) rather than with autouse
because the cachedir tests cannot pass if it is working.
2016-07-15 13:46:50 -04:00
Ryan Roden-Corrent
daaa5ff5c5 Don't create real config/data dirs from tests.
Running the tests would create ~/.config/qute_test and
~/.local/share/qute_test on the user's machine. The test_standardir
module needed a bit more mocking to prevent it from cluttering the
user's machine.

Two tests that created the data dir were fixed by passing basedir in
args, and one test that created the config dir was fixed by patching
os.makedirs to a noop.
2016-07-15 13:46:50 -04:00
Florian Bruhin
6f65973237 Adjust freeze_tests for cherrypy.wsgiserver merge 2016-07-15 17:05:41 +02:00
Florian Bruhin
701c2fe7d0 appveyor: Don't patch registry
In newer Appveyor images, only a key for Python 3.5 exists here.
2016-07-15 17:05:16 +02:00
Florian Bruhin
d710a98f46 Fix lint 2016-07-15 15:52:48 +02:00
Florian Bruhin
e2c4e6301f QtWebEngine: Implement scroll.at_top() 2016-07-15 14:00:41 +02:00
Florian Bruhin
695864281b QtWebEngine: Implement pixel scroll position in JS 2016-07-15 13:04:44 +02:00
Florian Bruhin
7adc8ab2d6 QtWebEngine: Implement scroll position based on JS 2016-07-15 12:52:27 +02:00
Florian Bruhin
83906d223a Remove unneeded pylint suppression
In the last CherryPy update, cherrypy.wsgiserver got converted to a
single module. While this issue still exists in pylint, we don't get it
here anymore.
2016-07-15 11:09:51 +02:00
Florian Bruhin
321e5319c6 pylint requirements: Update pylint to 1.6.2
* Do not crash when printing the help of options with default regular
  expressions
* More granular versions for deprecated modules.
* Do not crash in docparams when we can't infer the exception types.
2016-07-15 11:06:46 +02:00
Florian Bruhin
d079caafa8 Add possibility to disable spellcheck 2016-07-15 00:07:37 +02:00
Florian Bruhin
a8dc940b73 test/vulture reqs: Update vulture to 0.10
* Detect unused function and method arguments.
* Detect unused *args and **kwargs parameters.
* Change license from GPL to MIT.
2016-07-14 23:17:39 +02:00
Florian Bruhin
675e6eca23 Add old tab.seperator spellings to DELETED_OPTIONS
This was never caught due to the bug in the last commit, but now makes
reading very old configs fail.
2016-07-14 16:59:33 +02:00
Florian Bruhin
7b9d38e438 Fix config values being lost with DELETED_OPTIONS
When an option was deleted, we accidentally stopped reading instead of
ignoring that one option and then resuming.
2016-07-14 16:58:54 +02:00
Florian Bruhin
325846f20a Remove doubled check 2016-07-14 16:33:59 +02:00
Florian Bruhin
2848ab1904 test requirements: Update CherryPy to 6.1.0
* Combined wsgiserver2 and wsgiserver3 modules into a single module,
  `cherrypy.wsgiserver`.
2016-07-14 07:07:00 +02:00
Daniel Schadt
b995b9d5da downloads: fix docstrings for new return value 2016-07-13 22:25:23 +02:00
Florian Bruhin
7f8d4fa97a Merge branch 'blyxxyz-master' 2016-07-13 21:26:35 +02:00
Florian Bruhin
da417bff3e Update docs 2016-07-13 21:25:48 +02:00
Florian Bruhin
ff89ae7839 Merge branch 'master' of https://github.com/blyxxyz/qutebrowser into blyxxyz-master 2016-07-13 21:21:37 +02:00
Florian Bruhin
b59a1766c8 Fix long line 2016-07-13 21:19:02 +02:00
Florian Bruhin
a436468026 test requirements: Update hypothesis to 3.4.2
- Test functions defined using @given can now be called from other
  threads
- Attempting to delete a settings property would previously have
  silently done the wrong thing. Now it raises an AttributeError.
- Creating a settings object with a custom database_file parameter was
  silently getting ignored and the default was being used instead. Now
  it’s not.
2016-07-13 20:21:20 +02:00
Florian Bruhin
c9176b7c58 QtWebEngine: Fix 'unset' with run_js_blocking 2016-07-13 18:17:37 +02:00
Florian Bruhin
a7509d5978 Fix lint 2016-07-13 16:01:12 +02:00
Jan Verbeek
13cbdbb8bd Record mode for :repeat-command before executing 2016-07-13 15:24:45 +02:00
Daniel Schadt
029ffe3fc7 tests: add tests for usertypes.DownloadTarget 2016-07-13 14:17:41 +02:00
Florian Bruhin
5b1cca92ab Add run_js_blocking to tab API 2016-07-13 13:47:30 +02:00
Florian Bruhin
4f97b6342d tests: Add a tab fixture in test_tab 2016-07-13 13:47:29 +02:00
Florian Bruhin
558ef290e4 tests: Add a view fixture in test_tab 2016-07-13 13:47:26 +02:00
Florian Bruhin
68f5ed4fa4 tests: Fix FakeWebTabScroller
Pass tab correctly
2016-07-13 13:47:26 +02:00
Florian Bruhin
f5359b67a2 tests: Fix default_config fixture
This wasn't used since the command tests were deactivated. It was broken
during some refactoring and nobody noticed.
2016-07-13 13:43:46 +02:00
Florian Bruhin
c83a8a64dc QtWebEngine: Implement scroll.delta_page 2016-07-13 11:26:53 +02:00
Florian Bruhin
602d10c495 QtWebEngine: Implement scroll.to_point/.delta 2016-07-13 11:21:50 +02:00
Florian Bruhin
b78b89f04f QtWebEngine: Implement :scroll-perc via JS 2016-07-13 10:55:04 +02:00
Florian Bruhin
e0ab70c8cf end2end tests: Don't fail with "STUB:" warnings
We have some things like pos_px stubbed which will fail any test because
of the stub warning - but some tests don't actually need that, it just
happens when e.g. loading something.

So let's not fail tests based on stub warnings, and see how much works
that way.
2016-07-13 10:55:04 +02:00
Florian Bruhin
9c49900f9e QtWebEngine: Add JS logging 2016-07-13 10:55:04 +02:00
Florian Bruhin
e35dfe7aa3 Merge branch 'blyxxyz-style-in-lists' 2016-07-13 07:29:14 +02:00
Florian Bruhin
444bd7244a Merge branch 'style-in-lists' of https://github.com/blyxxyz/qutebrowser into blyxxyz-style-in-lists 2016-07-13 07:28:49 +02:00
Florian Bruhin
4328169274 flake8 requirements: Get rid of ebb-lint
We've had many checks disabled - these are the ones we actually lose:

    L104
    Docstrings must use Napoleon, not reStructuredText fields.

    L205
    __init__.py is not allowed to contain function or class definitions.

    L206
    Implicit relative imports are not allowed.

    L208
    Pokémon exception handling is always a mistake. If the intent is
    really to catch and ignore exceptions, explicitly name which
    exception types to silence.

    L209
    return, del, raise, assert, print (in python 2, without
    print_function) yield, and yield from are statements, not functions,
    and as such, do not require parentheses.

    L210
    Instead of intentionally relying on the side effects of map, filter,
    or a comprehension, write an explicit for loop.

    L211
    Using map or filter with a lambda as the first argument is always
    better written as list comprehension or generator expression. An
    expression is more readable and extensible, and less importantly,
    doesn't incur as much function call overhead.

    L212
    Using @staticmethod is always wrong.

    L301
    Files must end with a trailing newline.

    L303
    noqa is ignored, and as such, # noqa comments should be deleted to
    reduce pointless noise.

However, most of those are also checked by pylint (and the rest I don't
really care about), and ebb-lint increases flake8's runtime a lot
(45s -> almost 2min).
2016-07-13 07:24:10 +02:00
Daniel Schadt
7597221776 fix lint 2016-07-12 22:39:21 +02:00
Daniel Schadt
940b124253 switch set literal to list literal
See PR #1642.

😗🎶
2016-07-12 22:31:43 +02:00
Jan Verbeek
0cd39ae4ff Change sets to lists 2016-07-12 22:21:05 +02:00
Jan Verbeek
bbc46d28ff Use lists instead of tuples for comparing
Per one of the diff comments on #1597:
> I used to use a tuple for constant things, but nowadays I'd actually
> prefer a list as a tuple is something more heterogeneous (i.e. it
> makes sense to have a `(x, y)` point as a tuple, but a list of points
> would be a list).
> At some point I should probably change it to a list everywhere 😉
2016-07-12 22:05:32 +02:00
Florian Bruhin
46bdfa2932 Regenerate docs 2016-07-12 18:45:14 +02:00
Florian Bruhin
e5cab11979 Escape backslashes in end2end test commands
Let's hope this fixes stuff on Windows where \ is used as path
separator...
2016-07-12 17:52:05 +02:00
Florian Bruhin
1c86669628 Fix test_config.py
We used wrap-search to test interpolations there, but that's gone now.
2016-07-12 17:31:32 +02:00
Florian Bruhin
40455b2692 Always set FindWrapsAroundDocument in WebKitSearch
We accidentally set it to never wrap with the wrap-search option
removal, but we want to *always* wrap.
2016-07-12 17:28:59 +02:00
Florian Bruhin
54c00deb5b Remove checking of 'wrap' in WebEngineSearch
wrap functionality was removed, so no point in doing this here.
2016-07-12 17:28:28 +02:00
Daniel Schadt
a088f238e5 usertypes: remove Frankenstein's enum
This approach is not as weird in Python and still works.

DownloadTarget.OpenDownload has been renamed to OpenFileDownloadTarget,
since OpenDownloadDownloadTarget didn't look as nice.
2016-07-12 17:10:36 +02:00
Daniel Schadt
ed5fb4de4a downloads: make get_tmpdir private 2016-07-12 17:06:40 +02:00
Daniel Schadt
17175ec3d4 mhtml: remove duplicated usertypes import 2016-07-12 17:06:40 +02:00
Daniel Schadt
05c09a1476 tests: fix test_adblock for new download manager 2016-07-12 17:06:40 +02:00
Daniel Schadt
d42d980dda downloads: introduce target= param for .get/.fetch
This parameter replaces the filename and fileobj parameters. This makes
it easier to add more download targets, since only one may be "chosen".
With the OPEN_DOWNLOAD special case added, handling of filename got a
bit ugly, since it may be either None, OPEN_DOWNLOAD or a str with the
file path, and we had to make sure only one target was chosen.

With the new target enum, this handling can be simplified and we
automatically get the guarantee that only one target is chosen.
2016-07-12 17:06:40 +02:00
Daniel Schadt
16e1f8eac9 downloads: fix docstring for .get()
An earlier commit removed the prompt_download_directory parameter, it is
now passed via **kwargs and should thus be removed from the docstring.
2016-07-12 16:52:01 +02:00
Daniel Schadt
220203dd9c spellchecker: mark "occurs" as correct
See http://www.thefreedictionary.com/occur
2016-07-12 16:52:01 +02:00
Daniel Schadt
203dad0004 fix lint 2016-07-12 16:52:01 +02:00
Daniel Schadt
8795f89c88 tests: fix tests for downloads bdd test
The test was failing because of two reasons:

First, the old code had filename questions in DownloadManager.get and
DownloadManager.fetch which were almost identical, thus the part in
DownloadManager.get was removed in an earlier commit. All filename
asking is now done by DownloadManager.fetch. The good part is code
deduplication, the bad part is slightly modified behavior: The new code
doesn't wait for a filename to start the download, instead it tries to
fill the buffer immediately. This made the test fail because qute:// has
no registered handler, so in order for the test to pass now, the "no
crash" part is not enough, we also need to expect the "No handler"
error.

Secondly, and a rather rare (race) condition was the handling of errors
in the DownloadItem. If an error occured after the registration of
self.on_reply_error as error handler and before the check
    reply.error() != QNetworkReply.NoError
at the end of the function, the error signal would be emitted twice:
Once by _die() (called by on_reply_error), and once by the init_reply
function directly (in the last if block). This lead to duplicated error
messages. This is also explained in a comment in the file (with small
"stack traces").
2016-07-12 16:52:01 +02:00
Daniel Schadt
7608805c9a tests: adjust tests for new download mode
Now the prompt is shown with PromptMode.download instead of
PromptMode.text, which we need to change in the tests.
2016-07-12 16:52:01 +02:00
Daniel Schadt
b130c2a284 keybindings: add default for prompt-open-download 2016-07-12 16:52:01 +02:00
Daniel Schadt
81b2688c70 downloads: use global TempDownloadManager
This way, all temporary downloads will end up in the same directory and
everything is cleaned up at program exit, not when the corresponding
window is closed.
2016-07-12 16:52:01 +02:00
Daniel Schadt
c060f9e5c2 allow downloads to be openened directly
The file is saved in a temporary directory, which is cleaned up when the
window is closed.
2016-07-12 16:52:01 +02:00
Jan Verbeek
cafe7181c7 Blacklist command-accept, hide repeat-command
prompt-accept was blacklisted instead of command-accept.
2016-07-12 16:48:33 +02:00
Florian Bruhin
64b32ec87d Remove general -> wrap-search 2016-07-12 16:47:57 +02:00
Florian Bruhin
f0da508c21 Move searching out of WebView subclass
This also makes it work for QtWebEngine.
It also seems to fix #1050 though I'm not 100% sure why.
2016-07-12 16:29:50 +02:00
Florian Bruhin
038f803180 flake8 requirements: Update flake8-putty to 0.4.0
- Microsoft Windows filename selector fixes, with Appveyor CI testing
- Allow multiple codes to be disabled by per-line comments
- Allow comment after each rule line
- Prevent use with flake8 v3
- Support both pep8 and pycodestyle in test suite
2016-07-12 14:16:04 +02:00
Florian Bruhin
6de8f85e04 Merge branch 'jdkaplan-issues/1630' 2016-07-12 13:31:36 +02:00
Florian Bruhin
995b601222 Update docs 2016-07-12 13:31:21 +02:00
Florian Bruhin
9f6b3973d3 Adjust :print --pdf test title 2016-07-12 13:29:32 +02:00
Florian Bruhin
77035851a3 Sanity check the PDF file for :print --pdf test 2016-07-12 13:28:43 +02:00
Florian Bruhin
fce825f9df Remove redundant "Given a new tmpdir" step
With the (tmpdir) replacement we'll get a temporary directory no matter
what, so this is unnecessary.
2016-07-12 13:22:47 +02:00
Florian Bruhin
cd4eff364a Add printing to tab API
This fixes printing for QtWebKit, and hopefully will make printing to
PDF work with QtWebEngine with Qt >= 5.7
2016-07-12 12:54:11 +02:00
Jan Verbeek
ee4b24a5dc Blacklist leave-mode, use "not in" 2016-07-12 11:03:36 +02:00
Florian Bruhin
7a39021d41 Merge branch 'issues/1630' of https://github.com/jdkaplan/qutebrowser into jdkaplan-issues/1630 2016-07-12 10:07:59 +02:00
Florian Bruhin
37c6d63ddf tests: Ignore "load glyph failed" warnings
See #1637
2016-07-12 09:45:40 +02:00
Jeremy Kaplan
6b2b096f3c Add test for :print --pdf 2016-07-11 20:08:24 -04:00
Jeremy Kaplan
62ae793a24 Generate docs 2016-07-11 20:07:54 -04:00
Jan Verbeek
508c0f21fa Blacklist prompt-accept, remove mode_allowed 2016-07-12 00:23:34 +02:00
Florian Bruhin
d315a3131d Require QtWebEngine with --backend webengine 2016-07-11 22:51:05 +02:00
Florian Bruhin
ed3198db4e Merge branch 'rcorre-taskadd' 2016-07-11 21:15:14 +02:00
Florian Bruhin
31b4f2e383 Update changelog 2016-07-11 21:15:03 +02:00
Florian Bruhin
fc2787dd72 Merge branch 'taskadd' of https://github.com/rcorre/qutebrowser into rcorre-taskadd 2016-07-11 21:14:21 +02:00
Florian Bruhin
cb84cbf730 Merge branch 'ismail-s-add-bookmark-by-url' 2016-07-11 20:55:38 +02:00
Florian Bruhin
fc999f247b Update docs 2016-07-11 20:55:24 +02:00
Florian Bruhin
4f9be56d7d Merge branch 'add-bookmark-by-url' of https://github.com/ismail-s/qutebrowser into ismail-s-add-bookmark-by-url 2016-07-11 20:54:09 +02:00
Florian Bruhin
9f9e41687f Add a (tmpdir) replacement for BDD tests
See #1639
2016-07-11 20:47:37 +02:00
Florian Bruhin
3d4cf1fc92 QtWebEngine: Fix icon access on Qt < 5.7 2016-07-11 18:24:38 +02:00
Florian Bruhin
d91c922b4c Add --qute-bdd-webengine switch for end2end tests 2016-07-11 17:24:03 +02:00
Florian Bruhin
ed5af29ac9 Fix lint 2016-07-11 16:43:06 +02:00
Florian Bruhin
f2c52f96a1 Fix _on_url_changed 2016-07-11 16:33:19 +02:00
Florian Bruhin
8b67d68d4a Move qtutils.ensure_valid in WebView.openurl 2016-07-11 16:32:24 +02:00
Florian Bruhin
e80475ed57 Remove support for showing JS statusbar messages
Closes #1579.
2016-07-11 16:19:44 +02:00
Florian Bruhin
d0aea24568 Remove WebView.linkHovered signal 2016-07-11 16:09:17 +02:00
Florian Bruhin
914f9db8ca Actually connect _on_url_changed slot 2016-07-11 16:05:09 +02:00
Florian Bruhin
f46d6cbe27 Fix url_text, take 3 2016-07-11 15:06:36 +02:00
Florian Bruhin
9b75e661e2 Fix missed url_text substitutions 2016-07-11 14:59:35 +02:00
Florian Bruhin
5d6eedcd49 Move URL/title handling to AbstractTab 2016-07-11 14:36:57 +02:00
Florian Bruhin
a470bfc3f3 Get rid of url_text_changed signal
Instead we simply use url_changed which (similar to Qt's urlChanged)
simply has a QUrl argument.
2016-07-11 14:28:51 +02:00
Florian Bruhin
5cbd540e15 Get rid of WebView.cur_url attribute
The only thing which differs from url() is that it got set immediately
after openurl() was called, which might or might not have improved
something.

Let's see if things still work the same without it.
2016-07-11 14:20:46 +02:00
Florian Bruhin
7e36884cbd Add some debug logging to early QWEW import
See #1640
2016-07-11 13:49:24 +02:00
Florian Bruhin
64dc099d51 pylint: Enable docstyle extension 2016-07-11 13:47:18 +02:00
Florian Bruhin
6a07d231f4 pykint: Add some disable=unused-variable 2016-07-11 13:18:31 +02:00
Florian Bruhin
a64937122d Drop unneeded .keys() 2016-07-11 13:13:16 +02:00
Florian Bruhin
a5d2d3109e pylint: Reenable bad-builtin extension 2016-07-11 13:12:52 +02:00
Florian Bruhin
1d237b0569 pylint: ignore wrong-import-position project-wide
While it's more accurate with isort now, we get a wrong-import-position
for everything in scripts/ where we have to do the sys.path magic first.
2016-07-11 13:00:37 +02:00
Florian Bruhin
63b8d225e8 pylint requirements: pylint 1.6
Closes #1632.

new: mccabe
new: isort
deleted: colorama

astroid 1.4.7
-------------

* Stop saving assignment locals in ExceptHandlers, when the context is a store.

  This fixes a tripping case, where the RHS of a ExceptHandler can be redefined
  by the LHS, leading to a local save. For instance, ``except KeyError, exceptions.IndexError``
  could result in a local save for IndexError as KeyError, resulting in potential unexpected
  inferences. Since we don't lose a lot, this syntax gets prohibited.

pylint 1.6.0
------------

* Added a new extension, `pylint.extensions.mccabe`, for warning
  about complexity in code.

* Deprecate support for --optimize-ast.
* Deprecate support for the HTML output.
* Deprecate support for --output-files.

* Fixed a documentation error for the check_docs extension.

* Made the list of property-defining decorators configurable.

* Fix a bug where the top name of a qualified import was detected as unused variable.

* bad-builtin is now an extension check.

* generated-members support qualified name through regular expressions.

  For instance, one can specify a regular expression as --generated-members=astroid.node_classes.*
  for ignoring every no-member error that is accessed as in `astroid.node_classes.missing.object`.

* Add the ability to ignore files based on regex matching, with the new ``--ignore-patterns``
  option.

  This allows for multiple ignore patterns to be specified. Rather than
  clobber the existing ignore option, we introduced a new one called
  ignore-patterns.

* Added a new error, 'trailing-newlines', which is emitted when a file
  has trailing new lines.

* Add a new option, 'redefining-builtins-modules', for controlling the modules
  which can redefine builtins, such as six.moves and future.builtins.

* 'reimported' is emitted when the same name is imported from different module.

* Add a new recommendation checker, 'consider-iterating-dictionary', which is emitted
  which is emitted when a dictionary is iterated through .keys().

* Use the configparser backport for Python 2

  This fixes a problem we were having with comments inside values, which is fixed
  in Python 3's configparser.

* A new error was added, 'invalid-length-returned', when the `__len__`
  special method returned something else than a non-negative number.

* Switch to using isort internally for wrong-import-order.

* check_docs extension can find constructor parameters in __init__.

* Don't warn about invalid-sequence-index if the indexed object has unknown base
  classes.

* Don't crash when checking, for super-init-not-called, a method defined in an if block.

* Do not emit import-error or no-name-in-module for fallback import blocks by default.

  Until now, we warned with these errors when a fallback import block (a TryExcept block
  that contained imports for Python 2 and 3) was found, but this gets cumbersome when
  trying to write compatible code. As such, we don't check these blocks by default,
  but the analysis can be enforced by using the new ``--analyse-fallback-block`` flag.

pylint 1.6.1
------------

* Use environment markers for supporting conditional dependencies.
2016-07-11 13:00:08 +02:00
Florian Bruhin
216d8c3c13 test requirements: Update hypothesis to 3.4.1
On Windows when running two Hypothesis processes in parallel (e.g. using
pytest-xdist) they could race with each other and one would raise an
exception due to the non-atomic nature of file renaming on Windows and
the fact that you can't rename over an existing file. This is now fixed.
2016-07-11 12:45:47 +02:00
Florian Bruhin
c558a8b4ad Add missing docstring 2016-07-11 12:42:51 +02:00
Florian Bruhin
e9d606a782 Move load_status / SSL errors to AbstractTab 2016-07-11 12:09:41 +02:00
Florian Bruhin
6ae232d8fc Move 'progress' to AbstractTab
This makes load progress work with QtWebEngine.
2016-07-11 11:31:59 +02:00
Florian Bruhin
8567fffdad Merge branch 'qtwebengine' 2016-07-11 10:56:20 +02:00
Florian Bruhin
b4f993e2ab Add an early-import for QtWebEngineWidgets 2016-07-11 09:18:05 +02:00
Florian Bruhin
43c1f62e39 Also skip test_tab on PyQt < 5.6
See #1638
2016-07-11 09:10:11 +02:00
Jeremy Kaplan
a6a030e92f Add :print --pdf flag to skip printing dialog 2016-07-10 21:58:54 -04:00
Florian Bruhin
86f381a3b7 Skip tests using fake_web_tab on PyQt < 5.6
For some weird reason they cause a segfault in QObject::disconnect since
fake_web_tab was converted to a fixture...

See #1638
2016-07-10 21:42:13 +02:00
Florian Bruhin
01b7b27bda Fix lines which are now too long 2016-07-10 17:33:36 +02:00
Florian Bruhin
b791095324 Rename browser.tab module and classes 2016-07-10 17:27:02 +02:00
Florian Bruhin
2649418c0b Fix lint 2016-07-10 17:04:26 +02:00
Florian Bruhin
d2ece6b542 Move Backend enum to usertypes
Otherwise we have a cyclic import
2016-07-10 17:04:26 +02:00
Florian Bruhin
34e39bed4e Add a test for cmd instance with wrong backend 2016-07-10 17:04:25 +02:00
Florian Bruhin
fd8e66136f tests: Add a mode_manager fixture 2016-07-10 17:04:25 +02:00
Florian Bruhin
52e14950f1 tests: Fix messagemock with stack argument 2016-07-10 17:04:25 +02:00
Florian Bruhin
7859df74c4 Add tests for Command.run 2016-07-10 17:04:25 +02:00
Florian Bruhin
b07cca023b Fix Command.run with no arguments 2016-07-10 17:04:25 +02:00
Florian Bruhin
2136d00aa2 tests: Add a fake_args fixture 2016-07-10 17:04:25 +02:00
Florian Bruhin
949172809d Add tab_registry to test_tab 2016-07-10 16:57:02 +02:00
Florian Bruhin
e36123735b Use a fixture for FakeWebTab
We need to make sure qapp and tab_registry are available everywhere the
FakeWebTab class is used.
2016-07-10 16:57:02 +02:00
Florian Bruhin
0ab601aaf3 Make it possible to limit commands to backends 2016-07-10 16:57:02 +02:00
Florian Bruhin
11cd5f8653 Don't register webview in objreg
All places now should use the tab API instead.
2016-07-08 17:21:33 +02:00
Florian Bruhin
67eea1678e Move tab registry from webview to tab 2016-07-08 17:13:18 +02:00
Florian Bruhin
dcf39538a3 Improve objreg error message if no window is given 2016-07-08 17:09:20 +02:00
Ismail S
fd7342b055 Move """ to separate line 2016-07-08 15:58:47 +01:00
Ismail S
394d9d1404 Add more tests for :bookmark-add 2016-07-08 15:54:39 +01:00
Ismail S
5ad66c29e1 Update docstring 2016-07-08 15:38:58 +01:00
Ismail S
babbd0771c Refactor code 2016-07-08 15:33:13 +01:00
Florian Bruhin
7360ea69ba Fix lint 2016-07-08 13:08:31 +02:00
Florian Bruhin
06a6daee34 Add stubs for QtWebEngine 2016-07-08 12:53:48 +02:00
Florian Bruhin
cc11af5e28 Fix lint 2016-07-08 11:21:00 +02:00
Florian Bruhin
b0fa821bc3 pylint: Disable duplicate-code globally
We can't disable it more fine-grained:
https://github.com/PyCQA/pylint/issues/214

I think for the shown duplicate (histroy in webkittab/webenginetab) it
makes no sense to refactor things as a Mixin...
2016-07-08 11:03:27 +02:00
Florian Bruhin
b1fda1b0ef Get rid of tab.run_webaction
As mentioned here:
e4b0b7fffd (r70002693)

It makes no sense to add a backend-specific run_webaction method to
AbstractTab - better to just access _widget directly in this one place
instead of adding something backend-speficic to the API.
2016-07-08 10:42:54 +02:00
Florian Bruhin
55784f9783 Add tab.create() helper function 2016-07-08 10:38:52 +02:00
Florian Bruhin
334f6cda4f tab: Rename modeman to mode_manager
To avoid it being mixed up with the modeman module
2016-07-08 10:38:51 +02:00
Florian Bruhin
7c6dd60f35 Fix test_on_tab_changed in statusbar.url tests
- The invalid URL will now get encoded when using QUrl.
- The check for a None url_text is somewhat pointless as I don't think
  this can ever happen in the real circumstances.
2016-07-08 10:38:51 +02:00
Florian Bruhin
37d20023b3 Remove now unneeded vulture ignore
We're now using __slots__
2016-07-08 10:10:08 +02:00
Florian Bruhin
f9eecaf584 Fix typo 2016-07-08 10:09:49 +02:00
Florian Bruhin
782561462b Use stubs.FakeWebTab to fake tabs
This also fixes the cur_url access.
2016-07-08 10:09:03 +02:00
Florian Bruhin
09f4c2199e Rename widget to tab in some places
This also fixes the cur_url access.
2016-07-08 10:08:44 +02:00
Florian Bruhin
b8086d1d13 mhtml: web_view -> tab rename
Otherwise this sounds like we still have a QWebView.
This also fixes the cur_url access.
2016-07-08 10:08:11 +02:00
Florian Bruhin
2befebaf3a Don't use properties for AbstractTab
Otherwise exceptions in there could be hidden by Python/PyQt.

Some places are not changed yet, as there are also other renames in the
next commits.
2016-07-08 10:05:46 +02:00
Florian Bruhin
ecd399181d Fix AbstractSearch attribute docs 2016-07-08 09:15:28 +02:00
Florian Bruhin
3bb5b5d1c9 Merge branch 'blyxxyz-status-position' 2016-07-08 09:11:29 +02:00
Florian Bruhin
b5c1db6bae Merge branch 'status-position' of https://github.com/blyxxyz/qutebrowser into blyxxyz-status-position 2016-07-08 09:10:26 +02:00
Florian Bruhin
9898b1af37 pylint: Remove -j0
See https://github.com/PyCQA/pylint/issues/987

On my laptop I get about a 3m -> 2m20s speedup, which isn't really worth
the trouble...
2016-07-07 23:22:42 +02:00
Jan Verbeek
a8c55ffe08 Stop downloads bar from pushing away status bar 2016-07-07 23:11:08 +02:00
Ismail S
3b0fb84c47 Allow adding bookmarks by url 2016-07-07 20:16:19 +01:00
Florian Bruhin
e4b0b7fffd Fix lint 2016-07-07 20:12:17 +02:00
Florian Bruhin
70fb9bfd51 Regenerate docs again 2016-07-07 20:08:12 +02:00
Florian Bruhin
dde8ac6844 Use __slots__ for tab.TabData 2016-07-07 19:36:27 +02:00
Florian Bruhin
b999090a51 Hide --backend argument for now 2016-07-07 19:28:00 +02:00
Florian Bruhin
0207f8758b Only disable reports with --backend webengine 2016-07-07 18:32:52 +02:00
Florian Bruhin
2fd01e57e6 Add default value for --backend argument 2016-07-07 18:32:52 +02:00
Florian Bruhin
ee7b4256a9 Fix AbstractTab repr() with no URL available 2016-07-07 18:32:52 +02:00
Florian Bruhin
d9516b9c1d Adjust various comments 2016-07-07 18:32:52 +02:00
Florian Bruhin
3c99436950 Make on_* methods in tab private 2016-07-07 18:32:52 +02:00
Florian Bruhin
868f781f4d Rename zoom._set_default_zoom to zoom.set_default 2016-07-07 18:32:52 +02:00
Florian Bruhin
1c9d0857cb Various cleanups 2016-07-07 18:32:52 +02:00
Florian Bruhin
9421db8869 Update docs for --backend 2016-07-07 18:32:52 +02:00
Florian Bruhin
40f0aa0023 Fix pylint without QtWebEngine available 2016-07-07 18:32:52 +02:00
Florian Bruhin
fea25d715c Add Percentage test with None-value 2016-07-07 18:32:52 +02:00
Florian Bruhin
3fe851ed84 Temporarily remove tab.py from perfect_files
We have things like the mousewheel event handling in there which we
can't easily test just yet.
2016-07-07 18:32:52 +02:00
Florian Bruhin
5420d6d2ae Mark some session tests as xfail
There'll be a refactoring to add a session API to WebTab later anyways,
so no point in fixing this now.

As many tests as possible here should probably also be changed to
end2end ones as there's a lot of mocking going on.
2016-07-07 18:32:52 +02:00
Florian Bruhin
7e3e9618b2 Revert "Adjust QtWebKit _key_press to QtWebEngine one"
This reverts commit f52326c5eea83e58d95afb696480600c6a8a5b7b.

For some reason this causes a lot of segfaults...
2016-07-07 18:32:52 +02:00
Florian Bruhin
e1bad17f2a Split up SessionManager._save_tab_item 2016-07-07 18:32:52 +02:00
Florian Bruhin
b23ddb31c9 Fix lint 2016-07-07 18:32:52 +02:00
Florian Bruhin
be02bfb37d Unify arguments for on_mode_* slots in Caret 2016-07-07 18:32:52 +02:00
Florian Bruhin
df2c50aa60 Add class docstrings for webkittab/webenginetab 2016-07-07 18:32:52 +02:00
Florian Bruhin
de60ad04dc Fix mouse wheel zooming 2016-07-07 18:32:52 +02:00
Florian Bruhin
f72f82fb0c QtWebEngine: Fix userData() call on session saving 2016-07-07 18:32:52 +02:00
Florian Bruhin
b78de501c2 Adjust QtWebKit _key_press to QtWebEngine one 2016-07-07 18:32:52 +02:00
Florian Bruhin
47ce6aff89 Send QtWebEngine fake key events to focus proxy
This fixes simple scrolling with QtWebEngine.
2016-07-07 18:32:52 +02:00
Florian Bruhin
ec053f8007 Simplify TabData implementation
This uses direct attributes instead of self._data which means we can
only override __setattr__, and pylint will better understand what's
happening.
2016-07-07 18:32:52 +02:00
Florian Bruhin
40d28b80bf Fix typo 2016-07-07 18:32:52 +02:00
Florian Bruhin
2bd07937e5 Remove unnecessary TabData.__getattr__ code
__getattribute__ is used in that case; see
https://github.com/PyCQA/pylint/issues/979
2016-07-07 18:32:52 +02:00
Florian Bruhin
09f025628f Use tab.AbstractTab for signals/slots 2016-07-07 18:32:52 +02:00
Florian Bruhin
7444f83dbf Fix importing of QtWebEngine specific code 2016-07-07 18:32:52 +02:00
Florian Bruhin
17466b4f26 Fix some lint 2016-07-07 18:32:52 +02:00
Florian Bruhin
4e5a7a891e tests: Use FakeWebTab for stubbing 2016-07-07 18:32:52 +02:00
Florian Bruhin
5107a87291 Fix test_tab tests 2016-07-07 18:32:52 +02:00
Florian Bruhin
7b37d85150 Pass modeman as argument to AbstractTab 2016-07-07 18:32:52 +02:00
Florian Bruhin
e0cd878606 Fix/tunnel mhtml downloads 2016-07-07 18:32:52 +02:00
Florian Bruhin
0719101b6f Also tunnel :paste-primary 2016-07-07 18:32:52 +02:00
Florian Bruhin
21b282ce29 Get rid of _DummyUserscriptRunner
This simplifies the code a bit and only provides
userscript.run_async (and not the UserscriptRunner class) as entrypoint.
2016-07-07 18:32:52 +02:00
Florian Bruhin
a6307497c0 Rewrite userscripts to work with async dumping 2016-07-07 18:32:52 +02:00
Florian Bruhin
edafa7c99f Tunnel a few features until we have a proper API 2016-07-07 18:32:52 +02:00
Florian Bruhin
3c71337698 Handle OverflowError when scrolling 2016-07-07 18:32:52 +02:00
Florian Bruhin
00b287117a Fix tabs.feature tests 2016-07-07 18:32:52 +02:00
Florian Bruhin
0937a64f1c Fix :inspect 2016-07-07 18:32:52 +02:00
Florian Bruhin
675f95a2e4 Add stubs for on_mode_{entered,left} for WebEngine
This means at least we won't get a crash when using the commandline.
2016-07-07 18:32:52 +02:00
Florian Bruhin
12fc0821c0 Try to implement some QtWebEngine scrolling 2016-07-07 18:32:52 +02:00
Florian Bruhin
9c5143786c Implement scroll.pos_perc for QtWebEngine 2016-07-07 18:32:52 +02:00
Florian Bruhin
a1f4dcd542 Add QWebEngineView subclass 2016-07-07 18:32:52 +02:00
Florian Bruhin
822e193682 Fix :view-source 2016-07-07 18:32:52 +02:00
Florian Bruhin
04ee021bdb Add AbstractTab.set_html 2016-07-07 18:32:52 +02:00
Florian Bruhin
78f425c98b Add AbstractTab.data 2016-07-07 18:32:52 +02:00
Florian Bruhin
70b7314b76 Fix :debug-webaction 2016-07-07 18:32:52 +02:00
Florian Bruhin
deb0a10973 Add AbstractTab.backend attribute 2016-07-07 18:32:52 +02:00
Florian Bruhin
4de48620e3 Fix loading of sessions 2016-07-07 18:32:52 +02:00
Florian Bruhin
a62e2a0c27 Fix :undo 2016-07-07 18:32:52 +02:00
Florian Bruhin
09c3528585 Fix loading of marks
This probably broke when merging from master.
2016-07-07 18:32:52 +02:00
Florian Bruhin
5dfd8d68bf Set focus proxy for AbstractTab
By default, the AbstractTab object got the focus, which means things
like key events passed to it didn't actually get passed through to the
web view, causing these tests to fail:

    tests/end2end/features/test_keyinput_bdd.py::test_forwarding_all_keys
    tests/end2end/features/test_keyinput_bdd.py::test_forwarding_special_keys

Now we make sure the real underlying WebView always gets the keyboard
focus.
2016-07-07 18:32:52 +02:00
Florian Bruhin
3c3043eeae Add tab.clear_ssl_errors()
This fixes :debug-clear-ssl-errors
2016-07-07 18:32:52 +02:00
Florian Bruhin
86f63e1ae6 Add tab.new_tab_requested signal 2016-07-07 18:32:52 +02:00
Florian Bruhin
4d650c8dfd Add tab.caret.follow_selected() 2016-07-07 18:32:52 +02:00
Florian Bruhin
1148184892 Add tab.set_open_target
This fixes :follow-selected
2016-07-07 18:32:52 +02:00
Florian Bruhin
5dd4b2d56a Fix :buffer completion 2016-07-07 18:32:52 +02:00
Florian Bruhin
52efa9f185 Make AbstractHistory.history private 2016-07-07 18:32:52 +02:00
Florian Bruhin
b0ba2125a3 Fix :undo 2016-07-07 18:32:52 +02:00
Florian Bruhin
aebc29337a Move __iter__ to AbstractHistory 2016-07-07 18:32:52 +02:00
Florian Bruhin
59c9ee88e5 Quick fix for createWindow 2016-07-07 18:32:52 +02:00
Florian Bruhin
94b856c565 Make self._widget private in wrappers
While we need to set it from the outside (from AbstractTab) this still
is not considered public API for the rest of the code, so let's make it
private.
2016-07-07 18:32:52 +02:00
Florian Bruhin
0b88c5d413 Re-implement searching for QtWebKit 2016-07-07 18:32:52 +02:00
Florian Bruhin
515d16f137 Move selection()/has_selection() to caret 2016-07-07 18:32:52 +02:00
Florian Bruhin
9f130c6b27 Disable crash reports 2016-07-07 18:32:52 +02:00
Florian Bruhin
5c535213ad Random cleanups 2016-07-07 18:32:52 +02:00
Florian Bruhin
ac4186a0f0 Tunnel :hint and :navigate 2016-07-07 18:32:52 +02:00
Florian Bruhin
5fe2230e1f Fix various scrolling issues 2016-07-07 18:32:52 +02:00
Florian Bruhin
16c397a9d2 Fix various zooming issues 2016-07-07 18:32:52 +02:00
Florian Bruhin
edb65ecf50 Add run_js_eval and get :jseval to run 2016-07-07 18:32:52 +02:00
Florian Bruhin
cd95f94ac8 Disallow None-callback for dump_async 2016-07-07 18:32:52 +02:00
Florian Bruhin
21753bc65f Make AbstractCaret a QObject 2016-07-07 18:32:52 +02:00
Florian Bruhin
e21edd3e18 Implement selection 2016-07-07 18:32:52 +02:00
Florian Bruhin
90614d1fe3 Initial caret browsing support 2016-07-07 18:32:52 +02:00
Florian Bruhin
34d3d2cda6 Full scrolling implementation 2016-07-07 18:32:52 +02:00
Florian Bruhin
56852821e8 Try to fix _back_forward 2016-07-07 18:32:52 +02:00
Florian Bruhin
7319ced0bc Fix history deserializing 2016-07-07 18:32:52 +02:00
Florian Bruhin
67ffa67968 Fix deserialize call 2016-07-07 18:32:52 +02:00
Florian Bruhin
7cbe174f1e Fix set_zoom_factor call 2016-07-07 18:32:52 +02:00
Florian Bruhin
7e607a0cf2 Add icon() 2016-07-07 18:32:52 +02:00
Florian Bruhin
4fea285740 Add win_id attribute 2016-07-07 18:32:52 +02:00
Florian Bruhin
363f3d7ea7 Replace scroll_pos by scroll_pos_px()/_perc() 2016-07-07 18:32:52 +02:00
Florian Bruhin
ed716b2b90 Make session saving work 2016-07-07 18:32:52 +02:00
Florian Bruhin
2d590c581d Make :reload and :stop work 2016-07-07 18:32:52 +02:00
Florian Bruhin
6a42e0c96c Make shutdown work 2016-07-07 18:32:52 +02:00
Florian Bruhin
55753171f1 Make :back/:forward work 2016-07-07 18:32:52 +02:00
Florian Bruhin
3ee58fdea3 Get :debug-dump-page to work 2016-07-07 18:32:52 +02:00
Florian Bruhin
0c1e266073 Use QWebView/QWebEngineView for test_tab 2016-07-07 18:32:52 +02:00
Florian Bruhin
5b9ae8bc85 Initial history implementation 2016-07-07 18:32:52 +02:00
Florian Bruhin
37c3dbbc7d Fix test_tab 2016-07-07 18:32:52 +02:00
Florian Bruhin
d2dd32b979 Fix test_url 2016-07-07 18:32:52 +02:00
Florian Bruhin
8e5a86fb13 Don't require QtWebEngine 2016-07-07 18:32:52 +02:00
Florian Bruhin
bf286f8c74 Fix some tests 2016-07-07 18:32:52 +02:00
Florian Bruhin
115021b8ea Get QtWebEngine to start and work somewhat 2016-07-07 18:32:52 +02:00
Florian Bruhin
b0a391932a Get qutebrowser to run 2016-07-07 18:32:52 +02:00
Florian Bruhin
048f7dcaf5 Refactor signals 2016-07-07 18:32:52 +02:00
Florian Bruhin
4305966f7b Add WrapperLayout/AbstractTab 2016-07-07 18:32:52 +02:00
Florian Bruhin
29ee605c79 Fix version output test on Windows
On Windows we get something like C:\IMPORTPATH instead of /IMPORTPATH
2016-07-07 18:32:13 +02:00
Florian Bruhin
674b316db3 Simplify test_version_output 2016-07-07 09:14:36 +02:00
Florian Bruhin
ade9b17b22 Remove now unused version.GPL_BOILERPLATE 2016-07-07 08:58:02 +02:00
Florian Bruhin
f3979ad908 backers: Add Geir Isene 2016-07-07 07:14:21 +02:00
Florian Bruhin
ebf9bc4e0a Improve version output 2016-07-06 23:47:59 +02:00
Florian Bruhin
a58c3ff0c6 Print some version numbers in travis_install.sh 2016-07-06 22:25:14 +02:00
Florian Bruhin
28ea459c05 Prune .github in MANIFEST.in 2016-07-06 21:21:31 +02:00
Florian Bruhin
89cdef851d Install new node 2016-07-06 17:05:12 +02:00
Florian Bruhin
8d625393ec Merge branch 'munyari-issue_template' 2016-07-06 17:00:14 +02:00
Florian Bruhin
6dbbd894b8 Regenerate authors 2016-07-06 17:00:09 +02:00
Florian Bruhin
2e23ad59dc Merge branch 'issue_template' of https://github.com/munyari/qutebrowser into munyari-issue_template 2016-07-06 16:33:30 +02:00
Panashe M. Fundira
9bd2c60488 Move issue template to .github directory 2016-07-06 10:22:54 -04:00
Panashe M. Fundira
44eefc2c3b Simplify issue template 2016-07-06 10:02:14 -04:00
Ryan Roden-Corrent
06adfc5bff Leverage +LATEST in taskadd userscript.
This is cleaner than parsing the output of `task`.
2016-07-06 08:04:06 -04:00
Florian Bruhin
db1ba9ac88 Merge branch 'blyxxyz-status-position' 2016-07-06 13:59:05 +02:00
Florian Bruhin
7ddbd24c30 Add ui -> status-position to the changelog 2016-07-06 13:58:46 +02:00
Florian Bruhin
53c942a2de Regenerate authors 2016-07-06 13:58:02 +02:00
Florian Bruhin
794a275383 Merge branch 'status-position' of https://github.com/blyxxyz/qutebrowser into blyxxyz-status-position 2016-07-06 13:56:55 +02:00
Florian Bruhin
8ba04b460e Add $QUTE_*_DIR to changelog 2016-07-06 13:56:34 +02:00
Florian Bruhin
ef03a79956 Merge branch 'rcorre-completer_tests' 2016-07-06 13:36:01 +02:00
Florian Bruhin
b429b919b5 Merge branch 'completer_tests' of https://github.com/rcorre/qutebrowser into rcorre-completer_tests 2016-07-06 13:35:13 +02:00
Florian Bruhin
80238ec2ac Merge branch 'blyxxyz-userscript_vars' 2016-07-06 13:26:58 +02:00
Florian Bruhin
e0d5c3d2b1 Regenerate authors 2016-07-06 13:26:44 +02:00
Florian Bruhin
ab123b8c80 Merge branch 'userscript_vars' of https://github.com/blyxxyz/qutebrowser into blyxxyz-userscript_vars 2016-07-06 13:25:16 +02:00
Florian Bruhin
60cc72b5a6 test requirements: Update pytest-cov to 2.3.0
- Add support for specifying output location for html, xml, and annotate
  report.
- Fix bug hiding test failure when cov-fail-under failed.
- For coverage >= 4.0, match the default behaviour of coverage report
  and error if coverage fails to find the source instead of just
  printing a warning.
- Fixed bug occurred when bare --cov parameter was used with xdist.
- Add support for skip_covered and added --cov-report=term-skip-covered
  command line options.
2016-07-06 00:18:13 +02:00
Florian Bruhin
294560ec6d flake8 requirements: Update flake8-tuple to 0.2.11
- fixed: non exists file raise IOError
- fixed: compatibility with older versions of flake8
2016-07-06 00:16:43 +02:00
Florian Bruhin
30131f0ec4 flake8 reqs: Update flake8-tidy-imports to 1.0.2
- Fixed I201 rule to detect banned imports like from x import y.
2016-07-05 08:39:18 +02:00
Florian Bruhin
eb990d4bd5 Revert "flake8 reqs: Remove obsolete comment"
This reverts commit 3a481a2fa5.
2016-07-05 08:35:58 +02:00
Florian Bruhin
70117265d6 Fix some a/an misspellings
Thanks to https://github.com/jwilk/anorack
2016-07-05 08:34:03 +02:00
Ryan Roden-Corrent
6c2c9438a7 More completer unit test cases. 2016-07-04 19:06:00 -04:00
Jan Verbeek
85bd29a7c8 Add status-position documentation 2016-07-04 19:05:44 +02:00
Panashe M. Fundira
efa3cbf04f Add issue template 2016-07-04 12:55:50 -04:00
Ryan Roden-Corrent
4c9417ac6e Use helper method for cmd prompt text testing.
In test_completer, introduce two helper methods to reduce duplicate
code for handling fake cmd prompt text that uses '|' as a placeholder.
2016-07-04 12:31:56 -04:00
Jan Verbeek
965459ab36 Add status-position option
See #1257.
I can't think of a good way to do widget-packing because tabs can be on
the left or right.
2016-07-04 18:21:09 +02:00
Ryan Roden-Corrent
27d635a394 Test selection_changed, not change_completed_part.
For the Completer unit tests:
Although `change_completed_part` looks like a public method, it was
only used internally. Test the externally-used method
`selection_changed` instead.
2016-07-04 08:22:21 -04:00
Jan Verbeek
955478799b Shorten repeat-command description, remove .i, .o
If .i and .o exist there's a delay before . gets accepted.
2016-07-04 13:39:00 +02:00
Ryan Roden-Corrent
a6a8bf9304 Clean up test_completer further.
Based on code review:
- Use qtbot.waitSignal to test a signal firing
- Use pytest.mark.xfail for an expected test failure
- Ensure there are 2 newlines between module-level functions
2016-07-04 07:36:31 -04:00
Ryan Roden-Corrent
68e373df44 Use QLineEdit as a base for FakeStatusbarCommand.
Reduces the amount of mocking and keeps it more true to the original.
2016-07-03 23:06:36 -04:00
Ryan Roden-Corrent
7f690c3f3f More unit test coverage for Completer.
Test completion_item{next,prev} and change_completed_part.
2016-07-03 22:58:09 -04:00
Ryan Roden-Corrent
1ea28890b5 Move completion_item_del to completion widget.
It was implemented as a member of the Completer object, but registered
to the CompletionWidget. This led to the weird scenario where self was
actually a CompletionWidget, even though it was declared in Completer.
2016-07-03 17:35:17 -04:00
Ryan Roden-Corrent
9bfff1c685 Test more cases for completer.update_completion. 2016-07-03 17:18:16 -04:00
Ryan Roden-Corrent
13e8ed53d6 Clean up completer unit test.
Based on code review:

    - import modules, not classes
    - use methods, not lambdas for the mock command prompt class
    - use None rather than Mock for DUMB_SORT
    - autouse two fixtures and remove them from test signatures
2016-07-03 17:18:16 -04:00
Jan Verbeek
8039e7ab74 Move :repeat-command tests 2016-07-03 22:32:07 +02:00
Florian Bruhin
e3c6a0b766 Handle OSError when closing download fileobj 2016-07-03 18:26:21 +02:00
Florian Bruhin
d1f6ae99b5 tests: Skip :spawn with invalid quoting on Windows
For some reason this often causes segfaults lately - let's skip it until
we can investigate what's wrong.

See #1614
2016-07-03 17:32:19 +02:00
Florian Bruhin
9ad76011c7 Regenerate authors 2016-07-03 17:00:22 +02:00
Florian Bruhin
9f464fd283 Merge branch 'Kingdread-expected-error' 2016-07-03 16:59:57 +02:00
Florian Bruhin
f6cd73c784 Merge branch 'expected-error' of https://github.com/Kingdread/qutebrowser into Kingdread-expected-error 2016-07-03 16:58:06 +02:00
Florian Bruhin
3a481a2fa5 flake8 reqs: Remove obsolete comment 2016-07-03 16:09:53 +02:00
Florian Bruhin
dce3e0fb78 flake8 reqs: Update flake8-pep3101 to 0.4
- Rename pep8 to pycodestyle.
- Add support for python 3.5.
- Add flake8 pypi classifier.
- Drop python 3.3 and 3.4 support (only testing it probably works just
  fine).
- Fix travis and coveralls to work properly with python 3.5.
2016-07-03 16:09:16 +02:00
Florian Bruhin
596a3841dd flake8 reqs: Update flake8-future-import tp 0.4.3
- Restore old option name behaviour that an option like min-version in
  the tox.ini is recognized.
2016-07-03 16:06:28 +02:00
Ryan Roden-Corrent
07edcce697 Unit test Completer.update_completion.
Validate update_completion sets the completions widget's model
correctly based on the command text.
2016-07-02 12:25:55 -04:00
Daniel Schadt
ee9d3b6a49 quteprocess: replace Expected with expected 2016-07-02 17:22:40 +02:00
Daniel Schadt
4863df5ac8 quteprocess: also mark expected WARNINGs 2016-07-02 17:19:19 +02:00
Daniel Schadt
e2b521a408 fix lint 2016-07-02 16:51:58 +02:00
Daniel Schadt
bce06d6f43 quteproc: mark expected errors as such
Fixes #1611

This marks errors that are expected by a test with an "(Expected)"
marker and white color (instead of red).

The formatting of the log messages has been deferred to _render_log,
since the .expected attribute is not correctly set right after we read
the message.
2016-07-02 13:59:46 +02:00
Jan Verbeek
67ada03414 Add QUTE_DATA_DIR, QUTE_DOWNLOAD_DIR documentation 2016-07-02 13:20:11 +02:00
Ryan Roden-Corrent
6ce3ad68f8 Add a userscript to integrate with taskwarrior.
The `taskadd` userscript adds a task based on the current title and URL.
It passes additional arguments along to `task`.

For example:
    :spawn --userscript taskadd due:eod pri:H
will add a task with high priority due at the end of the day.
The description will be the current page title and it will be annotated
with the current page url.

If used with hints, the hint text is used as the description.

Unfortunately, there is currently no way to use :hint fill and maintain
the hint text, which limits the ability to provide additional args in
hint mode.
2016-07-01 22:38:25 -04:00
Jan Verbeek
4172e39045 Move :repeat-command tests to scroll.feature 2016-07-02 01:42:47 +02:00
Florian Bruhin
bd506b186b Merge branch 'edi9999-patch-2' 2016-07-01 23:34:18 +02:00
Florian Bruhin
3a73351779 Merge branch 'patch-2' of https://github.com/edi9999/qutebrowser into edi9999-patch-2 2016-07-01 22:53:49 +02:00
Florian Bruhin
e3500e8bdf backes.asciidoc: Add tuxlovesyou 2016-07-01 22:38:40 +02:00
Florian Bruhin
39fee34b91 flake8 reqs: Update flake8-tidy-imports to 1.0.1
- I201 rule that allows you to configure complaining about certain
  modules being imported, e.g. if you are moving from Python 2 to 3 you
  could stop urlparse being imported in favour of
  six.moves.urllib.parse.
2016-07-01 21:58:18 +02:00
Florian Bruhin
b262c34ed9 Fix crowdfunding link in README
Fixes #1615
2016-07-01 21:58:11 +02:00
Jan Verbeek
9678fd1e09 Make argument description more clear + style fix 2016-07-01 19:17:50 +02:00
Jan Verbeek
cc67dba9f1 Add and use $QUTE_DATA_DIR, $QUTE_DOWNLOAD_DIR
Also fix a few small issues in the userscripts like unquoted variables
and trailing whitespace.
2016-07-01 15:53:21 +02:00
Florian Bruhin
274644e83d tests: __tracebackhide__ in quteproc.after_test 2016-07-01 14:44:42 +02:00
Florian Bruhin
2ec820f366 Merge branch 'rcorre-completion_tests' 2016-07-01 14:42:42 +02:00
Florian Bruhin
a3b0e7c1cb Fix indent 2016-07-01 14:38:18 +02:00
Florian Bruhin
b178099f44 Merge branch 'completion_tests' of https://github.com/rcorre/qutebrowser into rcorre-completion_tests 2016-07-01 14:36:32 +02:00
Florian Bruhin
43812b6d2b behaviour -> behavior 2016-07-01 14:20:00 +02:00
Florian Bruhin
f1de4cc0cf Revert "Strip newline char with :edit-url"
This reverts commit ac9fee310d.
2016-07-01 13:52:08 +02:00
Florian Bruhin
ac9fee310d Strip newline char with :edit-url 2016-07-01 13:50:28 +02:00
Florian Bruhin
a6dbdc3e84 Update docs 2016-07-01 13:47:26 +02:00
Florian Bruhin
af37272246 Merge branch 'edit-url' of https://github.com/blyxxyz/qutebrowser into blyxxyz-edit-url 2016-07-01 13:40:00 +02:00
Florian Bruhin
1c2aca5e82 flake8 reqs: Blacklist flake8-future-import 0.4.2
0.4.2 ignores config options for flake8 2.6.2:
https://github.com/xZise/flake8-future-import/issues/8#issuecomment-229921673
2016-07-01 13:18:39 +02:00
Florian Bruhin
aa7282819e Add a 'backers' file for the crowdfunding 2016-07-01 12:38:08 +02:00
Ryan Roden-Corrent
d45acb0388 Eliminate FakeSettingSection/Value.
Don't really need to mock these out for tests as the real classes are
simple enough.
2016-06-30 20:12:44 -04:00
Ryan Roden-Corrent
94ec712ea8 Really clean up Qt view in completion tests.
Missed a spot.
2016-06-30 20:12:04 -04:00
Jan Verbeek
4f32d94f5f Remove obsolete :edit-url with count test 2016-06-30 22:58:05 +02:00
Florian Bruhin
4ae3df62c5 Handle invalid URLs in :jump-mark 2016-06-30 20:59:18 +02:00
Florian Bruhin
b527cf53d2 flake8 requirements: Update flake8-tuple to 0.2.10
pep8 -> pycodestyle
2016-06-30 18:33:58 +02:00
Florian Bruhin
1209d4192f test requirements: Update pytest-repeat to 0.3.0 2016-06-30 17:56:19 +02:00
Jan Verbeek
6fbbc3f123 Only load with :edit-url if the URL was changed, remove count argument 2016-06-30 17:48:06 +02:00
Florian Bruhin
dece5dda78 Add a docstring for pytest_bdd_apply_tag 2016-06-30 17:42:17 +02:00
Florian Bruhin
ead437be82 Don't define pytest_bdd_apply_tag when frozen
This should help with the undefined hook while not requiring to install
the pytest-bdd plugin when frozen.
2016-06-30 17:40:54 +02:00
Florian Bruhin
49d3e9ece8 Revert "Include pytest-bdd with frozen tests"
This reverts commit fcbb5b8bac.

This triggers a RecursionError when frozen...
2016-06-30 17:40:10 +02:00
Florian Bruhin
ce8315b720 Document :hint input and add default binding
See #1607, #1499
2016-06-30 17:33:51 +02:00
Florian Bruhin
fcbb5b8bac Include pytest-bdd with frozen tests
This is needed so the pytest_bdd_apply_hook in conftest is valid - BDD
tests are still skipped when frozen for now.
2016-06-30 16:34:42 +02:00
Florian Bruhin
50c1d85137 Use raw string for regex 2016-06-30 15:46:38 +02:00
Florian Bruhin
eb463ab2d3 Fix pytest-bdd tags 2016-06-30 14:47:55 +02:00
Florian Bruhin
8d9a699b5b test requirements: Update to pytest-bdd 2.17.0
This also allows us to have dynamic pyqt>=5.3.1 etc. tags.
2016-06-30 14:02:30 +02:00
Ryan Roden-Corrent
4178f73ed9 Move utility functions to top of test_completion.
Just a style change.
2016-06-30 07:26:02 -04:00
Ryan Roden-Corrent
849706e310 Clean up Qt view used in completion tests.
Add the view created during the tests to qtot so it gets cleaned up
after the test exits.
2016-06-30 07:26:02 -04:00
Ryan Roden-Corrent
ee59b133c0 Don't bother mocking a history entry.
Use webkit.history.Entry in tests instead of FakeHistoryEntry.
2016-06-30 07:26:02 -04:00
Ryan Roden-Corrent
f50b8bb8e8 Use iter instead of __iter__.
Corrent two places this was used.
2016-06-30 07:26:02 -04:00
Ryan Roden-Corrent
f94ee172c9 Add completion/models/base to perfect_files.
It now has 100% test coverage.
2016-06-30 07:26:02 -04:00
Ryan Roden-Corrent
d6012ad95c Test delete_cur_item more cleanly.
Reduce duplicate code by mocking out a QTreeView around the model and
setting its index.
2016-06-30 07:26:02 -04:00
Ryan Roden-Corrent
9d49f5a57d Test tab completion with multiple tabbed browsers. 2016-06-30 07:26:02 -04:00
Ryan Roden-Corrent
b6652ad6bc Test completion quickmark/bookmark deletion.
Validate that quickmarks and bookmarks can be deleted from the url
completion menu.
2016-06-30 07:26:02 -04:00
Ryan Roden-Corrent
409de10fb4 Unit test delete_cur_item for tab completion model. 2016-06-30 07:26:02 -04:00
Ryan Roden-Corrent
8321c1a90f Unit test setting value completion 2016-06-30 07:26:02 -04:00
Ryan Roden-Corrent
f5b1019d4d Unit test config option completion.
Had to add the `raw` parameter to ConfigStub.get as the setting option
completion model passes this argument explicitly (although the docs say
only raw=True is supported).
2016-06-30 07:26:02 -04:00
Ryan Roden-Corrent
5255a71bc5 Unit test setting section completion. 2016-06-30 07:26:02 -04:00
Ryan Roden-Corrent
d49fa7c4a3 Clean up objreg after test_history.
test_init in test_history was leaving a web-history in the objreg,
which was interfering with the completion tests.
2016-06-30 07:26:02 -04:00
Ryan Roden-Corrent
16f043034f Unit test tab (buffer) completion.
This involved adding a few stubs as well as expanding the FakeWebView.
2016-06-30 07:26:02 -04:00
Ryan Roden-Corrent
610f9b7068 Unit test session completion 2016-06-30 07:26:02 -04:00
Ryan Roden-Corrent
baf8d00a20 Unit test url/quickmark/bookmark completion.
This also adds a few more stubs/fakes to mock out the
quickmark-manager, bookmark-manager, and web-history objects.
2016-06-30 07:26:02 -04:00
Ryan Roden-Corrent
b037ec489f Add unit tests for help completion.
Also adds a FakeConfigSection stub for stubbing out configdata.DATA.
2016-06-30 07:26:02 -04:00
Ryan Roden-Corrent
555bdb75b5 Add unit test for CommandCompletionModel.
This establishes a pattern that can probably be used to test any of the
completion models.

See #999.
2016-06-30 07:26:02 -04:00
Florian Bruhin
f6fbb098cc Remove redundant sentence in docstring 2016-06-30 12:40:52 +02:00
Florian Bruhin
080f9f5bc2 Update docs 2016-06-30 12:40:20 +02:00
Florian Bruhin
900ad1ba6d Merge branch 'alias-multi-commands' of https://github.com/mgoral/qutebrowser into mgoral-alias-multi-commands 2016-06-30 12:34:18 +02:00
Florian Bruhin
3c43639cf1 Merge branch 'blyxxyz-scroll' 2016-06-30 12:27:53 +02:00
Florian Bruhin
f10841e003 Update docs 2016-06-30 12:27:43 +02:00
Florian Bruhin
99d921f169 Merge branch 'scroll' of https://github.com/blyxxyz/qutebrowser into blyxxyz-scroll 2016-06-30 12:18:15 +02:00
Florian Bruhin
7b63aea4ad travis: Allow OS X to fail again
I'm tired of trying to keep up with Homebrew packages...
2016-06-30 10:53:58 +02:00
Florian Bruhin
a0a53ad435 Merge branch 'Dietr1ch-fix-dvorak' 2016-06-30 10:02:07 +02:00
Florian Bruhin
3e167f7e2f Regenerate authors 2016-06-30 10:02:02 +02:00
Florian Bruhin
0f34e6b374 Merge branch 'fix-dvorak' of https://github.com/Dietr1ch/qutebrowser into Dietr1ch-fix-dvorak 2016-06-30 10:01:34 +02:00
Florian Bruhin
ad83950410 travis: Install v5.6.1_1-1 bottle
This should fix OS X builds.
2016-06-30 09:54:03 +02:00
Jan Verbeek
2ab1d35a7c Remove useless import and global declaration, add missing whitespace 2016-06-30 03:13:40 +02:00
Jan Verbeek
320b9cac3f Move repeat-command, make it work with multiple modes, improve tests 2016-06-30 02:22:02 +02:00
Florian Bruhin
03fbacd93c requirements: Update vulture to 0.9
* Don't flag attributes as unused if they are used as global variables in
  another module.
* Don't consider "True" and "False" variable names.
* Abort with error message when invoked on .pyc files.

This means we can remove the whitelisted globals in run_vulture.py and
the associated xfailing test.

We also needed to adjust run_vulture.py slightly as the file attribute
got renamed to filename.
2016-06-29 23:44:39 +02:00
Jan Verbeek
64731c2053 Fix confusing indent 2016-06-29 21:23:42 +02:00
Jan Verbeek
f9afa190b1 Handle check for allowed mode better 2016-06-29 20:58:29 +02:00
Jan Verbeek
cc1899ebca Add tests for repeat-command, make compatible with different count methods 2016-06-29 20:29:56 +02:00
Jan Verbeek
980cf5ada1 Documentation, check if config dir exists 2016-06-29 13:39:18 +02:00
Jan Verbeek
3c2c7ecaae Test for scroll with count and argument 2016-06-29 11:47:51 +02:00
Jan Verbeek
5e9fa2b57e Make gg accept count 2016-06-29 00:12:44 +02:00
Florian Bruhin
cd136b7b33 flake8 reqs: Update flake8-copyright to 0.2.0
Add flake8 v3.x compatibility
2016-06-28 20:48:52 +02:00
Florian Bruhin
87496617a4 Hide QtWebKit image format warning
This was hidden for tests in c390c797b2
but should be hidden for normal usage as well.
2016-06-28 20:46:06 +02:00
Jan Verbeek
7c350a29d5 Add $QUTE_CONFIG_DIR for userscripts (#937) 2016-06-27 22:16:25 +02:00
Jan Verbeek
7a6d26ef86 Fix protected-access for last_command (not sure what to do for _modes and _not_modes) 2016-06-27 18:40:47 +02:00
Jan Verbeek
81f25251a5 Fix singleton-comparison 2016-06-27 18:21:35 +02:00
Jan Verbeek
f654013372 Add repeat-command (.) command 2016-06-27 17:38:11 +02:00
Florian Bruhin
72e5bf35e1 flake8 requirements: Update pep8-naming to 0.4.1 2016-06-26 15:13:12 +02:00
Florian Bruhin
eda6fc6e17 flake8 requirements: Update flake8 to 2.6.2
- Update the config files to search for to include setup.cfg and
  tox.ini. This was broken in 2.5.5 when we stopped passing config_file
  to our Style Guide
2016-06-26 00:22:41 +02:00
Dietrich Daroch
bd8c576322 Fix Dvorak hint chars
- Use the whole home row
  - Produce easier chords
    - 2-key chords alternate hands first (f[aoeui][dhtns])
    - This is indeed a poor patch for easier chords as it should work on
      every home-row, but there are not that much homerows around.
2016-06-25 16:34:43 -04:00
Edgar Hipp
81c69421c5 Update test_readline.py 2016-06-24 12:00:53 +02:00
Florian Bruhin
d8a01d84b3 test requirements: Update CherryPy to 6.0.2
* Correct additional typos.
2016-06-24 11:17:03 +02:00
Edgar Hipp
bfeba3cee6 Update readline.py 2016-06-22 17:15:03 +02:00
Edgar Hipp
9e1c7e0117 Update readline.py 2016-06-22 16:30:38 +02:00
Florian Bruhin
2b285740d9 Update flake8 requirements
flake8 2.6.0
------------

- Switch to pycodestyle as all future pep8 releases will use that
  package name
- Allow for Windows users on select versions of Python to use --jobs and
  multiprocessing
- Update bounds on McCabe
- Update bounds on PyFlakes and blacklist known broken versions
- Handle new PyFlakes warning with a new error code: F405

flake8-copyright 0.1.1
----------------------

- Set line & column to 1,1 to avoid a flake8 error
- Support multi-year copyright notices

flake8-docstrings 0.2.8
-----------------------

- Try to import pydocstyle (not pycodestyle) as pep257
- Import either pycodestyle or pep8 to use stdin_get_value. This fixes
  the problem for newer Flake8’s (2.6.0+) and older ones.

pycodestyle 2.0.0
-----------------

Announcements:

- Repository renamed to `pycodestyle`
- Added joint Code of Conduct as member of PyCQA

Changes:

- Added tox test support for Python 3.5 and pypy3
- Added check E275 for whitespace on `from ... import ...` lines
- Added W503 to the list of codes ignored by default ignore list
- Removed use of project level `.pep8` configuration file

Bugs:

- Fixed bug with treating `~` operator as binary
- Identify binary operators as unary

Other changes
-------------

- Unpin mccabe as the flake8 requirement got updated
- Pin pep8 as an older version gets installed otherwise
- DIsable D403 warning (false-positivies)
2016-06-20 07:16:59 +02:00
Florian Bruhin
522b938974 pytest: Remove unnecessary norecursedirs 2016-06-17 11:56:43 +02:00
Florian Bruhin
8eee3f6bce Add a test for zooming in cloned tab 2016-06-15 14:44:01 +02:00
Florian Bruhin
f8301b185e flake8 requirements: Update flake8 to 2.5.5
- Fix setuptools integration when parsing config files
- Don't pass the user's config path as the config_file when creating a
  StyleGuide
2016-06-15 09:02:15 +02:00
Florian Bruhin
b099f64efa flake8 requirements: Update pyparsing to 2.1.5
- Added ParserElement.split() generator method, similar to re.split().
  Includes optional arguments maxsplit (to limit the number of splits),
  and includeSeparators (to include the separating matched text in the
  returned output, default=False).

- Added a new parse action construction helper tokenMap, which will
  apply a function and optional arguments to each element in a
  ParseResults. So this parse action:

      def lowercase_all(tokens):
          return [str(t).lower() for t in tokens]
      OneOrMore(Word(alphas)).setParseAction(lowercase_all)

  can now be written:

      OneOrMore(Word(alphas)).setParseAction(tokenMap(str.lower))

  Also simplifies writing conversion parse actions like:

      integer = Word(nums).setParseAction(lambda t: int(t[0]))

  to just:

      integer = Word(nums).setParseAction(tokenMap(int))

  If additional arguments are necessary, they can be included in the
  call to tokenMap, as in:

      hex_integer = Word(hexnums).setParseAction(tokenMap(int, 16))

- Added more expressions to pyparsing_common:
  . IPv4 and IPv6 addresses (including long, short, and mixed forms
    of IPv6)
  . MAC address
  . ISO8601 date and date time strings (with named fields for year, month, etc.)
  . UUID (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)
  . hex integer (returned as int)
  . fraction (integer '/' integer, returned as float)
  . mixed integer (integer '-' fraction, or just fraction, returned as float)
  . stripHTMLTags (parse action to remove tags from HTML source)
  . parse action helpers convertToDate and convertToDatetime to do custom parse
    time conversions of parsed ISO8601 strings

- runTests now returns a two-tuple: success if all tests succeed,
  and an output list of each test and its output lines.

- Added failureTests argument (default=False) to runTests, so that
  tests can be run that are expected failures, and runTests' success
  value will return True only if all tests *fail* as expected. Also,
  parseAll now defaults to True.

- New example numerics.py, shows samples of parsing integer and real
  numbers using locale-dependent formats:

    4.294.967.295,000
    4 294 967 295,000
    4,294,967,295.000
2016-06-14 09:48:14 +02:00
Florian Bruhin
5d4c1653e2 Regenerate authors 2016-06-14 09:36:18 +02:00
Ryan Roden-Corrent
9578a3324e Break two long lines that were failing pylint. 2016-06-13 22:31:21 -04:00
Michał Góral
9b394e4111 Use config_stub instead of monkeypatching config.get() 2016-06-13 19:39:00 +02:00
Florian Bruhin
f43f526c5b Exclude some misc/ stuff from releases 2016-06-13 11:32:27 +02:00
Florian Bruhin
4fccc89d7d Split browser into browser/browser.webkit 2016-06-13 11:18:21 +02:00
Florian Bruhin
f70a75978b Move browser.network.pastebin to misc 2016-06-13 09:48:53 +02:00
Florian Bruhin
77953e73f5 Merge branch 'djfinlay-master' 2016-06-13 09:34:44 +02:00
Florian Bruhin
8121b02f5c Update docs 2016-06-13 09:29:36 +02:00
Florian Bruhin
a83b86085a Merge branch 'master' of https://github.com/djfinlay/qutebrowser into djfinlay-master 2016-06-13 09:27:55 +02:00
Florian Bruhin
bc757b2b8c Use root-relative URLs for website header
Fixes #1265
Fixes #1489
2016-06-13 08:01:59 +02:00
Michał Góral
4b883c089e Fixed linters. 2016-06-12 23:04:14 +02:00
Michał Góral
370c8a8b07 Created tests for reading aliases. 2016-06-12 22:31:10 +02:00
Michał Góral
7ee99ba043 Moved searching for aliases to CommandRunner.parse_all()
This addresses issue with having alias for multiple commands splitted by ';;'.

See: The-Compiler/qutebrowser#956.
2016-06-12 21:09:10 +02:00
Daryl Finlay
f9a1c8b83a Update docs 2016-06-10 23:48:03 +01:00
Daryl Finlay
bf32c544a2 Refactor tab and window title update methods 2016-06-10 23:36:43 +01:00
Florian Bruhin
cfe360b95e travis: Update OS X bottle to Qt 5.6.1 2016-06-10 23:14:57 +02:00
Daryl Finlay
054e9ab439 Add host placeholder to window and tab title formats 2016-06-10 21:59:43 +01:00
Florian Bruhin
5c1401b0a1 Fix some stuff in CONTRIBUTING 2016-06-10 16:29:35 +02:00
219 changed files with 6380 additions and 2311 deletions

20
.flake8
View File

@@ -1,5 +1,5 @@
[flake8]
exclude = .venv,.hypothesis,.git,__pycache__,resources.py
exclude = .*,__pycache__,resources.py
# E128: continuation line under-indented for visual indent
# E226: missing whitespace around arithmetic operator
# E265: Block comment should start with '#'
@@ -8,17 +8,6 @@ exclude = .venv,.hypothesis,.git,__pycache__,resources.py
# E266: too many leading '#' for block comment
# F401: Unused import
# N802: function name should be lowercase
# L101: The __init__ method of classes must not have a docstring
# L102: A docstring was incorrectly formatted.
# L103: A test docstring must not start with any form of the words "test", ...
# L201: Container literals must have a trailing comma
# L202: print is not allowed except for debugging.
# L203: pdb and compatible modules are not allowed except for debugging.
# L204: Implicit string literal concatenation is only allowed if every string
# being concatenated is parenthesize
# L207: pass is only necessary in non-optional suites containing no other
# statements.
# L302: The line was too long.
# P101: format string does contain unindexed parameters
# P102: docstring does contain unindexed parameters
# P103: other string does contain unindexed parameters
@@ -30,6 +19,8 @@ exclude = .venv,.hypothesis,.git,__pycache__,resources.py
# D211: No blank lines allowed before class docstring
# (PEP257 got changed, but let's stick to the old standard)
# D402: First line should not be function's signature (false-positives)
# D403: First word of the first line should be properly capitalized
# (false-positives)
# H101: Use TODO(NAME)
# H201: bare except
# H238: Use new-stule classes
@@ -39,9 +30,8 @@ ignore =
E128,E226,E265,E501,E402,E266,
F401,
N802,
L101,L102,L103,L201,L202,L203,L204,L207,L302,
P101,P102,P103,
D102,D103,D104,D105,D209,D211,D402,
D102,D103,D104,D105,D209,D211,D402,D403,
H101,H201,H238,H301,H306
min-version = 3.4.0
max-complexity = 12
@@ -51,7 +41,7 @@ putty-ignore =
/# pylint: disable=wildcard-import/ : +F403
/# pragma: no mccabe/ : +C901
tests/*/test_*.py : +D100,D101,D401
tests/unit/browser/http/test_content_disposition.py : +D400
tests/unit/browser/webkit/http/test_content_disposition.py : +D400
scripts/dev/ci/appveyor_install.py : +FI53
copyright-check = True
copyright-regexp = # Copyright [\d-]+ .*

4
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,4 @@
Please remember to mention your version info (qutebrowser, Qt, PyQt,
OS/distribution) from the `qute:version` page or `qutebrowser --version`
---

View File

@@ -6,7 +6,10 @@ extension-pkg-whitelist=PyQt5,sip
load-plugins=qute_pylint.config,
qute_pylint.modeline,
qute_pylint.openencoding,
qute_pylint.settrace
qute_pylint.settrace,
pylint.extensions.bad_builtin,
pylint.extensions.docstyle
persistent=n
[MESSAGES CONTROL]
enable=all
@@ -14,6 +17,7 @@ disable=no-self-use,
fixme,
global-statement,
locally-disabled,
locally-enabled,
too-many-ancestors,
too-few-public-methods,
too-many-public-methods,
@@ -32,12 +36,14 @@ disable=no-self-use,
ungrouped-imports,
redefined-variable-type,
suppressed-message,
too-many-return-statements
too-many-return-statements,
duplicate-code,
wrong-import-position
[BASIC]
function-rgx=[a-z_][a-z0-9_]{2,50}$
const-rgx=[A-Za-z_][A-Za-z0-9_]{0,30}$
method-rgx=[a-z_][A-Za-z0-9_]{2,50}$
method-rgx=[a-z_][A-Za-z0-9_]{1,50}$
attr-rgx=[a-z_][a-z0-9_]{0,30}$
argument-rgx=[a-z_][a-z0-9_]{0,30}$
variable-rgx=[a-z_][a-z0-9_]{0,30}$
@@ -66,5 +72,5 @@ valid-metaclass-classmethod-first-arg=cls
# https://bitbucket.org/logilab/pylint/issues/690/
# UnsetObject because pylint infers any objreg.get(...) as UnsetObject.
ignored-classes=qutebrowser.utils.objreg.UnsetObject,
qutebrowser.browser.webelem.WebElementWrapper,
qutebrowser.browser.webkit.webelem.WebElementWrapper,
scripts.dev.check_coverage.MsgType

View File

@@ -12,9 +12,6 @@ matrix:
- os: linux
env: DOCKER=archlinux
services: docker
- os: linux
env: DOCKER=ubuntu-wily
services: docker
- os: linux
env: DOCKER=ubuntu-xenial
services: docker
@@ -63,8 +60,8 @@ notifications:
- https://buildtimetrend.herokuapp.com/travis
irc:
channels:
- "chat.freenode.net#qutebrowser"
on_success: change
- "chat.freenode.net#qutebrowser-dev"
on_success: always
on_failure: always
skip_join: true
template:

View File

@@ -14,6 +14,99 @@ This project adheres to http://semver.org/[Semantic Versioning].
// `Fixed` for any bug fixes.
// `Security` to invite users to upgrade in case of vulnerabilities.
v0.8.2
------
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
- Fixed local storage not working (and possible other bugs) when using a
relative path with `--basedir`.
- Fixed crash when deleting a quickmark with Ctrl-D
- Fixed HTML5 video playback on Windows
- Fixed crash when using `:prompt-open-download` with a file with chars not
encodable with the OS' filesystem encoding (e.g. with `LC_ALL=C`)
- Fixed `:prompt-open-download` with a too long filename (< 255 bytes)
- 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 HTML5 video playback on Windows
v0.8.1
------
Fixed
~~~~~
- Fix crash when pressing enter without a command
- Adjust error message to point out QtWebEngine is unsupported with the OS
X .app currently.
- Hide Harfbuzz warning with the OS X .app
v0.8.0
------
Added
~~~~~
- New `:repeat-command` command (mapped to `.`) to repeat the last command.
Note that two former default bundings conflict with that binding, unbinding
them via `:unbind .i` and `:unbind .o` is recommended.
- New `qute:bookmarks` page which displays all bookmarks and quickmarks.
- New `:prompt-open-download` (bound to `Ctrl-X`) which can be used to open a
download directly when getting the filename prompt.
- New `{host}` replacement for tab- and window titles which evaluates
to the current host.
- New default binding `;t` for `:hint input`.
- New variables `$QUTE_CONFIG_DIR`, `$QUTE_DATA_DIR` and
`$QUTE_DOWNLOAD_DIR` available for userscripts.
- New option `ui` -> `status-position` to configure the position of the
status bar (top/bottom).
- New `--pdf <filename>` argument for `:print` WHICH can be used to generate a
PDF without a dialog.
Changed
~~~~~~~
- `:scroll-perc` now prefers a count over the argument given to it, which means
`gg` can be used with a count.
- Aliases can now use `;;` to have an alias which executed multiple commands.
- `:edit-url` now does nothing if the URL isn't changed in the spawned editor.
- `:bookmark-add` can now be passed a URL and title to add that as a bookmark
rather than the current page.
- New `taskadd` userscript to add a taskwarrior task annotated with the
current URL.
- `:bookmark-del` and `:quickmark-del` now delete the current page's URL if none
is given.
Fixed
-----
- Compatibility with PyQt 5.7
- Fixed some configuration values being lost when a config option gets removed
from qutebrowser's code.
- Fix crash when downloading with a full disk
- Using `:jump-mark` (e.g. `''`) when the current URL is invalid doesn't crash
anymore.
Removed
-------
- The ability to display status messages from webpages, as well as the related
`ui -> display-statusbar-messages` setting.
- The `general -> wrap-search` setting as searches now always wrap.
According to a quick straw poll and prior crash logs, almost nobody is using
`wrap-search = false`, and turning off wrapping is not possible with
QtWebEngine.
- `:edit-url` now doesn't accept a count anymore as its behavior was confusing
and it doesn't make much sense to add a count.
v0.7.0
------
@@ -38,6 +131,7 @@ Added
- New `hints -> find-implementation` to select which implementation (JS/Python)
should be used to find hints on a page. The `javascript` implementation is
better, but slower.
- New `inputs` group for `:hint` to hint text input fields.
Changed
~~~~~~~
@@ -81,7 +175,7 @@ Fixed
`-v` argument for `:spawn` or pass flags to the spawned commands.
- Various fixes for hinting corner-cases where following a link didn't work or
the hint was drawn at the wrong position.
- Fixed crash when downloading from an URL with SSL errors
- Fixed crash when downloading from a URL with SSL errors
- Close file handles correctly when a download failed
- Fixed crash when using `;Y` (`:hint links yank-primary`) on a system without
primary selection
@@ -90,7 +184,7 @@ Fixed
- Fixed a crash when entering `:-- ` in the commandline
- Fixed `:debug-console` with PyQt 5.6
- Fixed qutebrowser not starting when `sys.stderr` is `None`
- Fixed crash when cancelling a download which belongs to a MHTML download
- Fixed crash when cancelling a download which belongs to an MHTML download
- Fixed rebinding of keybindings being case-sensitive
- Fix for tab indicators getting lost when moving tabs
- Fixed handling of backspace in number hinting mode
@@ -492,7 +586,7 @@ Fixed
~~~~~
- Scrolling should now work more reliably on some pages where arrow keys worked but `hjkl` didn't.
- Small improvements when checking if an input is an URL or not.
- Small improvements when checking if an input is a URL or not.
- Fixed wrong cursor position when completing the first item in the completion.
- Fixed exception when using search engines with {foo} in their name.
- Fixed a bug where the same title was shown for all tabs on some systems.
@@ -504,7 +598,7 @@ Fixed
- Various fixes for deprecated key bindings and auto-migrations.
- Workaround for qutebrowser not starting when there are NUL-bytes in the history (because of a currently unknown bug).
- Fixed handling of keybindings containing Ctrl/Meta on OS X.
- Fixed crash when downloading an URL without filename (e.g. magnet links) via "Save as...".
- Fixed crash when downloading a URL without filename (e.g. magnet links) via "Save as...".
- Fixed exception when starting qutebrowser with `:set` as argument.
- Fixed horrible completion performance when the `shrink` option was set.
- Sessions now store zoom/scroll-position correctly.
@@ -655,7 +749,7 @@ Fixed
- Scroll completion to top when showing it.
- Handle unencodable file paths in config types correctly.
- Fix for crash when executing a delayed command (because of a shadowed keybinding) and then unfocusing the window.
- Fix for crash when hinting on a page which doesn't have an URL yet.
- Fix for crash when hinting on a page which doesn't have a URL yet.
- Fix exception when using `:set-cmd-text` with an empty argument.
- Add a timeout to pastebin HTTP replies.
- Various other fixes for small/rare bugs.

View File

@@ -619,9 +619,9 @@ https://github.com/The-Compiler/qutebrowser/issues?q=is%3Aopen+is%3Aissue+label%
bugs] and check if they're fixed.
* As soon as Homebrew updated, update the custom OS X bottle:
- Update https://github.com/The-Compiler/homebrew-qt5-webkit/blob/master/Formula/qt5.rb[qt5.rb]
- `brew install --build-from-source --verbose qt5.rb`
- `brew bottle qt5`
- `brew install --build-from-source --verbose pyqt5`
- `brew install --build-from-source --build-bottle --verbose qt5.rb`
- `brew bottle qt5.rb`
- `brew install --build-from-source --build-bottle --verbose pyqt5`
- `brew bottle pyqt5`
- Upload bottles to github
- Adjust `scripts/dev/ci/travis_install.sh`
@@ -669,5 +669,5 @@ as closed.
- `scp bb-win8:proj/qutebrowser/qutebrowser-0.X.Y-windows.zip .`
- `aunpack qutebrowser-0.X.Y-windows.zip`
- `sudo mv qutebrowser-0.X.Y-windows/* /srv/http/qutebrowser/releases/v0.X.Y/windows`
* Update `qutebrowser-git` PKGBUILD
* Update `qutebrowser-git` PKGBUILD if dependencies/install changed
* Announce to qutebrowser mailinglist

View File

@@ -229,11 +229,17 @@ Then <<tox,install qutebrowser via tox>>.
On OS X
-------
To install qutebrowser on OS X, you'll want a package manager, e.g.
http://brew.sh/[Homebrew] or https://www.macports.org/[MacPorts].
The easiest way to install qutebrowser on OS X is to use the prebuilt `.app`
files from the
https://github.com/The-Compiler/qutebrowser/releases[release page].
Alternatively, you can install the dependencies via a package manager (like
http://brew.sh/[Homebrew] or https://www.macports.org/[MacPorts]) and run
qutebrowser from source.
For Homebrew, a few extra steps are necessary since Homebrew dropped QtWebKit
from Qt 5.6.
from Qt 5.6 - however, some users reported this didn't work for them, so using
the `.app` is strongly encouraged.
This installs a Qt 5.5 and symlinks it so PyQt5 will work with it instead of Qt
5.6. This requires that `qt5` is not installed via Homebrew:

View File

@@ -6,7 +6,8 @@ graft qutebrowser/html
graft qutebrowser/3rdparty
graft icons
graft doc/img
graft misc
graft misc/apparmor
graft misc/userscripts
recursive-include scripts *.py
include qutebrowser/utils/testfile
include qutebrowser/git-commit-id
@@ -15,16 +16,20 @@ include qutebrowser.desktop
include requirements.txt
include tox.ini
include qutebrowser.py
include misc/cheatsheet.svg
prune www
prune scripts/dev
prune scripts/testbrowser_cpp
prune .github
exclude scripts/asciidoc2html.py
exclude doc/notes
recursive-exclude doc *.asciidoc
include doc/qutebrowser.1.asciidoc
prune tests
prune qutebrowser/3rdparty
prune misc/requirements
prune misc/docker
exclude .editorconfig
exclude pytest.ini
exclude qutebrowser.rcc
@@ -38,6 +43,7 @@ exclude .travis.yml
exclude codecov.yml
exclude .pydocstylerc
exclude misc/appveyor_install.py
exclude misc/qutebrowser.spec
exclude .flake8
global-exclude __pycache__ *.pyc *.pyo

View File

@@ -141,8 +141,8 @@ Contributors, sorted by the number of commits in descending order:
// QUTE_AUTHORS_START
* Florian Bruhin
* Daniel Schadt
* Antoni Boucher
* Ryan Roden-Corrent
* Antoni Boucher
* Lamar Pavel
* Bruno Oliveira
* Alexander Cogneau
@@ -151,6 +151,7 @@ Contributors, sorted by the number of commits in descending order:
* Jakub Klinkovský
* Raphael Pierzina
* Joel Torstensson
* Jan Verbeek
* Tarcisio Fedrizzi
* Patric Schmitz
* Claude
@@ -164,6 +165,7 @@ Contributors, sorted by the number of commits in descending order:
* Kevin Velghe
* Austin Anderson
* Jimmy
* Marshall Lochbaum
* Alexey "Averrin" Nabrodov
* avk
* ZDarian
@@ -178,11 +180,14 @@ Contributors, sorted by the number of commits in descending order:
* skinnay
* Zach-Button
* Tomasz Kramkowski
* Ismail S
* Halfwit
* David Vogt
* rikn00
* kanikaa1234
* haitaka
* Nick Ginther
* Michał Góral
* Michael Ilsaas
* Martin Zimmermann
* Fritz Reichwald
@@ -194,16 +199,20 @@ Contributors, sorted by the number of commits in descending order:
* Stefan Tatschner
* Samuel Loury
* Peter Michely
* Panashe M. Fundira
* Link
* Larry Hynes
* Johannes Altmanninger
* Jeremy Kaplan
* Ismail
* Edgar Hipp
* Daryl Finlay
* adam
* Samir Benmendil
* Regina Hug
* Mathias Fussenegger
* Marcelo Santos
* Jan Verbeek
* Jean-Louis Fuchs
* Fritz V155 Reichwald
* Franz Fellner
* zwarag
@@ -229,6 +238,7 @@ Contributors, sorted by the number of commits in descending order:
* HalosGhost
* Gregor Pohl
* Eivind Uggedal
* Dietrich Daroch
* Daniel Lu
* Arseniy Seroka
* Andy Balaam
@@ -283,6 +293,7 @@ problems and helpful hints:
Also, thanks to:
* Everyone contributing to the link:doc/backers.asciidoc[crowdfunding].
* Everyone who had the patience to test qutebrowser before v0.1.
* Everyone triaging/fixing my bugs in the
https://bugreports.qt.io/secure/Dashboard.jspa[Qt bugtracker]

172
doc/backers.asciidoc Normal file
View File

@@ -0,0 +1,172 @@
Crowdfunding backers
====================
Mid-2016, qutebrowser did run a http://igg.me/at/qutebrowser[crowdfunding] for
QtWebEngine support in qutebrowser.
Thanks a lot to the following people who contributed to it:
Gold sponsors
-------------
- Chris Salzberg
- Clayton Craft
- Jean-Louis Fuchs
- Matthias Lisin
- 1 Anonymous
Day sponsors
------------
- Agent 42
- Iggy Jackson
- James B
- Rudi Seitz
- Tim „Das MooL“ Wegener
- amd1212
- gavtroy
- 4 Anonymous
Other sponsors
--------------
- AP M
- Alessandro Balzano
- Allan Nordhøy
- Andor Uhlar
- Andreas Leppert
- Andreas Saga Romsdal
- Andrew Rogers / tuxlovesyou
- André Glüpker
- Arian Sanusi
- Arin Lares
- Assaf Lavie
- Baptiste Wicht
- Benjamin Richter
- Benjamin Schnitzler
- Bernardo Kuri
- Boris Kourtoukov
- Brian Buccola
- Bruno Oliveira
- Bryan Gilbert
- Cassandra Rebecca Ruppen
- Charles Saternos
- Chris H
- Christian Karl
- Christian Lange
- Christian Strasser
- Colin O'Brien
- Corsin Pfister
- Cosmin Popescu
- Daniel Andersson
- David Wilson
- Demure Demeanor
- Doug Stone-Weaver
- Eero Kari
- Enric Morales
- Eric Krohn
- Eskild Hustvedt
- Federico Panico
- Felix Van der Jeugt
- Francis Tseng
- Geir Isene
- George Voronin
- German Correa
- Grady Martin
- Gregor Böhl
- Guilherme Stein
- Hannes Doyle
- Hasan Soydabas
- Ian Scott
- Jacob Boldman
- Jacob Wikmark
- Jan Verbeek
- Jarrod Seccombe
- Joel Bradshaw
- Johannes Martinsson
- Jonas Schürmann
- Josh Medeiros
- José Alberto Orejuela García
- Julie Engel
- Jörg Behrmann
- Jørgen Skancke
- Kevin Velghe
- Konstantin Shmelkov
- Kyle Frazer
- Lukas Gierth
- Mar v Leeuwaarde
- Marek Roszman
- Marius Betz
- Marius Krämer
- Markus Schmidinger
- Martin Gabelmann
- Martin Zimmermann
- Mathias Fußenegger
- Maxime Wack
- Michał Góral
- Nathan Isom
- Nathanael Philipp
- Nils Stål
- Oliver Hope
- Oskar Nyberg
- Pablo Navarro
- Panashe M. Fundira
- Patric Schmitz
- Pete M
- Peter Smith
- Phil Collins
- Philipp Hansch
- Philipp Kuhnz
- Raphael Khaiat
- Raphael Pierzina
- Renan Guilherme
- Rick Losie
- Robert Cross
- Roy Van Ginneken
- Rupus Reinefjord
- Ryan Roden-Corrent
- Samir Benmendil
- Simon Giotta
- Stephen England
- Sverrir H Steindorsson
- Tarcisio Fedrizzi
- Thorsten Wißmann
- Timon Stampfli
- Tjelvar Olsson
- Tomasz Kramkowski
- Tsukiko Tsutsukakushi
- Vasilij Schneidermann
- Vinney Cavallo
- Wesly Grefrath
- Will Ware
- Yousaf Khurshid
- Zach Schultz
- averrin
- ben hengst
- colin
- craigtski47
- dag.robole
- daniel.m.kao
- diepfann3
- eamonn oneil
- esakaforever
- francois47
- glspisso
- gmccoy4242
- gtcee3
- jonathf
- lapinski.maciej
- lauri.hakko
- ljanzen
- mutilx9
- nussgipfel
- oed
- p p
- r.c.bruno.andre
- robert.perce
- sghctoma
- targy
- freelancer
- pupu
- regines
- 37 Anonymous

View File

@@ -8,7 +8,7 @@
|<<adblock-update,adblock-update>>|Update the adblock block lists.
|<<back,back>>|Go back in the history of the current tab.
|<<bind,bind>>|Bind a key to a command.
|<<bookmark-add,bookmark-add>>|Save the current page as a bookmark.
|<<bookmark-add,bookmark-add>>|Save the current page as a bookmark, or a specific url.
|<<bookmark-del,bookmark-del>>|Delete a bookmark.
|<<bookmark-load,bookmark-load>>|Load a bookmark.
|<<buffer,buffer>>|Select tab by index or url/title best match.
@@ -116,16 +116,25 @@ Bind a key to a command.
[[bookmark-add]]
=== bookmark-add
Save the current page as a bookmark.
Syntax: +:bookmark-add ['url'] ['title']+
Save the current page as a bookmark, or a specific url.
If no url and title are provided, then save the current page as a bookmark. If a url and title have been provided, then save the given url as a bookmark with the provided title. You can view all saved bookmarks on the link:qute://bookmarks[bookmarks page].
==== positional arguments
* +'url'+: url to save as a bookmark. If None, use url of current page.
* +'title'+: title of the new bookmark.
[[bookmark-del]]
=== bookmark-del
Syntax: +:bookmark-del 'url'+
Syntax: +:bookmark-del ['url']+
Delete a bookmark.
==== positional arguments
* +'url'+: The URL of the bookmark to delete.
* +'url'+: The url of the bookmark to delete. If not given, use the current page's url.
==== note
* This command does not split arguments after the last argument and handles quotes literally.
@@ -243,9 +252,6 @@ The editor which should be launched can be configured via the `general -> editor
* +*-t*+, +*--tab*+: Open in a new tab.
* +*-w*+, +*--window*+: Open in a new window.
==== count
The tab index to open the URL in.
[[fake-key]]
=== fake-key
Syntax: +:fake-key [*--global*] 'keystring'+
@@ -308,6 +314,7 @@ Start hinting.
- `all`: All clickable elements.
- `links`: Only links.
- `images`: Only images.
- `inputs`: Only input fields.
@@ -489,12 +496,13 @@ If the pasted text contains newlines, each line gets opened in its own tab.
[[print]]
=== print
Syntax: +:print [*--preview*]+
Syntax: +:print [*--preview*] [*--pdf* 'file']+
Print the current/[count]th tab.
==== optional arguments
* +*-p*+, +*--preview*+: Show preview instead of printing.
* +*-f*+, +*--pdf*+: The file path to write the PDF to.
==== count
The tab index to print.
@@ -505,18 +513,22 @@ Syntax: +:quickmark-add 'url' 'name'+
Add a new quickmark.
You can view all saved quickmarks on the link:qute://bookmarks[bookmarks page].
==== positional arguments
* +'url'+: The url to add as quickmark.
* +'name'+: The name for the new quickmark.
[[quickmark-del]]
=== quickmark-del
Syntax: +:quickmark-del 'name'+
Syntax: +:quickmark-del ['name']+
Delete a quickmark.
==== positional arguments
* +'name'+: The name of the quickmark to delete.
* +'name'+: The name of the quickmark to delete. If not given, delete the quickmark for the current page (choosing one arbitrarily
if there are more than one).
==== note
* This command does not split arguments after the last argument and handles quotes literally.
@@ -924,7 +936,9 @@ How many steps to zoom out.
|<<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.
|<<prompt-yes,prompt-yes>>|Answer yes to a yes/no prompt.
|<<repeat-command,repeat-command>>|Repeat the last executed command.
|<<rl-backward-char,rl-backward-char>>|Move back a character.
|<<rl-backward-delete-char,rl-backward-delete-char>>|Delete the character before the cursor.
|<<rl-backward-word,rl-backward-word>>|Move back to the start of the current or previous word.
@@ -1147,10 +1161,21 @@ Accept the current prompt.
=== prompt-no
Answer no to a yes/no prompt.
[[prompt-open-download]]
=== prompt-open-download
Immediately open a download.
[[prompt-yes]]
=== prompt-yes
Answer yes to a yes/no prompt.
[[repeat-command]]
=== repeat-command
Repeat the last executed command.
==== count
Which count to pass the command.
[[rl-backward-char]]
=== rl-backward-char
Move back a character.

View File

@@ -5,7 +5,6 @@
|==============
|Setting|Description
|<<general-ignore-case,ignore-case>>|Whether to find text on a page case-insensitively.
|<<general-wrap-search,wrap-search>>|Whether to wrap finding text to the top when arriving at the end.
|<<general-startpage,startpage>>|The default page(s) to open at the start, separated by commas.
|<<general-default-page,default-page>>|The page to open if :open -t/-b/-w is used without URL. Use `about:blank` for a blank page.
|<<general-auto-search,auto-search>>|Whether to start a search when something else than a URL is entered.
@@ -33,10 +32,10 @@
|<<ui-zoom-levels,zoom-levels>>|The available zoom levels, separated by commas.
|<<ui-default-zoom,default-zoom>>|The default zoom level.
|<<ui-downloads-position,downloads-position>>|Where to show the downloaded files.
|<<ui-status-position,status-position>>|The position of the status bar.
|<<ui-message-timeout,message-timeout>>|Time (in ms) to show messages in the statusbar for.
|<<ui-message-unfocused,message-unfocused>>|Whether to show messages in unfocused windows.
|<<ui-confirm-quit,confirm-quit>>|Whether to confirm quitting the application.
|<<ui-display-statusbar-messages,display-statusbar-messages>>|Whether to display javascript statusbar messages.
|<<ui-zoom-text-only,zoom-text-only>>|Whether the zoom factor on a frame applies only to the text or to all content.
|<<ui-frame-flattening,frame-flattening>>|Whether to expand each subframe to its contents.
|<<ui-user-stylesheet,user-stylesheet>>|User stylesheet to use (absolute filename, filename relative to the config directory or CSS string). Will expand environment variables.
@@ -300,17 +299,6 @@ Valid values:
Default: +pass:[smart]+
[[general-wrap-search]]
=== wrap-search
Whether to wrap finding text to the top when arriving at the end.
Valid values:
* +true+
* +false+
Default: +pass:[true]+
[[general-startpage]]
=== startpage
The default page(s) to open at the start, separated by commas.
@@ -516,6 +504,17 @@ Valid values:
Default: +pass:[top]+
[[ui-status-position]]
=== status-position
The position of the status bar.
Valid values:
* +top+
* +bottom+
Default: +pass:[bottom]+
[[ui-message-timeout]]
=== message-timeout
Time (in ms) to show messages in the statusbar for.
@@ -546,17 +545,6 @@ Valid values:
Default: +pass:[never]+
[[ui-display-statusbar-messages]]
=== display-statusbar-messages
Whether to display javascript statusbar messages.
Valid values:
* +true+
* +false+
Default: +pass:[false]+
[[ui-zoom-text-only]]
=== zoom-text-only
Whether the zoom factor on a frame applies only to the text or to all content.
@@ -637,6 +625,7 @@ The format to use for the window title. The following placeholders are defined:
* `{title_sep}`: The string ` - ` if a title is set, empty otherwise.
* `{id}`: The internal window ID of this window.
* `{scroll_pos}`: The page scroll position.
* `{host}`: The host of the current web page.
Default: +pass:[{perc}{title}{title_sep}qutebrowser]+
@@ -1153,6 +1142,7 @@ The format to use for the tab title. The following placeholders are defined:
* `{index}`: The index of this tab.
* `{id}`: The internal tab ID of this tab.
* `{scroll_pos}`: The page scroll position.
* `{host}`: The host of the current web page.
Default: +pass:[{index}: {title}]+
@@ -1664,7 +1654,7 @@ Colors used in the UI.
A value can be in one of the following format:
* `#RGB`/`#RRGGBB`/`#RRRGGGBBB`/`#RRRRGGGGBBBB`
* A SVG color name as specified in http://www.w3.org/TR/SVG/types.html#ColorKeywords[the W3C specification].
* An SVG color name as specified in http://www.w3.org/TR/SVG/types.html#ColorKeywords[the W3C specification].
* transparent (no color)
* `rgb(r, g, b)` / `rgba(r, g, b, a)` (values 0-255 or percentages)
* `hsv(h, s, v)` / `hsva(h, s, v, a)` (values 0-255, hue 0-359)

View File

@@ -65,6 +65,9 @@ show it.
*--target* '{auto,tab,tab-bg,tab-silent,tab-bg-silent,window}'::
How URLs should be opened if there is already a qutebrowser instance running.
*--backend* '{webkit,webengine}'::
Which backend to use (webengine backend is EXPERIMENTAL!).
=== debug arguments
*-l* '{critical,error,warning,info,debug,vdebug}', *--loglevel* '{critical,error,warning,info,debug,vdebug}'::
Set loglevel

View File

@@ -50,7 +50,7 @@ $ git checkout symbols
$ export DEBUG_CFLAGS='-ggdb3 -fvar-tracking-assignments -Og'
$ export DEBUG_CXXFLAGS='-ggdb3 -fvar-tracking-assignments -Og'
$ cd qt5
$ makepkg -si --pkg qt5-base-debug,qt5-webkit-debug
$ makepkg -si --pkg qt5-base-debug,qt5-webkit-debug,qt5-webengine-debug
$ cd ../pyqt5
$ makepkg -si --pkg pyqt5-common-debug,python-pyqt5-debug
----
@@ -76,7 +76,7 @@ Server = http://qutebrowser.org/qt-debug/$arch
Then install the packages:
----
# pacman -Suy pyqt5-common-debug python-pyqt5-debug qt5-base-debug qt5-webkit-debug
# pacman -Suy pyqt5-common-debug python-pyqt5-debug qt5-base-debug qt5-webkit-debug,qt5-webengine-debug
----
The `-debug` packages conflict with the non-debug variants - it's safe to

View File

@@ -33,6 +33,9 @@ The following environment variables will be set when a userscript is launched:
- `QUTE_FIFO`: The FIFO or file to write commands to.
- `QUTE_HTML`: Path of a file containing the HTML source of the current page.
- `QUTE_TEXT`: Path of a file containing the plaintext of the current page.
- `QUTE_CONFIG_DIR`: Path of the directory containing qutebrowser's configuration.
- `QUTE_DATA_DIR`: Path of the directory containing qutebrowser's data.
- `QUTE_DOWNLOAD_DIR`: Path of the downloads directory.
In `command` mode:

View File

@@ -1,37 +0,0 @@
FROM ubuntu:wily
MAINTAINER Florian Bruhin <me@the-compiler.org>
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get -y update && \
apt-get -y dist-upgrade && \
apt-get -y install --no-install-recommends \
python3-pyqt5 \
python3-pyqt5.qtwebkit \
python-tox \
python3-sip \
xvfb \
git \
python3-setuptools \
wget \
herbstluftwm \
language-pack-en \
libjs-pdf \
dbus
RUN dbus-uuidgen --ensure
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 && \
cd qutebrowser.git && \
tox -e py34

View File

@@ -1,29 +1,26 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
ebb-lint==0.4.4
flake8==2.5.4
flake8-copyright==0.1
flake8==2.6.2 # rq.filter: < 3.0.0
flake8-copyright==0.2.0
flake8-debugger==1.4.0
flake8-deprecated==1.0
flake8-docstrings==0.2.6
flake8-future-import==0.4.1
flake8-docstrings==0.2.8
flake8-future-import==0.4.3
flake8-mock==0.2
flake8-pep3101==0.3
flake8-putty==0.3.2
flake8-pep3101==0.4
flake8-putty==0.4.0
flake8-string-format==0.2.2
flake8-tidy-imports==1.0.0
flake8-tuple==0.2.9
flake8-tidy-imports==1.0.2
flake8-tuple==0.2.12
hacking==0.11.0
intervaltree==2.1.0
mccabe==0.5.0
packaging==16.7
pbr==1.10.0
pep257==0.7.0 # still needed by flake8-docstrings but ignored
pep8==1.7.0
pep8-naming==0.3.3
pep8-naming==0.4.1
pycodestyle==2.0.0
pydocstyle==1.0.0
pyflakes==1.2.3
pyparsing==2.1.4
pyparsing==2.1.5
six==1.10.0
sortedcontainers==1.5.3
venusian==1.0

View File

@@ -1,5 +1,4 @@
ebb-lint
flake8
flake8<3.0.0
flake8-copyright
flake8-debugger
flake8-deprecated
@@ -16,6 +15,9 @@ pep8-naming
pydocstyle
pyflakes
mccabe==0.5.0
pep8==1.7.0
#@ comment: pep257 still needed by flake8-docstrings but ignored
# Waiting until hacking/flake8-tuple are updated
#@ filter: flake8 < 3.0.0

View File

@@ -1,9 +1,10 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
astroid==1.4.6
colorama==0.3.7
astroid==1.4.7
isort==4.2.5
lazy-object-proxy==1.2.2
pylint==1.5.6
mccabe==0.5.0
pylint==1.6.4
./scripts/dev/pylint_checkers
requests==2.10.0
six==1.10.0

View File

@@ -1,13 +1,13 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
beautifulsoup4==4.4.1
CherryPy==6.0.1
beautifulsoup4==4.5.0
CherryPy==7.1.0
coverage==4.1
decorator==4.0.10
Flask==0.10.1 # rq.filter: < 0.11.0
glob2==0.4.1
httpbin==0.4.1
hypothesis==3.4.0
hypothesis==3.4.2
itsdangerous==0.24
# Jinja2==2.8
Mako==1.0.4
@@ -16,17 +16,17 @@ parse==1.6.6
parse-type==0.3.4
py==1.4.31
pytest==2.9.2
pytest-bdd==2.16.1
pytest-bdd==2.17.0
pytest-catchlog==1.2.2
pytest-cov==2.2.1
pytest-cov==2.3.0
pytest-faulthandler==1.3.0
pytest-instafail==0.3.0
pytest-mock==1.1
pytest-qt==1.11.0
pytest-repeat==0.2
pytest-repeat==0.3.0
pytest-rerunfailures==2.0.0
pytest-travis-fold==1.2.0
pytest-xvfb==0.2.0
six==1.10.0
vulture==0.8.1
vulture==0.10
Werkzeug==0.11.10

View File

@@ -1,3 +1,3 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
vulture==0.8.1
vulture==0.10

View File

@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
# Copyright 2015 Zach-Button <zachrey.button@gmail.com>
#
@@ -39,10 +39,9 @@
[ -z "$QUTE_URL" ] && QUTE_URL='http://google.com'
url=$(echo "$QUTE_URL" | cat - ~/.config/qutebrowser/quickmarks ~/.local/share/qutebrowser/history | dmenu -l 15 -p qutebrowser)
url=$(echo $url | sed -E 's/[^ ]+ +//g' | egrep "https?:" || echo $url)
url=$(echo "$QUTE_URL" | cat - "$QUTE_CONFIG_DIR/quickmarks" "$QUTE_DATA_DIR/history" | dmenu -l 15 -p qutebrowser)
url=$(echo "$url" | sed -E 's/[^ ]+ +//g' | egrep "https?:" || echo "$url")
[ -z "${url// }" ] && exit
echo "open $url" >> "$QUTE_FIFO" || qutebrowser "$url"

View File

@@ -1,4 +1,4 @@
#!/bin/bash -e
#!/usr/bin/env bash
# Both standalone script and qutebrowser userscript that opens a rofi menu with
# all files from the download director and opens the selected file. It works
# both as a userscript and a standalone script that is called from outside of
@@ -18,7 +18,10 @@
# Thorsten Wißmann, 2015 (thorsten` on freenode)
# Any feedback is welcome!
set -e
# open a file from the download directory using rofi
DOWNLOAD_DIR=${DOWNLOAD_DIR:-$QUTE_DOWNLOAD_DIR}
DOWNLOAD_DIR=${DOWNLOAD_DIR:-$HOME/Downloads}
# the name of the rofi command
ROFI_CMD=${ROFI_CMD:-rofi}
@@ -49,7 +52,7 @@ die() {
if ! [ -d "$DOWNLOAD_DIR" ] ; then
die "Download directory »$DOWNLOAD_DIR« not found!"
fi
if ! $(which "${ROFI_CMD}" > /dev/null ) ; then
if ! which "${ROFI_CMD}" > /dev/null ; then
die "Rofi command »${ROFI_CMD}« not found in PATH!"
fi
@@ -109,5 +112,3 @@ fi
msg info "Opening »$file« (of type $filetype) with ${application%.desktop}"
xdg-open "$path" &

View File

@@ -1,4 +1,4 @@
#!/bin/bash -e
#!/usr/bin/env bash
help() {
blink=$'\e[1;31m' reset=$'\e[0m'
cat <<EOF
@@ -39,6 +39,7 @@ Configuration:
EOF
}
set -o errexit
set -o pipefail
shopt -s nocasematch # make regexp matching in bash case insensitive
@@ -61,7 +62,7 @@ die() {
}
javascript_escape() {
# print the first argument in a escaped way, such that it can safely
# print the first argument in an escaped way, such that it can safely
# be used within javascripts double quotes
sed "s,[\\\'\"],\\\&,g" <<< "$1"
}

View File

@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
# Copyright 2015 Zach-Button <zachrey.button@gmail.com>
#
@@ -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/>.
#
#
# This script fetches the unprocessed HTML source for a page and opens it in vim.
# :bind gf spawn --userscript qutebrowser_viewsource
#

View File

@@ -7,7 +7,6 @@
# If you would like to set a custom colorscheme/font use these dirs.
# https://github.com/halfwit/dotfiles/blob/master/.config/dmenu/bemenucolors
readonly confdir=${XDG_CONFIG_HOME:-$HOME/.config}
readonly datadir=${XDG_DATA_HOME:-$HOME/.local/share}
readonly optsfile=$confdir/dmenu/bemenucolors
@@ -15,17 +14,17 @@ create_menu() {
# Check quickmarks
while read -r url; do
printf -- '%s\n' "$url"
done < "$confdir"/qutebrowser/quickmarks
done < "$QUTE_CONFIG_DIR"/quickmarks
# Next bookmarks
while read -r url _; do
printf -- '%s\n' "$url"
done < "$confdir"/qutebrowser/bookmarks/urls
done < "$QUTE_CONFIG_DIR"/bookmarks/urls
# Finally history
while read -r _ url; do
printf -- '%s\n' "$url"
done < "$datadir"/qutebrowser/history
done < "$QUTE_DATA_DIR"/history
}
get_selection() {

36
misc/userscripts/taskadd Executable file
View File

@@ -0,0 +1,36 @@
#!/usr/bin/env bash
#
# Behavior:
# Userscript for qutebrowser which adds a task to taskwarrior.
# If run as a command (:spawn --userscript taskadd), it creates a new task
# with the description equal to the current page title and annotates it with
# the current page url. Additional arguments are passed along so you can add
# mods to the task (e.g. priority, due date, tags).
#
# Example:
# :spawn --userscript taskadd due:eod pri:H
#
# To enable passing along extra args, I suggest using a mapping like:
# :bind <somekey> set-cmd-text -s :spawn --userscript taskadd
#
# If run from hint mode, it uses the selected hint text as the description
# and the selected hint url as the annotation.
#
# Ryan Roden-Corrent (rcorre), 2016
# Any feedback is welcome!
#
# For more info on Taskwarrior, see http://taskwarrior.org/
# use either the current page title or the hint text as the task description
[[ $QUTE_MODE == 'hints' ]] && title=$QUTE_SELECTED_TEXT || title=$QUTE_TITLE
# try to add the task and grab the output
msg="$(task add $title $@ 2>&1)"
if [[ $? == 0 ]]; then
# annotate the new task with the url, send the output back to the browser
task +LATEST annotate "$QUTE_URL"
echo "message-info '$msg'" >> $QUTE_FIFO
else
echo "message-error '$msg'" >> $QUTE_FIFO
fi

View File

@@ -1,4 +1,4 @@
#!/bin/bash -e
#!/usr/bin/env bash
#
# Behavior:
# Userscript for qutebrowser which views the current web page in mpv using
@@ -24,6 +24,8 @@
# Thorsten Wißmann, 2015 (thorsten` on freenode)
# Any feedback is welcome!
set -e
if [ -z "$QUTE_FIFO" ] ; then
cat 1>&2 <<EOF
Error: $0 can not be run as a standalone script.

View File

@@ -1,5 +1,4 @@
[pytest]
norecursedirs = .tox .venv
addopts = --strict -rfEw --faulthandler-timeout=70 --instafail
markers =
gui: Tests using the GUI (e.g. spawning widgets)
@@ -13,7 +12,6 @@ markers =
frozen: Tests which can only be run if sys.frozen is True.
integration: Tests which test a bigger portion of code
end2end: End to end tests which run qutebrowser as subprocess
pyqt531_or_newer: Needs PyQt 5.3.1 or newer.
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
@@ -37,6 +35,6 @@ qt_log_ignore =
^Geoclue error: Process org\.freedesktop\.Geoclue\.Master exited with status 127
^QObject::connect: Cannot connect \(null\)::stateChanged\(QNetworkSession::State\) to QNetworkReplyHttpImpl::_q_networkSessionStateChanged\(QNetworkSession::State\)
^QXcbClipboard: Cannot transfer data, no data available
^Image of format '' blocked because it is not considered safe. If you are sure it is safe to do so, you can white-list the format by setting the environment variable QTWEBKIT_IMAGEFORMAT_WHITELIST=
^load glyph failed
qt_wait_signal_raising = true
xfail_strict = true

View File

@@ -28,7 +28,7 @@ __copyright__ = "Copyright 2014-2016 Florian Bruhin (The Compiler)"
__license__ = "GPL"
__maintainer__ = __author__
__email__ = "mail@qutebrowser.org"
__version_info__ = (0, 7, 0)
__version_info__ = (0, 8, 2)
__version__ = '.'.join(str(e) for e in __version_info__)
__description__ = "A keyboard-driven, vim-like browser based on PyQt5 and QtWebKit."

View File

@@ -46,8 +46,10 @@ 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, cookies, cache, adblock, history
from qutebrowser.browser.network import qutescheme, proxy, networkmanager
from qutebrowser.browser import urlmarks, adblock
from qutebrowser.browser.webkit import cookies, cache, history, downloads
from qutebrowser.browser.webkit.network import (qutescheme, proxy,
networkmanager)
from qutebrowser.mainwindow import mainwindow
from qutebrowser.misc import readline, ipc, savemanager, sessions, crashsignal
from qutebrowser.misc import utilcmds # pylint: disable=unused-import
@@ -62,12 +64,7 @@ qApp = None
def run(args):
"""Initialize everything and run the application."""
if args.version:
print(version.version(short=True))
print()
print()
print(qutebrowser.__copyright__)
print()
print(version.GPL_BOILERPLATE.strip())
print(version.version())
sys.exit(usertypes.Exit.ok)
if args.temp_basedir:
@@ -166,7 +163,7 @@ def _init_icon():
"""Initialize the icon of qutebrowser."""
icon = QIcon()
fallback_icon = QIcon()
for size in (16, 24, 32, 48, 64, 96, 128, 256, 512):
for size in [16, 24, 32, 48, 64, 96, 128, 256, 512]:
filename = ':/icons/qutebrowser-{}x{}.png'.format(size, size)
pixmap = QPixmap(filename)
qtutils.ensure_not_null(pixmap)
@@ -278,7 +275,7 @@ def process_pos_args(args, via_ipc=False, cwd=None, target_arg=None):
message.error('current', "Error in startup argument '{}': "
"{}".format(cmd, e))
else:
background = open_target in ('tab-bg', 'tab-bg-silent')
background = open_target in ['tab-bg', 'tab-bg-silent']
tabbed_browser.tabopen(url, background=background,
explicit=True)
@@ -340,7 +337,6 @@ def _save_version():
state_config['general']['version'] = qutebrowser.__version__
@pyqtSlot('QWidget*', 'QWidget*')
def on_focus_changed(_old, new):
"""Register currently focused main window in the object registry."""
if not isinstance(new, QWidget):
@@ -358,9 +354,8 @@ def on_focus_changed(_old, new):
_maybe_hide_mouse_cursor()
@pyqtSlot(QUrl)
def open_desktopservices_url(url):
"""Handler to open an URL via QDesktopServices."""
"""Handler to open a URL via QDesktopServices."""
win_id = mainwindow.get_window(via_ipc=True, force_window=False)
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
@@ -441,6 +436,8 @@ def _init_modules(args, crash_handler):
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)
def _init_late_modules(args):
@@ -479,7 +476,6 @@ class Quitter:
self._shutting_down = False
self._args = args
@pyqtSlot()
def on_last_window_closed(self):
"""Slot which gets invoked when the last window was closed."""
self.shutdown(last_window=True)
@@ -494,7 +490,7 @@ class Quitter:
else:
path = os.path.abspath(os.path.dirname(qutebrowser.__file__))
if not os.path.isdir(path):
# Probably running from an python egg.
# Probably running from a python egg.
return
for dirpath, _dirnames, filenames in os.walk(path):
@@ -527,7 +523,7 @@ class Quitter:
cwd = os.path.join(os.path.abspath(os.path.dirname(
qutebrowser.__file__)), '..')
if not os.path.isdir(cwd):
# Probably running from an python egg. Let's fallback to
# Probably running from a python egg. Let's fallback to
# cwd=None and see if that works out.
# See https://github.com/The-Compiler/qutebrowser/issues/323
cwd = None
@@ -714,6 +710,8 @@ class Quitter:
not restart):
atexit.register(shutil.rmtree, self._args.basedir,
ignore_errors=True)
# Delete temp download dir
objreg.get('temporary-downloads').cleanup()
# If we don't kill our custom handler here we might get segfaults
log.destroy.debug("Deactivating message handler...")
qInstallMessageHandler(None)

View File

@@ -1,6 +1,6 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
# Copyright 2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#

View File

@@ -27,7 +27,7 @@ import zipfile
import fnmatch
from qutebrowser.config import config
from qutebrowser.utils import objreg, standarddir, log, message
from qutebrowser.utils import objreg, standarddir, log, message, usertypes
from qutebrowser.commands import cmdutils, cmdexc
@@ -48,7 +48,7 @@ def guess_zip_filename(zf):
def get_fileobj(byte_io):
"""Get an usable file object to read the hosts file from."""
"""Get a usable file object to read the hosts file from."""
byte_io.seek(0) # rewind downloaded file
if zipfile.is_zipfile(byte_io):
byte_io.seek(0) # rewind what zipfile.is_zipfile did
@@ -210,7 +210,8 @@ class HostBlocker:
else:
fobj = io.BytesIO()
fobj.name = 'adblock: ' + url.host()
download = download_manager.get(url, fileobj=fobj,
target = usertypes.FileObjDownloadTarget(fobj)
download = download_manager.get(url, target=target,
auto_remove=True)
self._in_progress.append(download)
download.finished.connect(

View File

@@ -0,0 +1,641 @@
# 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/>.
"""Base class for a wrapper over QWebView/QWebEngineView."""
import itertools
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QUrl, QObject, QPoint
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QWidget, QLayout
from qutebrowser.keyinput import modeman
from qutebrowser.config import config
from qutebrowser.utils import utils, objreg, usertypes, message, log, qtutils
tab_id_gen = itertools.count(0)
def create(win_id, parent=None):
"""Get a QtWebKit/QtWebEngine tab object.
Args:
win_id: The window ID where the tab will be shown.
parent: The Qt parent to set.
"""
# Importing modules here so we don't depend on QtWebEngine without the
# argument and to avoid circular imports.
mode_manager = modeman.instance(win_id)
if objreg.get('args').backend == 'webengine':
from qutebrowser.browser.webengine import webenginetab
tab_class = webenginetab.WebEngineTab
else:
from qutebrowser.browser.webkit import webkittab
tab_class = webkittab.WebKitTab
return tab_class(win_id=win_id, mode_manager=mode_manager, parent=parent)
class WebTabError(Exception):
"""Base class for various errors."""
class WrapperLayout(QLayout):
"""A Qt layout which simply wraps a single widget.
This is used so the widget is hidden behind a AbstractTab API and can't
easily be accidentally accessed.
"""
def __init__(self, widget, parent=None):
super().__init__(parent)
self._widget = widget
def addItem(self, _widget):
raise AssertionError("Should never be called!")
def sizeHint(self):
return self._widget.sizeHint()
def itemAt(self, _index): # pragma: no cover
# For some reason this sometimes gets called by Qt.
return None
def takeAt(self, _index):
raise AssertionError("Should never be called!")
def setGeometry(self, rect):
self._widget.setGeometry(rect)
class TabData:
"""A simple namespace with a fixed set of attributes.
Attributes:
keep_icon: Whether the (e.g. cloned) icon should not be cleared on page
load.
inspector: The QWebInspector used for this webview.
viewing_source: Set if we're currently showing a source view.
"""
__slots__ = ['keep_icon', 'viewing_source', 'inspector']
def __init__(self):
self.keep_icon = False
self.viewing_source = False
self.inspector = None
class AbstractPrinting:
"""Attribute of AbstractTab for printing the page."""
def __init__(self):
self._widget = None
def check_pdf_support(self):
raise NotImplementedError
def check_printer_support(self):
raise NotImplementedError
def to_pdf(self, filename):
raise NotImplementedError
def to_printer(self, printer):
raise NotImplementedError
class AbstractSearch(QObject):
"""Attribute of AbstractTab for doing searches.
Attributes:
text: The last thing this view was searched for.
_flags: The flags of the last search (needs to be set by subclasses).
_widget: The underlying WebView widget.
"""
def __init__(self, parent=None):
super().__init__(parent)
self._widget = None
self.text = None
def search(self, text, *, ignore_case=False, reverse=False,
result_cb=None):
"""Find the given text on the page.
Args:
text: The text to search for.
ignore_case: Search case-insensitively. (True/False/'smart')
reverse: Reverse search direction.
result_cb: Called with a bool indicating whether a match was found.
"""
raise NotImplementedError
def clear(self):
"""Clear the current search."""
raise NotImplementedError
def prev_result(self, *, result_cb=None):
"""Go to the previous result of the current search.
Args:
result_cb: Called with a bool indicating whether a match was found.
"""
raise NotImplementedError
def next_result(self, *, result_cb=None):
"""Go to the next result of the current search.
Args:
result_cb: Called with a bool indicating whether a match was found.
"""
raise NotImplementedError
class AbstractZoom(QObject):
"""Attribute of AbstractTab for controlling zoom.
Attributes:
_neighborlist: A NeighborList with the zoom levels.
_default_zoom_changed: Whether the zoom was changed from the default.
"""
def __init__(self, win_id, parent=None):
super().__init__(parent)
self._widget = None
self._win_id = win_id
self._default_zoom_changed = False
self._init_neighborlist()
objreg.get('config').changed.connect(self._on_config_changed)
# # FIXME:qtwebengine is this needed?
# # For some reason, this signal doesn't get disconnected automatically
# # when the WebView is destroyed on older PyQt versions.
# # See https://github.com/The-Compiler/qutebrowser/issues/390
# self.destroyed.connect(functools.partial(
# cfg.changed.disconnect, self.init_neighborlist))
@pyqtSlot(str, str)
def _on_config_changed(self, section, option):
if section == 'ui' and option in ['zoom-levels', 'default-zoom']:
if not self._default_zoom_changed:
factor = float(config.get('ui', 'default-zoom')) / 100
self._set_factor_internal(factor)
self._default_zoom_changed = False
self._init_neighborlist()
def _init_neighborlist(self):
"""Initialize self._neighborlist."""
levels = config.get('ui', 'zoom-levels')
self._neighborlist = usertypes.NeighborList(
levels, mode=usertypes.NeighborList.Modes.edge)
self._neighborlist.fuzzyval = config.get('ui', 'default-zoom')
def offset(self, offset):
"""Increase/Decrease the zoom level by the given offset.
Args:
offset: The offset in the zoom level list.
Return:
The new zoom percentage.
"""
level = self._neighborlist.getitem(offset)
self.set_factor(float(level) / 100, fuzzyval=False)
return level
def set_factor(self, factor, *, fuzzyval=True):
"""Zoom to a given zoom factor.
Args:
factor: The zoom factor as float.
fuzzyval: Whether to set the NeighborLists fuzzyval.
"""
if fuzzyval:
self._neighborlist.fuzzyval = int(factor * 100)
if factor < 0:
raise ValueError("Can't zoom to factor {}!".format(factor))
self._default_zoom_changed = True
self._set_factor_internal(factor)
def factor(self):
raise NotImplementedError
def set_default(self):
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):
"""Attribute of AbstractTab for caret browsing."""
def __init__(self, win_id, tab, mode_manager, parent=None):
super().__init__(parent)
self._tab = tab
self._win_id = win_id
self._widget = None
self.selection_enabled = False
mode_manager.entered.connect(self._on_mode_entered)
mode_manager.left.connect(self._on_mode_left)
def _on_mode_entered(self, mode):
raise NotImplementedError
def _on_mode_left(self):
raise NotImplementedError
def move_to_next_line(self, count=1):
raise NotImplementedError
def move_to_prev_line(self, count=1):
raise NotImplementedError
def move_to_next_char(self, count=1):
raise NotImplementedError
def move_to_prev_char(self, count=1):
raise NotImplementedError
def move_to_end_of_word(self, count=1):
raise NotImplementedError
def move_to_next_word(self, count=1):
raise NotImplementedError
def move_to_prev_word(self, count=1):
raise NotImplementedError
def move_to_start_of_line(self):
raise NotImplementedError
def move_to_end_of_line(self):
raise NotImplementedError
def move_to_start_of_next_block(self, count=1):
raise NotImplementedError
def move_to_start_of_prev_block(self, count=1):
raise NotImplementedError
def move_to_end_of_next_block(self, count=1):
raise NotImplementedError
def move_to_end_of_prev_block(self, count=1):
raise NotImplementedError
def move_to_start_of_document(self):
raise NotImplementedError
def move_to_end_of_document(self):
raise NotImplementedError
def toggle_selection(self):
raise NotImplementedError
def drop_selection(self):
raise NotImplementedError
def has_selection(self):
raise NotImplementedError
def selection(self, html=False):
raise NotImplementedError
def follow_selected(self, *, tab=False):
raise NotImplementedError
class AbstractScroller(QObject):
"""Attribute of AbstractTab to manage scroll position."""
perc_changed = pyqtSignal(int, int)
def __init__(self, tab, parent=None):
super().__init__(parent)
self._tab = tab
self._widget = None
def _init_widget(self, widget):
self._widget = widget
def pos_px(self):
raise NotImplementedError
def pos_perc(self):
raise NotImplementedError
def to_perc(self, x=None, y=None):
raise NotImplementedError
def to_point(self, point):
raise NotImplementedError
def delta(self, x=0, y=0):
raise NotImplementedError
def delta_page(self, x=0, y=0):
raise NotImplementedError
def up(self, count=1):
raise NotImplementedError
def down(self, count=1):
raise NotImplementedError
def left(self, count=1):
raise NotImplementedError
def right(self, count=1):
raise NotImplementedError
def top(self):
raise NotImplementedError
def bottom(self):
raise NotImplementedError
def page_up(self, count=1):
raise NotImplementedError
def page_down(self, count=1):
raise NotImplementedError
def at_top(self):
raise NotImplementedError
def at_bottom(self):
raise NotImplementedError
class AbstractHistory:
"""The history attribute of a AbstractTab."""
def __init__(self, tab):
self._tab = tab
self._history = None
def __len__(self):
return len(self._history)
def __iter__(self):
return iter(self._history.items())
def current_idx(self):
raise NotImplementedError
def back(self):
raise NotImplementedError
def forward(self):
raise NotImplementedError
def can_go_back(self):
raise NotImplementedError
def can_go_forward(self):
raise NotImplementedError
def serialize(self):
"""Serialize into an opaque format understood by self.deserialize."""
raise NotImplementedError
def deserialize(self, data):
"""Serialize from a format produced by self.serialize."""
raise NotImplementedError
def load_items(self, items):
"""Deserialize from a list of WebHistoryItems."""
raise NotImplementedError
class AbstractTab(QWidget):
"""A wrapper over the given widget to hide its API and expose another one.
We use this to unify QWebView and QWebEngineView.
Attributes:
history: The AbstractHistory for the current tab.
registry: The ObjectRegistry associated with this tab.
_load_status: loading status of this page
Accessible via load_status() method.
_has_ssl_errors: Whether SSL errors happened.
Needs to be set by subclasses.
for properties, see WebView/WebEngineView docs.
Signals:
See related Qt signals.
new_tab_requested: Emitted when a new tab should be opened with the
given URL.
load_status_changed: The loading status changed
"""
window_close_requested = pyqtSignal()
link_hovered = pyqtSignal(str)
load_started = pyqtSignal()
load_progress = pyqtSignal(int)
load_finished = pyqtSignal(bool)
icon_changed = pyqtSignal(QIcon)
title_changed = pyqtSignal(str)
load_status_changed = pyqtSignal(str)
new_tab_requested = pyqtSignal(QUrl)
url_changed = pyqtSignal(QUrl)
shutting_down = pyqtSignal()
def __init__(self, win_id, parent=None):
self.win_id = win_id
self.tab_id = next(tab_id_gen)
super().__init__(parent)
self.registry = objreg.ObjectRegistry()
tab_registry = objreg.get('tab-registry', scope='window',
window=win_id)
tab_registry[self.tab_id] = self
objreg.register('tab', self, registry=self.registry)
# self.history = AbstractHistory(self)
# self.scroller = AbstractScroller(self, parent=self)
# self.caret = AbstractCaret(win_id=win_id, tab=self, mode_manager=...,
# parent=self)
# self.zoom = AbstractZoom(win_id=win_id)
# self.search = AbstractSearch(parent=self)
# self.printing = AbstractPrinting()
self.data = TabData()
self._layout = None
self._widget = None
self._progress = 0
self._has_ssl_errors = False
self._load_status = usertypes.LoadStatus.none
self.backend = None
def _set_widget(self, widget):
# pylint: disable=protected-access
self._layout = WrapperLayout(widget, self)
self._widget = widget
self.history._history = widget.history()
self.scroller._init_widget(widget)
self.caret._widget = widget
self.zoom._widget = widget
self.search._widget = widget
self.printing._widget = widget
widget.mouse_wheel_zoom.connect(self.zoom._on_mouse_wheel_zoom)
widget.setParent(self)
self.setFocusProxy(widget)
def _set_load_status(self, val):
"""Setter for load_status."""
if not isinstance(val, usertypes.LoadStatus):
raise TypeError("Type {} is no LoadStatus member!".format(val))
log.webview.debug("load status for {}: {}".format(repr(self), val))
self._load_status = val
self.load_status_changed.emit(val.name)
@pyqtSlot(QUrl)
def _on_url_changed(self, url):
"""Update title when URL has changed and no title is available."""
if url.isValid() and not self.title():
self.title_changed.emit(url.toDisplayString())
self.url_changed.emit(url)
@pyqtSlot()
def _on_load_started(self):
self._progress = 0
self._has_ssl_errors = False
self.data.viewing_source = False
self._set_load_status(usertypes.LoadStatus.loading)
self.load_started.emit()
@pyqtSlot(bool)
def _on_load_finished(self, ok):
if ok and not self._has_ssl_errors:
if self.url().scheme() == 'https':
self._set_load_status(usertypes.LoadStatus.success_https)
else:
self._set_load_status(usertypes.LoadStatus.success)
elif ok:
self._set_load_status(usertypes.LoadStatus.warn)
else:
self._set_load_status(usertypes.LoadStatus.error)
self.load_finished.emit(ok)
if not self.title():
self.title_changed.emit(self.url().toDisplayString())
@pyqtSlot(int)
def _on_load_progress(self, perc):
self._progress = perc
self.load_progress.emit(perc)
@pyqtSlot()
def _on_ssl_errors(self):
self._has_ssl_errors = True
def url(self):
raise NotImplementedError
def progress(self):
return self._progress
def load_status(self):
return self._load_status
def _openurl_prepare(self, url):
qtutils.ensure_valid(url)
self.title_changed.emit(url.toDisplayString())
def openurl(self, url):
raise NotImplementedError
def reload(self, *, force=False):
raise NotImplementedError
def stop(self):
raise NotImplementedError
def clear_ssl_errors(self):
raise NotImplementedError
def dump_async(self, callback, *, plain=False):
"""Dump the current page to a file ascync.
The given callback will be called with the result when dumping is
complete.
"""
raise NotImplementedError
def run_js_async(self, code, callback=None):
"""Run javascript async.
The given callback will be called with the result when running JS is
complete.
"""
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
def title(self):
raise NotImplementedError
def icon(self):
raise NotImplementedError
def set_html(self, html, base_url):
raise NotImplementedError
def __repr__(self):
try:
url = utils.elide(self.url().toDisplayString(QUrl.EncodeUnicode),
100)
except AttributeError:
url = '<AttributeError>'
return utils.get_repr(self, tab_id=self.tab_id, url=url)

File diff suppressed because it is too large Load Diff

View File

@@ -25,7 +25,7 @@ import sip
from PyQt5.QtCore import pyqtSlot, QSize, Qt, QTimer
from PyQt5.QtWidgets import QListView, QSizePolicy, QMenu
from qutebrowser.browser import downloads
from qutebrowser.browser.webkit import downloads
from qutebrowser.config import style
from qutebrowser.utils import qtutils, utils, objreg

View File

@@ -33,7 +33,7 @@ from PyQt5.QtWebKitWidgets import QWebPage
from qutebrowser.config import config
from qutebrowser.keyinput import modeman, modeparsers
from qutebrowser.browser import webelem
from qutebrowser.browser.webkit import webelem
from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners
from qutebrowser.utils import usertypes, log, qtutils, message, objreg, utils
@@ -52,7 +52,6 @@ class WordHintingError(Exception):
"""Exception raised on errors during word hinting."""
@pyqtSlot(usertypes.KeyMode)
def on_mode_entered(mode, win_id):
"""Stop hinting when insert mode was entered."""
if mode == usertypes.KeyMode.insert:
@@ -84,6 +83,7 @@ class HintContext:
args: Custom arguments for userscript/spawn
rapid: Whether to do rapid hinting.
mainframe: The main QWebFrame where we started hinting in.
tab: The WebTab object we started hinting in.
group: The group of web elements to hint.
"""
@@ -98,6 +98,7 @@ class HintContext:
self.destroyed_frames = []
self.args = []
self.mainframe = None
self.tab = None
self.group = None
def get_args(self, urlstr):
@@ -199,6 +200,7 @@ class HintManager(QObject):
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.
@@ -569,7 +571,6 @@ class HintManager(QObject):
"""
cmd = context.args[0]
args = context.args[1:]
frame = context.mainframe
env = {
'QUTE_MODE': 'hints',
'QUTE_SELECTED_TEXT': str(elem),
@@ -578,8 +579,12 @@ class HintManager(QObject):
url = self._resolve_url(elem, context.baseurl)
if url is not None:
env['QUTE_URL'] = url.toString(QUrl.FullyEncoded)
env.update(userscripts.store_source(frame))
userscripts.run(cmd, *args, win_id=self._win_id, env=env)
try:
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)
def _spawn(self, url, context):
"""Spawn a simple command from a hint.
@@ -603,7 +608,7 @@ class HintManager(QObject):
Return:
A QUrl with the absolute URL, or None.
"""
for attr in ('href', 'src'):
for attr in ['href', 'src']:
if attr in elem:
text = elem[attr].strip()
break
@@ -672,8 +677,8 @@ class HintManager(QObject):
"""
if not isinstance(target, Target):
raise TypeError("Target {} is no Target member!".format(target))
if target in (Target.userscript, Target.spawn, Target.run,
Target.fill):
if target in [Target.userscript, Target.spawn, Target.run,
Target.fill]:
if not args:
raise cmdexc.CommandError(
"'args' is required with target userscript/spawn/run/"
@@ -752,12 +757,13 @@ class HintManager(QObject):
window=self._win_id)
tabbed_browser.tabopen(url, background=background)
else:
webview = objreg.get('webview', scope='tab', window=self._win_id,
tab=self._tab_id)
webview.openurl(url)
tab = objreg.get('tab', scope='tab', window=self._win_id,
tab=self._tab_id)
tab.openurl(url)
@cmdutils.register(instance='hintmanager', scope='tab', name='hint',
star_args_optional=True, maxsplit=2)
star_args_optional=True, maxsplit=2,
backend=usertypes.Backend.QtWebKit)
@cmdutils.argument('win_id', win_id=True)
def start(self, rapid=False, group=webelem.Group.all, target=Target.normal,
*args, win_id):
@@ -772,6 +778,7 @@ class HintManager(QObject):
- `all`: All clickable elements.
- `links`: Only links.
- `images`: Only images.
- `inputs`: Only input fields.
target: What to do with the selected element.
@@ -810,10 +817,12 @@ class HintManager(QObject):
"""
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=self._win_id)
widget = tabbed_browser.currentWidget()
if widget is None:
tab = tabbed_browser.currentWidget()
if tab is None:
raise cmdexc.CommandError("No WebView available yet!")
mainframe = widget.page().mainFrame()
# FIXME:qtwebengine have a proper API for this
page = tab._widget.page() # pylint: disable=protected-access
mainframe = page.mainFrame()
if mainframe is None:
raise cmdexc.CommandError("No frame focused!")
mode_manager = objreg.get('mode-manager', scope='window',
@@ -836,6 +845,7 @@ class HintManager(QObject):
self._check_args(target, *args)
self._context = HintContext()
self._context.tab = tab
self._context.target = target
self._context.rapid = rapid
try:

View File

@@ -23,7 +23,7 @@ import os
from PyQt5.QtCore import QUrl
from qutebrowser.browser import webelem
from qutebrowser.browser.webkit import webelem
from qutebrowser.utils import utils
@@ -69,7 +69,7 @@ def _generate_pdfjs_script(url):
def fix_urls(asset):
"""Take a html page and replace each relative URL with an absolute.
"""Take an html page and replace each relative URL with an absolute.
This is specialized for pdf.js files and not a general purpose function.

View File

@@ -40,8 +40,7 @@ class SignalFilter(QObject):
BLACKLIST: List of signal names which should not be logged.
"""
BLACKLIST = ['cur_scroll_perc_changed', 'cur_progress',
'cur_statusbar_message', 'cur_link_hovered']
BLACKLIST = ['cur_scroll_perc_changed', 'cur_progress', 'cur_link_hovered']
def __init__(self, win_id, parent=None):
super().__init__(parent)

View File

@@ -32,8 +32,9 @@ import collections
from PyQt5.QtCore import pyqtSignal, QUrl, QObject
from qutebrowser.utils import message, usertypes, urlutils, standarddir, objreg
from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.utils import (message, usertypes, qtutils, urlutils,
standarddir, objreg)
from qutebrowser.commands import cmdutils
from qutebrowser.misc import lineparser
@@ -178,6 +179,9 @@ class QuickmarkManager(UrlMarkManager):
def quickmark_add(self, win_id, url, name):
"""Add a new quickmark.
You can view all saved quickmarks on the
link:qute://bookmarks[bookmarks page].
Args:
win_id: The window ID to display the errors in.
url: The url to add as quickmark.
@@ -204,19 +208,22 @@ class QuickmarkManager(UrlMarkManager):
else:
set_mark()
@cmdutils.register(instance='quickmark-manager', maxsplit=0)
@cmdutils.argument('name',
completion=usertypes.Completion.quickmark_by_name)
def quickmark_del(self, name):
"""Delete a quickmark.
def get_by_qurl(self, url):
"""Look up a quickmark by QUrl, returning its name.
Args:
name: The name of the quickmark to delete.
Takes O(n) time, where n is the number of quickmarks.
Use a name instead where possible.
"""
qtutils.ensure_valid(url)
urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded)
try:
self.delete(name)
except KeyError:
raise cmdexc.CommandError("Quickmark '{}' not found!".format(name))
index = list(self.marks.values()).index(urlstr)
key = list(self.marks.keys())[index]
except ValueError:
raise DoesNotExistError(
"Quickmark for '{}' not found!".format(urlstr))
return key
def get(self, name):
"""Get the URL of the quickmark named name as a QUrl."""
@@ -284,16 +291,3 @@ class BookmarkManager(UrlMarkManager):
self.marks[urlstr] = title
self.changed.emit()
self.added.emit(title, urlstr)
@cmdutils.register(instance='bookmark-manager', maxsplit=0)
@cmdutils.argument('url', completion=usertypes.Completion.bookmark_by_url)
def bookmark_del(self, url):
"""Delete a bookmark.
Args:
url: The URL of the bookmark to delete.
"""
try:
self.delete(url)
except KeyError:
raise cmdexc.CommandError("Bookmark '{}' not found!".format(url))

View File

@@ -0,0 +1,20 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Classes related to the browser widgets for QtWebEngine."""

View File

@@ -0,0 +1,431 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
# FIXME:qtwebengine remove this once the stubs are gone
# pylint: disable=unused-variable
"""Wrapper over a QWebEngineView."""
from PyQt5.QtCore import pyqtSlot, Qt, QEvent, QPoint
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
# pylint: enable=no-name-in-module,import-error,useless-suppression
from qutebrowser.browser import browsertab
from qutebrowser.browser.webengine import webview
from qutebrowser.utils import usertypes, qtutils, log, utils
class WebEnginePrinting(browsertab.AbstractPrinting):
"""QtWebEngine implementations related to printing."""
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")
def check_printer_support(self):
raise browsertab.WebTabError(
"Printing is unsupported with QtWebEngine")
def to_pdf(self, filename):
self._widget.page().printToPdf(filename)
def to_printer(self, printer):
# Should never be called
assert False
class WebEngineSearch(browsertab.AbstractSearch):
"""QtWebEngine implementations related to searching on the page."""
def __init__(self, parent=None):
super().__init__(parent)
self._flags = QWebEnginePage.FindFlags(0)
def _find(self, text, flags, cb=None):
"""Call findText on the widget with optional callback."""
if cb is None:
self._widget.findText(text, flags)
else:
self._widget.findText(text, flags, cb)
def search(self, text, *, ignore_case=False, reverse=False,
result_cb=None):
flags = QWebEnginePage.FindFlags(0)
if ignore_case == 'smart':
if not text.islower():
flags |= QWebEnginePage.FindCaseSensitively
elif not ignore_case:
flags |= QWebEnginePage.FindCaseSensitively
if reverse:
flags |= QWebEnginePage.FindBackward
self.text = text
self._flags = flags
self._find(text, flags, result_cb)
def clear(self):
self._widget.findText('')
def prev_result(self, *, result_cb=None):
# The int() here makes sure we get a copy of the flags.
flags = QWebEnginePage.FindFlags(int(self._flags))
if flags & QWebEnginePage.FindBackward:
flags &= ~QWebEnginePage.FindBackward
else:
flags |= QWebEnginePage.FindBackward
self._find(self.text, self._flags, result_cb)
def next_result(self, *, result_cb=None):
self._find(self.text, self._flags, result_cb)
class WebEngineCaret(browsertab.AbstractCaret):
"""QtWebEngine implementations related to moving the cursor/selection."""
@pyqtSlot(usertypes.KeyMode)
def _on_mode_entered(self, mode):
log.stub()
@pyqtSlot(usertypes.KeyMode)
def _on_mode_left(self):
log.stub()
def move_to_next_line(self, count=1):
log.stub()
def move_to_prev_line(self, count=1):
log.stub()
def move_to_next_char(self, count=1):
log.stub()
def move_to_prev_char(self, count=1):
log.stub()
def move_to_end_of_word(self, count=1):
log.stub()
def move_to_next_word(self, count=1):
log.stub()
def move_to_prev_word(self, count=1):
log.stub()
def move_to_start_of_line(self):
log.stub()
def move_to_end_of_line(self):
log.stub()
def move_to_start_of_next_block(self, count=1):
log.stub()
def move_to_start_of_prev_block(self, count=1):
log.stub()
def move_to_end_of_next_block(self, count=1):
log.stub()
def move_to_end_of_prev_block(self, count=1):
log.stub()
def move_to_start_of_document(self):
log.stub()
def move_to_end_of_document(self):
log.stub()
def toggle_selection(self):
log.stub()
def drop_selection(self):
log.stub()
def has_selection(self):
return self._widget.hasSelection()
def selection(self, html=False):
if html:
raise NotImplementedError
return self._widget.selectedText()
def follow_selected(self, *, tab=False):
log.stub()
class WebEngineScroller(browsertab.AbstractScroller):
"""QtWebEngine implementations related to scrolling."""
def __init__(self, tab, parent=None):
super().__init__(tab, parent)
self._pos_perc = (None, None)
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)
except AttributeError:
log.stub('scrollPositionChanged, on Qt < 5.7')
self._on_scroll_pos_changed()
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)
@pyqtSlot()
def _on_scroll_pos_changed(self):
"""Update the scroll position attributes when it changed."""
def update_scroll_pos(jsret):
"""Callback after getting scroll position via JS."""
if jsret is None:
# This can happen when the callback would get called after
# shutting down a tab
return
assert isinstance(jsret, dict), jsret
self._pos_perc = (jsret['perc']['x'], jsret['perc']['y'])
self._pos_px = QPoint(jsret['px']['x'], jsret['px']['y'])
self.perc_changed.emit(*self._pos_perc)
js_code = """
{scroll_js}
scroll_pos();
""".format(scroll_js=utils.read_file('javascript/scroll.js'))
self._tab.run_js_async(js_code, update_scroll_pos)
def pos_px(self):
return self._pos_px
def pos_perc(self):
return self._pos_perc
def to_perc(self, x=None, y=None):
js_code = """
{scroll_js}
scroll_to_perc({x}, {y});
""".format(scroll_js=utils.read_file('javascript/scroll.js'),
x='undefined' if x is None else x,
y='undefined' if y is None else 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()))
def delta(self, x=0, y=0):
self._tab.run_js_async("window.scrollBy({x}, {y});".format(x=x, y=y))
def delta_page(self, x=0, y=0):
js_code = """
{scroll_js}
scroll_delta_page({x}, {y});
""".format(scroll_js=utils.read_file('javascript/scroll.js'), x=x, y=y)
self._tab.run_js_async(js_code)
def up(self, count=1):
self._key_press(Qt.Key_Up, count)
def down(self, count=1):
self._key_press(Qt.Key_Down, count)
def left(self, count=1):
self._key_press(Qt.Key_Left, count)
def right(self, count=1):
self._key_press(Qt.Key_Right, count)
def top(self):
self._key_press(Qt.Key_Home)
def bottom(self):
self._key_press(Qt.Key_End)
def page_up(self, count=1):
self._key_press(Qt.Key_PageUp, count)
def page_down(self, count=1):
self._key_press(Qt.Key_PageDown, count)
def at_top(self):
return self.pos_px().y() == 0
def at_bottom(self):
log.stub()
class WebEngineHistory(browsertab.AbstractHistory):
"""QtWebEngine implementations related to page history."""
def current_idx(self):
return self._history.currentItemIndex()
def back(self):
self._history.back()
def forward(self):
self._history.forward()
def can_go_back(self):
return self._history.canGoBack()
def can_go_forward(self):
return self._history.canGoForward()
def serialize(self):
return qtutils.serialize(self._history)
def deserialize(self, data):
return qtutils.deserialize(data, self._history)
def load_items(self, items):
log.stub()
class WebEngineZoom(browsertab.AbstractZoom):
"""QtWebEngine implementations related to zooming."""
def _set_factor_internal(self, factor):
self._widget.setZoomFactor(factor)
def factor(self):
return self._widget.zoomFactor()
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()
self.history = WebEngineHistory(self)
self.scroller = WebEngineScroller(self, parent=self)
self.caret = WebEngineCaret(win_id=win_id, mode_manager=mode_manager,
tab=self, parent=self)
self.zoom = WebEngineZoom(win_id=win_id, parent=self)
self.search = WebEngineSearch(parent=self)
self.printing = WebEnginePrinting()
self._set_widget(widget)
self._connect_signals()
self.backend = usertypes.Backend.QtWebEngine
def openurl(self, url):
self._openurl_prepare(url)
self._widget.load(url)
def url(self):
return self._widget.url()
def dump_async(self, callback, *, plain=False):
if plain:
self._widget.page().toPlainText(callback)
else:
self._widget.page().toHtml(callback)
def run_js_async(self, code, callback=None):
if callback is None:
self._widget.page().runJavaScript(code)
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()
def reload(self, *, force=False):
if force:
action = QWebEnginePage.ReloadAndBypassCache
else:
action = QWebEnginePage.Reload
self._widget.triggerPageAction(action)
def stop(self):
self._widget.stop()
def title(self):
return self._widget.title()
def icon(self):
try:
return self._widget.icon()
except AttributeError:
log.stub('on Qt < 5.7')
return QIcon()
def set_html(self, html, base_url):
# FIXME:qtwebengine
# check this and raise an exception if too big:
# Warning: The content will be percent encoded before being sent to the
# renderer via IPC. This may increase its size. The maximum size of the
# percent encoded content is 2 megabytes minus 30 bytes.
self._widget.setHtml(html, base_url)
def clear_ssl_errors(self):
log.stub()
def _connect_signals(self):
view = self._widget
page = view.page()
page.windowCloseRequested.connect(self.window_close_requested)
page.linkHovered.connect(self.link_hovered)
page.loadProgress.connect(self._on_load_progress)
page.loadStarted.connect(self._on_load_started)
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)
try:
view.iconChanged.connect(self.icon_changed)
except AttributeError:
log.stub('iconChanged, on Qt < 5.7')

View File

@@ -0,0 +1,79 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""The main browser widget for QtWebEngine."""
from PyQt5.QtCore import pyqtSignal, Qt, QPoint
# 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
class WebEngineView(QWebEngineView):
"""Custom QWebEngineView subclass with qutebrowser-specific features."""
mouse_wheel_zoom = pyqtSignal(QPoint)
def __init__(self, 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)
class WebEnginePage(QWebEnginePage):
"""Custom QWebEnginePage subclass with qutebrowser-specific features."""
certificate_error = pyqtSignal()
def certificateError(self, error):
self.certificate_error.emit()
return super().certificateError(error)
def javaScriptConsoleMessage(self, level, msg, line, source):
"""Log javascript messages to qutebrowser's log."""
# FIXME:qtwebengine maybe unify this in the tab api somehow?
setting = config.get('general', 'log-javascript-console')
if setting == 'none':
return
level_to_logger = {
QWebEnginePage.InfoMessageLevel: log.js.info,
QWebEnginePage.WarningMessageLevel: log.js.warning,
QWebEnginePage.ErrorMessageLevel: log.js.error,
}
logstring = "[{}:{}] {}".format(source, line, msg)
logger = level_to_logger[level]
logger(logstring)

View File

@@ -0,0 +1,20 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2016 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Classes related to the browser widgets for QtWebKit."""

View File

@@ -37,9 +37,6 @@ class RAMCookieJar(QNetworkCookieJar):
changed = pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent)
def __repr__(self):
return utils.get_repr(self, count=len(self.allCookies()))

View File

@@ -25,6 +25,7 @@ import sys
import os.path
import shutil
import functools
import tempfile
import collections
import sip
@@ -39,8 +40,8 @@ from qutebrowser.config import config
from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.utils import (message, usertypes, log, utils, urlutils,
objreg, standarddir, qtutils)
from qutebrowser.browser import http
from qutebrowser.browser.network import networkmanager
from qutebrowser.browser.webkit import http
from qutebrowser.browser.webkit.network import networkmanager
ModelRole = usertypes.enum('ModelRole', ['item'], start=Qt.UserRole,
@@ -280,6 +281,7 @@ class DownloadItem(QObject):
_read_timer: A Timer which reads the QNetworkReply into self._buffer
periodically.
_win_id: The window ID the DownloadItem runs in.
_dead: Whether the Download has _die()'d.
Signals:
data_changed: The downloads metadata changed.
@@ -328,6 +330,7 @@ class DownloadItem(QObject):
self.init_reply(reply)
self._win_id = win_id
self.raw_headers = {}
self._dead = False
def __repr__(self):
return utils.get_repr(self, basename=self.basename)
@@ -345,7 +348,7 @@ class DownloadItem(QObject):
errmsg = ""
else:
errmsg = " - {}".format(self.error_msg)
if all(e is None for e in (perc, remaining, self.stats.total)):
if all(e is None for e in [perc, remaining, self.stats.total]):
return ('{index}: {name} [{speed:>10}|{down}]{errmsg}'.format(
index=self.index, name=self.basename, speed=speed,
down=down, errmsg=errmsg))
@@ -395,6 +398,21 @@ class DownloadItem(QObject):
def _die(self, msg):
"""Abort the download and emit an error."""
assert not self.successful
# Prevent actions if calling _die() twice. This might happen if the
# error handler correctly connects, and the error occurs in init_reply
# between reply.error.connect and the reply.error() check. In this
# case, the connected error handlers will be called twice, once via the
# direct error.emit() and once here in _die(). The stacks look like
# this then:
# <networkmanager error.emit> -> on_reply_error -> _die ->
# self.error.emit()
# and
# [init_reply -> <single shot timer> ->] <lambda in init_reply> ->
# self.error.emit()
# which may lead to duplicate error messages (and failing tests)
if self._dead:
return
self._dead = True
self._read_timer.stop()
self.reply.downloadProgress.disconnect()
self.reply.finished.disconnect()
@@ -413,7 +431,10 @@ class DownloadItem(QObject):
self.done = True
self.data_changed.emit()
if self.fileobj is not None:
self.fileobj.close()
try:
self.fileobj.close()
except OSError:
log.downloads.exception("Error while closing file object")
def init_reply(self, reply):
"""Set a new reply and connect its signals.
@@ -438,7 +459,7 @@ class DownloadItem(QObject):
# Here no signals are connected to the DownloadItem yet, so we use a
# singleShot QTimer to emit them after they are connected.
if reply.error() != QNetworkReply.NoError:
QTimer.singleShot(0, lambda: self.error.emit(reply.errorString()))
QTimer.singleShot(0, lambda: self._die(reply.errorString()))
def get_status_color(self, position):
"""Choose an appropriate color for presenting the download's status.
@@ -448,7 +469,7 @@ class DownloadItem(QObject):
"""
# pylint: disable=bad-config-call
# WORKAROUND for https://bitbucket.org/logilab/astroid/issue/104/
assert position in ("fg", "bg")
assert position in ["fg", "bg"]
start = config.get('colors', 'downloads.{}.start'.format(position))
stop = config.get('colors', 'downloads.{}.stop'.format(position))
system = config.get('colors', 'downloads.{}.system'.format(position))
@@ -510,7 +531,13 @@ class DownloadItem(QObject):
def open_file(self):
"""Open the downloaded file."""
assert self.successful
url = QUrl.fromLocalFile(self._filename)
filename = self._filename
if filename is None:
filename = getattr(self.fileobj, 'name', None)
if filename is None:
log.downloads.error("No filename to open the download!")
return
url = QUrl.fromLocalFile(filename)
QDesktopServices.openUrl(url)
def set_filename(self, filename):
@@ -675,7 +702,7 @@ class DownloadItem(QObject):
self.raw_headers[bytes(key)] = bytes(value)
def _handle_redirect(self):
"""Handle a HTTP redirect.
"""Handle an HTTP redirect.
Return:
True if the download was redirected, False otherwise.
@@ -735,6 +762,9 @@ class DownloadManager(QAbstractListModel):
def _postprocess_question(self, q):
"""Postprocess a Question object that is asked."""
q.destroyed.connect(functools.partial(self.questions.remove, q))
# We set the mode here so that other code that uses ask_for_filename
# doesn't need to handle the special download mode.
q.mode = usertypes.PromptMode.download
self.questions.append(q)
@pyqtSlot()
@@ -754,10 +784,7 @@ class DownloadManager(QAbstractListModel):
**kwargs: passed to get_request().
Return:
If the download could start immediately, (fileobj/filename given),
the created DownloadItem.
If not, None.
The created DownloadItem.
"""
if not url.isValid():
urlutils.invalid_url_error(self._win_id, url, "start download")
@@ -765,27 +792,17 @@ class DownloadManager(QAbstractListModel):
req = QNetworkRequest(url)
return self.get_request(req, **kwargs)
def get_request(self, request, *, fileobj=None, filename=None,
prompt_download_directory=None, **kwargs):
def get_request(self, request, *, target=None, **kwargs):
"""Start a download with a QNetworkRequest.
Args:
request: The QNetworkRequest to download.
fileobj: The file object to write the answer to.
filename: A path to write the data to.
prompt_download_directory: Whether to prompt for the download dir
or automatically download. If None, the
config is used.
target: Where to save the download as usertypes.DownloadTarget.
**kwargs: Passed to fetch_request.
Return:
If the download could start immediately, (fileobj/filename given),
the created DownloadItem.
If not, None.
The created DownloadItem.
"""
if fileobj is not None and filename is not None: # pragma: no cover
raise TypeError("Only one of fileobj/filename may be given!")
# WORKAROUND for Qt corrupting data loaded from cache:
# https://bugreports.qt.io/browse/QTBUG-42757
request.setAttribute(QNetworkRequest.CacheLoadControlAttribute,
@@ -813,27 +830,10 @@ class DownloadManager(QAbstractListModel):
if suggested_fn is None:
suggested_fn = 'qutebrowser-download'
# We won't need a question if a filename or fileobj is already given
if fileobj is None and filename is None:
filename, q = ask_for_filename(
suggested_fn, self._win_id, parent=self,
prompt_download_directory=prompt_download_directory
)
if fileobj is not None or filename is not None:
return self.fetch_request(request,
fileobj=fileobj,
filename=filename,
suggested_filename=suggested_fn,
**kwargs)
q.answered.connect(
lambda fn: self.fetch_request(request,
filename=fn,
suggested_filename=suggested_fn,
**kwargs))
self._postprocess_question(q)
q.ask()
return None
return self.fetch_request(request,
target=target,
suggested_filename=suggested_fn,
**kwargs)
def fetch_request(self, request, *, page=None, **kwargs):
"""Download a QNetworkRequest to disk.
@@ -854,27 +854,25 @@ class DownloadManager(QAbstractListModel):
return self.fetch(reply, **kwargs)
@pyqtSlot('QNetworkReply')
def fetch(self, reply, *, fileobj=None, filename=None, auto_remove=False,
def fetch(self, reply, *, target=None, auto_remove=False,
suggested_filename=None, prompt_download_directory=None):
"""Download a QNetworkReply to disk.
Args:
reply: The QNetworkReply to download.
fileobj: The file object to write the answer to.
filename: A path to write the data to.
target: Where to save the download as usertypes.DownloadTarget.
auto_remove: Whether to remove the download even if
ui -> remove-finished-downloads is set to -1.
Return:
The created DownloadItem.
"""
if fileobj is not None and filename is not None: # pragma: no cover
raise TypeError("Only one of fileobj/filename may be given!")
if not suggested_filename:
if filename is not None:
suggested_filename = os.path.basename(filename)
elif fileobj is not None and getattr(fileobj, 'name', None):
suggested_filename = fileobj.name
if isinstance(target, usertypes.FileDownloadTarget):
suggested_filename = os.path.basename(target.filename)
elif (isinstance(target, usertypes.FileObjDownloadTarget) and
getattr(target.fileobj, 'name', None)):
suggested_filename = target.fileobj.name
else:
_, suggested_filename = http.parse_content_disposition(reply)
log.downloads.debug("fetch: {} -> {}".format(reply.url(),
@@ -906,13 +904,8 @@ class DownloadManager(QAbstractListModel):
if not self._update_timer.isActive():
self._update_timer.start()
if fileobj is not None:
download.set_fileobj(fileobj)
download.autoclose = False
return download
if filename is not None:
download.set_filename(filename)
if target is not None:
self._set_download_target(download, suggested_filename, target)
return download
# Neither filename nor fileobj were given, prepare a question
@@ -923,12 +916,15 @@ class DownloadManager(QAbstractListModel):
# User doesn't want to be asked, so just use the download_dir
if filename is not None:
download.set_filename(filename)
target = usertypes.FileDownloadTarget(filename)
self._set_download_target(download, suggested_filename, target)
return download
# Ask the user for a filename
self._postprocess_question(q)
q.answered.connect(download.set_filename)
q.answered.connect(
functools.partial(self._set_download_target, download,
suggested_filename))
q.cancelled.connect(download.cancel)
download.cancelled.connect(q.abort)
download.error.connect(q.abort)
@@ -936,6 +932,43 @@ class DownloadManager(QAbstractListModel):
return download
def _set_download_target(self, download, suggested_filename, target):
"""Set the target for a given download.
Args:
download: The download to set the filename for.
suggested_filename: The suggested filename.
target: The usertypes.DownloadTarget for this download.
"""
if isinstance(target, usertypes.FileObjDownloadTarget):
download.set_fileobj(target.fileobj)
download.autoclose = False
elif isinstance(target, usertypes.FileDownloadTarget):
download.set_filename(target.filename)
elif isinstance(target, usertypes.OpenFileDownloadTarget):
tmp_manager = objreg.get('temporary-downloads')
try:
fobj = tmp_manager.get_tmpfile(suggested_filename)
except OSError as exc:
msg = "Download error: {}".format(exc)
message.error(self._win_id, msg)
download.cancel()
return
download.finished.connect(
functools.partial(self._open_download, download))
download.autoclose = True
download.set_fileobj(fobj)
else:
log.downloads.error("Unknown download target: {}".format(target))
def _open_download(self, download):
"""Open the given download but only if it was successful."""
if download.successful:
download.open_file()
else:
log.downloads.debug("{} finished but not successful, not opening!"
.format(download))
def raise_no_download(self, count):
"""Raise an exception that the download doesn't exist.
@@ -1038,7 +1071,7 @@ class DownloadManager(QAbstractListModel):
@pyqtSlot(QNetworkRequest, QNetworkReply)
def on_redirect(self, download, request, reply):
"""Handle a HTTP redirect of a download.
"""Handle an HTTP redirect of a download.
Args:
download: The old DownloadItem.
@@ -1246,3 +1279,64 @@ class DownloadManager(QAbstractListModel):
The number of unfinished downloads.
"""
return sum(1 for download in self.downloads if not download.done)
class TempDownloadManager(QObject):
"""Manager to handle temporary download files.
The downloads are downloaded to a temporary location and then openened with
the system standard application. The temporary files are deleted when
qutebrowser is shutdown.
Attributes:
files: A list of NamedTemporaryFiles of downloaded items.
"""
def __init__(self, parent=None):
super().__init__(parent)
self.files = []
self._tmpdir = None
def cleanup(self):
"""Clean up any temporary files."""
if self._tmpdir is not None:
self._tmpdir.cleanup()
self._tmpdir = None
def _get_tmpdir(self):
"""Return the temporary directory that is used for downloads.
The directory is created lazily on first access.
Return:
The tempfile.TemporaryDirectory that is used.
"""
if self._tmpdir is None:
self._tmpdir = tempfile.TemporaryDirectory(
prefix='qutebrowser-downloads-')
return self._tmpdir
def get_tmpfile(self, suggested_name):
"""Return a temporary file in the temporary downloads directory.
The files are kept as long as qutebrowser is running and automatically
cleaned up at program exit.
Args:
suggested_name: str of the "suggested"/original filename. Used as a
suffix, so any file extenions are preserved.
Return:
A tempfile.NamedTemporaryFile that should be used to save the file.
"""
tmpdir = self._get_tmpdir()
encoding = sys.getfilesystemencoding()
suggested_name = utils.force_encoding(suggested_name, encoding)
# Make sure that the filename is not too long
if len(suggested_name) > 50:
suggested_name = suggested_name[:25] + '...' + suggested_name[-25:]
fobj = tempfile.NamedTemporaryFile(dir=tmpdir.name, delete=False,
suffix=suggested_name)
self.files.append(fobj)
return fobj

View File

@@ -125,7 +125,7 @@ class WebHistoryInterface(QWebHistoryInterface):
pass
def historyContains(self, url_string):
"""Called by WebKit to determine if an URL is contained in the history.
"""Called by WebKit to determine if a URL is contained in the history.
Args:
url_string: The URL (as string) to check for.
@@ -285,16 +285,20 @@ class WebHistory(QObject):
self.cleared.emit()
def add_url(self, url, title="", *, redirect=False, atime=None):
"""Called by WebKit when an URL should be added to the history.
"""Called by WebKit when a URL should be added to the history.
Args:
url: An url (as QUrl) to add to the history.
url: A url (as QUrl) to add to the history.
redirect: Whether the entry was redirected to another URL
(hidden in completion)
atime: Override the atime used to add the entry
"""
if config.get('general', 'private-browsing'):
return
if not url.isValid():
log.misc.warning("Ignoring invalid URL being added to history")
return
if atime is None:
atime = time.time()
entry = Entry(atime, url, title, redirect=redirect)

View File

@@ -23,7 +23,7 @@
import os.path
from qutebrowser.utils import log
from qutebrowser.browser import rfc6266
from qutebrowser.browser.webkit import rfc6266
from PyQt5.QtNetwork import QNetworkRequest

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/>.
"""Utils for writing a MHTML file."""
"""Utils for writing an MHTML file."""
import functools
import io
@@ -34,7 +34,7 @@ import email.message
from PyQt5.QtCore import QUrl
from qutebrowser.browser import webelem, downloads
from qutebrowser.browser.webkit import webelem, downloads
from qutebrowser.utils import log, objreg, message, usertypes, utils, urlutils
try:
@@ -83,7 +83,7 @@ def _get_css_imports_cssutils(data, inline=False):
Args:
data: The content of the stylesheet to scan as string.
inline: True if the argument is a inline HTML style attribute.
inline: True if the argument is an inline HTML style attribute.
"""
# We don't care about invalid CSS data, this will only litter the log
# output with CSS errors
@@ -112,7 +112,7 @@ def _get_css_imports(data, inline=False):
Args:
data: The content of the stylesheet to scan as string.
inline: True if the argument is a inline HTML style attribute.
inline: True if the argument is an inline HTML style attribute.
"""
if cssutils is None:
return _get_css_imports_regex(data)
@@ -149,7 +149,7 @@ E_QUOPRI = email.encoders.encode_quopri
class MHTMLWriter:
"""A class for outputting multiple files to a MHTML document.
"""A class for outputting multiple files to an MHTML document.
Attributes:
root_content: The root content as bytes.
@@ -222,7 +222,7 @@ class _Downloader:
"""A class to download whole websites.
Attributes:
web_view: The QWebView which contains the website that will be saved.
tab: The AbstractTab which contains the website that will be saved.
dest: Destination filename.
writer: The MHTMLWriter object which is used to save the page.
loaded_urls: A set of QUrls of finished asset downloads.
@@ -233,15 +233,15 @@ class _Downloader:
_win_id: The window this downloader belongs to.
"""
def __init__(self, web_view, dest):
self.web_view = web_view
def __init__(self, tab, dest):
self.tab = tab
self.dest = dest
self.writer = None
self.loaded_urls = {web_view.url()}
self.loaded_urls = {tab.url()}
self.pending_downloads = set()
self._finished_file = False
self._used = False
self._win_id = web_view.win_id
self._win_id = tab.win_id
def run(self):
"""Download and save the page.
@@ -252,8 +252,11 @@ class _Downloader:
if self._used:
raise ValueError("Downloader already used")
self._used = True
web_url = self.web_view.url()
web_frame = self.web_view.page().mainFrame()
web_url = self.tab.url()
# FIXME:qtwebengine have a proper API for this
page = self.tab._widget.page() # pylint: disable=protected-access
web_frame = page.mainFrame()
self.writer = MHTMLWriter(
web_frame.toHtml().encode('utf-8'),
@@ -316,7 +319,7 @@ class _Downloader:
Args:
url: The file to download as QUrl.
"""
if url.scheme() not in {'http', 'https'}:
if url.scheme() not in ['http', 'https']:
return
# Prevent loading an asset twice
if url in self.loaded_urls:
@@ -340,7 +343,8 @@ class _Downloader:
download_manager = objreg.get('download-manager', scope='window',
window=self._win_id)
item = download_manager.get(url, fileobj=_NoCloseBytesIO(),
target = usertypes.FileObjDownloadTarget(_NoCloseBytesIO())
item = download_manager.get(url, target=target,
auto_remove=True)
self.pending_downloads.add((url, item))
item.finished.connect(functools.partial(self._finished, url, item))
@@ -479,28 +483,28 @@ class _NoCloseBytesIO(io.BytesIO):
super().close()
def _start_download(dest, web_view):
"""Start downloading the current page and all assets to a MHTML file.
def _start_download(dest, tab):
"""Start downloading the current page and all assets to an MHTML file.
This will overwrite dest if it already exists.
Args:
dest: The filename where the resulting file should be saved.
web_view: Specify the webview whose page should be loaded.
tab: Specify the tab whose page should be loaded.
"""
loader = _Downloader(web_view, dest)
loader = _Downloader(tab, dest)
loader.run()
def start_download_checked(dest, web_view):
def start_download_checked(dest, tab):
"""First check if dest is already a file, then start the download.
Args:
dest: The filename where the resulting file should be saved.
web_view: Specify the webview whose page should be loaded.
tab: Specify the tab whose page should be loaded.
"""
# The default name is 'page title.mht'
title = web_view.title()
title = tab.title()
default_name = utils.sanitize_filename(title + '.mht')
# Remove characters which cannot be expressed in the file system encoding
@@ -524,12 +528,12 @@ def start_download_checked(dest, web_view):
# saving the file anyway.
if not os.path.isdir(os.path.dirname(path)):
folder = os.path.dirname(path)
message.error(web_view.win_id,
message.error(tab.win_id,
"Directory {} does not exist.".format(folder))
return
if not os.path.isfile(path):
_start_download(path, web_view=web_view)
_start_download(path, tab=tab)
return
q = usertypes.Question()
@@ -537,7 +541,7 @@ def start_download_checked(dest, web_view):
q.text = "{} exists. Overwrite?".format(path)
q.completed.connect(q.deleteLater)
q.answered_yes.connect(functools.partial(
_start_download, path, web_view=web_view))
_start_download, path, tab=tab))
message_bridge = objreg.get('message-bridge', scope='window',
window=web_view.win_id)
window=tab.win_id)
message_bridge.ask(q, blocking=False)

View File

@@ -25,7 +25,7 @@
import os
from qutebrowser.browser.network import schemehandler, networkreply
from qutebrowser.browser.webkit.network import schemehandler, networkreply
from qutebrowser.utils import jinja

View File

@@ -31,8 +31,8 @@ from PyQt5.QtNetwork import (QNetworkAccessManager, QNetworkReply, QSslError,
from qutebrowser.config import config
from qutebrowser.utils import (message, log, usertypes, utils, objreg, qtutils,
urlutils, debug)
from qutebrowser.browser.network import qutescheme, networkreply
from qutebrowser.browser.network import filescheme
from qutebrowser.browser.webkit.network import qutescheme, networkreply
from qutebrowser.browser.webkit.network import filescheme
HOSTBLOCK_ERROR_STRING = '%HOSTBLOCK%'
@@ -228,9 +228,9 @@ class NetworkManager(QNetworkAccessManager):
# This might be a generic network manager, e.g. one belonging to a
# DownloadManager. In this case, just skip the webview thing.
if self._tab_id is not None:
webview = objreg.get('webview', scope='tab', window=self._win_id,
tab=self._tab_id)
webview.loadStarted.connect(q.abort)
tab = objreg.get('tab', scope='tab', window=self._win_id,
tab=self._tab_id)
tab.load_started.connect(q.abort)
bridge = objreg.get('message-bridge', scope='window',
window=self._win_id)
bridge.ask(q, blocking=True)
@@ -479,9 +479,9 @@ class NetworkManager(QNetworkAccessManager):
if self._tab_id is not None:
try:
webview = objreg.get('webview', scope='tab',
window=self._win_id, tab=self._tab_id)
current_url = webview.url()
tab = objreg.get('tab', scope='tab', window=self._win_id,
tab=self._tab_id)
current_url = tab.url()
except (KeyError, RuntimeError, TypeError):
# https://github.com/The-Compiler/qutebrowser/issues/889
# Catching RuntimeError and TypeError because we could be in

View File

@@ -33,7 +33,7 @@ from PyQt5.QtNetwork import QNetworkReply
import qutebrowser
from qutebrowser.browser import pdfjs
from qutebrowser.browser.network import schemehandler, networkreply
from qutebrowser.browser.webkit.network import schemehandler, networkreply
from qutebrowser.utils import (version, utils, jinja, log, message, docutils,
objreg)
from qutebrowser.config import configexc, configdata
@@ -89,7 +89,7 @@ class QuteSchemeHandler(schemehandler.SchemeHandler):
"""
path = request.url().path()
host = request.url().host()
# An url like "qute:foo" is split as "scheme:path", not "scheme:host".
# A url like "qute:foo" is split as "scheme:path", not "scheme:host".
log.misc.debug("url: {}, path: {}, host {}".format(
request.url().toDisplayString(), path, host))
try:
@@ -123,9 +123,6 @@ class JSBridge(QObject):
"""Javascript-bridge for special qute:... pages."""
def __init__(self, parent=None):
super().__init__(parent)
@pyqtSlot(int, str, str, str)
def set(self, win_id, sectname, optname, value):
"""Slot to set a setting from qute:settings."""
@@ -261,3 +258,18 @@ def qute_pdfjs(_win_id, request):
"pdfjs resource requested but not found: {}".format(e.path))
raise QuteSchemeError("Can't find pdfjs resource '{}'".format(e.path),
QNetworkReply.ContentNotFoundError)
@add_handler('bookmarks')
def qute_bookmarks(_win_id, _request):
"""Handler for qute:bookmarks. Display all quickmarks / bookmarks."""
bookmarks = sorted(objreg.get('bookmark-manager').marks.items(),
key=lambda x: x[1]) # Sort by title
quickmarks = sorted(objreg.get('quickmark-manager').marks.items(),
key=lambda x: x[0]) # Sort by name
html = jinja.render('bookmarks.html',
title='Bookmarks',
bookmarks=bookmarks,
quickmarks=quickmarks)
return html.encode('UTF-8', errors='xmlcharrefreplace')

View File

@@ -59,7 +59,7 @@ class TabHistoryItem:
def _encode_url(url):
"""Encode an QUrl suitable to pass to QWebHistory."""
"""Encode a QUrl suitable to pass to QWebHistory."""
data = bytes(QUrl.toPercentEncoding(url.toString(), b':/#?&+=@%*'))
return data.decode('ascii')

View File

@@ -52,7 +52,8 @@ SELECTORS = {
Group.focus: '*:focus',
Group.inputs: ('input[type=text], input[type=email], input[type=url], '
'input[type=tel], input[type=number], '
'input[type=password], input[type=search], textarea'),
'input[type=password], input[type=search], '
'input:not([type]), textarea'),
}
@@ -83,7 +84,7 @@ class WebElementWrapper(collections.abc.MutableMapping):
if elem.isNull():
raise IsNullError('{} is a null element!'.format(elem))
self._elem = elem
for name in ('addClass', 'appendInside', 'appendOutside',
for name in ['addClass', 'appendInside', 'appendOutside',
'attributeNS', 'classes', 'clone', 'document',
'encloseContentsWith', 'encloseWith',
'evaluateJavaScript', 'findAll', 'findFirst',
@@ -97,7 +98,7 @@ class WebElementWrapper(collections.abc.MutableMapping):
'setInnerXml', 'setOuterXml', 'setPlainText',
'setStyleProperty', 'styleProperty', 'tagName',
'takeFromDocument', 'toInnerXml', 'toOuterXml',
'toggleClass', 'webFrame', '__eq__', '__ne__'):
'toggleClass', 'webFrame', '__eq__', '__ne__']:
# We don't wrap some methods for which we have better alternatives:
# - Mapping access for attributeNames/hasAttribute/setAttribute/
# attribute/removeAttribute.
@@ -194,7 +195,7 @@ class WebElementWrapper(collections.abc.MutableMapping):
"""
self._check_vanished()
try:
return self['contenteditable'].lower() not in ('false', 'inherit')
return self['contenteditable'].lower() not in ['false', 'inherit']
except KeyError:
return False
@@ -269,7 +270,7 @@ class WebElementWrapper(collections.abc.MutableMapping):
return self._is_editable_input()
elif tag == 'textarea':
return self.is_writable()
elif tag in ('embed', 'applet'):
elif tag in ['embed', 'applet']:
# Flash/Java/...
return config.get('input', 'insert-mode-on-plugins') and not strict
elif tag == 'object':
@@ -284,7 +285,7 @@ class WebElementWrapper(collections.abc.MutableMapping):
self._check_vanished()
roles = ('combobox', 'textbox')
tag = self._elem.tagName().lower()
return self.get('role', None) in roles or tag in ('input', 'textarea')
return self.get('role', None) in roles or tag in ['input', 'textarea']
def remove_blank_target(self):
"""Remove target from link."""

View File

@@ -0,0 +1,589 @@
# 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/>.
"""Wrapper over our (QtWebKit) WebView."""
import sys
import functools
import xml.etree.ElementTree
from PyQt5.QtCore import pyqtSlot, Qt, QEvent, QUrl, QPoint, QTimer
from PyQt5.QtGui import QKeyEvent
from PyQt5.QtWebKitWidgets import QWebPage
from PyQt5.QtWebKit import QWebSettings
from PyQt5.QtPrintSupport import QPrinter
from qutebrowser.browser import browsertab
from qutebrowser.browser.webkit import webview, tabhistory
from qutebrowser.utils import qtutils, objreg, usertypes, utils
class WebKitPrinting(browsertab.AbstractPrinting):
"""QtWebKit implementations related to printing."""
def _do_check(self):
if not qtutils.check_print_compat():
# WORKAROUND (remove this when we bump the requirements to 5.3.0)
raise browsertab.WebTabError(
"Printing on Qt < 5.3.0 on Windows is broken, please upgrade!")
def check_pdf_support(self):
self._do_check()
def check_printer_support(self):
self._do_check()
def to_pdf(self, filename):
printer = QPrinter()
printer.setOutputFileName(filename)
self.to_printer(printer)
def to_printer(self, printer):
self._widget.print(printer)
class WebKitSearch(browsertab.AbstractSearch):
"""QtWebKit implementations related to searching on the page."""
def __init__(self, parent=None):
super().__init__(parent)
self._flags = QWebPage.FindFlags(0)
def _call_cb(self, callback, found):
"""Call the given callback if it's non-None.
Delays the call via a QTimer so the website is re-rendered in between.
Args:
callback: What to call
found: If the text was found
"""
if callback is not None:
QTimer.singleShot(0, functools.partial(callback, found))
def clear(self):
# We first clear the marked text, then the highlights
self._widget.findText('')
self._widget.findText('', QWebPage.HighlightAllOccurrences)
def search(self, text, *, ignore_case=False, reverse=False,
result_cb=None):
flags = QWebPage.FindWrapsAroundDocument
if ignore_case == 'smart':
if not text.islower():
flags |= QWebPage.FindCaseSensitively
elif not ignore_case:
flags |= QWebPage.FindCaseSensitively
if reverse:
flags |= QWebPage.FindBackward
# We actually search *twice* - once to highlight everything, then again
# to get a mark so we can navigate.
found = self._widget.findText(text, flags)
self._widget.findText(text, flags | QWebPage.HighlightAllOccurrences)
self.text = text
self._flags = flags
self._call_cb(result_cb, found)
def next_result(self, *, result_cb=None):
found = self._widget.findText(self.text, self._flags)
self._call_cb(result_cb, found)
def prev_result(self, *, result_cb=None):
# The int() here makes sure we get a copy of the flags.
flags = QWebPage.FindFlags(int(self._flags))
if flags & QWebPage.FindBackward:
flags &= ~QWebPage.FindBackward
else:
flags |= QWebPage.FindBackward
found = self._widget.findText(self.text, flags)
self._call_cb(result_cb, found)
class WebKitCaret(browsertab.AbstractCaret):
"""QtWebKit implementations related to moving the cursor/selection."""
@pyqtSlot(usertypes.KeyMode)
def _on_mode_entered(self, mode):
if mode != usertypes.KeyMode.caret:
return
settings = self._widget.settings()
settings.setAttribute(QWebSettings.CaretBrowsingEnabled, True)
self.selection_enabled = bool(self.selection())
if self._widget.isVisible():
# Sometimes the caret isn't immediately visible, but unfocusing
# and refocusing it fixes that.
self._widget.clearFocus()
self._widget.setFocus(Qt.OtherFocusReason)
# Move the caret to the first element in the viewport if there
# isn't any text which is already selected.
#
# Note: We can't use hasSelection() here, as that's always
# true in caret mode.
if not self.selection():
self._widget.page().currentFrame().evaluateJavaScript(
utils.read_file('javascript/position_caret.js'))
@pyqtSlot()
def _on_mode_left(self):
settings = self._widget.settings()
if settings.testAttribute(QWebSettings.CaretBrowsingEnabled):
if self.selection_enabled and self._widget.hasSelection():
# Remove selection if it exists
self._widget.triggerPageAction(QWebPage.MoveToNextChar)
settings.setAttribute(QWebSettings.CaretBrowsingEnabled, False)
self.selection_enabled = False
def move_to_next_line(self, count=1):
if not self.selection_enabled:
act = QWebPage.MoveToNextLine
else:
act = QWebPage.SelectNextLine
for _ in range(count):
self._widget.triggerPageAction(act)
def move_to_prev_line(self, count=1):
if not self.selection_enabled:
act = QWebPage.MoveToPreviousLine
else:
act = QWebPage.SelectPreviousLine
for _ in range(count):
self._widget.triggerPageAction(act)
def move_to_next_char(self, count=1):
if not self.selection_enabled:
act = QWebPage.MoveToNextChar
else:
act = QWebPage.SelectNextChar
for _ in range(count):
self._widget.triggerPageAction(act)
def move_to_prev_char(self, count=1):
if not self.selection_enabled:
act = QWebPage.MoveToPreviousChar
else:
act = QWebPage.SelectPreviousChar
for _ in range(count):
self._widget.triggerPageAction(act)
def move_to_end_of_word(self, count=1):
if not self.selection_enabled:
act = [QWebPage.MoveToNextWord]
if sys.platform == 'win32': # pragma: no cover
act.append(QWebPage.MoveToPreviousChar)
else:
act = [QWebPage.SelectNextWord]
if sys.platform == 'win32': # pragma: no cover
act.append(QWebPage.SelectPreviousChar)
for _ in range(count):
for a in act:
self._widget.triggerPageAction(a)
def move_to_next_word(self, count=1):
if not self.selection_enabled:
act = [QWebPage.MoveToNextWord]
if sys.platform != 'win32': # pragma: no branch
act.append(QWebPage.MoveToNextChar)
else:
act = [QWebPage.SelectNextWord]
if sys.platform != 'win32': # pragma: no branch
act.append(QWebPage.SelectNextChar)
for _ in range(count):
for a in act:
self._widget.triggerPageAction(a)
def move_to_prev_word(self, count=1):
if not self.selection_enabled:
act = QWebPage.MoveToPreviousWord
else:
act = QWebPage.SelectPreviousWord
for _ in range(count):
self._widget.triggerPageAction(act)
def move_to_start_of_line(self):
if not self.selection_enabled:
act = QWebPage.MoveToStartOfLine
else:
act = QWebPage.SelectStartOfLine
self._widget.triggerPageAction(act)
def move_to_end_of_line(self):
if not self.selection_enabled:
act = QWebPage.MoveToEndOfLine
else:
act = QWebPage.SelectEndOfLine
self._widget.triggerPageAction(act)
def move_to_start_of_next_block(self, count=1):
if not self.selection_enabled:
act = [QWebPage.MoveToNextLine,
QWebPage.MoveToStartOfBlock]
else:
act = [QWebPage.SelectNextLine,
QWebPage.SelectStartOfBlock]
for _ in range(count):
for a in act:
self._widget.triggerPageAction(a)
def move_to_start_of_prev_block(self, count=1):
if not self.selection_enabled:
act = [QWebPage.MoveToPreviousLine,
QWebPage.MoveToStartOfBlock]
else:
act = [QWebPage.SelectPreviousLine,
QWebPage.SelectStartOfBlock]
for _ in range(count):
for a in act:
self._widget.triggerPageAction(a)
def move_to_end_of_next_block(self, count=1):
if not self.selection_enabled:
act = [QWebPage.MoveToNextLine,
QWebPage.MoveToEndOfBlock]
else:
act = [QWebPage.SelectNextLine,
QWebPage.SelectEndOfBlock]
for _ in range(count):
for a in act:
self._widget.triggerPageAction(a)
def move_to_end_of_prev_block(self, count=1):
if not self.selection_enabled:
act = [QWebPage.MoveToPreviousLine, QWebPage.MoveToEndOfBlock]
else:
act = [QWebPage.SelectPreviousLine, QWebPage.SelectEndOfBlock]
for _ in range(count):
for a in act:
self._widget.triggerPageAction(a)
def move_to_start_of_document(self):
if not self.selection_enabled:
act = QWebPage.MoveToStartOfDocument
else:
act = QWebPage.SelectStartOfDocument
self._widget.triggerPageAction(act)
def move_to_end_of_document(self):
if not self.selection_enabled:
act = QWebPage.MoveToEndOfDocument
else:
act = QWebPage.SelectEndOfDocument
self._widget.triggerPageAction(act)
def toggle_selection(self):
self.selection_enabled = not self.selection_enabled
mainwindow = objreg.get('main-window', scope='window',
window=self._win_id)
mainwindow.status.set_mode_active(usertypes.KeyMode.caret, True)
def drop_selection(self):
self._widget.triggerPageAction(QWebPage.MoveToNextChar)
def has_selection(self):
return self._widget.hasSelection()
def selection(self, html=False):
if html:
return self._widget.selectedHtml()
return self._widget.selectedText()
def follow_selected(self, *, tab=False):
if not self.has_selection():
return
if QWebSettings.globalSettings().testAttribute(
QWebSettings.JavascriptEnabled):
if tab:
self._widget.page().open_target = usertypes.ClickTarget.tab
self._tab.run_js_async(
'window.getSelection().anchorNode.parentNode.click()')
else:
selection = self.selection(html=True)
try:
selected_element = xml.etree.ElementTree.fromstring(
'<html>{}</html>'.format(selection)).find('a')
except xml.etree.ElementTree.ParseError:
raise browsertab.WebTabError('Could not parse selected '
'element!')
if selected_element is not None:
try:
url = selected_element.attrib['href']
except KeyError:
raise browsertab.WebTabError('Anchor element without '
'href!')
url = self._tab.url().resolved(QUrl(url))
if tab:
self._tab.new_tab_requested.emit(url)
else:
self._tab.openurl(url)
class WebKitZoom(browsertab.AbstractZoom):
"""QtWebKit implementations related to zooming."""
def _set_factor_internal(self, factor):
self._widget.setZoomFactor(factor)
def factor(self):
return self._widget.zoomFactor()
class WebKitScroller(browsertab.AbstractScroller):
"""QtWebKit implementations related to scrolling."""
# FIXME:qtwebengine When to use the main frame, when the current one?
def pos_px(self):
return self._widget.page().mainFrame().scrollPosition()
def pos_perc(self):
return self._widget.scroll_pos
def to_point(self, point):
self._widget.page().mainFrame().setScrollPosition(point)
def delta(self, x=0, y=0):
qtutils.check_overflow(x, 'int')
qtutils.check_overflow(y, 'int')
self._widget.page().mainFrame().scroll(x, y)
def delta_page(self, x=0.0, y=0.0):
if y.is_integer():
y = int(y)
if y == 0:
pass
elif y < 0:
self.page_up(count=-y)
elif y > 0:
self.page_down(count=y)
y = 0
if x == 0 and y == 0:
return
size = self._widget.page().mainFrame().geometry()
self.delta(x * size.width(), y * size.height())
def to_perc(self, x=None, y=None):
if x is None and y == 0:
self.top()
elif x is None and y == 100:
self.bottom()
else:
for val, orientation in [(x, Qt.Horizontal), (y, Qt.Vertical)]:
if val is not None:
val = qtutils.check_overflow(val, 'int', fatal=False)
frame = self._widget.page().mainFrame()
m = frame.scrollBarMaximum(orientation)
if m == 0:
continue
frame.setScrollBarValue(orientation, int(m * val / 100))
def _key_press(self, key, count=1, getter_name=None, direction=None):
frame = self._widget.page().mainFrame()
press_evt = QKeyEvent(QEvent.KeyPress, key, Qt.NoModifier, 0, 0, 0)
release_evt = QKeyEvent(QEvent.KeyRelease, key, Qt.NoModifier, 0, 0, 0)
getter = None if getter_name is None else getattr(frame, getter_name)
# FIXME:qtwebengine needed?
# self._widget.setFocus()
for _ in range(count):
# Abort scrolling if the minimum/maximum was reached.
if (getter is not None and
frame.scrollBarValue(direction) == getter(direction)):
return
self._widget.keyPressEvent(press_evt)
self._widget.keyReleaseEvent(release_evt)
def up(self, count=1):
self._key_press(Qt.Key_Up, count, 'scrollBarMinimum', Qt.Vertical)
def down(self, count=1):
self._key_press(Qt.Key_Down, count, 'scrollBarMaximum', Qt.Vertical)
def left(self, count=1):
self._key_press(Qt.Key_Left, count, 'scrollBarMinimum', Qt.Horizontal)
def right(self, count=1):
self._key_press(Qt.Key_Right, count, 'scrollBarMaximum', Qt.Horizontal)
def top(self):
self._key_press(Qt.Key_Home)
def bottom(self):
self._key_press(Qt.Key_End)
def page_up(self, count=1):
self._key_press(Qt.Key_PageUp, count, 'scrollBarMinimum', Qt.Vertical)
def page_down(self, count=1):
self._key_press(Qt.Key_PageDown, count, 'scrollBarMaximum',
Qt.Vertical)
def at_top(self):
return self.pos_px().y() == 0
def at_bottom(self):
frame = self._widget.page().currentFrame()
return self.pos_px().y() >= frame.scrollBarMaximum(Qt.Vertical)
class WebKitHistory(browsertab.AbstractHistory):
"""QtWebKit implementations related to page history."""
def current_idx(self):
return self._history.currentItemIndex()
def back(self):
self._history.back()
def forward(self):
self._history.forward()
def can_go_back(self):
return self._history.canGoBack()
def can_go_forward(self):
return self._history.canGoForward()
def serialize(self):
return qtutils.serialize(self._history)
def deserialize(self, data):
return qtutils.deserialize(data, self._history)
def load_items(self, items):
stream, _data, user_data = tabhistory.serialize(items)
qtutils.deserialize_stream(stream, self._history)
for i, data in enumerate(user_data):
self._history.itemAt(i).setUserData(data)
cur_data = self._history.currentItem().userData()
if cur_data is not None:
if 'zoom' in cur_data:
self._tab.zoom.set_factor(cur_data['zoom'])
if ('scroll-pos' in cur_data and
self._tab.scroller.pos_px() == QPoint(0, 0)):
QTimer.singleShot(0, functools.partial(
self._tab.scroller.to_point, cur_data['scroll-pos']))
class WebKitTab(browsertab.AbstractTab):
"""A QtWebKit tab in the browser."""
def __init__(self, win_id, mode_manager, parent=None):
super().__init__(win_id)
widget = webview.WebView(win_id, self.tab_id, tab=self)
self.history = WebKitHistory(self)
self.scroller = WebKitScroller(self, parent=self)
self.caret = WebKitCaret(win_id=win_id, mode_manager=mode_manager,
tab=self, parent=self)
self.zoom = WebKitZoom(win_id=win_id, parent=self)
self.search = WebKitSearch(parent=self)
self.printing = WebKitPrinting()
self._set_widget(widget)
self._connect_signals()
self.zoom.set_default()
self.backend = usertypes.Backend.QtWebKit
def openurl(self, url):
self._openurl_prepare(url)
self._widget.openurl(url)
def url(self):
return self._widget.url()
def dump_async(self, callback, *, plain=False):
frame = self._widget.page().mainFrame()
if plain:
callback(frame.toPlainText())
else:
callback(frame.toHtml())
def run_js_async(self, code, callback=None):
result = self.run_js_blocking(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()
def shutdown(self):
self._widget.shutdown()
def reload(self, *, force=False):
if force:
action = QWebPage.ReloadAndBypassCache
else:
action = QWebPage.Reload
self._widget.triggerPageAction(action)
def stop(self):
self._widget.stop()
def title(self):
return self._widget.title()
def clear_ssl_errors(self):
nam = self._widget.page().networkAccessManager()
nam.clear_all_ssl_errors()
def set_html(self, html, base_url):
self._widget.setHtml(html, base_url)
@pyqtSlot()
def _on_frame_load_finished(self):
"""Make sure we emit an appropriate status when loading finished.
While Qt has a bool "ok" attribute for loadFinished, it always is True
when using error pages... See
https://github.com/The-Compiler/qutebrowser/issues/84
"""
self._on_load_finished(not self._widget.page().error_occurred)
@pyqtSlot()
def _on_webkit_icon_changed(self):
"""Emit iconChanged with a QIcon like QWebEngineView does."""
self.icon_changed.emit(self._widget.icon())
def _connect_signals(self):
view = self._widget
page = view.page()
frame = page.mainFrame()
page.windowCloseRequested.connect(self.window_close_requested)
page.linkHovered.connect(self.link_hovered)
page.loadProgress.connect(self._on_load_progress)
frame.loadStarted.connect(self._on_load_started)
view.scroll_pos_changed.connect(self.scroller.perc_changed)
view.titleChanged.connect(self.title_changed)
view.urlChanged.connect(self._on_url_changed)
view.shutting_down.connect(self.shutting_down)
page.networkAccessManager().sslErrors.connect(self._on_ssl_errors)
frame.loadFinished.connect(self._on_frame_load_finished)
view.iconChanged.connect(self._on_webkit_icon_changed)

View File

@@ -21,8 +21,7 @@
import functools
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, PYQT_VERSION, Qt, QUrl, QPoint,
QTimer)
from PyQt5.QtCore import pyqtSlot, pyqtSignal, PYQT_VERSION, Qt, QUrl, QPoint
from PyQt5.QtGui import QDesktopServices
from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest
from PyQt5.QtWidgets import QFileDialog
@@ -30,8 +29,9 @@ from PyQt5.QtPrintSupport import QPrintDialog
from PyQt5.QtWebKitWidgets import QWebPage
from qutebrowser.config import config
from qutebrowser.browser import http, tabhistory, pdfjs
from qutebrowser.browser.network import networkmanager
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)
@@ -242,23 +242,6 @@ class BrowserPage(QWebPage):
else:
nam.shutdown()
def load_history(self, entries):
"""Load the history from a list of TabHistoryItem objects."""
stream, _data, user_data = tabhistory.serialize(entries)
history = self.history()
qtutils.deserialize_stream(stream, history)
for i, data in enumerate(user_data):
history.itemAt(i).setUserData(data)
cur_data = history.currentItem().userData()
if cur_data is not None:
frame = self.mainFrame()
if 'zoom' in cur_data:
frame.page().view().zoom_perc(cur_data['zoom'] * 100)
if ('scroll-pos' in cur_data and
frame.scrollPosition() == QPoint(0, 0)):
QTimer.singleShot(0, functools.partial(
frame.setScrollPosition, cur_data['scroll-pos']))
def display_content(self, reply, mimetype):
"""Display a QNetworkReply with an explicitly set mimetype."""
self.mainFrame().setContent(reply.readAll(), mimetype, reply.url())
@@ -317,7 +300,7 @@ class BrowserPage(QWebPage):
else:
reply.finished.connect(functools.partial(
self.display_content, reply, 'image/jpeg'))
elif (mimetype in {'application/pdf', 'application/x-pdf'} and
elif (mimetype in ['application/pdf', 'application/x-pdf'] and
config.get('content', 'enable-pdfjs')):
# Use pdf.js to display the page
self._show_pdfjs(reply)
@@ -435,7 +418,7 @@ class BrowserPage(QWebPage):
if data is None:
return
if 'zoom' in data:
frame.page().view().zoom_perc(data['zoom'] * 100)
frame.page().view().tab.zoom.set_factor(data['zoom'])
if 'scroll-pos' in data and frame.scrollPosition() == QPoint(0, 0):
frame.setScrollPosition(data['scroll-pos'])

View File

@@ -20,10 +20,8 @@
"""The main browser widgets."""
import sys
import itertools
import functools
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QTimer, QUrl
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QTimer, QUrl, QPoint
from PyQt5.QtGui import QPalette
from PyQt5.QtWidgets import QApplication, QStyleFactory
from PyQt5.QtWebKit import QWebSettings
@@ -32,44 +30,23 @@ 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 webpage, hints, webelem
LoadStatus = usertypes.enum('LoadStatus', ['none', 'success', 'success_https',
'error', 'warn', 'loading'])
tab_id_gen = itertools.count(0)
from qutebrowser.browser import hints
from qutebrowser.browser.webkit import webpage, webelem
class WebView(QWebView):
"""One browser tab in TabbedBrowser.
Our own subclass of a QWebView with some added bells and whistles.
"""Custom QWebView subclass with qutebrowser-specific features.
Attributes:
tab: The WebKitTab object for this WebView
hintmanager: The HintManager instance for this view.
progress: loading progress of this page.
scroll_pos: The current scroll position as (x%, y%) tuple.
statusbar_message: The current javascript statusbar message.
inspector: The QWebInspector used for this webview.
load_status: loading status of this page (index into LoadStatus)
viewing_source: Whether the webview is currently displaying source
code.
keep_icon: Whether the (e.g. cloned) icon should not be cleared on page
load.
registry: The ObjectRegistry associated with this tab.
tab_id: The tab ID of the view.
win_id: The window ID of the view.
search_text: The text of the last search.
search_flags: The search flags of the last search.
_has_ssl_errors: Whether SSL errors occurred during loading.
_zoom: A NeighborList with the zoom levels.
_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.
_default_zoom_changed: Whether the zoom was changed from the default.
_ignore_wheel_event: Ignore the next wheel event.
See https://github.com/The-Compiler/qutebrowser/issues/395
@@ -77,72 +54,44 @@ class WebView(QWebView):
scroll_pos_changed: Scroll percentage of current tab changed.
arg 1: x-position in %.
arg 2: y-position in %.
linkHovered: QWebPages linkHovered signal exposed.
load_status_changed: The loading status changed
url_text_changed: Current URL string changed.
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)
linkHovered = pyqtSignal(str, str, str)
load_status_changed = pyqtSignal(str)
url_text_changed = pyqtSignal(str)
shutting_down = pyqtSignal()
mouse_wheel_zoom = pyqtSignal(QPoint)
def __init__(self, win_id, parent=None):
def __init__(self, win_id, tab_id, tab, parent=None):
super().__init__(parent)
if sys.platform == 'darwin' and qtutils.version_check('5.4'):
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-42948
# See https://github.com/The-Compiler/qutebrowser/issues/462
self.setStyle(QStyleFactory.create('Fusion'))
self.tab = tab
self.win_id = win_id
self.load_status = LoadStatus.none
self._check_insertmode = False
self.inspector = None
self.scroll_pos = (-1, -1)
self.statusbar_message = ''
self._old_scroll_pos = (-1, -1)
self._zoom = None
self._has_ssl_errors = False
self._ignore_wheel_event = False
self.keep_icon = False
self.search_text = None
self.search_flags = 0
self.selection_enabled = False
self.init_neighborlist()
self._set_bg_color()
cfg = objreg.get('config')
cfg.changed.connect(self.init_neighborlist)
# For some reason, this signal doesn't get disconnected automatically
# when the WebView is destroyed on older PyQt versions.
# See https://github.com/The-Compiler/qutebrowser/issues/390
self.destroyed.connect(functools.partial(
cfg.changed.disconnect, self.init_neighborlist))
self.cur_url = QUrl()
self.progress = 0
self.registry = objreg.ObjectRegistry()
self.tab_id = next(tab_id_gen)
tab_registry = objreg.get('tab-registry', scope='window',
window=win_id)
tab_registry[self.tab_id] = self
objreg.register('webview', self, registry=self.registry)
self._tab_id = tab_id
page = self._init_page()
hintmanager = hints.HintManager(win_id, self.tab_id, self)
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, registry=self.registry)
objreg.register('hintmanager', hintmanager, scope='tab', window=win_id,
tab=tab_id)
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)
self.viewing_source = False
self.setZoomFactor(float(config.get('ui', 'default-zoom')) / 100)
self._default_zoom_changed = False
if config.get('input', 'rocker-gestures'):
self.setContextMenuPolicy(Qt.PreventContextMenu)
self.urlChanged.connect(self.on_url_changed)
self.loadProgress.connect(lambda p: setattr(self, 'progress', p))
objreg.get('config').changed.connect(self.on_config_changed)
@pyqtSlot()
@@ -152,30 +101,24 @@ class WebView(QWebView):
no_formatting = QUrl.UrlFormattingOption(0)
orig_url = self.page().mainFrame().requestedUrl()
if (orig_url.isValid() and
not orig_url.matches(self.cur_url, no_formatting)):
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.cur_url, self.title())
history.add_url(self.url(), self.title())
def _init_page(self):
"""Initialize the QWebPage used by this view."""
page = webpage.BrowserPage(self.win_id, self.tab_id, self)
page = webpage.BrowserPage(self.win_id, self._tab_id, self)
self.setPage(page)
page.linkHovered.connect(self.linkHovered)
page.mainFrame().loadStarted.connect(self.on_load_started)
page.mainFrame().loadFinished.connect(self.on_load_finished)
page.mainFrame().initialLayoutCompleted.connect(
self.on_initial_layout_completed)
page.statusBarMessage.connect(
lambda msg: setattr(self, 'statusbar_message', msg))
page.networkAccessManager().sslErrors.connect(
lambda *args: setattr(self, '_has_ssl_errors', True))
return page
def __repr__(self):
url = utils.elide(self.url().toDisplayString(QUrl.EncodeUnicode), 100)
return utils.get_repr(self, tab_id=self.tab_id, url=url)
return utils.get_repr(self, tab_id=self._tab_id, url=url)
def __del__(self):
# Explicitly releasing the page here seems to prevent some segfaults
@@ -190,14 +133,6 @@ class WebView(QWebView):
# deleted
pass
def _set_load_status(self, val):
"""Setter for load_status."""
if not isinstance(val, LoadStatus):
raise TypeError("Type {} is no LoadStatus member!".format(val))
log.webview.debug("load status for {}: {}".format(repr(self), val))
self.load_status = val
self.load_status_changed.emit(val.name)
def _set_bg_color(self):
"""Set the webpage background color as configured."""
col = config.get('colors', 'webpage.bg')
@@ -209,14 +144,8 @@ class WebView(QWebView):
@pyqtSlot(str, str)
def on_config_changed(self, section, option):
"""Reinitialize the zoom neighborlist if related config changed."""
if section == 'ui' and option in ('zoom-levels', 'default-zoom'):
if not self._default_zoom_changed:
self.setZoomFactor(float(config.get('ui', 'default-zoom')) /
100)
self._default_zoom_changed = False
self.init_neighborlist()
elif section == 'input' and option == 'rocker-gestures':
"""Update rocker gestures/background color."""
if section == 'input' and option == 'rocker-gestures':
if config.get('input', 'rocker-gestures'):
self.setContextMenuPolicy(Qt.PreventContextMenu)
else:
@@ -224,27 +153,20 @@ class WebView(QWebView):
elif section == 'colors' and option == 'webpage.bg':
self._set_bg_color()
def init_neighborlist(self):
"""Initialize the _zoom neighborlist."""
levels = config.get('ui', 'zoom-levels')
self._zoom = usertypes.NeighborList(
levels, mode=usertypes.NeighborList.Modes.edge)
self._zoom.fuzzyval = config.get('ui', 'default-zoom')
def _mousepress_backforward(self, e):
"""Handle back/forward mouse button presses.
Args:
e: The QMouseEvent.
"""
if e.button() in (Qt.XButton1, Qt.LeftButton):
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):
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()
@@ -356,12 +278,6 @@ class WebView(QWebView):
Args:
url: The URL to load as QUrl
"""
qtutils.ensure_valid(url)
urlstr = url.toDisplayString()
log.webview.debug("New title: {}".format(urlstr))
self.titleChanged.emit(urlstr)
self.cur_url = url
self.url_text_changed.emit(url.toDisplayString())
self.load(url)
if url.scheme() == 'qute':
frame = self.page().mainFrame()
@@ -380,45 +296,6 @@ class WebView(QWebView):
bridge = objreg.get('js-bridge')
frame.addToJavaScriptWindowObject('qute', bridge)
def zoom_perc(self, perc, fuzzyval=True):
"""Zoom to a given zoom percentage.
Args:
perc: The zoom percentage as int.
fuzzyval: Whether to set the NeighborLists fuzzyval.
"""
if fuzzyval:
self._zoom.fuzzyval = int(perc)
if perc < 0:
raise ValueError("Can't zoom {}%!".format(perc))
self.setZoomFactor(float(perc) / 100)
self._default_zoom_changed = True
def zoom(self, offset):
"""Increase/Decrease the zoom level.
Args:
offset: The offset in the zoom level list.
Return:
The new zoom percentage.
"""
level = self._zoom.getitem(offset)
self.zoom_perc(level, fuzzyval=False)
return level
@pyqtSlot('QUrl')
def on_url_changed(self, url):
"""Update cur_url when URL has changed.
If the URL is invalid, we just ignore it here.
"""
if url.isValid():
self.cur_url = url
self.url_text_changed.emit(url.toDisplayString())
if not self.title():
self.titleChanged.emit(self.url().toDisplayString())
@pyqtSlot('QMouseEvent')
def on_mouse_event(self, evt):
"""Post a new mouse event from a hintmanager."""
@@ -426,14 +303,6 @@ class WebView(QWebView):
self.setFocus()
QApplication.postEvent(self, evt)
@pyqtSlot()
def on_load_started(self):
"""Leave insert/hint mode and set vars when a new page is loading."""
self.progress = 0
self.viewing_source = False
self._has_ssl_errors = False
self._set_load_status(LoadStatus.loading)
@pyqtSlot()
def on_load_finished(self):
"""Handle a finished page load.
@@ -443,18 +312,6 @@ class WebView(QWebView):
See https://github.com/The-Compiler/qutebrowser/issues/84
"""
ok = not self.page().error_occurred
if ok and not self._has_ssl_errors:
if self.cur_url.scheme() == 'https':
self._set_load_status(LoadStatus.success_https)
else:
self._set_load_status(LoadStatus.success)
elif ok:
self._set_load_status(LoadStatus.warn)
else:
self._set_load_status(LoadStatus.error)
if not self.title():
self.titleChanged.emit(self.url().toDisplayString())
self._handle_auto_insert_mode(ok)
def _handle_auto_insert_mode(self, ok):
@@ -480,91 +337,21 @@ class WebView(QWebView):
@pyqtSlot(usertypes.KeyMode)
def on_mode_entered(self, mode):
"""Ignore attempts to focus the widget if in any status-input mode."""
if mode in (usertypes.KeyMode.command, usertypes.KeyMode.prompt,
usertypes.KeyMode.yesno):
if mode in [usertypes.KeyMode.command, usertypes.KeyMode.prompt,
usertypes.KeyMode.yesno]:
log.webview.debug("Ignoring focus because mode {} was "
"entered.".format(mode))
self.setFocusPolicy(Qt.NoFocus)
elif mode == usertypes.KeyMode.caret:
settings = self.settings()
settings.setAttribute(QWebSettings.CaretBrowsingEnabled, True)
self.selection_enabled = bool(self.page().selectedText())
if self.isVisible():
# Sometimes the caret isn't immediately visible, but unfocusing
# and refocusing it fixes that.
self.clearFocus()
self.setFocus(Qt.OtherFocusReason)
# Move the caret to the first element in the viewport if there
# isn't any text which is already selected.
#
# Note: We can't use hasSelection() here, as that's always
# true in caret mode.
if not self.page().selectedText():
self.page().currentFrame().evaluateJavaScript(
utils.read_file('javascript/position_caret.js'))
@pyqtSlot(usertypes.KeyMode)
def on_mode_left(self, mode):
"""Restore focus policy if status-input modes were left."""
if mode in (usertypes.KeyMode.command, usertypes.KeyMode.prompt,
usertypes.KeyMode.yesno):
if mode in [usertypes.KeyMode.command, usertypes.KeyMode.prompt,
usertypes.KeyMode.yesno]:
log.webview.debug("Restoring focus policy because mode {} was "
"left.".format(mode))
elif mode == usertypes.KeyMode.caret:
settings = self.settings()
if settings.testAttribute(QWebSettings.CaretBrowsingEnabled):
if self.selection_enabled and self.hasSelection():
# Remove selection if it exists
self.triggerPageAction(QWebPage.MoveToNextChar)
settings.setAttribute(QWebSettings.CaretBrowsingEnabled, False)
self.selection_enabled = False
self.setFocusPolicy(Qt.WheelFocus)
def search(self, text, flags):
"""Search for text in the current page.
Args:
text: The text to search for.
flags: The QWebPage::FindFlags.
"""
log.webview.debug("Searching with text '{}' and flags "
"0x{:04x}.".format(text, int(flags)))
old_scroll_pos = self.scroll_pos
flags = QWebPage.FindFlags(flags)
found = self.findText(text, flags)
backward = flags & QWebPage.FindBackward
if not found and not flags & QWebPage.HighlightAllOccurrences and text:
# User disabled wrapping; but findText() just returns False. If we
# have a selection, we know there's a match *somewhere* on the page
if (not flags & QWebPage.FindWrapsAroundDocument and
self.hasSelection()):
if not backward:
message.warning(self.win_id, "Search hit BOTTOM without "
"match for: {}".format(text),
immediately=True)
else:
message.warning(self.win_id, "Search hit TOP without "
"match for: {}".format(text),
immediately=True)
else:
message.warning(self.win_id, "Text '{}' not found on "
"page!".format(text), immediately=True)
else:
def check_scroll_pos():
"""Check if the scroll position got smaller and show info."""
if not backward and self.scroll_pos < old_scroll_pos:
message.info(self.win_id, "Search hit BOTTOM, continuing "
"at TOP", immediately=True)
elif backward and self.scroll_pos > old_scroll_pos:
message.info(self.win_id, "Search hit TOP, continuing at "
"BOTTOM", immediately=True)
# We first want QWebPage to refresh.
QTimer.singleShot(0, check_scroll_pos)
def createWindow(self, wintype):
"""Called by Qt when a page wants to create a new window.
@@ -589,7 +376,8 @@ class WebView(QWebView):
"support that!")
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=self.win_id)
return tabbed_browser.tabopen(background=False)
# pylint: disable=protected-access
return tabbed_browser.tabopen(background=False)._widget
def paintEvent(self, e):
"""Extend paintEvent to emit a signal if the scroll position changed.
@@ -636,7 +424,7 @@ class WebView(QWebView):
is_rocker_gesture = (config.get('input', 'rocker-gestures') and
e.buttons() == Qt.LeftButton | Qt.RightButton)
if e.button() in (Qt.XButton1, Qt.XButton2) or is_rocker_gesture:
if e.button() in [Qt.XButton1, Qt.XButton2] or is_rocker_gesture:
self._mousepress_backforward(e)
super().mousePressEvent(e)
return
@@ -671,14 +459,6 @@ class WebView(QWebView):
return
if e.modifiers() & Qt.ControlModifier:
e.accept()
divider = config.get('input', 'mouse-zoom-divider')
factor = self.zoomFactor() + e.angleDelta().y() / divider
if factor < 0:
return
perc = int(100 * factor)
message.info(self.win_id, "Zoom level: {}%".format(perc))
self._zoom.fuzzyval = perc
self.setZoomFactor(factor)
self._default_zoom_changed = True
self.mouse_wheel_zoom.emit(e.angleDelta())
else:
super().wheelEvent(e)

View File

@@ -25,7 +25,7 @@ Defined here to avoid circular dependency hell.
class CommandError(Exception):
"""Raised when a command encounters a error while running."""
"""Raised when a command encounters an error while running."""
pass

View File

@@ -80,6 +80,8 @@ class Command:
parser: The ArgumentParser to use to parse this command.
flags_with_args: A list of flags which take an argument.
no_cmd_split: If true, ';;' to split sub-commands is ignored.
backend: Which backend the command works with (or None if it works with
both)
_qute_args: The saved data from @cmdutils.argument
_needs_js: Whether the command needs javascript enabled
_modes: The modes the command can be executed in.
@@ -92,7 +94,8 @@ class Command:
def __init__(self, *, handler, name, instance=None, maxsplit=None,
hide=False, modes=None, not_modes=None, needs_js=False,
debug=False, ignore_args=False, deprecated=False,
no_cmd_split=False, star_args_optional=False, scope='global'):
no_cmd_split=False, star_args_optional=False, scope='global',
backend=None):
# I really don't know how to solve this in a better way, I tried.
# pylint: disable=too-many-locals
if modes is not None and not_modes is not None:
@@ -123,6 +126,8 @@ class Command:
self.ignore_args = ignore_args
self.handler = handler
self.no_cmd_split = no_cmd_split
self.backend = backend
self.docparser = docutils.DocstringParser(handler)
self.parser = argparser.ArgumentParser(
name, description=self.docparser.short_desc,
@@ -170,10 +175,22 @@ class Command:
raise cmdexc.PrerequisitesError(
"{}: This command is not allowed in {} mode.".format(
self.name, mode_names))
if self._needs_js and not QWebSettings.globalSettings().testAttribute(
QWebSettings.JavascriptEnabled):
raise cmdexc.PrerequisitesError(
"{}: This command needs javascript enabled.".format(self.name))
backend_mapping = {
'webkit': usertypes.Backend.QtWebKit,
'webengine': usertypes.Backend.QtWebEngine,
}
used_backend = backend_mapping[objreg.get('args').backend]
if self.backend is not None and used_backend != self.backend:
raise cmdexc.PrerequisitesError(
"{}: Only available with {} "
"backend.".format(self.name, self.backend.name))
if self.deprecated:
message.warning(win_id, '{} is deprecated - {}'.format(
self.name, self.deprecated))
@@ -483,6 +500,9 @@ class Command:
dbgout = ["command called:", self.name]
if args:
dbgout.append(str(args))
elif args is None:
args = []
if count is not None:
dbgout.append("(count={})".format(count))
log.commands.debug(' '.join(dbgout))
@@ -497,8 +517,8 @@ class Command:
e.status, e))
return
self._count = count
posargs, kwargs = self._get_call_args(win_id)
self._check_prerequisites(win_id)
posargs, kwargs = self._get_call_args(win_id)
log.commands.debug('Calling {}'.format(
debug_utils.format_call(self.handler, posargs, kwargs)))
self.handler(*posargs, **kwargs)

View File

@@ -26,12 +26,13 @@ from PyQt5.QtCore import pyqtSlot, QUrl, QObject
from qutebrowser.config import config, configexc
from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.utils import message, log, objreg, qtutils
from qutebrowser.utils import message, objreg, qtutils
from qutebrowser.misc import split
ParseResult = collections.namedtuple('ParseResult', ['cmd', 'args', 'cmdline',
'count'])
last_command = {}
def _current_url(tabbed_browser):
@@ -80,21 +81,22 @@ class CommandRunner(QObject):
self._partial_match = partial_match
self._win_id = win_id
def _get_alias(self, text):
def _get_alias(self, text, default=None):
"""Get an alias from the config.
Args:
text: The text to parse.
default : Default value to return when alias was not found.
Return:
None if no alias was found.
The new command string if an alias was found.
The new command string if an alias was found. Default value
otherwise.
"""
parts = text.strip().split(maxsplit=1)
try:
alias = config.get('aliases', parts[0])
except (configexc.NoOptionError, configexc.NoSectionError):
return None
return default
try:
new_cmd = '{} {}'.format(alias, parts[1])
except IndexError:
@@ -103,7 +105,7 @@ class CommandRunner(QObject):
new_cmd += ' '
return new_cmd
def parse_all(self, text, *args, **kwargs):
def parse_all(self, text, aliases=True, *args, **kwargs):
"""Split a command on ;; and parse all parts.
If the first command in the commandline is a non-split one, it only
@@ -111,11 +113,18 @@ class CommandRunner(QObject):
Args:
text: Text to parse.
aliases: Whether to handle aliases.
*args/**kwargs: Passed to parse().
Yields:
ParseResult tuples.
"""
if not text:
raise cmdexc.NoSuchCommandError("No command given")
if aliases:
text = self._get_alias(text, text)
if ';;' in text:
# Get the first command and check if it doesn't want to have ;;
# split.
@@ -159,12 +168,11 @@ class CommandRunner(QObject):
cmdline = text.split()
return ParseResult(cmd=None, args=None, cmdline=cmdline, count=count)
def parse(self, text, *, aliases=True, fallback=False, keep=False):
def parse(self, text, *, fallback=False, keep=False):
"""Split the commandline text into command and arguments.
Args:
text: Text to parse.
aliases: Whether to handle aliases.
fallback: Whether to do a fallback splitting when the command was
unknown.
keep: Whether to keep special chars and whitespace
@@ -178,13 +186,6 @@ class CommandRunner(QObject):
if not cmdstr and not fallback:
raise cmdexc.NoSuchCommandError("No command given")
if aliases:
new_cmd = self._get_alias(text)
if new_cmd is not None:
log.commands.debug("Re-parsing with '{}'.".format(new_cmd))
return self.parse(new_cmd, aliases=False, fallback=fallback,
keep=keep)
if self._partial_match:
cmdstr = self._completion_match(cmdstr)
@@ -216,7 +217,7 @@ class CommandRunner(QObject):
cmdstr modified to the matching completion or unmodified
"""
matches = []
for valid_command in cmdutils.cmd_dict.keys():
for valid_command in cmdutils.cmd_dict:
if valid_command.find(cmdstr) == 0:
matches.append(valid_command)
if len(matches) == 1:
@@ -274,6 +275,10 @@ class CommandRunner(QObject):
count: The count to pass to the command.
"""
for result in self.parse_all(text):
mode_manager = objreg.get('mode-manager', scope='window',
window=self._win_id)
cur_mode = mode_manager.mode
args = replace_variables(self._win_id, result.args)
if count is not None:
if result.count is not None:
@@ -285,6 +290,11 @@ class CommandRunner(QObject):
else:
result.cmd.run(self._win_id, args)
if result.cmdline[0] != 'repeat-command':
last_command[cur_mode] = (
self._parse_count(text)[1],
count if count is not None else result.count)
@pyqtSlot(str, int)
@pyqtSlot(str)
def run_safely(self, text, count=None):

View File

@@ -26,9 +26,10 @@ import tempfile
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QSocketNotifier
from qutebrowser.utils import message, log, objreg, standarddir
from qutebrowser.commands import runners, cmdexc
from qutebrowser.commands import runners
from qutebrowser.config import config
from qutebrowser.misc import guiprocess
from qutebrowser.browser.webkit import downloads
class _QtFIFOReader(QObject):
@@ -85,6 +86,10 @@ class _BaseUserscriptRunner(QObject):
_proc: The GUIProcess which is being executed.
_win_id: The window ID this runner is associated with.
_cleaned_up: Whether temporary files were cleaned up.
_text_stored: Set when the page text was stored async.
_html_stored: Set when the page html was stored async.
_args: Arguments to pass to _run_process.
_kwargs: Keyword arguments to pass to _run_process.
Signals:
got_cmd: Emitted when a new command arrived and should be executed.
@@ -100,9 +105,41 @@ class _BaseUserscriptRunner(QObject):
self._win_id = win_id
self._filepath = None
self._proc = None
self._env = None
self._env = {}
self._text_stored = False
self._html_stored = False
self._args = None
self._kwargs = None
def _run_process(self, cmd, *args, env, verbose):
def store_text(self, text):
"""Called as callback when the text is ready from the web backend."""
with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8',
suffix='.txt',
delete=False) as txt_file:
txt_file.write(text)
self._env['QUTE_TEXT'] = txt_file.name
self._text_stored = True
log.procs.debug("Text stored from webview")
if self._text_stored and self._html_stored:
log.procs.debug("Both text/HTML stored, kicking off userscript!")
self._run_process(*self._args, **self._kwargs)
def store_html(self, html):
"""Called as callback when the html is ready from the web backend."""
with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8',
suffix='.html',
delete=False) as html_file:
html_file.write(html)
self._env['QUTE_HTML'] = html_file.name
self._html_stored = True
log.procs.debug("HTML stored from webview")
if self._text_stored and self._html_stored:
log.procs.debug("Both text/HTML stored, kicking off userscript!")
self._run_process(*self._args, **self._kwargs)
def _run_process(self, cmd, *args, env=None, verbose=False):
"""Start the given command.
Args:
@@ -111,7 +148,7 @@ class _BaseUserscriptRunner(QObject):
env: A dictionary of environment variables to add.
verbose: Show notifications when the command started/exited.
"""
self._env = {'QUTE_FIFO': self._filepath}
self._env['QUTE_FIFO'] = self._filepath
if env is not None:
self._env.update(env)
self._proc = guiprocess.GUIProcess(self._win_id, 'userscript',
@@ -143,18 +180,19 @@ class _BaseUserscriptRunner(QObject):
fn, e))
self._filepath = None
self._proc = None
self._env = None
self._env = {}
self._text_stored = False
self._html_stored = False
def run(self, cmd, *args, env=None, verbose=False):
"""Run the userscript given.
def prepare_run(self, *args, **kwargs):
"""Prepare running the userscript given.
Needs to be overridden by subclasses.
The script will actually run after store_text and store_html have been
called.
Args:
cmd: The command to be started.
*args: The arguments to hand to the command
env: A dictionary of environment variables to add.
verbose: Show notifications when the command started/exited.
Passed to _run_process.
"""
raise NotImplementedError
@@ -189,7 +227,10 @@ class _POSIXUserscriptRunner(_BaseUserscriptRunner):
super().__init__(win_id, parent)
self._reader = None
def run(self, cmd, *args, env=None, verbose=False):
def prepare_run(self, *args, **kwargs):
self._args = args
self._kwargs = kwargs
try:
# tempfile.mktemp is deprecated and discouraged, but we use it here
# to create a FIFO since the only other alternative would be to
@@ -208,8 +249,6 @@ class _POSIXUserscriptRunner(_BaseUserscriptRunner):
self._reader = _QtFIFOReader(self._filepath)
self._reader.got_line.connect(self.got_cmd)
self._run_process(cmd, *args, env=env, verbose=verbose)
@pyqtSlot()
def on_proc_finished(self):
self._cleanup()
@@ -279,86 +318,35 @@ class _WindowsUserscriptRunner(_BaseUserscriptRunner):
"""Read back the commands when the process finished."""
self._cleanup()
def run(self, cmd, *args, env=None, verbose=False):
def prepare_run(self, *args, **kwargs):
self._args = args
self._kwargs = kwargs
try:
self._oshandle, self._filepath = tempfile.mkstemp(text=True)
except OSError as e:
message.error(self._win_id, "Error while creating tempfile: "
"{}".format(e))
return
self._run_process(cmd, *args, env=env, verbose=verbose)
class _DummyUserscriptRunner(QObject):
class UnsupportedError(Exception):
"""Simple dummy runner which displays an error when using userscripts.
"""Raised when userscripts aren't supported on this platform."""
Used on unknown systems since we don't know what (or if any) approach will
work there.
Signals:
finished: Always emitted.
"""
finished = pyqtSignal()
def __init__(self, win_id, parent=None):
# pylint: disable=unused-argument
super().__init__(parent)
def run(self, cmd, *args, env=None, verbose=False):
"""Print an error as userscripts are not supported."""
# pylint: disable=unused-argument,unused-variable
self.finished.emit()
raise cmdexc.CommandError(
"Userscripts are not supported on this platform!")
def __str__(self):
return "Userscripts are not supported on this platform!"
# Here we basically just assign a generic UserscriptRunner class which does the
# right thing depending on the platform.
if os.name == 'posix':
UserscriptRunner = _POSIXUserscriptRunner
elif os.name == 'nt': # pragma: no cover
UserscriptRunner = _WindowsUserscriptRunner
else: # pragma: no cover
UserscriptRunner = _DummyUserscriptRunner
def run_async(tab, cmd, *args, win_id, env, verbose=False):
"""Run a userscript after dumping page html/source.
def store_source(frame):
"""Store HTML/plaintext in files.
This writes files containing the HTML/plaintext source of the page, and
returns a dict with the paths as QUTE_HTML/QUTE_TEXT.
Args:
frame: The QWebFrame to get the info from, or None to do nothing.
Return:
A dictionary with the needed environment variables.
Warning:
The caller is responsible to delete the files after using them!
"""
if frame is None:
return {}
env = {}
with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8',
suffix='.html',
delete=False) as html_file:
html_file.write(frame.toHtml())
env['QUTE_HTML'] = html_file.name
with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8',
suffix='.txt',
delete=False) as txt_file:
txt_file.write(frame.toPlainText())
env['QUTE_TEXT'] = txt_file.name
return env
def run(cmd, *args, win_id, env, verbose=False):
"""Convenience method to run a userscript.
Raises:
UnsupportedError if userscripts are not supported on the current
platform.
Args:
tab: The WebKitTab/WebEngineTab to get the source from.
cmd: The userscript binary to run.
*args: The arguments to pass to the userscript.
win_id: The window id the userscript is executed in.
@@ -368,7 +356,14 @@ def run(cmd, *args, win_id, env, verbose=False):
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=win_id)
commandrunner = runners.CommandRunner(win_id, parent=tabbed_browser)
runner = UserscriptRunner(win_id, tabbed_browser)
if os.name == 'posix':
runner = _POSIXUserscriptRunner(win_id, tabbed_browser)
elif os.name == 'nt': # pragma: no cover
runner = _WindowsUserscriptRunner(win_id, tabbed_browser)
else: # pragma: no cover
raise UnsupportedError
runner.got_cmd.connect(
lambda cmd:
log.commands.debug("Got userscript command: {}".format(cmd)))
@@ -376,6 +371,15 @@ def run(cmd, *args, win_id, env, verbose=False):
user_agent = config.get('network', 'user-agent')
if user_agent is not None:
env['QUTE_USER_AGENT'] = user_agent
config_dir = standarddir.config()
if config_dir is not None:
env['QUTE_CONFIG_DIR'] = config_dir
data_dir = standarddir.data()
if data_dir is not None:
env['QUTE_DATA_DIR'] = data_dir
download_dir = downloads.download_dir()
if download_dir is not None:
env['QUTE_DOWNLOAD_DIR'] = download_dir
cmd_path = os.path.expanduser(cmd)
# if cmd is not given as an absolute path, look it up
@@ -388,6 +392,9 @@ def run(cmd, *args, win_id, env, verbose=False):
"userscripts", cmd)
log.misc.debug("Userscript to run: {}".format(cmd_path))
runner.run(cmd_path, *args, env=env, verbose=verbose)
runner.finished.connect(commandrunner.deleteLater)
runner.finished.connect(runner.deleteLater)
runner.prepare_run(cmd_path, *args, env=env, verbose=verbose)
tab.dump_async(runner.store_html)
tab.dump_async(runner.store_text, plain=True)

View File

@@ -22,7 +22,7 @@
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QTimer
from qutebrowser.config import config
from qutebrowser.commands import cmdexc, cmdutils, runners
from qutebrowser.commands import cmdutils, runners
from qutebrowser.utils import usertypes, log, objreg, utils
from qutebrowser.completion.models import instances, sortfilter
@@ -354,7 +354,7 @@ class Completer(QObject):
if completion.enabled:
completion.show()
def split(self, keep=False, aliases=False):
def split(self, keep=False):
"""Get the text split up in parts.
Args:
@@ -371,7 +371,7 @@ class Completer(QObject):
# the whitespace.
return [text]
runner = runners.CommandRunner(self._win_id)
result = runner.parse(text, fallback=True, aliases=aliases, keep=keep)
result = runner.parse(text, fallback=True, keep=keep)
parts = result.cmdline
if self._empty_item_idx is not None:
log.completion.debug("Empty element queued at {}, "
@@ -485,16 +485,3 @@ class Completer(QObject):
"""Select the next completion item."""
self._open_completion_if_needed()
self.next_prev_item.emit(False)
@cmdutils.register(instance='completion', hide=True,
modes=[usertypes.KeyMode.command], scope='window')
def completion_item_del(self):
"""Delete the current completion item."""
completion = objreg.get('completion', scope='window',
window=self._win_id)
if not completion.currentIndex().isValid():
raise cmdexc.CommandError("No item selected!")
try:
self.model().srcmodel.delete_cur_item(completion)
except NotImplementedError:
raise cmdexc.CommandError("Cannot delete this item.")

View File

@@ -29,7 +29,8 @@ from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QItemSelectionModel
from qutebrowser.config import config, style
from qutebrowser.completion import completiondelegate, completer
from qutebrowser.completion.models import base
from qutebrowser.utils import qtutils, objreg, utils
from qutebrowser.utils import qtutils, objreg, utils, usertypes
from qutebrowser.commands import cmdexc, cmdutils
class CompletionView(QTreeView):
@@ -277,3 +278,14 @@ class CompletionView(QTreeView):
if scrollbar is not None:
scrollbar.setValue(scrollbar.minimum())
super().showEvent(e)
@cmdutils.register(instance='completion', hide=True,
modes=[usertypes.KeyMode.command], scope='window')
def completion_item_del(self):
"""Delete the current completion item."""
if not self.currentIndex().isValid():
raise cmdexc.CommandError("No item selected!")
try:
self.model().srcmodel.delete_cur_item(self)
except NotImplementedError:
raise cmdexc.CommandError("Cannot delete this item.")

View File

@@ -20,15 +20,13 @@
"""Global instances of the completion models.
Module attributes:
_instances: An dict of available completions.
_instances: A dict of available completions.
INITIALIZERS: A {usertypes.Completion: callable} dict of functions to
initialize completions.
"""
import functools
from PyQt5.QtCore import pyqtSlot
from qutebrowser.completion.models import (miscmodels, urlmodel, configmodel,
base)
from qutebrowser.utils import objreg, usertypes, log, debug
@@ -84,7 +82,6 @@ def _init_setting_completions():
_instances[usertypes.Completion.value][sectname][opt] = val_model
@pyqtSlot()
def init_quickmark_completions():
"""Initialize quickmark completion models."""
log.completion.debug("Initializing quickmark completion.")
@@ -96,7 +93,6 @@ def init_quickmark_completions():
_instances[usertypes.Completion.quickmark_by_name] = model
@pyqtSlot()
def init_bookmark_completions():
"""Initialize bookmark completion models."""
log.completion.debug("Initializing bookmark completion.")
@@ -108,7 +104,6 @@ def init_bookmark_completions():
_instances[usertypes.Completion.bookmark_by_url] = model
@pyqtSlot()
def init_session_completion():
"""Initialize session completion model."""
log.completion.debug("Initializing session completion.")

View File

@@ -19,12 +19,11 @@
"""Misc. CompletionModels."""
from collections import defaultdict
from PyQt5.QtCore import Qt, QTimer, pyqtSlot
from qutebrowser.browser import webview
from qutebrowser.browser import browsertab
from qutebrowser.config import config, configdata
from qutebrowser.utils import objreg, log, qtutils, utils
from qutebrowser.utils import objreg, log, qtutils
from qutebrowser.commands import cmdutils
from qutebrowser.completion.models import base
@@ -53,14 +52,8 @@ class CommandCompletionModel(base.BaseCompletionModel):
cat = self.new_category("Commands")
# map each command to its bound keys and show these in the misc column
keyconf = objreg.get('key-config')
cmd_to_keys = defaultdict(list)
for key, cmd in keyconf.get_bindings_for('normal').items():
# put special bindings last
if utils.is_special_key(key):
cmd_to_keys[cmd].append(key)
else:
cmd_to_keys[cmd].insert(0, key)
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]))
@@ -183,7 +176,7 @@ class TabCompletionModel(base.BaseCompletionModel):
window=win_id)
for i in range(tabbed_browser.count()):
tab = tabbed_browser.widget(i)
tab.url_text_changed.connect(self.rebuild)
tab.url_changed.connect(self.rebuild)
tab.shutting_down.connect(self.delayed_rebuild)
tabbed_browser.new_tab.connect(self.on_new_tab)
objreg.get("app").new_window.connect(self.on_new_window)
@@ -193,10 +186,10 @@ class TabCompletionModel(base.BaseCompletionModel):
"""Add hooks to new windows."""
window.tabbed_browser.new_tab.connect(self.on_new_tab)
@pyqtSlot(webview.WebView)
@pyqtSlot(browsertab.AbstractTab)
def on_new_tab(self, tab):
"""Add hooks to new tabs."""
tab.url_text_changed.connect(self.rebuild)
tab.url_changed.connect(self.rebuild)
tab.shutting_down.connect(self.delayed_rebuild)
self.rebuild()

View File

@@ -192,4 +192,4 @@ class UrlCompletionModel(base.BaseCompletionModel):
sibling = index.sibling(index.row(), self.TEXT_COLUMN)
qtutils.ensure_valid(sibling)
name = sibling.data()
quickmark_manager.quickmark_del(name)
quickmark_manager.delete(name)

View File

@@ -33,7 +33,7 @@ import contextlib
import collections
import collections.abc
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QUrl, QSettings
from PyQt5.QtCore import pyqtSignal, QObject, QUrl, QSettings
from qutebrowser.config import configdata, configexc, textwrapper
from qutebrowser.config.parsers import ini, keyconf
@@ -94,8 +94,6 @@ class change_filter: # pylint: disable=invalid-name
The decorated function.
"""
if self._function:
@pyqtSlot(str, str)
@pyqtSlot()
@functools.wraps(func)
def wrapper(sectname=None, optname=None):
if sectname is None and optname is None:
@@ -108,8 +106,6 @@ class change_filter: # pylint: disable=invalid-name
else:
return func()
else:
@pyqtSlot(str, str)
@pyqtSlot()
@functools.wraps(func)
def wrapper(wrapper_self, sectname=None, optname=None):
if sectname is None and optname is None:
@@ -211,7 +207,7 @@ def _init_misc():
"""Initialize misc. config-related files."""
save_manager = objreg.get('save-manager')
state_config = ini.ReadWriteConfigParser(standarddir.data(), 'state')
for sect in ('general', 'geometry'):
for sect in ['general', 'geometry']:
try:
state_config.add_section(sect)
except configparser.DuplicateSectionError:
@@ -242,7 +238,7 @@ def _init_misc():
path = os.devnull
else:
path = os.path.join(standarddir.config(), 'qsettings')
for fmt in (QSettings.NativeFormat, QSettings.IniFormat):
for fmt in [QSettings.NativeFormat, QSettings.IniFormat]:
QSettings.setPath(fmt, QSettings.UserScope, path)
@@ -310,7 +306,7 @@ class ConfigManager(QObject):
sections: The configuration data as an OrderedDict.
_fname: The filename to be opened.
_configdir: The dictionary to read the config from and save it in.
_interpolation: An configparser.Interpolation object
_interpolation: A configparser.Interpolation object
_proxies: configparser.SectionProxy objects for sections.
_initialized: Whether the ConfigManager is fully initialized yet.
@@ -346,11 +342,15 @@ class ConfigManager(QObject):
DELETED_OPTIONS = [
('colors', 'tab.separator'),
('colors', 'tabs.separator'),
('colors', 'tab.seperator'), # pragma: no spellcheck
('colors', 'tabs.seperator'), # pragma: no spellcheck
('colors', 'completion.item.bg'),
('tabs', 'indicator-space'),
('tabs', 'hide-auto'),
('tabs', 'auto-hide'),
('tabs', 'hide-always'),
('ui', 'display-statusbar-messages'),
('general', 'wrap-search'),
]
CHANGED_OPTIONS = {
('content', 'cookies-accept'):
@@ -494,7 +494,7 @@ class ConfigManager(QObject):
for sectname in cp:
if sectname in self.RENAMED_SECTIONS:
sectname = self.RENAMED_SECTIONS[sectname]
if sectname is not 'DEFAULT' and sectname not in self.sections:
if sectname != 'DEFAULT' and sectname not in self.sections:
if not relaxed:
raise configexc.NoSectionError(sectname)
for sectname in self.sections:
@@ -516,7 +516,7 @@ class ConfigManager(QObject):
k = k[1:]
if (sectname, k) in self.DELETED_OPTIONS:
return
continue
if (sectname, k) in self.RENAMED_OPTIONS:
k = self.RENAMED_OPTIONS[sectname, k]
if (sectname, k) in self.CHANGED_OPTIONS:
@@ -549,7 +549,7 @@ class ConfigManager(QObject):
"""Notify other objects the config has changed."""
log.config.debug("Config option changed: {} -> {}".format(
sectname, optname))
if sectname in ('colors', 'fonts'):
if sectname in ['colors', 'fonts']:
self.style_changed.emit(sectname, optname)
self.changed.emit(sectname, optname)

View File

@@ -94,7 +94,7 @@ SECTION_DESC = {
"Colors used in the UI.\n"
"A value can be in one of the following format:\n\n"
" * `#RGB`/`#RRGGBB`/`#RRRGGGBBB`/`#RRRRGGGGBBBB`\n"
" * A SVG color name as specified in http://www.w3.org/TR/SVG/"
" * An SVG color name as specified in http://www.w3.org/TR/SVG/"
"types.html#ColorKeywords[the W3C specification].\n"
" * transparent (no color)\n"
" * `rgb(r, g, b)` / `rgba(r, g, b, a)` (values 0-255 or "
@@ -134,11 +134,6 @@ def data(readonly=False):
SettingValue(typ.IgnoreCase(), 'smart'),
"Whether to find text on a page case-insensitively."),
('wrap-search',
SettingValue(typ.Bool(), 'true'),
"Whether to wrap finding text to the top when arriving at the "
"end."),
('startpage',
SettingValue(typ.List(), 'https://duckduckgo.com'),
"The default page(s) to open at the start, separated by commas."),
@@ -272,6 +267,10 @@ def data(readonly=False):
SettingValue(typ.VerticalPosition(), 'top'),
"Where to show the downloaded files."),
('status-position',
SettingValue(typ.VerticalPosition(), 'bottom'),
"The position of the status bar."),
('message-timeout',
SettingValue(typ.Int(), '2000'),
"Time (in ms) to show messages in the statusbar for."),
@@ -284,10 +283,6 @@ def data(readonly=False):
SettingValue(typ.ConfirmQuit(), 'never'),
"Whether to confirm quitting the application."),
('display-statusbar-messages',
SettingValue(typ.Bool(), 'false'),
"Whether to display javascript statusbar messages."),
('zoom-text-only',
SettingValue(typ.Bool(), 'false'),
"Whether the zoom factor on a frame applies only to the text or "
@@ -330,7 +325,7 @@ def data(readonly=False):
('window-title-format',
SettingValue(typ.FormatString(fields=['perc', 'perc_raw', 'title',
'title_sep', 'id',
'scroll_pos']),
'scroll_pos', 'host']),
'{perc}{title}{title_sep}qutebrowser'),
"The format to use for the window title. The following "
"placeholders are defined:\n\n"
@@ -340,7 +335,8 @@ def data(readonly=False):
"* `{title_sep}`: The string ` - ` if a title is set, empty "
"otherwise.\n"
"* `{id}`: The internal window ID of this window.\n"
"* `{scroll_pos}`: The page scroll position."),
"* `{scroll_pos}`: The page scroll position.\n"
"* `{host}`: The host of the current web page."),
('hide-mouse-cursor',
SettingValue(typ.Bool(), 'false'),
@@ -627,7 +623,7 @@ def data(readonly=False):
('title-format',
SettingValue(typ.FormatString(
fields=['perc', 'perc_raw', 'title', 'title_sep', 'index',
'id', 'scroll_pos']), '{index}: {title}'),
'id', 'scroll_pos', 'host']), '{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"
@@ -637,7 +633,8 @@ def data(readonly=False):
"otherwise.\n"
"* `{index}`: The index of this tab.\n"
"* `{id}`: The internal tab ID of this tab.\n"
"* `{scroll_pos}`: The page scroll position."),
"* `{scroll_pos}`: The page scroll position.\n"
"* `{host}`: The host of the current web page."),
('title-alignment',
SettingValue(typ.TextAlignment(), 'left'),
@@ -886,7 +883,7 @@ def data(readonly=False):
('chars',
SettingValue(typ.UniqueCharString(minlen=2, completions=[
('asdfghjkl', "Home row"),
('dhtnaoeu', "Home row (Dvorak)"),
('aoeuidnths', "Home row (Dvorak)"),
('abcdefghijklmnopqrstuvwxyz', "All letters"),
]), 'asdfghjkl'),
"Chars used for hint strings."),
@@ -1461,15 +1458,14 @@ KEY_DATA = collections.OrderedDict([
('hint all hover', [';h']),
('hint images', [';i']),
('hint images tab', [';I']),
('hint images tab-bg', ['.i']),
('hint links fill ":open {hint-url}"', [';o']),
('hint links fill ":open -t {hint-url}"', [';O']),
('hint links fill ":open -b {hint-url}"', ['.o']),
('hint links fill :open {hint-url}', [';o']),
('hint links fill :open -t {hint-url}', [';O']),
('hint links yank', [';y']),
('hint links yank-primary', [';Y']),
('hint --rapid links tab-bg', [';r']),
('hint --rapid links window', [';R']),
('hint links download', [';d']),
('hint inputs', [';t']),
('scroll left', ['h']),
('scroll down', ['j']),
('scroll up', ['k']),
@@ -1548,6 +1544,7 @@ KEY_DATA = collections.OrderedDict([
('open qute:settings', ['Ss']),
('follow-selected', RETURN_KEYS),
('follow-selected -t', ['<Ctrl-Return>', '<Ctrl-Enter>']),
('repeat-command', ['.']),
])),
('insert', collections.OrderedDict([
@@ -1577,6 +1574,7 @@ KEY_DATA = collections.OrderedDict([
('prompt-accept', RETURN_KEYS),
('prompt-yes', ['y']),
('prompt-no', ['n']),
('prompt-open-download', ['<Ctrl-X>']),
])),
('command,prompt', collections.OrderedDict([
@@ -1651,4 +1649,6 @@ CHANGED_KEY_COMMANDS = [
(re.compile(r'^leave-mode$'), r'clear-keychain ;; leave-mode'),
(re.compile(r'^download-remove --all$'), r'download-clear'),
(re.compile(r'^hint links fill "([^"]*)"$'), r'hint links fill \1'),
]

View File

@@ -481,7 +481,7 @@ class IntList(List):
class Float(BaseType):
"""Base class for an float setting.
"""Base class for a float setting.
Attributes:
minval: Minimum value (inclusive).
@@ -1342,7 +1342,7 @@ class AutoSearch(BaseType):
self._basic_validation(value)
if not value:
return
elif value.lower() in ('naive', 'dns'):
elif value.lower() in ['naive', 'dns']:
pass
else:
self.booltype.validate(value)
@@ -1350,7 +1350,7 @@ class AutoSearch(BaseType):
def transform(self, value):
if not value:
return None
elif value.lower() in ('naive', 'dns'):
elif value.lower() in ['naive', 'dns']:
return value.lower()
elif self.booltype.transform(value):
# boolean true is an alias for naive matching
@@ -1644,7 +1644,7 @@ class UserAgent(BaseType):
class TimestampTemplate(BaseType):
"""A strftime-like template for timestamps.
"""An strftime-like template for timestamps.
See
https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior

View File

@@ -419,3 +419,14 @@ class KeyConfigParser(QObject):
bindings = {k: v for k, v in bindings.items()
if v != self.UNBOUND_COMMAND}
return bindings
def get_reverse_bindings_for(self, section):
"""Get a dict of commands to a list of bindings for the section."""
cmd_to_keys = collections.defaultdict(list)
for key, cmd in self.get_bindings_for(section).items():
# put special bindings last
if utils.is_special_key(key):
cmd_to_keys[cmd].append(key)
else:
cmd_to_keys[cmd].insert(0, key)
return cmd_to_keys

View File

@@ -54,7 +54,7 @@ class Section:
def __iter__(self):
"""Iterate over all set values."""
return self.values.__iter__()
return iter(self.values)
def __bool__(self):
"""Get boolean state of section."""

View File

@@ -433,10 +433,10 @@ def update_settings(section, option):
QWebSettings.setIconDatabasePath('')
else:
QWebSettings.setIconDatabasePath(cache_path)
else:
try:
mapping = MAPPINGS[section][option]
except KeyError:
return
value = config.get(section, option)
mapping.set(value)
try:
mapping = MAPPINGS[section][option]
except KeyError:
return
value = config.get(section, option)
mapping.set(value)

View File

@@ -0,0 +1,34 @@
{% extends "base.html" %}
{% block style %}
table { border: 1px solid grey; border-collapse: collapse; width: 100%;}
th, td { border: 1px solid grey; padding: 0px 5px; }
th { background: lightgrey; }
{% endblock %}
{% block content %}
<table>
<tr>
<th><h3>Bookmark</h3></th>
<th><h3>URL</h3></th>
</tr>
{% for url, title in bookmarks %}
<tr>
<td><a href="{{url}}">{{title}}</a></td>
<td>{{url}}</td>
</tr>
{% endfor %}
<tr>
<th><h3>Quickmark</h3></th>
<th><h3>URL</h3></th>
</tr>
{% for name, url in quickmarks %}
<tr>
<td><a href="{{url}}">{{name}}</a></td>
<td>{{url}}</td>
</tr>
{% endfor %}
</table>
{% endblock %}

View File

@@ -0,0 +1,67 @@
/**
* 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/>.
*/
function scroll_to_perc(x, y) {
var elem = document.documentElement;
var x_px = window.scrollX;
var y_px = window.scrollY;
if (x !== undefined) {
x_px = (elem.scrollWidth - elem.clientWidth) / 100 * x;
}
if (y !== undefined) {
y_px = (elem.scrollHeight - elem.clientHeight) / 100 * y;
}
window.scroll(x_px, y_px);
}
function scroll_delta_page(x, y) {
var dx = document.documentElement.clientWidth * x;
var dy = document.documentElement.clientHeight * y;
window.scrollBy(dx, dy);
}
function scroll_pos() {
var elem = document.documentElement;
var dx = (elem.scrollWidth - elem.clientWidth);
var dy = (elem.scrollHeight - elem.clientHeight);
var perc_x, perc_y;
if (dx === 0) {
perc_x = 0;
} else {
perc_x = 100 / dx * window.scrollX;
}
if (dy === 0) {
perc_y = 0;
} else {
perc_y = 100 / dy * window.scrollY;
}
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};
// console.log(JSON.stringify(pos));
return pos;
}

View File

@@ -170,7 +170,7 @@ class ModeManager(QObject):
handled = parser.handle(event)
is_non_alnum = (
event.modifiers() not in (Qt.NoModifier, Qt.ShiftModifier) or
event.modifiers() not in [Qt.NoModifier, Qt.ShiftModifier] or
not event.text().strip())
if handled:

View File

@@ -34,7 +34,8 @@ from qutebrowser.mainwindow import tabbedbrowser
from qutebrowser.mainwindow.statusbar import bar
from qutebrowser.completion import completionwidget
from qutebrowser.keyinput import modeman
from qutebrowser.browser import hints, downloads, downloadview, commands
from qutebrowser.browser import commands, downloadview, hints
from qutebrowser.browser.webkit import downloads
from qutebrowser.misc import crashsignal, keyhintwidget
@@ -76,7 +77,7 @@ def get_window(via_ipc, force_window=False, force_tab=False,
win_id = window.win_id
window_to_raise = window
win_id = window.win_id
if open_target not in ('tab-silent', 'tab-bg-silent'):
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(
@@ -195,28 +196,41 @@ class MainWindow(QWidget):
@pyqtSlot(str, str)
def on_config_changed(self, section, option):
"""Resize the completion if related config options changed."""
if section == 'completion' and option in ('height', 'shrink'):
if section == 'completion' and option in ['height', 'shrink']:
self.resize_completion()
elif section == 'ui' and option == 'statusbar-padding':
self.resize_completion()
elif section == 'ui' and option == 'downloads-position':
self._add_widgets()
elif section == 'ui' and option == 'status-position':
self._add_widgets()
self.resize_completion()
def _add_widgets(self):
"""Add or readd all widgets to the VBox."""
self._vbox.removeWidget(self.tabbed_browser)
self._vbox.removeWidget(self._downloadview)
self._vbox.removeWidget(self.status)
position = config.get('ui', 'downloads-position')
if position == 'top':
self._vbox.addWidget(self._downloadview)
self._vbox.addWidget(self.tabbed_browser)
elif position == 'bottom':
self._vbox.addWidget(self.tabbed_browser)
self._vbox.addWidget(self._downloadview)
downloads_position = config.get('ui', 'downloads-position')
status_position = config.get('ui', 'status-position')
widgets = [self.tabbed_browser]
if downloads_position == 'top':
widgets.insert(0, self._downloadview)
elif downloads_position == 'bottom':
widgets.append(self._downloadview)
else:
raise ValueError("Invalid position {}!".format(position))
self._vbox.addWidget(self.status)
raise ValueError("Invalid position {}!".format(downloads_position))
if status_position == 'top':
widgets.insert(0, self.status)
elif status_position == 'bottom':
widgets.append(self.status)
else:
raise ValueError("Invalid position {}!".format(status_position))
for widget in widgets:
self._vbox.addWidget(widget)
def _load_state_geometry(self):
"""Load the geometry from the state file."""
@@ -331,12 +345,8 @@ class MainWindow(QWidget):
tabs.tab_index_changed.connect(status.tabindex.on_tab_index_changed)
tabs.current_tab_changed.connect(status.txt.on_tab_changed)
tabs.cur_statusbar_message.connect(status.txt.on_statusbar_message)
tabs.cur_load_started.connect(status.txt.on_load_started)
tabs.current_tab_changed.connect(status.url.on_tab_changed)
tabs.cur_url_text_changed.connect(status.url.set_url)
tabs.cur_url_changed.connect(status.url.set_url)
tabs.cur_link_hovered.connect(status.url.set_hover_url)
tabs.cur_load_status_changed.connect(status.url.on_load_status_changed)
@@ -369,12 +379,19 @@ class MainWindow(QWidget):
height = contents_height
else:
contents_height = -1
# hpoint now would be the bottom-left edge of the widget if it was on
# the top of the main window.
topleft_y = self.height() - self.status.height() - height
topleft_y = qtutils.check_overflow(topleft_y, 'int', fatal=False)
topleft = QPoint(0, topleft_y)
bottomright = self.status.geometry().topRight()
status_position = config.get('ui', 'status-position')
if status_position == 'bottom':
top = self.height() - self.status.height() - height
top = qtutils.check_overflow(top, 'int', fatal=False)
topleft = QPoint(0, top)
bottomright = self.status.geometry().topRight()
elif status_position == 'top':
topleft = self.status.geometry().bottomLeft()
bottom = self.status.height() + height
bottom = qtutils.check_overflow(bottom, 'int', fatal=False)
bottomright = QPoint(self.width(), bottom)
else:
raise ValueError("Invalid position {}!".format(status_position))
rect = QRect(topleft, bottomright)
log.misc.debug('completion rect: {}'.format(rect))
if rect.isValid():

View File

@@ -329,12 +329,12 @@ class StatusBar(QWidget):
log.statusbar.debug("Setting command_active to {}".format(val))
self._command_active = val
elif mode == usertypes.KeyMode.caret:
webview = objreg.get('tabbed-browser', scope='window',
window=self._win_id).currentWidget()
tab = objreg.get('tabbed-browser', scope='window',
window=self._win_id).currentWidget()
log.statusbar.debug("Setting caret_mode - val {}, selection "
"{}".format(val, webview.selection_enabled))
"{}".format(val, tab.caret.selection_enabled))
if val:
if webview.selection_enabled:
if tab.caret.selection_enabled:
self._set_mode_text("{} selection".format(mode.name))
self._caret_mode = CaretMode.selection
else:
@@ -519,9 +519,9 @@ class StatusBar(QWidget):
window=self._win_id)
if keyparsers[mode].passthrough:
self._set_mode_text(mode.name)
if mode in (usertypes.KeyMode.insert,
if mode in [usertypes.KeyMode.insert,
usertypes.KeyMode.command,
usertypes.KeyMode.caret):
usertypes.KeyMode.caret]:
self.set_mode_active(mode, True)
@pyqtSlot(usertypes.KeyMode, usertypes.KeyMode)
@@ -534,9 +534,9 @@ class StatusBar(QWidget):
self._set_mode_text(new_mode.name)
else:
self.txt.set_text(self.txt.Text.normal, '')
if old_mode in (usertypes.KeyMode.insert,
if old_mode in [usertypes.KeyMode.insert,
usertypes.KeyMode.command,
usertypes.KeyMode.caret):
usertypes.KeyMode.caret]:
self.set_mode_active(old_mode, False)
@config.change_filter('ui', 'message-timeout')

View File

@@ -21,8 +21,8 @@
from PyQt5.QtCore import pyqtSlot
from qutebrowser.browser import browsertab
from qutebrowser.mainwindow.statusbar import textbase
from qutebrowser.browser import webview
class Percentage(textbase.TextBase):
@@ -46,10 +46,12 @@ class Percentage(textbase.TextBase):
self.setText('[top]')
elif y == 100:
self.setText('[bot]')
elif y is None:
self.setText('[???]')
else:
self.setText('[{:2}%]'.format(y))
@pyqtSlot(webview.WebView)
@pyqtSlot(browsertab.AbstractTab)
def on_tab_changed(self, tab):
"""Update scroll position when tab changed."""
self.set_perc(*tab.scroll_pos)
self.set_perc(*tab.scroller.pos_perc())

View File

@@ -22,9 +22,9 @@
from PyQt5.QtCore import pyqtSlot, QSize
from PyQt5.QtWidgets import QProgressBar, QSizePolicy
from qutebrowser.browser import webview
from qutebrowser.browser import browsertab
from qutebrowser.config import style
from qutebrowser.utils import utils
from qutebrowser.utils import utils, usertypes
class Progress(QProgressBar):
@@ -59,15 +59,15 @@ class Progress(QProgressBar):
self.setValue(0)
self.show()
@pyqtSlot(webview.WebView)
@pyqtSlot(browsertab.AbstractTab)
def on_tab_changed(self, tab):
"""Set the correct value when the current tab changed."""
if self is None: # pragma: no branch
# This should never happen, but for some weird reason it does
# sometimes.
return # pragma: no cover
self.setValue(tab.progress)
if tab.load_status == webview.LoadStatus.loading:
self.setValue(tab.progress())
if tab.load_status() == usertypes.LoadStatus.loading:
self.show()
else:
self.hide()

View File

@@ -80,6 +80,7 @@ class Prompter(QObject):
usertypes.PromptMode.text: usertypes.KeyMode.prompt,
usertypes.PromptMode.user_pwd: usertypes.KeyMode.prompt,
usertypes.PromptMode.alert: usertypes.KeyMode.prompt,
usertypes.PromptMode.download: usertypes.KeyMode.prompt,
}
show_prompt = pyqtSignal()
@@ -152,33 +153,49 @@ class Prompter(QObject):
modeman.enter(self._win_id, mode, 'question asked')
return True
def _display_question_yesno(self, prompt):
"""Display a yes/no question."""
if self._question.default is None:
suffix = ""
elif self._question.default:
suffix = " (yes)"
else:
suffix = " (no)"
prompt.txt.setText(self._question.text + suffix)
prompt.lineedit.hide()
def _display_question_input(self, prompt):
"""Display a question with an input."""
text = self._question.text
if self._question.mode == usertypes.PromptMode.download:
key_mode = self.KEY_MODES[self._question.mode]
key_config = objreg.get('key-config')
all_bindings = key_config.get_reverse_bindings_for(key_mode.name)
bindings = all_bindings.get('prompt-open-download', [])
if bindings:
text += ' ({} to open)'.format(bindings[0])
prompt.txt.setText(text)
if self._question.default:
prompt.lineedit.setText(self._question.default)
prompt.lineedit.show()
def _display_question_alert(self, prompt):
"""Display a JS alert 'question'."""
prompt.txt.setText(self._question.text + ' (ok)')
prompt.lineedit.hide()
def _display_question(self):
"""Display the question saved in self._question."""
prompt = objreg.get('prompt', scope='window', window=self._win_id)
if self._question.mode == usertypes.PromptMode.yesno:
if self._question.default is None:
suffix = ""
elif self._question.default:
suffix = " (yes)"
else:
suffix = " (no)"
prompt.txt.setText(self._question.text + suffix)
prompt.lineedit.hide()
elif self._question.mode == usertypes.PromptMode.text:
prompt.txt.setText(self._question.text)
if self._question.default:
prompt.lineedit.setText(self._question.default)
prompt.lineedit.show()
elif self._question.mode == usertypes.PromptMode.user_pwd:
prompt.txt.setText(self._question.text)
if self._question.default:
prompt.lineedit.setText(self._question.default)
prompt.lineedit.show()
elif self._question.mode == usertypes.PromptMode.alert:
prompt.txt.setText(self._question.text + ' (ok)')
prompt.lineedit.hide()
else:
raise ValueError("Invalid prompt mode!")
handlers = {
usertypes.PromptMode.yesno: self._display_question_yesno,
usertypes.PromptMode.text: self._display_question_input,
usertypes.PromptMode.user_pwd: self._display_question_input,
usertypes.PromptMode.download: self._display_question_input,
usertypes.PromptMode.alert: self._display_question_alert,
}
handler = handlers[self._question.mode]
handler(prompt)
log.modes.debug("Question asked, focusing {!r}".format(
prompt.lineedit))
prompt.lineedit.setFocus()
@@ -207,7 +224,7 @@ class Prompter(QObject):
def on_mode_left(self, mode):
"""Clear and reset input when the mode was left."""
prompt = objreg.get('prompt', scope='window', window=self._win_id)
if mode in (usertypes.KeyMode.prompt, usertypes.KeyMode.yesno):
if mode in [usertypes.KeyMode.prompt, usertypes.KeyMode.yesno]:
prompt.txt.setText('')
prompt.lineedit.clear()
prompt.lineedit.setEchoMode(QLineEdit.Normal)
@@ -248,6 +265,13 @@ class Prompter(QObject):
modeman.maybe_leave(self._win_id, usertypes.KeyMode.prompt,
'prompt accept')
self._question.done()
elif self._question.mode == usertypes.PromptMode.download:
# User just entered a path for a download.
target = usertypes.FileDownloadTarget(prompt.lineedit.text())
self._question.answer = target
modeman.maybe_leave(self._win_id, usertypes.KeyMode.prompt,
'prompt accept')
self._question.done()
elif self._question.mode == usertypes.PromptMode.yesno:
# User wants to accept the default of a yes/no question.
self._question.answer = self._question.default
@@ -287,6 +311,18 @@ class Prompter(QObject):
'prompt accept')
self._question.done()
@cmdutils.register(instance='prompter', hide=True, scope='window',
modes=[usertypes.KeyMode.prompt])
def prompt_open_download(self):
"""Immediately open a download."""
if self._question.mode != usertypes.PromptMode.download:
# We just ignore this if we don't have a download question.
return
self._question.answer = usertypes.OpenFileDownloadTarget()
modeman.maybe_leave(self._win_id, usertypes.KeyMode.prompt,
'download open')
self._question.done()
@pyqtSlot(usertypes.Question, bool)
def ask_question(self, question, blocking):
"""Display a question in the statusbar.

View File

@@ -21,10 +21,8 @@
from PyQt5.QtCore import pyqtSlot
from qutebrowser.config import config
from qutebrowser.mainwindow.statusbar import textbase
from qutebrowser.utils import usertypes, log, objreg
from qutebrowser.browser import webview
from qutebrowser.utils import usertypes, log
class Text(textbase.TextBase):
@@ -34,20 +32,17 @@ class Text(textbase.TextBase):
Attributes:
_normaltext: The "permanent" text. Never automatically cleared.
_temptext: The temporary text to display.
_jstext: The text javascript wants to display.
The temptext is shown from StatusBar when a temporary text or error is
available. If not, the permanent text is shown.
"""
Text = usertypes.enum('Text', ['normal', 'temp', 'js'])
Text = usertypes.enum('Text', ['normal', 'temp'])
def __init__(self, parent=None):
super().__init__(parent)
self._normaltext = ''
self._temptext = ''
self._jstext = ''
objreg.get('config').changed.connect(self.update_text)
def set_text(self, which, text):
"""Set a text.
@@ -62,8 +57,6 @@ class Text(textbase.TextBase):
self._normaltext = text
elif which is self.Text.temp:
self._temptext = text
elif which is self.Text.js:
self._jstext = text
else:
raise ValueError("Invalid value {} for which!".format(which))
self.update_text()
@@ -77,29 +70,11 @@ class Text(textbase.TextBase):
else:
log.statusbar.debug("Ignoring reset: '{}'".format(text))
@config.change_filter('ui', 'display-statusbar-messages')
def update_text(self):
"""Update QLabel text when needed."""
if self._temptext:
self.setText(self._temptext)
elif self._jstext and config.get('ui', 'display-statusbar-messages'):
self.setText(self._jstext)
elif self._normaltext:
self.setText(self._normaltext)
else:
self.setText('')
@pyqtSlot(str)
def on_statusbar_message(self, val):
"""Called when javascript tries to set a statusbar message."""
self._jstext = val
@pyqtSlot()
def on_load_started(self):
"""Clear jstext when page loading started."""
self._jstext = ''
@pyqtSlot(webview.WebView)
def on_tab_changed(self, tab):
"""Set the correct jstext when the current tab changed."""
self._jstext = tab.statusbar_message

View File

@@ -21,7 +21,7 @@
from PyQt5.QtCore import pyqtSlot, pyqtProperty, Qt, QUrl
from qutebrowser.browser import webview
from qutebrowser.browser import browsertab
from qutebrowser.mainwindow.statusbar import textbase
from qutebrowser.config import style
from qutebrowser.utils import usertypes
@@ -119,29 +119,32 @@ class UrlText(textbase.TextBase):
Args:
status_str: The LoadStatus as string.
"""
status = webview.LoadStatus[status_str]
if status in (webview.LoadStatus.success,
webview.LoadStatus.success_https,
webview.LoadStatus.error,
webview.LoadStatus.warn):
status = usertypes.LoadStatus[status_str]
if status in [usertypes.LoadStatus.success,
usertypes.LoadStatus.success_https,
usertypes.LoadStatus.error,
usertypes.LoadStatus.warn]:
self._normal_url_type = UrlType[status_str]
else:
self._normal_url_type = UrlType.normal
self._update_url()
@pyqtSlot(str)
def set_url(self, s):
@pyqtSlot(QUrl)
def set_url(self, url):
"""Setter to be used as a Qt slot.
Args:
s: The URL to set as string.
url: The URL to set as QUrl, or None.
"""
self._normal_url = s
if url is None:
self._normal_url = None
else:
self._normal_url = url.toDisplayString()
self._normal_url_type = UrlType.normal
self._update_url()
@pyqtSlot(str, str, str)
def set_hover_url(self, link, _title, _text):
@pyqtSlot(str)
def set_hover_url(self, link):
"""Setter to be used as a Qt slot.
Saves old shown URL in self._old_url and restores it later if a link is
@@ -149,8 +152,6 @@ class UrlText(textbase.TextBase):
Args:
link: The link which was hovered (string)
_title: The title of the hovered link (string)
_text: The text of the hovered link (string)
"""
if link:
qurl = QUrl(link)
@@ -162,10 +163,10 @@ class UrlText(textbase.TextBase):
self._hover_url = None
self._update_url()
@pyqtSlot(webview.WebView)
@pyqtSlot(browsertab.AbstractTab)
def on_tab_changed(self, tab):
"""Update URL if the tab changed."""
self._hover_url = None
self._normal_url = tab.cur_url.toDisplayString()
self.on_load_status_changed(tab.load_status.name)
self._normal_url = tab.url().toDisplayString()
self.on_load_status_changed(tab.load_status().name)
self._update_url()

View File

@@ -29,7 +29,7 @@ from PyQt5.QtGui import QIcon
from qutebrowser.config import config
from qutebrowser.keyinput import modeman
from qutebrowser.mainwindow import tabwidget
from qutebrowser.browser import signalfilter, webview
from qutebrowser.browser import signalfilter, browsertab
from qutebrowser.utils import (log, usertypes, utils, qtutils, objreg,
urlutils, message)
@@ -55,8 +55,8 @@ class TabbedBrowser(tabwidget.TabWidget):
emitted if the signal occurred in the current tab.
Attributes:
search_text/search_flags: Search parameters which are shared between
all tabs.
search_text/search_options: Search parameters which are shared between
all tabs.
_win_id: The window ID this tabbedbrowser is associated with.
_filter: A SignalFilter instance.
_now_focused: The tab which is focused now.
@@ -70,13 +70,11 @@ class TabbedBrowser(tabwidget.TabWidget):
default_window_icon: The qutebrowser window icon
Signals:
cur_progress: Progress of the current tab changed (loadProgress).
cur_load_started: Current tab started loading (loadStarted)
cur_load_finished: Current tab finished loading (loadFinished)
cur_statusbar_message: Current tab got a statusbar message
(statusBarMessage)
cur_url_text_changed: Current URL text changed.
cur_link_hovered: Link hovered in current tab (linkHovered)
cur_progress: Progress of the current tab changed (load_progress).
cur_load_started: Current tab started loading (load_started)
cur_load_finished: Current tab finished loading (load_finished)
cur_url_changed: Current URL changed.
cur_link_hovered: Link hovered in current tab (link_hovered)
cur_scroll_perc_changed: Scroll percentage of current tab changed.
arg 1: x-position in %.
arg 2: y-position in %.
@@ -85,22 +83,21 @@ class TabbedBrowser(tabwidget.TabWidget):
resized: Emitted when the browser window has resized, so the completion
widget can adjust its size to it.
arg: The new size.
current_tab_changed: The current tab changed to the emitted WebView.
current_tab_changed: The current tab changed to the emitted tab.
new_tab: Emits the new WebView and its index when a new tab is opened.
"""
cur_progress = pyqtSignal(int)
cur_load_started = pyqtSignal()
cur_load_finished = pyqtSignal(bool)
cur_statusbar_message = pyqtSignal(str)
cur_url_text_changed = pyqtSignal(str)
cur_link_hovered = pyqtSignal(str, str, str)
cur_url_changed = pyqtSignal(QUrl)
cur_link_hovered = pyqtSignal(str)
cur_scroll_perc_changed = pyqtSignal(int, int)
cur_load_status_changed = pyqtSignal(str)
close_window = pyqtSignal()
resized = pyqtSignal('QRect')
current_tab_changed = pyqtSignal(webview.WebView)
new_tab = pyqtSignal(webview.WebView, int)
current_tab_changed = pyqtSignal(browsertab.AbstractTab)
new_tab = pyqtSignal(browsertab.AbstractTab, int)
def __init__(self, win_id, parent=None):
super().__init__(win_id, parent)
@@ -116,7 +113,7 @@ class TabbedBrowser(tabwidget.TabWidget):
self._filter = signalfilter.SignalFilter(win_id, self)
self._now_focused = None
self.search_text = None
self.search_flags = 0
self.search_options = {}
self._local_marks = {}
self._global_marks = {}
self.default_window_icon = self.window().windowIcon()
@@ -161,67 +158,46 @@ class TabbedBrowser(tabwidget.TabWidget):
# (e.g. last tab removed)
log.webview.debug("Not updating window title because index is -1")
return
tabtitle = self.page_title(idx)
widget = self.widget(idx)
fields = {}
if widget.load_status == webview.LoadStatus.loading:
fields['perc'] = '[{}%] '.format(widget.progress)
else:
fields['perc'] = ''
fields['perc_raw'] = widget.progress
fields['title'] = tabtitle
fields['title_sep'] = ' - ' if tabtitle else ''
fields = self.get_tab_fields(idx)
fields['id'] = self._win_id
y = widget.scroll_pos[1]
if y <= 0:
scroll_pos = 'top'
elif y >= 100:
scroll_pos = 'bot'
else:
scroll_pos = '{:2}%'.format(y)
fields['scroll_pos'] = scroll_pos
fmt = config.get('ui', 'window-title-format')
self.window().setWindowTitle(fmt.format(**fields))
def _connect_tab_signals(self, tab):
"""Set up the needed signals for tab."""
page = tab.page()
frame = page.mainFrame()
# filtered signals
tab.linkHovered.connect(
tab.link_hovered.connect(
self._filter.create(self.cur_link_hovered, tab))
tab.loadProgress.connect(
tab.load_progress.connect(
self._filter.create(self.cur_progress, tab))
frame.loadFinished.connect(
tab.load_finished.connect(
self._filter.create(self.cur_load_finished, tab))
frame.loadStarted.connect(
tab.load_started.connect(
self._filter.create(self.cur_load_started, tab))
tab.statusBarMessage.connect(
self._filter.create(self.cur_statusbar_message, tab))
tab.scroll_pos_changed.connect(
tab.scroller.perc_changed.connect(
self._filter.create(self.cur_scroll_perc_changed, tab))
tab.scroll_pos_changed.connect(self.on_scroll_pos_changed)
tab.url_text_changed.connect(
self._filter.create(self.cur_url_text_changed, tab))
tab.scroller.perc_changed.connect(self.on_scroll_pos_changed)
tab.url_changed.connect(
self._filter.create(self.cur_url_changed, tab))
tab.load_status_changed.connect(
self._filter.create(self.cur_load_status_changed, tab))
tab.url_text_changed.connect(
functools.partial(self.on_url_text_changed, tab))
tab.url_changed.connect(
functools.partial(self.on_url_changed, tab))
# misc
tab.titleChanged.connect(
tab.title_changed.connect(
functools.partial(self.on_title_changed, tab))
tab.iconChanged.connect(
tab.icon_changed.connect(
functools.partial(self.on_icon_changed, tab))
tab.loadProgress.connect(
tab.load_progress.connect(
functools.partial(self.on_load_progress, tab))
frame.loadFinished.connect(
tab.load_finished.connect(
functools.partial(self.on_load_finished, tab))
frame.loadStarted.connect(
tab.load_started.connect(
functools.partial(self.on_load_started, tab))
page.windowCloseRequested.connect(
tab.window_close_requested.connect(
functools.partial(self.on_window_close_requested, tab))
tab.new_tab_requested.connect(self.tabopen)
def current_url(self):
"""Get the URL of the current tab.
@@ -231,14 +207,8 @@ class TabbedBrowser(tabwidget.TabWidget):
Return:
The current URL as QUrl.
"""
widget = self.currentWidget()
if widget is None:
url = QUrl()
else:
url = widget.cur_url
# It's possible for url to be invalid, but the caller will handle that.
qtutils.ensure_valid(url)
return url
idx = self.currentIndex()
return super().tab_url(idx)
def shutdown(self):
"""Try to shut down all tabs cleanly."""
@@ -288,12 +258,12 @@ class TabbedBrowser(tabwidget.TabWidget):
window=self._win_id):
objreg.delete('last-focused-tab', scope='window',
window=self._win_id)
if tab.cur_url.isValid():
history_data = qtutils.serialize(tab.history())
entry = UndoEntry(tab.cur_url, history_data)
if tab.url().isValid():
history_data = tab.history.serialize()
entry = UndoEntry(tab.url(), history_data)
self._undo_stack.append(entry)
elif tab.cur_url.isEmpty():
# There are some good reasons why an URL could be empty
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
# this.
# [1] https://github.com/The-Compiler/qutebrowser/issues/163
@@ -302,7 +272,7 @@ class TabbedBrowser(tabwidget.TabWidget):
# We display a warnings for URLs which are not empty but invalid -
# but we don't return here because we want the tab to close either
# way.
urlutils.invalid_url_error(self._win_id, tab.cur_url, "saving tab")
urlutils.invalid_url_error(self._win_id, tab.url(), "saving tab")
tab.shutdown()
self.removeTab(idx)
tab.deleteLater()
@@ -314,13 +284,13 @@ class TabbedBrowser(tabwidget.TabWidget):
use_current_tab = False
if last_close in ['blank', 'startpage', 'default-page']:
only_one_tab_open = self.count() == 1
no_history = self.widget(0).history().count() == 1
no_history = len(self.widget(0).history) == 1
urls = {
'blank': QUrl('about:blank'),
'startpage': QUrl(config.get('general', 'startpage')[0]),
'default-page': config.get('general', 'default-page'),
}
first_tab_url = self.widget(0).page().mainFrame().requestedUrl()
first_tab_url = self.widget(0).url()
last_close_urlstr = urls[last_close].toString().rstrip('/')
first_tab_urlstr = first_tab_url.toString().rstrip('/')
last_close_url_used = first_tab_urlstr == last_close_urlstr
@@ -335,7 +305,7 @@ class TabbedBrowser(tabwidget.TabWidget):
else:
newtab = self.tabopen(url, background=False)
qtutils.deserialize(history_data, newtab.history())
newtab.history.deserialize(history_data)
@pyqtSlot('QUrl', bool)
def openurl(self, url, newtab):
@@ -361,7 +331,7 @@ class TabbedBrowser(tabwidget.TabWidget):
return
self.close_tab(tab)
@pyqtSlot(webview.WebView)
@pyqtSlot(browsertab.AbstractTab)
def on_window_close_requested(self, widget):
"""Close a tab with a widget given."""
try:
@@ -370,6 +340,7 @@ class TabbedBrowser(tabwidget.TabWidget):
log.webview.debug("Requested to close {!r} which does not "
"exist!".format(widget))
@pyqtSlot('QUrl')
@pyqtSlot('QUrl', bool)
def tabopen(self, url=None, background=None, explicit=False):
"""Open a new tab with a given URL.
@@ -401,10 +372,13 @@ class TabbedBrowser(tabwidget.TabWidget):
tabbed_browser = objreg.get('tabbed-browser', scope='window',
window=window.win_id)
return tabbed_browser.tabopen(url, background, explicit)
tab = webview.WebView(self._win_id, self)
tab = browsertab.create(win_id=self._win_id, parent=self)
self._connect_tab_signals(tab)
idx = self._get_new_tab_idx(explicit)
self.insertTab(idx, tab, "")
if url is not None:
tab.openurl(url)
if background is None:
@@ -480,8 +454,8 @@ class TabbedBrowser(tabwidget.TabWidget):
# We can get signals for tabs we already deleted...
return
self.update_tab_title(idx)
if tab.keep_icon:
tab.keep_icon = False
if tab.data.keep_icon:
tab.data.keep_icon = False
else:
self.setTabIcon(idx, QIcon())
if (config.get('tabs', 'tabs-are-windows') and
@@ -498,11 +472,11 @@ class TabbedBrowser(tabwidget.TabWidget):
modeman.maybe_leave(self._win_id, usertypes.KeyMode.hint,
'load started')
@pyqtSlot(webview.WebView, str)
@pyqtSlot(browsertab.AbstractTab, str)
def on_title_changed(self, tab, text):
"""Set the title of a tab.
Slot for the titleChanged signal of any tab.
Slot for the title_changed signal of any tab.
Args:
tab: The WebView where the title was changed.
@@ -522,8 +496,8 @@ class TabbedBrowser(tabwidget.TabWidget):
if idx == self.currentIndex():
self.update_window_title()
@pyqtSlot(webview.WebView, str)
def on_url_text_changed(self, tab, url):
@pyqtSlot(browsertab.AbstractTab, QUrl)
def on_url_changed(self, tab, url):
"""Set the new URL as title if there's no title yet.
Args:
@@ -536,16 +510,17 @@ class TabbedBrowser(tabwidget.TabWidget):
# We can get signals for tabs we already deleted...
return
if not self.page_title(idx):
self.set_page_title(idx, url)
self.set_page_title(idx, url.toDisplayString())
@pyqtSlot(webview.WebView)
def on_icon_changed(self, tab):
@pyqtSlot(browsertab.AbstractTab, QIcon)
def on_icon_changed(self, tab, icon):
"""Set the icon of a tab.
Slot for the iconChanged signal of any tab.
Args:
tab: The WebView where the title was changed.
icon: The new icon
"""
if not config.get('tabs', 'show-favicons'):
return
@@ -554,15 +529,15 @@ class TabbedBrowser(tabwidget.TabWidget):
except TabDeletedError:
# We can get signals for tabs we already deleted...
return
self.setTabIcon(idx, tab.icon())
self.setTabIcon(idx, icon)
if config.get('tabs', 'tabs-are-windows'):
self.window().setWindowIcon(tab.icon())
self.window().setWindowIcon(icon)
@pyqtSlot(usertypes.KeyMode)
def on_mode_left(self, mode):
"""Give focus to current tab if command mode was left."""
if mode in (usertypes.KeyMode.command, usertypes.KeyMode.prompt,
usertypes.KeyMode.yesno):
if mode in [usertypes.KeyMode.command, usertypes.KeyMode.prompt,
usertypes.KeyMode.yesno]:
widget = self.currentWidget()
log.modes.debug("Left status-input mode, focusing {!r}".format(
widget))
@@ -579,8 +554,8 @@ class TabbedBrowser(tabwidget.TabWidget):
tab = self.widget(idx)
log.modes.debug("Current tab changed, focusing {!r}".format(tab))
tab.setFocus()
for mode in (usertypes.KeyMode.hint, usertypes.KeyMode.insert,
usertypes.KeyMode.caret, usertypes.KeyMode.passthrough):
for mode in [usertypes.KeyMode.hint, usertypes.KeyMode.insert,
usertypes.KeyMode.caret, usertypes.KeyMode.passthrough]:
modeman.maybe_leave(self._win_id, mode, 'tab changed')
if self._now_focused is not None:
objreg.register('last-focused-tab', self._now_focused, update=True,
@@ -612,25 +587,20 @@ class TabbedBrowser(tabwidget.TabWidget):
if idx == self.currentIndex():
self.update_window_title()
def on_load_finished(self, tab):
"""Adjust tab indicator when loading finished.
We don't take loadFinished's ok argument here as it always seems to be
true when the QWebPage has an ErrorPageExtension implemented.
See https://github.com/The-Compiler/qutebrowser/issues/84
"""
def on_load_finished(self, tab, ok):
"""Adjust tab indicator when loading finished."""
try:
idx = self._tab_index(tab)
except TabDeletedError:
# We can get signals for tabs we already deleted...
return
if tab.page().error_occurred:
color = config.get('colors', 'tabs.indicator.error')
else:
if ok:
start = config.get('colors', 'tabs.indicator.start')
stop = config.get('colors', 'tabs.indicator.stop')
system = config.get('colors', 'tabs.indicator.system')
color = utils.interpolate_color(start, stop, 100, system)
else:
color = config.get('colors', 'tabs.indicator.error')
self.set_tab_indicator_color(idx, color)
self.update_tab_title(idx)
if idx == self.currentIndex():
@@ -676,7 +646,7 @@ class TabbedBrowser(tabwidget.TabWidget):
if key != "'":
message.error(self._win_id, "Failed to set mark: url invalid")
return
point = self.currentWidget().page().currentFrame().scrollPosition()
point = self.currentWidget().scroller.pos_px()
if key.isupper():
self._global_marks[key] = point, url
@@ -691,21 +661,29 @@ class TabbedBrowser(tabwidget.TabWidget):
Args:
key: mark identifier; capital indicates a global mark
"""
# consider urls that differ only in fragment to be identical
urlkey = self.current_url().adjusted(QUrl.RemoveFragment)
frame = self.currentWidget().page().currentFrame()
try:
# consider urls that differ only in fragment to be identical
urlkey = self.current_url().adjusted(QUrl.RemoveFragment)
except qtutils.QtValueError:
urlkey = None
if key.isupper() and key in self._global_marks:
point, url = self._global_marks[key]
tab = self.currentWidget()
@pyqtSlot(bool)
def callback(ok):
if ok:
self.cur_load_finished.disconnect(callback)
frame.setScrollPosition(point)
if key.isupper():
if key in self._global_marks:
point, url = self._global_marks[key]
self.openurl(url, newtab=False)
self.cur_load_finished.connect(callback)
def callback(ok):
if ok:
self.cur_load_finished.disconnect(callback)
tab.scroller.to_point(point)
self.openurl(url, newtab=False)
self.cur_load_finished.connect(callback)
else:
message.error(self._win_id, "Mark {} is not set".format(key))
elif urlkey is None:
message.error(self._win_id, "Current URL is invalid!")
elif urlkey in self._local_marks and key in self._local_marks[urlkey]:
point = self._local_marks[urlkey][key]
@@ -714,6 +692,6 @@ class TabbedBrowser(tabwidget.TabWidget):
# "'" would just jump to the current position every time
self.set_mark("'")
frame.setScrollPosition(point)
tab.scroller.to_point(point)
else:
message.error(self._win_id, "Mark {} is not set".format(key))

View File

@@ -22,14 +22,13 @@
import collections
import functools
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QSize, QRect, QTimer
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QSize, QRect, QTimer, QUrl
from PyQt5.QtWidgets import (QTabWidget, QTabBar, QSizePolicy, QCommonStyle,
QStyle, QStylePainter, QStyleOptionTab)
from PyQt5.QtGui import QIcon, QPalette, QColor
from qutebrowser.utils import qtutils, objreg, utils, usertypes
from qutebrowser.config import config
from qutebrowser.browser import webview
PixelMetrics = usertypes.enum('PixelMetrics', ['icon_padding'],
@@ -73,7 +72,7 @@ class TabWidget(QTabWidget):
position = config.get('tabs', 'position')
selection_behavior = config.get('tabs', 'select-on-remove')
self.setTabPosition(position)
tabbar.vertical = position in (QTabWidget.West, QTabWidget.East)
tabbar.vertical = position in [QTabWidget.West, QTabWidget.East]
tabbar.setSelectionBehaviorOnRemove(selection_behavior)
tabbar.refresh()
@@ -99,21 +98,38 @@ class TabWidget(QTabWidget):
def update_tab_title(self, idx):
"""Update the tab text for the given tab."""
widget = self.widget(idx)
page_title = self.page_title(idx).replace('&', '&&')
fields = self.get_tab_fields(idx)
fields['title'] = fields['title'].replace('&', '&&')
fields['index'] = idx + 1
fmt = config.get('tabs', 'title-format')
self.tabBar().setTabText(idx, fmt.format(**fields))
def get_tab_fields(self, idx):
"""Get the tab field data."""
tab = self.widget(idx)
page_title = self.page_title(idx)
fields = {}
if widget.load_status == webview.LoadStatus.loading:
fields['perc'] = '[{}%] '.format(widget.progress)
fields['id'] = tab.tab_id
fields['title'] = page_title
fields['title_sep'] = ' - ' if page_title else ''
fields['perc_raw'] = tab.progress()
if tab.load_status() == usertypes.LoadStatus.loading:
fields['perc'] = '[{}%] '.format(tab.progress())
else:
fields['perc'] = ''
fields['perc_raw'] = widget.progress
fields['title'] = page_title
fields['index'] = idx + 1
fields['id'] = widget.tab_id
fields['title_sep'] = ' - ' if page_title else ''
y = widget.scroll_pos[1]
if y <= 0:
try:
fields['host'] = self.tab_url(idx).host()
except qtutils.QtValueError:
fields['host'] = ''
y = tab.scroller.pos_perc()[1]
if y is None:
scroll_pos = '???'
elif y <= 0:
scroll_pos = 'top'
elif y >= 100:
scroll_pos = 'bot'
@@ -121,9 +137,7 @@ class TabWidget(QTabWidget):
scroll_pos = '{:2}%'.format(y)
fields['scroll_pos'] = scroll_pos
fmt = config.get('tabs', 'title-format')
self.tabBar().setTabText(idx, fmt.format(**fields))
return fields
@config.change_filter('tabs', 'title-format')
def update_tab_titles(self):
@@ -205,6 +219,21 @@ class TabWidget(QTabWidget):
self.tabBar().on_change()
self.tab_index_changed.emit(index, self.count())
def tab_url(self, idx):
"""Get the URL of the tab at the given index.
Return:
The tab URL as QUrl.
"""
tab = self.widget(idx)
if tab is None:
url = QUrl()
else:
url = tab.url()
# It's possible for url to be invalid, but the caller will handle that.
qtutils.ensure_valid(url)
return url
class TabBar(QTabBar):
@@ -513,11 +542,11 @@ class TabBarStyle(QCommonStyle):
style: The base/"parent" style.
"""
self._style = style
for method in ('drawComplexControl', 'drawItemPixmap',
for method in ['drawComplexControl', 'drawItemPixmap',
'generatedIconPixmap', 'hitTestComplexControl',
'itemPixmapRect', 'itemTextRect', 'polish', 'styleHint',
'subControlRect', 'unpolish', 'drawItemText',
'sizeFromContents', 'drawPrimitive'):
'sizeFromContents', 'drawPrimitive']:
target = getattr(self._style, method)
setattr(self, method, functools.partial(target))
super().__init__()

View File

@@ -36,8 +36,8 @@ from PyQt5.QtWidgets import (QDialog, QLabel, QTextEdit, QPushButton,
import qutebrowser
from qutebrowser.utils import version, log, utils, objreg
from qutebrowser.misc import miscwidgets, autoupdate, msgbox, httpclient
from qutebrowser.browser.network import pastebin
from qutebrowser.misc import (miscwidgets, autoupdate, msgbox, httpclient,
pastebin)
from qutebrowser.config import config
@@ -420,9 +420,6 @@ class ExceptionCrashDialog(_CrashDialog):
text = "<b>Argh! qutebrowser crashed unexpectedly.</b>"
self._lbl.setText(text)
def _init_buttons(self):
super()._init_buttons()
def _init_checkboxes(self):
"""Add checkboxes to the dialog."""
super()._init_checkboxes()

View File

@@ -116,7 +116,7 @@ class CrashHandler(QObject):
window=win_id)
for tab in tabbed_browser.widgets():
try:
urlstr = tab.cur_url.toString(
urlstr = tab.url().toString(
QUrl.RemovePassword | QUrl.FullyEncoded)
if urlstr:
win_pages.append(urlstr)
@@ -311,7 +311,7 @@ class SignalHandler(QObject):
# pylint: disable=import-error,no-member,useless-suppression
import fcntl
read_fd, write_fd = os.pipe()
for fd in (read_fd, write_fd):
for fd in [read_fd, write_fd]:
flags = fcntl.fcntl(fd, fcntl.F_GETFL)
fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
self._notifier = QSocketNotifier(read_fd, QSocketNotifier.Read,

View File

@@ -44,21 +44,31 @@ except ImportError:
# initialization needs to take place before that!
def _missing_str(name, *, windows=None, pip=None):
def _missing_str(name, *, windows=None, pip=None, webengine=False):
"""Get an error string for missing packages.
Args:
name: The name of the package.
windows: String to be displayed for Windows.
pip: pypi package name.
webengine: Whether this is checking the QtWebEngine package
"""
blocks = ["Fatal error: <b>{}</b> is required to run qutebrowser but "
"could not be imported! Maybe it's not installed?".format(name)]
lines = ['Please search for the python3 version of {} in your '
'distributions packages, or install it via pip.'.format(name)]
blocks.append('<br />'.join(lines))
lines = ['<b>If you installed a qutebrowser package for your '
'distribution, please report this as a bug.</b>']
if webengine:
lines = [
'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',
]
else:
lines = ['<b>If you installed a qutebrowser package for your '
'distribution, please report this as a bug.</b>']
blocks.append('<br />'.join(lines))
if windows is not None:
lines = ["<b>On Windows:</b>"]
@@ -160,8 +170,11 @@ def fix_harfbuzz(args):
from qutebrowser.utils import log
from PyQt5.QtCore import qVersion
if 'PyQt5.QtWidgets' in sys.modules:
log.init.warning("Harfbuzz fix attempted but QtWidgets is already "
"imported!")
msg = "Harfbuzz fix attempted but QtWidgets is already imported!"
if getattr(sys, 'frozen', False):
log.init.debug(msg)
else:
log.init.warning(msg)
if sys.platform.startswith('linux') and args.harfbuzz == 'auto':
if qVersion() == '5.3.0':
log.init.debug("Using new harfbuzz engine (auto)")
@@ -169,7 +182,7 @@ def fix_harfbuzz(args):
else:
log.init.debug("Using old harfbuzz engine (auto)")
os.environ['QT_HARFBUZZ'] = 'old'
elif args.harfbuzz in ('old', 'new'):
elif args.harfbuzz in ['old', 'new']:
# forced harfbuzz variant
# FIXME looking at the Qt code, 'new' isn't a valid value, but leaving
# it empty and using new yields different behavior...
@@ -184,7 +197,7 @@ def fix_harfbuzz(args):
def check_pyqt_core():
"""Check if PyQt core is installed."""
try:
import PyQt5.QtCore
import PyQt5.QtCore # pylint: disable=unused-variable
except ImportError as e:
text = _missing_str('PyQt5',
windows="Use the installer by Riverbank computing "
@@ -230,7 +243,7 @@ def check_ssl_support():
_die(text)
def check_libraries():
def check_libraries(args):
"""Check if all needed Python libraries are installed."""
modules = {
'PyQt5.QtWebKit': _missing_str("PyQt5.QtWebKit"),
@@ -257,6 +270,9 @@ def check_libraries():
"or Install via pip.",
pip="PyYAML"),
}
if args.backend == 'webengine':
modules['PyQt5.QtWebEngineWidgets'] = _missing_str("QtWebEngine",
webengine=True)
for name, text in modules.items():
try:
importlib.import_module(name)
@@ -300,13 +316,14 @@ def earlyinit(args):
# Here we check if QtCore is available, and if not, print a message to the
# console or via Tk.
check_pyqt_core()
# Init logging as early as possible
init_log(args)
# Now the faulthandler is enabled we fix the Qt harfbuzzing library, before
# importing QtWidgets.
fix_harfbuzz(args)
# Now we can be sure QtCore is available, so we can print dialogs on
# errors, so people only using the GUI notice them as well.
check_qt_version()
check_ssl_support()
remove_inputhook()
check_libraries()
init_log(args)
check_libraries(args)
check_ssl_support()

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/>.
"""A HTTP client based on QNetworkAccessManager."""
"""An HTTP client based on QNetworkAccessManager."""
import functools
import urllib.request
@@ -30,7 +30,7 @@ from PyQt5.QtNetwork import (QNetworkAccessManager, QNetworkRequest,
class HTTPClient(QObject):
"""A HTTP client based on QNetworkAccessManager.
"""An HTTP client based on QNetworkAccessManager.
Intended for APIs, automatically decodes data.

View File

@@ -263,8 +263,8 @@ class IPCServer(QObject):
log.ipc.debug("We can read a line immediately.")
self.on_ready_read()
socket.error.connect(self.on_error)
if socket.error() not in (QLocalSocket.UnknownSocketError,
QLocalSocket.PeerClosedError):
if socket.error() not in [QLocalSocket.UnknownSocketError,
QLocalSocket.PeerClosedError]:
log.ipc.debug("We got an error immediately.")
self.on_error(socket.error())
socket.disconnected.connect(self.on_disconnected)
@@ -311,7 +311,7 @@ class IPCServer(QObject):
self._handle_invalid_data()
return
for name in ('args', 'target_arg'):
for name in ['args', 'target_arg']:
if name not in json_data:
log.ipc.error("Missing {}: {}".format(name, decoded.strip()))
self._handle_invalid_data()
@@ -493,8 +493,8 @@ def send_to_running_instance(socketname, command, target_arg, *,
socket.waitForDisconnected(CONNECT_TIMEOUT)
return True
else:
if socket.error() not in (QLocalSocket.ConnectionRefusedError,
QLocalSocket.ServerNotFoundError):
if socket.error() not in [QLocalSocket.ConnectionRefusedError,
QLocalSocket.ServerNotFoundError]:
raise SocketError("connecting to running instance", socket)
else:
log.ipc.debug("No existing instance present (error {})".format(

View File

@@ -26,7 +26,7 @@ from PyQt5.QtWidgets import QMessageBox
def msgbox(parent, title, text, *, icon, buttons=QMessageBox.Ok,
on_finished=None, plain_text=None):
"""Display an QMessageBox with the given icon.
"""Display a QMessageBox with the given icon.
Args:
parent: The parent to set for the message box.

View File

@@ -158,7 +158,23 @@ class ReadlineBridge:
widget = self._widget()
if widget is None:
return
widget.cursorWordBackward(True)
cursor_position = widget.cursorPosition()
text = widget.text()
target_position = cursor_position
is_word_boundary = True
while is_word_boundary and target_position > 0:
is_word_boundary = text[target_position - 1] == " "
target_position -= 1
is_word_boundary = False
while not is_word_boundary and target_position > 0:
is_word_boundary = text[target_position - 1] == " "
target_position -= 1
moveby = cursor_position - target_position - 1
widget.cursorBackward(True, moveby)
self._deleted[widget] = widget.selectedText()
widget.del_()

View File

@@ -67,7 +67,6 @@ class Saveable:
save_on_exit=self._save_on_exit,
filename=self._filename)
@pyqtSlot()
def mark_dirty(self):
"""Mark this saveable as dirty (having changes)."""
log.save.debug("Marking {} as dirty.".format(self._name))

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