aad042650e
Covers all major Live API objects with get/set/listen/method handlers: song, track, return_track, clip, clip_slot, device (incl. rack chains and drum pads), scene, view, application, browser, groove. Zero external runtime dependencies — OSC encoded/decoded in osc_server.py. Wildcard * support for track/scene indices. Listener callbacks fire to matching /get/ addresses for bidirectional state sync. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
217 lines
7.9 KiB
Python
217 lines
7.9 KiB
Python
"""Handles /live/view/* OSC addresses (UI navigation)."""
|
|
import logging
|
|
from typing import Optional
|
|
from .handler import AbletonOSCHandler
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class ViewHandler(AbletonOSCHandler):
|
|
def init_api(self) -> None:
|
|
self.clear_listeners()
|
|
view = self._song_view()
|
|
|
|
# selected scene
|
|
self._add("/live/view/get/selected_scene", self._get_selected_scene)
|
|
self._add("/live/view/set/selected_scene", self._set_selected_scene)
|
|
self._add("/live/view/start_listen/selected_scene", self._start_listen_selected_scene)
|
|
self._add("/live/view/stop_listen/selected_scene", self._stop_listen_selected_scene)
|
|
|
|
# selected track
|
|
self._add("/live/view/get/selected_track", self._get_selected_track)
|
|
self._add("/live/view/set/selected_track", self._set_selected_track)
|
|
self._add("/live/view/start_listen/selected_track", self._start_listen_selected_track)
|
|
self._add("/live/view/stop_listen/selected_track", self._stop_listen_selected_track)
|
|
|
|
# selected clip
|
|
self._add("/live/view/get/selected_clip", self._get_selected_clip)
|
|
self._add("/live/view/set/selected_clip", self._set_selected_clip)
|
|
|
|
# selected device
|
|
self._add("/live/view/get/selected_device", self._get_selected_device)
|
|
|
|
# focus
|
|
self._add("/live/view/show_clip_detail_view", self._show_clip_detail_view)
|
|
self._add("/live/view/show_device_detail_view", self._show_device_detail_view)
|
|
self._add("/live/view/focus_browser", self._focus_browser)
|
|
|
|
# arrangement / session toggle
|
|
self._add("/live/view/get/focused_document_view", self._get_focused_document_view)
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
def _song_view(self):
|
|
try:
|
|
return self.song.view
|
|
except Exception:
|
|
return None
|
|
|
|
def _get_selected_scene(self, params: tuple) -> Optional[tuple]:
|
|
try:
|
|
view = self._song_view()
|
|
scenes = list(self.song.scenes)
|
|
scene = view.selected_scene
|
|
idx = scenes.index(scene)
|
|
return (idx,)
|
|
except Exception as e:
|
|
logger.warning("get selected_scene: %s", e)
|
|
return None
|
|
|
|
def _set_selected_scene(self, params: tuple) -> None:
|
|
if not params:
|
|
return None
|
|
try:
|
|
idx = int(params[0])
|
|
scenes = list(self.song.scenes)
|
|
if 0 <= idx < len(scenes):
|
|
self._song_view().selected_scene = scenes[idx]
|
|
except Exception as e:
|
|
logger.warning("set selected_scene: %s", e)
|
|
return None
|
|
|
|
def _start_listen_selected_scene(self, params: tuple) -> None:
|
|
view = self._song_view()
|
|
if view is None:
|
|
return
|
|
key = "view.selected_scene"
|
|
self._remove_listener(key)
|
|
|
|
def cb():
|
|
result = self._get_selected_scene(())
|
|
if result:
|
|
self._send("/live/view/get/selected_scene", result)
|
|
|
|
try:
|
|
view.add_selected_scene_listener(cb)
|
|
self._listener_store[key] = (view.remove_selected_scene_listener, cb)
|
|
cb()
|
|
except Exception as e:
|
|
logger.warning("selected_scene listener: %s", e)
|
|
|
|
def _stop_listen_selected_scene(self, params: tuple) -> None:
|
|
self._remove_listener("view.selected_scene")
|
|
|
|
def _get_selected_track(self, params: tuple) -> Optional[tuple]:
|
|
try:
|
|
view = self._song_view()
|
|
all_tracks = list(self.song.tracks) + list(self.song.return_tracks)
|
|
track = view.selected_track
|
|
idx = all_tracks.index(track)
|
|
return (idx,)
|
|
except Exception as e:
|
|
logger.warning("get selected_track: %s", e)
|
|
return None
|
|
|
|
def _set_selected_track(self, params: tuple) -> None:
|
|
if not params:
|
|
return None
|
|
try:
|
|
idx = int(params[0])
|
|
all_tracks = list(self.song.tracks) + list(self.song.return_tracks)
|
|
if 0 <= idx < len(all_tracks):
|
|
self._song_view().selected_track = all_tracks[idx]
|
|
except Exception as e:
|
|
logger.warning("set selected_track: %s", e)
|
|
return None
|
|
|
|
def _start_listen_selected_track(self, params: tuple) -> None:
|
|
view = self._song_view()
|
|
if view is None:
|
|
return
|
|
key = "view.selected_track"
|
|
self._remove_listener(key)
|
|
|
|
def cb():
|
|
result = self._get_selected_track(())
|
|
if result:
|
|
self._send("/live/view/get/selected_track", result)
|
|
|
|
try:
|
|
view.add_selected_track_listener(cb)
|
|
self._listener_store[key] = (view.remove_selected_track_listener, cb)
|
|
cb()
|
|
except Exception as e:
|
|
logger.warning("selected_track listener: %s", e)
|
|
|
|
def _stop_listen_selected_track(self, params: tuple) -> None:
|
|
self._remove_listener("view.selected_track")
|
|
|
|
def _get_selected_clip(self, params: tuple) -> Optional[tuple]:
|
|
try:
|
|
view = self._song_view()
|
|
clip_view = view.detail_clip
|
|
if clip_view is None:
|
|
return None
|
|
tracks = list(self.song.tracks)
|
|
for ti, track in enumerate(tracks):
|
|
for ci, slot in enumerate(track.clip_slots):
|
|
if slot.has_clip and slot.clip == clip_view:
|
|
return (ti, ci)
|
|
except Exception as e:
|
|
logger.warning("get selected_clip: %s", e)
|
|
return None
|
|
|
|
def _set_selected_clip(self, params: tuple) -> None:
|
|
if len(params) < 2:
|
|
return None
|
|
try:
|
|
track_idx = int(params[0])
|
|
clip_idx = int(params[1])
|
|
tracks = list(self.song.tracks)
|
|
if 0 <= track_idx < len(tracks):
|
|
slots = list(tracks[track_idx].clip_slots)
|
|
if 0 <= clip_idx < len(slots) and slots[clip_idx].has_clip:
|
|
self._song_view().detail_clip = slots[clip_idx].clip
|
|
except Exception as e:
|
|
logger.warning("set selected_clip: %s", e)
|
|
return None
|
|
|
|
def _get_selected_device(self, params: tuple) -> Optional[tuple]:
|
|
try:
|
|
view = self._song_view()
|
|
track = view.selected_track
|
|
tracks = list(self.song.tracks) + list(self.song.return_tracks)
|
|
track_idx = tracks.index(track)
|
|
selected = track.view.selected_device
|
|
if selected is None:
|
|
return None
|
|
devices = list(track.devices)
|
|
device_idx = devices.index(selected)
|
|
return (track_idx, device_idx)
|
|
except Exception as e:
|
|
logger.warning("get selected_device: %s", e)
|
|
return None
|
|
|
|
def _show_clip_detail_view(self, params: tuple) -> None:
|
|
try:
|
|
app = self.manager.application()
|
|
app.view.show_view("Detail/Clip")
|
|
except Exception as e:
|
|
logger.warning("show_clip_detail_view: %s", e)
|
|
return None
|
|
|
|
def _show_device_detail_view(self, params: tuple) -> None:
|
|
try:
|
|
app = self.manager.application()
|
|
app.view.show_view("Detail/DeviceChain")
|
|
except Exception as e:
|
|
logger.warning("show_device_detail_view: %s", e)
|
|
return None
|
|
|
|
def _focus_browser(self, params: tuple) -> None:
|
|
try:
|
|
app = self.manager.application()
|
|
app.view.show_view("Browser")
|
|
except Exception as e:
|
|
logger.warning("focus_browser: %s", e)
|
|
return None
|
|
|
|
def _get_focused_document_view(self, params: tuple) -> Optional[tuple]:
|
|
try:
|
|
app = self.manager.application()
|
|
view = app.view.focused_document_view
|
|
return (view,)
|
|
except Exception as e:
|
|
logger.warning("get focused_document_view: %s", e)
|
|
return None
|