Merge branch 'download_cmd_interface' of https://github.com/oed/qutebrowser into oed-download_cmd_interface

This commit is contained in:
Florian Bruhin 2015-02-14 19:06:47 +01:00
commit 8e2e996369
6 changed files with 188 additions and 62 deletions

View File

@ -130,12 +130,12 @@ Contributors, sorted by the number of commits in descending order:
// QUTE_AUTHORS_START
* Florian Bruhin
* Claude
* Joel Torstensson
* ZDarian
* Peter Vilim
* John ShaggyTwoDope Jenkins
* rikn00
* Martin Zimmermann
* Joel Torstensson
* Error 800
* Brian Jackson
* Patric Schmitz

View File

@ -8,10 +8,12 @@
|<<adblock-update,adblock-update>>|Update the adblock block lists.
|<<back,back>>|Go back in the history of the current tab.
|<<bind,bind>>|Bind a key to a command.
|<<cancel-download,cancel-download>>|Cancel the first/[count]th download.
|<<close,close>>|Close the current window.
|<<download,download>>|Download a given URL, given as string.
|<<download-page,download-page>>|Download the current page.
|<<download,download>>|Download a given URL, or current page if no URL given.
|<<download-cancel,download-cancel>>|Cancel the last/[count]th download.
|<<download-delete,download-delete>>|Delete the last/[count]th download from disk.
|<<download-open,download-open>>|Open the last/[count]th download.
|<<download-remove,download-remove>>|Remove the last/[count]th download from the list.
|<<forward,forward>>|Go forward in the history of the current tab.
|<<fullscreen,fullscreen>>|Toggle fullscreen mode.
|<<help,help>>|Show help about a command or setting.
@ -85,30 +87,52 @@ Bind a key to a command.
* +*-m*+, +*--mode*+: A comma-separated list of modes to bind the key in (default: `normal`).
[[cancel-download]]
=== cancel-download
Cancel the first/[count]th download.
==== count
The index of the download to cancel.
[[close]]
=== close
Close the current window.
[[download]]
=== download
Syntax: +:download 'url' ['dest']+
Syntax: +:download ['url'] ['dest']+
Download a given URL, given as string.
Download a given URL, or current page if no URL given.
==== positional arguments
* +'url'+: The URL to download
* +'url'+: The URL to download. If not given, download the current page.
* +'dest'+: The file path to write the download to to ask.
[[download-page]]
=== download-page
Download the current page.
[[download-cancel]]
=== download-cancel
Cancel the last/[count]th download.
==== count
The index of the download to cancel.
[[download-delete]]
=== download-delete
Delete the last/[count]th download from disk.
==== count
The index of the download to cancel.
[[download-open]]
=== download-open
Open the last/[count]th download.
==== count
The index of the download to cancel.
[[download-remove]]
=== download-remove
Syntax: +:download-remove [*--all*]+
Remove the last/[count]th download from the list.
==== optional arguments
* +*-a*+, +*--all*+: If given removes all finished downloads.
==== count
The index of the download to cancel.
[[forward]]
=== forward

View File

@ -912,12 +912,28 @@ class CommandDispatcher:
cur.inspector.show()
@cmdutils.register(instance='command-dispatcher', scope='window')
def download_page(self):
"""Download the current page."""
page = self._current_widget().page()
def download(self, url=None, dest=None):
"""Download a given URL, or current page if no URL given.
Args:
url: The URL to download. If not given, download the current page.
dest: The file path to write the download to, or None to ask.
"""
download_manager = objreg.get('download-manager', scope='window',
window=self._win_id)
download_manager.get(self._current_url(), page)
if url:
url = urlutils.qurl_from_user_input(url)
urlutils.raise_cmdexc_if_invalid(url)
download_manager.get(url, filename=dest)
else:
page = self._current_widget().page()
download_manager.get(self._current_url(), page)
@cmdutils.register(instance='command-dispatcher', scope='window',
deprecated="Use :download instead.")
def download_page(self):
"""Download the current page."""
self.download()
@cmdutils.register(instance='command-dispatcher', scope='window')
def view_source(self):

View File

