Files
ableton-osc/AbletonOSC/handler.py
T
valknar 6fc0a84dfe Fix song/application access: property not method call
ControlSurface exposes song and application as properties, not callable
methods. Replace manager.song() and manager.application() with the correct
attribute access throughout handler, application, browser, and view modules.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 13:27:55 +02:00

130 lines
4.7 KiB
Python

"""Base handler with get/set/listen/call infrastructure."""
import logging
from functools import partial
from typing import Any, Callable, Dict, Optional, Tuple
logger = logging.getLogger(__name__)
class AbletonOSCHandler:
def __init__(self, manager):
self.manager = manager
self.osc_server = manager.osc_server
self._listener_store: Dict[str, Callable] = {} # key -> (target, remove_fn)
@property
def song(self):
return self.manager.song
# ------------------------------------------------------------------
# Registration helpers
# ------------------------------------------------------------------
def _add(self, address: str, callback: Callable) -> None:
self.osc_server.add_handler(address, callback)
def _send(self, address: str, args: tuple) -> None:
self.osc_server.send_all_clients(address, args)
# ------------------------------------------------------------------
# Generic property get
# ------------------------------------------------------------------
def _get_prop(self, target: Any, prop: str) -> Any:
try:
return getattr(target, prop)
except Exception as e:
logger.warning("get %s: %s", prop, e)
return None
def _set_prop(self, target: Any, prop: str, value: Any) -> None:
try:
setattr(target, prop, value)
except Exception as e:
logger.warning("set %s=%s: %s", prop, value, e)
# ------------------------------------------------------------------
# Listener management
# ------------------------------------------------------------------
def _register_listener(self, key: str, target: Any, prop: str,
notify_address: str, notify_args_prefix: tuple = ()) -> None:
"""Attach a Live listener and remember it so we can remove it later."""
self._remove_listener(key)
add_fn_name = f"add_{prop}_listener"
remove_fn_name = f"remove_{prop}_listener"
add_fn = getattr(target, add_fn_name, None)
remove_fn = getattr(target, remove_fn_name, None)
if add_fn is None:
logger.warning("No listener support for %s", prop)
return
def callback():
val = self._get_prop(target, prop)
if val is None:
return
if isinstance(val, (list, tuple)):
self._send(notify_address, notify_args_prefix + tuple(val))
else:
self._send(notify_address, notify_args_prefix + (val,))
add_fn(callback)
self._listener_store[key] = (remove_fn, callback)
# Send current value immediately
callback()
def _remove_listener(self, key: str) -> None:
entry = self._listener_store.pop(key, None)
if entry is None:
return
remove_fn, callback = entry
try:
if remove_fn and remove_fn(callback):
pass
elif remove_fn:
remove_fn(callback)
except Exception as e:
logger.warning("remove listener %s: %s", key, e)
def clear_listeners(self) -> None:
for key in list(self._listener_store.keys()):
self._remove_listener(key)
# ------------------------------------------------------------------
# Handler factories for the common get/set/listen pattern
# ------------------------------------------------------------------
def _make_getter(self, get_target: Callable, prop: str,
prefix_from_params: Callable = None) -> Callable:
"""Returns a handler fn that reads target.prop and returns the value."""
def handler(params: tuple) -> Optional[tuple]:
target = get_target(params)
if target is None:
return None
val = self._get_prop(target, prop)
if val is None:
return None
prefix = prefix_from_params(params) if prefix_from_params else ()
if isinstance(val, (list, tuple)):
return prefix + tuple(val)
return prefix + (val,)
return handler
def _make_setter(self, get_target: Callable, prop: str,
value_index: int = 0) -> Callable:
"""Returns a handler fn that sets target.prop from params."""
def handler(params: tuple) -> None:
target = get_target(params)
if target is None:
return None
value = params[value_index]
self._set_prop(target, prop, value)
return None
return handler
def init_api(self) -> None:
"""Override in subclasses to register all OSC handlers."""
pass