device: add callback to call when changing a setting

This commit is contained in:
Peter F. Patel-Schneider 2024-02-18 13:48:52 -05:00
parent 476f41f8ae
commit ed248c62b9
6 changed files with 39 additions and 34 deletions

View File

@ -63,7 +63,8 @@ class Device:
short=None, short=None,
long=None, long=None,
product_id=None, product_id=None,
bus_id=None bus_id=None,
setting_callback=None
): ):
assert receiver or handle assert receiver or handle
Device.instances.append(self) Device.instances.append(self)
@ -76,6 +77,7 @@ class Device:
self.hidpp_short = short self.hidpp_short = short
self.hidpp_long = long self.hidpp_long = long
self.bluetooth = bus_id == 0x0005 # Bluetooth connections need long messages self.bluetooth = bus_id == 0x0005 # Bluetooth connections need long messages
self.setting_callback = setting_callback
if receiver: if receiver:
assert number > 0 and number <= 15 # some receivers have devices past their max # of devices assert number > 0 and number <= 15 # some receivers have devices past their max # of devices
@ -526,7 +528,7 @@ class Device:
pass pass
@classmethod @classmethod
def open(self, device_info): def open(self, device_info, setting_callback=None):
"""Opens a Logitech Device found attached to the machine, by Linux device path. """Opens a Logitech Device found attached to the machine, by Linux device path.
:returns: An open file handle for the found receiver, or ``None``. :returns: An open file handle for the found receiver, or ``None``.
""" """
@ -541,7 +543,8 @@ class Device:
short=device_info.hidpp_short, short=device_info.hidpp_short,
long=device_info.hidpp_long, long=device_info.hidpp_long,
product_id=device_info.product_id, product_id=device_info.product_id,
bus_id=device_info.bus_id bus_id=device_info.bus_id,
setting_callback=setting_callback
) )
except OSError as e: except OSError as e:
logger.exception('open %s', device_info) logger.exception('open %s', device_info)

View File

@ -1285,9 +1285,6 @@ class Set(Action):
return 'Set: ' + ' '.join([str(a) for a in self.args]) return 'Set: ' + ' '.join([str(a) for a in self.args])
def evaluate(self, feature, notification, device, status, last_result): def evaluate(self, feature, notification, device, status, last_result):
# importing here to avoid circular imports
from solaar.ui.config_panel import change_setting as _change_setting
if len(self.args) < 3: if len(self.args) < 3:
return None return None
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
@ -1304,7 +1301,9 @@ class Set(Action):
if args is None: if args is None:
logger.warning('Set Action: invalid args %s for setting %s of %s', self.args[2:], self.args[1], self.args[0]) logger.warning('Set Action: invalid args %s for setting %s of %s', self.args[2:], self.args[1], self.args[0])
return None return None
_change_setting(dev, setting, args) setting.write(*args)
if device.setting_callback:
device.setting_callback(device, type(setting), args)
return None return None
def data(self): def data(self):

View File