@ -159,6 +159,7 @@ class DownloadItem(QObject):
Attributes:
done: Whether the download is finished.
stats: A DownloadItemStats object.
index: The index of the download in the view.
successful: Whether the download has completed sucessfully.
error_msg: The current error message, or None
autoclose: Whether to close the associated file if the download is
@ -206,6 +207,7 @@ class DownloadItem(QObject):
self.done = False
self.stats = DownloadItemStats(self)
self.stats.updated.connect(self.data_changed)
self.index = 0
self.autoclose = True
self.reply = None
self._buffer = io.BytesIO()
@ -238,8 +240,9 @@ class DownloadItem(QObject):
else:
errmsg = " - {}".format(self.error_msg)
if all(e is None for e in (perc, remaining, self.stats.total)):
return ('{name} [{speed:>10}|{down}]{errmsg}'.format(
name=self.basename, speed=speed, down=down, errmsg=errmsg))
return ('{index}: {name} [{speed:>10}|{down}]{errmsg}'.format(
index=self.index, name=self.basename, speed=speed,
down=down, errmsg=errmsg))
if perc is None:
perc = '??'
else:
@ -250,14 +253,15 @@ class DownloadItem(QObject):
remaining = utils.format_seconds(remaining)
total = utils.format_size(self.stats.total, suffix='B')
if self.done:
return ('{name} [{perc:>2}%|{total}]{errmsg}'.format(
name=self.basename, perc=perc, total=total,
errmsg=errmsg))
return ('{index}: {name} [{perc:>2}%|{total}]{errmsg}'.format(
index=self.index, name=self.basename, perc=perc,
total=total, errmsg=errmsg))
else:
return ('{name} [{speed:>10}|{remaining:>5}|{perc:>2}%|'
return ('{index}: {name} [{speed:>10}|{remaining:>5}|{perc:>2}%|'
'{down}/{total}]{errmsg}'.format(
name=self.basename, speed=speed, remaining=remaining,
perc=perc, down=down, total=total, errmsg=errmsg))
index=self.index, name=self.basename, speed=speed,
remaining=remaining, perc=perc, down=down,
total=total, errmsg=errmsg))
def _create_fileobj(self):
"""Creates a file object using the internal filename."""
@ -354,16 +358,20 @@ class DownloadItem(QObject):
self.reply = None
if self.fileobj is not None:
self.fileobj.close()
try:
if (self._filename is not None and os.path.exists(self._filename)
and remove_data):
os.remove(self._filename)
except OSError:
log.downloads.exception("Failed to remove partial file")
if remove_data:
self.delete()
self.done = True
self.finished.emit()
self.data_changed.emit()
def delete(self):
"""Delete the downloaded file"""
try:
if self._filename is not None and os.path.exists(self._filename):
os.remove(self._filename)
except OSError:
log.downloads.exception("Failed to remove partial file")
def retry(self):
"""Retry a failed download."""
self.cancel()
@ -580,18 +588,6 @@ class DownloadManager(QAbstractListModel):
self.questions.append(q)
return q
@cmdutils.register(instance='download-manager', scope='window')
def download(self, url, dest=None):
"""Download a given URL, given as string.
Args:
url: The URL to download
dest: The file path to write the download to, or None to ask.
"""
url = urlutils.qurl_from_user_input(url)
urlutils.raise_cmdexc_if_invalid(url)
self.get(url, filename=dest)
@pyqtSlot('QUrl', 'QWebPage')
def get(self, url, page=None, fileobj=None, filename=None,
auto_remove=False):
@ -644,7 +640,7 @@ class DownloadManager(QAbstractListModel):
request.setAttribute(QNetworkRequest.CacheLoadControlAttribute,
QNetworkRequest.AlwaysNetwork)
if fileobj is not None or filename is not None:
return self.fetch_request(request, filename, fileobj, page,
return self.fetch_request(request, page, fileobj, filename,
auto_remove)
q = self._prepare_question()
filename = urlutils.filename_from_url(request.url())
@ -719,6 +715,7 @@ class DownloadManager(QAbstractListModel):
download.do_retry.connect(self.fetch)
download.basename = suggested_filename
idx = len(self.downloads) + 1
download.index = idx
self.beginInsertRows(QModelIndex(), idx, idx)
self.downloads.append(download)
self.endInsertRows()
@ -744,20 +741,78 @@ class DownloadManager(QAbstractListModel):
return download
def raise_no_download(self, count):
"""Raise an exception that the download doesn't exist
Args:
count: The index of the download
"""
if not count:
raise cmdexc.CommandError("There's no download!")
raise cmdexc.CommandError("There's no download {}!".format(count))
@cmdutils.register(instance='download-manager', scope='window')
def download_cancel(self, count: {'special': 'count'}=0):
"""Cancel the last/[count]th download.
Args:
count: The index of the download to cancel.
"""
try:
download = self.downloads[count - 1]
except IndexError:
self.raise_no_download(count)
if download.done:
if not count:
count = len(self.downloads)
raise cmdexc.CommandError("Download {} is already done!"
.format(count))
download.cancel()
@cmdutils.register(instance='download-manager', scope='window')
def download_delete(self, count: {'special': 'count'}=0):
"""Delete the last/[count]th download from disk.
Args:
count: The index of the download to cancel.
"""
try:
download = self.downloads[count - 1]
except IndexError:
self.raise_no_download(count)
if not download.successful:
if not count:
count = len(self.downloads)
raise cmdexc.CommandError("Download {} is not done!".format(count))
download.delete()
self.remove_item(download)
@cmdutils.register(instance='download-manager', scope='window',
deprecated="Use :download instead.")
def cancel_download(self, count: {'special': 'count'}=1):
"""Cancel the first/[count]th download.
Args:
count: The index of the download to cancel.
"""
if count == 0:
return
self.download_cancel(count)
@cmdutils.register(instance='download-manager', scope='window')
def download_open(self, count: {'special': 'count'}=0):
"""Open the last/[count]th download.
Args:
count: The index of the download to cancel.
"""
try:
download = self.downloads[count - 1]
except IndexError:
raise cmdexc.CommandError("There's no download {}!".format(count))
download.cancel()
self.raise_no_download(count)
if not download.successful:
if not count:
count = len(self.downloads)
raise cmdexc.CommandError("Download {} is not done!".format(count))
download.open_file()
@pyqtSlot(QNetworkRequest, QNetworkReply)
def on_redirect(self, download, request, reply):
@ -815,14 +870,31 @@ class DownloadManager(QAbstractListModel):
def can_clear(self):
"""Check if there are finished downloads to clear."""
if self.downloads:
return any(download.done for download in self.downloads)
else:
return False
return any(download.done for download in self.downloads)
def clear(self):
"""Remove all finished downloads."""
self.remove_items(d for d in self.downloads if d.done)
@cmdutils.register(instance='download-manager', scope='window')
def download_remove(self, all_: {'name': 'all'}=False,
count: {'special': 'count'}=0):
"""Remove the last/[count]th download from the list.
Args:
all_: If given removes all finished downloads.
count: The index of the download to cancel.
"""
if all_:
finished_items = [d for d in self.downloads if d.done]
self.remove_items(finished_items)
else:
try:
download = self.downloads[count - 1]
except IndexError:
self.raise_no_download(count)
if not download.done:
if not count:
count = len(self.downloads)
raise cmdexc.CommandError("Download {} is not done!"
.format(count))
self.remove_item(download)
def last_index(self):
"""Get the last index in the model.
@ -844,6 +916,7 @@ class DownloadManager(QAbstractListModel):
del self.downloads[idx]
self.endRemoveRows()
download.deleteLater()
self.update_indexes()
def remove_items(self, downloads):
"""Remove an iterable of downloads."""
@ -873,6 +946,18 @@ class DownloadManager(QAbstractListModel):
download.deleteLater()
self.endRemoveRows()
def update_indexes(self):
"""Update indexes of all DownloadItems"""
first_idx = None
for i, d in enumerate(self.downloads, 1):
if first_idx is None and d.index != i:
first_idx = i - 1
d.index = i
if first_idx is not None:
model_idx = self.index(first_idx, 0)
qtutils.ensure_valid(model_idx)
self.dataChanged.emit(model_idx, self.last_index())
def headerData(self, section, orientation, role):
"""Simple constant header."""
if (section == 0 and orientation == Qt.Horizontal and

View File

@ -142,7 +142,8 @@ class DownloadView(QListView):
actions.append(("Cancel", item.cancel))
if self.model().can_clear():
actions.append((None, None))
actions.append(("Remove all finished", self.model().clear))
actions.append(("Remove all finished", functools.partial(
self.model().download_remove, True)))
return actions
@pyqtSlot('QPoint')

View File

@ -1055,8 +1055,8 @@ KEY_DATA = collections.OrderedDict([
('navigate increment', ['<Ctrl-A>']),
('navigate decrement', ['<Ctrl-X>']),
('inspector', ['wi']),
('download-page', ['gd']),
('cancel-download', ['ad']),
('download', ['gd']),
('download-cancel', ['ad']),
('view-source', ['gf']),
('tab-focus last', ['<Ctrl-Tab>']),
('enter-mode passthrough', ['<Ctrl-V>']),