From 9553bd44c1bcfe34bcbf0030b2b653704db249a2 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 10 Dec 2014 20:48:19 +0100 Subject: [PATCH] Add an open menu entry to downloads. See #39. --- qutebrowser/browser/downloads.py | 60 ++++++++++++++++++++++---------- qutebrowser/config/configdata.py | 4 +++ qutebrowser/widgets/downloads.py | 12 +++++-- 3 files changed, 56 insertions(+), 20 deletions(-) diff --git a/qutebrowser/browser/downloads.py b/qutebrowser/browser/downloads.py index 41d47ea65..5fa861f05 100644 --- a/qutebrowser/browser/downloads.py +++ b/qutebrowser/browser/downloads.py @@ -28,7 +28,8 @@ import collections from PyQt5.QtCore import (pyqtSlot, pyqtSignal, QObject, QTimer, QStandardPaths, Qt, QVariant, QAbstractListModel, - QModelIndex) + QModelIndex, QUrl) +from PyQt5.QtGui import QDesktopServices from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply # We need this import so PyQt can use it inside pyqtSlot from PyQt5.QtWebKitWidgets import QWebPage # pylint: disable=unused-import @@ -151,6 +152,7 @@ class DownloadItem(QObject): MAX_REDIRECTS: The maximum redirection count. Attributes: + done: Whether the download is finished. stats: A DownloadItemStats object. successful: Whether the download has completed sucessfully. error_msg: The current error message, or None @@ -190,6 +192,7 @@ class DownloadItem(QObject): reply: The QNetworkReply to download. """ super().__init__(parent) + self.done = False self.stats = DownloadItemStats(self) self.stats.updated.connect(self.data_changed) self.autoclose = True @@ -234,10 +237,15 @@ class DownloadItem(QObject): else: remaining = utils.format_seconds(remaining) total = utils.format_size(self.stats.total, suffix='B') - return ('{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)) + if self.done: + return ('{name} [{perc:>2}%|{total}]{errmsg}'.format( + name=self.basename, perc=perc, total=total, + errmsg=errmsg)) + else: + return ('{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)) def _die(self, msg): """Abort the download and emit an error.""" @@ -253,6 +261,7 @@ class DownloadItem(QObject): self.reply.abort() self.reply.deleteLater() self.reply = None + self.done = True if self.fileobj is not None: try: self.fileobj.close() @@ -312,7 +321,15 @@ class DownloadItem(QObject): os.remove(self._filename) except OSError: log.downloads.exception("Failed to remove partial file") + self.done = True self.finished.emit() + self.data_changed.emit() + + def open_file(self): + """Open the downloaded file.""" + assert self.successful + url = QUrl.fromLocalFile(self._filename) + QDesktopServices.openUrl(url) def set_filename(self, filename): """Set the filename to save the download to. @@ -393,7 +410,9 @@ class DownloadItem(QObject): self.reply.deleteLater() self.reply = None self.finished.emit() + self.done = True log.downloads.debug("Download finished") + self.data_changed.emit() @pyqtSlot() def on_reply_finished(self): @@ -620,8 +639,11 @@ class DownloadManager(QAbstractListModel): log.downloads.debug("fetch: {} -> {}".format(reply.url(), suggested_filename)) download = DownloadItem(reply, self) - download.finished.connect( - functools.partial(self.on_finished, download)) + download.cancelled.connect( + functools.partial(self.remove_item, download)) + if config.get('ui', 'remove-finished-downloads'): + download.finished.connect( + functools.partial(self.remove_item, download)) download.data_changed.connect( functools.partial(self.on_data_changed, download)) download.error.connect(self.on_error) @@ -680,20 +702,14 @@ class DownloadManager(QAbstractListModel): new_reply = reply.manager().get(request) download.init_reply(new_reply) - @pyqtSlot(DownloadItem) - def on_finished(self, download): - """Remove finished download.""" - log.downloads.debug("on_finished: {}".format(download)) - idx = self.downloads.index(download) - self.beginRemoveRows(QModelIndex(), idx, idx) - del self.downloads[idx] - self.endRemoveRows() - download.deleteLater() - @pyqtSlot(DownloadItem) def on_data_changed(self, download): """Emit data_changed signal when download data changed.""" - idx = self.downloads.index(download) + try: + idx = self.downloads.index(download) + except ValueError: + # download has been deleted in the meantime + return model_idx = self.index(idx, 0) qtutils.ensure_valid(model_idx) self.dataChanged.emit(model_idx, model_idx) @@ -726,6 +742,14 @@ class DownloadManager(QAbstractListModel): idx = self.index(self.rowCount() - 1) return idx + def remove_item(self, download): + """Remove a given download.""" + idx = self.downloads.index(download) + self.beginRemoveRows(QModelIndex(), idx, idx) + del self.downloads[idx] + self.endRemoveRows() + download.deleteLater() + def headerData(self, section, orientation, role): """Simple constant header.""" if (section == 0 and orientation == Qt.Horizontal and diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 84d063117..aba5694d4 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -228,6 +228,10 @@ DATA = collections.OrderedDict([ ('css-media-type', SettingValue(typ.String(none_ok=True), ''), "Set the CSS media type."), + + ('remove-finished-downloads', + SettingValue(typ.Bool(), 'false'), + "Whether to remove finished downloads automatically."), )), ('network', sect.KeyValue( diff --git a/qutebrowser/widgets/downloads.py b/qutebrowser/widgets/downloads.py index 66aeb6076..7f6f43005 100644 --- a/qutebrowser/widgets/downloads.py +++ b/qutebrowser/widgets/downloads.py @@ -110,8 +110,16 @@ class DownloadView(QListView): return item = self.model().data(index, downloads.ModelRole.item) self._menu = QMenu(self) - cancel = self._menu.addAction("Cancel") - cancel.triggered.connect(item.cancel) + if item.done: + if item.successful: + open_action = self._menu.addAction("Open") + open_action.triggered.connect(item.open_file) + remove = self._menu.addAction("Remove") + remove.triggered.connect(functools.partial( + self.model().remove_item, item)) + else: + cancel = self._menu.addAction("Cancel") + cancel.triggered.connect(item.cancel) self._menu.popup(self.viewport().mapToGlobal(point)) def minimumSizeHint(self):