Package _Framework :: Module ControlSurface
[hide private]
[frames] | no frames]

Source Code for Module _Framework.ControlSurface

  1  #Embedded file name: /Users/versonator/Hudson/live/Projects/AppLive/Resources/MIDI Remote Scripts/_Framework/ControlSurface.py 
  2  from __future__ import with_statement 
  3  from functools import partial, wraps 
  4  from itertools import chain 
  5  from contextlib import contextmanager 
  6  import Live 
  7  from Dependency import inject 
  8  from Util import BooleanContext, first, find_if, const, in_range 
  9  from Debug import debug_print 
 10  from SubjectSlot import SlotManager 
 11  from DeviceComponent import DeviceComponent 
 12  from PhysicalDisplayElement import PhysicalDisplayElement 
 13  from InputControlElement import InputControlElement, MIDI_CC_TYPE, MIDI_PB_TYPE, MIDI_NOTE_TYPE, MIDI_SYSEX_TYPE, MIDI_PB_STATUS 
 14  import Task 
 15  import Defaults 
 16  CS_LIST_KEY = 'control_surfaces' 
17 18 -class _ModuleLoadedCheck(object):
19 """ 20 This class is not to be instantiated. We just use it to check 21 wether the module have been unloaded (showing leaking listeners). 22 """ 23 pass
24
25 26 -def _scheduled_method(method):
27 """ 28 Methods from two control surfaces that use the component_guard can 29 not live in the stack at the same time. Use this for methods that 30 can be called implicitly by other control surfaces to delay 31 execution. 32 """ 33 34 @wraps(method) 35 def wrapper(self, *a, **k): 36 37 def doit(): 38 return method(self, *a, **k)
39 40 self.schedule_message(1, doit) 41 42 return wrapper 43
44 45 -class ControlSurface(SlotManager):
46 """ 47 Central base class for scripts based on the new Framework. New 48 scripts need to subclass this class and add special behavior. 49 """ 50
51 - def __init__(self, c_instance, publish_self = True, *a, **k):
52 """ Define and Initialize standard behavior """ 53 super(ControlSurface, self).__init__(*a, **k) 54 self.canonical_parent = None 55 if publish_self: 56 if isinstance(__builtins__, dict): 57 if CS_LIST_KEY not in __builtins__.keys(): 58 __builtins__[CS_LIST_KEY] = [] 59 __builtins__[CS_LIST_KEY].append(self) 60 else: 61 if not hasattr(__builtins__, CS_LIST_KEY): 62 setattr(__builtins__, CS_LIST_KEY, []) 63 cs_list = getattr(__builtins__, CS_LIST_KEY) 64 cs_list.append(self) 65 setattr(__builtins__, CS_LIST_KEY, cs_list) 66 self._c_instance = c_instance 67 self._pad_translations = None 68 self._suggested_input_port = str('') 69 self._suggested_output_port = str('') 70 self._components = [] 71 self._displays = [] 72 self.controls = [] 73 self._highlighting_session_component = None 74 self._device_component = None 75 self._device_selection_follows_track_selection = False 76 self._forwarding_long_identifier_registry = {} 77 self._forwarding_registry = {} 78 self._is_sending_scheduled_messages = BooleanContext() 79 self._remaining_scheduled_messages = [] 80 self._task_group = Task.TaskGroup(auto_kill=False) 81 self._in_build_midi_map = BooleanContext() 82 self._suppress_requests_counter = 0 83 self._rebuild_requests_during_suppression = 0 84 self._enabled = True 85 self._in_component_guard = BooleanContext() 86 self._accumulate_midi_messages = BooleanContext() 87 self._midi_message_dict = {} 88 self._midi_message_list = [] 89 self._midi_message_count = 0 90 self._control_surface_injector = inject(parent_task_group=const(self._task_group), show_message=const(self.show_message), register_component=const(self._register_component), register_control=const(self._register_control), request_rebuild_midi_map=const(self.request_rebuild_midi_map), send_midi=const(self._send_midi), song=self.song).everywhere() 91 with self.setting_listener_caller(): 92 self.song().add_visible_tracks_listener(self._on_track_list_changed) 93 self.song().add_scenes_listener(self._on_scene_list_changed) 94 self.song().view.add_selected_track_listener(self._on_selected_track_changed) 95 self.song().view.add_selected_scene_listener(self._on_selected_scene_changed)
96 97 @property
98 - def components(self):
99 return tuple(filter(lambda comp: not comp.is_private, self._components))
100
101 - def enable_test_mode(self):
102 """ Acceptance tests should call this function before using the script, 103 to ensure an appropriate testing state """ 104 pass
105
106 - def _get_tasks(self):
107 return self._task_group
108 109 _tasks = property(_get_tasks) 110
111 - def application(self):
112 """ Returns a reference to the application that we are running in """ 113 return Live.Application.get_application()
114
115 - def song(self):
116 """ Returns a reference to the Live song instance that we control """ 117 return self._c_instance.song()
118
119 - def disconnect(self):
120 """ 121 Live -> Script: Called right before we get disconnected from Live 122 """ 123 with self.component_guard(): 124 for component in self._components: 125 component.disconnect() 126 127 for control in self.controls: 128 control.disconnect() 129 130 self._forwarding_registry = None 131 self.controls = None 132 self._components = None 133 self._displays = None 134 self._device_component = None 135 self._pad_translations = None 136 self.song().remove_visible_tracks_listener(self._on_track_list_changed) 137 self.song().remove_scenes_listener(self._on_scene_list_changed) 138 self.song().view.remove_selected_track_listener(self._on_selected_track_changed) 139 self.song().view.remove_selected_scene_listener(self._on_selected_scene_changed) 140 if isinstance(__builtins__, dict): 141 cs_list = __builtins__.get(CS_LIST_KEY, []) 142 else: 143 cs_list = getattr(__builtins__, CS_LIST_KEY, []) 144 if self in cs_list: 145 cs_list.remove(self) 146 self._task_group.clear() 147 super(ControlSurface, self).disconnect()
148
149 - def _control_surfaces(self):
150 """ Returns list of registered control surfaces """ 151 control_surfaces = [] 152 if isinstance(__builtins__, dict): 153 if CS_LIST_KEY in __builtins__.keys(): 154 control_surfaces = __builtins__[CS_LIST_KEY] 155 elif hasattr(__builtins__, CS_LIST_KEY): 156 control_surfaces = getattr(__builtins__, CS_LIST_KEY) 157 return control_surfaces
158
159 - def can_lock_to_devices(self):
160 """ 161 Live -> Script 162 """ 163 return self._device_component != None
164 165 @_scheduled_method
166 - def lock_to_device(self, device):
167 """ 168 Live -> Script 169 Live tells the script which device to control 170 """ 171 raise self._device_component != None or AssertionError 172 with self.component_guard(): 173 self._device_component.set_lock_to_device(True, device)
174 175 @_scheduled_method
176 - def unlock_from_device(self, device):
177 """ 178 Live -> Script 179 Live tells the script to unlock from a certain device 180 """ 181 raise self._device_component != None or AssertionError 182 with self.component_guard(): 183 self._device_component.set_lock_to_device(False, device)
184 185 @_scheduled_method
186 - def restore_bank(self, bank_index):
187 """ 188 Live -> Script 189 Live tells the script which bank to use. 190 """ 191 raise self._device_component != None or AssertionError 192 with self.component_guard(): 193 self._device_component.restore_bank(bank_index)
194 195 @_scheduled_method
196 - def set_appointed_device(self, device):
197 """ 198 Live -> Script 199 Live tells the script to unlock from a certain device 200 """ 201 with self.component_guard(): 202 self._device_component.set_device(device)
203
204 - def suggest_input_port(self):
205 """ Live -> Script: Live can ask for the name of the script's 206 prefered input port""" 207 return self._suggested_input_port
208
209 - def suggest_output_port(self):
210 """ Live -> Script: Live can ask for the name of the script's 211 prefered output port""" 212 return self._suggested_output_port
213
214 - def suggest_map_mode(self, cc_no, channel):
215 """ Live -> Script: Live can ask for a suitable mapping mode 216 for a given CC""" 217 raise in_range(cc_no, 0, 128) or AssertionError 218 raise in_range(channel, 0, 16) or AssertionError 219 suggested_map_mode = -1 220 for control in self.controls: 221 if isinstance(control, InputControlElement) and control.message_type() == MIDI_CC_TYPE and control.message_identifier() == cc_no and control.message_channel() == channel: 222 suggested_map_mode = control.message_map_mode() 223 break 224 225 return suggested_map_mode
226
227 - def suggest_needs_takeover(self, cc_no, channel):
228 """ Live -> Script: Live can ask whether a given CC needs takeover """ 229 raise in_range(cc_no, 0, 128) or AssertionError 230 raise in_range(channel, 0, 16) or AssertionError 231 needs_takeover = True 232 for control in self._controls: 233 if isinstance(control, InputControlElement) and control.message_type() == MIDI_CC_TYPE and control.message_identifier() == cc_no and control.message_channel() == channel: 234 needs_takeover = control.needs_takeover() 235 break 236 237 return needs_takeover
238
239 - def supports_pad_translation(self):
240 return self._pad_translations != None
241
242 - def set_highlighting_session_component(self, session_component):
243 raise self._highlighting_session_component is None or AssertionError, 'There must be one session component only' 244 self._highlighting_session_component = session_component 245 self._highlighting_session_component.set_highlighting_callback(self._set_session_highlight)
246
248 """ Return the session component showing the ring in Live session """ 249 return self._highlighting_session_component
250
251 - def show_message(self, message):
252 """ Displays the given message in Live's status bar """ 253 raise isinstance(message, (str, unicode)) or AssertionError 254 self._c_instance.show_message(message)
255
256 - def log_message(self, *message):
257 """ Writes the given message into Live's main log file """ 258 message = '(%s) %s' % (self.__class__.__name__, ' '.join(map(str, message))) 259 console_message = 'LOG: ' + message 260 if debug_print != None: 261 debug_print(console_message) 262 else: 263 print console_message 264 if self._c_instance: 265 self._c_instance.log_message(message)
266
267 - def instance_identifier(self):
268 return self._c_instance.instance_identifier()
269
270 - def connect_script_instances(self, instanciated_scripts):
271 """ Called by the Application as soon as all scripts are initialized. 272 You can connect yourself to other running scripts here, as we do it 273 connect the extension modules (MackieControlXTs). 274 """ 275 pass
276
277 - def request_rebuild_midi_map(self):
278 """ Script -> Live. 279 When the internal MIDI controller has changed in a way that 280 you need to rebuild the MIDI mappings, request a rebuild 281 by calling this function This is processed as a request, 282 to be sure that its not too often called, because its 283 time-critical. 284 """ 285 if not not self._in_build_midi_map: 286 raise AssertionError 287 self._suppress_requests_counter > 0 and self._rebuild_requests_during_suppression += 1 288 else: 289 self._c_instance.request_rebuild_midi_map()
290
291 - def build_midi_map(self, midi_map_handle):
292 """ Live -> Script 293 Build DeviceParameter Mappings, that are processed in Audio time, or 294 forward MIDI messages explicitly to our receive_midi_functions. 295 Which means that when you are not forwarding MIDI, nor mapping parameters, 296 you will never get any MIDI messages at all. 297 """ 298 with self._in_build_midi_map(): 299 self._forwarding_registry.clear() 300 self._forwarding_long_identifier_registry.clear() 301 for control in self.controls: 302 if isinstance(control, InputControlElement): 303 control.install_connections(self._translate_message, partial(self._install_mapping, midi_map_handle), partial(self._install_forwarding, midi_map_handle)) 304 305 if self._pad_translations != None: 306 self._c_instance.set_pad_translation(self._pad_translations)
307
308 - def toggle_lock(self):
309 """ Script -> Live 310 Use this function to toggle the script's lock on devices 311 """ 312 self._c_instance.toggle_lock()
313
314 - def refresh_state(self):
315 """ Live -> Script 316 Send out MIDI to completely update the attached MIDI controller. 317 Will be called when requested by the user, after for example having reconnected 318 the MIDI cables... 319 """ 320 self.update()
321
322 - def update(self):
323 with self.component_guard(): 324 for control in self.controls: 325 control.clear_send_cache() 326 327 for component in self._components: 328 component.update()
329
330 - def update_display(self):
331 """ Live -> Script 332 Aka on_timer. Called every 100 ms and should be used to update display relevant 333 parts of the controller 334 """ 335 with self.component_guard(): 336 with self._is_sending_scheduled_messages(): 337 self._task_group.update(Defaults.TIMER_DELAY)
338
339 - def receive_midi(self, midi_bytes):
340 """ Live -> Script 341 MIDI messages are only received through this function, when explicitly 342 forwarded in 'build_midi_map'. 343 """ 344 with self.component_guard(): 345 self._do_receive_midi(midi_bytes)
346
347 - def is_sysex_message(self, midi_bytes):
348 return len(midi_bytes) != 3
349
350 - def _do_receive_midi(self, midi_bytes):
351 if not self.is_sysex_message(midi_bytes): 352 self.handle_nonsysex(midi_bytes) 353 else: 354 self.handle_sysex(midi_bytes)
355
356 - def handle_nonsysex(self, midi_bytes):
357 is_pitchbend = midi_bytes[0] & 240 == MIDI_PB_STATUS 358 forwarding_key = midi_bytes[:1 if is_pitchbend else 2] 359 value = midi_bytes[1] + (midi_bytes[2] << 7) if is_pitchbend else midi_bytes[2] 360 if forwarding_key in self._forwarding_registry: 361 recipient = self._forwarding_registry[forwarding_key] 362 if recipient != None: 363 recipient.receive_value(value) 364 else: 365 self.log_message('Got unknown message: ' + str(midi_bytes))
366
367 - def handle_sysex(self, midi_bytes):
368 result = find_if(lambda (id, _): midi_bytes[:len(id)] == id, self._forwarding_long_identifier_registry.iteritems()) 369 if result != None: 370 id, control = result 371 control.receive_value(midi_bytes[len(id):-1]) 372 else: 373 self.log_message('Got unknown sysex message: ', midi_bytes)
374
375 - def set_device_component(self, device_component):
376 raise self._device_component == None or AssertionError 377 raise device_component != None or AssertionError 378 raise isinstance(device_component, DeviceComponent) or AssertionError 379 self._device_component = device_component 380 self._device_component.set_lock_callback(self._toggle_lock)
381 382 @contextmanager
384 """ 385 Delays requesting a MIDI map rebuild, if any, until the scope 386 of the context manager is exited. 387 """ 388 try: 389 self._set_suppress_rebuild_requests(True) 390 yield 391 finally: 392 self._set_suppress_rebuild_requests(False)
393
394 - def _set_suppress_rebuild_requests(self, suppress_requests):
395 if not not self._in_build_midi_map: 396 raise AssertionError 397 suppress_requests and self._suppress_requests_counter += 1 398 elif not self._suppress_requests_counter > 0: 399 raise AssertionError 400 self._suppress_requests_counter -= 1 401 self._suppress_requests_counter == 0 and self._rebuild_requests_during_suppression > 0 and self.request_rebuild_midi_map() 402 self._rebuild_requests_during_suppression = 0
403
404 - def set_pad_translations(self, pad_translations):
405 raise self._pad_translations == None or AssertionError 406 raise len(pad_translations) <= 16 or AssertionError 407 408 def check_translation(translation): 409 raise len(translation) == 4 or AssertionError 410 raise in_range(translation[0], 0, 4) or AssertionError 411 raise in_range(translation[1], 0, 4) or AssertionError 412 raise in_range(translation[2], 0, 128) or AssertionError 413 raise in_range(translation[3], 0, 16) or AssertionError 414 return True
415 416 raise all(map(check_translation, pad_translations)) or AssertionError 417 self._pad_translations = pad_translations
418
419 - def set_enabled(self, enable):
420 bool_enable = bool(enable) 421 if self._enabled != bool_enable: 422 with self.component_guard(): 423 self._enabled = bool_enable 424 for component in self._components: 425 component._set_enabled_recursive(bool_enable)
426
427 - def schedule_message(self, delay_in_ticks, callback, parameter = None):
428 """ Schedule a callback to be called after a specified time """ 429 if not delay_in_ticks > 0: 430 raise AssertionError 431 if not callable(callback): 432 raise AssertionError 433 self._is_sending_scheduled_messages or delay_in_ticks -= 1 434 message_reference = [None] 435 436 def message(delta): 437 if parameter: 438 callback(parameter) 439 else: 440 callback() 441 self._remaining_scheduled_messages.remove(message_reference)
442 443 message_reference[0] = message 444 self._remaining_scheduled_messages.append(message_reference) 445 delay_in_ticks and self._task_group.add(Task.sequence(Task.delay(delay_in_ticks), message)) 446 else: 447 self._task_group.add(message) 448
449 - def _process_remaining_scheduled_messages(self):
450 current_scheduled_messages = tuple(self._remaining_scheduled_messages) 451 for message, in current_scheduled_messages: 452 message(None)
453
454 - def set_feedback_channels(self, channels):
455 self._c_instance.set_feedback_channels(channels)
456
457 - def set_controlled_track(self, track):
458 """ Sets the track that will send its feedback to the control surface """ 459 raise track == None or isinstance(track, Live.Track.Track) or AssertionError 460 self._c_instance.set_controlled_track(track)
461
462 - def _register_control(self, control):
463 """ puts control into the list of controls for triggering updates """ 464 if not control != None: 465 raise AssertionError 466 raise control not in self.controls or AssertionError, 'Control registered twice' 467 self.controls.append(control) 468 control.canonical_parent = self 469 isinstance(control, PhysicalDisplayElement) and self._displays.append(control)
470
471 - def _register_component(self, component):
472 """ puts component into the list of controls for triggering updates """ 473 raise component != None or AssertionError 474 raise component not in self._components or AssertionError, 'Component registered twice' 475 self._components.append(component) 476 component.canonical_parent = self
477 478 @contextmanager
479 - def component_guard(self):
480 """ 481 Context manager that guards user code. This prevents 482 unnecesary updating and enables several optimisations. Should 483 be used to guard calls to components or control elements. 484 """ 485 if not self._in_component_guard: 486 with self._in_component_guard(): 487 with self.setting_listener_caller(): 488 with self._control_surface_injector: 489 with self.suppressing_rebuild_requests(): 490 with self.accumulating_midi_messages(): 491 yield 492 else: 493 yield
494 495 @property
496 - def in_component_guard(self):
497 return bool(self._in_component_guard)
498 499 @contextmanager
500 - def setting_listener_caller(self):
501 try: 502 self._c_instance.set_listener_caller(self._call_guarded_listener) 503 yield 504 finally: 505 self._c_instance.set_listener_caller(None)
506
507 - def _call_guarded_listener(self, listener):
508 if _ModuleLoadedCheck == None or self._c_instance == None: 509 self.log_message('Disconnecting leaked listener at:', listener.name) 510 listener.disconnect() 511 else: 512 try: 513 with self.component_guard(): 514 listener() 515 except: 516 self.log_message('Detected broken listener at:', listener.name) 517 raise
518 519 @contextmanager
520 - def accumulating_midi_messages(self):
521 with self._accumulate_midi_messages(): 522 try: 523 yield 524 finally: 525 self._flush_midi_messages()
526
527 - def _send_midi(self, midi_event_bytes, optimized = True):
528 """ 529 Script -> Live 530 Use this function to send MIDI events through Live to the 531 _real_ MIDI devices that this script is assigned to. 532 533 When optimized=True it is assumed that messages can be 534 dropped -- only the last message within an update for a 535 given (channel, key) has visible effects. 536 """ 537 if self._accumulate_midi_messages: 538 sysex_status_byte = 240 539 entry = (self._midi_message_count, midi_event_bytes) 540 if optimized and midi_event_bytes[0] != sysex_status_byte: 541 self._midi_message_dict[midi_event_bytes[0], midi_event_bytes[1]] = entry 542 else: 543 self._midi_message_list.append(entry) 544 self._midi_message_count += 1 545 else: 546 self._do_send_midi(midi_event_bytes) 547 return True
548
549 - def _flush_midi_messages(self):
550 raise self._accumulate_midi_messages or AssertionError 551 for _, message in sorted(chain(self._midi_message_list, self._midi_message_dict.itervalues()), key=first): 552 self._do_send_midi(message) 553 554 self._midi_message_dict.clear() 555 self._midi_message_list[:] = [] 556 self._midi_message_count = 0
557
558 - def _do_send_midi(self, midi_event_bytes):
559 self._c_instance.send_midi(midi_event_bytes) 560 return True
561
562 - def _install_mapping(self, midi_map_handle, control, parameter, feedback_delay, feedback_map):
563 if not self._in_build_midi_map: 564 raise AssertionError 565 raise control != None and parameter != None or AssertionError 566 raise isinstance(parameter, Live.DeviceParameter.DeviceParameter) or AssertionError 567 raise isinstance(control, InputControlElement) or AssertionError 568 raise isinstance(feedback_delay, int) or AssertionError 569 if not isinstance(feedback_map, tuple): 570 raise AssertionError 571 success = False 572 feedback_rule = None 573 feedback_rule = control.message_type() is MIDI_NOTE_TYPE and Live.MidiMap.NoteFeedbackRule() 574 feedback_rule.note_no = control.message_identifier() 575 feedback_rule.vel_map = feedback_map 576 elif control.message_type() is MIDI_CC_TYPE: 577 feedback_rule = Live.MidiMap.CCFeedbackRule() 578 feedback_rule.cc_no = control.message_identifier() 579 feedback_rule.cc_value_map = feedback_map 580 elif control.message_type() is MIDI_PB_TYPE: 581 feedback_rule = Live.MidiMap.PitchBendFeedbackRule() 582 feedback_rule.value_pair_map = feedback_map 583 if not feedback_rule != None: 584 raise AssertionError 585 feedback_rule.channel = control.message_channel() 586 feedback_rule.delay_in_ms = feedback_delay 587 success = control.message_type() is MIDI_NOTE_TYPE and Live.MidiMap.map_midi_note_with_feedback_map(midi_map_handle, parameter, control.message_channel(), control.message_identifier(), feedback_rule) 588 elif control.message_type() is MIDI_CC_TYPE: 589 success = Live.MidiMap.map_midi_cc_with_feedback_map(midi_map_handle, parameter, control.message_channel(), control.message_identifier(), control.message_map_mode(), feedback_rule, not control.needs_takeover(), control.mapping_sensitivity) 590 elif control.message_type() is MIDI_PB_TYPE: 591 success = Live.MidiMap.map_midi_pitchbend_with_feedback_map(midi_map_handle, parameter, control.message_channel(), feedback_rule, not control.needs_takeover()) 592 success and Live.MidiMap.send_feedback_for_parameter(midi_map_handle, parameter) 593 return success
594
595 - def _install_forwarding(self, midi_map_handle, control):
596 if not self._in_build_midi_map: 597 raise AssertionError 598 raise control != None or AssertionError 599 if not isinstance(control, InputControlElement): 600 raise AssertionError 601 success = False 602 success = control.message_type() is MIDI_NOTE_TYPE and Live.MidiMap.forward_midi_note(self._c_instance.handle(), midi_map_handle, control.message_channel(), control.message_identifier()) 603 elif control.message_type() is MIDI_CC_TYPE: 604 success = Live.MidiMap.forward_midi_cc(self._c_instance.handle(), midi_map_handle, control.message_channel(), control.message_identifier()) 605 elif control.message_type() is MIDI_PB_TYPE: 606 success = Live.MidiMap.forward_midi_pitchbend(self._c_instance.handle(), midi_map_handle, control.message_channel()) 607 else: 608 raise control.message_type() == MIDI_SYSEX_TYPE or AssertionError 609 success = True 610 forwarding_keys = success and control.identifier_bytes() 611 for key in forwarding_keys: 612 registry = self._forwarding_registry if control.message_type() != MIDI_SYSEX_TYPE else self._forwarding_long_identifier_registry 613 raise key not in registry.keys() or AssertionError, 'Registry key %s registered twice. Check Midi messages!' % str(key) 614 registry[key] = control 615 616 return success
617
618 - def _translate_message(self, type, from_identifier, from_channel, to_identifier, to_channel):
619 if not type in (MIDI_CC_TYPE, MIDI_NOTE_TYPE): 620 raise AssertionError 621 raise from_identifier in range(128) or AssertionError 622 raise from_channel in range(16) or AssertionError 623 raise to_identifier in range(128) or AssertionError 624 raise to_channel in range(16) or AssertionError 625 type == MIDI_CC_TYPE and self._c_instance.set_cc_translation(from_identifier, from_channel, to_identifier, to_channel) 626 elif type == MIDI_NOTE_TYPE: 627 self._c_instance.set_note_translation(from_identifier, from_channel, to_identifier, to_channel) 628 else: 629 raise False or AssertionError
630
631 - def _set_session_highlight(self, track_offset, scene_offset, width, height, include_return_tracks):
632 raise list((track_offset, 633 scene_offset, 634 width, 635 height)).count(-1) != 4 and (width > 0 or AssertionError) 636 if not height > 0: 637 raise AssertionError 638 self._c_instance.set_session_highlight(track_offset, scene_offset, width, height, include_return_tracks)
639
640 - def _on_track_list_changed(self):
641 for component in self._components: 642 component.on_track_list_changed() 643 644 self.schedule_message(1, self._on_selected_track_changed)
645
646 - def _on_scene_list_changed(self):
647 for component in self._components: 648 component.on_scene_list_changed()
649
650 - def _on_selected_track_changed(self):
651 for component in self._components: 652 component.on_selected_track_changed() 653 654 if self._device_selection_follows_track_selection: 655 self._update_device_selection()
656
657 - def _on_selected_scene_changed(self):
658 for component in self._components: 659 component.on_selected_scene_changed()
660
661 - def _toggle_lock(self):
662 raise self._device_component != None or AssertionError 663 self._c_instance.toggle_lock()
664
665 - def _refresh_displays(self):
666 """ 667 Make sure the displays of the control surface display current 668 data. 669 """ 670 for display in self._displays: 671 display.update() 672 display._tasks.update(Defaults.TIMER_DELAY)
673
674 - def _update_device_selection(self):
675 track = self.song().view.selected_track 676 device_to_select = track.view.selected_device 677 if device_to_select == None and len(track.devices) > 0: 678 device_to_select = track.devices[0] 679 if device_to_select != None: 680 self.song().view.select_device(device_to_select) 681 self._device_component.set_device(self.song().appointed_device) 682 else: 683 self._device_component.set_device(None)
684