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",
|
||||
"DumpableAttrs",
|
||||
"KeywordAttrs",
|
||||
"with_units",
|
||||
"get_units",
|
||||
"Alias",
|
||||
"Ignored",
|
||||
"DumpEnumAsStr",
|
||||
|
@ -222,6 +224,18 @@ class KeywordAttrs(DumpableAttrs):
|
|||
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
|
||||
class Alias:
|
||||
"""
|
||||
|
|
|
@ -12,7 +12,7 @@ import attr
|
|||
|
||||
from corrscope import outputs as outputs_
|
||||
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.renderer import MatplotlibRenderer, RendererConfig, Renderer
|
||||
from corrscope.triggers import (
|
||||
|
@ -51,13 +51,13 @@ class Config(
|
|||
""" Default values indicate optional attributes. """
|
||||
|
||||
master_audio: Optional[str]
|
||||
begin_time: float = 0
|
||||
begin_time: float = with_units("s", default=0)
|
||||
end_time: Optional[float] = None
|
||||
|
||||
fps: int
|
||||
|
||||
trigger_ms: int
|
||||
render_ms: int
|
||||
trigger_ms: int = with_units("ms")
|
||||
render_ms: int = with_units("ms")
|
||||
|
||||
# Performance
|
||||
trigger_subsampling: int = 1
|
||||
|
|
|
@ -10,14 +10,16 @@ from typing import (
|
|||
TYPE_CHECKING,
|
||||
Union,
|
||||
Sequence,
|
||||
Tuple,
|
||||
)
|
||||
|
||||
import attr
|
||||
from PyQt5 import QtWidgets as qw, QtCore as qc
|
||||
from PyQt5.QtCore import pyqtSlot
|
||||
from PyQt5.QtGui import QPalette, QColor
|
||||
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.triggers import lerp
|
||||
from corrscope.util import obj_name, perr
|
||||
|
@ -229,12 +231,25 @@ class BoundLineEdit(qw.QLineEdit, 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")
|
||||
gui_changed = alias("valueChanged")
|
||||
set_model = model_setter(int)
|
||||
|
||||
|
||||
class BoundDoubleSpinBox(qw.QDoubleSpinBox, BoundWidget):
|
||||
bind_widget = BoundSpinBox.bind_widget
|
||||
|
||||
set_gui = alias("setValue")
|
||||
gui_changed = alias("valueChanged")
|
||||
set_model = model_setter(float)
|
||||
|
@ -495,11 +510,13 @@ def rgetattr(obj: DumpableAttrs, dunder_delim_path: str, *default) -> Any:
|
|||
:return: obj.attr1.attr2.etc
|
||||
"""
|
||||
|
||||
def _getattr(obj, attr):
|
||||
return getattr(obj, attr, *default)
|
||||
|
||||
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):
|
||||
|
@ -510,6 +527,20 @@ def rhasattr(obj, dunder_delim_path: str):
|
|||
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
|
||||
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 val: obj.attr1.attr2.etc = val
|
||||
"""
|
||||
parent, _, name = dunder_delim_path.rpartition(DUNDER)
|
||||
parent_obj = rgetattr(obj, parent) if parent else obj
|
||||
|
||||
parent_obj, name = flatten_attr(obj, dunder_delim_path)
|
||||
return setattr(parent_obj, name, val)
|
||||
|
|
|
@ -54,7 +54,7 @@
|
|||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="trigger_msL">
|
||||
<property name="text">
|
||||
<string>Trigger Width (ms)</string>
|
||||
<string>Trigger Width</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
|
@ -71,7 +71,7 @@
|
|||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="render_msL">
|
||||
<property name="text">
|
||||
<string>Render Width (ms)</string>
|
||||
<string>Render Width</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
|
@ -102,7 +102,7 @@
|
|||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="begin_timeL">
|
||||
<property name="text">
|
||||
<string>Begin Time (s)</string>
|
||||
<string>Begin Time</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
|
|
|
@ -6,7 +6,7 @@ import attr
|
|||
import matplotlib
|
||||
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.outputs import RGB_DEPTH, ByteBuffer
|
||||
from corrscope.util import coalesce
|
||||
|
@ -54,7 +54,7 @@ def default_color() -> str:
|
|||
class RendererConfig(DumpableAttrs, always_dump="*"):
|
||||
width: int
|
||||
height: int
|
||||
line_width: float = 1.5
|
||||
line_width: float = with_units("px", default=1.5)
|
||||
|
||||
bg_color: str = "#000000"
|
||||
init_line_color: str = default_color()
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import attr
|
||||
import pytest
|
||||
from ruamel.yaml import yaml_object
|
||||
|
||||
|
@ -9,12 +10,13 @@ from corrscope.config import (
|
|||
Ignored,
|
||||
CorrError,
|
||||
CorrWarning,
|
||||
with_units,
|
||||
get_units,
|
||||
)
|
||||
|
||||
# YAML Idiosyncrasies: https://docs.saltstack.com/en/develop/topics/troubleshooting/yaml_idiosyncrasies.html
|
||||
|
||||
# Load/dump infrastructure testing
|
||||
import attr
|
||||
|
||||
|
||||
def test_dumpable_attrs():
|
||||
|
@ -52,6 +54,26 @@ def test_yaml_object():
|
|||
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
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,24 @@
|
|||
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():
|
||||
|
@ -9,22 +27,6 @@ def test_rgetattr():
|
|||
|
||||
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()
|
||||
|
||||
# Test rgetattr(present)
|
||||
|
@ -55,9 +57,20 @@ def test_rgetattr():
|
|||
assert not rhasattr(p, "ghost__species")
|
||||
|
||||
|
||||
@pytest.mark.xfail(
|
||||
reason="rgetattr copied from Stack Overflow and subtly broken", strict=True
|
||||
)
|
||||
def test_flatten_attr():
|
||||
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():
|
||||
"""
|
||||
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
|
||||
"""
|
||||
|
||||
result = rgetattr(None, "foo__bar__imag", 1)
|
||||
result = rgetattr(object(), "nothing__imag", 1)
|
||||
assert result == 1, result
|
Ładowanie…
Reference in New Issue