Merge branch 'master' of https://github.com/The-Compiler/qutebrowser into pintab
This commit is contained in:
commit
9f70fa3ec8
@ -75,4 +75,5 @@ ignored-modules=pytest
|
|||||||
# UnsetObject because pylint infers any objreg.get(...) as UnsetObject.
|
# UnsetObject because pylint infers any objreg.get(...) as UnsetObject.
|
||||||
ignored-classes=qutebrowser.utils.objreg.UnsetObject,
|
ignored-classes=qutebrowser.utils.objreg.UnsetObject,
|
||||||
qutebrowser.browser.webkit.webelem.WebElementWrapper,
|
qutebrowser.browser.webkit.webelem.WebElementWrapper,
|
||||||
scripts.dev.check_coverage.MsgType
|
scripts.dev.check_coverage.MsgType,
|
||||||
|
qutebrowser.browser.downloads.UnsupportedAttribute
|
||||||
|
@ -140,6 +140,8 @@ Changed
|
|||||||
- `qute:version` and `qutebrowser --version` now show various important paths
|
- `qute:version` and `qutebrowser --version` now show various important paths
|
||||||
- `:spawn`/userscripts now show a nicer error when a script wasn't found
|
- `:spawn`/userscripts now show a nicer error when a script wasn't found
|
||||||
- Various functionality now works when javascript is disabled with QtWebKit
|
- Various functionality now works when javascript is disabled with QtWebKit
|
||||||
|
- Various commands/settings taking `left`/`right`/`previous` arguments now take
|
||||||
|
`prev`/`next`/`last-used` to remove ambiguity.
|
||||||
|
|
||||||
Deprecated
|
Deprecated
|
||||||
~~~~~~~~~~
|
~~~~~~~~~~
|
||||||
|
@ -154,8 +154,8 @@ Contributors, sorted by the number of commits in descending order:
|
|||||||
* Bruno Oliveira
|
* Bruno Oliveira
|
||||||
* Alexander Cogneau
|
* Alexander Cogneau
|
||||||
* Felix Van der Jeugt
|
* Felix Van der Jeugt
|
||||||
* Martin Tournoij
|
|
||||||
* Daniel Karbach
|
* Daniel Karbach
|
||||||
|
* Martin Tournoij
|
||||||
* Kevin Velghe
|
* Kevin Velghe
|
||||||
* Raphael Pierzina
|
* Raphael Pierzina
|
||||||
* Joel Torstensson
|
* Joel Torstensson
|
||||||
|
@ -779,13 +779,13 @@ Duplicate the current tab.
|
|||||||
|
|
||||||
[[tab-close]]
|
[[tab-close]]
|
||||||
=== tab-close
|
=== tab-close
|
||||||
Syntax: +:tab-close [*--left*] [*--right*] [*--opposite*]+
|
Syntax: +:tab-close [*--prev*] [*--next*] [*--opposite*]+
|
||||||
|
|
||||||
Close the current/[count]th tab.
|
Close the current/[count]th tab.
|
||||||
|
|
||||||
==== optional arguments
|
==== optional arguments
|
||||||
* +*-l*+, +*--left*+: Force selecting the tab to the left of the current tab.
|
* +*-p*+, +*--prev*+: Force selecting the tab before the current tab.
|
||||||
* +*-r*+, +*--right*+: Force selecting the tab to the right of the current tab.
|
* +*-n*+, +*--next*+: Force selecting the tab after the current tab.
|
||||||
* +*-o*+, +*--opposite*+: Force selecting the tab in the opposite direction of what's configured in 'tabs->select-on-remove'.
|
* +*-o*+, +*--opposite*+: Force selecting the tab in the opposite direction of what's configured in 'tabs->select-on-remove'.
|
||||||
|
|
||||||
|
|
||||||
@ -841,13 +841,13 @@ How many tabs to switch forward.
|
|||||||
|
|
||||||
[[tab-only]]
|
[[tab-only]]
|
||||||
=== tab-only
|
=== tab-only
|
||||||
Syntax: +:tab-only [*--left*] [*--right*]+
|
Syntax: +:tab-only [*--prev*] [*--next*]+
|
||||||
|
|
||||||
Close all tabs except for the current one.
|
Close all tabs except for the current one.
|
||||||
|
|
||||||
==== optional arguments
|
==== optional arguments
|
||||||
* +*-l*+, +*--left*+: Keep tabs to the left of the current.
|
* +*-p*+, +*--prev*+: Keep tabs before the current.
|
||||||
* +*-r*+, +*--right*+: Keep tabs to the right of the current.
|
* +*-n*+, +*--next*+: Keep tabs after the current.
|
||||||
|
|
||||||
[[tab-prev]]
|
[[tab-prev]]
|
||||||
=== tab-prev
|
=== tab-prev
|
||||||
|
@ -1038,11 +1038,11 @@ Which tab to select when the focused tab is removed.
|
|||||||
|
|
||||||
Valid values:
|
Valid values:
|
||||||
|
|
||||||
* +left+: Select the tab on the left.
|
* +prev+: Select the tab which came before the closed one (left in horizontal, above in vertical).
|
||||||
* +right+: Select the tab on the right.
|
* +next+: Select the tab which came after the closed one (right in horizontal, below in vertical).
|
||||||
* +previous+: Select the previously selected tab.
|
* +last-used+: Select the previously selected tab.
|
||||||
|
|
||||||
Default: +pass:[right]+
|
Default: +pass:[next]+
|
||||||
|
|
||||||
[[tabs-new-tab-position]]
|
[[tabs-new-tab-position]]
|
||||||
=== new-tab-position
|
=== new-tab-position
|
||||||
@ -1050,12 +1050,12 @@ How new tabs are positioned.
|
|||||||
|
|
||||||
Valid values:
|
Valid values:
|
||||||
|
|
||||||
* +left+: On the left of the current tab.
|
* +prev+: Before the current tab.
|
||||||
* +right+: On the right of the current tab.
|
* +next+: After the current tab.
|
||||||
* +first+: At the left end.
|
* +first+: At the beginning.
|
||||||
* +last+: At the right end.
|
* +last+: At the end.
|
||||||
|
|
||||||
Default: +pass:[right]+
|
Default: +pass:[next]+
|
||||||
|
|
||||||
[[tabs-new-tab-position-explicit]]
|
[[tabs-new-tab-position-explicit]]
|
||||||
=== new-tab-position-explicit
|
=== new-tab-position-explicit
|
||||||
@ -1063,10 +1063,10 @@ How new tabs opened explicitly are positioned.
|
|||||||
|
|
||||||
Valid values:
|
Valid values:
|
||||||
|
|
||||||
* +left+: On the left of the current tab.
|
* +prev+: Before the current tab.
|
||||||
* +right+: On the right of the current tab.
|
* +next+: After the current tab.
|
||||||
* +first+: At the left end.
|
* +first+: At the beginning.
|
||||||
* +last+: At the right end.
|
* +last+: At the end.
|
||||||
|
|
||||||
Default: +pass:[last]+
|
Default: +pass:[last]+
|
||||||
|
|
||||||
|
@ -45,8 +45,9 @@ import qutebrowser.resources
|
|||||||
from qutebrowser.completion.models import instances as completionmodels
|
from qutebrowser.completion.models import instances as completionmodels
|
||||||
from qutebrowser.commands import cmdutils, runners, cmdexc
|
from qutebrowser.commands import cmdutils, runners, cmdexc
|
||||||
from qutebrowser.config import style, config, websettings, configexc
|
from qutebrowser.config import style, config, websettings, configexc
|
||||||
from qutebrowser.browser import urlmarks, adblock, history, browsertab
|
from qutebrowser.browser import (urlmarks, adblock, history, browsertab,
|
||||||
from qutebrowser.browser.webkit import cookies, cache, downloads
|
downloads)
|
||||||
|
from qutebrowser.browser.webkit import cookies, cache
|
||||||
from qutebrowser.browser.webkit.network import networkmanager
|
from qutebrowser.browser.webkit.network import networkmanager
|
||||||
from qutebrowser.mainwindow import mainwindow, prompt
|
from qutebrowser.mainwindow import mainwindow, prompt
|
||||||
from qutebrowser.misc import (readline, ipc, savemanager, sessions,
|
from qutebrowser.misc import (readline, ipc, savemanager, sessions,
|
||||||
@ -371,7 +372,6 @@ def _init_modules(args, crash_handler):
|
|||||||
args: The argparse namespace.
|
args: The argparse namespace.
|
||||||
crash_handler: The CrashHandler instance.
|
crash_handler: The CrashHandler instance.
|
||||||
"""
|
"""
|
||||||
# pylint: disable=too-many-statements
|
|
||||||
log.init.debug("Initializing prompts...")
|
log.init.debug("Initializing prompts...")
|
||||||
prompt.init()
|
prompt.init()
|
||||||
|
|
||||||
@ -435,8 +435,6 @@ def _init_modules(args, crash_handler):
|
|||||||
os.environ['QT_WAYLAND_DISABLE_WINDOWDECORATION'] = '1'
|
os.environ['QT_WAYLAND_DISABLE_WINDOWDECORATION'] = '1'
|
||||||
else:
|
else:
|
||||||
os.environ.pop('QT_WAYLAND_DISABLE_WINDOWDECORATION', None)
|
os.environ.pop('QT_WAYLAND_DISABLE_WINDOWDECORATION', None)
|
||||||
temp_downloads = downloads.TempDownloadManager(qApp)
|
|
||||||
objreg.register('temporary-downloads', temp_downloads)
|
|
||||||
# Init backend-specific stuff
|
# Init backend-specific stuff
|
||||||
browsertab.init(args)
|
browsertab.init(args)
|
||||||
|
|
||||||
@ -705,6 +703,7 @@ class Quitter:
|
|||||||
atexit.register(shutil.rmtree, self._args.basedir,
|
atexit.register(shutil.rmtree, self._args.basedir,
|
||||||
ignore_errors=True)
|
ignore_errors=True)
|
||||||
# Delete temp download dir
|
# Delete temp download dir
|
||||||
|
downloads.temp_download_manager.cleanup()
|
||||||
# If we don't kill our custom handler here we might get segfaults
|
# If we don't kill our custom handler here we might get segfaults
|
||||||
log.destroy.debug("Deactivating message handler...")
|
log.destroy.debug("Deactivating message handler...")
|
||||||
qInstallMessageHandler(None)
|
qInstallMessageHandler(None)
|
||||||
|
@ -26,8 +26,9 @@ import posixpath
|
|||||||
import zipfile
|
import zipfile
|
||||||
import fnmatch
|
import fnmatch
|
||||||
|
|
||||||
|
from qutebrowser.browser import downloads
|
||||||
from qutebrowser.config import config
|
from qutebrowser.config import config
|
||||||
from qutebrowser.utils import objreg, standarddir, log, message, usertypes
|
from qutebrowser.utils import objreg, standarddir, log, message
|
||||||
from qutebrowser.commands import cmdutils, cmdexc
|
from qutebrowser.commands import cmdutils, cmdexc
|
||||||
|
|
||||||
|
|
||||||
@ -190,8 +191,8 @@ class HostBlocker:
|
|||||||
self._blocked_hosts = set()
|
self._blocked_hosts = set()
|
||||||
self._done_count = 0
|
self._done_count = 0
|
||||||
urls = config.get('content', 'host-block-lists')
|
urls = config.get('content', 'host-block-lists')
|
||||||
download_manager = objreg.get('download-manager', scope='window',
|
download_manager = objreg.get('qtnetwork-download-manager',
|
||||||
window='last-focused')
|
scope='window', window='last-focused')
|
||||||
if urls is None:
|
if urls is None:
|
||||||
return
|
return
|
||||||
for url in urls:
|
for url in urls:
|
||||||
@ -208,7 +209,7 @@ class HostBlocker:
|
|||||||
else:
|
else:
|
||||||
fobj = io.BytesIO()
|
fobj = io.BytesIO()
|
||||||
fobj.name = 'adblock: ' + url.host()
|
fobj.name = 'adblock: ' + url.host()
|
||||||
target = usertypes.FileObjDownloadTarget(fobj)
|
target = downloads.FileObjDownloadTarget(fobj)
|
||||||
download = download_manager.get(url, target=target,
|
download = download_manager.get(url, target=target,
|
||||||
auto_remove=True)
|
auto_remove=True)
|
||||||
self._in_progress.append(download)
|
self._in_progress.append(download)
|
||||||
|
@ -43,8 +43,7 @@ import pygments.formatters
|
|||||||
from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners
|
from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners
|
||||||
from qutebrowser.config import config, configexc
|
from qutebrowser.config import config, configexc
|
||||||
from qutebrowser.browser import (urlmarks, browsertab, inspector, navigate,
|
from qutebrowser.browser import (urlmarks, browsertab, inspector, navigate,
|
||||||
webelem)
|
webelem, downloads)
|
||||||
from qutebrowser.browser.webkit import downloads
|
|
||||||
try:
|
try:
|
||||||
from qutebrowser.browser.webkit import mhtml
|
from qutebrowser.browser.webkit import mhtml
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@ -179,12 +178,12 @@ class CommandDispatcher:
|
|||||||
raise cmdexc.CommandError("Last focused tab vanished!")
|
raise cmdexc.CommandError("Last focused tab vanished!")
|
||||||
self._set_current_index(idx)
|
self._set_current_index(idx)
|
||||||
|
|
||||||
def _get_selection_override(self, left, right, opposite):
|
def _get_selection_override(self, prev, next_, opposite):
|
||||||
"""Helper function for tab_close to get the tab to select.
|
"""Helper function for tab_close to get the tab to select.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
left: Force selecting the tab to the left of the current tab.
|
prev: Force selecting the tab before the current tab.
|
||||||
right: Force selecting the tab to the right of the current tab.
|
next_: Force selecting the tab after the current tab.
|
||||||
opposite: Force selecting the tab in the opposite direction of
|
opposite: Force selecting the tab in the opposite direction of
|
||||||
what's configured in 'tabs->select-on-remove'.
|
what's configured in 'tabs->select-on-remove'.
|
||||||
|
|
||||||
@ -192,10 +191,10 @@ class CommandDispatcher:
|
|||||||
QTabBar.SelectLeftTab, QTabBar.SelectRightTab, or None if no change
|
QTabBar.SelectLeftTab, QTabBar.SelectRightTab, or None if no change
|
||||||
should be made.
|
should be made.
|
||||||
"""
|
"""
|
||||||
cmdutils.check_exclusive((left, right, opposite), 'lro')
|
cmdutils.check_exclusive((prev, next_, opposite), 'pno')
|
||||||
if left:
|
if prev:
|
||||||
return QTabBar.SelectLeftTab
|
return QTabBar.SelectLeftTab
|
||||||
elif right:
|
elif next_:
|
||||||
return QTabBar.SelectRightTab
|
return QTabBar.SelectRightTab
|
||||||
elif opposite:
|
elif opposite:
|
||||||
conf_selection = config.get('tabs', 'select-on-remove')
|
conf_selection = config.get('tabs', 'select-on-remove')
|
||||||
@ -206,25 +205,24 @@ class CommandDispatcher:
|
|||||||
elif conf_selection == QTabBar.SelectPreviousTab:
|
elif conf_selection == QTabBar.SelectPreviousTab:
|
||||||
raise cmdexc.CommandError(
|
raise cmdexc.CommandError(
|
||||||
"-o is not supported with 'tabs->select-on-remove' set to "
|
"-o is not supported with 'tabs->select-on-remove' set to "
|
||||||
"'previous'!")
|
"'last-used'!")
|
||||||
else: # pragma: no cover
|
else: # pragma: no cover
|
||||||
raise ValueError("Invalid select-on-remove value "
|
raise ValueError("Invalid select-on-remove value "
|
||||||
"{!r}!".format(conf_selection))
|
"{!r}!".format(conf_selection))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _tab_close(self, tab, left=False, right=False, opposite=False):
|
def _tab_close(self, tab, prev=False, next_=False, opposite=False):
|
||||||
"""Helper function for tab_close be able to handle message.async.
|
"""Helper function for tab_close be able to handle message.async.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
tab: Tab select to be closed.
|
tab: Tab select to be closed.
|
||||||
left: Force selecting the tab to the left of the current tab.
|
prev: Force selecting the tab before the current tab.
|
||||||
right: Force selecting the tab to the right of the current tab.
|
next_: Force selecting the tab after the current tab.
|
||||||
opposite: Force selecting the tab in the opposite direction of
|
opposite: Force selecting the tab in the opposite direction of
|
||||||
what's configured in 'tabs->select-on-remove'.
|
what's configured in 'tabs->select-on-remove'.
|
||||||
count: The tab index to close, or None
|
count: The tab index to close, or None
|
||||||
"""
|
"""
|
||||||
tabbar = self._tabbed_browser.tabBar()
|
tabbar = self._tabbed_browser.tabBar()
|
||||||
selection_override = self._get_selection_override(left, right,
|
selection_override = self._get_selection_override(prev, next_,
|
||||||
opposite)
|
opposite)
|
||||||
|
|
||||||
if tab.data.pinned:
|
if tab.data.pinned:
|
||||||
@ -240,12 +238,11 @@ class CommandDispatcher:
|
|||||||
|
|
||||||
@cmdutils.register(instance='command-dispatcher', scope='window')
|
@cmdutils.register(instance='command-dispatcher', scope='window')
|
||||||
@cmdutils.argument('count', count=True)
|
@cmdutils.argument('count', count=True)
|
||||||
def tab_close(self, left=False, right=False, opposite=False, count=None):
|
def tab_close(self, prev=False, next_=False, opposite=False, count=None):
|
||||||
"""Close the current/[count]th tab.
|
"""Close the current/[count]th tab.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
left: Force selecting the tab to the left of the current tab.
|
prev: Force selecting the tab before the current tab.
|
||||||
right: Force selecting the tab to the right of the current tab.
|
next_: Force selecting the tab after the current tab.
|
||||||
opposite: Force selecting the tab in the opposite direction of
|
opposite: Force selecting the tab in the opposite direction of
|
||||||
what's configured in 'tabs->select-on-remove'.
|
what's configured in 'tabs->select-on-remove'.
|
||||||
count: The tab index to close, or None
|
count: The tab index to close, or None
|
||||||
@ -253,9 +250,8 @@ class CommandDispatcher:
|
|||||||
tab = self._cntwidget(count)
|
tab = self._cntwidget(count)
|
||||||
if tab is None:
|
if tab is None:
|
||||||
return
|
return
|
||||||
|
close = functools.partial(self._tab_close, tab, prev,
|
||||||
close = functools.partial(self._tab_close, tab, left,
|
next_, opposite)
|
||||||
right, opposite)
|
|
||||||
|
|
||||||
if tab.data.pinned:
|
if tab.data.pinned:
|
||||||
message.confirm_async(title='Pinned Tab',
|
message.confirm_async(title='Pinned Tab',
|
||||||
@ -854,20 +850,20 @@ class CommandDispatcher:
|
|||||||
message.info("Zoom level: {}%".format(level))
|
message.info("Zoom level: {}%".format(level))
|
||||||
|
|
||||||
@cmdutils.register(instance='command-dispatcher', scope='window')
|
@cmdutils.register(instance='command-dispatcher', scope='window')
|
||||||
def tab_only(self, left=False, right=False):
|
def tab_only(self, prev=False, next_=False):
|
||||||
"""Close all tabs except for the current one.
|
"""Close all tabs except for the current one.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
left: Keep tabs to the left of the current.
|
prev: Keep tabs before the current.
|
||||||
right: Keep tabs to the right of the current.
|
next_: Keep tabs after the current.
|
||||||
"""
|
"""
|
||||||
cmdutils.check_exclusive((left, right), 'lr')
|
cmdutils.check_exclusive((prev, next_), 'pn')
|
||||||
cur_idx = self._tabbed_browser.currentIndex()
|
cur_idx = self._tabbed_browser.currentIndex()
|
||||||
assert cur_idx != -1
|
assert cur_idx != -1
|
||||||
|
|
||||||
for i, tab in enumerate(self._tabbed_browser.widgets()):
|
for i, tab in enumerate(self._tabbed_browser.widgets()):
|
||||||
if (i == cur_idx or (left and i < cur_idx) or
|
if (i == cur_idx or (prev and i < cur_idx) or
|
||||||
(right and i > cur_idx)):
|
(next_ and i > cur_idx)):
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
self._tabbed_browser.close_tab(tab)
|
self._tabbed_browser.close_tab(tab)
|
||||||
@ -1348,8 +1344,7 @@ class CommandDispatcher:
|
|||||||
except inspector.WebInspectorError as e:
|
except inspector.WebInspectorError as e:
|
||||||
raise cmdexc.CommandError(e)
|
raise cmdexc.CommandError(e)
|
||||||
|
|
||||||
@cmdutils.register(instance='command-dispatcher', scope='window',
|
@cmdutils.register(instance='command-dispatcher', scope='window')
|
||||||
backend=usertypes.Backend.QtWebKit)
|
|
||||||
@cmdutils.argument('dest_old', hide=True)
|
@cmdutils.argument('dest_old', hide=True)
|
||||||
def download(self, url=None, dest_old=None, *, mhtml_=False, dest=None):
|
def download(self, url=None, dest_old=None, *, mhtml_=False, dest=None):
|
||||||
"""Download a given URL, or current page if no URL given.
|
"""Download a given URL, or current page if no URL given.
|
||||||
@ -1371,8 +1366,9 @@ class CommandDispatcher:
|
|||||||
" download.")
|
" download.")
|
||||||
dest = dest_old
|
dest = dest_old
|
||||||
|
|
||||||
download_manager = objreg.get('download-manager', scope='window',
|
# FIXME:qtwebengine do this with the QtWebEngine download manager?
|
||||||
window=self._win_id)
|
download_manager = objreg.get('qtnetwork-download-manager',
|
||||||
|
scope='window', window=self._win_id)
|
||||||
if url:
|
if url:
|
||||||
if mhtml_:
|
if mhtml_:
|
||||||
raise cmdexc.CommandError("Can only download the current page"
|
raise cmdexc.CommandError("Can only download the current page"
|
||||||
@ -1382,21 +1378,16 @@ class CommandDispatcher:
|
|||||||
if dest is None:
|
if dest is None:
|
||||||
target = None
|
target = None
|
||||||
else:
|
else:
|
||||||
target = usertypes.FileDownloadTarget(dest)
|
target = downloads.FileDownloadTarget(dest)
|
||||||
download_manager.get(url, target=target)
|
download_manager.get(url, target=target)
|
||||||
elif mhtml_:
|
elif mhtml_:
|
||||||
self._download_mhtml(dest)
|
self._download_mhtml(dest)
|
||||||
else:
|
else:
|
||||||
tab = self._current_widget()
|
|
||||||
# FIXME:qtwebengine have a proper API for this
|
|
||||||
# pylint: disable=protected-access
|
|
||||||
qnam = tab._widget.page().networkAccessManager()
|
|
||||||
# pylint: enable=protected-access
|
|
||||||
if dest is None:
|
if dest is None:
|
||||||
target = None
|
target = None
|
||||||
else:
|
else:
|
||||||
target = usertypes.FileDownloadTarget(dest)
|
target = downloads.FileDownloadTarget(dest)
|
||||||
download_manager.get(self._current_url(), qnam=qnam, target=target)
|
download_manager.get(self._current_url(), target=target)
|
||||||
|
|
||||||
def _download_mhtml(self, dest=None):
|
def _download_mhtml(self, dest=None):
|
||||||
"""Download the current page as an MHTML file, including all assets.
|
"""Download the current page as an MHTML file, including all assets.
|
||||||
@ -1405,17 +1396,23 @@ class CommandDispatcher:
|
|||||||
dest: The file path to write the download to.
|
dest: The file path to write the download to.
|
||||||
"""
|
"""
|
||||||
tab = self._current_widget()
|
tab = self._current_widget()
|
||||||
|
if tab.backend == usertypes.Backend.QtWebEngine:
|
||||||
|
raise cmdexc.CommandError("Download --mhtml is not implemented "
|
||||||
|
"with QtWebEngine yet")
|
||||||
|
|
||||||
if dest is None:
|
if dest is None:
|
||||||
suggested_fn = self._current_title() + ".mht"
|
suggested_fn = self._current_title() + ".mht"
|
||||||
suggested_fn = utils.sanitize_filename(suggested_fn)
|
suggested_fn = utils.sanitize_filename(suggested_fn)
|
||||||
filename, q = downloads.ask_for_filename(suggested_fn, parent=tab,
|
|
||||||
url=tab.url())
|
filename = downloads.immediate_download_path()
|
||||||
if filename is not None:
|
if filename is not None:
|
||||||
mhtml.start_download_checked(filename, tab=tab)
|
mhtml.start_download_checked(filename, tab=tab)
|
||||||
else:
|
else:
|
||||||
q.answered.connect(functools.partial(
|
question = downloads.get_filename_question(
|
||||||
|
suggested_filename=suggested_fn, url=tab.url(), parent=tab)
|
||||||
|
question.answered.connect(functools.partial(
|
||||||
mhtml.start_download_checked, tab=tab))
|
mhtml.start_download_checked, tab=tab))
|
||||||
q.ask()
|
message.global_bridge.ask(question, blocking=False)
|
||||||
else:
|
else:
|
||||||
mhtml.start_download_checked(dest, tab=tab)
|
mhtml.start_download_checked(dest, tab=tab)
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -25,7 +25,7 @@ import sip
|
|||||||
from PyQt5.QtCore import pyqtSlot, QSize, Qt, QTimer
|
from PyQt5.QtCore import pyqtSlot, QSize, Qt, QTimer
|
||||||
from PyQt5.QtWidgets import QListView, QSizePolicy, QMenu
|
from PyQt5.QtWidgets import QListView, QSizePolicy, QMenu
|
||||||
|
|
||||||
from qutebrowser.browser.webkit import downloads
|
from qutebrowser.browser import downloads
|
||||||
from qutebrowser.config import style
|
from qutebrowser.config import style
|
||||||
from qutebrowser.utils import qtutils, utils, objreg
|
from qutebrowser.utils import qtutils, utils, objreg
|
||||||
|
|
||||||
|
@ -283,14 +283,10 @@ class HintActions:
|
|||||||
else:
|
else:
|
||||||
prompt = None
|
prompt = None
|
||||||
|
|
||||||
# FIXME:qtwebengine get a proper API for this
|
# FIXME:qtwebengine do this with QtWebEngine downloads?
|
||||||
# pylint: disable=protected-access
|
download_manager = objreg.get('qtnetwork-download-manager',
|
||||||
qnam = elem._elem.webFrame().page().networkAccessManager()
|
scope='window', window=self._win_id)
|
||||||
# pylint: enable=protected-access
|
download_manager.get(url, prompt_download_directory=prompt)
|
||||||
|
|
||||||
download_manager = objreg.get('download-manager', scope='window',
|
|
||||||
window=self._win_id)
|
|
||||||
download_manager.get(url, qnam=qnam, prompt_download_directory=prompt)
|
|
||||||
|
|
||||||
def call_userscript(self, elem, context):
|
def call_userscript(self, elem, context):
|
||||||
"""Call a userscript from a hint.
|
"""Call a userscript from a hint.
|
||||||
@ -662,11 +658,6 @@ class HintManager(QObject):
|
|||||||
tab = tabbed_browser.currentWidget()
|
tab = tabbed_browser.currentWidget()
|
||||||
if tab is None:
|
if tab is None:
|
||||||
raise cmdexc.CommandError("No WebView available yet!")
|
raise cmdexc.CommandError("No WebView available yet!")
|
||||||
if (tab.backend == usertypes.Backend.QtWebEngine and
|
|
||||||
target == Target.download):
|
|
||||||
message.error("The download target is not available yet with "
|
|
||||||
"QtWebEngine.")
|
|
||||||
return
|
|
||||||
|
|
||||||
mode_manager = objreg.get('mode-manager', scope='window',
|
mode_manager = objreg.get('mode-manager', scope='window',
|
||||||
window=self._win_id)
|
window=self._win_id)
|
||||||
|
469
qutebrowser/browser/qtnetworkdownloads.py
Normal file
469
qutebrowser/browser/qtnetworkdownloads.py
Normal file
@ -0,0 +1,469 @@
|
|||||||
|
# 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/>.
|
||||||
|
|
||||||
|
"""Download manager."""
|
||||||
|
|
||||||
|
import io
|
||||||
|
import shutil
|
||||||
|
import functools
|
||||||
|
import collections
|
||||||
|
|
||||||
|
from PyQt5.QtCore import pyqtSlot, QTimer
|
||||||
|
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply
|
||||||
|
|
||||||
|
from qutebrowser.utils import message, usertypes, log, urlutils
|
||||||
|
from qutebrowser.browser import downloads
|
||||||
|
from qutebrowser.browser.webkit import http
|
||||||
|
from qutebrowser.browser.webkit.network import networkmanager
|
||||||
|
|
||||||
|
|
||||||
|
_RetryInfo = collections.namedtuple('_RetryInfo', ['request', 'manager'])
|
||||||
|
|
||||||
|
|
||||||
|
class DownloadItem(downloads.AbstractDownloadItem):
|
||||||
|
|
||||||
|
"""A single download currently running.
|
||||||
|
|
||||||
|
There are multiple ways the data can flow from the QNetworkReply to the
|
||||||
|
disk.
|
||||||
|
|
||||||
|
If the filename/file object is known immediately when starting the
|
||||||
|
download, QNetworkReply's readyRead writes to the target file directly.
|
||||||
|
|
||||||
|
If not, readyRead is ignored and with self._read_timer we periodically read
|
||||||
|
into the self._buffer BytesIO slowly, so some broken servers don't close
|
||||||
|
our connection.
|
||||||
|
|
||||||
|
As soon as we know the file object, we copy self._buffer over and the next
|
||||||
|
readyRead will write to the real file object.
|
||||||
|
|
||||||
|
Class attributes:
|
||||||
|
_MAX_REDIRECTS: The maximum redirection count.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
_retry_info: A _RetryInfo instance.
|
||||||
|
_redirects: How many time we were redirected already.
|
||||||
|
_buffer: A BytesIO object to buffer incoming data until we know the
|
||||||
|
target file.
|
||||||
|
_read_timer: A Timer which reads the QNetworkReply into self._buffer
|
||||||
|
periodically.
|
||||||
|
_manager: The DownloadManager which started this download
|
||||||
|
_reply: The QNetworkReply associated with this download.
|
||||||
|
_autoclose: Whether to close the associated file when the download is
|
||||||
|
done.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_MAX_REDIRECTS = 10
|
||||||
|
|
||||||
|
def __init__(self, reply, manager):
|
||||||
|
"""Constructor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
reply: The QNetworkReply to download.
|
||||||
|
"""
|
||||||
|
super().__init__(parent=manager)
|
||||||
|
self.fileobj = None
|
||||||
|
self.raw_headers = {}
|
||||||
|
|
||||||
|
self._autoclose = True
|
||||||
|
self._manager = manager
|
||||||
|
self._retry_info = None
|
||||||
|
self._reply = None
|
||||||
|
self._buffer = io.BytesIO()
|
||||||
|
self._read_timer = usertypes.Timer(self, name='download-read-timer')
|
||||||
|
self._read_timer.setInterval(500)
|
||||||
|
self._read_timer.timeout.connect(self._on_read_timer_timeout)
|
||||||
|
self._redirects = 0
|
||||||
|
self._init_reply(reply)
|
||||||
|
|
||||||
|
def _create_fileobj(self):
|
||||||
|
"""Create a file object using the internal filename."""
|
||||||
|
try:
|
||||||
|
fileobj = open(self._filename, 'wb')
|
||||||
|
except OSError as e:
|
||||||
|
self._die(e.strerror)
|
||||||
|
else:
|
||||||
|
self._set_fileobj(fileobj)
|
||||||
|
|
||||||
|
def _do_die(self):
|
||||||
|
"""Abort the download and emit an error."""
|
||||||
|
self._read_timer.stop()
|
||||||
|
self._reply.downloadProgress.disconnect()
|
||||||
|
self._reply.finished.disconnect()
|
||||||
|
self._reply.error.disconnect()
|
||||||
|
self._reply.readyRead.disconnect()
|
||||||
|
with log.hide_qt_warning('QNetworkReplyImplPrivate::error: Internal '
|
||||||
|
'problem, this method must only be called '
|
||||||
|
'once.'):
|
||||||
|
# See https://codereview.qt-project.org/#/c/107863/
|
||||||
|
self._reply.abort()
|
||||||
|
self._reply.deleteLater()
|
||||||
|
self._reply = None
|
||||||
|
if self.fileobj is not None:
|
||||||
|
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.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
reply: The QNetworkReply to handle.
|
||||||
|
"""
|
||||||
|
self.done = False
|
||||||
|
self.successful = False
|
||||||
|
self._reply = reply
|
||||||
|
reply.setReadBufferSize(16 * 1024 * 1024) # 16 MB
|
||||||
|
reply.downloadProgress.connect(self.stats.on_download_progress)
|
||||||
|
reply.finished.connect(self._on_reply_finished)
|
||||||
|
reply.error.connect(self._on_reply_error)
|
||||||
|
reply.readyRead.connect(self._on_ready_read)
|
||||||
|
reply.metaDataChanged.connect(self._on_meta_data_changed)
|
||||||
|
self._retry_info = _RetryInfo(request=reply.request(),
|
||||||
|
manager=reply.manager())
|
||||||
|
if not self.fileobj:
|
||||||
|
self._read_timer.start()
|
||||||
|
# We could have got signals before we connected slots to them.
|
||||||
|
# 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._die(reply.errorString()))
|
||||||
|
|
||||||
|
def _do_cancel(self):
|
||||||
|
if self._reply is not None:
|
||||||
|
self._reply.finished.disconnect(self._on_reply_finished)
|
||||||
|
self._reply.abort()
|
||||||
|
self._reply.deleteLater()
|
||||||
|
self._reply = None
|
||||||
|
if self.fileobj is not None:
|
||||||
|
self.fileobj.close()
|
||||||
|
self.cancelled.emit()
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def retry(self):
|
||||||
|
"""Retry a failed download."""
|
||||||
|
assert self.done
|
||||||
|
assert not self.successful
|
||||||
|
new_reply = self._retry_info.manager.get(self._retry_info.request)
|
||||||
|
self._manager.fetch(new_reply, suggested_filename=self.basename)
|
||||||
|
self.cancel()
|
||||||
|
|
||||||
|
def _get_open_filename(self):
|
||||||
|
filename = self._filename
|
||||||
|
if filename is None:
|
||||||
|
filename = getattr(self.fileobj, 'name', None)
|
||||||
|
return filename
|
||||||
|
|
||||||
|
def _ensure_can_set_filename(self, filename):
|
||||||
|
if self.fileobj is not None: # pragma: no cover
|
||||||
|
raise ValueError("fileobj was already set! filename: {}, "
|
||||||
|
"existing: {}, fileobj {}".format(
|
||||||
|
filename, self._filename, self.fileobj))
|
||||||
|
|
||||||
|
def _after_set_filename(self):
|
||||||
|
self._create_fileobj()
|
||||||
|
|
||||||
|
def _ask_confirm_question(self, title, msg):
|
||||||
|
no_action = functools.partial(self.cancel, remove_data=False)
|
||||||
|
message.confirm_async(title=title, text=msg,
|
||||||
|
yes_action=self._after_set_filename,
|
||||||
|
no_action=no_action, cancel_action=no_action,
|
||||||
|
abort_on=[self.cancelled, self.error])
|
||||||
|
|
||||||
|
def _set_fileobj(self, fileobj, *, autoclose=True):
|
||||||
|
""""Set the file object to write the download to.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
fileobj: A file-like object.
|
||||||
|
"""
|
||||||
|
if self.fileobj is not None: # pragma: no cover
|
||||||
|
raise ValueError("fileobj was already set! Old: {}, new: "
|
||||||
|
"{}".format(self.fileobj, fileobj))
|
||||||
|
self.fileobj = fileobj
|
||||||
|
self._autoclose = autoclose
|
||||||
|
try:
|
||||||
|
self._read_timer.stop()
|
||||||
|
log.downloads.debug("buffer: {} bytes".format(self._buffer.tell()))
|
||||||
|
self._buffer.seek(0)
|
||||||
|
shutil.copyfileobj(self._buffer, fileobj)
|
||||||
|
self._buffer.close()
|
||||||
|
if self._reply.isFinished():
|
||||||
|
# Downloading to the buffer in RAM has already finished so we
|
||||||
|
# write out the data and clean up now.
|
||||||
|
self._on_reply_finished()
|
||||||
|
else:
|
||||||
|
# Since the buffer already might be full, on_ready_read might
|
||||||
|
# not be called at all anymore, so we force it here to flush
|
||||||
|
# the buffer and continue receiving new data.
|
||||||
|
self._on_ready_read()
|
||||||
|
except OSError as e:
|
||||||
|
self._die(e.strerror)
|
||||||
|
|
||||||
|
def _set_tempfile(self, fileobj):
|
||||||
|
self._set_fileobj(fileobj)
|
||||||
|
|
||||||
|
def _finish_download(self):
|
||||||
|
"""Write buffered data to disk and finish the QNetworkReply."""
|
||||||
|
log.downloads.debug("Finishing download...")
|
||||||
|
if self._reply.isOpen():
|
||||||
|
self.fileobj.write(self._reply.readAll())
|
||||||
|
if self._autoclose:
|
||||||
|
self.fileobj.close()
|
||||||
|
self.successful = self._reply.error() == QNetworkReply.NoError
|
||||||
|
self._reply.close()
|
||||||
|
self._reply.deleteLater()
|
||||||
|
self._reply = None
|
||||||
|
self.finished.emit()
|
||||||
|
self.done = True
|
||||||
|
log.downloads.debug("Download {} finished".format(self.basename))
|
||||||
|
self.data_changed.emit()
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def _on_reply_finished(self):
|
||||||
|
"""Clean up when the download was finished.
|
||||||
|
|
||||||
|
Note when this gets called, only the QNetworkReply has finished. This
|
||||||
|
doesn't mean the download (i.e. writing data to the disk) is finished
|
||||||
|
as well. Therefore, we can't close() the QNetworkReply in here yet.
|
||||||
|
"""
|
||||||
|
if self._reply is None:
|
||||||
|
return
|
||||||
|
self._read_timer.stop()
|
||||||
|
self.stats.finish()
|
||||||
|
is_redirected = self._handle_redirect()
|
||||||
|
if is_redirected:
|
||||||
|
return
|
||||||
|
log.downloads.debug("Reply finished, fileobj {}".format(self.fileobj))
|
||||||
|
if self.fileobj is not None:
|
||||||
|
# We can do a "delayed" write immediately to empty the buffer and
|
||||||
|
# clean up.
|
||||||
|
self._finish_download()
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def _on_ready_read(self):
|
||||||
|
"""Read available data and save file when ready to read."""
|
||||||
|
if self.fileobj is None or self._reply is None:
|
||||||
|
# No filename has been set yet (so we don't empty the buffer) or we
|
||||||
|
# got a readyRead after the reply was finished (which happens on
|
||||||
|
# qute:log for example).
|
||||||
|
return
|
||||||
|
if not self._reply.isOpen():
|
||||||
|
raise OSError("Reply is closed!")
|
||||||
|
try:
|
||||||
|
self.fileobj.write(self._reply.readAll())
|
||||||
|
except OSError as e:
|
||||||
|
self._die(e.strerror)
|
||||||
|
|
||||||
|
@pyqtSlot('QNetworkReply::NetworkError')
|
||||||
|
def _on_reply_error(self, code):
|
||||||
|
"""Handle QNetworkReply errors."""
|
||||||
|
if code == QNetworkReply.OperationCanceledError:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
self._die(self._reply.errorString())
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def _on_read_timer_timeout(self):
|
||||||
|
"""Read some bytes from the QNetworkReply periodically."""
|
||||||
|
if not self._reply.isOpen():
|
||||||
|
raise OSError("Reply is closed!")
|
||||||
|
data = self._reply.read(1024)
|
||||||
|
if data is not None:
|
||||||
|
self._buffer.write(data)
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def _on_meta_data_changed(self):
|
||||||
|
"""Update the download's metadata."""
|
||||||
|
if self._reply is None:
|
||||||
|
return
|
||||||
|
self.raw_headers = {}
|
||||||
|
for key, value in self._reply.rawHeaderPairs():
|
||||||
|
self.raw_headers[bytes(key)] = bytes(value)
|
||||||
|
|
||||||
|
def _handle_redirect(self):
|
||||||
|
"""Handle an HTTP redirect.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
True if the download was redirected, False otherwise.
|
||||||
|
"""
|
||||||
|
redirect = self._reply.attribute(
|
||||||
|
QNetworkRequest.RedirectionTargetAttribute)
|
||||||
|
if redirect is None or redirect.isEmpty():
|
||||||
|
return False
|
||||||
|
new_url = self._reply.url().resolved(redirect)
|
||||||
|
new_request = self._reply.request()
|
||||||
|
if new_url == new_request.url():
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self._redirects > self._MAX_REDIRECTS:
|
||||||
|
self._die("Maximum redirection count reached!")
|
||||||
|
self.delete()
|
||||||
|
return True # so on_reply_finished aborts
|
||||||
|
|
||||||
|
log.downloads.debug("{}: Handling redirect".format(self))
|
||||||
|
self._redirects += 1
|
||||||
|
new_request.setUrl(new_url)
|
||||||
|
old_reply = self._reply
|
||||||
|
old_reply.finished.disconnect(self._on_reply_finished)
|
||||||
|
self._read_timer.stop()
|
||||||
|
self._reply = None
|
||||||
|
if self.fileobj is not None:
|
||||||
|
self.fileobj.seek(0)
|
||||||
|
|
||||||
|
log.downloads.debug("redirected: {} -> {}".format(
|
||||||
|
old_reply.url(), new_request.url()))
|
||||||
|
new_reply = old_reply.manager().get(new_request)
|
||||||
|
self._init_reply(new_reply)
|
||||||
|
|
||||||
|
old_reply.deleteLater()
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class DownloadManager(downloads.AbstractDownloadManager):
|
||||||
|
|
||||||
|
"""Manager for currently running downloads.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
_networkmanager: A NetworkManager for generic downloads.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, win_id, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self._networkmanager = networkmanager.NetworkManager(
|
||||||
|
win_id, None, self)
|
||||||
|
|
||||||
|
@pyqtSlot('QUrl')
|
||||||
|
def get(self, url, **kwargs):
|
||||||
|
"""Start a download with a link URL.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url: The URL to get, as QUrl
|
||||||
|
**kwargs: passed to get_request().
|
||||||
|
|
||||||
|
Return:
|
||||||
|
The created DownloadItem.
|
||||||
|
"""
|
||||||
|
if not url.isValid():
|
||||||
|
urlutils.invalid_url_error(url, "start download")
|
||||||
|
return
|
||||||
|
req = QNetworkRequest(url)
|
||||||
|
return self.get_request(req, **kwargs)
|
||||||
|
|
||||||
|
def get_request(self, request, *, target=None, **kwargs):
|
||||||
|
"""Start a download with a QNetworkRequest.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: The QNetworkRequest to download.
|
||||||
|
target: Where to save the download as downloads.DownloadTarget.
|
||||||
|
**kwargs: Passed to _fetch_request.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
The created DownloadItem.
|
||||||
|
"""
|
||||||
|
# WORKAROUND for Qt corrupting data loaded from cache:
|
||||||
|
# https://bugreports.qt.io/browse/QTBUG-42757
|
||||||
|
request.setAttribute(QNetworkRequest.CacheLoadControlAttribute,
|
||||||
|
QNetworkRequest.AlwaysNetwork)
|
||||||
|
|
||||||
|
if request.url().scheme().lower() != 'data':
|
||||||
|
suggested_fn = urlutils.filename_from_url(request.url())
|
||||||
|
else:
|
||||||
|
# We might be downloading a binary blob embedded on a page or even
|
||||||
|
# generated dynamically via javascript. We try to figure out a more
|
||||||
|
# sensible name than the base64 content of the data.
|
||||||
|
origin = request.originatingObject()
|
||||||
|
try:
|
||||||
|
origin_url = origin.url()
|
||||||
|
except AttributeError:
|
||||||
|
# Raised either if origin is None or some object that doesn't
|
||||||
|
# have its own url. We're probably fine with a default fallback
|
||||||
|
# then.
|
||||||
|
suggested_fn = 'binary blob'
|
||||||
|
else:
|
||||||
|
# Use the originating URL as a base for the filename (works
|
||||||
|
# e.g. for pdf.js).
|
||||||
|
suggested_fn = urlutils.filename_from_url(origin_url)
|
||||||
|
|
||||||
|
if suggested_fn is None:
|
||||||
|
suggested_fn = 'qutebrowser-download'
|
||||||
|
|
||||||
|
return self._fetch_request(request,
|
||||||
|
target=target,
|
||||||
|
suggested_filename=suggested_fn,
|
||||||
|
**kwargs)
|
||||||
|
|
||||||
|
def _fetch_request(self, request, **kwargs):
|
||||||
|
"""Download a QNetworkRequest to disk.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: The QNetworkRequest to download.
|
||||||
|
**kwargs: passed to fetch().
|
||||||
|
|
||||||
|
Return:
|
||||||
|
The created DownloadItem.
|
||||||
|
"""
|
||||||
|
reply = self._networkmanager.get(request)
|
||||||
|
return self.fetch(reply, **kwargs)
|
||||||
|
|
||||||
|
@pyqtSlot('QNetworkReply')
|
||||||
|
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.
|
||||||
|
target: Where to save the download as downloads.DownloadTarget.
|
||||||
|
auto_remove: Whether to remove the download even if
|
||||||
|
ui -> remove-finished-downloads is set to -1.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
The created DownloadItem.
|
||||||
|
"""
|
||||||
|
if not suggested_filename:
|
||||||
|
try:
|
||||||
|
suggested_filename = target.suggested_filename()
|
||||||
|
except downloads.NoFilenameError:
|
||||||
|
_, suggested_filename = http.parse_content_disposition(reply)
|
||||||
|
log.downloads.debug("fetch: {} -> {}".format(reply.url(),
|
||||||
|
suggested_filename))
|
||||||
|
download = DownloadItem(reply, manager=self)
|
||||||
|
self._init_item(download, auto_remove, suggested_filename)
|
||||||
|
|
||||||
|
if target is not None:
|
||||||
|
download.set_target(target)
|
||||||
|
return download
|
||||||
|
|
||||||
|
# Neither filename nor fileobj were given
|
||||||
|
|
||||||
|
filename = downloads.immediate_download_path(prompt_download_directory)
|
||||||
|
if filename is not None:
|
||||||
|
# User doesn't want to be asked, so just use the download_dir
|
||||||
|
target = downloads.FileDownloadTarget(filename)
|
||||||
|
download.set_target(target)
|
||||||
|
return download
|
||||||
|
|
||||||
|
# Ask the user for a filename
|
||||||
|
question = downloads.get_filename_question(
|
||||||
|
suggested_filename=suggested_filename, url=reply.url(),
|
||||||
|
parent=self)
|
||||||
|
self._init_filename_question(question, download)
|
||||||
|
message.global_bridge.ask(question, blocking=False)
|
||||||
|
|
||||||
|
return download
|
158
qutebrowser/browser/webengine/webenginedownloads.py
Normal file
158
qutebrowser/browser/webengine/webenginedownloads.py
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
# 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/>.
|
||||||
|
|
||||||
|
"""QtWebEngine specific code for downloads."""
|
||||||
|
|
||||||
|
import os.path
|
||||||
|
import functools
|
||||||
|
|
||||||
|
from PyQt5.QtCore import pyqtSlot, Qt
|
||||||
|
# pylint: disable=no-name-in-module,import-error,useless-suppression
|
||||||
|
from PyQt5.QtWebEngineWidgets import QWebEngineDownloadItem
|
||||||
|
# pylint: enable=no-name-in-module,import-error,useless-suppression
|
||||||
|
|
||||||
|
from qutebrowser.browser import downloads
|
||||||
|
from qutebrowser.utils import debug, usertypes, message, log
|
||||||
|
|
||||||
|
|
||||||
|
class DownloadItem(downloads.AbstractDownloadItem):
|
||||||
|
|
||||||
|
"""A wrapper over a QWebEngineDownloadItem.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
_qt_item: The wrapped item.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, qt_item, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self._qt_item = qt_item
|
||||||
|
qt_item.downloadProgress.connect(self.stats.on_download_progress)
|
||||||
|
qt_item.stateChanged.connect(self._on_state_changed)
|
||||||
|
|
||||||
|
@pyqtSlot(QWebEngineDownloadItem.DownloadState)
|
||||||
|
def _on_state_changed(self, state):
|
||||||
|
state_name = debug.qenum_key(QWebEngineDownloadItem, state)
|
||||||
|
log.downloads.debug("State for {!r} changed to {}".format(
|
||||||
|
self, state_name))
|
||||||
|
|
||||||
|
if state == QWebEngineDownloadItem.DownloadRequested:
|
||||||
|
pass
|
||||||
|
elif state == QWebEngineDownloadItem.DownloadInProgress:
|
||||||
|
pass
|
||||||
|
elif state == QWebEngineDownloadItem.DownloadCompleted:
|
||||||
|
log.downloads.debug("Download {} finished".format(self.basename))
|
||||||
|
self.successful = True
|
||||||
|
self.done = True
|
||||||
|
self.finished.emit()
|
||||||
|
self.stats.finish()
|
||||||
|
elif state == QWebEngineDownloadItem.DownloadCancelled:
|
||||||
|
self.successful = False
|
||||||
|
self.done = True
|
||||||
|
self.cancelled.emit()
|
||||||
|
self.stats.finish()
|
||||||
|
elif state == QWebEngineDownloadItem.DownloadInterrupted:
|
||||||
|
self.successful = False
|
||||||
|
self.done = True
|
||||||
|
# https://bugreports.qt.io/browse/QTBUG-56839
|
||||||
|
self.error.emit("Download failed")
|
||||||
|
self.stats.finish()
|
||||||
|
else:
|
||||||
|
raise ValueError("_on_state_changed was called with unknown state "
|
||||||
|
"{}".format(state_name))
|
||||||
|
|
||||||
|
def _do_die(self):
|
||||||
|
self._qt_item.downloadProgress.disconnect()
|
||||||
|
self._qt_item.cancel()
|
||||||
|
|
||||||
|
def _do_cancel(self):
|
||||||
|
self._qt_item.cancel()
|
||||||
|
|
||||||
|
def retry(self):
|
||||||
|
# https://bugreports.qt.io/browse/QTBUG-56840
|
||||||
|
raise downloads.UnsupportedOperationError
|
||||||
|
|
||||||
|
def _get_open_filename(self):
|
||||||
|
return self._filename
|
||||||
|
|
||||||
|
def _set_fileobj(self, fileobj):
|
||||||
|
raise downloads.UnsupportedOperationError
|
||||||
|
|
||||||
|
def _set_tempfile(self, fileobj):
|
||||||
|
self._set_filename(fileobj.name, force_overwrite=True)
|
||||||
|
|
||||||
|
def _ensure_can_set_filename(self, filename):
|
||||||
|
state = self._qt_item.state()
|
||||||
|
if state != QWebEngineDownloadItem.DownloadRequested:
|
||||||
|
state_name = debug.qenum_key(QWebEngineDownloadItem, state)
|
||||||
|
raise ValueError("Trying to set filename {} on {!r} which is "
|
||||||
|
"state {} (not in requested state)!".format(
|
||||||
|
filename, self, state_name))
|
||||||
|
|
||||||
|
def _ask_confirm_question(self, title, msg):
|
||||||
|
no_action = functools.partial(self.cancel, remove_data=False)
|
||||||
|
question = usertypes.Question()
|
||||||
|
question.title = title
|
||||||
|
question.text = msg
|
||||||
|
question.mode = usertypes.PromptMode.yesno
|
||||||
|
question.answered_yes.connect(self._after_set_filename)
|
||||||
|
question.answered_no.connect(no_action)
|
||||||
|
question.cancelled.connect(no_action)
|
||||||
|
self.cancelled.connect(question.abort)
|
||||||
|
self.error.connect(question.abort)
|
||||||
|
message.global_bridge.ask(question, blocking=True)
|
||||||
|
|
||||||
|
def _after_set_filename(self):
|
||||||
|
self._qt_item.setPath(self._filename)
|
||||||
|
self._qt_item.accept()
|
||||||
|
|
||||||
|
|
||||||
|
class DownloadManager(downloads.AbstractDownloadManager):
|
||||||
|
|
||||||
|
"""Manager for currently running downloads."""
|
||||||
|
|
||||||
|
def install(self, profile):
|
||||||
|
"""Set up the download manager on a QWebEngineProfile."""
|
||||||
|
profile.downloadRequested.connect(self.handle_download,
|
||||||
|
Qt.DirectConnection)
|
||||||
|
|
||||||
|
@pyqtSlot(QWebEngineDownloadItem)
|
||||||
|
def handle_download(self, qt_item):
|
||||||
|
"""Start a download coming from a QWebEngineProfile."""
|
||||||
|
suggested_filename = os.path.basename(qt_item.path())
|
||||||
|
|
||||||
|
download = DownloadItem(qt_item)
|
||||||
|
self._init_item(download, auto_remove=False,
|
||||||
|
suggested_filename=suggested_filename)
|
||||||
|
|
||||||
|
filename = downloads.immediate_download_path()
|
||||||
|
if filename is not None:
|
||||||
|
# User doesn't want to be asked, so just use the download_dir
|
||||||
|
target = downloads.FileDownloadTarget(filename)
|
||||||
|
download.set_target(target)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Ask the user for a filename - needs to be blocking!
|
||||||
|
question = downloads.get_filename_question(
|
||||||
|
suggested_filename=suggested_filename, url=qt_item.url(),
|
||||||
|
parent=self)
|
||||||
|
self._init_filename_question(question, download)
|
||||||
|
|
||||||
|
message.global_bridge.ask(question, blocking=True)
|
||||||
|
# The filename is set via the question.answered signal, connected in
|
||||||
|
# _init_filename_question.
|
@ -34,7 +34,8 @@ from PyQt5.QtWebEngineWidgets import (QWebEnginePage, QWebEngineScript,
|
|||||||
|
|
||||||
from qutebrowser.browser import browsertab, mouse
|
from qutebrowser.browser import browsertab, mouse
|
||||||
from qutebrowser.browser.webengine import (webview, webengineelem, tabhistory,
|
from qutebrowser.browser.webengine import (webview, webengineelem, tabhistory,
|
||||||
interceptor, webenginequtescheme)
|
interceptor, webenginequtescheme,
|
||||||
|
webenginedownloads)
|
||||||
from qutebrowser.utils import (usertypes, qtutils, log, javascript, utils,
|
from qutebrowser.utils import (usertypes, qtutils, log, javascript, utils,
|
||||||
objreg)
|
objreg)
|
||||||
|
|
||||||
@ -61,6 +62,11 @@ def init():
|
|||||||
host_blocker, parent=app)
|
host_blocker, parent=app)
|
||||||
req_interceptor.install(profile)
|
req_interceptor.install(profile)
|
||||||
|
|
||||||
|
log.init.debug("Initializing QtWebEngine downloads...")
|
||||||
|
download_manager = webenginedownloads.DownloadManager(parent=app)
|
||||||
|
download_manager.install(profile)
|
||||||
|
objreg.register('webengine-download-manager', download_manager)
|
||||||
|
|
||||||
|
|
||||||
# Mapping worlds from usertypes.JsWorld to QWebEngineScript world IDs.
|
# Mapping worlds from usertypes.JsWorld to QWebEngineScript world IDs.
|
||||||
_JS_WORLD_MAP = {
|
_JS_WORLD_MAP = {
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
"""Utils for writing an MHTML file."""
|
"""Utils for writing an MHTML file."""
|
||||||
|
|
||||||
|
import html
|
||||||
import functools
|
import functools
|
||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
@ -34,7 +35,8 @@ import email.message
|
|||||||
|
|
||||||
from PyQt5.QtCore import QUrl
|
from PyQt5.QtCore import QUrl
|
||||||
|
|
||||||
from qutebrowser.browser.webkit import webkitelem, downloads
|
from qutebrowser.browser import downloads
|
||||||
|
from qutebrowser.browser.webkit import webkitelem
|
||||||
from qutebrowser.utils import log, objreg, message, usertypes, utils, urlutils
|
from qutebrowser.utils import log, objreg, message, usertypes, utils, urlutils
|
||||||
|
|
||||||
_File = collections.namedtuple('_File',
|
_File = collections.namedtuple('_File',
|
||||||
@ -341,9 +343,9 @@ class _Downloader:
|
|||||||
self.writer.add_file(urlutils.encoded_url(url), b'')
|
self.writer.add_file(urlutils.encoded_url(url), b'')
|
||||||
return
|
return
|
||||||
|
|
||||||
download_manager = objreg.get('download-manager', scope='window',
|
download_manager = objreg.get('qtnetwork-download-manager',
|
||||||
window=self._win_id)
|
scope='window', window=self._win_id)
|
||||||
target = usertypes.FileObjDownloadTarget(_NoCloseBytesIO())
|
target = downloads.FileObjDownloadTarget(_NoCloseBytesIO())
|
||||||
item = download_manager.get(url, target=target,
|
item = download_manager.get(url, target=target,
|
||||||
auto_remove=True)
|
auto_remove=True)
|
||||||
self.pending_downloads.add((url, item))
|
self.pending_downloads.add((url, item))
|
||||||
@ -536,10 +538,10 @@ def start_download_checked(dest, tab):
|
|||||||
|
|
||||||
q = usertypes.Question()
|
q = usertypes.Question()
|
||||||
q.mode = usertypes.PromptMode.yesno
|
q.mode = usertypes.PromptMode.yesno
|
||||||
q.text = "{} exists. Overwrite?".format(path)
|
q.title = "Overwrite existing file?"
|
||||||
|
q.text = "<b>{}</b> already exists. Overwrite?".format(
|
||||||
|
html.escape(path))
|
||||||
q.completed.connect(q.deleteLater)
|
q.completed.connect(q.deleteLater)
|
||||||
q.answered_yes.connect(functools.partial(
|
q.answered_yes.connect(functools.partial(
|
||||||
_start_download, path, tab=tab))
|
_start_download, path, tab=tab))
|
||||||
message_bridge = objreg.get('message-bridge', scope='window',
|
message.global_bridge.ask(q, blocking=False)
|
||||||
window=tab.win_id)
|
|
||||||
message_bridge.ask(q, blocking=False)
|
|
||||||
|
@ -135,11 +135,6 @@ class NetworkManager(QNetworkAccessManager):
|
|||||||
"""Our own QNetworkAccessManager.
|
"""Our own QNetworkAccessManager.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
adopted_downloads: If downloads are running with this QNAM but the
|
|
||||||
associated tab gets closed already, the NAM gets
|
|
||||||
reparented to the DownloadManager. This counts the
|
|
||||||
still running downloads, so the QNAM can clean
|
|
||||||
itself up when this reaches zero again.
|
|
||||||
_requests: Pending requests.
|
_requests: Pending requests.
|
||||||
_scheme_handlers: A dictionary (scheme -> handler) of supported custom
|
_scheme_handlers: A dictionary (scheme -> handler) of supported custom
|
||||||
schemes.
|
schemes.
|
||||||
@ -161,7 +156,6 @@ class NetworkManager(QNetworkAccessManager):
|
|||||||
# http://www.riverbankcomputing.com/pipermail/pyqt/2014-November/035045.html
|
# http://www.riverbankcomputing.com/pipermail/pyqt/2014-November/035045.html
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
log.init.debug("NetworkManager init done")
|
log.init.debug("NetworkManager init done")
|
||||||
self.adopted_downloads = 0
|
|
||||||
self._win_id = win_id
|
self._win_id = win_id
|
||||||
self._tab_id = tab_id
|
self._tab_id = tab_id
|
||||||
self._requests = []
|
self._requests = []
|
||||||
@ -394,28 +388,6 @@ class NetworkManager(QNetworkAccessManager):
|
|||||||
# switched from private mode to normal mode
|
# switched from private mode to normal mode
|
||||||
self._set_cookiejar()
|
self._set_cookiejar()
|
||||||
|
|
||||||
@pyqtSlot()
|
|
||||||
def on_adopted_download_destroyed(self):
|
|
||||||
"""Check if we can clean up if an adopted download was destroyed.
|
|
||||||
|
|
||||||
See the description for adopted_downloads for details.
|
|
||||||
"""
|
|
||||||
self.adopted_downloads -= 1
|
|
||||||
log.downloads.debug("Adopted download destroyed, {} left.".format(
|
|
||||||
self.adopted_downloads))
|
|
||||||
assert self.adopted_downloads >= 0
|
|
||||||
if self.adopted_downloads == 0:
|
|
||||||
self.deleteLater()
|
|
||||||
|
|
||||||
@pyqtSlot(object) # DownloadItem
|
|
||||||
def adopt_download(self, download):
|
|
||||||
"""Adopt a new DownloadItem."""
|
|
||||||
self.adopted_downloads += 1
|
|
||||||
log.downloads.debug("Adopted download, {} adopted.".format(
|
|
||||||
self.adopted_downloads))
|
|
||||||
download.destroyed.connect(self.on_adopted_download_destroyed)
|
|
||||||
download.do_retry.connect(self.adopt_download)
|
|
||||||
|
|
||||||
def set_referer(self, req, current_url):
|
def set_referer(self, req, current_url):
|
||||||
"""Set the referer header."""
|
"""Set the referer header."""
|
||||||
referer_header_conf = config.get('network', 'referer-header')
|
referer_header_conf = config.get('network', 'referer-header')
|
||||||
|
@ -219,13 +219,7 @@ class BrowserPage(QWebPage):
|
|||||||
"""Prepare the web page for being deleted."""
|
"""Prepare the web page for being deleted."""
|
||||||
self._is_shutting_down = True
|
self._is_shutting_down = True
|
||||||
self.shutting_down.emit()
|
self.shutting_down.emit()
|
||||||
download_manager = objreg.get('download-manager', scope='window',
|
self.networkAccessManager().shutdown()
|
||||||
window=self._win_id)
|
|
||||||
nam = self.networkAccessManager()
|
|
||||||
if download_manager.has_downloads_with_nam(nam):
|
|
||||||
nam.setParent(download_manager)
|
|
||||||
else:
|
|
||||||
nam.shutdown()
|
|
||||||
|
|
||||||
def display_content(self, reply, mimetype):
|
def display_content(self, reply, mimetype):
|
||||||
"""Display a QNetworkReply with an explicitly set mimetype."""
|
"""Display a QNetworkReply with an explicitly set mimetype."""
|
||||||
@ -252,9 +246,9 @@ class BrowserPage(QWebPage):
|
|||||||
after this slot returns.
|
after this slot returns.
|
||||||
"""
|
"""
|
||||||
req = QNetworkRequest(request)
|
req = QNetworkRequest(request)
|
||||||
download_manager = objreg.get('download-manager', scope='window',
|
download_manager = objreg.get('qtnetwork-download-manager',
|
||||||
window=self._win_id)
|
scope='window', window=self._win_id)
|
||||||
download_manager.get_request(req, qnam=self.networkAccessManager())
|
download_manager.get_request(req)
|
||||||
|
|
||||||
@pyqtSlot('QNetworkReply*')
|
@pyqtSlot('QNetworkReply*')
|
||||||
def on_unsupported_content(self, reply):
|
def on_unsupported_content(self, reply):
|
||||||
@ -267,8 +261,8 @@ class BrowserPage(QWebPage):
|
|||||||
here: http://mimesniff.spec.whatwg.org/
|
here: http://mimesniff.spec.whatwg.org/
|
||||||
"""
|
"""
|
||||||
inline, suggested_filename = http.parse_content_disposition(reply)
|
inline, suggested_filename = http.parse_content_disposition(reply)
|
||||||
download_manager = objreg.get('download-manager', scope='window',
|
download_manager = objreg.get('qtnetwork-download-manager',
|
||||||
window=self._win_id)
|
scope='window', window=self._win_id)
|
||||||
if not inline:
|
if not inline:
|
||||||
# Content-Disposition: attachment -> force download
|
# Content-Disposition: attachment -> force download
|
||||||
download_manager.fetch(reply,
|
download_manager.fetch(reply,
|
||||||
|
@ -29,7 +29,7 @@ from qutebrowser.utils import message, log, objreg, standarddir
|
|||||||
from qutebrowser.commands import runners
|
from qutebrowser.commands import runners
|
||||||
from qutebrowser.config import config
|
from qutebrowser.config import config
|
||||||
from qutebrowser.misc import guiprocess
|
from qutebrowser.misc import guiprocess
|
||||||
from qutebrowser.browser.webkit import downloads
|
from qutebrowser.browser import downloads
|
||||||
|
|
||||||
|
|
||||||
class _QtFIFOReader(QObject):
|
class _QtFIFOReader(QObject):
|
||||||
|
@ -413,7 +413,20 @@ class ConfigManager(QObject):
|
|||||||
CHANGED_OPTIONS = {
|
CHANGED_OPTIONS = {
|
||||||
('content', 'cookies-accept'):
|
('content', 'cookies-accept'):
|
||||||
_get_value_transformer({'default': 'no-3rdparty'}),
|
_get_value_transformer({'default': 'no-3rdparty'}),
|
||||||
|
('tabs', 'new-tab-position'):
|
||||||
|
_get_value_transformer({
|
||||||
|
'left': 'prev',
|
||||||
|
'right': 'next'}),
|
||||||
|
('tabs', 'new-tab-position-explicit'):
|
||||||
|
_get_value_transformer({
|
||||||
|
'left': 'prev',
|
||||||
|
'right': 'next'}),
|
||||||
('tabs', 'position'): _transform_position,
|
('tabs', 'position'): _transform_position,
|
||||||
|
('tabs', 'select-on-remove'):
|
||||||
|
_get_value_transformer({
|
||||||
|
'left': 'prev',
|
||||||
|
'right': 'next',
|
||||||
|
'previous': 'last-used'}),
|
||||||
('ui', 'downloads-position'): _transform_position,
|
('ui', 'downloads-position'): _transform_position,
|
||||||
('ui', 'remove-finished-downloads'):
|
('ui', 'remove-finished-downloads'):
|
||||||
_get_value_transformer({'false': '-1', 'true': '1000'}),
|
_get_value_transformer({'false': '-1', 'true': '1000'}),
|
||||||
|
@ -584,11 +584,11 @@ def data(readonly=False):
|
|||||||
"background."),
|
"background."),
|
||||||
|
|
||||||
('select-on-remove',
|
('select-on-remove',
|
||||||
SettingValue(typ.SelectOnRemove(), 'right'),
|
SettingValue(typ.SelectOnRemove(), 'next'),
|
||||||
"Which tab to select when the focused tab is removed."),
|
"Which tab to select when the focused tab is removed."),
|
||||||
|
|
||||||
('new-tab-position',
|
('new-tab-position',
|
||||||
SettingValue(typ.NewTabPosition(), 'right'),
|
SettingValue(typ.NewTabPosition(), 'next'),
|
||||||
"How new tabs are positioned."),
|
"How new tabs are positioned."),
|
||||||
|
|
||||||
('new-tab-position-explicit',
|
('new-tab-position-explicit',
|
||||||
@ -1814,4 +1814,14 @@ CHANGED_KEY_COMMANDS = [
|
|||||||
|
|
||||||
(re.compile(r'^prompt-yes$'), r'prompt-accept yes'),
|
(re.compile(r'^prompt-yes$'), r'prompt-accept yes'),
|
||||||
(re.compile(r'^prompt-no$'), r'prompt-accept no'),
|
(re.compile(r'^prompt-no$'), r'prompt-accept no'),
|
||||||
|
|
||||||
|
(re.compile(r'^tab-close -l$'), r'tab-close --prev'),
|
||||||
|
(re.compile(r'^tab-close --left$'), r'tab-close --prev'),
|
||||||
|
(re.compile(r'^tab-close -r$'), r'tab-close --next'),
|
||||||
|
(re.compile(r'^tab-close --right$'), r'tab-close --next'),
|
||||||
|
|
||||||
|
(re.compile(r'^tab-only -l$'), r'tab-only --prev'),
|
||||||
|
(re.compile(r'^tab-only --left$'), r'tab-only --prev'),
|
||||||
|
(re.compile(r'^tab-only -r$'), r'tab-only --next'),
|
||||||
|
(re.compile(r'^tab-only --right$'), r'tab-only --next'),
|
||||||
]
|
]
|
||||||
|
@ -1379,18 +1379,20 @@ class SelectOnRemove(MappingType):
|
|||||||
"""Which tab to select when the focused tab is removed."""
|
"""Which tab to select when the focused tab is removed."""
|
||||||
|
|
||||||
MAPPING = {
|
MAPPING = {
|
||||||
'left': QTabBar.SelectLeftTab,
|
'prev': QTabBar.SelectLeftTab,
|
||||||
'right': QTabBar.SelectRightTab,
|
'next': QTabBar.SelectRightTab,
|
||||||
'previous': QTabBar.SelectPreviousTab,
|
'last-used': QTabBar.SelectPreviousTab,
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, none_ok=False):
|
def __init__(self, none_ok=False):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
none_ok,
|
none_ok,
|
||||||
valid_values=ValidValues(
|
valid_values=ValidValues(
|
||||||
('left', "Select the tab on the left."),
|
('prev', "Select the tab which came before the closed one "
|
||||||
('right', "Select the tab on the right."),
|
"(left in horizontal, above in vertical)."),
|
||||||
('previous', "Select the previously selected tab.")))
|
('next', "Select the tab which came after the closed one "
|
||||||
|
"(right in horizontal, below in vertical)."),
|
||||||
|
('last-used', "Select the previously selected tab.")))
|
||||||
|
|
||||||
|
|
||||||
class ConfirmQuit(FlagList):
|
class ConfirmQuit(FlagList):
|
||||||
@ -1434,10 +1436,10 @@ class NewTabPosition(BaseType):
|
|||||||
def __init__(self, none_ok=False):
|
def __init__(self, none_ok=False):
|
||||||
super().__init__(none_ok)
|
super().__init__(none_ok)
|
||||||
self.valid_values = ValidValues(
|
self.valid_values = ValidValues(
|
||||||
('left', "On the left of the current tab."),
|
('prev', "Before the current tab."),
|
||||||
('right', "On the right of the current tab."),
|
('next', "After the current tab."),
|
||||||
('first', "At the left end."),
|
('first', "At the beginning."),
|
||||||
('last', "At the right end."))
|
('last', "At the end."))
|
||||||
|
|
||||||
|
|
||||||
class IgnoreCase(Bool):
|
class IgnoreCase(Bool):
|
||||||
|
@ -35,8 +35,8 @@ from qutebrowser.mainwindow import tabbedbrowser, messageview, prompt
|
|||||||
from qutebrowser.mainwindow.statusbar import bar
|
from qutebrowser.mainwindow.statusbar import bar
|
||||||
from qutebrowser.completion import completionwidget, completer
|
from qutebrowser.completion import completionwidget, completer
|
||||||
from qutebrowser.keyinput import modeman
|
from qutebrowser.keyinput import modeman
|
||||||
from qutebrowser.browser import commands, downloadview, hints
|
from qutebrowser.browser import (commands, downloadview, hints,
|
||||||
from qutebrowser.browser.webkit import downloads
|
qtnetworkdownloads, downloads)
|
||||||
from qutebrowser.misc import crashsignal, keyhintwidget
|
from qutebrowser.misc import crashsignal, keyhintwidget
|
||||||
|
|
||||||
|
|
||||||
@ -258,10 +258,20 @@ class MainWindow(QWidget):
|
|||||||
|
|
||||||
def _init_downloadmanager(self):
|
def _init_downloadmanager(self):
|
||||||
log.init.debug("Initializing downloads...")
|
log.init.debug("Initializing downloads...")
|
||||||
download_manager = downloads.DownloadManager(self.win_id, self)
|
qtnetwork_download_manager = qtnetworkdownloads.DownloadManager(
|
||||||
objreg.register('download-manager', download_manager, scope='window',
|
self.win_id, self)
|
||||||
window=self.win_id)
|
objreg.register('qtnetwork-download-manager',
|
||||||
download_model = downloads.DownloadModel(download_manager)
|
qtnetwork_download_manager,
|
||||||
|
scope='window', window=self.win_id)
|
||||||
|
|
||||||
|
try:
|
||||||
|
webengine_download_manager = objreg.get(
|
||||||
|
'webengine-download-manager')
|
||||||
|
except KeyError:
|
||||||
|
webengine_download_manager = None
|
||||||
|
|
||||||
|
download_model = downloads.DownloadModel(qtnetwork_download_manager,
|
||||||
|
webengine_download_manager)
|
||||||
objreg.register('download-model', download_model, scope='window',
|
objreg.register('download-model', download_model, scope='window',
|
||||||
window=self.win_id)
|
window=self.win_id)
|
||||||
|
|
||||||
|
@ -29,6 +29,7 @@ from PyQt5.QtCore import (pyqtSlot, pyqtSignal, Qt, QTimer, QDir, QModelIndex,
|
|||||||
from PyQt5.QtWidgets import (QWidget, QGridLayout, QVBoxLayout, QLineEdit,
|
from PyQt5.QtWidgets import (QWidget, QGridLayout, QVBoxLayout, QLineEdit,
|
||||||
QLabel, QFileSystemModel, QTreeView, QSizePolicy)
|
QLabel, QFileSystemModel, QTreeView, QSizePolicy)
|
||||||
|
|
||||||
|
from qutebrowser.browser import downloads
|
||||||
from qutebrowser.config import style
|
from qutebrowser.config import style
|
||||||
from qutebrowser.utils import usertypes, log, utils, qtutils, objreg, message
|
from qutebrowser.utils import usertypes, log, utils, qtutils, objreg, message
|
||||||
from qutebrowser.keyinput import modeman
|
from qutebrowser.keyinput import modeman
|
||||||
@ -687,11 +688,11 @@ class DownloadFilenamePrompt(FilenamePrompt):
|
|||||||
|
|
||||||
def accept(self, value=None):
|
def accept(self, value=None):
|
||||||
text = value if value is not None else self._lineedit.text()
|
text = value if value is not None else self._lineedit.text()
|
||||||
self.question.answer = usertypes.FileDownloadTarget(text)
|
self.question.answer = downloads.FileDownloadTarget(text)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def download_open(self, cmdline):
|
def download_open(self, cmdline):
|
||||||
self.question.answer = usertypes.OpenFileDownloadTarget(cmdline)
|
self.question.answer = downloads.OpenFileDownloadTarget(cmdline)
|
||||||
self.question.done()
|
self.question.done()
|
||||||
message.global_bridge.prompt_done.emit(self.KEY_MODE)
|
message.global_bridge.prompt_done.emit(self.KEY_MODE)
|
||||||
|
|
||||||
|
@ -61,8 +61,8 @@ class TabbedBrowser(tabwidget.TabWidget):
|
|||||||
_filter: A SignalFilter instance.
|
_filter: A SignalFilter instance.
|
||||||
_now_focused: The tab which is focused now.
|
_now_focused: The tab which is focused now.
|
||||||
_tab_insert_idx_left: Where to insert a new tab with
|
_tab_insert_idx_left: Where to insert a new tab with
|
||||||
tabbar -> new-tab-position set to 'left'.
|
tabbar -> new-tab-position set to 'prev'.
|
||||||
_tab_insert_idx_right: Same as above, for 'right'.
|
_tab_insert_idx_right: Same as above, for 'next'.
|
||||||
_undo_stack: List of UndoEntry namedtuples of closed tabs.
|
_undo_stack: List of UndoEntry namedtuples of closed tabs.
|
||||||
shutting_down: Whether we're currently shutting down.
|
shutting_down: Whether we're currently shutting down.
|
||||||
_local_marks: Jump markers local to each page
|
_local_marks: Jump markers local to each page
|
||||||
@ -411,14 +411,14 @@ class TabbedBrowser(tabwidget.TabWidget):
|
|||||||
pos = config.get('tabs', 'new-tab-position-explicit')
|
pos = config.get('tabs', 'new-tab-position-explicit')
|
||||||
else:
|
else:
|
||||||
pos = config.get('tabs', 'new-tab-position')
|
pos = config.get('tabs', 'new-tab-position')
|
||||||
if pos == 'left':
|
if pos == 'prev':
|
||||||
idx = self._tab_insert_idx_left
|
idx = self._tab_insert_idx_left
|
||||||
# On first sight, we'd think we have to decrement
|
# On first sight, we'd think we have to decrement
|
||||||
# self._tab_insert_idx_left here, as we want the next tab to be
|
# self._tab_insert_idx_left here, as we want the next tab to be
|
||||||
# *before* the one we just opened. However, since we opened a tab
|
# *before* the one we just opened. However, since we opened a tab
|
||||||
# *to the left* of the currently focused tab, indices will shift by
|
# *before* the currently focused tab, indices will shift by
|
||||||
# 1 automatically.
|
# 1 automatically.
|
||||||
elif pos == 'right':
|
elif pos == 'next':
|
||||||
idx = self._tab_insert_idx_right
|
idx = self._tab_insert_idx_right
|
||||||
self._tab_insert_idx_right += 1
|
self._tab_insert_idx_right += 1
|
||||||
elif pos == 'first':
|
elif pos == 'first':
|
||||||
|
@ -362,7 +362,7 @@ class IPCServer(QObject):
|
|||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def on_timeout(self):
|
def on_timeout(self):
|
||||||
"""Cancel the current connection if it was idle for too long."""
|
"""Cancel the current connection if it was idle for too long."""
|
||||||
if self._socket is None:
|
if self._socket is None: # pragma: no cover
|
||||||
log.ipc.error("on_timeout got called with None socket!")
|
log.ipc.error("on_timeout got called with None socket!")
|
||||||
return
|
return
|
||||||
log.ipc.error("IPC connection timed out "
|
log.ipc.error("IPC connection timed out "
|
||||||
|
@ -23,7 +23,6 @@
|
|||||||
import collections
|
import collections
|
||||||
import functools
|
import functools
|
||||||
|
|
||||||
import sip
|
|
||||||
from PyQt5.QtCore import QObject, QTimer
|
from PyQt5.QtCore import QObject, QTimer
|
||||||
|
|
||||||
from qutebrowser.utils import log
|
from qutebrowser.utils import log
|
||||||
@ -144,8 +143,12 @@ class ObjectRegistry(collections.UserDict):
|
|||||||
"""Dump all objects as a list of strings."""
|
"""Dump all objects as a list of strings."""
|
||||||
lines = []
|
lines = []
|
||||||
for name, obj in self.data.items():
|
for name, obj in self.data.items():
|
||||||
if not (isinstance(obj, QObject) and sip.isdeleted(obj)):
|
try:
|
||||||
lines.append("{}: {}".format(name, repr(obj)))
|
obj_repr = repr(obj)
|
||||||
|
except (RuntimeError, TypeError):
|
||||||
|
# Underlying object deleted probably
|
||||||
|
obj_repr = '<deleted>'
|
||||||
|
lines.append("{}: {}".format(name, obj_repr))
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
|
|
||||||
|
@ -268,56 +268,6 @@ JsWorld = enum('JsWorld', ['main', 'application', 'user', 'jseval'])
|
|||||||
MessageLevel = enum('MessageLevel', ['error', 'warning', 'info'])
|
MessageLevel = enum('MessageLevel', ['error', 'warning', 'info'])
|
||||||
|
|
||||||
|
|
||||||
# Where a download should be saved
|
|
||||||
class DownloadTarget:
|
|
||||||
|
|
||||||
"""Abstract base class for different download targets."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
class FileDownloadTarget(DownloadTarget):
|
|
||||||
|
|
||||||
"""Save the download to the given file.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
filename: Filename where the download should be saved.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, filename):
|
|
||||||
# pylint: disable=super-init-not-called
|
|
||||||
self.filename = filename
|
|
||||||
|
|
||||||
|
|
||||||
class FileObjDownloadTarget(DownloadTarget):
|
|
||||||
|
|
||||||
"""Save the download to the given file-like object.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
fileobj: File-like object where the download should be written to.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, fileobj):
|
|
||||||
# pylint: disable=super-init-not-called
|
|
||||||
self.fileobj = fileobj
|
|
||||||
|
|
||||||
|
|
||||||
class OpenFileDownloadTarget(DownloadTarget):
|
|
||||||
|
|
||||||
"""Save the download in a temp dir and directly open it.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
cmdline: The command to use as string. A `{}` is expanded to the
|
|
||||||
filename. None means to use the system's default application.
|
|
||||||
If no `{}` is found, the filename is appended to the cmdline.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, cmdline=None):
|
|
||||||
# pylint: disable=super-init-not-called
|
|
||||||
self.cmdline = cmdline
|
|
||||||
|
|
||||||
|
|
||||||
class Question(QObject):
|
class Question(QObject):
|
||||||
|
|
||||||
"""A question asked to the user, e.g. via the status bar.
|
"""A question asked to the user, e.g. via the status bar.
|
||||||
|
@ -75,6 +75,7 @@ Feature: Downloading things from a website.
|
|||||||
And I run :leave-mode
|
And I run :leave-mode
|
||||||
Then no crash should happen
|
Then no crash should happen
|
||||||
|
|
||||||
|
@qtwebengine_todo: ssl-strict is not implemented yet
|
||||||
Scenario: Downloading with SSL errors (issue 1413)
|
Scenario: Downloading with SSL errors (issue 1413)
|
||||||
When I run :debug-clear-ssl-errors
|
When I run :debug-clear-ssl-errors
|
||||||
And I set network -> ssl-strict to ask
|
And I set network -> ssl-strict to ask
|
||||||
@ -85,7 +86,7 @@ Feature: Downloading things from a website.
|
|||||||
|
|
||||||
Scenario: Closing window with remove-finished-downloads timeout (issue 1242)
|
Scenario: Closing window with remove-finished-downloads timeout (issue 1242)
|
||||||
When I set ui -> remove-finished-downloads to 500
|
When I set ui -> remove-finished-downloads to 500
|
||||||
And I open data/downloads/download.bin in a new window
|
And I open data/downloads/download.bin in a new window without waiting
|
||||||
And I wait until the download is finished
|
And I wait until the download is finished
|
||||||
And I run :close
|
And I run :close
|
||||||
And I wait 0.5s
|
And I wait 0.5s
|
||||||
@ -95,7 +96,7 @@ Feature: Downloading things from a website.
|
|||||||
Given I have a fresh instance
|
Given I have a fresh instance
|
||||||
When I set storage -> prompt-download-directory to false
|
When I set storage -> prompt-download-directory to false
|
||||||
And I set ui -> confirm-quit to downloads
|
And I set ui -> confirm-quit to downloads
|
||||||
And I open data/downloads/download.bin
|
And I open data/downloads/download.bin without waiting
|
||||||
And I wait until the download is finished
|
And I wait until the download is finished
|
||||||
And I run :close
|
And I run :close
|
||||||
Then qutebrowser should quit
|
Then qutebrowser should quit
|
||||||
@ -171,18 +172,36 @@ Feature: Downloading things from a website.
|
|||||||
|
|
||||||
## mhtml downloads
|
## mhtml downloads
|
||||||
|
|
||||||
|
@qtwebengine_todo: :download --mhtml is not implemented yet
|
||||||
Scenario: Downloading as mhtml is available
|
Scenario: Downloading as mhtml is available
|
||||||
When I open html
|
When I open html
|
||||||
And I run :download --mhtml
|
And I run :download --mhtml
|
||||||
And I wait for "File successfully written." in the log
|
And I wait for "File successfully written." in the log
|
||||||
Then no crash should happen
|
Then no crash should happen
|
||||||
|
|
||||||
|
@qtwebengine_todo: :download --mhtml is not implemented yet
|
||||||
Scenario: Downloading as mhtml with non-ASCII headers
|
Scenario: Downloading as mhtml with non-ASCII headers
|
||||||
When I open response-headers?Content-Type=text%2Fpl%C3%A4in
|
When I open response-headers?Content-Type=text%2Fpl%C3%A4in
|
||||||
And I run :download --mhtml --dest mhtml-response-headers.mht
|
And I run :download --mhtml --dest mhtml-response-headers.mht
|
||||||
And I wait for "File successfully written." in the log
|
And I wait for "File successfully written." in the log
|
||||||
Then no crash should happen
|
Then no crash should happen
|
||||||
|
|
||||||
|
@qtwebengine_todo: :download --mhtml is not implemented yet
|
||||||
|
Scenario: Overwriting existing mhtml file
|
||||||
|
When I set storage -> prompt-download-directory to true
|
||||||
|
And I open html
|
||||||
|
And I run :download --mhtml
|
||||||
|
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default='*' mode=<PromptMode.text: 2> text='Please enter a location for <b>http://localhost:*/html</b>' title='Save file to:'>, *" in the log
|
||||||
|
And I run :prompt-accept
|
||||||
|
And I wait for "File successfully written." in the log
|
||||||
|
And I run :download --mhtml
|
||||||
|
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default='*' mode=<PromptMode.text: 2> text='Please enter a location for <b>http://localhost:*/html</b>' title='Save file to:'>, *" in the log
|
||||||
|
And I run :prompt-accept
|
||||||
|
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default=None mode=<PromptMode.yesno: 1> text='<b>*</b> already exists. Overwrite?' title='Overwrite existing file?'>, *" in the log
|
||||||
|
And I run :prompt-accept yes
|
||||||
|
And I wait for "File successfully written." in the log
|
||||||
|
Then no crash should happen
|
||||||
|
|
||||||
## :download-cancel
|
## :download-cancel
|
||||||
|
|
||||||
Scenario: Cancelling a download
|
Scenario: Cancelling a download
|
||||||
@ -199,13 +218,13 @@ Feature: Downloading things from a website.
|
|||||||
Then the error "There's no download 42!" should be shown
|
Then the error "There's no download 42!" should be shown
|
||||||
|
|
||||||
Scenario: Cancelling a download which is already done
|
Scenario: Cancelling a download which is already done
|
||||||
When I open data/downloads/download.bin
|
When I open data/downloads/download.bin without waiting
|
||||||
And I wait until the download is finished
|
And I wait until the download is finished
|
||||||
And I run :download-cancel
|
And I run :download-cancel
|
||||||
Then the error "Download 1 is already done!" should be shown
|
Then the error "Download 1 is already done!" should be shown
|
||||||
|
|
||||||
Scenario: Cancelling a download which is already done (with count)
|
Scenario: Cancelling a download which is already done (with count)
|
||||||
When I open data/downloads/download.bin
|
When I open data/downloads/download.bin without waiting
|
||||||
And I wait until the download is finished
|
And I wait until the download is finished
|
||||||
And I run :download-cancel with count 1
|
And I run :download-cancel with count 1
|
||||||
Then the error "Download 1 is already done!" should be shown
|
Then the error "Download 1 is already done!" should be shown
|
||||||
@ -218,6 +237,7 @@ Feature: Downloading things from a website.
|
|||||||
And "cancelled" should be logged
|
And "cancelled" should be logged
|
||||||
|
|
||||||
# https://github.com/The-Compiler/qutebrowser/issues/1535
|
# https://github.com/The-Compiler/qutebrowser/issues/1535
|
||||||
|
@qtwebengine_todo: :download --mhtml is not implemented yet
|
||||||
Scenario: Cancelling an MHTML download (issue 1535)
|
Scenario: Cancelling an MHTML download (issue 1535)
|
||||||
When I open data/downloads/issue1535.html
|
When I open data/downloads/issue1535.html
|
||||||
And I run :download --mhtml
|
And I run :download --mhtml
|
||||||
@ -228,7 +248,7 @@ Feature: Downloading things from a website.
|
|||||||
## :download-remove / :download-clear
|
## :download-remove / :download-clear
|
||||||
|
|
||||||
Scenario: Removing a download
|
Scenario: Removing a download
|
||||||
When I open data/downloads/download.bin
|
When I open data/downloads/download.bin without waiting
|
||||||
And I wait until the download is finished
|
And I wait until the download is finished
|
||||||
And I run :download-remove
|
And I run :download-remove
|
||||||
Then "Removed download *" should be logged
|
Then "Removed download *" should be logged
|
||||||
@ -248,17 +268,17 @@ Feature: Downloading things from a website.
|
|||||||
Then the error "Download 1 is not done!" should be shown
|
Then the error "Download 1 is not done!" should be shown
|
||||||
|
|
||||||
Scenario: Removing all downloads via :download-remove
|
Scenario: Removing all downloads via :download-remove
|
||||||
When I open data/downloads/download.bin
|
When I open data/downloads/download.bin without waiting
|
||||||
And I wait until the download is finished
|
And I wait until the download is finished
|
||||||
And I open data/downloads/download2.bin
|
And I open data/downloads/download2.bin without waiting
|
||||||
And I wait until the download is finished
|
And I wait until the download is finished
|
||||||
And I run :download-remove --all
|
And I run :download-remove --all
|
||||||
Then "Removed download *" should be logged
|
Then "Removed download *" should be logged
|
||||||
|
|
||||||
Scenario: Removing all downloads via :download-clear
|
Scenario: Removing all downloads via :download-clear
|
||||||
When I open data/downloads/download.bin
|
When I open data/downloads/download.bin without waiting
|
||||||
And I wait until the download is finished
|
And I wait until the download is finished
|
||||||
And I open data/downloads/download2.bin
|
And I open data/downloads/download2.bin without waiting
|
||||||
And I wait until the download is finished
|
And I wait until the download is finished
|
||||||
And I run :download-clear
|
And I run :download-clear
|
||||||
Then "Removed download *" should be logged
|
Then "Removed download *" should be logged
|
||||||
@ -266,7 +286,7 @@ Feature: Downloading things from a website.
|
|||||||
## :download-delete
|
## :download-delete
|
||||||
|
|
||||||
Scenario: Deleting a download
|
Scenario: Deleting a download
|
||||||
When I open data/downloads/download.bin
|
When I open data/downloads/download.bin without waiting
|
||||||
And I wait until the download is finished
|
And I wait until the download is finished
|
||||||
And I run :download-delete
|
And I run :download-delete
|
||||||
And I wait for "deleted download *" in the log
|
And I wait for "deleted download *" in the log
|
||||||
@ -289,13 +309,13 @@ Feature: Downloading things from a website.
|
|||||||
## :download-open
|
## :download-open
|
||||||
|
|
||||||
Scenario: Opening a download
|
Scenario: Opening a download
|
||||||
When I open data/downloads/download.bin
|
When I open data/downloads/download.bin without waiting
|
||||||
And I wait until the download is finished
|
And I wait until the download is finished
|
||||||
And I open the download
|
And I open the download
|
||||||
Then "Opening *download.bin* with [*python*]" should be logged
|
Then "Opening *download.bin* with [*python*]" should be logged
|
||||||
|
|
||||||
Scenario: Opening a download with a placeholder
|
Scenario: Opening a download with a placeholder
|
||||||
When I open data/downloads/download.bin
|
When I open data/downloads/download.bin without waiting
|
||||||
And I wait until the download is finished
|
And I wait until the download is finished
|
||||||
And I open the download with a placeholder
|
And I open the download with a placeholder
|
||||||
Then "Opening *download.bin* with [*python*]" should be logged
|
Then "Opening *download.bin* with [*python*]" should be logged
|
||||||
@ -318,7 +338,8 @@ Feature: Downloading things from a website.
|
|||||||
|
|
||||||
Scenario: Opening a download directly
|
Scenario: Opening a download directly
|
||||||
When I set storage -> prompt-download-directory to true
|
When I set storage -> prompt-download-directory to true
|
||||||
And I open data/downloads/download.bin
|
And I open data/downloads/download.bin without waiting
|
||||||
|
And I wait for the download prompt for "*"
|
||||||
And I directly open the download
|
And I directly open the download
|
||||||
And I wait until the download is finished
|
And I wait until the download is finished
|
||||||
Then "Opening *download.bin* with [*python*]" should be logged
|
Then "Opening *download.bin* with [*python*]" should be logged
|
||||||
@ -328,6 +349,7 @@ Feature: Downloading things from a website.
|
|||||||
Scenario: Cancelling a download that should be opened
|
Scenario: Cancelling a download that should be opened
|
||||||
When I set storage -> prompt-download-directory to true
|
When I set storage -> prompt-download-directory to true
|
||||||
And I run :download http://localhost:(port)/drip?numbytes=128&duration=5
|
And I run :download http://localhost:(port)/drip?numbytes=128&duration=5
|
||||||
|
And I wait for the download prompt for "*"
|
||||||
And I directly open the download
|
And I directly open the download
|
||||||
And I run :download-cancel
|
And I run :download-cancel
|
||||||
Then "* finished but not successful, not opening!" should be logged
|
Then "* finished but not successful, not opening!" should be logged
|
||||||
@ -338,7 +360,7 @@ Feature: Downloading things from a website.
|
|||||||
When I set storage -> prompt-download-directory to true
|
When I set storage -> prompt-download-directory to true
|
||||||
And I open data/downloads/issue1725.html
|
And I open data/downloads/issue1725.html
|
||||||
And I run :click-element id long-link
|
And I run :click-element id long-link
|
||||||
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default=* mode=<PromptMode.download: 5> text=* title='Save file to:'>, *" in the log
|
And I wait for the download prompt for "*"
|
||||||
And I directly open the download
|
And I directly open the download
|
||||||
And I wait until the download is finished
|
And I wait until the download is finished
|
||||||
Then "Opening * with [*python*]" should be logged
|
Then "Opening * with [*python*]" should be logged
|
||||||
@ -348,19 +370,19 @@ Feature: Downloading things from a website.
|
|||||||
Scenario: completion -> download-path-suggestion = path
|
Scenario: completion -> download-path-suggestion = path
|
||||||
When I set storage -> prompt-download-directory to true
|
When I set storage -> prompt-download-directory to true
|
||||||
And I set completion -> download-path-suggestion to path
|
And I set completion -> download-path-suggestion to path
|
||||||
And I open data/downloads/download.bin
|
And I open data/downloads/download.bin without waiting
|
||||||
Then the download prompt should be shown with "(tmpdir)/"
|
Then the download prompt should be shown with "(tmpdir)/"
|
||||||
|
|
||||||
Scenario: completion -> download-path-suggestion = filename
|
Scenario: completion -> download-path-suggestion = filename
|
||||||
When I set storage -> prompt-download-directory to true
|
When I set storage -> prompt-download-directory to true
|
||||||
And I set completion -> download-path-suggestion to filename
|
And I set completion -> download-path-suggestion to filename
|
||||||
And I open data/downloads/download.bin
|
And I open data/downloads/download.bin without waiting
|
||||||
Then the download prompt should be shown with "download.bin"
|
Then the download prompt should be shown with "download.bin"
|
||||||
|
|
||||||
Scenario: completion -> download-path-suggestion = both
|
Scenario: completion -> download-path-suggestion = both
|
||||||
When I set storage -> prompt-download-directory to true
|
When I set storage -> prompt-download-directory to true
|
||||||
And I set completion -> download-path-suggestion to both
|
And I set completion -> download-path-suggestion to both
|
||||||
And I open data/downloads/download.bin
|
And I open data/downloads/download.bin without waiting
|
||||||
Then the download prompt should be shown with "(tmpdir)/download.bin"
|
Then the download prompt should be shown with "(tmpdir)/download.bin"
|
||||||
|
|
||||||
## storage -> remember-download-directory
|
## storage -> remember-download-directory
|
||||||
@ -369,20 +391,20 @@ Feature: Downloading things from a website.
|
|||||||
When I set storage -> prompt-download-directory to true
|
When I set storage -> prompt-download-directory to true
|
||||||
And I set completion -> download-path-suggestion to both
|
And I set completion -> download-path-suggestion to both
|
||||||
And I set storage -> remember-download-directory to true
|
And I set storage -> remember-download-directory to true
|
||||||
And I open data/downloads/download.bin
|
And I open data/downloads/download.bin without waiting
|
||||||
And I wait for the download prompt for "*/download.bin"
|
And I wait for the download prompt for "*/download.bin"
|
||||||
And I run :prompt-accept (tmpdir)(dirsep)subdir
|
And I run :prompt-accept (tmpdir)(dirsep)subdir
|
||||||
And I open data/downloads/download2.bin
|
And I open data/downloads/download2.bin without waiting
|
||||||
Then the download prompt should be shown with "(tmpdir)/subdir/download2.bin"
|
Then the download prompt should be shown with "(tmpdir)/subdir/download2.bin"
|
||||||
|
|
||||||
Scenario: Not remembering the last download directory
|
Scenario: Not remembering the last download directory
|
||||||
When I set storage -> prompt-download-directory to true
|
When I set storage -> prompt-download-directory to true
|
||||||
And I set completion -> download-path-suggestion to both
|
And I set completion -> download-path-suggestion to both
|
||||||
And I set storage -> remember-download-directory to false
|
And I set storage -> remember-download-directory to false
|
||||||
And I open data/downloads/download.bin
|
And I open data/downloads/download.bin without waiting
|
||||||
And I wait for the download prompt for "(tmpdir)/download.bin"
|
And I wait for the download prompt for "(tmpdir)/download.bin"
|
||||||
And I run :prompt-accept (tmpdir)(dirsep)subdir
|
And I run :prompt-accept (tmpdir)(dirsep)subdir
|
||||||
And I open data/downloads/download2.bin
|
And I open data/downloads/download2.bin without waiting
|
||||||
Then the download prompt should be shown with "(tmpdir)/download2.bin"
|
Then the download prompt should be shown with "(tmpdir)/download2.bin"
|
||||||
|
|
||||||
# Overwriting files
|
# Overwriting files
|
||||||
@ -475,6 +497,7 @@ Feature: Downloading things from a website.
|
|||||||
And I run :download foo!
|
And I run :download foo!
|
||||||
Then the error "Invalid URL" should be shown
|
Then the error "Invalid URL" should be shown
|
||||||
|
|
||||||
|
@qtwebengine_todo: pdfjs is not implemented yet
|
||||||
Scenario: Downloading via pdfjs
|
Scenario: Downloading via pdfjs
|
||||||
Given pdfjs is available
|
Given pdfjs is available
|
||||||
When I set storage -> prompt-download-directory to false
|
When I set storage -> prompt-download-directory to false
|
||||||
@ -496,3 +519,9 @@ Feature: Downloading things from a website.
|
|||||||
And I wait until the download is finished
|
And I wait until the download is finished
|
||||||
Then the downloaded file download.bin should exist
|
Then the downloaded file download.bin should exist
|
||||||
And the downloaded file download2.bin should not exist
|
And the downloaded file download2.bin should not exist
|
||||||
|
|
||||||
|
Scenario: Downloading a file with unknown size
|
||||||
|
When I set storage -> prompt-download-directory to false
|
||||||
|
And I open stream-bytes/1024 without waiting
|
||||||
|
And I wait until the download is finished
|
||||||
|
Then the downloaded file 1024 should exist
|
||||||
|
@ -75,8 +75,8 @@ Feature: Opening pages
|
|||||||
|
|
||||||
Scenario: Opening in a new tab (explicit)
|
Scenario: Opening in a new tab (explicit)
|
||||||
Given I open about:blank
|
Given I open about:blank
|
||||||
When I set tabs -> new-tab-position-explicit to right
|
When I set tabs -> new-tab-position-explicit to next
|
||||||
And I set tabs -> new-tab-position to left
|
And I set tabs -> new-tab-position to prev
|
||||||
And I run :tab-only
|
And I run :tab-only
|
||||||
And I run :open -t http://localhost:(port)/data/numbers/7.txt
|
And I run :open -t http://localhost:(port)/data/numbers/7.txt
|
||||||
And I wait until data/numbers/7.txt is loaded
|
And I wait until data/numbers/7.txt is loaded
|
||||||
@ -86,8 +86,8 @@ Feature: Opening pages
|
|||||||
|
|
||||||
Scenario: Opening in a new tab (implicit)
|
Scenario: Opening in a new tab (implicit)
|
||||||
Given I open about:blank
|
Given I open about:blank
|
||||||
When I set tabs -> new-tab-position-explicit to right
|
When I set tabs -> new-tab-position-explicit to next
|
||||||
And I set tabs -> new-tab-position to left
|
And I set tabs -> new-tab-position to prev
|
||||||
And I run :tab-only
|
And I run :tab-only
|
||||||
And I run :open -t -i http://localhost:(port)/data/numbers/8.txt
|
And I run :open -t -i http://localhost:(port)/data/numbers/8.txt
|
||||||
And I wait until data/numbers/8.txt is loaded
|
And I wait until data/numbers/8.txt is loaded
|
||||||
|
@ -35,8 +35,8 @@ Feature: Tab management
|
|||||||
- data/numbers/2.txt
|
- data/numbers/2.txt
|
||||||
- data/numbers/3.txt (active)
|
- data/numbers/3.txt (active)
|
||||||
|
|
||||||
Scenario: :tab-close with select-on-remove = right
|
Scenario: :tab-close with select-on-remove = next
|
||||||
When I set tabs -> select-on-remove to right
|
When I set tabs -> select-on-remove to next
|
||||||
And I open data/numbers/1.txt
|
And I open data/numbers/1.txt
|
||||||
And I open data/numbers/2.txt in a new tab
|
And I open data/numbers/2.txt in a new tab
|
||||||
And I open data/numbers/3.txt in a new tab
|
And I open data/numbers/3.txt in a new tab
|
||||||
@ -46,8 +46,8 @@ Feature: Tab management
|
|||||||
- data/numbers/1.txt
|
- data/numbers/1.txt
|
||||||
- data/numbers/3.txt (active)
|
- data/numbers/3.txt (active)
|
||||||
|
|
||||||
Scenario: :tab-close with select-on-remove = left
|
Scenario: :tab-close with select-on-remove = prev
|
||||||
When I set tabs -> select-on-remove to left
|
When I set tabs -> select-on-remove to prev
|
||||||
And I open data/numbers/1.txt
|
And I open data/numbers/1.txt
|
||||||
And I open data/numbers/2.txt in a new tab
|
And I open data/numbers/2.txt in a new tab
|
||||||
And I open data/numbers/3.txt in a new tab
|
And I open data/numbers/3.txt in a new tab
|
||||||
@ -57,8 +57,8 @@ Feature: Tab management
|
|||||||
- data/numbers/1.txt (active)
|
- data/numbers/1.txt (active)
|
||||||
- data/numbers/3.txt
|
- data/numbers/3.txt
|
||||||
|
|
||||||
Scenario: :tab-close with select-on-remove = previous
|
Scenario: :tab-close with select-on-remove = last-used
|
||||||
When I set tabs -> select-on-remove to previous
|
When I set tabs -> select-on-remove to last-used
|
||||||
And I open data/numbers/1.txt
|
And I open data/numbers/1.txt
|
||||||
And I open data/numbers/2.txt in a new tab
|
And I open data/numbers/2.txt in a new tab
|
||||||
And I open data/numbers/3.txt in a new tab
|
And I open data/numbers/3.txt in a new tab
|
||||||
@ -70,30 +70,30 @@ Feature: Tab management
|
|||||||
- data/numbers/3.txt
|
- data/numbers/3.txt
|
||||||
- data/numbers/4.txt (active)
|
- data/numbers/4.txt (active)
|
||||||
|
|
||||||
Scenario: :tab-close with select-on-remove = left and --right
|
Scenario: :tab-close with select-on-remove = prev and --next
|
||||||
When I set tabs -> select-on-remove to left
|
When I set tabs -> select-on-remove to prev
|
||||||
And I open data/numbers/1.txt
|
And I open data/numbers/1.txt
|
||||||
And I open data/numbers/2.txt in a new tab
|
And I open data/numbers/2.txt in a new tab
|
||||||
And I open data/numbers/3.txt in a new tab
|
And I open data/numbers/3.txt in a new tab
|
||||||
And I run :tab-focus 2
|
And I run :tab-focus 2
|
||||||
And I run :tab-close --right
|
And I run :tab-close --next
|
||||||
Then the following tabs should be open:
|
Then the following tabs should be open:
|
||||||
- data/numbers/1.txt
|
- data/numbers/1.txt
|
||||||
- data/numbers/3.txt (active)
|
- data/numbers/3.txt (active)
|
||||||
|
|
||||||
Scenario: :tab-close with select-on-remove = right and --left
|
Scenario: :tab-close with select-on-remove = next and --prev
|
||||||
When I set tabs -> select-on-remove to right
|
When I set tabs -> select-on-remove to next
|
||||||
And I open data/numbers/1.txt
|
And I open data/numbers/1.txt
|
||||||
And I open data/numbers/2.txt in a new tab
|
And I open data/numbers/2.txt in a new tab
|
||||||
And I open data/numbers/3.txt in a new tab
|
And I open data/numbers/3.txt in a new tab
|
||||||
And I run :tab-focus 2
|
And I run :tab-focus 2
|
||||||
And I run :tab-close --left
|
And I run :tab-close --prev
|
||||||
Then the following tabs should be open:
|
Then the following tabs should be open:
|
||||||
- data/numbers/1.txt (active)
|
- data/numbers/1.txt (active)
|
||||||
- data/numbers/3.txt
|
- data/numbers/3.txt
|
||||||
|
|
||||||
Scenario: :tab-close with select-on-remove = left and --opposite
|
Scenario: :tab-close with select-on-remove = prev and --opposite
|
||||||
When I set tabs -> select-on-remove to left
|
When I set tabs -> select-on-remove to prev
|
||||||
And I open data/numbers/1.txt
|
And I open data/numbers/1.txt
|
||||||
And I open data/numbers/2.txt in a new tab
|
And I open data/numbers/2.txt in a new tab
|
||||||
And I open data/numbers/3.txt in a new tab
|
And I open data/numbers/3.txt in a new tab
|
||||||
@ -103,8 +103,8 @@ Feature: Tab management
|
|||||||
- data/numbers/1.txt
|
- data/numbers/1.txt
|
||||||
- data/numbers/3.txt (active)
|
- data/numbers/3.txt (active)
|
||||||
|
|
||||||
Scenario: :tab-close with select-on-remove = right and --opposite
|
Scenario: :tab-close with select-on-remove = next and --opposite
|
||||||
When I set tabs -> select-on-remove to right
|
When I set tabs -> select-on-remove to next
|
||||||
And I open data/numbers/1.txt
|
And I open data/numbers/1.txt
|
||||||
And I open data/numbers/2.txt in a new tab
|
And I open data/numbers/2.txt in a new tab
|
||||||
And I open data/numbers/3.txt in a new tab
|
And I open data/numbers/3.txt in a new tab
|
||||||
@ -114,19 +114,19 @@ Feature: Tab management
|
|||||||
- data/numbers/1.txt (active)
|
- data/numbers/1.txt (active)
|
||||||
- data/numbers/3.txt
|
- data/numbers/3.txt
|
||||||
|
|
||||||
Scenario: :tab-close with select-on-remove = previous and --opposite
|
Scenario: :tab-close with select-on-remove = last-used and --opposite
|
||||||
When I set tabs -> select-on-remove to previous
|
When I set tabs -> select-on-remove to last-used
|
||||||
And I run :tab-close --opposite
|
And I run :tab-close --opposite
|
||||||
Then the error "-o is not supported with 'tabs->select-on-remove' set to 'previous'!" should be shown
|
Then the error "-o is not supported with 'tabs->select-on-remove' set to 'last-used'!" should be shown
|
||||||
|
|
||||||
Scenario: :tab-close should restore selection behavior
|
Scenario: :tab-close should restore selection behavior
|
||||||
When I set tabs -> select-on-remove to right
|
When I set tabs -> select-on-remove to next
|
||||||
And I open data/numbers/1.txt
|
And I open data/numbers/1.txt
|
||||||
And I open data/numbers/2.txt in a new tab
|
And I open data/numbers/2.txt in a new tab
|
||||||
And I open data/numbers/3.txt in a new tab
|
And I open data/numbers/3.txt in a new tab
|
||||||
And I open data/numbers/4.txt in a new tab
|
And I open data/numbers/4.txt in a new tab
|
||||||
And I run :tab-focus 2
|
And I run :tab-focus 2
|
||||||
And I run :tab-close --left
|
And I run :tab-close --prev
|
||||||
And I run :tab-focus 2
|
And I run :tab-focus 2
|
||||||
And I run :tab-close
|
And I run :tab-close
|
||||||
Then the following tabs should be open:
|
Then the following tabs should be open:
|
||||||
@ -143,29 +143,29 @@ Feature: Tab management
|
|||||||
Then the following tabs should be open:
|
Then the following tabs should be open:
|
||||||
- data/numbers/3.txt (active)
|
- data/numbers/3.txt (active)
|
||||||
|
|
||||||
Scenario: :tab-only with --left
|
Scenario: :tab-only with --prev
|
||||||
When I open data/numbers/1.txt
|
When I open data/numbers/1.txt
|
||||||
And I open data/numbers/2.txt in a new tab
|
And I open data/numbers/2.txt in a new tab
|
||||||
And I open data/numbers/3.txt in a new tab
|
And I open data/numbers/3.txt in a new tab
|
||||||
And I run :tab-focus 2
|
And I run :tab-focus 2
|
||||||
And I run :tab-only --left
|
And I run :tab-only --prev
|
||||||
Then the following tabs should be open:
|
Then the following tabs should be open:
|
||||||
- data/numbers/1.txt
|
- data/numbers/1.txt
|
||||||
- data/numbers/2.txt (active)
|
- data/numbers/2.txt (active)
|
||||||
|
|
||||||
Scenario: :tab-only with --right
|
Scenario: :tab-only with --next
|
||||||
When I open data/numbers/1.txt
|
When I open data/numbers/1.txt
|
||||||
And I open data/numbers/2.txt in a new tab
|
And I open data/numbers/2.txt in a new tab
|
||||||
And I open data/numbers/3.txt in a new tab
|
And I open data/numbers/3.txt in a new tab
|
||||||
And I run :tab-focus 2
|
And I run :tab-focus 2
|
||||||
And I run :tab-only --right
|
And I run :tab-only --next
|
||||||
Then the following tabs should be open:
|
Then the following tabs should be open:
|
||||||
- data/numbers/2.txt (active)
|
- data/numbers/2.txt (active)
|
||||||
- data/numbers/3.txt
|
- data/numbers/3.txt
|
||||||
|
|
||||||
Scenario: :tab-only with --left and --right
|
Scenario: :tab-only with --prev and --next
|
||||||
When I run :tab-only --left --right
|
When I run :tab-only --prev --next
|
||||||
Then the error "Only one of -l/-r can be given!" should be shown
|
Then the error "Only one of -p/-n can be given!" should be shown
|
||||||
|
|
||||||
# :tab-focus
|
# :tab-focus
|
||||||
|
|
||||||
@ -821,8 +821,8 @@ Feature: Tab management
|
|||||||
- data/hints/html/simple.html (active)
|
- data/hints/html/simple.html (active)
|
||||||
- data/hello.txt
|
- data/hello.txt
|
||||||
|
|
||||||
Scenario: opening tab with tabs->new-tab-position left
|
Scenario: opening tab with tabs->new-tab-position prev
|
||||||
When I set tabs -> new-tab-position to left
|
When I set tabs -> new-tab-position to prev
|
||||||
And I set tabs -> background-tabs to false
|
And I set tabs -> background-tabs to false
|
||||||
And I open about:blank
|
And I open about:blank
|
||||||
And I open data/hints/html/simple.html in a new tab
|
And I open data/hints/html/simple.html in a new tab
|
||||||
@ -833,8 +833,8 @@ Feature: Tab management
|
|||||||
- data/hello.txt (active)
|
- data/hello.txt (active)
|
||||||
- data/hints/html/simple.html
|
- data/hints/html/simple.html
|
||||||
|
|
||||||
Scenario: opening tab with tabs->new-tab-position right
|
Scenario: opening tab with tabs->new-tab-position next
|
||||||
When I set tabs -> new-tab-position to right
|
When I set tabs -> new-tab-position to next
|
||||||
And I set tabs -> background-tabs to false
|
And I set tabs -> background-tabs to false
|
||||||
And I open about:blank
|
And I open about:blank
|
||||||
And I open data/hints/html/simple.html in a new tab
|
And I open data/hints/html/simple.html in a new tab
|
||||||
|
@ -21,15 +21,10 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
import shlex
|
import shlex
|
||||||
|
|
||||||
import pytest
|
|
||||||
import pytest_bdd as bdd
|
import pytest_bdd as bdd
|
||||||
bdd.scenarios('downloads.feature')
|
bdd.scenarios('downloads.feature')
|
||||||
|
|
||||||
|
|
||||||
pytestmark = pytest.mark.qtwebengine_todo("Downloads not implemented yet",
|
|
||||||
run=False)
|
|
||||||
|
|
||||||
|
|
||||||
PROMPT_MSG = ("Asking question <qutebrowser.utils.usertypes.Question "
|
PROMPT_MSG = ("Asking question <qutebrowser.utils.usertypes.Question "
|
||||||
"default={!r} mode=<PromptMode.download: 5> text=* "
|
"default={!r} mode=<PromptMode.download: 5> text=* "
|
||||||
"title='Save file to:'>, *")
|
"title='Save file to:'>, *")
|
||||||
|
@ -263,6 +263,7 @@ Feature: Yanking and pasting.
|
|||||||
And I run :click-element id qute-textarea
|
And I run :click-element id qute-textarea
|
||||||
And I wait for "Clicked editable element!" in the log
|
And I wait for "Clicked editable element!" in the log
|
||||||
And I run :insert-text Hello world
|
And I run :insert-text Hello world
|
||||||
|
And I wait for "Inserting text into element *" in the log
|
||||||
And I run :jseval console.log("textarea contents: " + document.getElementById('qute-textarea').value);
|
And I run :jseval console.log("textarea contents: " + document.getElementById('qute-textarea').value);
|
||||||
# Enable javascript again for the other tests
|
# Enable javascript again for the other tests
|
||||||
And I set content -> allow-javascript to true
|
And I set content -> allow-javascript to true
|
||||||
|
@ -101,10 +101,11 @@ class FakeDownloadManager:
|
|||||||
def download_stub(win_registry):
|
def download_stub(win_registry):
|
||||||
"""Register a FakeDownloadManager."""
|
"""Register a FakeDownloadManager."""
|
||||||
stub = FakeDownloadManager()
|
stub = FakeDownloadManager()
|
||||||
objreg.register('download-manager', stub,
|
objreg.register('qtnetwork-download-manager', stub,
|
||||||
scope='window', window='last-focused')
|
scope='window', window='last-focused')
|
||||||
yield
|
yield
|
||||||
objreg.delete('download-manager', scope='window', window='last-focused')
|
objreg.delete('qtnetwork-download-manager', scope='window',
|
||||||
|
window='last-focused')
|
||||||
|
|
||||||
|
|
||||||
def create_zipfile(directory, files, zipname='test'):
|
def create_zipfile(directory, files, zipname='test'):
|
||||||
|
@ -17,13 +17,46 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from qutebrowser.browser.webkit import downloads
|
from qutebrowser.browser import downloads, qtnetworkdownloads
|
||||||
|
|
||||||
|
|
||||||
def test_download_model(qapp, qtmodeltester, config_stub, cookiejar_and_cache):
|
def test_download_model(qapp, qtmodeltester, config_stub, cookiejar_and_cache):
|
||||||
"""Simple check for download model internals."""
|
"""Simple check for download model internals."""
|
||||||
config_stub.data = {'general': {'private-browsing': False}}
|
config_stub.data = {'general': {'private-browsing': False}}
|
||||||
manager = downloads.DownloadManager(win_id=0)
|
manager = qtnetworkdownloads.DownloadManager(win_id=0)
|
||||||
model = downloads.DownloadModel(manager)
|
model = downloads.DownloadModel(manager)
|
||||||
qtmodeltester.check(model)
|
qtmodeltester.check(model)
|
||||||
|
|
||||||
|
|
||||||
|
class TestDownloadTarget:
|
||||||
|
|
||||||
|
def test_base(self):
|
||||||
|
with pytest.raises(NotImplementedError):
|
||||||
|
downloads._DownloadTarget()
|
||||||
|
|
||||||
|
def test_filename(self):
|
||||||
|
target = downloads.FileDownloadTarget("/foo/bar")
|
||||||
|
assert target.filename == "/foo/bar"
|
||||||
|
|
||||||
|
def test_fileobj(self):
|
||||||
|
fobj = object()
|
||||||
|
target = downloads.FileObjDownloadTarget(fobj)
|
||||||
|
assert target.fileobj is fobj
|
||||||
|
|
||||||
|
def test_openfile(self):
|
||||||
|
target = downloads.OpenFileDownloadTarget()
|
||||||
|
assert target.cmdline is None
|
||||||
|
|
||||||
|
def test_openfile_custom_command(self):
|
||||||
|
target = downloads.OpenFileDownloadTarget('echo')
|
||||||
|
assert target.cmdline == 'echo'
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('obj', [
|
||||||
|
downloads.FileDownloadTarget('foobar'),
|
||||||
|
downloads.FileObjDownloadTarget(None),
|
||||||
|
downloads.OpenFileDownloadTarget(),
|
||||||
|
])
|
||||||
|
def test_class_hierarchy(self, obj):
|
||||||
|
assert isinstance(obj, downloads._DownloadTarget)
|
||||||
|
@ -348,6 +348,16 @@ class TestKeyConfigParser:
|
|||||||
|
|
||||||
('prompt-yes', 'prompt-accept yes'),
|
('prompt-yes', 'prompt-accept yes'),
|
||||||
('prompt-no', 'prompt-accept no'),
|
('prompt-no', 'prompt-accept no'),
|
||||||
|
|
||||||
|
('tab-close -l', 'tab-close --prev'),
|
||||||
|
('tab-close --left', 'tab-close --prev'),
|
||||||
|
('tab-close -r', 'tab-close --next'),
|
||||||
|
('tab-close --right', 'tab-close --next'),
|
||||||
|
|
||||||
|
('tab-only -l', 'tab-only --prev'),
|
||||||
|
('tab-only --left', 'tab-only --prev'),
|
||||||
|
('tab-only -r', 'tab-only --next'),
|
||||||
|
('tab-only --right', 'tab-only --next'),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
def test_migrations(self, old, new_expected):
|
def test_migrations(self, old, new_expected):
|
||||||
|
@ -1,59 +0,0 @@
|
|||||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
|
||||||
|
|
||||||
# Copyright 2016 Daniel Schadt
|
|
||||||
#
|
|
||||||
# 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/>.
|
|
||||||
|
|
||||||
"""Tests for the DownloadTarget class."""
|
|
||||||
|
|
||||||
from qutebrowser.utils import usertypes
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
|
|
||||||
def test_base():
|
|
||||||
with pytest.raises(NotImplementedError):
|
|
||||||
usertypes.DownloadTarget()
|
|
||||||
|
|
||||||
|
|
||||||
def test_filename():
|
|
||||||
target = usertypes.FileDownloadTarget("/foo/bar")
|
|
||||||
assert target.filename == "/foo/bar"
|
|
||||||
|
|
||||||
|
|
||||||
def test_fileobj():
|
|
||||||
fobj = object()
|
|
||||||
target = usertypes.FileObjDownloadTarget(fobj)
|
|
||||||
assert target.fileobj is fobj
|
|
||||||
|
|
||||||
|
|
||||||
def test_openfile():
|
|
||||||
target = usertypes.OpenFileDownloadTarget()
|
|
||||||
assert target.cmdline is None
|
|
||||||
|
|
||||||
|
|
||||||
def test_openfile_custom_command():
|
|
||||||
target = usertypes.OpenFileDownloadTarget('echo')
|
|
||||||
assert target.cmdline == 'echo'
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('obj', [
|
|
||||||
usertypes.FileDownloadTarget('foobar'),
|
|
||||||
usertypes.FileObjDownloadTarget(None),
|
|
||||||
usertypes.OpenFileDownloadTarget(),
|
|
||||||
])
|
|
||||||
def test_class_hierarchy(obj):
|
|
||||||
assert isinstance(obj, usertypes.DownloadTarget)
|
|
Loading…
Reference in New Issue
Block a user