Split up huge settings module

- Move validators into their own module.
- Convert Kind to IntEnum

Related #2273
This commit is contained in:
MattHag 2024-11-03 18:42:41 +01:00 committed by Peter F. Patel-Schneider
parent 789d35450c
commit 03de6fb276
7 changed files with 847 additions and 809 deletions

View File

@ -16,14 +16,16 @@
from __future__ import annotations
import logging
import math
import struct
import time
from enum import IntEnum
from solaar.i18n import _
from . import common
from . import hidpp20_constants
from . import settings_validator
from .common import NamedInt
from .common import NamedInts
@ -43,22 +45,15 @@ KIND = NamedInts(
)
def bool_or_toggle(current: bool | str, new: bool | str) -> bool:
if isinstance(new, bool):
return new
try:
return bool(int(new))
except (TypeError, ValueError):
new = str(new).lower()
if new in ("true", "yes", "on", "t", "y"):
return True
if new in ("false", "no", "off", "f", "n"):
return False
if new in ("~", "toggle"):
return not current
return None
class Kind(IntEnum):
TOGGLE = 0x01
CHOICE = 0x02
RANGE = 0x04
MAP_CHOICE = 0x0A
MULTIPLE_TOGGLE = 0x10
PACKED_RANGE = 0x20
MULTIPLE_RANGE = 0x40
HETERO = 0x80
class Setting:
@ -103,14 +98,14 @@ class Setting:
assert hasattr(self, "_value")
assert hasattr(self, "_device")
return self._validator.choices if self._validator and self._validator.kind & KIND.choice else None
return self._validator.choices if self._validator and self._validator.kind & Kind.CHOICE else None
@property
def range(self):
assert hasattr(self, "_value")
assert hasattr(self, "_device")
if self._validator.kind == KIND.range:
if self._validator.kind == Kind.RANGE:
return self._validator.min_value, self._validator.max_value
def _pre_read(self, cached, key=None):
@ -692,709 +687,6 @@ class FeatureRWMap(FeatureRW):
return reply if not self.no_reply else True
class Validator:
@classmethod
def build(cls, setting_class, device, **kwargs):
return cls(**kwargs)
@classmethod
def to_string(cls, value):
return str(value)
def compare(self, args, current):
if len(args) != 1:
return False
return args[0] == current
class BooleanValidator(Validator):
__slots__ = ("true_value", "false_value", "read_skip_byte_count", "write_prefix_bytes", "mask", "needs_current_value")
kind = KIND.toggle
default_true = 0x01
default_false = 0x00
# mask specifies all the affected bits in the value
default_mask = 0xFF
def __init__(
self,
true_value=default_true,
false_value=default_false,
mask=default_mask,
read_skip_byte_count=0,
write_prefix_bytes=b"",
):
if isinstance(true_value, int):
assert isinstance(false_value, int)
if mask is None:
mask = self.default_mask
else:
assert isinstance(mask, int)
assert true_value & false_value == 0
assert true_value & mask == true_value
assert false_value & mask == false_value
self.needs_current_value = mask != self.default_mask
elif isinstance(true_value, bytes):
if false_value is None or false_value == self.default_false:
false_value = b"\x00" * len(true_value)
else:
assert isinstance(false_value, bytes)
if mask is None or mask == self.default_mask:
mask = b"\xff" * len(true_value)
else:
assert isinstance(mask, bytes)
assert len(mask) == len(true_value) == len(false_value)
tv = common.bytes2int(true_value)
fv = common.bytes2int(false_value)
mv = common.bytes2int(mask)
assert tv != fv # true and false might be something other than bit values
assert tv & mv == tv
assert fv & mv == fv
self.needs_current_value = any(m != 0xFF for m in mask)
else:
raise Exception(f"invalid mask '{mask!r}', type {type(mask)}")
self.true_value = true_value
self.false_value = false_value
self.mask = mask
self.read_skip_byte_count = read_skip_byte_count
self.write_prefix_bytes = write_prefix_bytes
def validate_read(self, reply_bytes):
reply_bytes = reply_bytes[self.read_skip_byte_count :]
if isinstance(self.mask, int):
reply_value = ord(reply_bytes[:1]) & self.mask
if logger.isEnabledFor(logging.DEBUG):
logger.debug("BooleanValidator: validate read %r => %02X", reply_bytes, reply_value)
if reply_value == self.true_value:
return True
if reply_value == self.false_value:
return False
logger.warning(
"BooleanValidator: reply %02X mismatched %02X/%02X/%02X",
reply_value,
self.true_value,
self.false_value,
self.mask,
)
return False
count = len(self.mask)
mask = common.bytes2int(self.mask)
reply_value = common.bytes2int(reply_bytes[:count]) & mask
true_value = common.bytes2int(self.true_value)
if reply_value == true_value:
return True
false_value = common.bytes2int(self.false_value)
if reply_value == false_value:
return False
logger.warning(
"BooleanValidator: reply %r mismatched %r/%r/%r", reply_bytes, self.true_value, self.false_value, self.mask
)
return False
def prepare_write(self, new_value, current_value=None):
if new_value is None:
new_value = False
else:
assert isinstance(new_value, bool), f"New value {new_value} for boolean setting is not a boolean"
to_write = self.true_value if new_value else self.false_value
if isinstance(self.mask, int):
if current_value is not None and self.needs_current_value:
to_write |= ord(current_value[:1]) & (0xFF ^ self.mask)
if current_value is not None and to_write == ord(current_value[:1]):
return None
to_write = bytes([to_write])
else:
to_write = bytearray(to_write)
count = len(self.mask)
for i in range(0, count):
b = ord(to_write[i : i + 1])
m = ord(self.mask[i : i + 1])
assert b & m == b
# b &= m
if current_value is not None and self.needs_current_value:
b |= ord(current_value[i : i + 1]) & (0xFF ^ m)
to_write[i] = b
to_write = bytes(to_write)
if current_value is not None and to_write == current_value[: len(to_write)]:
return None
if logger.isEnabledFor(logging.DEBUG):
logger.debug("BooleanValidator: prepare_write(%s, %s) => %r", new_value, current_value, to_write)
return self.write_prefix_bytes + to_write
def acceptable(self, args, current):
if len(args) != 1:
return None
val = bool_or_toggle(current, args[0])
return [val] if val is not None else None
class BitFieldValidator(Validator):
__slots__ = ("byte_count", "options")
kind = KIND.multiple_toggle
def __init__(self, options, byte_count=None):
assert isinstance(options, list)
self.options = options
self.byte_count = (max(x.bit_length() for x in options) + 7) // 8
if byte_count:
assert isinstance(byte_count, int) and byte_count >= self.byte_count
self.byte_count = byte_count
def to_string(self, value):
def element_to_string(key, val):
k = next((k for k in self.options if int(key) == k), None)
return str(k) + ":" + str(val) if k is not None else "?"
return "{" + ", ".join([element_to_string(k, value[k]) for k in value]) + "}"
def validate_read(self, reply_bytes):
r = common.bytes2int(reply_bytes[: self.byte_count])
value = {int(k): False for k in self.options}
m = 1
for _ignore in range(8 * self.byte_count):
if m in self.options:
value[int(m)] = bool(r & m)
m <<= 1
return value
def prepare_write(self, new_value):
assert isinstance(new_value, dict)
w = 0
for k, v in new_value.items():
if v:
w |= int(k)
return common.int2bytes(w, self.byte_count)
def get_options(self):
return self.options
def acceptable(self, args, current):
if len(args) != 2:
return None
key = next((key for key in self.options if key == args[0]), None)
if key is None:
return None
val = bool_or_toggle(current[int(key)], args[1])
return None if val is None else [int(key), val]
def compare(self, args, current):
if len(args) != 2:
return False
key = next((key for key in self.options if key == args[0]), None)
if key is None:
return False
return args[1] == current[int(key)]
class BitFieldWithOffsetAndMaskValidator(Validator):
__slots__ = ("byte_count", "options", "_option_from_key", "_mask_from_offset", "_option_from_offset_mask")
kind = KIND.multiple_toggle
sep = 0x01
def __init__(self, options, om_method=None, byte_count=None):
assert isinstance(options, list)
# each element of options is an instance of a class
# that has an id (which is used as an index in other dictionaries)
# and where om_method is a method that returns a byte offset and byte mask
# that says how to access and modify the bit toggle for the option
self.options = options
self.om_method = om_method
# to retrieve the options efficiently:
self._option_from_key = {}
self._mask_from_offset = {}
self._option_from_offset_mask = {}
for opt in options:
offset, mask = om_method(opt)
self._option_from_key[int(opt)] = opt
try:
self._mask_from_offset[offset] |= mask
except KeyError:
self._mask_from_offset[offset] = mask
try:
mask_to_opt = self._option_from_offset_mask[offset]
except KeyError:
mask_to_opt = {}
self._option_from_offset_mask[offset] = mask_to_opt
mask_to_opt[mask] = opt
self.byte_count = (max(om_method(x)[1].bit_length() for x in options) + 7) // 8 # is this correct??
if byte_count:
assert isinstance(byte_count, int) and byte_count >= self.byte_count
self.byte_count = byte_count
def prepare_read(self):
r = []
for offset, mask in self._mask_from_offset.items():
b = offset << (8 * (self.byte_count + 1))
b |= (self.sep << (8 * self.byte_count)) | mask
r.append(common.int2bytes(b, self.byte_count + 2))
return r
def prepare_read_key(self, key):
option = self._option_from_key.get(key, None)
if option is None:
return None
offset, mask = option.om_method(option)
b = offset << (8 * (self.byte_count + 1))
b |= (self.sep << (8 * self.byte_count)) | mask
return common.int2bytes(b, self.byte_count + 2)
def validate_read(self, reply_bytes_dict):
values = {int(k): False for k in self.options}
for query, b in reply_bytes_dict.items():
offset = common.bytes2int(query[0:1])
b += (self.byte_count - len(b)) * b"\x00"
value = common.bytes2int(b[: self.byte_count])
mask_to_opt = self._option_from_offset_mask.get(offset, {})
m = 1
for _ignore in range(8 * self.byte_count):
if m in mask_to_opt:
values[int(mask_to_opt[m])] = bool(value & m)
m <<= 1
return values
def prepare_write(self, new_value):
assert isinstance(new_value, dict)
w = {}
for k, v in new_value.items():
option = self._option_from_key[int(k)]
offset, mask = self.om_method(option)
if offset not in w:
w[offset] = 0
if v:
w[offset] |= mask
return [
common.int2bytes(
(offset << (8 * (2 * self.byte_count + 1)))
| (self.sep << (16 * self.byte_count))
| (self._mask_from_offset[offset] << (8 * self.byte_count))
| value,
2 * self.byte_count + 2,
)
for offset, value in w.items()
]
def get_options(self):
return [int(opt) if isinstance(opt, int) else opt.as_int() for opt in self.options]
def acceptable(self, args, current):
if len(args) != 2:
return None
key = next((option.id for option in self.options if option.as_int() == args[0]), None)
if key is None:
return None
val = bool_or_toggle(current[int(key)], args[1])
return None if val is None else [int(key), val]
def compare(self, args, current):
if len(args) != 2:
return False
key = next((option.id for option in self.options if option.as_int() == args[0]), None)
if key is None:
return False
return args[1] == current[int(key)]
class ChoicesValidator(Validator):
"""Translates between NamedInts and a byte sequence.
:param choices: a list of NamedInts
:param byte_count: the size of the derived byte sequence. If None, it
will be calculated from the choices."""
kind = KIND.choice
def __init__(self, choices=None, byte_count=None, read_skip_byte_count=0, write_prefix_bytes=b""):
assert choices is not None
assert isinstance(choices, NamedInts)
assert len(choices) > 1
self.choices = choices
self.needs_current_value = False
max_bits = max(x.bit_length() for x in choices)
self._byte_count = (max_bits // 8) + (1 if max_bits % 8 else 0)
if byte_count:
assert self._byte_count <= byte_count
self._byte_count = byte_count
assert self._byte_count < 8
self._read_skip_byte_count = read_skip_byte_count
self._write_prefix_bytes = write_prefix_bytes if write_prefix_bytes else b""
assert self._byte_count + self._read_skip_byte_count <= 14
assert self._byte_count + len(self._write_prefix_bytes) <= 14
def to_string(self, value):
return str(self.choices[value]) if isinstance(value, int) else str(value)
def validate_read(self, reply_bytes):
reply_value = common.bytes2int(reply_bytes[self._read_skip_byte_count : self._read_skip_byte_count + self._byte_count])
valid_value = self.choices[reply_value]
assert valid_value is not None, f"{self.__class__.__name__}: failed to validate read value {reply_value:02X}"
return valid_value
def prepare_write(self, new_value, current_value=None):
if new_value is None:
value = self.choices[:][0]
else:
value = self.choice(new_value)
if value is None:
raise ValueError(f"invalid choice {new_value!r}")
assert isinstance(value, NamedInt)
return self._write_prefix_bytes + value.bytes(self._byte_count)
def choice(self, value):
if isinstance(value, int):
return self.choices[value]
try:
int(value)
if int(value) in self.choices:
return self.choices[int(value)]
except Exception:
pass
if value in self.choices:
return self.choices[value]
else:
return None
def acceptable(self, args, current):
choice = self.choice(args[0]) if len(args) == 1 else None
return None if choice is None else [choice]
class ChoicesMapValidator(ChoicesValidator):
kind = KIND.map_choice
def __init__(
self,
choices_map,
key_byte_count=0,
key_postfix_bytes=b"",
byte_count=0,
read_skip_byte_count=0,
write_prefix_bytes=b"",
extra_default=None,
mask=-1,
activate=0,
):
assert choices_map is not None
assert isinstance(choices_map, dict)
max_key_bits = 0
max_value_bits = 0
for key, choices in choices_map.items():
assert isinstance(key, NamedInt)
assert isinstance(choices, NamedInts)
max_key_bits = max(max_key_bits, key.bit_length())
for key_value in choices:
assert isinstance(key_value, NamedInt)
max_value_bits = max(max_value_bits, key_value.bit_length())
self._key_byte_count = (max_key_bits + 7) // 8
if key_byte_count:
assert self._key_byte_count <= key_byte_count
self._key_byte_count = key_byte_count
self._byte_count = (max_value_bits + 7) // 8
if byte_count:
assert self._byte_count <= byte_count
self._byte_count = byte_count
self.choices = choices_map
self.needs_current_value = False
self.extra_default = extra_default
self._key_postfix_bytes = key_postfix_bytes
self._read_skip_byte_count = read_skip_byte_count if read_skip_byte_count else 0
self._write_prefix_bytes = write_prefix_bytes if write_prefix_bytes else b""
self.activate = activate
self.mask = mask
assert self._byte_count + self._read_skip_byte_count + self._key_byte_count <= 14
assert self._byte_count + len(self._write_prefix_bytes) + self._key_byte_count <= 14
def to_string(self, value):
def element_to_string(key, val):
k, c = next(((k, c) for k, c in self.choices.items() if int(key) == k), (None, None))
return str(k) + ":" + str(c[val]) if k is not None else "?"
return "{" + ", ".join([element_to_string(k, value[k]) for k in sorted(value)]) + "}"
def validate_read(self, reply_bytes, key):
start = self._key_byte_count + self._read_skip_byte_count
end = start + self._byte_count
reply_value = common.bytes2int(reply_bytes[start:end]) & self.mask
# reprogrammable keys starts out as 0, which is not a choice, so don't use assert here
if self.extra_default is not None and self.extra_default == reply_value:
return int(self.choices[key][0])
if reply_value not in self.choices[key]:
assert reply_value in self.choices[key], "%s: failed to validate read value %02X" % (
self.__class__.__name__,
reply_value,
)
return reply_value
def prepare_key(self, key):
return key.to_bytes(self._key_byte_count, "big") + self._key_postfix_bytes
def prepare_write(self, key, new_value):
choices = self.choices.get(key)
if choices is None or (new_value not in choices and new_value != self.extra_default):
logger.error("invalid choice %r for %s", new_value, key)
return None
new_value = new_value | self.activate
return self._write_prefix_bytes + new_value.to_bytes(self._byte_count, "big")
def acceptable(self, args, current):
if len(args) != 2:
return None
key, choices = next(((key, item) for key, item in self.choices.items() if key == args[0]), (None, None))
if choices is None or args[1] not in choices:
return None
choice = next((item for item in choices if item == args[1]), None)
return [int(key), int(choice)] if choice is not None else None
def compare(self, args, current):
if len(args) != 2:
return False
key = next((key for key in self.choices if key == int(args[0])), None)
if key is None:
return False
return args[1] == current[int(key)]
class RangeValidator(Validator):
kind = KIND.range
"""Translates between integers and a byte sequence.
:param min_value: minimum accepted value (inclusive)
:param max_value: maximum accepted value (inclusive)
:param byte_count: the size of the derived byte sequence. If None, it
will be calculated from the range."""
min_value = 0
max_value = 255
@classmethod
def build(cls, setting_class, device, **kwargs):
kwargs["min_value"] = setting_class.min_value
kwargs["max_value"] = setting_class.max_value
return cls(**kwargs)
def __init__(self, min_value=0, max_value=255, byte_count=1):
assert max_value > min_value
self.min_value = min_value
self.max_value = max_value
self.needs_current_value = True # read and check before write (needed for ADC power and probably a good idea anyway)
self._byte_count = math.ceil(math.log(max_value + 1, 256))
if byte_count:
assert self._byte_count <= byte_count
self._byte_count = byte_count
assert self._byte_count < 8
def validate_read(self, reply_bytes):
reply_value = common.bytes2int(reply_bytes[: self._byte_count])
assert reply_value >= self.min_value, f"{self.__class__.__name__}: failed to validate read value {reply_value:02X}"
assert reply_value <= self.max_value, f"{self.__class__.__name__}: failed to validate read value {reply_value:02X}"
return reply_value
def prepare_write(self, new_value, current_value=None):
if new_value < self.min_value or new_value > self.max_value:
raise ValueError(f"invalid choice {new_value!r}")
current_value = self.validate_read(current_value) if current_value is not None else None
to_write = common.int2bytes(new_value, self._byte_count)
# current value is known and same as value to be written return None to signal not to write it
return None if current_value is not None and current_value == new_value else to_write
def acceptable(self, args, current):
arg = args[0]
# None if len(args) != 1 or type(arg) != int or arg < self.min_value or arg > self.max_value else args)
return None if len(args) != 1 or isinstance(arg, int) or arg < self.min_value or arg > self.max_value else args
def compare(self, args, current):
if len(args) == 1:
return args[0] == current
elif len(args) == 2:
return args[0] <= current <= args[1]
else:
return False
class HeteroValidator(Validator):
kind = KIND.hetero
@classmethod
def build(cls, setting_class, device, **kwargs):
return cls(**kwargs)
def __init__(self, data_class=None, options=None, readable=True):
assert data_class is not None and options is not None
self.data_class = data_class
self.options = options
self.readable = readable
self.needs_current_value = False
def validate_read(self, reply_bytes):
if self.readable:
reply_value = self.data_class.from_bytes(reply_bytes, options=self.options)
return reply_value
def prepare_write(self, new_value, current_value=None):
to_write = new_value.to_bytes(options=self.options)
return to_write
def acceptable(self, args, current): # should this actually do some checking?
return True
class PackedRangeValidator(Validator):
kind = KIND.packed_range
"""Several range values, all the same size, all the same min and max"""
min_value = 0
max_value = 255
count = 1
rsbc = 0
write_prefix_bytes = b""
def __init__(
self, keys, min_value=0, max_value=255, count=1, byte_count=1, read_skip_byte_count=0, write_prefix_bytes=b""
):
assert max_value > min_value
self.needs_current_value = True
self.keys = keys
self.min_value = min_value
self.max_value = max_value
self.count = count
self.bc = math.ceil(math.log(max_value + 1 - min(0, min_value), 256))
if byte_count:
assert self.bc <= byte_count
self.bc = byte_count
assert self.bc * self.count
self.rsbc = read_skip_byte_count
self.write_prefix_bytes = write_prefix_bytes
def validate_read(self, reply_bytes):
rvs = {
n: common.bytes2int(reply_bytes[self.rsbc + n * self.bc : self.rsbc + (n + 1) * self.bc], signed=True)
for n in range(self.count)
}
for n in range(self.count):
assert rvs[n] >= self.min_value, f"{self.__class__.__name__}: failed to validate read value {rvs[n]:02X}"
assert rvs[n] <= self.max_value, f"{self.__class__.__name__}: failed to validate read value {rvs[n]:02X}"
return rvs
def prepare_write(self, new_values):
if len(new_values) != self.count:
raise ValueError(f"wrong number of values {new_values!r}")
for new_value in new_values.values():
if new_value < self.min_value or new_value > self.max_value:
raise ValueError(f"invalid value {new_value!r}")
bytes = self.write_prefix_bytes + b"".join(
common.int2bytes(new_values[n], self.bc, signed=True) for n in range(self.count)
)
return bytes
def acceptable(self, args, current):
if len(args) != 2 or int(args[0]) < 0 or int(args[0]) >= self.count:
return None
return None if not isinstance(args[1], int) or args[1] < self.min_value or args[1] > self.max_value else args
def compare(self, args, current):
logger.warning("compare not implemented for packed range settings")
return False
class MultipleRangeValidator(Validator):
kind = KIND.multiple_range
def __init__(self, items, sub_items):
assert isinstance(items, list) # each element must have .index and its __int__ must return its id (not its index)
assert isinstance(sub_items, dict)
# sub_items: items -> class with .minimum, .maximum, .length (in bytes), .id (a string) and .widget (e.g. 'Scale')
self.items = items
self.keys = NamedInts(**{str(item): int(item) for item in items})
self._item_from_id = {int(k): k for k in items}
self.sub_items = sub_items
def prepare_read_item(self, item):
return common.int2bytes((self._item_from_id[int(item)].index << 1) | 0xFF, 2)
def validate_read_item(self, reply_bytes, item):
item = self._item_from_id[int(item)]
start = 0
value = {}
for sub_item in self.sub_items[item]:
r = reply_bytes[start : start + sub_item.length]
if len(r) < sub_item.length:
r += b"\x00" * (sub_item.length - len(value))
v = common.bytes2int(r)
if not (sub_item.minimum < v < sub_item.maximum):
logger.warning(
f"{self.__class__.__name__}: failed to validate read value for {item}.{sub_item}: "
+ f"{v} not in [{sub_item.minimum}..{sub_item.maximum}]"
)
value[str(sub_item)] = v
start += sub_item.length
return value
def prepare_write(self, value):
seq = []
w = b""
for item in value.keys():
_item = self._item_from_id[int(item)]
b = common.int2bytes(_item.index, 1)
for sub_item in self.sub_items[_item]:
try:
v = value[int(item)][str(sub_item)]
except KeyError:
return None
if not (sub_item.minimum <= v <= sub_item.maximum):
raise ValueError(
f"invalid choice for {item}.{sub_item}: {v} not in [{sub_item.minimum}..{sub_item.maximum}]"
)
b += common.int2bytes(v, sub_item.length)
if len(w) + len(b) > 15:
seq.append(b + b"\xff")
w = b""
w += b
seq.append(w + b"\xff")
return seq
def prepare_write_item(self, item, value):
_item = self._item_from_id[int(item)]
w = common.int2bytes(_item.index, 1)
for sub_item in self.sub_items[_item]:
try:
v = value[str(sub_item)]
except KeyError:
return None
if not (sub_item.minimum <= v <= sub_item.maximum):
raise ValueError(f"invalid choice for {item}.{sub_item}: {v} not in [{sub_item.minimum}..{sub_item.maximum}]")
w += common.int2bytes(v, sub_item.length)
return w + b"\xff"
def acceptable(self, args, current):
# just one item, with at least one sub-item
if not isinstance(args, list) or len(args) != 2 or not isinstance(args[1], dict):
return None
item = next((p for p in self.items if p.id == args[0] or str(p) == args[0]), None)
if not item:
return None
for sub_key, value in args[1].items():
sub_item = next((it for it in self.sub_items[item] if it.id == sub_key), None)
if not sub_item:
return None
if not isinstance(value, int) or not (sub_item.minimum <= value <= sub_item.maximum):
return None
return [int(item), {**args[1]}]
def compare(self, args, current):
logger.warning("compare not implemented for multiple range settings")
return False
class ActionSettingRW:
"""Special RW class for settings that turn on and off special processing when a key or button is depressed"""
@ -1578,4 +870,4 @@ def apply_all_settings(device):
s.apply()
Setting.validator_class = BooleanValidator
Setting.validator_class = settings_validator.BooleanValidator

View File

@ -36,6 +36,7 @@ from . import hidpp10_constants
from . import hidpp20
from . import hidpp20_constants
from . import settings
from . import settings_validator
from . import special_keys
from .hidpp10_constants import Registers
from .hidpp20_constants import GestureId
@ -177,7 +178,7 @@ class RegisterDpi(settings.Setting):
description = _("Mouse movement sensitivity")
register = Registers.MOUSE_DPI
choices_universe = common.NamedInts.range(0x81, 0x8F, lambda x: str((x - 0x80) * 100))
validator_class = settings.ChoicesValidator
validator_class = settings_validator.ChoicesValidator
validator_options = {"choices": choices_universe}
@ -251,7 +252,7 @@ class Backlight(settings.Setting):
description = _("Set illumination time for keyboard.")
feature = _F.BACKLIGHT
choices_universe = common.NamedInts(Off=0, Varying=2, VeryShort=5, Short=10, Medium=20, Long=60, VeryLong=180)
validator_class = settings.ChoicesValidator
validator_class = settings_validator.ChoicesValidator
validator_options = {"choices": choices_universe}
@ -285,7 +286,7 @@ class Backlight2(settings.Setting):
backlight.write()
return True
class validator_class(settings.ChoicesValidator):
class validator_class(settings_validator.ChoicesValidator):
@classmethod
def build(cls, setting_class, device):
backlight = device.backlight
@ -322,7 +323,7 @@ class Backlight2Level(settings.Setting):
device.backlight.write()
return True
class validator_class(settings.RangeValidator):
class validator_class(settings_validator.RangeValidator):
@classmethod
def build(cls, setting_class, device):
reply = device.feature_request(_F.BACKLIGHT2, 0x20)
@ -334,7 +335,7 @@ class Backlight2Level(settings.Setting):
class Backlight2Duration(settings.Setting):
feature = _F.BACKLIGHT2
min_version = 3
validator_class = settings.RangeValidator
validator_class = settings_validator.RangeValidator
min_value = 1
max_value = 600 # 10 minutes - actual maximum is 2 hours
validator_options = {"byte_count": 2}
@ -363,7 +364,7 @@ class Backlight2DurationHandsOut(Backlight2Duration):
label = _("Backlight Delay Hands Out")
description = _("Delay in seconds until backlight fades out with hands away from keyboard.")
feature = _F.BACKLIGHT2
validator_class = settings.RangeValidator
validator_class = settings_validator.RangeValidator
rw_options = {"field": "dho"}
@ -372,7 +373,7 @@ class Backlight2DurationHandsIn(Backlight2Duration):
label = _("Backlight Delay Hands In")
description = _("Delay in seconds until backlight fades out with hands near keyboard.")
feature = _F.BACKLIGHT2
validator_class = settings.RangeValidator
validator_class = settings_validator.RangeValidator
rw_options = {"field": "dhi"}
@ -381,7 +382,7 @@ class Backlight2DurationPowered(Backlight2Duration):
label = _("Backlight Delay Powered")
description = _("Delay in seconds until backlight fades out with external power.")
feature = _F.BACKLIGHT2
validator_class = settings.RangeValidator
validator_class = settings_validator.RangeValidator
rw_options = {"field": "dpow"}
@ -391,7 +392,7 @@ class Backlight3(settings.Setting):
description = _("Set illumination time for keyboard.")
feature = _F.BACKLIGHT3
rw_options = {"read_fnid": 0x10, "write_fnid": 0x20, "suffix": b"\x09"}
validator_class = settings.RangeValidator
validator_class = settings_validator.RangeValidator
min_value = 0
max_value = 1000
validator_options = {"byte_count": 2}
@ -455,7 +456,7 @@ class PointerSpeed(settings.Setting):
label = _("Sensitivity (Pointer Speed)")
description = _("Speed multiplier for mouse (256 is normal multiplier).")
feature = _F.POINTER_SPEED
validator_class = settings.RangeValidator
validator_class = settings_validator.RangeValidator
min_value = 0x002E
max_value = 0x01FF
validator_options = {"byte_count": 2}
@ -502,7 +503,7 @@ class OnboardProfiles(settings.Setting):
for i in range(1, 16):
choices_universe[i] = f"Profile {i}"
choices_universe[i + 0x100] = f"Read-Only Profile {i}"
validator_class = settings.ChoicesValidator
validator_class = settings_validator.ChoicesValidator
class rw_class:
def __init__(self, feature):
@ -526,7 +527,7 @@ class OnboardProfiles(settings.Setting):
profile_change(device, common.bytes2int(data_bytes))
return result
class validator_class(settings.ChoicesValidator):
class validator_class(settings_validator.ChoicesValidator):
@classmethod
def build(cls, setting_class, device):
headers = hidpp20.OnboardProfiles.get_profile_headers(device)
@ -556,7 +557,7 @@ class ReportRate(settings.Setting):
choices_universe[7] = "7ms"
choices_universe[8] = "8ms"
class validator_class(settings.ChoicesValidator):
class validator_class(settings_validator.ChoicesValidator):
@classmethod
def build(cls, setting_class, device):
# if device.wpid == '408E':
@ -588,7 +589,7 @@ class ExtendedReportRate(settings.Setting):
choices_universe[5] = "250us"
choices_universe[6] = "125us"
class validator_class(settings.ChoicesValidator):
class validator_class(settings_validator.ChoicesValidator):
@classmethod
def build(cls, setting_class, device):
reply = device.feature_request(_F.EXTENDED_ADJUSTABLE_REPORT_RATE, 0x10)
@ -640,7 +641,7 @@ class ScrollRatchet(settings.Setting):
description = _("Switch the mouse wheel between speed-controlled ratcheting and always freespin.")
feature = _F.SMART_SHIFT
choices_universe = common.NamedInts(**{_("Freespinning"): 1, _("Ratcheted"): 2})
validator_class = settings.ChoicesValidator
validator_class = settings_validator.ChoicesValidator
validator_options = {"choices": choices_universe}
@ -683,7 +684,7 @@ class SmartShift(settings.Setting):
min_value = rw_class.MIN_VALUE
max_value = rw_class.MAX_VALUE
validator_class = settings.RangeValidator
validator_class = settings_validator.RangeValidator
class SmartShiftEnhanced(SmartShift):
@ -730,7 +731,7 @@ class ReprogrammableKeys(settings.Settings):
key_struct.remap(special_keys.CONTROL[common.bytes2int(data_bytes)])
return True
class validator_class(settings.ChoicesMapValidator):
class validator_class(settings_validator.ChoicesMapValidator):
@classmethod
def build(cls, setting_class, device):
choices = {}
@ -907,7 +908,7 @@ class DivertKeys(settings.Settings):
key_struct.set_diverted(common.bytes2int(data_bytes) != 0) # not regular
return True
class validator_class(settings.ChoicesMapValidator):
class validator_class(settings_validator.ChoicesMapValidator):
def __init__(self, choices, key_byte_count=2, byte_count=1, mask=0x01):
super().__init__(choices, key_byte_count, byte_count, mask)
@ -983,7 +984,7 @@ class AdjustableDpi(settings.Setting):
rw_options = {"read_fnid": 0x20, "write_fnid": 0x30}
choices_universe = common.NamedInts.range(100, 4000, str, 50)
class validator_class(settings.ChoicesValidator):
class validator_class(settings_validator.ChoicesValidator):
@classmethod
def build(cls, setting_class, device):
dpilist = produce_dpi_list(setting_class.feature, 0x10, 1, device, 0)
@ -1024,7 +1025,7 @@ class ExtendedAdjustableDpi(settings.Setting):
result = self.write(self._value, save)
return result[key] if isinstance(result, dict) else result
class validator_class(settings.ChoicesMapValidator):
class validator_class(settings_validator.ChoicesMapValidator):
@classmethod
def build(cls, setting_class, device):
reply = device.feature_request(setting_class.feature, 0x10, 0x00)
@ -1107,7 +1108,7 @@ class SpeedChange(settings.Setting):
if self.device.persister:
self.device.persister["_speed-change"] = currentSpeed
class validator_class(settings.ChoicesValidator):
class validator_class(settings_validator.ChoicesValidator):
@classmethod
def build(cls, setting_class, device):
key_index = device.keys.index(special_keys.CONTROL.DPI_Change)
@ -1126,7 +1127,7 @@ class DisableKeyboardKeys(settings.BitFieldSetting):
_labels = {k: (None, _("Disables the %s key.") % k) for k in special_keys.DISABLE}
choices_universe = special_keys.DISABLE
class validator_class(settings.BitFieldValidator):
class validator_class(settings_validator.BitFieldValidator):
@classmethod
def build(cls, setting_class, device):
mask = device.feature_request(_F.KEYBOARD_DISABLE_KEYS, 0x00)[0]
@ -1158,7 +1159,7 @@ class Multiplatform(settings.Setting):
# the problem here is how to construct the right values for the rules Set GUI,
# as, for example, the integer value for 'Windows' can be different on different devices
class validator_class(settings.ChoicesValidator):
class validator_class(settings_validator.ChoicesValidator):
@classmethod
def build(cls, setting_class, device):
def _str_os_versions(low, high):
@ -1200,7 +1201,7 @@ class DualPlatform(settings.Setting):
choices_universe[0x01] = "Android, Windows"
feature = _F.DUALPLATFORM
rw_options = {"read_fnid": 0x00, "write_fnid": 0x20}
validator_class = settings.ChoicesValidator
validator_class = settings_validator.ChoicesValidator
validator_options = {"choices": choices_universe}
@ -1213,7 +1214,7 @@ class ChangeHost(settings.Setting):
rw_options = {"read_fnid": 0x00, "write_fnid": 0x10, "no_reply": True}
choices_universe = common.NamedInts(**{"Host " + str(i + 1): i for i in range(3)})
class validator_class(settings.ChoicesValidator):
class validator_class(settings_validator.ChoicesValidator):
@classmethod
def build(cls, setting_class, device):
infos = device.feature_request(_F.CHANGE_HOST)
@ -1325,7 +1326,7 @@ class Gesture2Gestures(settings.BitFieldWithOffsetAndMaskSetting):
choices_universe = hidpp20_constants.GestureId
_labels = _GESTURE2_GESTURES_LABELS
class validator_class(settings.BitFieldWithOffsetAndMaskValidator):
class validator_class(settings_validator.BitFieldWithOffsetAndMaskValidator):
@classmethod
def build(cls, setting_class, device, om_method=None):
options = [g for g in device.gestures.gestures.values() if g.can_be_enabled or g.default_enabled]
@ -1342,7 +1343,7 @@ class Gesture2Divert(settings.BitFieldWithOffsetAndMaskSetting):
choices_universe = hidpp20_constants.GestureId
_labels = _GESTURE2_GESTURES_LABELS
class validator_class(settings.BitFieldWithOffsetAndMaskValidator):
class validator_class(settings_validator.BitFieldWithOffsetAndMaskValidator):
@classmethod
def build(cls, setting_class, device, om_method=None):
options = [g for g in device.gestures.gestures.values() if g.can_be_diverted]
@ -1363,7 +1364,7 @@ class Gesture2Params(settings.LongSettings):
_labels = _GESTURE2_PARAMS_LABELS
_labels_sub = _GESTURE2_PARAMS_LABELS_SUB
class validator_class(settings.MultipleRangeValidator):
class validator_class(settings_validator.MultipleRangeValidator):
@classmethod
def build(cls, setting_class, device):
params = _hidpp20.get_gestures(device).params.values()
@ -1397,7 +1398,7 @@ class MKeyLEDs(settings.BitFieldSetting):
def read(self, device): # no way to read, so just assume off
return b"\x00"
class validator_class(settings.BitFieldValidator):
class validator_class(settings_validator.BitFieldValidator):
@classmethod
def build(cls, setting_class, device):
number = device.feature_request(setting_class.feature, 0x00)[0]
@ -1455,7 +1456,7 @@ class PersistentRemappableAction(settings.Settings):
v = ks.remap(data_bytes)
return v
class validator_class(settings.ChoicesMapValidator):
class validator_class(settings_validator.ChoicesMapValidator):
@classmethod
def build(cls, setting_class, device):
remap_keys = device.remap_keys
@ -1494,7 +1495,7 @@ class Sidetone(settings.Setting):
label = _("Sidetone")
description = _("Set sidetone level.")
feature = _F.SIDETONE
validator_class = settings.RangeValidator
validator_class = settings_validator.RangeValidator
min_value = 0
max_value = 100
@ -1507,7 +1508,7 @@ class Equalizer(settings.RangeFieldSetting):
rw_options = {"read_fnid": 0x20, "write_fnid": 0x30, "read_prefix": b"\x00"}
keys_universe = []
class validator_class(settings.PackedRangeValidator):
class validator_class(settings_validator.PackedRangeValidator):
@classmethod
def build(cls, setting_class, device):
data = device.feature_request(_F.EQUALIZER, 0x00)
@ -1534,7 +1535,7 @@ class ADCPower(settings.Setting):
description = _("Power off in minutes (0 for never).")
feature = _F.ADC_MEASUREMENT
rw_options = {"read_fnid": 0x10, "write_fnid": 0x20}
validator_class = settings.RangeValidator
validator_class = settings_validator.RangeValidator
min_value = 0x00
max_value = 0xFF
validator_options = {"byte_count": 1}
@ -1546,7 +1547,7 @@ class BrightnessControl(settings.Setting):
description = _("Control overall brightness")
feature = _F.BRIGHTNESS_CONTROL
rw_options = {"read_fnid": 0x10, "write_fnid": 0x20}
validator_class = settings.RangeValidator
validator_class = settings_validator.RangeValidator
def __init__(self, device, rw, validator):
super().__init__(device, rw, validator)
@ -1570,7 +1571,7 @@ class BrightnessControl(settings.Setting):
return reply
return super().write(device, data_bytes)
class validator_class(settings.RangeValidator):
class validator_class(settings_validator.RangeValidator):
@classmethod
def build(cls, setting_class, device):
reply = device.feature_request(_F.BRIGHTNESS_CONTROL)
@ -1591,7 +1592,7 @@ class LEDControl(settings.Setting):
feature = _F.COLOR_LED_EFFECTS
rw_options = {"read_fnid": 0x70, "write_fnid": 0x80}
choices_universe = common.NamedInts(Device=0, Solaar=1)
validator_class = settings.ChoicesValidator
validator_class = settings_validator.ChoicesValidator
validator_options = {"choices": choices_universe}
@ -1605,12 +1606,13 @@ class LEDZoneSetting(settings.Setting):
label = _("LED Zone Effects")
description = _("Set effect for LED Zone") + "\n" + _("LED Control needs to be set to Solaar to be effective.")
feature = _F.COLOR_LED_EFFECTS
color_field = {"name": _LEDP.color, "kind": settings.KIND.choice, "label": None, "choices": colors}
speed_field = {"name": _LEDP.speed, "kind": settings.KIND.range, "label": _("Speed"), "min": 0, "max": 255}
period_field = {"name": _LEDP.period, "kind": settings.KIND.range, "label": _("Period"), "min": 100, "max": 5000}
intensity_field = {"name": _LEDP.intensity, "kind": settings.KIND.range, "label": _("Intensity"), "min": 0, "max": 100}
ramp_field = {"name": _LEDP.ramp, "kind": settings.KIND.choice, "label": _("Ramp"), "choices": hidpp20.LEDRampChoices}
# form_field = {"name": _LEDP.form, "kind": settings.KIND.choice, "label": _("Form"), "choices": _hidpp20.LEDFormChoices}
color_field = {"name": _LEDP.color, "kind": settings.Kind.CHOICE, "label": None, "choices": colors}
speed_field = {"name": _LEDP.speed, "kind": settings.Kind.RANGE, "label": _("Speed"), "min": 0, "max": 255}
period_field = {"name": _LEDP.period, "kind": settings.Kind.RANGE, "label": _("Period"), "min": 100, "max": 5000}
intensity_field = {"name": _LEDP.intensity, "kind": settings.Kind.RANGE, "label": _("Intensity"), "min": 0, "max": 100}
ramp_field = {"name": _LEDP.ramp, "kind": settings.Kind.CHOICE, "label": _("Ramp"), "choices": hidpp20.LEDRampChoices}
# form_field = {"name": _LEDP.form, "kind": settings.Kind.CHOICE, "label": _("Form"), "choices":
# _hidpp20.LEDFormChoices}
possible_fields = [color_field, speed_field, period_field, intensity_field, ramp_field]
@classmethod
@ -1620,14 +1622,14 @@ class LEDZoneSetting(settings.Setting):
for zone in infos.zones:
prefix = common.int2bytes(zone.index, 1)
rw = settings.FeatureRW(cls.feature, read_fnid, write_fnid, prefix=prefix, suffix=suffix)
validator = settings.HeteroValidator(
validator = settings_validator.HeteroValidator(
data_class=hidpp20.LEDEffectSetting, options=zone.effects, readable=infos.readable
)
setting = cls(device, rw, validator)
setting.name = cls.name + str(int(zone.location))
setting.label = _("LEDs") + " " + str(hidpp20.LEDZoneLocations[zone.location])
choices = [hidpp20.LEDEffects[e.ID][0] for e in zone.effects if e.ID in hidpp20.LEDEffects]
ID_field = {"name": "ID", "kind": settings.KIND.choice, "label": None, "choices": choices}
ID_field = {"name": "ID", "kind": settings.Kind.CHOICE, "label": None, "choices": choices}
setting.possible_fields = [ID_field] + cls.possible_fields
setting.fields_map = hidpp20.LEDEffects
settings_.append(setting)
@ -1645,7 +1647,7 @@ class RGBControl(settings.Setting):
feature = _F.RGB_EFFECTS
rw_options = {"read_fnid": 0x50, "write_fnid": 0x50}
choices_universe = common.NamedInts(Device=0, Solaar=1)
validator_class = settings.ChoicesValidator
validator_class = settings_validator.ChoicesValidator
validator_options = {"choices": choices_universe, "write_prefix_bytes": b"\x01", "read_skip_byte_count": 1}
@ -1723,7 +1725,7 @@ class PerKeyLighting(settings.Settings):
class rw_class(settings.FeatureRWMap):
pass
class validator_class(settings.ChoicesMapValidator):
class validator_class(settings_validator.ChoicesMapValidator):
@classmethod
def build(cls, setting_class, device):
choices_map = {}
@ -1742,7 +1744,7 @@ class PerKeyLighting(settings.Settings):
return result
SETTINGS = [
SETTINGS: list[settings.Setting] = [
RegisterHandDetection, # simple
RegisterSmoothScroll, # simple
RegisterSideScroll, # simple

View File

@ -0,0 +1,744 @@
from __future__ import annotations
import logging
import math
from enum import IntEnum
from logitech_receiver import common
from logitech_receiver.common import NamedInt
from logitech_receiver.common import NamedInts
logger = logging.getLogger(__name__)
def bool_or_toggle(current: bool | str, new: bool | str) -> bool:
if isinstance(new, bool):
return new
try:
return bool(int(new))
except (TypeError, ValueError):
new = str(new).lower()
if new in ("true", "yes", "on", "t", "y"):
return True
if new in ("false", "no", "off", "f", "n"):
return False
if new in ("~", "toggle"):
return not current
return None
class Kind(IntEnum):
TOGGLE = 0x01
CHOICE = 0x02
RANGE = 0x04
MAP_CHOICE = 0x0A
MULTIPLE_TOGGLE = 0x10
PACKED_RANGE = 0x20
MULTIPLE_RANGE = 0x40
HETERO = 0x80
class Validator:
@classmethod
def build(cls, setting_class, device, **kwargs):
return cls(**kwargs)
@classmethod
def to_string(cls, value):
return str(value)
def compare(self, args, current):
if len(args) != 1:
return False
return args[0] == current
class BooleanValidator(Validator):
__slots__ = ("true_value", "false_value", "read_skip_byte_count", "write_prefix_bytes", "mask", "needs_current_value")
kind = Kind.TOGGLE
default_true = 0x01
default_false = 0x00
# mask specifies all the affected bits in the value
default_mask = 0xFF
def __init__(
self,
true_value=default_true,
false_value=default_false,
mask=default_mask,
read_skip_byte_count=0,
write_prefix_bytes=b"",
):
if isinstance(true_value, int):
assert isinstance(false_value, int)
if mask is None:
mask = self.default_mask
else:
assert isinstance(mask, int)
assert true_value & false_value == 0
assert true_value & mask == true_value
assert false_value & mask == false_value
self.needs_current_value = mask != self.default_mask
elif isinstance(true_value, bytes):
if false_value is None or false_value == self.default_false:
false_value = b"\x00" * len(true_value)
else:
assert isinstance(false_value, bytes)
if mask is None or mask == self.default_mask:
mask = b"\xff" * len(true_value)
else:
assert isinstance(mask, bytes)
assert len(mask) == len(true_value) == len(false_value)
tv = common.bytes2int(true_value)
fv = common.bytes2int(false_value)
mv = common.bytes2int(mask)
assert tv != fv # true and false might be something other than bit values
assert tv & mv == tv
assert fv & mv == fv
self.needs_current_value = any(m != 0xFF for m in mask)
else:
raise Exception(f"invalid mask '{mask!r}', type {type(mask)}")
self.true_value = true_value
self.false_value = false_value
self.mask = mask
self.read_skip_byte_count = read_skip_byte_count
self.write_prefix_bytes = write_prefix_bytes
def validate_read(self, reply_bytes):
reply_bytes = reply_bytes[self.read_skip_byte_count :]
if isinstance(self.mask, int):
reply_value = ord(reply_bytes[:1]) & self.mask
if logger.isEnabledFor(logging.DEBUG):
logger.debug("BooleanValidator: validate read %r => %02X", reply_bytes, reply_value)
if reply_value == self.true_value:
return True
if reply_value == self.false_value:
return False
logger.warning(
"BooleanValidator: reply %02X mismatched %02X/%02X/%02X",
reply_value,
self.true_value,
self.false_value,
self.mask,
)
return False
count = len(self.mask)
mask = common.bytes2int(self.mask)
reply_value = common.bytes2int(reply_bytes[:count]) & mask
true_value = common.bytes2int(self.true_value)
if reply_value == true_value:
return True
false_value = common.bytes2int(self.false_value)
if reply_value == false_value:
return False
logger.warning(
"BooleanValidator: reply %r mismatched %r/%r/%r", reply_bytes, self.true_value, self.false_value, self.mask
)
return False
def prepare_write(self, new_value, current_value=None):
if new_value is None:
new_value = False
else:
assert isinstance(new_value, bool), f"New value {new_value} for boolean setting is not a boolean"
to_write = self.true_value if new_value else self.false_value
if isinstance(self.mask, int):
if current_value is not None and self.needs_current_value:
to_write |= ord(current_value[:1]) & (0xFF ^ self.mask)
if current_value is not None and to_write == ord(current_value[:1]):
return None
to_write = bytes([to_write])
else:
to_write = bytearray(to_write)
count = len(self.mask)
for i in range(0, count):
b = ord(to_write[i : i + 1])
m = ord(self.mask[i : i + 1])
assert b & m == b
# b &= m
if current_value is not None and self.needs_current_value:
b |= ord(current_value[i : i + 1]) & (0xFF ^ m)
to_write[i] = b
to_write = bytes(to_write)
if current_value is not None and to_write == current_value[: len(to_write)]:
return None
if logger.isEnabledFor(logging.DEBUG):
logger.debug("BooleanValidator: prepare_write(%s, %s) => %r", new_value, current_value, to_write)
return self.write_prefix_bytes + to_write
def acceptable(self, args, current):
if len(args) != 1:
return None
val = bool_or_toggle(current, args[0])
return [val] if val is not None else None
class BitFieldValidator(Validator):
__slots__ = ("byte_count", "options")
kind = Kind.MULTIPLE_TOGGLE
def __init__(self, options, byte_count=None):
assert isinstance(options, list)
self.options = options
self.byte_count = (max(x.bit_length() for x in options) + 7) // 8
if byte_count:
assert isinstance(byte_count, int) and byte_count >= self.byte_count
self.byte_count = byte_count
def to_string(self, value):
def element_to_string(key, val):
k = next((k for k in self.options if int(key) == k), None)
return str(k) + ":" + str(val) if k is not None else "?"
return "{" + ", ".join([element_to_string(k, value[k]) for k in value]) + "}"
def validate_read(self, reply_bytes):
r = common.bytes2int(reply_bytes[: self.byte_count])
value = {int(k): False for k in self.options}
m = 1
for _ignore in range(8 * self.byte_count):
if m in self.options:
value[int(m)] = bool(r & m)
m <<= 1
return value
def prepare_write(self, new_value):
assert isinstance(new_value, dict)
w = 0
for k, v in new_value.items():
if v:
w |= int(k)
return common.int2bytes(w, self.byte_count)
def get_options(self):
return self.options
def acceptable(self, args, current):
if len(args) != 2:
return None
key = next((key for key in self.options if key == args[0]), None)
if key is None:
return None
val = bool_or_toggle(current[int(key)], args[1])
return None if val is None else [int(key), val]
def compare(self, args, current):
if len(args) != 2:
return False
key = next((key for key in self.options if key == args[0]), None)
if key is None:
return False
return args[1] == current[int(key)]
class BitFieldWithOffsetAndMaskValidator(Validator):
__slots__ = ("byte_count", "options", "_option_from_key", "_mask_from_offset", "_option_from_offset_mask")
kind = Kind.MULTIPLE_TOGGLE
sep = 0x01
def __init__(self, options, om_method=None, byte_count=None):
assert isinstance(options, list)
# each element of options is an instance of a class
# that has an id (which is used as an index in other dictionaries)
# and where om_method is a method that returns a byte offset and byte mask
# that says how to access and modify the bit toggle for the option
self.options = options
self.om_method = om_method
# to retrieve the options efficiently:
self._option_from_key = {}
self._mask_from_offset = {}
self._option_from_offset_mask = {}
for opt in options:
offset, mask = om_method(opt)
self._option_from_key[int(opt)] = opt
try:
self._mask_from_offset[offset] |= mask
except KeyError:
self._mask_from_offset[offset] = mask
try:
mask_to_opt = self._option_from_offset_mask[offset]
except KeyError:
mask_to_opt = {}
self._option_from_offset_mask[offset] = mask_to_opt
mask_to_opt[mask] = opt
self.byte_count = (max(om_method(x)[1].bit_length() for x in options) + 7) // 8 # is this correct??
if byte_count:
assert isinstance(byte_count, int) and byte_count >= self.byte_count
self.byte_count = byte_count
def prepare_read(self):
r = []
for offset, mask in self._mask_from_offset.items():
b = offset << (8 * (self.byte_count + 1))
b |= (self.sep << (8 * self.byte_count)) | mask
r.append(common.int2bytes(b, self.byte_count + 2))
return r
def prepare_read_key(self, key):
option = self._option_from_key.get(key, None)
if option is None:
return None
offset, mask = option.om_method(option)
b = offset << (8 * (self.byte_count + 1))
b |= (self.sep << (8 * self.byte_count)) | mask
return common.int2bytes(b, self.byte_count + 2)
def validate_read(self, reply_bytes_dict):
values = {int(k): False for k in self.options}
for query, b in reply_bytes_dict.items():
offset = common.bytes2int(query[0:1])
b += (self.byte_count - len(b)) * b"\x00"
value = common.bytes2int(b[: self.byte_count])
mask_to_opt = self._option_from_offset_mask.get(offset, {})
m = 1
for _ignore in range(8 * self.byte_count):
if m in mask_to_opt:
values[int(mask_to_opt[m])] = bool(value & m)
m <<= 1
return values
def prepare_write(self, new_value):
assert isinstance(new_value, dict)
w = {}
for k, v in new_value.items():
option = self._option_from_key[int(k)]
offset, mask = self.om_method(option)
if offset not in w:
w[offset] = 0
if v:
w[offset] |= mask
return [
common.int2bytes(
(offset << (8 * (2 * self.byte_count + 1)))
| (self.sep << (16 * self.byte_count))
| (self._mask_from_offset[offset] << (8 * self.byte_count))
| value,
2 * self.byte_count + 2,
)
for offset, value in w.items()
]
def get_options(self):
return [int(opt) if isinstance(opt, int) else opt.as_int() for opt in self.options]
def acceptable(self, args, current):
if len(args) != 2:
return None
key = next((option.id for option in self.options if option.as_int() == args[0]), None)
if key is None:
return None
val = bool_or_toggle(current[int(key)], args[1])
return None if val is None else [int(key), val]
def compare(self, args, current):
if len(args) != 2:
return False
key = next((option.id for option in self.options if option.as_int() == args[0]), None)
if key is None:
return False
return args[1] == current[int(key)]
class ChoicesValidator(Validator):
"""Translates between NamedInts and a byte sequence.
:param choices: a list of NamedInts
:param byte_count: the size of the derived byte sequence. If None, it
will be calculated from the choices."""
kind = Kind.CHOICE
def __init__(self, choices=None, byte_count=None, read_skip_byte_count=0, write_prefix_bytes=b""):
assert choices is not None
assert isinstance(choices, NamedInts)
assert len(choices) > 1
self.choices = choices
self.needs_current_value = False
max_bits = max(x.bit_length() for x in choices)
self._byte_count = (max_bits // 8) + (1 if max_bits % 8 else 0)
if byte_count:
assert self._byte_count <= byte_count
self._byte_count = byte_count
assert self._byte_count < 8
self._read_skip_byte_count = read_skip_byte_count
self._write_prefix_bytes = write_prefix_bytes if write_prefix_bytes else b""
assert self._byte_count + self._read_skip_byte_count <= 14
assert self._byte_count + len(self._write_prefix_bytes) <= 14
def to_string(self, value):
return str(self.choices[value]) if isinstance(value, int) else str(value)
def validate_read(self, reply_bytes):
reply_value = common.bytes2int(reply_bytes[self._read_skip_byte_count : self._read_skip_byte_count + self._byte_count])
valid_value = self.choices[reply_value]
assert valid_value is not None, f"{self.__class__.__name__}: failed to validate read value {reply_value:02X}"
return valid_value
def prepare_write(self, new_value, current_value=None):
if new_value is None:
value = self.choices[:][0]
else:
value = self.choice(new_value)
if value is None:
raise ValueError(f"invalid choice {new_value!r}")
assert isinstance(value, NamedInt)
return self._write_prefix_bytes + value.bytes(self._byte_count)
def choice(self, value):
if isinstance(value, int):
return self.choices[value]
try:
int(value)
if int(value) in self.choices:
return self.choices[int(value)]
except Exception:
pass
if value in self.choices:
return self.choices[value]
else:
return None
def acceptable(self, args, current):
choice = self.choice(args[0]) if len(args) == 1 else None
return None if choice is None else [choice]
class ChoicesMapValidator(ChoicesValidator):
kind = Kind.MAP_CHOICE
def __init__(
self,
choices_map,
key_byte_count=0,
key_postfix_bytes=b"",
byte_count=0,
read_skip_byte_count=0,
write_prefix_bytes=b"",
extra_default=None,
mask=-1,
activate=0,
):
assert choices_map is not None
assert isinstance(choices_map, dict)
max_key_bits = 0
max_value_bits = 0
for key, choices in choices_map.items():
assert isinstance(key, NamedInt)
assert isinstance(choices, NamedInts)
max_key_bits = max(max_key_bits, key.bit_length())
for key_value in choices:
assert isinstance(key_value, NamedInt)
max_value_bits = max(max_value_bits, key_value.bit_length())
self._key_byte_count = (max_key_bits + 7) // 8
if key_byte_count:
assert self._key_byte_count <= key_byte_count
self._key_byte_count = key_byte_count
self._byte_count = (max_value_bits + 7) // 8
if byte_count:
assert self._byte_count <= byte_count
self._byte_count = byte_count
self.choices = choices_map
self.needs_current_value = False
self.extra_default = extra_default
self._key_postfix_bytes = key_postfix_bytes
self._read_skip_byte_count = read_skip_byte_count if read_skip_byte_count else 0
self._write_prefix_bytes = write_prefix_bytes if write_prefix_bytes else b""
self.activate = activate
self.mask = mask
assert self._byte_count + self._read_skip_byte_count + self._key_byte_count <= 14
assert self._byte_count + len(self._write_prefix_bytes) + self._key_byte_count <= 14
def to_string(self, value):
def element_to_string(key, val):
k, c = next(((k, c) for k, c in self.choices.items() if int(key) == k), (None, None))
return str(k) + ":" + str(c[val]) if k is not None else "?"
return "{" + ", ".join([element_to_string(k, value[k]) for k in sorted(value)]) + "}"
def validate_read(self, reply_bytes, key):
start = self._key_byte_count + self._read_skip_byte_count
end = start + self._byte_count
reply_value = common.bytes2int(reply_bytes[start:end]) & self.mask
# reprogrammable keys starts out as 0, which is not a choice, so don't use assert here
if self.extra_default is not None and self.extra_default == reply_value:
return int(self.choices[key][0])
if reply_value not in self.choices[key]:
assert reply_value in self.choices[key], "%s: failed to validate read value %02X" % (
self.__class__.__name__,
reply_value,
)
return reply_value
def prepare_key(self, key):
return key.to_bytes(self._key_byte_count, "big") + self._key_postfix_bytes
def prepare_write(self, key, new_value):
choices = self.choices.get(key)
if choices is None or (new_value not in choices and new_value != self.extra_default):
logger.error("invalid choice %r for %s", new_value, key)
return None
new_value = new_value | self.activate
return self._write_prefix_bytes + new_value.to_bytes(self._byte_count, "big")
def acceptable(self, args, current):
if len(args) != 2:
return None
key, choices = next(((key, item) for key, item in self.choices.items() if key == args[0]), (None, None))
if choices is None or args[1] not in choices:
return None
choice = next((item for item in choices if item == args[1]), None)
return [int(key), int(choice)] if choice is not None else None
def compare(self, args, current):
if len(args) != 2:
return False
key = next((key for key in self.choices if key == int(args[0])), None)
if key is None:
return False
return args[1] == current[int(key)]
class RangeValidator(Validator):
kind = Kind.RANGE
"""Translates between integers and a byte sequence.
:param min_value: minimum accepted value (inclusive)
:param max_value: maximum accepted value (inclusive)
:param byte_count: the size of the derived byte sequence. If None, it
will be calculated from the range."""
min_value = 0
max_value = 255
@classmethod
def build(cls, setting_class, device, **kwargs):
kwargs["min_value"] = setting_class.min_value
kwargs["max_value"] = setting_class.max_value
return cls(**kwargs)
def __init__(self, min_value=0, max_value=255, byte_count=1):
assert max_value > min_value
self.min_value = min_value
self.max_value = max_value
self.needs_current_value = True # read and check before write (needed for ADC power and probably a good idea anyway)
self._byte_count = math.ceil(math.log(max_value + 1, 256))
if byte_count:
assert self._byte_count <= byte_count
self._byte_count = byte_count
assert self._byte_count < 8
def validate_read(self, reply_bytes):
reply_value = common.bytes2int(reply_bytes[: self._byte_count])
assert reply_value >= self.min_value, f"{self.__class__.__name__}: failed to validate read value {reply_value:02X}"
assert reply_value <= self.max_value, f"{self.__class__.__name__}: failed to validate read value {reply_value:02X}"
return reply_value
def prepare_write(self, new_value, current_value=None):
if new_value < self.min_value or new_value > self.max_value:
raise ValueError(f"invalid choice {new_value!r}")
current_value = self.validate_read(current_value) if current_value is not None else None
to_write = common.int2bytes(new_value, self._byte_count)
# current value is known and same as value to be written return None to signal not to write it
return None if current_value is not None and current_value == new_value else to_write
def acceptable(self, args, current):
arg = args[0]
# None if len(args) != 1 or type(arg) != int or arg < self.min_value or arg > self.max_value else args)
return None if len(args) != 1 or isinstance(arg, int) or arg < self.min_value or arg > self.max_value else args
def compare(self, args, current):
if len(args) == 1:
return args[0] == current
elif len(args) == 2:
return args[0] <= current <= args[1]
else:
return False
class HeteroValidator(Validator):
kind = Kind.HETERO
@classmethod
def build(cls, setting_class, device, **kwargs):
return cls(**kwargs)
def __init__(self, data_class=None, options=None, readable=True):
assert data_class is not None and options is not None
self.data_class = data_class
self.options = options
self.readable = readable
self.needs_current_value = False
def validate_read(self, reply_bytes):
if self.readable:
reply_value = self.data_class.from_bytes(reply_bytes, options=self.options)
return reply_value
def prepare_write(self, new_value, current_value=None):
to_write = new_value.to_bytes(options=self.options)
return to_write
def acceptable(self, args, current): # should this actually do some checking?
return True
class PackedRangeValidator(Validator):
kind = Kind.PACKED_RANGE
"""Several range values, all the same size, all the same min and max"""
min_value = 0
max_value = 255
count = 1
rsbc = 0
write_prefix_bytes = b""
def __init__(
self, keys, min_value=0, max_value=255, count=1, byte_count=1, read_skip_byte_count=0, write_prefix_bytes=b""
):
assert max_value > min_value
self.needs_current_value = True
self.keys = keys
self.min_value = min_value
self.max_value = max_value
self.count = count
self.bc = math.ceil(math.log(max_value + 1 - min(0, min_value), 256))
if byte_count:
assert self.bc <= byte_count
self.bc = byte_count
assert self.bc * self.count
self.rsbc = read_skip_byte_count
self.write_prefix_bytes = write_prefix_bytes
def validate_read(self, reply_bytes):
rvs = {
n: common.bytes2int(reply_bytes[self.rsbc + n * self.bc : self.rsbc + (n + 1) * self.bc], signed=True)
for n in range(self.count)
}
for n in range(self.count):
assert rvs[n] >= self.min_value, f"{self.__class__.__name__}: failed to validate read value {rvs[n]:02X}"
assert rvs[n] <= self.max_value, f"{self.__class__.__name__}: failed to validate read value {rvs[n]:02X}"
return rvs
def prepare_write(self, new_values):
if len(new_values) != self.count:
raise ValueError(f"wrong number of values {new_values!r}")
for new_value in new_values.values():
if new_value < self.min_value or new_value > self.max_value:
raise ValueError(f"invalid value {new_value!r}")
bytes = self.write_prefix_bytes + b"".join(
common.int2bytes(new_values[n], self.bc, signed=True) for n in range(self.count)
)
return bytes
def acceptable(self, args, current):
if len(args) != 2 or int(args[0]) < 0 or int(args[0]) >= self.count:
return None
return None if not isinstance(args[1], int) or args[1] < self.min_value or args[1] > self.max_value else args
def compare(self, args, current):
logger.warning("compare not implemented for packed range settings")
return False
class MultipleRangeValidator(Validator):
kind = Kind.MULTIPLE_RANGE
def __init__(self, items, sub_items):
assert isinstance(items, list) # each element must have .index and its __int__ must return its id (not its index)
assert isinstance(sub_items, dict)
# sub_items: items -> class with .minimum, .maximum, .length (in bytes), .id (a string) and .widget (e.g. 'Scale')
self.items = items
self.keys = NamedInts(**{str(item): int(item) for item in items})
self._item_from_id = {int(k): k for k in items}
self.sub_items = sub_items
def prepare_read_item(self, item):
return common.int2bytes((self._item_from_id[int(item)].index << 1) | 0xFF, 2)
def validate_read_item(self, reply_bytes, item):
item = self._item_from_id[int(item)]
start = 0
value = {}
for sub_item in self.sub_items[item]:
r = reply_bytes[start : start + sub_item.length]
if len(r) < sub_item.length:
r += b"\x00" * (sub_item.length - len(value))
v = common.bytes2int(r)
if not (sub_item.minimum < v < sub_item.maximum):
logger.warning(
f"{self.__class__.__name__}: failed to validate read value for {item}.{sub_item}: "
+ f"{v} not in [{sub_item.minimum}..{sub_item.maximum}]"
)
value[str(sub_item)] = v
start += sub_item.length
return value
def prepare_write(self, value):
seq = []
w = b""
for item in value.keys():
_item = self._item_from_id[int(item)]
b = common.int2bytes(_item.index, 1)
for sub_item in self.sub_items[_item]:
try:
v = value[int(item)][str(sub_item)]
except KeyError:
return None
if not (sub_item.minimum <= v <= sub_item.maximum):
raise ValueError(
f"invalid choice for {item}.{sub_item}: {v} not in [{sub_item.minimum}..{sub_item.maximum}]"
)
b += common.int2bytes(v, sub_item.length)
if len(w) + len(b) > 15:
seq.append(b + b"\xff")
w = b""
w += b
seq.append(w + b"\xff")
return seq
def prepare_write_item(self, item, value):
_item = self._item_from_id[int(item)]
w = common.int2bytes(_item.index, 1)
for sub_item in self.sub_items[_item]:
try:
v = value[str(sub_item)]
except KeyError:
return None
if not (sub_item.minimum <= v <= sub_item.maximum):
raise ValueError(f"invalid choice for {item}.{sub_item}: {v} not in [{sub_item.minimum}..{sub_item.maximum}]")
w += common.int2bytes(v, sub_item.length)
return w + b"\xff"
def acceptable(self, args, current):
# just one item, with at least one sub-item
if not isinstance(args, list) or len(args) != 2 or not isinstance(args[1], dict):
return None
item = next((p for p in self.items if p.id == args[0] or str(p) == args[0]), None)
if not item:
return None
for sub_key, value in args[1].items():
sub_item = next((it for it in self.sub_items[item] if it.id == sub_key), None)
if not sub_item:
return None
if not isinstance(value, int) or not (sub_item.minimum <= value <= sub_item.maximum):
return None
return [int(item), {**args[1]}]
def compare(self, args, current):
logger.warning("compare not implemented for multiple range settings")
return False

View File

@ -30,9 +30,9 @@ def _print_setting(s, verbose=True):
if verbose:
if s.description:
print("#", s.description.replace("\n", " "))
if s.kind == settings.KIND.toggle:
if s.kind == settings.Kind.TOGGLE:
print("# possible values: on/true/t/yes/y/1 or off/false/f/no/n/0 or Toggle/~")
elif s.kind == settings.KIND.choice:
elif s.kind == settings.Kind.CHOICE:
print(
"# possible values: one of [",
", ".join(str(v) for v in s.choices),
@ -53,7 +53,7 @@ def _print_setting_keyed(s, key, verbose=True):
if verbose:
if s.description:
print("#", s.description.replace("\n", " "))
if s.kind == settings.KIND.multiple_toggle:
if s.kind == settings.Kind.MULTIPLE_TOGGLE:
k = next((k for k in s._labels if key == k), None)
if k is None:
print(s.name, "=? (key not found)")
@ -64,7 +64,7 @@ def _print_setting_keyed(s, key, verbose=True):
print(s.name, "= ? (failed to read from device)")
else:
print(s.name, s.val_to_string({k: value[str(int(k))]}))
elif s.kind == settings.KIND.map_choice:
elif s.kind == settings.Kind.MAP_CHOICE:
k = next((k for k in s.choices.keys() if key == k), None)
if k is None:
print(s.name, "=? (key not found)")
@ -215,26 +215,26 @@ def run(receivers, args, _find_receiver, find_device):
dev.persister[setting.name] = setting._value
def set(dev, setting, args, save):
if setting.kind == settings.KIND.toggle:
def set(dev, setting: settings.Setting, args, save):
if setting.kind == settings.Kind.TOGGLE:
value = select_toggle(args.value_key, setting)
args.value_key = value
message = f"Setting {setting.name} of {dev.name} to {value}"
result = setting.write(value, save=save)
elif setting.kind == settings.KIND.range:
elif setting.kind == settings.Kind.RANGE:
value = select_range(args.value_key, setting)
args.value_key = value
message = f"Setting {setting.name} of {dev.name} to {value}"
result = setting.write(value, save=save)
elif setting.kind == settings.KIND.choice:
elif setting.kind == settings.Kind.CHOICE:
value = select_choice(args.value_key, setting.choices, setting, None)
args.value_key = int(value)
message = f"Setting {setting.name} of {dev.name} to {value}"
result = setting.write(value, save=save)
elif setting.kind == settings.KIND.map_choice:
elif setting.kind == settings.Kind.MAP_CHOICE:
if args.extra_subkey is None:
_print_setting_keyed(setting, args.value_key)
return None, None, None
@ -252,7 +252,7 @@ def set(dev, setting, args, save):
message = f"Setting {setting.name} of {dev.name} key {k!r} to {value!r}"
result = setting.write_key_value(int(k), value, save=save)
elif setting.kind == settings.KIND.multiple_toggle:
elif setting.kind == settings.Kind.MULTIPLE_TOGGLE:
if args.extra_subkey is None:
_print_setting_keyed(setting, args.value_key)
return None, None, None
@ -271,7 +271,7 @@ def set(dev, setting, args, save):
message = f"Setting {setting.name} key {k!r} to {value!r}"
result = setting.write_key_value(str(int(k)), value, save=save)
elif setting.kind == settings.KIND.multiple_range:
elif setting.kind == settings.Kind.MULTIPLE_RANGE:
if args.extra_subkey is None:
raise Exception(f"{setting.name}: setting needs both key and value to set")
key = args.value_key

View File

@ -666,21 +666,21 @@ def _create_sbox(s, _device):
change.set_sensitive(True)
change.connect("clicked", _change_click, sbox)
if s.kind == settings.KIND.toggle:
if s.kind == settings.Kind.TOGGLE:
control = ToggleControl(sbox)
elif s.kind == settings.KIND.range:
elif s.kind == settings.Kind.RANGE:
control = SliderControl(sbox)
elif s.kind == settings.KIND.choice:
elif s.kind == settings.Kind.CHOICE:
control = _create_choice_control(sbox)
elif s.kind == settings.KIND.map_choice:
elif s.kind == settings.Kind.MAP_CHOICE:
control = MapChoiceControl(sbox)
elif s.kind == settings.KIND.multiple_toggle:
elif s.kind == settings.Kind.MULTIPLE_TOGGLE:
control = MultipleToggleControl(sbox, change)
elif s.kind == settings.KIND.multiple_range:
elif s.kind == settings.Kind.MULTIPLE_RANGE:
control = MultipleRangeControl(sbox, change)
elif s.kind == settings.KIND.packed_range:
elif s.kind == settings.Kind.PACKED_RANGE:
control = PackedRangeControl(sbox, change)
elif s.kind == settings.KIND.hetero:
elif s.kind == settings.Kind.HETERO:
control = HeteroKeyControl(sbox, change)
else:
if logger.isEnabledFor(logging.WARNING):

View File

@ -39,7 +39,7 @@ from logitech_receiver import diversion as _DIV
from logitech_receiver.common import NamedInt
from logitech_receiver.common import NamedInts
from logitech_receiver.common import UnsortedNamedInts
from logitech_receiver.settings import KIND as _SKIND
from logitech_receiver.settings import Kind
from logitech_receiver.settings import Setting
from logitech_receiver.settings_templates import SETTINGS
@ -1455,7 +1455,7 @@ class HostUI(ConditionUI):
class _SettingWithValueUI:
ALL_SETTINGS = _all_settings()
MULTIPLE = [_SKIND.multiple_toggle, _SKIND.map_choice, _SKIND.multiple_range]
MULTIPLE = [Kind.MULTIPLE_TOGGLE, Kind.MAP_CHOICE, Kind.MULTIPLE_RANGE]
ACCEPT_TOGGLE = True
label_text = ""
@ -1569,7 +1569,7 @@ class _SettingWithValueUI:
if kind in cls.MULTIPLE:
keys = UnsortedNamedInts()
for s in settings:
universe = getattr(s, "keys_universe" if kind == _SKIND.map_choice else "choices_universe", None)
universe = getattr(s, "keys_universe" if kind == Kind.MAP_CHOICE else "choices_universe", None)
if universe:
keys |= universe
# only one key per number is used
@ -1641,12 +1641,12 @@ class _SettingWithValueUI:
supported_keys = None
if device_setting:
val = device_setting._validator
if device_setting.kind == _SKIND.multiple_toggle:
if device_setting.kind == Kind.MULTIPLE_TOGGLE:
supported_keys = val.get_options() or None
elif device_setting.kind == _SKIND.map_choice:
elif device_setting.kind == Kind.MAP_CHOICE:
choices = val.choices or None
supported_keys = choices.keys() if choices else None
elif device_setting.kind == _SKIND.multiple_range:
elif device_setting.kind == Kind.MULTIPLE_RANGE:
supported_keys = val.keys
self.key_field.show_only(supported_keys, include_new=True)
self._update_validation()
@ -1655,24 +1655,24 @@ class _SettingWithValueUI:
setting, val_class, kind, keys = self._setting_attributes(setting_name, device)
ds = device.settings if device else {}
device_setting = ds.get(setting_name, None)
if kind in (_SKIND.toggle, _SKIND.multiple_toggle):
if kind in (Kind.TOGGLE, Kind.MULTIPLE_TOGGLE):
self.value_field.make_toggle()
elif kind in (_SKIND.choice, _SKIND.map_choice):
elif kind in (Kind.CHOICE, Kind.MAP_CHOICE):
all_values, extra = self._all_choices(device_setting or setting_name)
self.value_field.make_choice(all_values, extra)
supported_values = None
if device_setting:
val = device_setting._validator
choices = getattr(val, "choices", None) or None
if kind == _SKIND.choice:
if kind == Kind.CHOICE:
supported_values = choices
elif kind == _SKIND.map_choice and isinstance(choices, dict):
elif kind == Kind.MAP_CHOICE and isinstance(choices, dict):
supported_values = choices.get(key, None) or None
self.value_field.choice_widget.show_only(supported_values, include_new=True)
self._update_validation()
elif kind == _SKIND.range:
elif kind == Kind.RANGE:
self.value_field.make_range(val_class.min_value, val_class.max_value)
elif kind == _SKIND.multiple_range:
elif kind == Kind.MULTIPLE_RANGE:
self.value_field.make_range_with_key(
getattr(setting, "sub_items_universe", {}).get(key, {}) if setting else {},
getattr(setting, "_labels_sub", None) if setting else None,
@ -1703,7 +1703,7 @@ class _SettingWithValueUI:
key = self.key_field.get_value(invalid_as_str=False, accept_hidden=False)
icon = "dialog-warning" if key is None else ""
self.key_field.get_child().set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, icon)
if kind in (_SKIND.choice, _SKIND.map_choice):
if kind in (Kind.CHOICE, Kind.MAP_CHOICE):
value = self.value_field.choice_widget.get_value(invalid_as_str=False, accept_hidden=False)
icon = "dialog-warning" if value is None else ""
self.value_field.choice_widget.get_child().set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, icon)
@ -1758,26 +1758,26 @@ class _SettingWithValueUI:
key_label = getattr(setting, "_labels", {}).get(key, [None])[0] if setting else None
disp.append(key_label or key)
value = next(a, None)
if setting and (kind in (_SKIND.choice, _SKIND.map_choice)):
if setting and (kind in (Kind.CHOICE, Kind.MAP_CHOICE)):
all_values = cls._all_choices(setting or setting_name)[0]
supported_values = None
if device_setting:
val = device_setting._validator
choices = getattr(val, "choices", None) or None
if kind == _SKIND.choice:
if kind == Kind.CHOICE:
supported_values = choices
elif kind == _SKIND.map_choice and isinstance(choices, dict):
elif kind == Kind.MAP_CHOICE and isinstance(choices, dict):
supported_values = choices.get(key, None) or None
if supported_values and isinstance(supported_values, NamedInts):
value = supported_values[value]
if not supported_values and all_values and isinstance(all_values, NamedInts):
value = all_values[value]
disp.append(value)
elif kind == _SKIND.multiple_range and isinstance(value, dict) and len(value) == 1:
elif kind == Kind.MULTIPLE_RANGE and isinstance(value, dict) and len(value) == 1:
k, v = next(iter(value.items()))
k = (getattr(setting, "_labels_sub", {}).get(k, (None,))[0] if setting else None) or k
disp.append(f"{k}={v}")
elif kind in (_SKIND.toggle, _SKIND.multiple_toggle):
elif kind in (Kind.TOGGLE, Kind.MULTIPLE_TOGGLE):
disp.append(_(str(value)))
else:
disp.append(value)

View File

@ -1,6 +1,6 @@
import pytest
from logitech_receiver import settings
from logitech_receiver import settings_validator
@pytest.mark.parametrize(
@ -20,6 +20,6 @@ from logitech_receiver import settings
],
)
def test_bool_or_toggle(current, new, expected):
result = settings.bool_or_toggle(current=current, new=new)
result = settings_validator.bool_or_toggle(current=current, new=new)
assert result == expected