@ -22,8 +22,6 @@ import threading as _threading
from struct import unpack as _unpack from struct import unpack as _unpack
from solaar.ui.config_panel import record_setting
from . import diversion as _diversion from . import diversion as _diversion
from . import hidpp10 as _hidpp10 from . import hidpp10 as _hidpp10
from . import hidpp20 as _hidpp20 from . import hidpp20 as _hidpp20
@ -413,7 +411,8 @@ def _process_feature_notification(device, status, n, feature):
elif feature == _F.BACKLIGHT2: elif feature == _F.BACKLIGHT2:
if (n.address == 0x00): if (n.address == 0x00):
level = _unpack('!B', n.data[1:2])[0] level = _unpack('!B', n.data[1:2])[0]
record_setting(device, _st.Backlight2Level, [level]) if device.setting_callback:
device.setting_callback(device, _st.Backlight2Level, [level])
elif feature == _F.REPROG_CONTROLS_V4: elif feature == _F.REPROG_CONTROLS_V4:
if n.address == 0x00: if n.address == 0x00:
@ -442,7 +441,8 @@ def _process_feature_notification(device, status, n, feature):
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
logger.info('%s: WHEEL: ratchet: %d', device, ratchet) logger.info('%s: WHEEL: ratchet: %d', device, ratchet)
if ratchet < 2: # don't process messages with unusual ratchet values if ratchet < 2: # don't process messages with unusual ratchet values
record_setting(device, _st.ScrollRatchet, [2 if ratchet else 1]) if device.setting_callback:
device.setting_callback(device, _st.ScrollRatchet, [2 if ratchet else 1])
else: else:
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
logger.info('%s: unknown WHEEL %s', device, n) logger.info('%s: unknown WHEEL %s', device, n)
@ -459,9 +459,11 @@ def _process_feature_notification(device, status, n, feature):
elif (n.address == 0x10): elif (n.address == 0x10):
resolution_index = _unpack('!B', n.data[:1])[0] resolution_index = _unpack('!B', n.data[:1])[0]
profile_sector = _unpack('!H', device.feature_request(_F.ONBOARD_PROFILES, 0x40)[:2])[0] profile_sector = _unpack('!H', device.feature_request(_F.ONBOARD_PROFILES, 0x40)[:2])[0]
for profile in device.profiles.profiles.values() if device.profiles else []: if device.setting_callback:
if profile.sector == profile_sector: for profile in device.profiles.profiles.values() if device.profiles else []:
record_setting(device, _st.AdjustableDpi, [profile.resolutions[resolution_index]]) if profile.sector == profile_sector:
device.setting_callback(device, _st.AdjustableDpi, [profile.resolutions[resolution_index]])
break
_diversion.process_notification(device, status, n, feature) _diversion.process_notification(device, status, n, feature)
return True return True

View File

@ -47,12 +47,13 @@ class Receiver:
number = 0xFF number = 0xFF
kind = None kind = None
def __init__(self, handle, path, product_id): def __init__(self, handle, path, product_id, setting_callback=None):
assert handle assert handle
self.isDevice = False # some devices act as receiver so we need a property to distinguish them self.isDevice = False # some devices act as receiver so we need a property to distinguish them
self.handle = handle self.handle = handle
self.path = path self.path = path
self.product_id = product_id self.product_id = product_id
self.setting_callback = setting_callback
product_info = _product_information(self.product_id) product_info = _product_information(self.product_id)
if not product_info: if not product_info:
logger.warning('Unknown receiver type: %s', self.product_id) logger.warning('Unknown receiver type: %s', self.product_id)
@ -237,7 +238,7 @@ class Receiver:
assert notification is None or notification.sub_id == 0x41 assert notification is None or notification.sub_id == 0x41
try: try:
dev = Device(self, number, notification) dev = Device(self, number, notification, setting_callback=self.setting_callback)
if logger.isEnabledFor(logging.INFO): if logger.isEnabledFor(logging.INFO):
logger.info('%s: found new device %d (%s)', self, number, dev.wpid) logger.info('%s: found new device %d (%s)', self, number, dev.wpid)
self._devices[number] = dev self._devices[number] = dev
@ -382,7 +383,7 @@ class Receiver:
__bool__ = __nonzero__ = lambda self: self.handle is not None __bool__ = __nonzero__ = lambda self: self.handle is not None
@classmethod @classmethod
def open(self, device_info): def open(self, device_info, setting_callback=None):
"""Opens a Logitech Receiver found attached to the machine, by Linux device path. """Opens a Logitech Receiver found attached to the machine, by Linux device path.
:returns: An open file handle for the found receiver, or ``None``. :returns: An open file handle for the found receiver, or ``None``.
@ -390,7 +391,7 @@ class Receiver:
try: try:
handle = _base.open_path(device_info.path) handle = _base.open_path(device_info.path)
if handle: if handle:
return Receiver(handle, device_info.path, device_info.product_id) return Receiver(handle, device_info.path, device_info.product_id, setting_callback)
except OSError as e: except OSError as e:
logger.exception('open %s', device_info) logger.exception('open %s', device_info)
if e.errno == _errno.EACCES: if e.errno == _errno.EACCES:

