Merge pull request #187 from nyanpasu64/gui-color-picker

Add color picker to GUI
pull/357/head
nyanpasu64 2019-02-05 02:03:47 -08:00 zatwierdzone przez GitHub
commit 32ca28cf92
3 zmienionych plików z 214 dodań i 51 usunięć

Wyświetl plik

@ -211,6 +211,7 @@ class MainWindow(qw.QMainWindow):
cfg = yaml.load(cfg_path)
# Raises color getter exceptions
# FIXME if error halfway, clear "file path" and load empty model.
self.load_cfg(cfg, cfg_path)
except Exception as e:
@ -582,37 +583,6 @@ def default_property(path: str, default: Any) -> property:
return property(getter, setter)
def color2hex_property(path: str) -> property:
def getter(self: "ConfigModel"):
color_attr = rgetattr(self.cfg, path)
return color2hex(color_attr)
def setter(self: "ConfigModel", val: str):
color = color2hex(val)
rsetattr(self.cfg, path, color)
return property(getter, setter)
def color2hex_maybe_property(path: str) -> property:
# TODO turn into class, and use __set_name__ to determine assignment LHS=path.
def getter(self: "ConfigModel"):
color_attr = rgetattr(self.cfg, path)
if not color_attr:
return ""
return color2hex(color_attr)
def setter(self: "ConfigModel", val: str):
color: Optional[str]
if val:
color = color2hex(val)
else:
color = None
rsetattr(self.cfg, path, color)
return property(getter, setter)
def path_strip_quotes(path: str) -> str:
if len(path) and path[0] == path[-1] == '"':
return path[1:-1]
@ -656,10 +626,6 @@ class ConfigModel(PresentationModel):
combo_text[path] = flatten_text
del path
render__bg_color = color2hex_property("render__bg_color")
render__init_line_color = color2hex_property("render__init_line_color")
render__grid_color = color2hex_maybe_property("render__grid_color")
@property
def render_resolution(self) -> str:
render = self.cfg.render

Wyświetl plik

