diff --git a/qutebrowser/browser/downloads.py b/qutebrowser/browser/downloads.py index 6140ebc3d..8cbec2035 100644 --- a/qutebrowser/browser/downloads.py +++ b/qutebrowser/browser/downloads.py @@ -26,6 +26,7 @@ import qutebrowser.config.config as config import qutebrowser.utils.message as message from qutebrowser.utils.log import downloads as logger from qutebrowser.utils.usertypes import PromptMode +from qutebrowser.utils.misc import interpolate_color class DownloadItem(QObject): @@ -111,6 +112,16 @@ class DownloadItem(QObject): else: return 100 * self.bytes_done / self.bytes_total + def bg_color(self): + """Background color to be shown.""" + start = config.get('colors', 'download.bg.start') + stop = config.get('colors', 'download.bg.stop') + system = config.get('colors', 'download.bg.system') + if self.percentage is None: + return start + else: + return interpolate_color(start, stop, self.percentage, system) + def cancel(self): """Cancel the download.""" logger.debug("cancelled") diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index bbc2834ff..6562cb130 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -894,6 +894,22 @@ DATA = OrderedDict([ 'left bottom, color-stop(0%,#FFF785), ' 'color-stop(100%,#FFC542))'), "Background color for hints."), + + ('download.fg', + SettingValue(types.QtColor(), '#ffffff'), + "Foreground color for downloads."), + + ('download.bg.start', + SettingValue(types.QtColor(), '#0000aa'), + "Color gradient start for downloads."), + + ('download.bg.stop', + SettingValue(types.QtColor(), '#00aa00'), + "Color gradient end for downloads."), + + ('download.bg.system', + SettingValue(types.ColorSystem(), 'rgb'), + "Color gradient interpolation system for downloads."), )), ('fonts', sect.KeyValue( diff --git a/qutebrowser/config/conftypes.py b/qutebrowser/config/conftypes.py index a8c0b215f..f6dfd6a09 100644 --- a/qutebrowser/config/conftypes.py +++ b/qutebrowser/config/conftypes.py @@ -434,6 +434,42 @@ class Command(BaseType): return out +class ColorSystem(BaseType): + + """Color systems for interpolation.""" + + valid_values = ValidValues(('rgb', "Interpolate in the RGB color system."), + ('hsv', "Interpolate in the HSV color system."), + ('hsl', "Interpolate in the HSV color system.")) + + def validate(self, value): + super().validate(value.lower()) + + def transform(self, value): + mapping = { + 'rgb': QColor.Rgb, + 'hsv': QColor.Hsv, + 'hsl': QColor.Hsl, + } + return mapping[value.lower()] + + +class QtColor(BaseType): + + """Base class for QColor.""" + + typestr = 'qcolor' + + def validate(self, value): + if QColor.isValidColor(value): + pass + else: + raise ValidationError(value, "must be a valid color") + + def transform(self, value): + return QColor(value) + + class CssColor(BaseType): """Base class for a CSS color value.""" diff --git a/qutebrowser/models/downloadmodel.py b/qutebrowser/models/downloadmodel.py index 18fae8748..1c28f6148 100644 --- a/qutebrowser/models/downloadmodel.py +++ b/qutebrowser/models/downloadmodel.py @@ -21,6 +21,8 @@ from PyQt5.QtCore import (pyqtSlot, Qt, QVariant, QAbstractListModel, QModelIndex) from PyQt5.QtWidgets import QApplication +import qutebrowser.config.config as config + class DownloadModel(QAbstractListModel): @@ -59,19 +61,26 @@ class DownloadModel(QAbstractListModel): """Download data from DownloadManager.""" if not index.isValid(): return QVariant() - elif role != Qt.DisplayRole: - return QVariant() elif index.parent().isValid() or index.column() != 0: return QVariant() + try: item = self.downloadmanager.downloads[index.row()] except IndexError: return QVariant() - perc = item.percentage - if perc is None: - return QVariant() + if role == Qt.DisplayRole: + perc = item.percentage + if perc is None: + data = QVariant() + else: + data = str(round(perc)) # FIXME + elif role == Qt.ForegroundRole: + data = config.get('colors', 'download.fg') + elif role == Qt.BackgroundRole: + data = item.bg_color() else: - return str(round(perc)) # FIXME + data = QVariant() + return data def rowCount(self, parent): """Get count of active downloads.""" diff --git a/qutebrowser/test/utils/test_misc.py b/qutebrowser/test/utils/test_misc.py index d9b042b3f..8cdace43e 100644 --- a/qutebrowser/test/utils/test_misc.py +++ b/qutebrowser/test/utils/test_misc.py @@ -30,6 +30,7 @@ from tempfile import mkdtemp from unittest import TestCase from PyQt5.QtCore import QStandardPaths, QCoreApplication +from PyQt5.QtGui import QColor import qutebrowser.utils.misc as utils from qutebrowser.test.helpers import environ_set_temp @@ -436,5 +437,93 @@ class GetQtArgsTests(TestCase): self.assertEqual(utils.get_qt_args(ns), [sys.argv[0]]) +class InterpolateColorTests(TestCase): + + """Tests for interpolate_color. + + Attributes: + white: The QColor white as a valid QColor for tests. + white: The QColor black as a valid QColor for tests. + """ + + def setUp(self): + self.white = QColor('white') + self.black = QColor('black') + + def test_invalid_start(self): + """Test an invalid start color.""" + with self.assertRaises(ValueError): + utils.interpolate_color(QColor(), self.white, 0) + + def test_invalid_end(self): + """Test an invalid end color.""" + with self.assertRaises(ValueError): + utils.interpolate_color(self.white, QColor(), 0) + + def test_invalid_percentage(self): + """Test an invalid percentage.""" + with self.assertRaises(ValueError): + utils.interpolate_color(self.white, self.white, -1) + with self.assertRaises(ValueError): + utils.interpolate_color(self.white, self.white, 101) + + def test_invalid_colorspace(self): + """Test an invalid colorspace.""" + with self.assertRaises(ValueError): + utils.interpolate_color(self.white, self.black, 10, QColor.Cmyk) + + def test_valid_percentages_rgb(self): + """Test 0% and 100% in the RGB colorspace.""" + white = utils.interpolate_color(self.white, self.black, 0, QColor.Rgb) + black = utils.interpolate_color(self.white, self.black, 100, + QColor.Rgb) + self.assertEqual(white, self.white) + self.assertEqual(black, self.black) + + def test_valid_percentages_hsv(self): + """Test 0% and 100% in the HSV colorspace.""" + white = utils.interpolate_color(self.white, self.black, 0, QColor.Hsv) + black = utils.interpolate_color(self.white, self.black, 100, + QColor.Hsv) + self.assertEqual(white, self.white) + self.assertEqual(black, self.black) + + def test_valid_percentages_hsl(self): + """Test 0% and 100% in the HSL colorspace.""" + white = utils.interpolate_color(self.white, self.black, 0, QColor.Hsl) + black = utils.interpolate_color(self.white, self.black, 100, + QColor.Hsl) + self.assertEqual(white, self.white) + self.assertEqual(black, self.black) + + def test_interpolation_rgb(self): + """Test an interpolation in the RGB colorspace.""" + color = utils.interpolate_color(QColor(0, 40, 100), QColor(0, 20, 200), + 50, QColor.Rgb) + self.assertEqual(color, QColor(0, 30, 150)) + + def test_interpolation_hsv(self): + """Test an interpolation in the HSV colorspace.""" + start = QColor() + stop = QColor() + start.setHsv(0, 40, 100) + stop.setHsv(0, 20, 200) + color = utils.interpolate_color(start, stop, 50, QColor.Hsv) + expected = QColor() + expected.setHsv(0, 30, 150) + self.assertEqual(color, expected) + + def test_interpolation_hsl(self): + """Test an interpolation in the HSL colorspace.""" + start = QColor() + stop = QColor() + start.setHsl(0, 40, 100) + stop.setHsl(0, 20, 200) + color = utils.interpolate_color(start, stop, 50, QColor.Hsl) + expected = QColor() + expected.setHsl(0, 30, 150) + self.assertEqual(color, expected) + + if __name__ == '__main__': unittest.main() diff --git a/qutebrowser/utils/misc.py b/qutebrowser/utils/misc.py index e2527aa0a..8ef39e166 100644 --- a/qutebrowser/utils/misc.py +++ b/qutebrowser/utils/misc.py @@ -33,6 +33,7 @@ from functools import reduce from distutils.version import StrictVersion as Version from PyQt5.QtCore import QCoreApplication, QStandardPaths, qVersion +from PyQt5.QtGui import QColor from pkg_resources import resource_string import qutebrowser @@ -285,3 +286,71 @@ def get_qt_args(namespace): argv.append('-' + argname) argv.append(val[0]) return argv + + +def _get_color_percentage(a_c1, a_c2, a_c3, b_c1, b_c2, b_c3, percent): + """Get a color which is percent% interpolated between start and end. + + Args: + a_c1, a_c2, a_c3: Start color components (R, G, B / H, S, V / H, S, L) + b_c1, b_c2, b_c3: End color components (R, G, B / H, S, V / H, S, L) + percent: Percentage to interpolate, 0-100. + 0: Start color will be returned. + 100: End color will be returned. + + Return: + A (c1, c2, c3) tuple with the interpolated color components. + + Raise: + ValueError if the percentage was invalid. + """ + if not 0 <= percent <= 100: + raise ValueError("percent needs to be between 0 and 100!") + out_c1 = round(a_c1 + (b_c1 - a_c1) * percent / 100) + out_c2 = round(a_c2 + (b_c2 - a_c2) * percent / 100) + out_c3 = round(a_c3 + (b_c3 - a_c3) * percent / 100) + return (out_c1, out_c2, out_c3) + + +def interpolate_color(start, end, percent, colorspace=QColor.Rgb): + """Get an interpolated color value. + + Args: + start: The start color. + end: The end color. + percent: Which value to get (0 - 100) + colorspace: The desired interpolation colorsystem, + QColor::{Rgb,Hsv,Hsl} (from QColor::Spec enum) + + Return: + The interpolated QColor, with the same spec as the given start color. + + Raise: + ValueError if invalid parameters are passed. + """ + if not start.isValid(): + raise ValueError("Invalid start color") + if not end.isValid(): + raise ValueError("Invalid end color") + out = QColor() + if colorspace == QColor.Rgb: + a_c1, a_c2, a_c3, _alpha = start.getRgb() + b_c1, b_c2, b_c3, _alpha = end.getRgb() + components = _get_color_percentage(a_c1, a_c2, a_c3, b_c1, b_c2, b_c3, + percent) + out.setRgb(*components) + elif colorspace == QColor.Hsv: + a_c1, a_c2, a_c3, _alpha = start.getHsv() + b_c1, b_c2, b_c3, _alpha = end.getHsv() + components = _get_color_percentage(a_c1, a_c2, a_c3, b_c1, b_c2, b_c3, + percent) + out.setHsv(*components) + elif colorspace == QColor.Hsl: + a_c1, a_c2, a_c3, _alpha = start.getHsl() + b_c1, b_c2, b_c3, _alpha = end.getHsl() + components = _get_color_percentage(a_c1, a_c2, a_c3, b_c1, b_c2, b_c3, + percent) + out.setHsl(*components) + else: + raise ValueError("Invalid colorspace!") + return out.convertTo(start.spec())