PICARD-1524: Make colours user-configurable

This commit is contained in:
Laurent Monin
2019-06-08 17:00:45 +02:00
parent b3eeb55995
commit 2f45d40017
10 changed files with 456 additions and 23 deletions

View File

@@ -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')),
])

98
picard/ui/colors.py Normal file
View File

@@ -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()

View File

@@ -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

View File

@@ -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):

View File

@@ -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

View File

@@ -43,6 +43,7 @@ from picard.ui.options import ( # pylint: disable=unused-import
general,
genres,
interface,
interface_colors,
matching,
metadata,
network,

View File

@@ -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)

View File

@@ -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"))

View File

@@ -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"))

View File

@@ -0,0 +1,95 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>InterfaceColorsOptionsPage</class>
<widget class="QWidget" name="InterfaceColorsOptionsPage">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>171</width>
<height>137</height>
</rect>
</property>
<layout class="QVBoxLayout">
<property name="spacing">
<number>6</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QScrollArea" name="scrollArea">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<widget class="QWidget" name="scrollAreaWidgetContents">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>199</width>
<height>137</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>6</number>
</property>
<property name="leftMargin">
<number>9</number>
</property>
<property name="topMargin">
<number>9</number>
</property>
<property name="rightMargin">
<number>9</number>
</property>
<property name="bottomMargin">
<number>9</number>
</property>
<item>
<widget class="QGroupBox" name="colors">
<property name="title">
<string>Colors</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>