View File

@ -1447,10 +1447,10 @@ class ActionSettingRW:
self.active = True self.active = True
if divertSetting: if divertSetting:
divertSetting.write_key_value(int(self.key.key), 1) divertSetting.write_key_value(int(self.key.key), 1)
if self.device.setting_callback:
self.device.setting_callback(device, type(divertSetting), [self.key.key, 1])
device.add_notification_handler(self.name, handler) device.add_notification_handler(self.name, handler)
from solaar.ui import status_changed as _status_changed
self.activate_action() self.activate_action()
_status_changed(device, refresh=True) # update main window
else: else:
logger.error('cannot enable %s on %s for key %s', self.name, device, key) logger.error('cannot enable %s on %s for key %s', self.name, device, key)
else: # Disable else: # Disable
@ -1458,8 +1458,8 @@ class ActionSettingRW:
self.active = False self.active = False
if divertSetting: if divertSetting:
divertSetting.write_key_value(int(self.key.key), 0) divertSetting.write_key_value(int(self.key.key), 0)
from solaar.ui import status_changed as _status_changed if self.device.setting_callback:
_status_changed(device, refresh=True) # update main window self.device.setting_callback(device, type(divertSetting), [self.key.key, 0])
try: try:
device.remove_notification_handler(self.name) device.remove_notification_handler(self.name)
except Exception: except Exception:

View File

@ -479,14 +479,14 @@ class ThumbInvert(_Setting):
# change UI to show result of onboard profile change # change UI to show result of onboard profile change
def profile_change(device, profile_sector): def profile_change(device, profile_sector):
from solaar.ui.config_panel import record_setting # prevent circular import if device.setting_callback:
record_setting(device, OnboardProfiles, [profile_sector]) device.setting_callback(device, OnboardProfiles, [profile_sector])
for profile in device.profiles.profiles.values() if device.profiles else []: for profile in device.profiles.profiles.values() if device.profiles else []:
if profile.sector == profile_sector: if profile.sector == profile_sector:
resolution_index = profile.resolution_default_index resolution_index = profile.resolution_default_index
record_setting(device, AdjustableDpi, [profile.resolutions[resolution_index]]) device.setting_callback(device, AdjustableDpi, [profile.resolutions[resolution_index]])
record_setting(device, ReportRate, [profile.report_rate]) device.setting_callback(device, ReportRate, [profile.report_rate])
break break
class OnboardProfiles(_Setting): class OnboardProfiles(_Setting):
@ -772,8 +772,8 @@ class DpiSlidingXY(_RawXYProcessing):
def setNewDpi(self, newDpiIdx): def setNewDpi(self, newDpiIdx):
newDpi = self.dpiChoices[newDpiIdx] newDpi = self.dpiChoices[newDpiIdx]
self.dpiSetting.write(newDpi) self.dpiSetting.write(newDpi)
from solaar.ui import status_changed as _status_changed if self.device.setting_callback:
_status_changed(self.device, refresh=True) # update main window self.device.setting_callback(self.device, type(self.dpiSetting), [newDpi])
def displayNewDpi(self, newDpiIdx): def displayNewDpi(self, newDpiIdx):
from solaar.ui import notify as _notify # import here to avoid circular import when running `solaar show`, from solaar.ui import notify as _notify # import here to avoid circular import when running `solaar show`,
@ -1024,10 +1024,10 @@ class SpeedChange(_Setting):
if newSpeed is not None: if newSpeed is not None:
if speed_setting: if speed_setting:
speed_setting.write(newSpeed) speed_setting.write(newSpeed)
if self.device.setting_callback:
self.device.setting_callback(self.device, type(speed_setting), [newSpeed])
else: else:
logger.error('cannot save sensitivity setting on %s', self.device) logger.error('cannot save sensitivity setting on %s', self.device)
from solaar.ui import status_changed as _status_changed
_status_changed(self.device, refresh=True) # update main window
if self.device.persister: if self.device.persister:
self.device.persister['_speed-change'] = currentSpeed self.device.persister['_speed-change'] = currentSpeed