diff --git a/picard/log.py b/picard/log.py index 0c805277e..1cf6570d9 100644 --- a/picard/log.py +++ b/picard/log.py @@ -42,13 +42,13 @@ def get_effective_level(): return main_logger.getEffectiveLevel() -_feat = namedtuple('_feat', ['name', 'prefix', 'fgcolor']) +_feat = namedtuple('_feat', ['name', 'prefix', 'color_key']) levels_features = OrderedDict([ - (logging.ERROR, _feat('Error', 'E', 'red')), - (logging.WARNING, _feat('Warning', 'W', 'darkorange')), - (logging.INFO, _feat('Info', 'I', 'black')), - (logging.DEBUG, _feat('Debug', 'D', 'purple')), + (logging.ERROR, _feat('Error', 'E', 'log_error')), + (logging.WARNING, _feat('Warning', 'W', 'log_warning')), + (logging.INFO, _feat('Info', 'I', 'log_info')), + (logging.DEBUG, _feat('Debug', 'D', 'log_debug')), ]) diff --git a/picard/ui/colors.py b/picard/ui/colors.py new file mode 100644 index 000000000..8dea2dc6b --- /dev/null +++ b/picard/ui/colors.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +# +# Picard, the next-generation MusicBrainz tagger +# Copyright (C) 2019 Laurent Monin +# +# This program 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 2 +# of the License, or (at your option) any later version. +# +# This program 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 this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +from PyQt5 import QtGui + +from picard import config + + +class DefaultColor: + + def __init__(self, value, description): + self.value = value + self.description = description + + +_DEFAULT_COLORS = { + 'entity_error': DefaultColor('#C80000', N_("Errored entity")), + 'entity_pending': DefaultColor('#808080', N_("Pending entity")), + 'entity_saved': DefaultColor('#00AA00', N_("Saved entity")), + 'log_debug': DefaultColor('purple', N_('Log view text (debug)')), + 'log_error': DefaultColor('red', N_('Log view text (error)')), + 'log_info': DefaultColor('black', N_('Log view text (info)')), + 'log_warning': DefaultColor('darkorange', N_('Log view text (warning)')), + 'tagstatus_added': DefaultColor('green', N_("Tag added")), + 'tagstatus_changed': DefaultColor('darkgoldenrod', N_("Tag changed")), + 'tagstatus_removed': DefaultColor('red', N_("Tag removed")), +} + + +class InterfaceColors: + + def __init__(self): + self.default_colors() + + def default_colors(self): + self._colors = dict() + for color_key in _DEFAULT_COLORS: + color_value = _DEFAULT_COLORS[color_key].value + self.set_color(color_key, color_value) + + def set_colors(self, colors_dict): + for color_key in _DEFAULT_COLORS: + if color_key in colors_dict: + color_value = colors_dict[color_key] + else: + color_value = _DEFAULT_COLORS[color_key].value + self.set_color(color_key, color_value) + + def load_from_config(self): + self.set_colors(config.setting['interface_colors']) + + def get_colors(self): + return self._colors + + def get_color(self, color_key): + try: + return self._colors[color_key] + except KeyError: + if color_key in _DEFAULT_COLORS: + return _DEFAULT_COLORS[color_key].value + raise Exception("Unknown color key: %s" % color_key) + + def get_qcolor(self, color_key): + return QtGui.QColor(self.get_color(color_key)) + + @staticmethod + def get_color_description(color_key): + return _DEFAULT_COLORS[color_key].description + + def set_color(self, color_key, color_value): + if color_key in _DEFAULT_COLORS: + qcolor = QtGui.QColor(color_value) + if qcolor.isValid(): + color = qcolor.name() + else: + color = _DEFAULT_COLORS[color_key].value + self._colors[color_key] = color + else: + raise Exception("Unknown color key: %s" % color_key) + + +interface_colors = InterfaceColors() diff --git a/picard/ui/itemviews.py b/picard/ui/itemviews.py index bc1a88d8c..39279cbc7 100644 --- a/picard/ui/itemviews.py +++ b/picard/ui/itemviews.py @@ -57,6 +57,7 @@ from picard.util import ( ) from picard.ui.collectionmenu import CollectionMenu +from picard.ui.colors import interface_colors from picard.ui.ratingwidget import RatingWidget from picard.ui.scriptsmenu import ScriptsMenu @@ -141,16 +142,16 @@ class MainPanel(QtWidgets.QSplitter): TreeItem.text_color_secondary = self.palette() \ .brush(QtGui.QPalette.Disabled, QtGui.QPalette.Text).color() TrackItem.track_colors = { - File.NORMAL: config.setting["color_saved"], + File.NORMAL: interface_colors.get_qcolor('entity_saved'), File.CHANGED: TreeItem.text_color, - File.PENDING: config.setting["color_pending"], - File.ERROR: config.setting["color_error"], + File.PENDING: interface_colors.get_qcolor('entity_pending'), + File.ERROR: interface_colors.get_qcolor('entity_error'), } FileItem.file_colors = { File.NORMAL: TreeItem.text_color, File.CHANGED: TreeItem.text_color, - File.PENDING: config.setting["color_pending"], - File.ERROR: config.setting["color_error"], + File.PENDING: interface_colors.get_qcolor('entity_pending'), + File.ERROR: interface_colors.get_qcolor('entity_error'), } def save_state(self): @@ -244,12 +245,6 @@ class MainPanel(QtWidgets.QSplitter): class BaseTreeView(QtWidgets.QTreeWidget): - options = [ - config.Option("setting", "color_saved", QtGui.QColor(0, 128, 0)), - config.Option("setting", "color_error", QtGui.QColor(200, 0, 0)), - config.Option("setting", "color_pending", QtGui.QColor(128, 128, 128)), - ] - def __init__(self, window, parent=None): super().__init__(parent) self.window = window diff --git a/picard/ui/logview.py b/picard/ui/logview.py index 0b5a21198..01cba7b25 100644 --- a/picard/ui/logview.py +++ b/picard/ui/logview.py @@ -34,6 +34,7 @@ from picard import ( from picard.util import reconnect from picard.ui import PicardDialog +from picard.ui.colors import interface_colors class LogViewDialog(PicardDialog): @@ -218,13 +219,14 @@ class LogView(LogViewCommon): self.clear_highlight_button.setEnabled(bool(self.hl)) def _setup_formats(self): + interface_colors.load_from_config() self.formats = {} font = QtGui.QFont() font.setFamily("Monospace") for level, feat in log.levels_features.items(): text_fmt = QtGui.QTextCharFormat() text_fmt.setFont(font) - text_fmt.setForeground(QtGui.QColor(feat.fgcolor)) + text_fmt.setForeground(interface_colors.get_qcolor(feat.color_key)) self.formats[level] = text_fmt def _format(self, level): diff --git a/picard/ui/metadatabox.py b/picard/ui/metadatabox.py index 7dacb89fc..d53501fdf 100644 --- a/picard/ui/metadatabox.py +++ b/picard/ui/metadatabox.py @@ -43,6 +43,7 @@ from picard.util import ( ) from picard.util.tags import display_tag_name +from picard.ui.colors import interface_colors from picard.ui.edittagdialog import EditTagDialog @@ -204,12 +205,6 @@ class MetadataBox(QtWidgets.QTableWidget): self.setTabKeyNavigation(False) self.setStyleSheet("QTableWidget {border: none;}") self.setAttribute(QtCore.Qt.WA_MacShowFocusRect, 1) - self.colors = { - TagStatus.NOCHANGE: self.palette().color(QtGui.QPalette.Text), - TagStatus.REMOVED: QtGui.QBrush(QtGui.QColor("red")), - TagStatus.ADDED: QtGui.QBrush(QtGui.QColor("green")), - TagStatus.CHANGED: QtGui.QBrush(QtGui.QColor("darkgoldenrod")) - } self.files = set() self.tracks = set() self.objects = set() @@ -458,6 +453,13 @@ class MetadataBox(QtWidgets.QTableWidget): if not (files or tracks): return None + self.colors = { + TagStatus.NOCHANGE: self.palette().color(QtGui.QPalette.Text), + TagStatus.REMOVED: QtGui.QBrush(interface_colors.get_qcolor('tagstatus_removed')), + TagStatus.ADDED: QtGui.QBrush(interface_colors.get_qcolor('tagstatus_added')), + TagStatus.CHANGED: QtGui.QBrush(interface_colors.get_qcolor('tagstatus_changed')) + } + tag_diff = TagDiff(max_length_diff=config.setting["ignore_track_duration_difference_under"]) orig_tags = tag_diff.orig new_tags = tag_diff.new diff --git a/picard/ui/options/dialog.py b/picard/ui/options/dialog.py index 7e00e2b79..dae05915a 100644 --- a/picard/ui/options/dialog.py +++ b/picard/ui/options/dialog.py @@ -43,6 +43,7 @@ from picard.ui.options import ( # pylint: disable=unused-import general, genres, interface, + interface_colors, matching, metadata, network, diff --git a/picard/ui/options/interface_colors.py b/picard/ui/options/interface_colors.py new file mode 100644 index 000000000..1ca09bc98 --- /dev/null +++ b/picard/ui/options/interface_colors.py @@ -0,0 +1,140 @@ +# -*- coding: utf-8 -*- +# +# Picard, the next-generation MusicBrainz tagger +# Copyright (C) 2019 Laurent Monin +# +# This program 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 2 +# of the License, or (at your option) any later version. +# +# This program 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 this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +from functools import partial + +from PyQt5 import ( + QtCore, + QtGui, + QtWidgets, +) + +from picard import config + +from picard.ui.colors import interface_colors +from picard.ui.options import ( + OptionsPage, + register_options_page, +) +from picard.ui.ui_options_interface_colors import Ui_InterfaceColorsOptionsPage + + +class ColorButton(QtWidgets.QPushButton): + + color_changed = QtCore.pyqtSignal(str) + + def __init__(self, initial_color=None, parent=None): + super().__init__(' ', parent=parent) + + color = QtGui.QColor(initial_color) + if not color.isValid(): + color = QtGui.QColor("black") + self.color = color + self.clicked.connect(self.open_color_dialog) + self.update_color() + + def update_color(self): + self.setStyleSheet("QPushButton { background-color: %s; }" % self.color.name()) + + def open_color_dialog(self): + dlg = QtWidgets.QColorDialog() + new_color = dlg.getColor(self.color, title=_("Choose a color"), parent=self) + + if new_color.isValid(): + self.color = new_color + self.update_color() + self.color_changed.emit(self.color.name()) + + +def delete_items_of_layout(layout): + # Credits: + # https://stackoverflow.com/a/45790404 + # https://riverbankcomputing.com/pipermail/pyqt/2009-November/025214.html + if layout is not None: + while layout.count(): + item = layout.takeAt(0) + widget = item.widget() + if widget is not None: + widget.setParent(None) + else: + delete_items_of_layout(item.layout()) + + +class InterfaceColorsOptionsPage(OptionsPage): + + NAME = "interface_colors" + TITLE = N_("Colors") + PARENT = "interface" + SORT_ORDER = 30 + ACTIVE = True + + options = [ + config.Option("setting", "interface_colors", interface_colors.get_colors()), + ] + + def __init__(self, parent=None): + super().__init__(parent) + self.ui = Ui_InterfaceColorsOptionsPage() + self.ui.setupUi(self) + self.new_colors = {} + self.colors_list = QtWidgets.QVBoxLayout() + self.ui.colors.setLayout(self.colors_list) + + def update_color_selectors(self): + if self.colors_list: + delete_items_of_layout(self.colors_list) + + def color_changed(color_key, color_value): + interface_colors.set_color(color_key, color_value) + + for color_key, color_value in interface_colors.get_colors().items(): + widget = QtWidgets.QWidget() + + hlayout = QtWidgets.QHBoxLayout() + hlayout.setContentsMargins(0, 0, 0, 0) + + label = QtWidgets.QLabel(interface_colors.get_color_description(color_key)) + hlayout.addWidget(label) + + spacer = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + hlayout.addItem(spacer) + + button = ColorButton(color_value) + button.color_changed.connect(partial(color_changed, color_key)) + hlayout.addWidget(button, 0, QtCore.Qt.AlignRight) + + widget.setLayout(hlayout) + self.colors_list.addWidget(widget) + + spacerItem1 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.colors_list.addItem(spacerItem1) + + def load(self): + interface_colors.load_from_config() + self.update_color_selectors() + + def save(self): + config.setting['interface_colors'] = interface_colors.get_colors() + + def restore_defaults(self): + interface_colors.default_colors() + self.update_color_selectors() + + +register_options_page(InterfaceColorsOptionsPage) diff --git a/picard/ui/ui_options_interface_colors.py b/picard/ui/ui_options_interface_colors.py new file mode 100644 index 000000000..9e4784220 --- /dev/null +++ b/picard/ui/ui_options_interface_colors.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- + +# Automatically generated - don't edit. +# Use `python setup.py build_ui` to update it. + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_InterfaceColorsOptionsPage(object): + def setupUi(self, InterfaceColorsOptionsPage): + InterfaceColorsOptionsPage.setObjectName("InterfaceColorsOptionsPage") + InterfaceColorsOptionsPage.resize(171, 137) + self.vboxlayout = QtWidgets.QVBoxLayout(InterfaceColorsOptionsPage) + self.vboxlayout.setContentsMargins(0, 0, 0, 0) + self.vboxlayout.setSpacing(6) + self.vboxlayout.setObjectName("vboxlayout") + self.scrollArea = QtWidgets.QScrollArea(InterfaceColorsOptionsPage) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.scrollArea.sizePolicy().hasHeightForWidth()) + self.scrollArea.setSizePolicy(sizePolicy) + self.scrollArea.setFrameShape(QtWidgets.QFrame.NoFrame) + self.scrollArea.setFrameShadow(QtWidgets.QFrame.Plain) + self.scrollArea.setLineWidth(0) + self.scrollArea.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.scrollArea.setWidgetResizable(True) + self.scrollArea.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop) + self.scrollArea.setObjectName("scrollArea") + self.scrollAreaWidgetContents = QtWidgets.QWidget() + self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 199, 137)) + self.scrollAreaWidgetContents.setObjectName("scrollAreaWidgetContents") + self.verticalLayout = QtWidgets.QVBoxLayout(self.scrollAreaWidgetContents) + self.verticalLayout.setContentsMargins(9, 9, 9, 9) + self.verticalLayout.setSpacing(6) + self.verticalLayout.setObjectName("verticalLayout") + self.colors = QtWidgets.QGroupBox(self.scrollAreaWidgetContents) + self.colors.setObjectName("colors") + self.verticalLayout.addWidget(self.colors) + self.scrollArea.setWidget(self.scrollAreaWidgetContents) + self.vboxlayout.addWidget(self.scrollArea) + + self.retranslateUi(InterfaceColorsOptionsPage) + QtCore.QMetaObject.connectSlotsByName(InterfaceColorsOptionsPage) + + def retranslateUi(self, InterfaceColorsOptionsPage): + _translate = QtCore.QCoreApplication.translate + self.colors.setTitle(_("Colors")) + + diff --git a/picard/ui/ui_options_interface_theme.py b/picard/ui/ui_options_interface_theme.py new file mode 100644 index 000000000..7542d5030 --- /dev/null +++ b/picard/ui/ui_options_interface_theme.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- + +# Automatically generated - don't edit. +# Use `python setup.py build_ui` to update it. + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_InterfaceThemeOptionsPage(object): + def setupUi(self, InterfaceThemeOptionsPage): + InterfaceThemeOptionsPage.setObjectName("InterfaceThemeOptionsPage") + InterfaceThemeOptionsPage.resize(171, 137) + self.vboxlayout = QtWidgets.QVBoxLayout(InterfaceThemeOptionsPage) + self.vboxlayout.setContentsMargins(0, 0, 0, 0) + self.vboxlayout.setSpacing(6) + self.vboxlayout.setObjectName("vboxlayout") + self.scrollArea = QtWidgets.QScrollArea(InterfaceThemeOptionsPage) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.scrollArea.sizePolicy().hasHeightForWidth()) + self.scrollArea.setSizePolicy(sizePolicy) + self.scrollArea.setFrameShape(QtWidgets.QFrame.NoFrame) + self.scrollArea.setFrameShadow(QtWidgets.QFrame.Plain) + self.scrollArea.setLineWidth(0) + self.scrollArea.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.scrollArea.setWidgetResizable(True) + self.scrollArea.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop) + self.scrollArea.setObjectName("scrollArea") + self.scrollAreaWidgetContents = QtWidgets.QWidget() + self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 199, 137)) + self.scrollAreaWidgetContents.setObjectName("scrollAreaWidgetContents") + self.verticalLayout = QtWidgets.QVBoxLayout(self.scrollAreaWidgetContents) + self.verticalLayout.setContentsMargins(9, 9, 9, 9) + self.verticalLayout.setSpacing(6) + self.verticalLayout.setObjectName("verticalLayout") + self.colors = QtWidgets.QGroupBox(self.scrollAreaWidgetContents) + self.colors.setObjectName("colors") + self.verticalLayout.addWidget(self.colors) + self.scrollArea.setWidget(self.scrollAreaWidgetContents) + self.vboxlayout.addWidget(self.scrollArea) + + self.retranslateUi(InterfaceThemeOptionsPage) + QtCore.QMetaObject.connectSlotsByName(InterfaceThemeOptionsPage) + + def retranslateUi(self, InterfaceThemeOptionsPage): + _translate = QtCore.QCoreApplication.translate + self.colors.setTitle(_("Colors")) + + diff --git a/ui/options_interface_colors.ui b/ui/options_interface_colors.ui new file mode 100644 index 000000000..797cac6f4 --- /dev/null +++ b/ui/options_interface_colors.ui @@ -0,0 +1,95 @@ + + + InterfaceColorsOptionsPage + + + + 0 + 0 + 171 + 137 + + + + + 6 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + QFrame::NoFrame + + + QFrame::Plain + + + 0 + + + Qt::ScrollBarAlwaysOff + + + true + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + 0 + 0 + 199 + 137 + + + + + 6 + + + 9 + + + 9 + + + 9 + + + 9 + + + + + Colors + + + + + + + + + + + +