kopia lustrzana https://github.com/corrscope/corrscope
Merge pull request #191 from nyanpasu64/gui-unit-suffix
Add unit suffixes to GUI spinboxespull/357/head
commit
3ccbcc6734
|
@ -24,6 +24,8 @@ __all__ = [
|
||||||
"copy_config",
|
"copy_config",
|
||||||
"DumpableAttrs",
|
"DumpableAttrs",
|
||||||
"KeywordAttrs",
|
"KeywordAttrs",
|
||||||
|
"with_units",
|
||||||
|
"get_units",
|
||||||
"Alias",
|
"Alias",
|
||||||
"Ignored",
|
"Ignored",
|
||||||
"DumpEnumAsStr",
|
"DumpEnumAsStr",
|
||||||
|
@ -222,6 +224,18 @@ class KeywordAttrs(DumpableAttrs):
|
||||||
super().__init_subclass__(kw_only=True, **kwargs)
|
super().__init_subclass__(kw_only=True, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
UNIT_SUFFIX = "suffix"
|
||||||
|
|
||||||
|
|
||||||
|
def with_units(unit, **kwargs):
|
||||||
|
metadata = {UNIT_SUFFIX: f" {unit}"}
|
||||||
|
return attr.ib(metadata=metadata, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def get_units(field: attr.Attribute) -> str:
|
||||||
|
return field.metadata.get(UNIT_SUFFIX, "")
|
||||||
|
|
||||||
|
|
||||||
@attr.dataclass
|
@attr.dataclass
|
||||||
class Alias:
|
class Alias:
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -12,7 +12,7 @@ import attr
|
||||||
|
|
||||||
from corrscope import outputs as outputs_
|
from corrscope import outputs as outputs_
|
||||||
from corrscope.channel import Channel, ChannelConfig
|
from corrscope.channel import Channel, ChannelConfig
|
||||||
from corrscope.config import KeywordAttrs, DumpEnumAsStr, CorrError
|
from corrscope.config import KeywordAttrs, DumpEnumAsStr, CorrError, with_units
|
||||||
from corrscope.layout import LayoutConfig
|
from corrscope.layout import LayoutConfig
|
||||||
from corrscope.renderer import MatplotlibRenderer, RendererConfig, Renderer
|
from corrscope.renderer import MatplotlibRenderer, RendererConfig, Renderer
|
||||||
from corrscope.triggers import (
|
from corrscope.triggers import (
|
||||||
|
@ -51,13 +51,13 @@ class Config(
|
||||||
""" Default values indicate optional attributes. """
|
""" Default values indicate optional attributes. """
|
||||||
|
|
||||||
master_audio: Optional[str]
|
master_audio: Optional[str]
|
||||||
begin_time: float = 0
|
begin_time: float = with_units("s", default=0)
|
||||||
end_time: Optional[float] = None
|
end_time: Optional[float] = None
|
||||||
|
|
||||||
fps: int
|
fps: int
|
||||||
|
|
||||||
trigger_ms: int
|
trigger_ms: int = with_units("ms")
|
||||||
render_ms: int
|
render_ms: int = with_units("ms")
|
||||||
|
|
||||||
# Performance
|
# Performance
|
||||||
trigger_subsampling: int = 1
|
trigger_subsampling: int = 1
|
||||||
|
|
|
@ -10,14 +10,16 @@ from typing import (
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
Union,
|
Union,
|
||||||
Sequence,
|
Sequence,
|
||||||
|
Tuple,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
import attr
|
||||||
from PyQt5 import QtWidgets as qw, QtCore as qc
|
from PyQt5 import QtWidgets as qw, QtCore as qc
|
||||||
from PyQt5.QtCore import pyqtSlot
|
from PyQt5.QtCore import pyqtSlot
|
||||||
from PyQt5.QtGui import QPalette, QColor
|
from PyQt5.QtGui import QPalette, QColor
|
||||||
from PyQt5.QtWidgets import QWidget
|
from PyQt5.QtWidgets import QWidget
|
||||||
|
|
||||||
from corrscope.config import CorrError, DumpableAttrs
|
from corrscope.config import CorrError, DumpableAttrs, get_units
|
||||||
from corrscope.gui.util import color2hex
|
from corrscope.gui.util import color2hex
|
||||||
from corrscope.triggers import lerp
|
from corrscope.triggers import lerp
|
||||||
from corrscope.util import obj_name, perr
|
from corrscope.util import obj_name, perr
|
||||||
|
@ -229,12 +231,25 @@ class BoundLineEdit(qw.QLineEdit, BoundWidget):
|
||||||
|
|
||||||
|
|
||||||
class BoundSpinBox(qw.QSpinBox, BoundWidget):
|
class BoundSpinBox(qw.QSpinBox, BoundWidget):
|
||||||
|
def bind_widget(self, model: PresentationModel, path: str, *args, **kwargs) -> None:
|
||||||
|
BoundWidget.bind_widget(self, model, path, *args, **kwargs)
|
||||||
|
try:
|
||||||
|
parent, name = flatten_attr(model.cfg, path)
|
||||||
|
except AttributeError:
|
||||||
|
return
|
||||||
|
|
||||||
|
fields = attr.fields_dict(type(parent))
|
||||||
|
field = fields[name]
|
||||||
|
self.setSuffix(get_units(field))
|
||||||
|
|
||||||
set_gui = alias("setValue")
|
set_gui = alias("setValue")
|
||||||
gui_changed = alias("valueChanged")
|
gui_changed = alias("valueChanged")
|
||||||
set_model = model_setter(int)
|
set_model = model_setter(int)
|
||||||
|
|
||||||
|
|
||||||
class BoundDoubleSpinBox(qw.QDoubleSpinBox, BoundWidget):
|
class BoundDoubleSpinBox(qw.QDoubleSpinBox, BoundWidget):
|
||||||
|
bind_widget = BoundSpinBox.bind_widget
|
||||||
|
|
||||||
set_gui = alias("setValue")
|
set_gui = alias("setValue")
|
||||||
gui_changed = alias("valueChanged")
|
gui_changed = alias("valueChanged")
|
||||||
set_model = model_setter(float)
|
set_model = model_setter(float)
|
||||||
|
@ -495,11 +510,13 @@ def rgetattr(obj: DumpableAttrs, dunder_delim_path: str, *default) -> Any:
|
||||||
:return: obj.attr1.attr2.etc
|
:return: obj.attr1.attr2.etc
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def _getattr(obj, attr):
|
|
||||||
return getattr(obj, attr, *default)
|
|
||||||
|
|
||||||
attrs: List[Any] = dunder_delim_path.split(DUNDER)
|
attrs: List[Any] = dunder_delim_path.split(DUNDER)
|
||||||
return functools.reduce(_getattr, [obj] + attrs)
|
try:
|
||||||
|
return functools.reduce(getattr, attrs, obj)
|
||||||
|
except AttributeError:
|
||||||
|
if default:
|
||||||
|
return default[0]
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
def rhasattr(obj, dunder_delim_path: str):
|
def rhasattr(obj, dunder_delim_path: str):
|
||||||
|
@ -510,6 +527,20 @@ def rhasattr(obj, dunder_delim_path: str):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def flatten_attr(obj, dunder_delim_path: str) -> Tuple[Any, str]:
|
||||||
|
"""
|
||||||
|
:param obj: Object
|
||||||
|
:param dunder_delim_path: 'attr1__attr2__etc'
|
||||||
|
:return: (shallow_obj, name) such that
|
||||||
|
getattr(shallow_obj, name) == rgetattr(obj, dunder_delim_path).
|
||||||
|
"""
|
||||||
|
|
||||||
|
parent, _, name = dunder_delim_path.rpartition(DUNDER)
|
||||||
|
parent_obj = rgetattr(obj, parent) if parent else obj
|
||||||
|
|
||||||
|
return parent_obj, name
|
||||||
|
|
||||||
|
|
||||||
# https://stackoverflow.com/a/31174427/2683842
|
# https://stackoverflow.com/a/31174427/2683842
|
||||||
def rsetattr(obj, dunder_delim_path: str, val):
|
def rsetattr(obj, dunder_delim_path: str, val):
|
||||||
"""
|
"""
|
||||||
|
@ -517,7 +548,5 @@ def rsetattr(obj, dunder_delim_path: str, val):
|
||||||
:param dunder_delim_path: 'attr1__attr2__etc'
|
:param dunder_delim_path: 'attr1__attr2__etc'
|
||||||
:param val: obj.attr1.attr2.etc = val
|
:param val: obj.attr1.attr2.etc = val
|
||||||
"""
|
"""
|
||||||
parent, _, name = dunder_delim_path.rpartition(DUNDER)
|
parent_obj, name = flatten_attr(obj, dunder_delim_path)
|
||||||
parent_obj = rgetattr(obj, parent) if parent else obj
|
|
||||||
|
|
||||||
return setattr(parent_obj, name, val)
|
return setattr(parent_obj, name, val)
|
||||||
|
|
|
@ -54,7 +54,7 @@
|
||||||
<item row="1" column="0">
|
<item row="1" column="0">
|
||||||
<widget class="QLabel" name="trigger_msL">
|
<widget class="QLabel" name="trigger_msL">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Trigger Width (ms)</string>
|
<string>Trigger Width</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
@ -71,7 +71,7 @@
|
||||||
<item row="2" column="0">
|
<item row="2" column="0">
|
||||||
<widget class="QLabel" name="render_msL">
|
<widget class="QLabel" name="render_msL">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Render Width (ms)</string>
|
<string>Render Width</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
@ -102,7 +102,7 @@
|
||||||
<item row="4" column="0">
|
<item row="4" column="0">
|
||||||
<widget class="QLabel" name="begin_timeL">
|
<widget class="QLabel" name="begin_timeL">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Begin Time (s)</string>
|
<string>Begin Time</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
|
|
@ -6,7 +6,7 @@ import attr
|
||||||
import matplotlib
|
import matplotlib
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from corrscope.config import DumpableAttrs
|
from corrscope.config import DumpableAttrs, with_units
|
||||||
from corrscope.layout import RendererLayout, LayoutConfig, EdgeFinder
|
from corrscope.layout import RendererLayout, LayoutConfig, EdgeFinder
|
||||||
from corrscope.outputs import RGB_DEPTH, ByteBuffer
|
from corrscope.outputs import RGB_DEPTH, ByteBuffer
|
||||||
from corrscope.util import coalesce
|
from corrscope.util import coalesce
|
||||||
|
@ -54,7 +54,7 @@ def default_color() -> str:
|
||||||
class RendererConfig(DumpableAttrs, always_dump="*"):
|
class RendererConfig(DumpableAttrs, always_dump="*"):
|
||||||
width: int
|
width: int
|
||||||
height: int
|
height: int
|
||||||
line_width: float = 1.5
|
line_width: float = with_units("px", default=1.5)
|
||||||
|
|
||||||
bg_color: str = "#000000"
|
bg_color: str = "#000000"
|
||||||
init_line_color: str = default_color()
|
init_line_color: str = default_color()
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import attr
|
||||||
import pytest
|
import pytest
|
||||||
from ruamel.yaml import yaml_object
|
from ruamel.yaml import yaml_object
|
||||||
|
|
||||||
|
@ -9,12 +10,13 @@ from corrscope.config import (
|
||||||
Ignored,
|
Ignored,
|
||||||
CorrError,
|
CorrError,
|
||||||
CorrWarning,
|
CorrWarning,
|
||||||
|
with_units,
|
||||||
|
get_units,
|
||||||
)
|
)
|
||||||
|
|
||||||
# YAML Idiosyncrasies: https://docs.saltstack.com/en/develop/topics/troubleshooting/yaml_idiosyncrasies.html
|
# YAML Idiosyncrasies: https://docs.saltstack.com/en/develop/topics/troubleshooting/yaml_idiosyncrasies.html
|
||||||
|
|
||||||
# Load/dump infrastructure testing
|
# Load/dump infrastructure testing
|
||||||
import attr
|
|
||||||
|
|
||||||
|
|
||||||
def test_dumpable_attrs():
|
def test_dumpable_attrs():
|
||||||
|
@ -52,6 +54,26 @@ def test_yaml_object():
|
||||||
assert s == "!Bar {}\n"
|
assert s == "!Bar {}\n"
|
||||||
|
|
||||||
|
|
||||||
|
# Test per-field unit suffixes (used by GUI)
|
||||||
|
|
||||||
|
|
||||||
|
def test_unit_suffix():
|
||||||
|
class Foo(DumpableAttrs):
|
||||||
|
xs: int = with_units("xs")
|
||||||
|
ys: int = with_units("ys", default=2)
|
||||||
|
no_unit: int = 3
|
||||||
|
|
||||||
|
# Assert class constructor works.
|
||||||
|
foo = Foo(1, 2, 3)
|
||||||
|
foo_default = Foo(1)
|
||||||
|
|
||||||
|
# Assert units work.
|
||||||
|
foo_fields = attr.fields(Foo)
|
||||||
|
assert get_units(foo_fields.xs) == " xs"
|
||||||
|
assert get_units(foo_fields.ys) == " ys"
|
||||||
|
assert get_units(foo_fields.no_unit) == ""
|
||||||
|
|
||||||
|
|
||||||
# Dataclass dump testing
|
# Dataclass dump testing
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,24 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from corrscope.gui.data_bind import rgetattr, rsetattr, rhasattr
|
from corrscope.gui.data_bind import rgetattr, rsetattr, rhasattr, flatten_attr
|
||||||
|
|
||||||
|
|
||||||
|
class Person(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.pet = Pet()
|
||||||
|
self.residence = Residence()
|
||||||
|
|
||||||
|
|
||||||
|
class Pet(object):
|
||||||
|
def __init__(self, name="Fido", species="Dog"):
|
||||||
|
self.name = name
|
||||||
|
self.species = species
|
||||||
|
|
||||||
|
|
||||||
|
class Residence(object):
|
||||||
|
def __init__(self, type="House", sqft=None):
|
||||||
|
self.type = type
|
||||||
|
self.sqft = sqft
|
||||||
|
|
||||||
|
|
||||||
def test_rgetattr():
|
def test_rgetattr():
|
||||||
|
@ -9,22 +27,6 @@ def test_rgetattr():
|
||||||
|
|
||||||
https://stackoverflow__com/a/31174427/
|
https://stackoverflow__com/a/31174427/
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class Person(object):
|
|
||||||
def __init__(self):
|
|
||||||
self.pet = Pet()
|
|
||||||
self.residence = Residence()
|
|
||||||
|
|
||||||
class Pet(object):
|
|
||||||
def __init__(self, name="Fido", species="Dog"):
|
|
||||||
self.name = name
|
|
||||||
self.species = species
|
|
||||||
|
|
||||||
class Residence(object):
|
|
||||||
def __init__(self, type="House", sqft=None):
|
|
||||||
self.type = type
|
|
||||||
self.sqft = sqft
|
|
||||||
|
|
||||||
p = Person()
|
p = Person()
|
||||||
|
|
||||||
# Test rgetattr(present)
|
# Test rgetattr(present)
|
||||||
|
@ -55,9 +57,20 @@ def test_rgetattr():
|
||||||
assert not rhasattr(p, "ghost__species")
|
assert not rhasattr(p, "ghost__species")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.xfail(
|
def test_flatten_attr():
|
||||||
reason="rgetattr copied from Stack Overflow and subtly broken", strict=True
|
p = Person()
|
||||||
)
|
|
||||||
|
# Test nested
|
||||||
|
flat, name = flatten_attr(p, "pet__name")
|
||||||
|
assert flat is p.pet
|
||||||
|
assert name == "name"
|
||||||
|
|
||||||
|
# Test 1 level
|
||||||
|
flat, name = flatten_attr(p, "pet")
|
||||||
|
assert flat is p
|
||||||
|
assert name == "pet"
|
||||||
|
|
||||||
|
|
||||||
def test_rgetattr_broken():
|
def test_rgetattr_broken():
|
||||||
"""
|
"""
|
||||||
rgetattr(default) fails to short-circuit/return on the first missing attribute.
|
rgetattr(default) fails to short-circuit/return on the first missing attribute.
|
||||||
|
@ -72,5 +85,5 @@ def test_rgetattr_broken():
|
||||||
- None.foo AKA return 1 to caller
|
- None.foo AKA return 1 to caller
|
||||||
"""
|
"""
|
||||||
|
|
||||||
result = rgetattr(None, "foo__bar__imag", 1)
|
result = rgetattr(object(), "nothing__imag", 1)
|
||||||
assert result == 1, result
|
assert result == 1, result
|
Ładowanie…
Reference in New Issue