@ -18,6 +18,7 @@ from PyQt5.QtGui import QPalette, QColor
from PyQt5.QtWidgets import QWidget
from corrscope.config import CorrError, DumpableAttrs
from corrscope.gui.util import color2hex
from corrscope.triggers import lerp
from corrscope.util import obj_name, perr
@ -95,7 +96,12 @@ def map_gui(view: "MainWindow", model: PresentationModel) -> None:
) # dear pyqt, add generic mypy return types
for widget in widgets:
path = widget.objectName()
widget.bind_widget(model, path)
# Exclude nameless ColorText inside BoundColorWidget wrapper,
# since bind_widget(path="") will crash.
# BoundColorWidget.bind_widget() handles binding children.
if path:
widget.bind_widget(model, path)
Signal = Any
@ -108,7 +114,18 @@ class BoundWidget(QWidget):
pmodel: PresentationModel
path: str
def bind_widget(self, model: PresentationModel, path: str) -> None:
def bind_widget(
self, model: PresentationModel, path: str, connect_to_model=True
) -> None:
"""
connect_to_model=False means:
- self: ColorText is created and owned by BoundColorWidget wrapper.
- Wrapper forwards model changes to self (which updates other widgets).
(model.update_widget[path] != self)
- self.gui_changed invokes self.set_model() (which updates model
AND other widgets).
- wrapper.gui_changed signal is NEVER emitted.
"""
try:
self.default_palette = self.palette()
self.error_palette = self.calc_error_palette()
@ -117,8 +134,9 @@ class BoundWidget(QWidget):
self.path = path
self.cfg2gui()
# Allow widget to be updated by other events.
model.update_widget[path] = self.cfg2gui
if connect_to_model:
# Allow widget to be updated by other events.
model.update_widget[path] = self.cfg2gui
# Allow pmodel to be changed by widget.
self.gui_changed.connect(self.set_model)
@ -278,6 +296,181 @@ class BoundComboBox(qw.QComboBox, BoundWidget):
self.pmodel[self.path] = self.combo_symbols[combo_index]
# Color-specific widgets
class BoundColorWidget(BoundWidget, qw.QWidget):
"""
- set_gui(): Model is sent to self.text, which updates all other widgets.
- self.text: ColorText
- When self.text changes, it converts to hex, then updates the pmodel
and sends signal `hex_color` which updates check/button.
- self does NOT update the pmodel. (gui_changed is never emitted.)
"""
optional = False
def __init__(self, parent: qw.QWidget):
qw.QWidget.__init__(self, parent)
layout = qw.QHBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
self.setLayout(layout)
# Setup text field.
self.text = _ColorText(self, self.optional)
layout.addWidget(self.text) # http://doc.qt.io/qt-5/qlayout.html#addItem
# Setup checkbox
if self.optional:
self.check = _ColorCheckBox(self, self.text)
self.check.setToolTip("Enable/Disable Color")
layout.addWidget(self.check)
# Setup colored button.
self.button = _ColorButton(self, self.text)
self.button.setToolTip("Pick Color")
layout.addWidget(self.button)
# override BoundWidget
def bind_widget(self, model: PresentationModel, path: str) -> None:
super().bind_widget(model, path)
self.text.bind_widget(model, path, connect_to_model=False)
# impl BoundWidget
def set_gui(self, value: Optional[str]):
self.text.set_gui(value)
# impl BoundWidget
# Never gets emitted. self.text.set_model is responsible for updating model.
gui_changed = qc.pyqtSignal(str)
# impl BoundWidget
# Never gets called.
def set_model(self, value):
raise RuntimeError(
"BoundColorWidget.gui_changed -> set_model should not be called"
)
class OptionalColorWidget(BoundColorWidget):
optional = True
class _ColorText(BoundLineEdit):
"""
- Validates and converts colors to hex (from model AND gui)
- If __init__ optional, special-cases missing colors.
"""
def __init__(self, parent: QWidget, optional: bool):
super().__init__(parent)
self.optional = optional
hex_color = qc.pyqtSignal(str)
def set_gui(self, value: Optional[str]):
"""model2gui"""
if self.optional and not value:
value = ""
else:
value = color2hex(value) # raises CorrError if invalid.
# Don't write back to model immediately.
# Loading is a const process, only editing the GUI should change the model.
with qc.QSignalBlocker(self):
self.setText(value)
# Write to other GUI widgets immediately.
self.hex_color.emit(value) # calls button.set_color()
@pyqtSlot(str)
def set_model(self: BoundWidget, value: str):
"""gui2model"""
if self.optional and not value:
value = None
else:
try:
value = color2hex(value)
except CorrError:
self.setPalette(self.error_palette)
self.hex_color.emit("") # calls button.set_color()
return
self.setPalette(self.default_palette)
self.hex_color.emit(value or "") # calls button.set_color()
self.pmodel[self.path] = value
def sizeHint(self) -> qc.QSize:
"""Reduce the width taken up by #rrggbb color text boxes."""
return self.minimumSizeHint()
class _ColorButton(qw.QPushButton):
def __init__(self, parent: QWidget, text: "_ColorText"):
qw.QPushButton.__init__(self, parent)
self.clicked.connect(self.on_clicked)
# Initialize "current color"
self.curr_color = QColor()
# Initialize text
self.color_text = text
text.hex_color.connect(self.set_color)
@pyqtSlot()
def on_clicked(self):
# https://bugreports.qt.io/browse/QTBUG-38537
# On Windows, QSpinBox height is wrong if stylesheets are enabled.
# And QColorDialog(parent=self) contains QSpinBox and inherits our stylesheets.
# So set parent=self.window().
color: QColor = qw.QColorDialog.getColor(self.curr_color, self.window())
if not color.isValid():
return
self.color_text.setText(color.name()) # textChanged calls self.set_color()
@pyqtSlot(str)
def set_color(self, hex_color: str):
color = QColor(hex_color)
self.curr_color = color
if color.isValid():
# Tooltips inherit our styles. Don't change their background.
qss = f"{obj_name(self)} {{ background: {color.name()}; }}"
else:
qss = ""
self.setStyleSheet(qss)
class _ColorCheckBox(qw.QCheckBox):
def __init__(self, parent: QWidget, text: "_ColorText"):
qw.QCheckBox.__init__(self, parent)
self.stateChanged.connect(self.on_check)
self.color_text = text
text.hex_color.connect(self.set_color)
@pyqtSlot(str)
def set_color(self, hex_color: str):
with qc.QSignalBlocker(self):
self.setChecked(bool(hex_color))
@pyqtSlot(CheckState)
def on_check(self, value: CheckState):
"""Qt.PartiallyChecked probably should not happen."""
Qt = qc.Qt
assert value in [Qt.Unchecked, Qt.PartiallyChecked, Qt.Checked]
if value != Qt.Unchecked:
self.color_text.setText("#ffffff")
else:
self.color_text.setText("")
# Unused
def try_behead(string: str, header: str) -> Optional[str]:
if not string.startswith(header):

Wyświetl plik

@ -144,11 +144,7 @@
</widget>
</item>
<item row="1" column="1">
<widget class="BoundLineEdit" name="render__bg_color">
<property name="text">
<string>bg</string>
</property>
</widget>
<widget class="BoundColorWidget" name="render__bg_color" native="true"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="render__init_line_colorL">
@ -158,11 +154,7 @@
</widget>
</item>
<item row="2" column="1">
<widget class="BoundLineEdit" name="render__init_line_color">
<property name="text">
<string>fg</string>
</property>
</widget>
<widget class="BoundColorWidget" name="render__init_line_color" native="true"/>
</item>
<item row="3" column="0">
<widget class="QLabel" name="render__line_widthL">
@ -189,7 +181,7 @@
</widget>
</item>
<item row="4" column="1">
<widget class="BoundLineEdit" name="render__grid_color"/>
<widget class="OptionalColorWidget" name="render__grid_color" native="true"/>
</item>
<item row="5" column="0">
<widget class="QLabel" name="render__midline_colorL">
@ -199,7 +191,7 @@
</widget>
</item>
<item row="5" column="1">
<widget class="BoundLineEdit" name="render__midline_color"/>
<widget class="OptionalColorWidget" name="render__midline_color" native="true"/>
</item>
<item row="6" column="0">
<widget class="BoundCheckBox" name="render__v_midline">
@ -706,6 +698,18 @@
<extends>QTableView</extends>
<header>corrscope/gui/__init__.h</header>
</customwidget>
<customwidget>
<class>BoundColorWidget</class>
<extends>QWidget</extends>
<header>corrscope/gui/data_bind.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>OptionalColorWidget</class>
<extends>QWidget</extends>
<header>corrscope/gui/data_bind.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>