예제 #1
0
class ConvertComponent(Component):
    __events__ = (u'cancel', u'success')
    action_buttons = control_list(ButtonControl,
                                  color='Option.Unselected',
                                  pressed_color='Option.Selected')
    cancel_button = ButtonControl(color='Option.Unselected',
                                  pressed_color='Option.Selected')
    source_color_index = listenable_property.managed(UNCOLORED_INDEX)
    source_name = listenable_property.managed(unicode(''))

    def __init__(self,
                 tracks_provider=None,
                 conversions_provider=possible_conversions,
                 decorator_factory=None,
                 *a,
                 **k):
        assert tracks_provider is not None
        assert callable(conversions_provider)
        super(ConvertComponent, self).__init__(*a, **k)
        self._tracks_provider = tracks_provider
        self._conversions_provider = conversions_provider
        self._decorator_factory = decorator_factory
        self._category = NullConvertCategory()
        self._update_possible_conversions()
        return

    @listenable_property
    def available_conversions(self):
        return map(lambda x: x.label, self._category.actions)

    def on_enabled_changed(self):
        super(ConvertComponent, self).on_enabled_changed()
        self._update_possible_conversions()

    def _update_possible_conversions(self):
        self.disconnect_disconnectable(self._category)
        track = self._tracks_provider.selected_item
        self._category = self.register_disconnectable(
            self._conversions_provider(track, self._decorator_factory))
        self.__on_action_invalidated.subject = self._category
        self.__on_action_source_color_index_changed.subject = self._category.color_source
        self.__on_action_source_name_changed.subject = self._category.name_source
        self.__on_action_source_color_index_changed()
        self.__on_action_source_name_changed()
        self.action_buttons.control_count = len(self._category.actions)
        self.notify_available_conversions()

    @listens('color_index')
    def __on_action_source_color_index_changed(self):
        color_source = self.__on_action_source_color_index_changed.subject
        self.source_color_index = color_source.color_index if color_source and color_source.color_index is not None else UNCOLORED_INDEX
        return

    @listens('name')
    def __on_action_source_name_changed(self):
        name_source = self.__on_action_source_name_changed.subject
        self.source_name = name_source.name if name_source else unicode()

    @action_buttons.released
    def action_buttons(self, button):
        if self._do_conversion(button.index):
            self.notify_cancel()

    def _do_conversion(self, action_index):
        self._update_possible_conversions()
        if action_index < len(self._category.actions):
            action = self._category.actions[action_index]
            if action.needs_deferred_invocation:
                self._tasks.add(
                    task.sequence(
                        task.delay(1),
                        task.run(
                            lambda: self._do_conversion_deferred(action))))
                return False
            self._invoke_conversion(action)
        return True

    def _do_conversion_deferred(self, action):
        self._invoke_conversion(action)
        self.notify_cancel()

    def _invoke_conversion(self, action):
        self._category.convert(self.song, action)
        self.notify_success(self._category.internal_name)

    @cancel_button.released
    def cancel_button(self, button):
        self.notify_cancel()

    @listens('action_invalidated')
    def __on_action_invalidated(self):
        self.notify_cancel()
예제 #2
0
class StepDuplicatorComponent(Component, Messenger):
    button = ButtonControl()

    def __init__(self, *a, **k):
        super(StepDuplicatorComponent, self).__init__(*a, **k)
        self._clip = None
        self._source_step = None
        self._notification_reference = partial(nop, None)
        return

    @property
    def is_duplicating(self):
        return self.button.is_pressed and liveobj_valid(self._clip)

    def set_clip(self, clip):
        self._cancel_duplicate()
        self._clip = clip

    def add_step_with_pitch(self, note, step_start, step_end, nudge_offset=0, is_page=False):
        if self.is_enabled() and self.is_duplicating:
            current_step = (note,
             step_start,
             step_end - step_start,
             nudge_offset,
             is_page)
            if self._source_step is not None:
                self._duplicate_to(current_step)
            else:
                self._duplicate_from(current_step)
        return

    def add_step(self, step_start, step_end, nudge_offset=0, is_page=False):
        self.add_step_with_pitch(ALL_NOTES, step_start, step_end, nudge_offset, is_page)

    def _duplicate_from(self, source_step):
        message = MessageBoxText.CANNOT_COPY_EMPTY_PAGE if source_step[4] else MessageBoxText.CANNOT_COPY_EMPTY_STEP
        from_pitch = source_step[0]
        pitch_span = 1
        if from_pitch == ALL_NOTES:
            from_pitch = 0
            pitch_span = 127
        notes = self._clip.get_notes(source_step[1], from_pitch, source_step[2], pitch_span)
        if len(notes) > 0:
            message = MessageBoxText.COPIED_PAGE if source_step[4] else MessageBoxText.COPIED_STEP
            self._source_step = source_step
        self._notification_reference = self.show_notification(message)

    def _duplicate_to(self, destination_step):
        if self._source_step[4] == destination_step[4]:
            message = MessageBoxText.CANNOT_PASTE_TO_SOURCE_PAGE if destination_step[4] else MessageBoxText.CANNOT_PASTE_TO_SOURCE_STEP
            if destination_step != self._source_step:
                message = MessageBoxText.PASTED_PAGE if destination_step[4] else MessageBoxText.PASTED_STEP
                self._clip.duplicate_region(self._source_step[1], self._source_step[2], destination_step[1] + self._source_step[3], self._source_step[0], get_transposition_amount(self._source_step, destination_step))
        else:
            message = MessageBoxText.CANNOT_PASTE_FROM_STEP_TO_PAGE if destination_step[4] else MessageBoxText.CANNOT_PASTE_FROM_PAGE_TO_STEP
        loop_start = destination_step[1]
        loop_end = loop_start + self._source_step[2]
        if destination_step[4] and not (loop_start >= self._clip.loop_start and loop_end <= self._clip.loop_end):
            set_loop(self._clip, loop_start, loop_end)
        self._notification_reference = self.show_notification(message)
        self._source_step = None
        return

    def _cancel_duplicate(self):
        self._source_step = None
        if self._notification_reference() is not None:
            self._notification_reference().hide()
        return

    @button.released
    def button(self, _):
        self._cancel_duplicate()

    def update(self):
        super(StepDuplicatorComponent, self).update()
        self._cancel_duplicate()
class LoopSelectorComponent(Component, Messenger):
    """
    Component that uses a button matrix to display the timeline of a
    clip. It allows you to select the loop of the clip and a page
    within it of a given Paginator object.
    """
    next_page_button = ButtonControl()
    prev_page_button = ButtonControl()
    delete_button = ButtonControl()
    select_button = ButtonControl()
    loop_selector_matrix = control_matrix(PadControl,
                                          sensitivity_profile=b'loop',
                                          mode=PlayableControl.Mode.listenable)
    short_loop_selector_matrix = control_matrix(ButtonControl)
    is_following = listenable_property.managed(False)

    def __init__(self,
                 clip_creator=None,
                 measure_length=4.0,
                 follow_detail_clip=False,
                 paginator=None,
                 default_size=None,
                 *a,
                 **k):
        super(LoopSelectorComponent, self).__init__(*a, **k)
        assert default_size is not None
        self._clip_creator = clip_creator
        self._sequencer_clip = None
        self._paginator = Paginator()
        self._loop_start = 0
        self._loop_end = 0
        self._loop_length = 0
        self._default_size = default_size
        self._pressed_pages = []
        self._page_colors = []
        self._measure_length = measure_length
        self._last_playhead_page = -1

        def set_is_following_true():
            self.is_following = True

        self._follow_task = self._tasks.add(
            task.sequence(task.wait(defaults.MOMENTARY_DELAY),
                          task.run(set_is_following_true)))
        self._follow_task.kill()
        self.set_step_duplicator(None)
        self._notification_reference = partial(nop, None)
        self.is_deleting = False
        if follow_detail_clip:
            self._on_detail_clip_changed.subject = self.song.view
            self._on_detail_clip_changed()
        self._on_session_record_changed.subject = self.song
        self._on_song_playback_status_changed.subject = self.song
        if paginator is not None:
            self.set_paginator(paginator)
        return

    def set_paginator(self, paginator):
        self._paginator = paginator or Paginator()
        self._on_page_index_changed.subject = paginator
        self._on_page_length_changed.subject = paginator
        self._update_page_colors()

    @listens(b'page_index')
    def _on_page_index_changed(self):
        self._update_page_colors()

    @listens(b'page_length')
    def _on_page_length_changed(self):
        self._update_page_colors()
        self._select_start_page()

    def set_step_duplicator(self, duplicator):
        self._step_duplicator = duplicator or NullStepDuplicator()
        self._step_duplicator.set_clip(self._sequencer_clip)

    @listens(b'detail_clip')
    def _on_detail_clip_changed(self):
        self.set_detail_clip(self.song.view.detail_clip)

    def set_detail_clip(self, clip):
        if liveobj_changed(clip, self._sequencer_clip):
            self.is_following = liveobj_valid(clip) and (
                self.is_following or clip_is_new_recording(clip))
            self._on_playing_position_changed.subject = clip
            self._on_playing_status_changed.subject = clip
            self._on_loop_start_changed.subject = clip
            self._on_loop_end_changed.subject = clip
            self._on_is_recording_changed.subject = clip
            self._sequencer_clip = clip
            self._step_duplicator.set_clip(clip)
            self._on_loop_changed()

    def _select_start_page(self):
        if liveobj_valid(self._sequencer_clip):
            page_start = self._paginator.page_index * self._paginator.page_length
            to_select = page_start
            if page_start <= self._sequencer_clip.loop_start:
                to_select = self._sequencer_clip.loop_start
            elif page_start >= self._sequencer_clip.loop_end:
                to_select = max(
                    self._sequencer_clip.loop_end -
                    self._paginator.page_length,
                    self._sequencer_clip.loop_start)
            self._paginator.select_page_in_point(to_select)

    @listens(b'loop_start')
    def _on_loop_start_changed(self):
        self._on_loop_changed()

    @listens(b'loop_end')
    def _on_loop_end_changed(self):
        self._on_loop_changed()

    def _on_loop_changed(self):
        if liveobj_valid(self._sequencer_clip):
            self._loop_start = self._sequencer_clip.loop_start
            self._loop_end = self._sequencer_clip.loop_end
            self._loop_length = self._loop_end - self._loop_start
        else:
            self._loop_start = 0
            self._loop_end = 0
            self._loop_length = 0
        self._select_start_page()
        self._update_page_colors()

    def set_loop_selector_matrix(self, matrix):
        self.loop_selector_matrix.set_control_element(matrix)
        self._update_page_colors()

    def set_short_loop_selector_matrix(self, matrix):
        self.short_loop_selector_matrix.set_control_element(matrix)
        self._update_page_colors()

    def update(self):
        super(LoopSelectorComponent, self).update()
        self._update_page_and_playhead_leds()

    @listens(b'is_recording')
    def _on_is_recording_changed(self):
        self.is_following = self.is_following or clip_is_new_recording(
            self._sequencer_clip)

    @listens(b'playing_position')
    def _on_playing_position_changed(self):
        self._update_page_and_playhead_leds()
        self._update_page_selection()

    @listens(b'playing_status')
    def _on_playing_status_changed(self):
        self._update_page_and_playhead_leds()

    @listens(b'session_record')
    def _on_session_record_changed(self):
        self._update_page_and_playhead_leds()

    @listens(b'is_playing')
    def _on_song_playback_status_changed(self):
        self._update_page_and_playhead_leds()

    def _has_running_clip(self):
        return liveobj_valid(
            self._sequencer_clip) and (self._sequencer_clip.is_playing
                                       or self._sequencer_clip.is_recording)

    def _update_page_selection(self):
        if self.is_enabled() and self.is_following and self._has_running_clip(
        ):
            position = self._sequencer_clip.playing_position
            self._paginator.select_page_in_point(position)

    def _update_page_and_playhead_leds(self):
        @contextmanager
        def save_page_color(page_colors, page):
            old_page_value = page_colors[page]
            yield
            page_colors[page] = old_page_value

        @contextmanager
        def replace_and_restore_tail_colors(page_colors, page):
            if clip_is_new_recording(self._sequencer_clip):
                old_tail_values = page_colors[page + 1:]
                page_colors[(
                    page +
                    1):] = [b'LoopSelector.OutsideLoop'] * len(old_tail_values)
            yield
            if clip_is_new_recording(self._sequencer_clip):
                page_colors[(page + 1):] = old_tail_values

        if self.is_enabled() and self._has_running_clip():
            position = self._sequencer_clip.playing_position
            visible_page = int(
                position / self._page_length_in_beats) - self.page_offset
            page_colors = self._page_colors
            if 0 <= visible_page < len(page_colors):
                with save_page_color(page_colors, visible_page):
                    if self.song.is_playing:
                        page_colors[
                            visible_page] = b'LoopSelector.PlayheadRecord' if self.song.session_record else b'LoopSelector.Playhead'
                    with replace_and_restore_tail_colors(
                            page_colors, visible_page):
                        self._update_page_leds()
            else:
                self._update_page_leds()
            self._last_playhead_page = visible_page
        else:
            self._update_page_leds()

    def _get_size(self):
        return max(self.loop_selector_matrix.control_count,
                   self.short_loop_selector_matrix.control_count,
                   self._default_size)

    def _get_loop_in_pages(self):
        page_length = self._page_length_in_beats
        loop_start = int(self._loop_start / page_length)
        loop_end = int(self._loop_end / page_length)
        loop_length = loop_end - loop_start + int(
            self._loop_end % page_length != 0)
        return (loop_start, loop_length)

    def _selected_pages_range(self):
        size = self._get_size()
        page_length = self._page_length_in_beats
        seq_page_length = max(self._paginator.page_length / page_length, 1)
        seq_page_start = int(self._paginator.page_index *
                             self._paginator.page_length / page_length)
        seq_page_end = int(
            min(seq_page_start + seq_page_length, self.page_offset + size))
        return (seq_page_start, seq_page_end)

    def _update_page_colors(self):
        """
        Update the offline array mapping the timeline of the clip to buttons.
        """
        page_length = self._page_length_in_beats
        size = self._get_size()

        def calculate_page_colors():
            l_start, l_length = self._get_loop_in_pages()
            page_offset = self.page_offset
            pages_per_measure = int(self._one_measure_in_beats / page_length)

            def color_for_page(absolute_page):
                if l_start <= absolute_page < l_start + l_length:
                    if absolute_page % pages_per_measure == 0:
                        return b'LoopSelector.InsideLoopStartBar'
                    return b'LoopSelector.InsideLoop'
                return b'LoopSelector.OutsideLoop'

            return map(color_for_page, xrange(page_offset, page_offset + size))

        def mark_selected_pages(page_colors):
            for page_index in xrange(*self._selected_pages_range()):
                button_index = page_index - self.page_offset
                if page_colors[button_index].startswith(
                        b'LoopSelector.InsideLoop'):
                    page_colors[button_index] = b'LoopSelector.SelectedPage'

        page_colors = calculate_page_colors()
        mark_selected_pages(page_colors)
        self._page_colors = page_colors
        self._update_page_and_playhead_leds()

    def _update_page_leds(self):
        self._update_page_leds_in_matrix(self.loop_selector_matrix)
        self._update_page_leds_in_matrix(self.short_loop_selector_matrix)

    def _update_page_leds_in_matrix(self, matrix):
        """ update hardware leds to match precomputed map """
        if self.is_enabled() and matrix:
            for button, color in izip(matrix, self._page_colors):
                button.color = color

    def _jump_to_page(self, next_page):
        start, length = self._get_loop_in_pages()
        if next_page >= start + length:
            next_page = start
        elif next_page < start:
            next_page = start + length - 1
        self._paginator.select_page_in_point(next_page *
                                             self._page_length_in_beats)

    @next_page_button.pressed
    def next_page_button(self, button):
        if self.is_following:
            self.is_following = False
        else:
            _, end = self._selected_pages_range()
            self._jump_to_page(end)
            self._follow_task.restart()

    @next_page_button.released
    def next_page_button(self, button):
        self._follow_task.kill()

    @prev_page_button.pressed
    def prev_page_button(self, button):
        if self.is_following:
            self.is_following = False
        else:
            begin, end = self._selected_pages_range()
            self._jump_to_page(begin - (end - begin))
            self._follow_task.restart()

    @prev_page_button.released
    def prev_page_button(self, button):
        self._follow_task.kill()

    @short_loop_selector_matrix.pressed
    def short_loop_selector_matrix(self, button):
        if self.is_enabled():
            page = self._get_corresponding_page(
                button, self.short_loop_selector_matrix)
            self._pressed_pages = [page]
            self._try_set_loop()
            self._pressed_pages = []

    @loop_selector_matrix.pressed
    def loop_selector_matrix(self, button):
        if self.is_enabled():
            page = self._get_corresponding_page(button,
                                                self.loop_selector_matrix)
            if page not in self._pressed_pages:
                self._on_press_loop_selector_matrix(page)

    @loop_selector_matrix.released
    def loop_selector_matrix(self, button):
        page = self._get_corresponding_page(button, self.loop_selector_matrix)
        if page in self._pressed_pages:
            self._pressed_pages.remove(page)

    def _get_corresponding_page(self, button, matrix):
        y, x = button.coordinate
        return x + y * matrix.width

    def _quantize_page_index(self, page_index, quant):
        page_length = self._page_length_in_beats
        return quant * float(int(page_length * page_index / quant))

    def _clear_page(self, page):
        page_start, page_end = self._selected_pages_time_range(page)
        notes = self._sequencer_clip.get_notes(page_start, 0, page_end, 128)
        if len(notes) > 0:
            self._sequencer_clip.remove_notes(page_start, 0,
                                              page_end - page_start, 128)
            self._notification_reference = self.show_notification(
                MessageBoxText.PAGE_CLEARED)
        else:
            self._notification_reference = self.show_notification(
                MessageBoxText.CANNOT_CLEAR_EMPTY_PAGE)

    def _selected_pages_time_range(self, page):
        page_start = 0
        page_end = 0
        page_length = self._page_length_in_beats
        if self._loop_length > page_length:
            range_start, range_end = self._selected_pages_range()
            page_start = range_start * page_length
            page_end = range_end * page_length
        else:
            page_start = page * page_length
            page_end = page_start + page_length
        return (page_start, page_end)

    def _add_page_to_duplicator(self, page):
        page_start, page_end = self._selected_pages_time_range(page)
        self._step_duplicator.add_step(page_start,
                                       page_end,
                                       nudge_offset=0,
                                       is_page=True)

    def _on_press_loop_selector_matrix(self, page):
        def create_clip(pages):
            measure = self._one_measure_in_beats
            length = self._quantize_page_index(pages, measure) + measure
            create_clip_in_selected_slot(self._clip_creator, self.song, length)

        def handle_page_press_on_clip(page):
            l_start, l_length = self._get_loop_in_pages()
            page_in_loop = l_start <= page < l_start + l_length
            buttons_pressed = len(self._pressed_pages)
            if buttons_pressed == 1 and page_in_loop:
                self._try_select_page(page)
            elif buttons_pressed > 1 or not page_in_loop:
                self._try_set_loop()
            if self._step_duplicator.is_duplicating:
                self._add_page_to_duplicator(page)
            if self.delete_button.is_pressed:
                self._clear_page(page)

        self._pressed_pages.append(page)
        absolute_page = page + self.page_offset
        if not self.select_button.is_pressed:
            if not liveobj_valid(
                    self._sequencer_clip
            ) and not self.song.view.highlighted_clip_slot.has_clip:
                create_clip(absolute_page)
            elif liveobj_valid(self._sequencer_clip):
                handle_page_press_on_clip(absolute_page)
        elif not self.is_following:
            self._try_select_page(absolute_page)

    def _try_select_page(self, page):
        step_time = page * self._page_length_in_beats
        if self._paginator.select_page_in_point(step_time):
            self.is_following = False
            return True
        return False

    def _try_set_loop(self):
        did_set_loop = False
        if liveobj_valid(self._sequencer_clip):
            if not clip_is_new_recording(self._sequencer_clip):
                lowest_page = min(self._pressed_pages) + self.page_offset
                if self._try_select_page(lowest_page):
                    self._set_loop_in_live()
                    did_set_loop = True
            if did_set_loop:
                self.is_following = True
        return did_set_loop

    def _set_loop_in_live(self):
        quant = self._page_length_in_beats
        start_page = min(self._pressed_pages) + self.page_offset
        end_page = max(self._pressed_pages) + self.page_offset
        loop_start = self._quantize_page_index(start_page, quant)
        loop_end = self._quantize_page_index(end_page, quant) + quant
        set_loop(self._sequencer_clip, loop_start, loop_end)
        self._sequencer_clip.view.show_loop()

    @property
    def _page_length_in_beats(self):
        return clamp(self._paginator.page_length, 0.25,
                     self._one_measure_in_beats)

    @property
    def _one_measure_in_beats(self):
        return self._measure_length * self.song.signature_numerator / self.song.signature_denominator

    @property
    def page_offset(self):
        def zero_if_none(n):
            if n is None:
                return 0
            else:
                return n

        width = zero_if_none(self.loop_selector_matrix.width)
        height = zero_if_none(self.loop_selector_matrix.height)
        size = max(width * height, 1)
        page_index = self._paginator.page_index
        page_length = self._paginator.page_length
        selected_page_index = int(page_index * page_length /
                                  self._page_length_in_beats)
        return size * int(selected_page_index / size)
class SessionRecordingComponent(SessionRecordingComponentBase):
    record_stop_button = ButtonControl()

    @record_stop_button.pressed
    def record_stop_button(self, _):
        self.song.session_record = False
class TrackMixerControlComponent(Component):
    __events__ = (u'parameters', u'scroll_offset', u'items')
    BUTTON_SKIN = dict(color='TrackControlView.ButtonOff',
                       pressed_color='TrackControlView.ButtonOn',
                       disabled_color='TrackControlView.ButtonDisabled')
    controls = control_list(MappedSensitivitySettingControl)
    scroll_right_button = ButtonControl(**BUTTON_SKIN)
    scroll_left_button = ButtonControl(**BUTTON_SKIN)

    @depends(tracks_provider=None,
             real_time_mapper=None,
             register_real_time_data=None)
    def __init__(self,
                 real_time_mapper=None,
                 register_real_time_data=None,
                 tracks_provider=None,
                 *a,
                 **k):
        assert liveobj_valid(real_time_mapper)
        assert tracks_provider is not None
        super(TrackMixerControlComponent, self).__init__(*a, **k)
        self._tracks_provider = tracks_provider
        self._on_return_tracks_changed.subject = self.song
        self.real_time_meter_channel = RealTimeDataComponent(
            parent=self,
            real_time_mapper=real_time_mapper,
            register_real_time_data=register_real_time_data,
            channel_type='meter')
        self._scroll_offset = 0
        self._items = []
        self._number_return_tracks = self._number_sends()
        self._update_scroll_buttons()
        self.__on_selected_item_changed.subject = self._tracks_provider
        self.__on_selected_item_changed()
        return

    def set_controls(self, controls):
        self.controls.set_control_element(controls)
        self._update_controls()

    @listens('panning_mode')
    def __on_pan_mode_changed(self):
        self._update_controls()
        self._update_scroll_offset()

    @listens('selected_item')
    def __on_selected_item_changed(self):
        self._update_scroll_offset()
        self._update_real_time_channel_id()
        mixer = self._tracks_provider.selected_item.mixer_device
        self.__on_pan_mode_changed.subject = mixer if has_pan_mode(
            mixer) else None
        self.__on_pan_mode_changed()
        return

    def update(self):
        super(TrackMixerControlComponent, self).update()
        if self.is_enabled():
            self._update_controls()
            self._update_scroll_buttons()
            self._update_real_time_channel_id()

    def _update_real_time_channel_id(self):
        self.real_time_meter_channel.set_data(
            self._tracks_provider.selected_item.mixer_device)

    def _update_controls(self):
        if self.is_enabled():
            assign_parameters(self.controls,
                              self.parameters[self.scroll_offset:])
            self.notify_parameters()

    @property
    def parameters(self):
        return self._get_track_mixer_parameters()

    @property
    def scroll_offset(self):
        return self._scroll_offset

    @listens('return_tracks')
    def _on_return_tracks_changed(self):
        self._update_controls()
        self._update_scroll_offset()

    def _number_sends(self):
        mixable = self._tracks_provider.selected_item
        if mixable != self.song.master_track:
            return len(mixable.mixer_device.sends)
        return 0

    def _max_return_tracks(self):
        mixer = self._tracks_provider.selected_item.mixer_device
        if is_set_to_split_stereo(mixer):
            return 5
        return 6

    def _update_scroll_offset(self):
        new_number_return_tracks = self._number_sends()
        max_return_tracks = self._max_return_tracks()
        if max_return_tracks <= new_number_return_tracks < self._number_return_tracks and max_return_tracks + self._scroll_offset > new_number_return_tracks:
            delta = min(new_number_return_tracks - self._number_return_tracks,
                        0)
            self._scroll_controls(delta)
        elif new_number_return_tracks < max_return_tracks or self._tracks_provider.selected_item == self.song.master_track:
            self._scroll_offset = 0
        self._update_controls()
        self._update_scroll_buttons()
        self._number_return_tracks = new_number_return_tracks

    def _get_track_mixer_parameters(self):
        mixer_params = []
        if self._tracks_provider.selected_item:
            mixer = self._tracks_provider.selected_item.mixer_device
            mixer_params = [mixer.volume]
            if is_set_to_split_stereo(mixer):
                mixer_params += [
                    mixer.left_split_stereo, mixer.right_split_stereo
                ]
            else:
                mixer_params += [mixer.panning]
            mixer_params += list(mixer.sends)
        return mixer_params

    @scroll_right_button.pressed
    def scroll_right_button(self, button):
        self._scroll_controls(1)

    @scroll_left_button.pressed
    def scroll_left_button(self, button):
        self._scroll_controls(-1)

    def _update_scroll_buttons(self):
        if self.is_enabled():
            num_return_tracks = self._number_sends()
            self.scroll_right_button.enabled = num_return_tracks > self._max_return_tracks(
            ) + self._scroll_offset
            self.scroll_left_button.enabled = self._scroll_offset > 0
            self._update_view_slots()

    @property
    def items(self):
        return self._items

    def _update_view_slots(self):
        self._items = [IconItemSlot() for _ in xrange(6)]
        self._items.append(
            IconItemSlot(icon='page_left.svg' if self.scroll_left_button.
                         enabled else ''))
        self._items.append(
            IconItemSlot(icon='page_right.svg' if self.scroll_right_button.
                         enabled else ''))
        self.notify_items()

    def _scroll_controls(self, delta):
        num_return_tracks = self._number_sends()
        self._scroll_offset = clamp(
            self._scroll_offset + delta, 0, num_return_tracks
            if num_return_tracks > self._max_return_tracks() else 0)
        self.notify_scroll_offset()
        self._update_controls()
        self._update_scroll_buttons()
class NoteSettingsComponentBase(Component):
    __events__ = (u'setting_changed', u'full_velocity')
    full_velocity_button = ButtonControl()

    def __init__(self, grid_resolution=None, *a, **k):
        super(NoteSettingsComponentBase, self).__init__(*a, **k)
        self._settings = []
        self._encoders = []
        self._create_settings(grid_resolution)

    def _create_settings(self, grid_resolution):
        self._add_setting(NoteNudgeSetting(grid_resolution=grid_resolution))
        self._add_setting(
            NoteLengthCoarseSetting(grid_resolution=grid_resolution))
        self._add_setting(
            NoteLengthFineSetting(grid_resolution=grid_resolution))
        self._add_setting(NoteVelocitySetting(grid_resolution=grid_resolution))

    def _add_setting(self, setting):
        assert len(self._settings) < 8, 'Cannot show more than 8 settings'
        self._settings.append(setting)
        self._update_encoders()
        self.register_disconnectable(setting)
        self.register_slot(setting, self.notify_setting_changed,
                           'setting_changed')

    @property
    def number_of_settings(self):
        return len(self._settings)

    def set_info_message(self, message):
        pass

    def set_encoder_controls(self, encoders):
        self._encoders = encoders or []
        self._update_encoders()

    def set_min_max(self, index, min_max_value):
        setting_for_index = [
            i for i in self._settings if i.attribute_index == index
        ]
        for setting in setting_for_index:
            setting.set_min_max(min_max_value)

    @full_velocity_button.pressed
    def full_velocity_button(self, button):
        if self.is_enabled():
            self.notify_full_velocity()

    def _update_encoders(self):
        if self.is_enabled() and self._encoders:
            for encoder, setting in izip_longest(
                    self._encoders[-len(self._settings):], self._settings):
                setting.encoder.set_control_element(encoder)

        else:
            map(lambda setting: setting.encoder.set_control_element(None),
                self._settings)

    def update(self):
        super(NoteSettingsComponentBase, self).update()
        self._update_encoders()
예제 #7
0
class ActionsComponent(Component):
    actions_display = TextDisplayControl(segments=ACTION_NAMES)
    actions_color_fields = control_list(ColorSysexControl, len(ACTION_NAMES))
    actions_selection_fields = control_list(BinaryControl, len(ACTION_NAMES))
    undo_button = ButtonControl(color='Action.Available')
    redo_button = ButtonControl(color='Action.Available')
    capture_midi_button = ButtonControl()
    metronome_button = ToggleButtonControl(
        toggled_color='Transport.MetronomeOn',
        untoggled_color='Transport.MetronomeOff')

    def __init__(self, *a, **k):
        super(ActionsComponent, self).__init__(*a, **k)
        self.__on_can_capture_midi_changed.subject = self.song
        self.__on_can_capture_midi_changed()
        self.actions_color_fields[
            METRONOME_DISPLAY_INDEX].color = 'Transport.MetronomeOn'
        self.actions_color_fields[
            UNDO_DISPLAY_INDEX].color = 'Action.Available'
        self.actions_color_fields[
            REDO_DISPLAY_INDEX].color = 'Action.Available'
        self.__on_metronome_changed.subject = self.song
        self.__on_metronome_changed()

    @property
    def capture_midi_display(self):
        return self.actions_display[CAPTURE_DISPLAY_INDEX]

    @capture_midi_display.setter
    def capture_midi_display(self, string):
        self.actions_display[CAPTURE_DISPLAY_INDEX] = string

    @property
    def capture_midi_color_field(self):
        return self.actions_color_fields[CAPTURE_DISPLAY_INDEX]

    @property
    def capture_midi_selection_field(self):
        return self.actions_selection_fields[CAPTURE_DISPLAY_INDEX]

    @undo_button.pressed
    def undo_button(self, _):
        if self.song.can_undo:
            self.song.undo()

    @redo_button.pressed
    def redo_button(self, _):
        if self.song.can_redo:
            self.song.redo()

    @capture_midi_button.pressed
    def capture_midi_button(self, _):
        try:
            self.song.capture_midi()
        except RuntimeError:
            pass

    @metronome_button.toggled
    def metronome_button(self, toggled, _):
        self.song.metronome = toggled

    @listens('can_capture_midi')
    def __on_can_capture_midi_changed(self):
        self._update_capture_midi_controls()

    @listens('metronome')
    def __on_metronome_changed(self):
        self._update_metronome_controls()

    def _update_capture_midi_controls(self):
        can_capture_midi = self.song.can_capture_midi
        self.capture_midi_button.enabled = can_capture_midi
        self.capture_midi_display = 'capture' if can_capture_midi else ''
        self.capture_midi_color_field.color = 'DefaultButton.On' if can_capture_midi else 'DefaultButton.Disabled'
        self.capture_midi_selection_field.is_on = can_capture_midi

    def _update_metronome_controls(self):
        metronome = self.song.metronome
        self.metronome_button.is_toggled = metronome
        self.actions_selection_fields[
            METRONOME_DISPLAY_INDEX].is_on = metronome
예제 #8
0
class ChannelStripComponent(ChannelStripComponentBase, Messenger):
    empty_color = 'DefaultButton.Disabled'
    monitoring_state_button = ButtonControl()
    pan_encoder_color_field = ColorSysexControl()
    track_color_field = ColorSysexControl()
    volume_led = ColorSysexControl()
    track_selection_field = BinaryControl()

    def __init__(self, *a, **k):
        (super(ChannelStripComponent, self).__init__)(*a, **k)
        self._pan_value_display_data_source = DisplayDataSource()
        self._ChannelStripComponent__on_selected_track_changed.subject = self.song.view

    @property
    def pan_value_display_data_source(self):
        return self._pan_value_display_data_source

    def set_track(self, track):
        super(ChannelStripComponent, self).set_track(track)
        self._update_pan_encoder_color_field()
        self._update_track_selection_field()
        self._update_listeners()

    def set_volume_control(self, control):
        super(ChannelStripComponent, self).set_volume_control(control)
        self._ChannelStripComponent__on_volume_control_value_received.subject = control

    def set_pan_control(self, control):
        if control != None:
            control.mapping_sensitivity = CONTINUOUS_MAPPING_SENSITIVITY
        super(ChannelStripComponent, self).set_pan_control(control)

    def set_send_controls(self, controls):
        for control in controls or []:
            if control != None:
                control.mapping_sensitivity = CONTINUOUS_MAPPING_SENSITIVITY

        super(ChannelStripComponent, self).set_send_controls(controls)

    @monitoring_state_button.pressed
    def monitoring_state_button(self, _):
        if has_monitoring_state(self.track):
            self.track.current_monitoring_state = (
                self.track.current_monitoring_state + 1) % len(
                    monitoring_states.values)
            self._message_monitoring_state()

    def _message_monitoring_state(self):
        track = self.track
        self.message(
            track.name, 'Monitor {}'.format(
                MONITORING_STATES_TO_STR[track.current_monitoring_state]))

    def _update_monitoring_state_button(self):
        color = self.empty_color
        if liveobj_valid(self.track):
            if has_monitoring_state(self.track):
                color = 'Monitor.{}'.format(MONITORING_STATES_TO_STR[
                    self.track.current_monitoring_state])
            else:
                color = 'Monitor.Disabled'
        self.monitoring_state_button.color = color

    def _update_volume_led(self):
        track = self.track
        value = (0, 0, 0)
        if liveobj_valid(track):
            value = tuple([
                clamp(
                    int(
                        old_round(
                            old_div(
                                channel * normalized_parameter_value(
                                    track.mixer_device.volume), 2))), 0, 127)
                for channel in hex_to_channels(track.color)
            ])
        self.volume_led.color = SysexRGBColor(value)

    def _message_volume_value(self):
        track = self.track
        if liveobj_valid(track):
            self.message(track.name, str(track.mixer_device.volume))

    def _update_pan_encoder_color_field(self):
        self.pan_encoder_color_field.color = 'Mixer.Pan' if liveobj_valid(
            self.track) else 'DefaultButton.Disabled'

    def _update_pan_value_display(self):
        track = self.track
        self.pan_value_display_data_source.set_display_string(
            str(track.mixer_device.panning) if liveobj_valid(track) else '')

    def _update_track_color_field(self):
        self.track_color_field.color = color_for_track(self.track)

    def _update_select_button(self):
        track = self.track
        color = 'DefaultButton.Disabled'
        if liveobj_valid(track):
            if track == self.song.view.selected_track:
                color = color_for_track(track)
            else:
                color = 'Mixer.TrackSelect'
        self.select_button.color = color

    def _update_track_selection_field(self):
        self.track_selection_field.is_on = self.track == self.song.view.selected_track

    @listens('current_monitoring_state')
    def __on_track_monitoring_state_changed(self):
        self._update_monitoring_state_button()

    @listens('value')
    def __on_volume_changed(self):
        self._update_volume_led()

    @listens('value')
    def __on_pan_changed(self):
        self._update_pan_value_display()

    @listens('color')
    def __on_track_color_changed(self):
        self._update_track_color_field()
        self._update_select_button()
        self._update_volume_led()

    @listens('selected_track')
    def __on_selected_track_changed(self):
        self._update_select_button()
        self._update_track_selection_field()

    @listens('value')
    def __on_volume_control_value_received(self, _):
        self._message_volume_value()

    def _update_listeners(self):
        track = self.track
        self._ChannelStripComponent__on_track_monitoring_state_changed.subject = track
        self._ChannelStripComponent__on_track_monitoring_state_changed()
        self._ChannelStripComponent__on_volume_changed.subject = track.mixer_device.volume if liveobj_valid(
            track) else None
        self._ChannelStripComponent__on_volume_changed()
        self._ChannelStripComponent__on_pan_changed.subject = track.mixer_device.panning if liveobj_valid(
            track) else None
        self._ChannelStripComponent__on_pan_changed()
        self._ChannelStripComponent__on_track_color_changed.subject = track if liveobj_valid(
            track) else None
        self._ChannelStripComponent__on_track_color_changed()
예제 #9
0
class DeviceSelectorComponent(Component):


	assign_button = ButtonControl(color = 'DeviceSelector.AssignOff', pressed_color = 'DeviceSelector.AssignOn')

	def __init__(self, script, prefix = '@d', *a, **k):
		super(DeviceSelectorComponent, self).__init__(*a, **k)
		self.log_message = script.log_message
		self._script = script
		self._prefix = prefix
		self._offset = 0
		self._buttons = []
		self._device_registry = []
		self._watched_device = None
		self._device_colors = DEVICE_COLORS
		self._selected_colorshift = SELECTED_COLORSHIFT
		self._device_listener.subject = self.song
		self._device_listener()
		self._off_value = 0


	def disconnect(self, *a, **k):
		super(DeviceSelectorComponent, self).disconnect()


	def set_offset(self, offset):
		self._offset = offset
		self.update()


	def set_matrix(self, matrix):
		buttons = []
		if not matrix is None:
			for button, address in matrix.iterbuttons():
				#self._script.log_message('button is: ' + str(button))
				if not button is None:
					button.use_default_message()
					if hasattr(button, 'set_enabled'):
 						button.set_enabled(True)
					elif hasattr(button, 'suppress_script_forwarding'):
						button.suppress_script_forwarding = False
					buttons.append(button)
		self.set_buttons(buttons)


	def set_buttons(self, buttons):
		for button in self._buttons:
			button and button.reset()
		self._buttons = buttons or []
		self._on_button_value.replace_subjects(self._buttons)
		self.update()


	def set_assign_button(self, button):
		debug('set assign button:', button)
		self.assign_button.set_control_element(button)


	@listens_group('value')
	def _on_button_value(self, value, sender):
		if self.is_enabled():
			if value:
				if self.assign_button.is_pressed:
					self.assign_device(self._buttons.index(sender))
				else:
					self.select_device(self._buttons.index(sender))


	def assign_device(self, index):
		device = self.song.appointed_device
		if not device is None and hasattr(device, 'name'):
			prefix = str(self._prefix)+'_'
			offset = self._offset
			key =  prefix + str(index + 1 + offset)
			name = device.name.split(' ')
			if key in name:
				name.remove(key)
			else:
				old_entry = self._device_registry[index]
				if old_entry and hasattr(old_entry, 'name'):
					old_name = old_entry.name.split(' ')
					if key in old_name:
						old_name.remove(key)
						old_entry.name = ' '.join(old_name)
				for sub in name:
					sub.startswith(prefix) and name.remove(sub)
				name.insert(0, key)
			device.name = ' '.join(name)
			self.scan_all()
			self.update()


	def select_device(self, index):
		if self.is_enabled():
			preset = None
			if index < len(self._device_registry):
				preset = self._device_registry[index]
			if not preset is None and isinstance(preset, Live.Device.Device):
				self.song.view.select_device(preset)
				self._script._device_provider.device = preset
				#self._script.set_appointed_device(preset)
				try:
					self._script.monomodular.is_mod(preset) and self._script.modhandler.select_mod(self._script.monomodular.is_mod(preset))
				except:
					pass
			self.update()


	def scan_all(self):
		#debug('scan all--------------------------------')
		self._device_registry = [None for index in range(len(self._buttons))]
		prefix = str(self._prefix)+':'
		prefix2 = str(self._prefix)+'_'
		offset = self._offset
		preset = None
		tracks = self.song.tracks + self.song.return_tracks + tuple([self.song.master_track])
		for track in tracks:
			for device in enumerate_track_device(track):
				for index, entry in enumerate(self._device_registry):
					key = str(prefix + str(index + 1 + offset))
					key2 = str(prefix2 + str(index + 1 + offset))
					if device.name.startswith(key+' ') or device.name == key:
						self._device_registry[index] = device
					elif device.name.startswith(key2+' ') or device.name == key2:
						self._device_registry[index] = device
					elif (device.name.startswith('*' +key+' ') or device.name == ('*' +key))  and device.can_have_chains and len(device.chains) and len(device.chains[0].devices):
						self._device_registry[index] = device.chains[0].devices[0]
		self.update()
		#debug('device registry: ' + str(self._device_registry))



	@listens('appointed_device')
	def _device_listener(self, *a, **k):
		#debug('device_listener')
		self._on_name_changed.subject = self.song.appointed_device
		self._watched_device = self.song.appointed_device
		if self.is_enabled():
			self.update()


	@listens('name')
	def _on_name_changed(self):
		#debug('on name changed')
		if self._watched_device == self.song.appointed_device:
			self.scan_all()


	def on_enabled_changed(self):
		if self.is_enabled():
			self.update()


	def update(self):
		if self.is_enabled():
			if len(self._device_registry) != len(self._buttons):
				self.scan_all()
			name = 'None'
			dev = self.song.appointed_device
			offset = self._offset
			if self._buttons:
				for index in range(len(self._buttons)):
					preset = self._device_registry[index]
					button = self._buttons[index]
					if isinstance(button, ButtonElement):
						if isinstance(preset, Live.Device.Device) and hasattr(preset, 'name'):
							name = preset.name
							dev_type = preset.type
							dev_class = preset.class_name
							val = (dev_class in self._device_colors and self._device_colors[dev_class]) or (dev_type in self._device_colors and self._device_colors[dev_type]) or 7
							selected_shift = (dev == preset)*self._selected_colorshift
							button.send_value(val + selected_shift)
						else:
							button.send_value(self._off_value)
예제 #10
0
class ScalesComponent(Component):
    __events__ = ('close', )
    root_note_buttons = control_list(RadioButtonControl,
                                     control_count=len(ROOT_NOTES),
                                     checked_color='Scales.OptionOn',
                                     unchecked_color='Scales.OptionOff')
    in_key_toggle_button = ToggleButtonControl(
        toggled_color='Scales.OptionOn', untoggled_color='Scales.OptionOn')
    fixed_toggle_button = ToggleButtonControl(
        toggled_color='Scales.OptionOn', untoggled_color='Scales.OptionOff')
    scale_encoders = control_list(StepEncoderControl)
    close_button = ButtonControl(color='Scales.Close')

    def __init__(self, note_layout=None, *a, **k):
        raise note_layout is not None or AssertionError
        super(ScalesComponent, self).__init__(*a, **k)
        self._note_layout = note_layout
        self._scale_list = list(SCALES)
        self._scale_name_list = map(lambda m: m.name, self._scale_list)
        self._selected_scale_index = -1
        self._selected_root_note_index = -1
        self.in_key_toggle_button.connect_property(note_layout, 'is_in_key')
        self.fixed_toggle_button.connect_property(note_layout, 'is_fixed')
        self.__on_root_note_changed.subject = self._note_layout
        self.__on_scale_changed.subject = self._note_layout
        self.__on_root_note_changed(note_layout.root_note)
        self.__on_scale_changed(note_layout.scale)

    @root_note_buttons.pressed
    def root_note_buttons(self, button):
        self._note_layout.root_note = ROOT_NOTES[button.index]

    @listens('root_note')
    def __on_root_note_changed(self, root_note):
        self._selected_root_note_index = list(ROOT_NOTES).index(root_note)
        self.root_note_buttons.checked_index = self._selected_root_note_index
        self.notify_selected_root_note_index()

    @property
    def root_note_names(self):
        return [NOTE_NAMES[note] for note in ROOT_NOTES]

    @listenable_property
    def selected_root_note_index(self):
        return self._selected_root_note_index

    @scale_encoders.value
    def scale_encoders(self, value, encoder):
        index = clamp(self._selected_scale_index + value, 0,
                      len(self._scale_list) - 1)
        self._note_layout.scale = self._scale_list[index]

    @property
    def scale_names(self):
        return self._scale_name_list

    @listenable_property
    def selected_scale_index(self):
        return self._selected_scale_index

    @listens('scale')
    def __on_scale_changed(self, scale):
        index = self._scale_list.index(
            scale) if scale in self._scale_list else -1
        if index != self._selected_scale_index:
            self._selected_scale_index = index
            self.notify_selected_scale_index()

    @close_button.pressed
    def close_button(self, button):
        self.notify_close()

    @property
    def note_layout(self):
        return self._note_layout
예제 #11
0
class MixerComponent(MixerComponentBase):
    num_sends_control = SendValueControl()
    master_button = ButtonControl()

    def __init__(self, *a, **k):
        super(MixerComponent, self).__init__(*a, **k)
        self._last_selected_track = None
        self._last_track_offset = None
        self.__on_offsets_changed.subject = self._provider
        self.__on_offsets_changed(self._provider.track_offset,
                                  self._provider.scene_offset)
        return

    def __getattr__(self, name):
        if name.startswith(b'set_') and name.endswith(b's'):
            return partial(self._set_channel_strip_controls, name[4:-1])
        raise AttributeError

    def on_num_sends_changed(self):
        self.num_sends_control.value = clamp(self.num_sends, 0, MAX_NUM_SENDS)

    @property
    def max_track_offset(self):
        return max(
            0,
            len(self._provider.tracks_to_use()) - self._provider.num_tracks)

    def _on_selected_track_changed(self):
        selected_track = self.song.view.selected_track
        button_color = b'DefaultButton.On'
        if selected_track != self.song.master_track:
            self._last_selected_track = selected_track
            button_color = b'DefaultButton.Off'
        self.master_button.color = button_color

    @listens(b'offset')
    def __on_offsets_changed(self, track_offset, _):
        max_track_offset = self.max_track_offset
        if max_track_offset == 0 or track_offset < max_track_offset:
            self._last_track_offset = track_offset

    def set_send_controls(self, controls):
        self._send_controls = controls
        for strip, row in izip_longest(self._channel_strips,
                                       controls.rows() if controls else []):
            strip.set_send_controls(row)

    def set_send_value_displays(self, displays):
        for strip, row in izip_longest(self._channel_strips,
                                       displays.rows() if displays else []):
            strip.set_send_value_displays(row)

    def set_selected_track_mute_button(self, button):
        self._selected_strip.mpc_mute_button.set_control_element(button)

    set_selected_track_arm_button = forward_property(b'_selected_strip')(
        b'set_arm_button')
    set_selected_track_solo_button = forward_property(b'_selected_strip')(
        b'set_solo_button')

    def set_track_type_controls(self, controls):
        for strip, control in izip_longest(self._channel_strips, controls
                                           or []):
            strip.track_type_control.set_control_element(control)

    def _set_channel_strip_controls(self, name, controls):
        for strip, control in izip_longest(self._channel_strips, controls
                                           or []):
            set_method = getattr(strip, (b'set_{}').format(name), None)
            if not set_method:
                set_method = getattr(strip, name, None).set_control_element
            set_method(control)

        return

    def set_solo_mute_buttons(self, buttons):
        for strip, button in izip_longest(self._channel_strips, buttons or []):
            strip.solo_mute_button.set_control_element(button)

    @master_button.pressed
    def master_button_value(self, _button):
        master_track = self.song.master_track
        if self.song.view.selected_track != master_track:
            self.song.view.selected_track = master_track
        else:
            self.song.view.selected_track = self._last_selected_track if liveobj_valid(
                self._last_selected_track) else self.song.tracks[0]
        if self._provider.track_offset < self.max_track_offset:
            self._provider.track_offset = self.max_track_offset
        else:
            self._provider.track_offset = self._last_track_offset
class BrowserComponent(Component, Messenger):
    __events__ = (u'loaded', u'close')
    NUM_ITEMS_PER_COLUMN = 6
    NUM_VISIBLE_BROWSER_LISTS = 7
    NUM_COLUMNS_IN_EXPANDED_LIST = 3
    EXPAND_LIST_TIME = 1.5
    REVEAL_PREVIEW_LIST_TIME = 0.2
    MIN_TIME = 0.6
    MAX_TIME = 1.4
    MIN_TIME_TEXT_LENGTH = 30
    MAX_TIME_TEXT_LENGTH = 70
    up_button = ButtonControl(repeat=True)
    down_button = ButtonControl(repeat=True)
    right_button = ButtonControl(repeat=True, **NAVIGATION_COLORS)
    left_button = ButtonControl(repeat=True, **NAVIGATION_COLORS)
    back_button = ButtonControl(**NAVIGATION_COLORS)
    open_button = ButtonControl(**NAVIGATION_COLORS)
    load_button = ButtonControl(**NAVIGATION_COLORS)
    close_button = ButtonControl()
    prehear_button = ToggleButtonControl(toggled_color='Browser.Option', untoggled_color='Browser.OptionDisabled')
    scroll_encoders = control_list(StepEncoderControl, num_steps=10, control_count=NUM_VISIBLE_BROWSER_LISTS)
    scroll_focused_encoder = StepEncoderControl(num_steps=10)
    scrolling = listenable_property.managed(False)
    horizontal_navigation = listenable_property.managed(False)
    list_offset = listenable_property.managed(0)
    can_enter = listenable_property.managed(False)
    can_exit = listenable_property.managed(False)
    context_color_index = listenable_property.managed(-1)
    context_text = listenable_property.managed('')

    @depends(commit_model_changes=None, selection=None)
    def __init__(self, preferences=dict(), commit_model_changes=None, selection=None, main_modes_ref=None, *a, **k):
        assert commit_model_changes is not None
        super(BrowserComponent, self).__init__(*a, **k)
        self._lists = []
        self._browser = Live.Application.get_application().browser
        self._current_hotswap_target = self._browser.hotswap_target
        self._updating_root_items = BooleanContext()
        self._focused_list_index = 0
        self._commit_model_changes = commit_model_changes
        self._preferences = preferences
        self._expanded = False
        self._unexpand_with_scroll_encoder = False
        self._delay_preview_list = BooleanContext()
        self._selection = selection
        self._main_modes_ref = main_modes_ref if main_modes_ref is not None else nop
        self._load_neighbour_overlay = LoadNeighbourOverlayComponent(parent=self, is_enabled=False)
        self._content_filter_type = None
        self._content_hotswap_target = None
        self._preview_list_task = self._tasks.add(task.sequence(task.wait(self.REVEAL_PREVIEW_LIST_TIME), task.run(self._replace_preview_list_by_task))).kill()
        self._update_root_items()
        self._update_navigation_buttons()
        self._update_context()
        self.prehear_button.is_toggled = preferences.setdefault('browser_prehear', True)
        self._on_selected_track_color_index_changed.subject = self.song.view
        self._on_selected_track_name_changed.subject = self.song.view
        self._on_detail_clip_name_changed.subject = self.song.view
        self._on_hotswap_target_changed.subject = self._browser
        self._on_load_next.subject = self._load_neighbour_overlay
        self._on_load_previous.subject = self._load_neighbour_overlay
        self._on_focused_item_changed.subject = self
        self.register_slot(self, self.notify_focused_item, 'focused_list_index')

        def auto_unexpand():
            self.expanded = False
            self._update_list_offset()

        self._unexpand_task = self._tasks.add(task.sequence(task.wait(self.EXPAND_LIST_TIME), task.run(auto_unexpand))).kill()
        return

    @up_button.pressed
    def up_button(self, button):
        with self._delay_preview_list():
            self.focused_list.select_index_with_offset(-1)
        self._update_auto_expand()
        self._update_scrolling()
        self._update_horizontal_navigation()

    @up_button.released
    def up_button(self, button):
        self._finish_preview_list_task()
        self._update_scrolling()

    @down_button.pressed
    def down_button(self, button):
        with self._delay_preview_list():
            self.focused_list.select_index_with_offset(1)
        self._update_auto_expand()
        self._update_scrolling()
        self._update_horizontal_navigation()

    @down_button.released
    def down_button(self, button):
        self._finish_preview_list_task()
        self._update_scrolling()

    @right_button.pressed
    def right_button(self, button):
        if self._expanded and self._can_auto_expand() and self._focused_list_index > 0:
            self.focused_list.select_index_with_offset(self.NUM_ITEMS_PER_COLUMN)
            self._update_scrolling()
            self.horizontal_navigation = True
        elif not self._enter_selected_item():
            self._update_auto_expand()

    @right_button.released
    def right_button(self, button):
        self._update_scrolling()

    @left_button.pressed
    def left_button(self, button):
        if self._expanded and self._focused_list_index > 0 and self.focused_list.selected_index >= self.NUM_ITEMS_PER_COLUMN:
            self.focused_list.select_index_with_offset(-self.NUM_ITEMS_PER_COLUMN)
            self._update_scrolling()
            self.horizontal_navigation = True
        else:
            self._exit_selected_item()

    @left_button.released
    def left_button(self, button):
        self._update_scrolling()

    @open_button.pressed
    def open_button(self, button):
        self._enter_selected_item()

    @back_button.pressed
    def back_button(self, button):
        self._exit_selected_item()

    @scroll_encoders.touched
    def scroll_encoders(self, encoder):
        list_index = self._get_list_index_for_encoder(encoder)
        if list_index is not None:
            try:
                if self._focus_list_with_index(list_index, crop=False):
                    self._unexpand_with_scroll_encoder = True
                    self._prehear_selected_item()
                if self.focused_list.selected_item.is_loadable and encoder.index == self.scroll_encoders.control_count - 1:
                    self._update_list_offset()
                self._on_encoder_touched()
            except CannotFocusListError:
                pass

        return

    @scroll_encoders.released
    def scroll_encoders(self, encoders):
        self._on_encoder_released()

    @scroll_encoders.value
    def scroll_encoders(self, value, encoder):
        list_index = self._get_list_index_for_encoder(encoder)
        if list_index is not None:
            try:
                if self._focus_list_with_index(list_index):
                    self._unexpand_with_scroll_encoder = True
                self._on_encoder_value(value)
            except CannotFocusListError:
                pass

        return

    @scroll_focused_encoder.value
    def scroll_focused_encoder(self, value, encoder):
        self._on_encoder_value(value)

    @scroll_focused_encoder.touched
    def scroll_focused_encoder(self, encoder):
        self._on_encoder_touched()

    @scroll_focused_encoder.released
    def scroll_focused_encoder(self, encoder):
        self._on_encoder_released()

    def _on_encoder_value(self, value):
        with self._delay_preview_list():
            self.focused_list.select_index_with_offset(value)
        first_visible_list_focused = self.focused_list_index == self.list_offset
        if self.expanded and first_visible_list_focused:
            self.expanded = False
            self._unexpand_with_scroll_encoder = True
        elif not first_visible_list_focused and not self.expanded and self._can_auto_expand():
            self._update_auto_expand()
            self._unexpand_with_scroll_encoder = True
        self._update_scrolling()
        self._update_horizontal_navigation()

    def _on_encoder_touched(self):
        self._unexpand_task.kill()
        self._update_scrolling()
        self._update_horizontal_navigation()

    def _on_encoder_released(self):
        any_encoder_touched = any(imap(lambda e: e.is_touched, self.scroll_encoders)) or self.scroll_focused_encoder.is_touched
        if not any_encoder_touched and self._unexpand_with_scroll_encoder:
            self._unexpand_task.restart()
        self._update_scrolling()

    def _get_list_index_for_encoder(self, encoder):
        if self.expanded:
            if encoder.index == 0:
                return self.list_offset
            return self.list_offset + 1
        else:
            index = self.list_offset + encoder.index
            if self.focused_list_index + 1 == index and self.should_widen_focused_item:
                index = self.focused_list_index
            if 0 <= index < len(self._lists):
                return index
            return
            return

    @load_button.pressed
    def load_button(self, button):
        self._load_selected_item()

    @prehear_button.toggled
    def prehear_button(self, toggled, button):
        if toggled:
            self._prehear_selected_item()
        else:
            self._browser.stop_preview()
        self._preferences['browser_prehear'] = toggled
        self.notify_prehear_enabled()

    @close_button.pressed
    def close_button(self, button):
        self.notify_close()

    @listenable_property
    def lists(self):
        return self._lists

    @listenable_property
    def focused_list_index(self):
        return self._focused_list_index

    @listenable_property
    def prehear_enabled(self):
        return self.prehear_button.is_toggled

    @property
    def focused_list(self):
        return self._lists[self._focused_list_index]

    @listenable_property
    def focused_item(self):
        return self.focused_list.selected_item

    @listenable_property
    def expanded(self):
        return self._expanded

    @property
    def load_neighbour_overlay(self):
        return self._load_neighbour_overlay

    @listenable_property
    def should_widen_focused_item(self):
        return self.focused_item.is_loadable and not self.focused_item.is_device

    @property
    def context_display_type(self):
        return 'custom_button'

    def disconnect(self):
        super(BrowserComponent, self).disconnect()
        self._lists = []
        self._commit_model_changes = None
        return

    @expanded.setter
    def expanded(self, expanded):
        if self._expanded != expanded:
            self._expanded = expanded
            self._unexpand_with_scroll_encoder = False
            self._update_navigation_buttons()
            if len(self._lists) > self._focused_list_index + 1:
                self._lists[(self._focused_list_index + 1)].limit = self.num_preview_items
            self.notify_expanded()

    @listens('selected_track.color_index')
    def _on_selected_track_color_index_changed(self):
        if self.is_enabled():
            self._update_context()
            self._update_navigation_buttons()

    @listens('selected_track.name')
    def _on_selected_track_name_changed(self):
        if self.is_enabled():
            self._update_context()

    @listens('detail_clip.name')
    def _on_detail_clip_name_changed(self):
        if self.is_enabled():
            self._update_context()

    @listens('hotswap_target')
    def _on_hotswap_target_changed(self):
        if self.is_enabled():
            if not self._switched_to_empty_pad():
                self._update_root_items()
                self._update_context()
                self._update_list_offset()
                self._update_load_neighbour_overlay_visibility()
            else:
                self._load_neighbour_overlay.set_enabled(False)
        self._current_hotswap_target = self._browser.hotswap_target

    @listens('focused_item')
    def _on_focused_item_changed(self):
        self.notify_should_widen_focused_item()

    @property
    def browse_for_audio_clip(self):
        main_modes = self._main_modes_ref()
        if main_modes is None:
            return False
        else:
            has_midi_support = self.song.view.selected_track.has_midi_input
            return not has_midi_support and 'clip' in main_modes.active_modes

    def _switched_to_empty_pad(self):
        hotswap_target = self._browser.hotswap_target
        is_browsing_drumpad = isinstance(hotswap_target, Live.DrumPad.DrumPad)
        was_browsing_pad = isinstance(self._current_hotswap_target, Live.DrumPad.DrumPad)
        return is_browsing_drumpad and was_browsing_pad and len(hotswap_target.chains) == 0

    def _focus_list_with_index(self, index, crop=True):
        u"""
        Focus the list with the given index.
        Raises CannotFocusListError if the operation fails.
        Returns True if a new list was focused and False if it was already focused.
        """
        if self._focused_list_index != index:
            if self._finish_preview_list_task():
                if index >= len(self._lists):
                    raise CannotFocusListError()
            assert 0 <= index < len(self._lists)
            self._on_focused_selection_changed.subject = None
            if self._focused_list_index > index and crop:
                for l in self._lists[self._focused_list_index:]:
                    l.selected_index = -1

            self._focused_list_index = index
            self.focused_list.limit = -1
            if self.focused_list.selected_index == -1:
                self.focused_list.selected_index = 0
            self.notify_focused_list_index()
            self._on_focused_selection_changed.subject = self.focused_list
            if crop:
                self._crop_browser_lists(self._focused_list_index + 2)
            if self._focused_list_index == len(self._lists) - 1:
                self._replace_preview_list()
            self._load_neighbour_overlay.set_enabled(False)
            self._update_navigation_buttons()
            return True
        else:
            return False

    @listens('selected_index')
    def _on_focused_selection_changed(self):
        if self._delay_preview_list and not self.focused_item.is_loadable:
            self._preview_list_task.restart()
        else:
            self._replace_preview_list()
        self._update_navigation_buttons()
        self._prehear_selected_item()
        self._load_neighbour_overlay.set_enabled(False)
        self.notify_focused_item()

    def _get_actual_item(self, item):
        contained_item = getattr(item, 'contained_item', None)
        if contained_item is not None:
            return contained_item
        else:
            return item

    def _previous_can_be_loaded(self):
        return self.focused_list.selected_index > 0 and self.focused_list.items[(self.focused_list.selected_index - 1)].is_loadable

    def _next_can_be_loaded(self):
        items = self.focused_list.items
        return self.focused_list.selected_index < len(items) - 1 and items[(self.focused_list.selected_index + 1)].is_loadable

    @listens('load_next')
    def _on_load_next(self):
        self.focused_list.selected_index += 1
        self._load_selected_item()

    @listens('load_previous')
    def _on_load_previous(self):
        self.focused_list.selected_index -= 1
        self._load_selected_item()

    def _update_load_neighbour_overlay_visibility(self):
        self._load_neighbour_overlay.set_enabled(liveobj_valid(self._browser.hotswap_target) and (self._next_can_be_loaded() or self._previous_can_be_loaded()) and not self.focused_list.selected_item.is_device)

    def _load_selected_item(self):
        focused_list = self.focused_list
        self._update_load_neighbour_overlay_visibility()
        self._update_navigation_buttons()
        item = self._get_actual_item(focused_list.selected_item)
        self._load_item(item)
        self.notify_loaded()

    def _show_load_notification(self, item):
        notification_text = self._make_notification_text(item)
        text_length = len(notification_text)
        notification_time = self.MIN_TIME
        if text_length > self.MIN_TIME_TEXT_LENGTH:
            if text_length > self.MAX_TIME_TEXT_LENGTH:
                notification_time = self.MAX_TIME
            else:
                notification_time = self.MIN_TIME + (self.MAX_TIME - self.MIN_TIME) * float(text_length - self.MIN_TIME_TEXT_LENGTH) / (self.MAX_TIME_TEXT_LENGTH - self.MIN_TIME_TEXT_LENGTH)
        self.show_notification(notification_text, notification_time=notification_time)
        self._commit_model_changes()

    def _make_notification_text(self, browser_item):
        return 'Loading %s' % browser_item.name

    def _load_item(self, item):
        self._show_load_notification(item)
        if liveobj_valid(self._browser.hotswap_target):
            if isinstance(item, PluginPresetBrowserItem):
                self._browser.hotswap_target.selected_preset_index = item.preset_index
            else:
                self._browser.load_item(item)
                self._content_hotswap_target = self._browser.hotswap_target
        else:
            with self._insert_right_of_selected():
                self._browser.load_item(item)

    @contextmanager
    def _insert_right_of_selected(self):
        DeviceInsertMode = Live.Track.DeviceInsertMode
        device_to_select = get_selection_for_new_device(self._selection)
        if device_to_select:
            self._selection.selected_object = device_to_select
        selected_track_view = self.song.view.selected_track.view
        selected_track_view.device_insert_mode = DeviceInsertMode.selected_right
        yield
        selected_track_view.device_insert_mode = DeviceInsertMode.default

    def _prehear_selected_item(self):
        if self.prehear_button.is_toggled and not self._updating_root_items:
            self._browser.stop_preview()
            item = self._get_actual_item(self.focused_list.selected_item)
            if item and item.is_loadable and isinstance(item, Live.Browser.BrowserItem):
                self._browser.preview_item(item)

    def _stop_prehear(self):
        if self.prehear_button.is_toggled and not self._updating_root_items:
            self._browser.stop_preview()

    def _update_navigation_buttons(self):
        focused_list = self.focused_list
        self.up_button.enabled = focused_list.selected_index > 0
        self.down_button.enabled = focused_list.selected_index < len(focused_list.items) - 1
        selected_item_loadable = self.focused_list.selected_item.is_loadable
        can_exit = self._focused_list_index > 0
        assume_can_enter = self._preview_list_task.is_running and not selected_item_loadable
        can_enter = self._focused_list_index < len(self._lists) - 1 or assume_can_enter
        self.back_button.enabled = can_exit
        self.open_button.enabled = can_enter
        self.load_button.enabled = selected_item_loadable
        self._load_neighbour_overlay.can_load_previous = self._previous_can_be_loaded()
        self._load_neighbour_overlay.can_load_next = self._next_can_be_loaded()
        context_button_color = IndexedColor.from_live_index(self.context_color_index, DISPLAY_BUTTON_SHADE_LEVEL) if self.context_color_index > -1 else 'Browser.Navigation'
        self.load_button.color = context_button_color
        self.close_button.color = context_button_color
        self._load_neighbour_overlay.load_next_button.color = context_button_color
        self._load_neighbour_overlay.load_previous_button.color = context_button_color
        if not self._expanded:
            self.left_button.enabled = self.back_button.enabled
            self.right_button.enabled = can_enter or self._can_auto_expand()
        else:
            num_columns = int(ceil(float(len(self.focused_list.items)) / self.NUM_ITEMS_PER_COLUMN))
            last_column_start_index = (num_columns - 1) * self.NUM_ITEMS_PER_COLUMN
            self.left_button.enabled = self._focused_list_index > 0
            self.right_button.enabled = can_enter or self.focused_list.selected_index < last_column_start_index
        self.can_enter = can_enter
        self.can_exit = can_exit

    def _update_scrolling(self):
        self.scrolling = self.up_button.is_pressed or self.down_button.is_pressed or self.scroll_focused_encoder.is_touched or any(imap(lambda e: e.is_touched, self.scroll_encoders)) or self.right_button.is_pressed and self._expanded or self.left_button.is_pressed and self._expanded

    def _update_horizontal_navigation(self):
        self.horizontal_navigation = self.right_button.is_pressed or self.left_button.is_pressed

    def _update_context(self):
        selected_track = self.song.view.selected_track
        clip = self.song.view.detail_clip
        if self.browse_for_audio_clip and clip:
            self.context_text = clip.name
        elif liveobj_valid(self._browser.hotswap_target):
            self.context_text = self._browser.hotswap_target.name
        else:
            self.context_text = selected_track.name
        selected_track_color_index = selected_track.color_index
        self.context_color_index = selected_track_color_index if selected_track_color_index is not None else -1
        return

    def _enter_selected_item(self):
        item_entered = False
        self._finish_preview_list_task()
        new_index = self._focused_list_index + 1
        if 0 <= new_index < len(self._lists):
            self._focus_list_with_index(new_index)
            self._unexpand_task.kill()
            self._update_list_offset()
            self._update_auto_expand()
            self._prehear_selected_item()
            item_entered = True
        return item_entered

    def _exit_selected_item(self):
        item_exited = False
        try:
            self._focus_list_with_index(self._focused_list_index - 1)
            self._update_list_offset()
            self._update_auto_expand()
            self._stop_prehear()
            item_exited = True
        except CannotFocusListError:
            pass

        return item_exited

    def _can_auto_expand(self):
        return len(self.focused_list.items) > self.NUM_ITEMS_PER_COLUMN * 2 and self.focused_list.selected_item.is_loadable and getattr(self.focused_list.selected_item, 'contained_item', None) == None

    def _update_auto_expand(self):
        self.expanded = self._can_auto_expand()
        self._update_list_offset()

    def _update_list_offset(self):
        if self.expanded:
            self.list_offset = max(0, self.focused_list_index - 1)
        else:
            offset = len(self._lists)
            if self.focused_list.selected_item.is_loadable:
                offset += 1
            self.list_offset = max(0, offset - self.NUM_VISIBLE_BROWSER_LISTS)

    def _replace_preview_list_by_task(self):
        self._replace_preview_list()
        self._update_navigation_buttons()

    def _finish_preview_list_task(self):
        if self._preview_list_task.is_running:
            self._replace_preview_list_by_task()
            return True
        return False

    def _replace_preview_list(self):
        self._preview_list_task.kill()
        self._crop_browser_lists(self._focused_list_index + 1)
        selected_item = self.focused_list.selected_item
        children_iterator = selected_item.iter_children
        if len(children_iterator) > 0:
            enable_wrapping = getattr(selected_item, 'enable_wrapping', True) and self.focused_list.items_wrapped
            self._append_browser_list(children_iterator=children_iterator, limit=self.num_preview_items, enable_wrapping=enable_wrapping)

    def _append_browser_list(self, children_iterator, limit=-1, enable_wrapping=True):
        l = BrowserList(item_iterator=children_iterator, item_wrapper=self._wrap_item if enable_wrapping else nop, limit=limit)
        l.items_wrapped = enable_wrapping
        self._lists.append(l)
        self.register_disconnectable(l)
        self.notify_lists()

    def _crop_browser_lists(self, length):
        num_items_to_crop = len(self._lists) - length
        for _ in xrange(num_items_to_crop):
            l = self._lists.pop()
            self.unregister_disconnectable(l)

        if num_items_to_crop > 0:
            self.notify_lists()

    def _make_root_browser_items(self):
        filter_type = self._browser.filter_type
        hotswap_target = self._browser.hotswap_target
        if liveobj_valid(hotswap_target):
            filter_type = filter_type_for_hotswap_target(hotswap_target, default=filter_type)
        return make_root_browser_items(self._browser, filter_type)

    def _content_cache_is_valid(self):
        return self._content_filter_type == self._browser.filter_type and not liveobj_changed(self._content_hotswap_target, self._browser.hotswap_target)

    def _invalidate_content_cache(self):
        self._content_hotswap_target = None
        self._content_filter_type = None
        return

    def _update_content_cache(self):
        self._content_filter_type = self._browser.filter_type
        self._content_hotswap_target = self._browser.hotswap_target

    def _update_root_items(self):
        if not self._content_cache_is_valid():
            self._update_content_cache()
            with self._updating_root_items():
                self._on_focused_selection_changed.subject = None
                self._crop_browser_lists(0)
                self._append_browser_list(children_iterator=self._make_root_browser_items())
                self._focused_list_index = 0
                self.focused_list.selected_index = 0
                self._select_hotswap_target()
                self._on_focused_selection_changed.subject = self.focused_list
                self._on_focused_selection_changed()
        return

    def _select_hotswap_target(self, list_index=0):
        if list_index < len(self._lists):
            l = self._lists[list_index]
            l.access_all = True
            children = l.items
            i = index_if(lambda i: i.is_selected, children)
            if i < len(children):
                self._focused_list_index = list_index
                l.selected_index = i
                self._replace_preview_list()
                self._select_hotswap_target(list_index + 1)

    @property
    def num_preview_items(self):
        if self._expanded:
            return self.NUM_ITEMS_PER_COLUMN * self.NUM_COLUMNS_IN_EXPANDED_LIST
        return 6

    def update(self):
        super(BrowserComponent, self).update()
        self._invalidate_content_cache()
        if self.is_enabled():
            self._update_root_items()
            self._update_context()
            self._update_list_offset()
            self._update_load_neighbour_overlay_visibility()
            self._update_navigation_buttons()
            self.expanded = False
            self._update_list_offset()
        else:
            self._stop_prehear()
            self.list_offset = 0

    def _wrap_item(self, item):
        if item.is_device:
            return self._wrap_device_item(item)
        if self._is_hotswap_target_plugin(item):
            return self._wrap_hotswapped_plugin_item(item)
        return item

    def _wrap_device_item(self, item):
        u"""
        Create virtual folder around items that can be loaded AND have children, to avoid
        having two actions on an item (open and load).
        """
        wrapped_loadable = WrappedLoadableBrowserItem(name=item.name, is_loadable=True, contained_item=item)
        return FolderBrowserItem(name=item.name, is_loadable=True, is_device=True, contained_item=item, wrapped_loadable=wrapped_loadable, icon='browser_arrowcontent.svg')

    def _is_hotswap_target_plugin(self, item):
        return isinstance(self._browser.hotswap_target, Live.PluginDevice.PluginDevice) and isinstance(item, Live.Browser.BrowserItem) and self._browser.relation_to_hotswap_target(item) == Live.Browser.Relation.equal

    def _wrap_hotswapped_plugin_item(self, item):
        return PluginBrowserItem(name=item.name, vst_device=self._browser.hotswap_target)
class NoteEditorComponent(Component):
    __events__ = ('page_length', 'active_note_regions', 'active_steps',
                  'notes_changed', 'modify_all_notes')
    matrix = control_matrix(PadControl,
                            channel=PLAYHEAD_FEEDBACK_CHANNELS[0],
                            sensitivity_profile=b'loop',
                            mode=PlayableControl.Mode.listenable)
    mute_button = ButtonControl(color=b'DefaultButton.Transparent')

    def __init__(self,
                 clip_creator=None,
                 grid_resolution=None,
                 skin_base_key=b'NoteEditor',
                 velocity_range_thresholds=None,
                 velocity_provider=None,
                 get_notes_handler=get_single_note,
                 remove_notes_handler=remove_single_note,
                 duplicate_all_notes=False,
                 *a,
                 **k):
        assert skin_base_key is not None
        super(NoteEditorComponent, self).__init__(*a, **k)
        self._duplicate_all_notes = duplicate_all_notes
        self._get_notes_handler = get_notes_handler
        self._remove_notes_handler = remove_notes_handler
        self._skin_base_key = skin_base_key
        self.full_velocity = False
        self._provided_velocity = None
        self._selected_page_point = 0
        self._page_index = 0
        self._clip_creator = clip_creator
        self._sequencer_clip = None
        self._step_colors = []
        self._pressed_steps = []
        self._modified_steps = []
        self._pressed_step_callback = None
        self._modify_task = self._tasks.add(task.run(self._do_modification))
        self._modify_task.kill()
        self._modify_all_notes_enabled = False
        self._step_tap_tasks = {}
        self._clip_notes = []
        self._pitches = [DEFAULT_START_NOTE]
        self._grid_resolution = grid_resolution
        self.__on_resolution_changed.subject = self._grid_resolution
        self.set_step_duplicator(None)
        self._nudge_offset = 0
        self._length_offset = 0
        self._velocity_offset = 0
        self._triplet_factor = 1.0
        self._update_from_grid()
        self.background_color = self._skin_base_key + b'.StepEmpty'
        self._velocity_range_thresholds = velocity_range_thresholds or DEFAULT_VELOCITY_RANGE_THRESHOLDS
        self._velocity_provider = velocity_provider or NullVelocityProvider()
        self.__on_provided_velocity_changed.subject = self._velocity_provider
        return

    @property
    def page_index(self):
        return self._page_index

    @property
    def page_length(self):
        return self._get_step_count() * self._get_step_length(
        ) * self._triplet_factor

    @property
    def can_change_page(self):
        return not self._pressed_steps and not self._modified_steps

    @property
    def notes_in_selected_step(self):
        time_steps = []
        if self._modify_all_notes_enabled:
            time_steps = [TimeStep(0.0, ONE_YEAR_AT_120BPM_IN_BEATS)]
        else:
            time_steps = [
                self._time_step(self.get_step_start_time(start_time)) for
                start_time in chain(self._pressed_steps, self._modified_steps)
            ]
        time_steps_with_notes = [
            time_step.filter_notes(self._clip_notes)
            for time_step in time_steps
        ]
        notes_in_step = []
        for time_step in time_steps_with_notes:
            for note in time_step:
                notes_in_step.append(note_pitch(note))

        return notes_in_step

    def set_selected_page_point(self, point):
        assert self.can_change_page
        self._selected_page_point = point
        index = int(point / self.page_length) if self.page_length != 0 else 0
        if index != self._page_index:
            self._page_index = index
            self._on_clip_notes_changed()

    def _get_modify_all_notes_enabled(self):
        return self._modify_all_notes_enabled

    def _set_modify_all_notes_enabled(self, enabled):
        if enabled != self._modify_all_notes_enabled:
            self._modify_all_notes_enabled = enabled
            self._on_clip_notes_changed()
            self.notify_modify_all_notes()

    modify_all_notes_enabled = property(_get_modify_all_notes_enabled,
                                        _set_modify_all_notes_enabled)

    def set_detail_clip(self, clip):
        if liveobj_changed(clip, self._sequencer_clip):
            self._sequencer_clip = clip
            self._step_duplicator.set_clip(clip)
            if self.can_change_page:
                self.set_selected_page_point(0)
            self._on_clip_notes_changed.subject = clip
            self._on_clip_notes_changed()

    def _can_edit(self):
        return len(self._pitches) != 0

    def _get_editing_notes(self):
        return self._pitches

    def _set_editing_notes(self, pitches):
        self._pitches = pitches
        enabled = self._can_edit()
        for button in self.matrix:
            button.enabled = enabled

        if enabled:
            self._on_clip_notes_changed()

    editing_notes = property(_get_editing_notes, _set_editing_notes)

    def _get_width(self):
        if self.matrix.width:
            return self.matrix.width
        return 4

    def _get_height(self):
        if self.matrix.height:
            return self.matrix.height
        return 4

    def set_matrix(self, matrix):
        last_page_length = self.page_length
        self.matrix.set_control_element(matrix)
        for t in self._step_tap_tasks.itervalues():
            t.kill()

        def trigger_modification_task(x, y):
            trigger = partial(self._trigger_modification, (x, y), done=True)
            return self._tasks.add(
                task.sequence(task.wait(defaults.MOMENTARY_DELAY),
                              task.run(trigger))).kill()

        self._step_tap_tasks = dict([
            ((x, y), trigger_modification_task(x, y)) for x, y in product(
                xrange(self._get_width()), xrange(self._get_height()))
        ])
        if matrix and last_page_length != self.page_length:
            self._on_clip_notes_changed()
            self.notify_page_length()
        else:
            self._update_editor_matrix()

    def set_step_duplicator(self, duplicator):
        self._step_duplicator = duplicator or NullStepDuplicator()
        self._step_duplicator.set_clip(self._sequencer_clip)

    def update(self):
        super(NoteEditorComponent, self).update()
        self._update_editor_matrix_leds()

    def _get_clip_notes_time_range(self):
        if self._modify_all_notes_enabled:
            time_length = self._get_step_count() * 4.0
            time_start = 0
        else:
            time_length = self.page_length
            time_start = self._page_index * time_length
        return (time_start - self._time_step(0).offset, time_length)

    @listens(b'notes')
    def _on_clip_notes_changed(self):
        """ get notes from clip for offline array """
        if liveobj_valid(self._sequencer_clip) and self._can_edit():
            time_start, time_length = self._get_clip_notes_time_range()
            self._clip_notes = self._get_notes_handler(self._sequencer_clip,
                                                       time_start,
                                                       self._pitches,
                                                       time_length)
        else:
            self._clip_notes = []
        self._update_editor_matrix()
        self.notify_notes_changed()

    def _update_editor_matrix(self):
        """
        update offline array of button LED values, based on note
        velocity and mute states
        """
        step_colors = [self._skin_base_key + b'.StepDisabled'
                       ] * self._get_step_count()

        def coords_to_index(coord):
            return coord[0] + coord[1] * self._get_width()

        editing_indices = set(map(coords_to_index, self._modified_steps))
        selected_indices = set(map(coords_to_index, self._pressed_steps))
        last_editing_notes = []
        for time_step, index in self._visible_steps():
            notes = time_step.filter_notes(self._clip_notes)
            if len(notes) > 0:
                last_editing_notes = []
                if index in selected_indices:
                    color = self._skin_base_key + b'.StepSelected'
                elif index in editing_indices:
                    note_color = self._determine_color(notes)
                    color = self._skin_base_key + b'.StepEditing.' + note_color
                    last_editing_notes = notes
                else:
                    note_color = self._determine_color(notes)
                    color = self._skin_base_key + b'.Step.' + note_color
            elif any(imap(time_step.overlaps_note, last_editing_notes)):
                color = self._skin_base_key + b'.StepEditing.' + note_color
            elif index in editing_indices or index in selected_indices:
                color = self._skin_base_key + b'.StepSelected'
                last_editing_notes = []
            else:
                color = self.background_color
                last_editing_notes = []
            step_colors[index] = color

        self._step_colors = step_colors
        self._update_editor_matrix_leds()

    def _determine_color(self, notes):
        return color_for_note(
            most_significant_note(notes),
            velocity_range_thresholds=self._velocity_range_thresholds)

    def _visible_steps(self):
        first_time = self.page_length * self._page_index
        steps_per_page = self._get_step_count()
        step_length = self._get_step_length()
        indices = range(steps_per_page)
        if is_triplet_quantization(self._triplet_factor):
            indices = filter(lambda k: k % 8 not in (6, 7), indices)
        return [(self._time_step(first_time + k * step_length), index)
                for k, index in enumerate(indices)]

    def _update_editor_matrix_leds(self):
        if self.is_enabled():
            for control in self.matrix:
                control.color = self._step_colors[control.index]

    def _get_step_count(self):
        return self._get_width() * self._get_height()

    def get_step_start_time(self, step):
        x, y = step
        assert in_range(x, 0, self._get_width())
        assert in_range(y, 0, self._get_height())
        page_time = self._page_index * self._get_step_count(
        ) * self._triplet_factor
        step_time = x + y * self._get_width() * self._triplet_factor
        return (page_time + step_time) * self._get_step_length()

    def _get_step_length(self):
        return self._grid_resolution.step_length

    def get_row_start_times(self):
        return [
            self.get_step_start_time((0, row))
            for row in xrange(self._get_height())
        ]

    def _update_from_grid(self):
        quantization, is_triplet = self._grid_resolution.clip_grid
        self._triplet_factor = 1.0 if not is_triplet else 0.75
        if self._clip_creator:
            self._clip_creator.grid_quantization = quantization
            self._clip_creator.is_grid_triplet = is_triplet
        if liveobj_valid(self._sequencer_clip):
            self._sequencer_clip.view.grid_quantization = quantization
            self._sequencer_clip.view.grid_is_triplet = is_triplet

    @mute_button.pressed
    def mute_button(self, button):
        self._trigger_modification(immediate=True)

    @listens(b'index')
    def __on_resolution_changed(self, *a):
        self._release_active_steps()
        self._update_from_grid()
        self.set_selected_page_point(self._selected_page_point)
        self.notify_page_length()
        self._on_clip_notes_changed()

    @matrix.pressed
    def matrix(self, button):
        self._on_pad_pressed(button.coordinate)

    @matrix.released
    def matrix(self, button):
        self._on_pad_released(button.coordinate)

    def _on_pad_pressed(self, coordinate):
        y, x = coordinate
        if self.is_enabled():
            if not liveobj_valid(self._sequencer_clip):
                self.set_detail_clip(
                    create_clip_in_selected_slot(self._clip_creator,
                                                 self.song))
            if self._can_press_or_release_step(x, y):
                self._on_press_step((x, y))
                self._update_editor_matrix()

    def _on_pad_released(self, coordinate):
        y, x = coordinate
        if self.is_enabled() and self._can_press_or_release_step(x, y):
            self._on_release_step((x, y))
            self._update_editor_matrix()

    def _can_press_or_release_step(self, x, y):
        width = self._get_width(
        ) * self._triplet_factor if is_triplet_quantization(
            self._triplet_factor) else self._get_width()
        return x < width and y < self._get_height()

    @listens(b'velocity')
    def __on_provided_velocity_changed(self):
        if len(self._pressed_steps) + len(self._modified_steps) > 0:
            self._provided_velocity = self._velocity_provider.velocity
            self._trigger_modification(immediate=True)
            self._provided_velocity = None
        return

    def _get_step_time_range(self, step):
        time = self.get_step_start_time(step)
        return (time, time + self._get_step_length())

    @property
    def editing_note_regions(self):
        active_time_spans = self.active_time_spans
        editing_notes = set(self.notes_in_selected_step)
        if len(editing_notes) > 1:
            editing_notes = [-1]
        return list(product(editing_notes, list(active_time_spans)))

    @property
    def active_time_spans(self):
        if self._modify_all_notes_enabled:
            return [(0.0, ONE_YEAR_AT_120BPM_IN_BEATS)]
        else:

            def time_span(step):
                time_step = self._time_step(self.get_step_start_time(step))
                return (time_step.left_boundary(), time_step.right_boundary())

            return imap(time_span,
                        chain(self._pressed_steps, self._modified_steps))

    @property
    def active_steps(self):
        return imap(self._get_step_time_range,
                    chain(self._pressed_steps, self._modified_steps))

    @property
    def active_note_regions(self):
        return imap(self._get_time_range,
                    chain(self._pressed_steps, self._modified_steps))

    def _get_time_range(self, step):
        def note_compare(left, right):
            return note_start_time(left) - note_start_time(right)

        time = self.get_step_start_time(step)
        notes = self._time_step(time).filter_notes(self._clip_notes)
        if notes:
            beginning_note = first(sorted(notes, key=cmp_to_key(note_compare)))
            start = note_start_time(beginning_note)
            end = start + note_length(beginning_note)
            if len(notes) > 1:
                end_note = notes[(-1)]
                end = note_start_time(end_note) + note_length(end_note)
            return (start, end)
        else:
            return (time, time + self._get_step_length())

    def _release_active_steps(self):
        for step in self._pressed_steps + self._modified_steps:
            self._on_release_step(step, do_delete_notes=False)

    def _on_release_step(self, step, do_delete_notes=True):
        self._step_tap_tasks[step].kill()
        if step in self._pressed_steps:
            if do_delete_notes:
                self._delete_notes_in_step(step)
            self._pressed_steps.remove(step)
            for pitch in self._pitches:
                self._add_or_modify_note_in_step(step, pitch)

        if step in self._modified_steps:
            self._modified_steps.remove(step)
        if len(self._modified_steps) + len(self._pressed_steps) == 0:
            self._velocity_provider.set_velocities_playable(True)
        self.notify_active_steps()
        self.notify_active_note_regions()

    def _find_continued_step(self, step):
        def steps_to_note_start(steps):
            return map(lambda step: self.get_step_start_time(step), steps)

        time = self.get_step_start_time(step)
        all_steps_with_notes = [
            time_step for time_step, index in self._visible_steps()
            if time_step.filter_notes(self._clip_notes)
        ]
        all_time_step_starts = map(lambda ts: ts.start, all_steps_with_notes)
        if all_time_step_starts:
            insert_point = bisect(all_time_step_starts, time)
            if insert_point > 0:
                prev_filled_step = all_steps_with_notes[(insert_point - 1)]
                notes_in_modified_steps = steps_to_note_start(
                    self._modified_steps)
                if prev_filled_step.start in notes_in_modified_steps:
                    return prev_filled_step
        return

    def _add_step_to_duplicator(self, step):
        nudge_offset = 0
        time = self.get_step_start_time(step)
        notes = self._time_step(time).filter_notes(self._clip_notes)
        step_start, step_end = self._get_time_range(step)
        if notes:
            nudge_offset = min(imap(note_start_time, notes)) - time
        if self._duplicate_all_notes:
            self._step_duplicator.add_step(step_start, step_end, nudge_offset)
        else:
            self._step_duplicator.add_step_with_pitch(self.editing_notes[0],
                                                      step_start, step_end,
                                                      nudge_offset)

    def _on_press_step(self, step):
        if liveobj_valid(
                self._sequencer_clip
        ) and step not in self._pressed_steps and step not in self._modified_steps:
            if self._step_duplicator.is_duplicating:
                self._add_step_to_duplicator(step)
            else:
                self._step_tap_tasks[step].restart()
                continued_step = self._find_continued_step(step)
                if continued_step:
                    self._modify_length_of_notes_within_existing_step(
                        continued_step, step)
                else:
                    self._pressed_steps.append(step)
                self._velocity_provider.set_velocities_playable(False)
        self.notify_active_steps()
        self.notify_active_note_regions()

    def _modify_length_of_notes_within_existing_step(self, existing_time_step,
                                                     new_step):
        notes_in_step = existing_time_step.filter_notes(self._clip_notes)
        new_end = float(
            self.get_step_start_time(new_step) + self._get_step_length())
        step_mute = all(map(lambda note: note_muted(note), notes_in_step))
        new_notes = []
        for note in self._clip_notes:
            length_offset = new_end - (
                note_length(note) +
                note_start_time(note)) if note in notes_in_step else 0
            new_notes.append(
                self._modify_single_note(step_mute, existing_time_step,
                                         length_offset, note))

        self._replace_notes(tuple(new_notes))

    def _time_step(self, time):
        return TimeStep(time, self._get_step_length())

    def _get_notes_info_from_step(self, step):
        time = self.get_step_start_time(step)
        notes = self._time_step(time).filter_notes(self._clip_notes)
        pitches = [note_pitch(note) for note in notes]
        return (time, notes, pitches)

    def toggle_pitch_for_all_modified_steps(self, pitch):
        assert liveobj_valid(self._sequencer_clip)
        for step in set(chain(self._pressed_steps, self._modified_steps)):
            time, notes, pitches = self._get_notes_info_from_step(step)
            if pitch not in pitches:
                self._add_new_note_in_step(step, pitch, time)
            else:
                time_step = self._time_step(self.get_step_start_time(step))
                for time, length in time_step.connected_time_ranges():
                    remove_single_note(self._sequencer_clip, time, (pitch, ),
                                       length)

    def _add_new_note_in_step(self, step, pitch, time):
        mute = self.mute_button.is_pressed
        velocity = 127 if self.full_velocity else self._velocity_provider.velocity
        note = (pitch, time, self._get_step_length(), velocity, mute)
        self._sequencer_clip.set_notes((note, ))
        self._sequencer_clip.deselect_all_notes()
        self._trigger_modification(step, done=True)

    def _add_or_modify_note_in_step(self, step, pitch, modify_existing=True):
        """
        Add note in given step if there are none in there, otherwise
        select the step for potential deletion or modification
        """
        if liveobj_valid(self._sequencer_clip):
            time, notes, pitches = self._get_notes_info_from_step(step)
            if notes:
                if pitch in pitches and modify_existing:
                    most_significant_velocity = most_significant_note(notes)[3]
                    if self.mute_button.is_pressed or most_significant_velocity != 127 and self.full_velocity:
                        self._trigger_modification(step, immediate=True)
            else:
                self._add_new_note_in_step(step, pitch, time)
                return True
        return False

    def _delete_notes_in_step(self, step):
        """ Delete all notes in the given step """
        if liveobj_valid(self._sequencer_clip) and self._can_edit():
            time_step = self._time_step(self.get_step_start_time(step))
            for time, length in time_step.connected_time_ranges():
                self._remove_notes_handler(self._sequencer_clip, time,
                                           self._pitches, length)

    def set_nudge_offset(self, value):
        self._modify_note_property(b'_nudge_offset', value)

    def set_length_offset(self, value):
        self._modify_note_property(b'_length_offset', value)

    def set_velocity_offset(self, value):
        self._modify_note_property(b'_velocity_offset', value)

    def _modify_note_property(self, note_property, value):
        if self.is_enabled():
            with self._full_velocity_context(False):
                setattr(self, note_property,
                        getattr(self, note_property) + value)
                self._trigger_modification(immediate=True)

    @contextmanager
    def _full_velocity_context(self, desired_full_velocity_state):
        saved_velocity = self.full_velocity
        self.full_velocity = desired_full_velocity_state
        yield
        self.full_velocity = saved_velocity

    def set_full_velocity(self):
        if self.is_enabled():
            with self._full_velocity_context(True):
                self._trigger_modification()

    def notify_modification(self):
        """
        Tell the note editor about changes to pressed steps, so further modifications
        by the note editor are ignored.
        """
        self._trigger_modification(done=True)

    def _trigger_modification(self, step=None, done=False, immediate=False):
        """
        Because the modification of notes is slow, we
        accumulate modification events and perform all of them
        alltogether in a task. Call this function whenever a given set
        of steps (or potentially all steps) are to be modified.

        If done=True, we just notify that the given steps have been
        modified without actually executing the task.
        """
        needs_update = False
        if step is None:
            needs_update = bool(self._pressed_steps)
            self._modified_steps += self._pressed_steps
            self._pressed_steps = []
        else:
            if step not in self._modified_steps:
                self._modified_steps.append(step)
            if step in self._pressed_steps:
                self._pressed_steps.remove(step)
            needs_update = True
        if not done:
            if immediate:
                self._do_modification()
                self._modify_task.kill()
            elif self._modify_task.is_killed:
                self._do_modification()
                self._modify_task.restart()
        if needs_update:
            self._update_editor_matrix()
        return

    def _reset_modifications(self):
        self._velocity_offset = 0
        self._length_offset = 0
        self._nudge_offset = 0

    def _do_modification(self):
        if self._modify_all_notes_enabled:
            new_notes = self._modify_all_notes()
            self._replace_notes(new_notes)
        elif self._modified_steps:
            notes_added = map(
                lambda step_and_pitch: self._add_or_modify_note_in_step(
                    modify_existing=False, *step_and_pitch),
                product(self._modified_steps, self._pitches))
            if any(notes_added):
                self._modify_task.restart()
            else:
                new_notes = self._modify_step_notes(self._modified_steps)
                self._replace_notes(new_notes)
        self._reset_modifications()

    def _replace_notes(self, new_notes):
        if new_notes != self._clip_notes and self._can_edit():
            time_start, time_length = self._get_clip_notes_time_range()
            self._remove_notes_handler(self._sequencer_clip, time_start,
                                       self._pitches, time_length)
            self._sequencer_clip.set_notes(tuple(new_notes))
            self._sequencer_clip.deselect_all_notes()

    def _modify_all_notes(self):
        """ modify all notes in the current pitch """
        return self._modify_notes_in_time(TimeStep(0.0, MAX_CLIP_LENGTH),
                                          self._clip_notes,
                                          self._length_offset)

    def _limited_nudge_offset(self, steps, notes, nudge_offset):
        limited_nudge_offset = MAX_CLIP_LENGTH
        for step in steps:
            time_step = self._time_step(self.get_step_start_time(step))
            for note in time_step.filter_notes(notes):
                start_time = note_start_time(note)
                time_after_nudge = time_step.clamp(start_time, nudge_offset)
                limited_nudge_offset = min(limited_nudge_offset,
                                           abs(start_time - time_after_nudge))

        return sign(nudge_offset) * limited_nudge_offset

    def _modify_step_notes(self, steps):
        """ Return a new list with all notes within steps modified. """
        notes = self._clip_notes
        self._nudge_offset = self._limited_nudge_offset(
            steps, notes, self._nudge_offset)
        for step in steps:
            time_step = self._time_step(self.get_step_start_time(step))
            notes = self._modify_notes_in_time(time_step, notes,
                                               self._length_offset)

        return notes

    def _modify_notes_in_time(self, time_step, notes, length_offset):
        step_notes = time_step.filter_notes(self._clip_notes)
        step_mute = all(map(lambda note: note_muted(note), step_notes))
        return map(
            partial(self._modify_single_note, step_mute, time_step,
                    length_offset), notes)

    def _modify_single_note(self, step_mute, time_step, length_offset, note):
        """
        Return a modified version of the passed in note taking into
        account current modifiers. If the note is not within
        the given step, returns the note as-is.

        If the time_step is inside a loop, the last part of the loop
        is considered as being the same as the part just before the
        loop, so the resulting note may, in this case, jump between
        the beginning and the end.
        """
        pitch, time, length, velocity, mute = note
        if time_step.includes_time(time):
            time = time_step.clamp(time, self._nudge_offset)
            if length_offset <= -time_step.length and length + length_offset < time_step.length:
                if length > time_step.length:
                    length = time_step.length
            else:
                length = max(0, length + length_offset)
            if self._provided_velocity:
                velocity = self._provided_velocity
            elif self.full_velocity:
                velocity = 127
            else:
                velocity = clamp(velocity + self._velocity_offset, 1, 127)
            mute = not step_mute if self.mute_button.is_pressed else mute
        return (pitch, time, length, velocity, mute)

    def get_min_max_note_values(self):
        if self._modify_all_notes_enabled and len(self._clip_notes) > 0:
            return min_max_for_notes(self._clip_notes, 0.0)
        else:
            if len(self._pressed_steps) + len(self._modified_steps) > 0:
                min_max_values = None
                for step in chain(self._modified_steps, self._pressed_steps):
                    start_time = self.get_step_start_time(step)
                    min_max_values = min_max_for_notes(
                        self._time_step(start_time).filter_notes(
                            self._clip_notes), start_time, min_max_values)

                return min_max_values
            return
예제 #14
0
class ConvertEnabler(Component):
    convert_toggle_button = ButtonControl(color='DefaultButton.On')

    def __init__(self, enter_dialog_mode=None, exit_dialog_mode=None, *a, **k):
        assert enter_dialog_mode is not None
        assert exit_dialog_mode is not None
        super(ConvertEnabler, self).__init__(*a, **k)
        self._enter_dialog_mode = partial(enter_dialog_mode, 'convert')
        self._exit_dialog_mode = partial(exit_dialog_mode, 'convert')
        self._selected_item = self.register_disconnectable(
            SelectedMixerTrackProvider(song=self.song))
        self.__on_selected_item_changed.subject = self._selected_item
        self.__on_selected_item_changed(None)
        song = self.song
        self.__on_devices_changed.subject = song.view
        self.__on_selected_scene_changed.subject = song.view
        self.__on_detail_clip_updated.subject = song.view
        self._update_clip_slot_listener()
        self._update_drum_pad_listeners()
        return

    @listens('selected_mixer_track')
    def __on_selected_item_changed(self, _):
        self._update_clip_slot_listener()
        self._disable_and_check_enabled_state()

    @convert_toggle_button.pressed
    def convert_toggle_button(self, button):
        self._enter_dialog_mode()

    def _can_enable_mode(self):
        category = possible_conversions(
            self._selected_item.selected_mixer_track)
        category.disconnect()
        return len(category.actions) > 0

    def _disable_and_check_enabled_state(self):
        self._exit_dialog_mode()
        self.convert_toggle_button.enabled = self._can_enable_mode()

    @listens('detail_clip')
    def __on_detail_clip_updated(self):
        self._disable_and_check_enabled_state()

    @listens('selected_track.devices')
    def __on_devices_changed(self):
        self._disable_and_check_enabled_state()
        self._update_drum_pad_listeners()

    def _update_drum_pad_listeners(self):
        drum_rack = find_drum_rack_instrument(
            self._selected_item.selected_mixer_track)
        drum_rack_view_or_none = drum_rack.view if liveobj_valid(
            drum_rack) else None
        self.__on_selected_drum_pad_changed.subject = drum_rack_view_or_none
        self.__on_drum_pad_chains_changed.subject = drum_rack_view_or_none
        return

    @listens('selected_drum_pad')
    def __on_selected_drum_pad_changed(self):
        self._disable_and_check_enabled_state()
        drum_rack_view = self.__on_selected_drum_pad_changed.subject
        if liveobj_valid(drum_rack_view):
            selected_drum_pad = drum_rack_view.selected_drum_pad
            first_chain_or_none = None
            if liveobj_valid(selected_drum_pad):
                first_chain_or_none = selected_drum_pad.chains[0] if len(
                    selected_drum_pad.chains) > 0 else None
            self.__on_drum_pad_chain_devices_changed.subject = first_chain_or_none
        return

    @listens('selected_drum_pad.chains')
    def __on_drum_pad_chains_changed(self):
        self._disable_and_check_enabled_state()

    @listens('devices')
    def __on_drum_pad_chain_devices_changed(self):
        self._disable_and_check_enabled_state()

    @listens('selected_scene')
    def __on_selected_scene_changed(self):
        self._update_clip_slot_listener()
        self._disable_and_check_enabled_state()

    def _update_clip_slot_listener(self):
        clip_slot = self.song.view.highlighted_clip_slot
        self.__on_clip_slot_has_clip_changed.subject = clip_slot

    @listens('has_clip')
    def __on_clip_slot_has_clip_changed(self):
        self._disable_and_check_enabled_state()
        clip_slot = self.__on_clip_slot_has_clip_changed.subject
        self._update_clip_listeners(clip_slot)

    def _update_clip_listeners(self, clip_slot):
        self.__on_clip_playing_status_changed.subject = clip_slot.clip
        self.__on_clip_recording_status_changed.subject = clip_slot.clip

    @listens('is_recording')
    def __on_clip_recording_status_changed(self):
        self._disable_and_check_enabled_state()

    @listens('playing_status')
    def __on_clip_playing_status_changed(self):
        self._disable_and_check_enabled_state()
class GlobalMixerActionComponent(Component):
    action_button = ButtonControl(delay_time=GLOBAL_ACTION_LOCK_MODE_DELAY)

    def __init__(self,
                 track_list_component=None,
                 mode=None,
                 immediate_action=None,
                 default_color_indicator=None,
                 mode_locked_color=None,
                 mode_active_color=None,
                 *a,
                 **k):
        assert track_list_component is not None
        assert mode is not None
        assert mode in track_list_component.modes
        super(GlobalMixerActionComponent, self).__init__(*a, **k)
        self._mode = mode
        self._immediate_action = immediate_action
        self._mode_locked = False
        self._default_color_indicator = None
        self._locked_color = mode_locked_color
        self._active_color = mode_active_color if mode_active_color is not None else 'DefaultButton.On'
        self._allow_released_immediately_action = True
        self._track_list_component = track_list_component
        if default_color_indicator is not None:
            self._default_color_indicator = self.register_disconnectable(
                default_color_indicator)
            self.__on_default_color_changed.subject = default_color_indicator
        self._update_default_color()
        return

    @listenable_property
    def mode_locked(self):
        return self._mode_locked

    @property
    def mode(self):
        return self._mode

    def cancel_release_action(self):
        self._allow_released_immediately_action = False

    def cancel_locked_mode(self):
        self._track_list_component.pop_mode(self._mode)
        self._mode_locked = False
        self._update_default_color()

    @listens('color')
    def __on_default_color_changed(self, _):
        self._update_default_color()

    @action_button.released_immediately
    def action_button(self, button):
        if self._allow_released_immediately_action:
            self._immediate_action()

    @action_button.pressed
    def action_button(self, button):
        if self._mode_locked:
            self._allow_released_immediately_action = False
            self._unlock_mode()
        else:
            self._allow_released_immediately_action = True
            self._track_list_component.push_mode(self._mode)
            self.action_button.color = self._active_color

    @action_button.pressed_delayed
    def action_button(self, button):
        if self._allow_released_immediately_action:
            self._lock_mode()

    @action_button.released
    def action_button(self, button):
        if not self._mode_locked:
            self._track_list_component.pop_mode(self._mode)
            self._update_default_color()

    def _lock_mode(self):
        self._mode_locked = True
        self.action_button.color = self._locked_color
        self.notify_mode_locked(self._mode_locked)

    def _unlock_mode(self):
        self.cancel_locked_mode()
        self.notify_mode_locked(self._mode_locked)

    def _update_default_color(self):
        if self._mode not in self._track_list_component.active_modes:
            self.action_button.color = self._default_color_indicator.color if self._default_color_indicator else 'DefaultButton.On'
예제 #16
0
class InstrumentScalesComponent(Component):
    presets_toggle_button = ButtonControl(color='DefaultButton.Off',
                                          pressed_color='DefaultButton.On')

    def __init__(self, note_layout=None, *a, **k):
        assert note_layout is not None
        super(InstrumentScalesComponent, self).__init__(*a, **k)
        self._note_layout = note_layout
        self._key_center_buttons = []
        self._encoder_touch_button_slots = self.register_disconnectable(
            EventObject())
        self._encoder_touch_buttons = []
        self._top_key_center_buttons = None
        self._bottom_key_center_buttons = None
        self._absolute_relative_button = None
        self._diatonic_chromatic_button = None
        self._info_sources = map(DisplayDataSource,
                                 (u'Scale selection:', u'', u''))
        self._line_sources = recursive_map(
            DisplayDataSource, ((u'', u'', u'', u'', u'', u'', u''),
                                (u'', u'', u'', u'', u'', u'', u'')))
        self._scale_sources = map(
            partial(DisplayDataSource, adjust_string_fn=adjust_string_crop),
            (u'', u'', u'', u''))
        self._presets = InstrumentPresetsComponent(self._note_layout,
                                                   is_enabled=False,
                                                   parent=self)
        self._presets.selected_mode = 'scale_p4_vertical'
        self._scale_list = ListComponent(parent=self,
                                         data_sources=self._scale_sources)
        self._scale_list.scrollable_list.fixed_offset = 1
        self._scale_list.scrollable_list.assign_items(SCALES)
        self._scale_list.scrollable_list.select_item_index_with_offset(
            list(SCALES).index(self._note_layout.scale), 1)
        self._on_selected_scale.subject = self._scale_list.scrollable_list
        self._update_data_sources()
        return

    presets_layer = forward_property('_presets')('layer')

    @property
    def available_scales(self):
        return self._note_layout.scale.scale_for_notes(ROOT_NOTES)

    def set_scale_line1(self, display):
        self._set_scale_line(display, 0)

    def set_scale_line2(self, display):
        self._set_scale_line(display, 1)

    def set_scale_line3(self, display):
        self._set_scale_line(display, 2)

    def set_scale_line4(self, display):
        self._set_scale_line(display, 3)

    def _set_scale_line(self, display, index):
        if display:
            display.set_data_sources([self._scale_sources[index]])
            for segment in display.segments:
                segment.separator = ''

    def set_info_line(self, display):
        if display:
            display.set_data_sources(self._info_sources)

    def set_top_display_line(self, display):
        self._set_display_line(display, 0)

    def set_bottom_display_line(self, display):
        self._set_display_line(display, 1)

    def _set_display_line(self, display, line):
        if display:
            display.set_data_sources(self._line_sources[line])

    @presets_toggle_button.pressed
    def presets_toggle_button(self, button):
        self._presets.set_enabled(True)

    @presets_toggle_button.released
    def presets_toggle_button(self, button):
        self._presets.set_enabled(False)

    def set_top_buttons(self, buttons):
        if buttons:
            buttons.reset()
            self.set_absolute_relative_button(buttons[7])
            self._top_key_center_buttons = buttons[1:7]
            self.set_scale_up_button(buttons[0])
        else:
            self.set_absolute_relative_button(None)
            self._top_key_center_buttons = None
            self.set_scale_up_button(None)
        if self._top_key_center_buttons and self._bottom_key_center_buttons:
            self.set_key_center_buttons(self._top_key_center_buttons +
                                        self._bottom_key_center_buttons)
        else:
            self.set_key_center_buttons(tuple())
        return

    def set_bottom_buttons(self, buttons):
        if buttons:
            buttons.reset()
            self.set_diatonic_chromatic_button(buttons[7])
            self._bottom_key_center_buttons = buttons[1:7]
            self.set_scale_down_button(buttons[0])
        else:
            self.set_diatonic_chromatic_button(None)
            self._bottom_key_center_buttons = None
            self.set_scale_down_button(None)
        if self._top_key_center_buttons and self._bottom_key_center_buttons:
            self.set_key_center_buttons(self._top_key_center_buttons +
                                        self._bottom_key_center_buttons)
        else:
            self.set_key_center_buttons([])
        return

    def set_scale_down_button(self, button):
        self._scale_list.select_next_button.set_control_element(button)

    def set_scale_up_button(self, button):
        self._scale_list.select_prev_button.set_control_element(button)

    def set_encoder_controls(self, encoders):
        self._scale_list.encoders.set_control_element(
            [encoders[0]] if encoders else [])

    def set_key_center_buttons(self, buttons):
        assert not buttons or len(buttons) == 12
        buttons = buttons or []
        self._key_center_buttons = buttons
        self._on_key_center_button_value.replace_subjects(buttons)
        self._update_key_center_buttons()

    def set_absolute_relative_button(self, absolute_relative_button):
        self._absolute_relative_button = absolute_relative_button
        self._on_absolute_relative_value.subject = absolute_relative_button
        self._update_absolute_relative_button()

    def set_diatonic_chromatic_button(self, diatonic_chromatic_button):
        self._diatonic_chromatic_button = diatonic_chromatic_button
        self._on_diatonic_chromatic_value.subject = diatonic_chromatic_button
        self._update_diatonic_chromatic_button()

    @listens_group('value')
    def _on_key_center_button_value(self, value, sender):
        if self.is_enabled() and (value or not sender.is_momentary()):
            index = list(self._key_center_buttons).index(sender)
            self._note_layout.root_note = ROOT_NOTES[index]
            self._update_key_center_buttons()
            self._update_data_sources()

    @listens('value')
    def _on_absolute_relative_value(self, value):
        if self.is_enabled():
            if value != 0 or not self._absolute_relative_button.is_momentary():
                self._note_layout.is_fixed = not self._note_layout.is_fixed
                self._update_absolute_relative_button()
                self._update_data_sources()

    @listens('value')
    def _on_diatonic_chromatic_value(self, value):
        if self.is_enabled():
            if value != 0 or not self._diatonic_chromatic_button.is_momentary(
            ):
                self._note_layout.is_in_key = not self._note_layout.is_in_key
                self._update_diatonic_chromatic_button()
                self._update_data_sources()

    @listens('selected_item')
    def _on_selected_scale(self):
        self._note_layout.scale = self._scale_list.scrollable_list.selected_item.content
        self._update_data_sources()

    def update(self):
        super(InstrumentScalesComponent, self).update()
        if self.is_enabled():
            self._update_key_center_buttons()
            self._update_absolute_relative_button()
            self._update_diatonic_chromatic_button()
        else:
            self._presets.set_enabled(False)

    def _update_key_center_buttons(self):
        if self.is_enabled():
            for index, button in enumerate(self._key_center_buttons):
                if button:
                    color = 'Scales.Selected' if self._note_layout.root_note == ROOT_NOTES[
                        index] else 'Scales.Unselected'
                    button.set_light(color)

    def _update_absolute_relative_button(self):
        if self.is_enabled() and self._absolute_relative_button != None:
            color = 'Scales.FixedOn' if self._note_layout.is_fixed else 'Scales.FixedOff'
            self._absolute_relative_button.set_light(color)
        return

    def _update_diatonic_chromatic_button(self):
        if self.is_enabled() and self._diatonic_chromatic_button != None:
            color = 'Scales.Diatonic' if self._note_layout.is_in_key else 'Scales.Chromatic'
            self._diatonic_chromatic_button.set_light(color)
        return

    def _update_data_sources(self):
        key_index = list(ROOT_NOTES).index(self._note_layout.root_note)
        key_sources = self._line_sources[0][:6] + self._line_sources[1][:6]
        key_names = [scale.name for scale in self.available_scales]
        for idx, (source, orig) in enumerate(zip(key_sources, key_names)):
            source.set_display_string('   ' + consts.CHAR_SELECT +
                                      orig if idx == key_index else '    ' +
                                      orig)

        self._line_sources[0][6].set_display_string(
            'Fixed: Y' if self._note_layout.is_fixed else 'Fixed: N')
        self._line_sources[1][6].set_display_string(
            'In Key' if self._note_layout.is_in_key else 'Chromatc')
        self._info_sources[1].set_display_string(
            str(self._scale_list.scrollable_list.selected_item))
예제 #17
0
class LoopSettingsControllerComponent(LoopSettingsControllerComponentBase):
    __events__ = (u'looping', u'loop_parameters', u'zoom', u'clip')
    ZOOM_DEFAULT_SENSITIVITY = MappedSensitivitySettingControl.DEFAULT_SENSITIVITY
    ZOOM_FINE_SENSITIVITY = MappedSensitivitySettingControl.FINE_SENSITIVITY
    zoom_encoder = MappedSensitivitySettingControl()
    zoom_touch_encoder = EncoderControl()
    loop_button = ToggleButtonControl(toggled_color='Clip.Option', untoggled_color='Clip.OptionDisabled')
    crop_button = ButtonControl(color='Clip.Action')

    def __init__(self, *a, **k):
        super(LoopSettingsControllerComponent, self).__init__(*a, **k)
        self._looping_settings = [
         LoopSetting(name=PARAMETERS_LOOPED[0], parent=self._loop_model, source_property='position'),
         LoopSetting(name=PARAMETERS_LOOPED[1], parent=self._loop_model, use_length_conversion=True, source_property='loop_length'),
         LoopSetting(name=PARAMETERS_LOOPED[2], parent=self._loop_model, source_property='start_marker')]
        self._non_looping_settings = [
         LoopSetting(name=PARAMETERS_NOT_LOOPED[0], parent=self._loop_model, source_property='loop_start'),
         LoopSetting(name=PARAMETERS_NOT_LOOPED[1], parent=self._loop_model, source_property='loop_end')]
        for setting in self._looping_settings + self._non_looping_settings:
            self.register_disconnectable(setting)

        self.__on_looping_changed.subject = self._loop_model
        self.__on_looping_changed()

    def update(self):
        super(LoopSettingsControllerComponent, self).update()
        if self.is_enabled():
            self.notify_timeline_navigation()
            self.notify_clip()

    @loop_button.toggled
    def loop_button(self, toggled, button):
        self._loop_model.looping = toggled

    @crop_button.pressed
    def crop_button(self, button):
        if liveobj_valid(self.clip):
            self.clip.crop()

    @property
    def looping(self):
        if self.clip:
            return self._loop_model.looping
        return False

    @property
    def loop_parameters(self):
        if not liveobj_valid(self.clip):
            return []
        parameters = self._looping_settings if self.looping else self._non_looping_settings
        if self.zoom:
            return [self.zoom] + parameters
        return parameters

    @property
    def zoom(self):
        if liveobj_valid(self.clip):
            return getattr(self.clip, 'zoom', None)
        return

    @listenable_property
    def timeline_navigation(self):
        if liveobj_valid(self.clip):
            return getattr(self.clip, 'timeline_navigation', None)
        return

    @listens('is_recording')
    def __on_is_recording_changed(self):
        self._update_recording_state()

    @listens('is_overdubbing')
    def __on_is_overdubbing_changed(self):
        self._update_recording_state()

    def _update_recording_state(self):
        clip = self._loop_model.clip
        if liveobj_valid(clip):
            recording = clip.is_recording and not clip.is_overdubbing
            self._looping_settings[1].recording = recording
            self._non_looping_settings[1].recording = recording

    @listens('looping')
    def __on_looping_changed(self):
        self._update_and_notify()

    def _update_loop_button(self):
        self.loop_button.enabled = liveobj_valid(self.clip)
        if liveobj_valid(self.clip):
            self.loop_button.is_toggled = self._loop_model.looping

    def _on_clip_changed(self):
        if self.timeline_navigation is not None:
            self.timeline_navigation.reset_focus_and_animation()
        self._update_and_notify()
        self.__on_is_recording_changed.subject = self._loop_model.clip
        self.__on_is_overdubbing_changed.subject = self._loop_model.clip
        self._update_recording_state()
        self.crop_button.enabled = liveobj_valid(self.clip) and self.clip.is_midi_clip
        self._connect_encoder()
        if self.is_enabled():
            self.notify_timeline_navigation()
        return

    def _on_clip_start_marker_touched(self):
        if self.timeline_navigation is not None:
            self.timeline_navigation.touch_object(self.timeline_navigation.start_marker_focus)
        return

    def _on_clip_position_touched(self):
        if self.timeline_navigation is not None:
            self.timeline_navigation.touch_object(self.timeline_navigation.loop_start_focus)
        return

    def _on_clip_end_touched(self):
        if self.timeline_navigation is not None:
            self.timeline_navigation.touch_object(self.timeline_navigation.loop_end_focus)
        return

    def _on_clip_start_marker_released(self):
        if self.timeline_navigation is not None:
            self.timeline_navigation.release_object(self.timeline_navigation.start_marker_focus)
        return

    def _on_clip_position_released(self):
        if self.timeline_navigation is not None:
            self.timeline_navigation.release_object(self.timeline_navigation.loop_start_focus)
        return

    def _on_clip_end_released(self):
        if self.timeline_navigation is not None:
            self.timeline_navigation.release_object(self.timeline_navigation.loop_end_focus)
        return

    @zoom_touch_encoder.touched
    def zoom_touch_encoder(self, encoder):
        if self.timeline_navigation is not None:
            self.timeline_navigation.touch_object(self.timeline_navigation.zoom_focus)
        return

    @zoom_touch_encoder.released
    def zoom_touch_encoder(self, encoder):
        if self.timeline_navigation is not None:
            self.timeline_navigation.release_object(self.timeline_navigation.zoom_focus)
        return

    def _update_and_notify(self):
        self._update_loop_button()
        self.notify_looping()
        self.notify_loop_parameters()
        self.notify_zoom()

    def _connect_encoder(self):
        self.zoom_encoder.mapped_parameter = self.zoom
        self.zoom_encoder.update_sensitivities(self.ZOOM_DEFAULT_SENSITIVITY, self.ZOOM_FINE_SENSITIVITY)

    def set_zoom_encoder(self, encoder):
        self.zoom_encoder.set_control_element(encoder)
        self.zoom_touch_encoder.set_control_element(encoder)
        self._connect_encoder()
예제 #18
0
class MixerControlComponent(ModesComponent):
    __events__ = (u'items', u'selected_item')
    controls = control_list(MappedControl)
    cycle_sends_button = ButtonControl(color=u'DefaultButton.Off')

    @staticmethod
    def get_tracks(items):
        return filter(
            lambda item: item is not None and isinstance(
                item.proxied_object, Live.Track.Track), items)

    @depends(tracks_provider=None,
             real_time_mapper=None,
             register_real_time_data=None)
    def __init__(self,
                 view_model=None,
                 tracks_provider=None,
                 real_time_mapper=None,
                 register_real_time_data=None,
                 *a,
                 **k):
        assert liveobj_valid(real_time_mapper)
        assert view_model is not None
        assert tracks_provider is not None
        super(MixerControlComponent, self).__init__(*a, **k)
        self._send_offset = 0
        self.real_time_meter_handlers = [
            RealTimeDataComponent(
                channel_type=u'meter',
                real_time_mapper=real_time_mapper,
                register_real_time_data=register_real_time_data,
                is_enabled=False) for _ in xrange(8)
        ]
        self._track_provider = tracks_provider
        self._on_return_tracks_changed.subject = self.song
        self._on_mode_changed.subject = self
        self._mixer_section_view = None
        self._mixer_sections = []
        self._selected_view = view_model.volumeControlListView
        self._parameter_getter = lambda x: None
        self._setup_modes(view_model)
        self.selected_mode = u'volume'
        self._selected_item = u''
        self._items = []
        self._on_return_tracks_changed()
        self._update_mixer_sections()
        self._on_items_changed.subject = self._track_provider
        self._on_selected_item_changed.subject = self._track_provider
        tracks = self.get_tracks(self._track_provider.items)
        self.__on_track_frozen_state_changed.replace_subjects(tracks)
        self.__on_panning_mode_changed.replace_subjects(
            filter(has_pan_mode, [t.mixer_device for t in tracks]))
        return

    def _setup_modes(self, view_model):
        self._add_mode(u'volume',
                       view_model.volumeControlListView,
                       lambda mixer: mixer.volume,
                       additional_mode_contents=self.real_time_meter_handlers)
        self._add_mode(
            u'panning', view_model.panControlListView, lambda mixer:
            (ConstantParameter(original_parameter=mixer.panning)
             if is_set_to_split_stereo(mixer) else mixer.panning))

        def add_send_mode(index):
            self._add_mode(
                SEND_MODE_NAMES[index], view_model.sendControlListView,
                lambda mixer:
                (mixer.sends[self._send_offset + index]
                 if len(mixer.sends) > self._send_offset + index else None))

        for i in xrange(SEND_LIST_LENGTH):
            add_send_mode(i)

    def _add_mode(self,
                  mode,
                  view,
                  parameter_getter,
                  additional_mode_contents=[]):
        description = MixerSectionDescription(
            view=view, parameter_getter=parameter_getter)
        self.add_mode(
            mode,
            additional_mode_contents + [partial(self._set_mode, description)])
        mode_button = self.get_mode_button(mode)
        mode_button.mode_selected_color = u'MixerControlView.SectionSelected'
        mode_button.mode_unselected_color = u'MixerControlView.SectionUnSelected'

    def on_enabled_changed(self):
        super(MixerControlComponent, self).on_enabled_changed()
        self._selected_view.visible = self.is_enabled()
        self._update_mixer_sections()
        if not self.is_enabled():
            self._update_realtime_ids()

    def set_mixer_section(self, mixer_section):
        self._mixer_section_view = mixer_section
        if self._mixer_section_view:
            self._mixer_section_view.model.mode = u'Global'
            self._update_mixer_sections()

    @property
    def number_sends(self):
        return len(self._track_provider.selected_item.mixer_device.sends)

    def _set_mode(self, description):
        self._selected_view.visible = False
        self._selected_view = description.view
        self._parameter_getter = description.parameter_getter
        self._update_controls(self._parameter_getter, self._selected_view)
        self._selected_view.visible = True

    @listens(u'selected_mode')
    def _on_mode_changed(self, selected_mode):
        if selected_mode in SEND_MODE_NAMES:
            index = SEND_MODE_NAMES.index(selected_mode)
            self._selected_item = SEND_SECTIONS[clamp(
                index + self._send_offset, 0, self.number_sends - 1)]
        else:
            self._selected_item = MIXER_SECTIONS[
                1] if selected_mode == u'panning' else MIXER_SECTIONS[0]
        self.notify_selected_item()

    @listens(u'return_tracks')
    def _on_return_tracks_changed(self):
        with self._updating_send_offset_mode_selection():
            self._update_mode_selection()
            new_send_offset = max(
                0,
                int(
                    ceil(
                        float(self.number_sends) / float(SEND_LIST_LENGTH) - 1)
                    * SEND_LIST_LENGTH))
            if new_send_offset < self._send_offset:
                self._send_offset = new_send_offset

    @listens(u'items')
    def _on_items_changed(self):
        tracks = self.get_tracks(self._track_provider.items)
        self.__on_panning_mode_changed.replace_subjects(
            filter(has_pan_mode, [t.mixer_device for t in tracks]))
        self._update_controls(self._parameter_getter, self._selected_view)

    @listens_group(u'is_frozen')
    def __on_track_frozen_state_changed(self, identifier):
        self._update_controls(self._parameter_getter, self._selected_view)

    @listens_group(u'panning_mode')
    def __on_panning_mode_changed(self, identifier):
        self._update_controls(self._parameter_getter, self._selected_view)

    @listens(u'selected_item')
    def _on_selected_item_changed(self):
        if self.number_sends <= SEND_LIST_LENGTH:
            self._send_offset = 0
        self._update_mode_selection()
        self._update_mixer_sections()
        self._update_buttons(self.selected_mode)

    def _update_mode_selection(self):
        number_sends = self.number_sends
        if self.selected_mode in SEND_MODE_NAMES:
            index = SEND_MODE_NAMES.index(self.selected_mode)
            if index + self._send_offset >= number_sends and number_sends > 0:
                self.selected_mode = SEND_MODE_NAMES[number_sends %
                                                     SEND_LIST_LENGTH - 1]
            elif index == 0 and number_sends == 0:
                self.selected_mode = u'panning'

    def _update_mixer_sections(self):
        if self.is_enabled():
            position = max(self._send_offset, 0)
            pos_range = min(self.number_sends - position, SEND_LIST_LENGTH)
            mixer_section_names = list(
                MIXER_SECTIONS) + SEND_SECTIONS[position:position + pos_range]
            self._mixer_sections = [
                SimpleItemSlot(name=name) for name in mixer_section_names
            ]
            if self.number_sends > SEND_LIST_LENGTH:
                self._mixer_sections.extend([SimpleItemSlot()] *
                                            (8 - len(self._mixer_sections)))
                self._mixer_sections[7] = SimpleItemSlot(
                    icon=u'page_right.svg')
            self.notify_items()
            if self.selected_mode in SEND_MODE_NAMES:
                index = SEND_MODE_NAMES.index(self.selected_mode)
                self._selected_item = SEND_SECTIONS[index + self._send_offset]
                self.notify_selected_item()

    @property
    def items(self):
        return self._mixer_sections

    @property
    def selected_item(self):
        return self._selected_item

    def _update_controls(self, parameter_getter, control_view):
        if self.is_enabled():
            parameters = self._get_parameter_for_tracks(parameter_getter)
            control_view.parameters = parameters
            self._update_realtime_ids()
            assign_parameters(self.controls, parameters)

    def _update_realtime_ids(self):
        mixables = self._track_provider.items
        for handler, mixable in izip(self.real_time_meter_handlers, mixables):
            handler.set_data(
                mixable.mixer_device if liveobj_valid(mixable) else None)

        return

    def _get_parameter_for_tracks(self, parameter_getter):
        tracks = self._track_provider.items
        self.controls.control_count = len(tracks)
        return map(lambda t: (parameter_getter(t.mixer_device)
                              if t else None), tracks)

    def mode_can_be_used(self, mode):
        return mode not in SEND_MODE_NAMES or SEND_MODE_NAMES.index(
            mode) + self._send_offset < self.number_sends

    def _update_buttons(self, selected_mode):
        for name in self._mode_map.iterkeys():
            self.get_mode_button(name).enabled = self.mode_can_be_used(name)

        self.cycle_sends_button.enabled = self.number_sends > SEND_LIST_LENGTH

    @cycle_sends_button.pressed
    def cycle_sends_button(self, button):
        button.color = u'MixerControlView.SectionSelected'

    @cycle_sends_button.released
    def cycle_sends_button(self, button):
        button.color = u'MixerControlView.SectionUnSelected'
        self._cycle_send_offset()

    def _cycle_send_offset(self):
        with self._updating_send_offset_mode_selection():
            new_offset = self._send_offset + SEND_LIST_LENGTH
            self._send_offset = new_offset if new_offset < self.number_sends else 0
            if self.selected_mode in SEND_MODE_NAMES:
                self.selected_mode = SEND_MODE_NAMES[0]

    @contextmanager
    def _updating_send_offset_mode_selection(self):
        yield
        self._update_mixer_sections()
        self._update_buttons(self.selected_mode)
        self._update_controls(self._parameter_getter, self._selected_view)
예제 #19
0
class SetupComponent(ModesComponent):
    category_radio_buttons = control_list(RadioButtonControl,
                                          checked_color=u'Option.Selected',
                                          unchecked_color=u'Option.Unselected')
    make_it_go_boom_button = ButtonControl()
    make_it_go_boom = listenable_property.managed(False)

    def __init__(self,
                 settings=None,
                 pad_curve_sender=None,
                 firmware_switcher=None,
                 *a,
                 **k):
        assert settings is not None
        super(SetupComponent, self).__init__(*a, **k)
        self._settings = settings
        self._pad_curve_sender = pad_curve_sender
        has_option = self.application.has_option
        self.make_it_go_boom_button.enabled = not has_option(
            u'_Push2DeveloperMode') and has_option(u'_MakePush2GoBoom')
        self._general = self.register_component(
            GeneralSettingsComponent(settings=settings.general,
                                     hardware_settings=settings.hardware,
                                     is_enabled=False))
        self._info = self.register_component(
            InfoComponent(firmware_switcher=firmware_switcher,
                          is_enabled=False))
        self._pad_settings = self.register_component(
            PadSettingsComponent(pad_settings=settings.pad_settings,
                                 is_enabled=False))
        self._display_debug = self.register_component(
            DisplayDebugSettingsComponent(settings=settings.display_debug,
                                          is_enabled=False))
        self._profiling = self.register_component(
            ProfilingSettingsComponent(settings=settings.profiling,
                                       is_enabled=False))
        self.add_mode(u'Settings', [self._general, self._pad_settings])
        self.add_mode(u'Info', [self._info])
        if self.application.has_option(u'_Push2DeveloperMode'):
            self.add_mode(u'Display Debug', [self._display_debug])
            self.add_mode(u'Profiling', [self._profiling])
        self.selected_mode = u'Settings'
        self.category_radio_buttons.control_count = len(self.modes)
        self.category_radio_buttons.checked_index = 0
        return

    @make_it_go_boom_button.pressed
    def make_it_go_boom_button(self, _button):
        self.make_it_go_boom = True

    @property
    def general(self):
        return self._general

    @property
    def info(self):
        return self._info

    @property
    def pad_settings(self):
        return self._pad_settings

    @property
    def display_debug(self):
        return self._display_debug

    @property
    def profiling(self):
        return self._profiling

    @property
    def settings(self):
        return self._settings

    @property
    def velocity_curve(self):
        return self._pad_curve_sender

    @category_radio_buttons.checked
    def category_radio_buttons(self, button):
        self.selected_mode = self.modes[button.index]
예제 #20
0
class ListComponent(CompoundComponent):
    u"""
    Component that handles a ScrollableList.  If an action button is
    passed, it can handle an ActionList.
    """
    __events__ = (u'item_action', u'selected_item')
    SELECTION_DELAY = 0.5
    ENCODER_FACTOR = 10.0
    empty_list_message = ''
    _current_action_item = None
    _last_action_item = None
    action_button = ButtonControl(color='Browser.Load')
    encoders = control_list(EncoderControl)

    def __init__(self, scrollable_list=None, data_sources=tuple(), *a, **k):
        super(ListComponent, self).__init__(*a, **k)
        self._data_sources = data_sources
        self._activation_task = task.Task()
        self._action_on_scroll_task = task.Task()
        self._scrollable_list = None
        self._scroller = self.register_component(ScrollComponent())
        self._pager = self.register_component(ScrollComponent())
        self.last_action_item = lambda: self._last_action_item
        self.item_formatter = DefaultItemFormatter()
        for c in (self._scroller, self._pager):
            for button in (c.scroll_up_button, c.scroll_down_button):
                button.color = 'List.ScrollerOn'
                button.pressed_color = None
                button.disabled_color = 'List.ScrollerOff'

        if scrollable_list == None:
            self.scrollable_list = ActionList(
                num_visible_items=len(data_sources))
        else:
            self.scrollable_list = scrollable_list
        self._scrollable_list.num_visible_items = len(data_sources)
        self._delay_activation = BooleanContext()
        self._selected_index_float = 0.0
        self._in_encoder_selection = BooleanContext(False)
        self._execute_action_task = self._tasks.add(
            task.sequence(task.delay(1), task.run(self._execute_action)))
        self._execute_action_task.kill()
        return

    @property
    def _trigger_action_on_scrolling(self):
        return self.action_button.is_pressed

    def _get_scrollable_list(self):
        return self._scrollable_list

    def _set_scrollable_list(self, new_list):
        if new_list != self._scrollable_list:
            self._scrollable_list = new_list
            if new_list != None:
                new_list.num_visible_items = len(self._data_sources)
                self._scroller.scrollable = new_list
                self._pager.scrollable = new_list.pager
                self._on_scroll.subject = new_list
                self._selected_index_float = new_list.selected_item_index
            else:
                self._scroller.scrollable = ScrollComponent.default_scrollable
                self._scroller.scrollable = ScrollComponent.default_pager
            self._on_selected_item_changed.subject = new_list
            self.update_all()
        return

    scrollable_list = property(_get_scrollable_list, _set_scrollable_list)

    def set_data_sources(self, sources):
        self._data_sources = sources
        if self._scrollable_list:
            self._scrollable_list.num_visible_items = len(sources)
        self._update_display()

    select_next_button = forward_property('_scroller')('scroll_down_button')
    select_prev_button = forward_property('_scroller')('scroll_up_button')
    next_page_button = forward_property('_pager')('scroll_down_button')
    prev_page_button = forward_property('_pager')('scroll_up_button')

    def on_enabled_changed(self):
        super(ListComponent, self).on_enabled_changed()
        if not self.is_enabled():
            self._execute_action_task.kill()

    @listens('scroll')
    def _on_scroll(self):
        if self._trigger_action_on_scrolling:
            trigger_selected = partial(self._trigger_action,
                                       self.selected_item)
            self._action_on_scroll_task.kill()
            self._action_on_scroll_task = self._tasks.add(
                task.sequence(task.wait(defaults.MOMENTARY_DELAY),
                              task.delay(1), task.run(trigger_selected)))

    @listens('selected_item')
    def _on_selected_item_changed(self):
        self._scroller.update()
        self._pager.update()
        self._update_display()
        self._update_action_feedback()
        self._activation_task.kill()
        self._action_on_scroll_task.kill()
        if self.SELECTION_DELAY and self._delay_activation:
            self._activation_task = self._tasks.add(
                task.sequence(
                    task.wait(self.SELECTION_DELAY),
                    task.run(
                        self._scrollable_list.request_notify_item_activated)))
        else:
            self._scrollable_list.request_notify_item_activated()
        if not self._in_encoder_selection:
            self._selected_index_float = float(
                self._scrollable_list.selected_item_index)
        self.notify_selected_item(self._scrollable_list.selected_item)

    @encoders.value
    def encoders(self, value, encoder):
        self._add_offset_to_selected_index(value)

    def _add_offset_to_selected_index(self, offset):
        if self.is_enabled() and self._scrollable_list:
            with self._delay_activation():
                with self._in_encoder_selection():
                    self._selected_index_float = clamp(
                        self._selected_index_float +
                        offset * self.ENCODER_FACTOR, 0,
                        len(self._scrollable_list.items))
                    self._scrollable_list.select_item_index_with_border(
                        int(self._selected_index_float), 1)

    @action_button.pressed
    def action_button(self, button):
        if self._current_action_item == None:
            self._trigger_action(
                self.next_item if self._action_target_is_next_item(
                ) else self.selected_item)
        return

    def do_trigger_action(self, item):
        item.action()
        self.notify_item_action(item)

    def _trigger_action(self, item):
        if self.is_enabled() and self._can_be_used_for_action(item):
            if self._scrollable_list != None:
                self._scrollable_list.select_item(item)
            self._current_action_item = item
            self.update()
            self._execute_action_task.restart()
        return

    def _execute_action(self):
        u""" Is called by the execute action task and should not be called directly
        use _trigger_action instead """
        if self._current_action_item != None:
            self.do_trigger_action(self._current_action_item)
            self._last_action_item = self._current_action_item
            self._current_action_item = None
            self.update()
        return

    @property
    def selected_item(self):
        if self._scrollable_list != None:
            return self._scrollable_list.selected_item
        return

    @property
    def next_item(self):
        item = None
        if self._scrollable_list != None:
            all_items = self._scrollable_list.items
            next_index = self._scrollable_list.selected_item_index + 1
            item = all_items[next_index] if in_range(next_index, 0,
                                                     len(all_items)) else None
        return item

    def _can_be_used_for_action(self, item):
        return item != None and item.supports_action and item != self.last_action_item(
        )

    def _action_target_is_next_item(self):
        return self.selected_item == self.last_action_item(
        ) and self._can_be_used_for_action(self.next_item)

    def _update_action_feedback(self):
        color = 'Browser.Loading'
        if self._current_action_item == None:
            if self._action_target_is_next_item():
                color = 'Browser.LoadNext'
            elif self._can_be_used_for_action(self.selected_item):
                color = 'Browser.Load'
            else:
                color = 'Browser.LoadNotPossible'
        self.action_button.color = color
        return

    def _update_display(self):
        visible_items = self._scrollable_list.visible_items if self._scrollable_list else []
        for index, data_source in enumerate(self._data_sources):
            if index < len(visible_items):
                item = visible_items[index] if 1 else None
                action_in_progress = item and item == self._current_action_item
                display_string = self.item_formatter(index, item,
                                                     action_in_progress)
                data_source.set_display_string(display_string)

        if not visible_items and self._data_sources and self.empty_list_message:
            self._data_sources[0].set_display_string(self.empty_list_message)
        return

    def update(self):
        super(ListComponent, self).update()
        if self.is_enabled():
            self._update_action_feedback()
            self._update_display()
class InstrumentComponent(PlayableComponent, Slideable, Messenger):
    u"""
    Class that sets up the button matrix as a piano, using different
    selectable layouts for the notes.
    """
    __events__ = (u'pattern', )
    matrix = control_matrix(PadControl)
    delete_button = ButtonControl()

    def __init__(self, note_layout=None, *a, **k):
        assert note_layout is not None
        super(InstrumentComponent, self).__init__(*a, **k)
        self._note_layout = note_layout
        self._first_note = self.page_length * 3 + self.page_offset
        self._last_page_length = self.page_length
        self._last_page_offset = self.page_offset
        self._detail_clip = None
        self._has_notes = [False] * 128
        self._has_notes_pattern = self._get_pattern(0)
        self._aftertouch_control = None
        self._show_notifications = True
        self.__on_detail_clip_changed.subject = self.song.view
        self.__on_detail_clip_changed()
        self._slider = SlideComponent(slideable=self, parent=self)
        self._touch_slider = SlideableTouchStripComponent(touch_slideable=self, parent=self)
        for event in (u'scale', u'root_note', u'is_in_key', u'is_fixed', u'is_horizontal',
                      u'interval'):
            self.register_slot(self._note_layout, self._on_note_layout_changed, event)

        self._update_pattern()
        return

    @listens('detail_clip')
    def __on_detail_clip_changed(self):
        clip = self.song.view.detail_clip
        self.set_detail_clip(clip if liveobj_valid(clip) and clip.is_midi_clip else None)
        return

    def set_detail_clip(self, clip):
        if clip != self._detail_clip:
            self._detail_clip = clip
            self._on_clip_notes_changed.subject = clip
            self._on_loop_start_changed.subject = clip
            self._on_loop_end_changed.subject = clip
            self._on_clip_notes_changed()

    @listens('notes')
    def _on_clip_notes_changed(self):
        if self._detail_clip:
            self._has_notes = [
             False] * 128
            loop_start = self._detail_clip.loop_start
            loop_length = self._detail_clip.loop_end - loop_start
            notes = self._detail_clip.get_notes(loop_start, 0, loop_length, 128)
            for note in notes:
                self._has_notes[note[0]] = True

        self.notify_contents()

    @listens('loop_start')
    def _on_loop_start_changed(self):
        self._on_loop_selection_changed()

    @listens('loop_end')
    def _on_loop_end_changed(self):
        self._on_loop_selection_changed()

    def _on_loop_selection_changed(self):
        self._on_clip_notes_changed()

    def contents(self, index):
        if self._detail_clip:
            note = self._has_notes_pattern[index].index
            if note is not None:
                return self._has_notes[note]
            return False
        return False

    @property
    def show_notifications(self):
        return self._show_notifications

    @show_notifications.setter
    def show_notifications(self, value):
        self._show_notifications = value

    @property
    def page_length(self):
        if self._note_layout.is_in_key:
            return len(self._note_layout.notes)
        return 12

    @property
    def position_count(self):
        if not self._note_layout.is_in_key:
            return 139
        else:
            offset = self.page_offset
            octaves = 11 if self._note_layout.notes[0] < 8 else 10
            return offset + len(self._note_layout.notes) * octaves

    def _first_scale_note_offset(self):
        if not self._note_layout.is_in_key:
            return self._note_layout.notes[0]
        else:
            if self._note_layout.notes[0] == 0:
                return 0
            return len(self._note_layout.notes) - index_if(lambda n: n >= 12, self._note_layout.notes)

    @property
    def page_offset(self):
        if self._note_layout.is_fixed:
            return 0
        return self._first_scale_note_offset()

    def _get_position(self):
        return self._first_note

    def _set_position(self, note):
        self._first_note = note
        self._update_pattern()
        self._update_matrix()
        self.notify_position()

    position = property(_get_position, _set_position)

    @property
    def min_pitch(self):
        return self.pattern[0].index

    @property
    def max_pitch(self):
        identifiers = [ control.identifier for control in self.matrix ]
        if len(identifiers) > 0:
            return max(identifiers)
        return 127

    @property
    def pattern(self):
        return self._pattern

    @matrix.pressed
    def matrix(self, button):
        self._on_matrix_pressed(button)

    def _on_matrix_pressed(self, button):
        if self.delete_button.is_pressed:
            pitch = self._get_note_info_for_coordinate(button.coordinate).index
            if pitch and self._detail_clip:
                self._do_delete_pitch(pitch)

    @matrix.released
    def matrix(self, button):
        self._on_matrix_released(button)

    def _on_matrix_released(self, button):
        pass

    def _do_delete_pitch(self, pitch):
        clip = self._detail_clip
        if clip:
            note_name = pitch_index_to_string(pitch)
            loop_length = clip.loop_end - clip.loop_start
            clip.remove_notes(clip.loop_start, pitch, loop_length, 1)
            self.show_notification(consts.MessageBoxText.DELETE_NOTES % note_name)

    @delete_button.pressed
    def delete_button(self, value):
        self._set_control_pads_from_script(True)

    @delete_button.released
    def delete_button(self, value):
        self._set_control_pads_from_script(False)

    def set_note_strip(self, strip):
        self._touch_slider.set_scroll_strip(strip)

    def set_octave_strip(self, strip):
        self._touch_slider.set_page_strip(strip)

    def set_octave_up_button(self, button):
        self._slider.set_scroll_page_up_button(button)

    def set_octave_down_button(self, button):
        self._slider.set_scroll_page_down_button(button)

    def set_scale_up_button(self, button):
        self._slider.set_scroll_up_button(button)

    def set_scale_down_button(self, button):
        self._slider.set_scroll_down_button(button)

    def set_aftertouch_control(self, control):
        self._aftertouch_control = control
        self._update_aftertouch()

    def _align_first_note(self):
        self._first_note = self.page_offset + (self._first_note - self._last_page_offset) * float(self.page_length) / float(self._last_page_length)
        if self._first_note >= self.position_count:
            self._first_note -= self.page_length
        self._last_page_length = self.page_length
        self._last_page_offset = self.page_offset

    def _on_note_layout_changed(self, _):
        self._update_scale()

    def show_pitch_range_notification(self):
        if self.is_enabled() and self.show_notifications:
            self.show_notification(('Play {start_note} to {end_note}').format(start_note=pitch_index_to_string(self.pattern.note(0, 0).index), end_note=pitch_index_to_string(self.pattern.note(self.width - 1, self.height - 1).index)))

    def _update_scale(self):
        self._align_first_note()
        self._update_pattern()
        self._update_matrix()
        self.notify_position_count()
        self.notify_position()
        self.notify_contents()

    def update(self):
        super(InstrumentComponent, self).update()
        if self.is_enabled():
            self._update_matrix()
            self._update_aftertouch()

    def _update_pattern(self):
        self._pattern = self._get_pattern()
        self._has_notes_pattern = self._get_pattern(0)
        self.notify_pattern()

    def _invert_and_swap_coordinates(self, coordinates):
        return (
         coordinates[1], self.height - 1 - coordinates[0])

    def _get_note_info_for_coordinate(self, coordinate):
        x, y = self._invert_and_swap_coordinates(coordinate)
        return self.pattern.note(x, y)

    def _update_button_color(self, button):
        note_info = self._get_note_info_for_coordinate(button.coordinate)
        button.color = 'Instrument.' + note_info.color

    def _button_should_be_enabled(self, button):
        return self._get_note_info_for_coordinate(button.coordinate).index != None

    def _note_translation_for_button(self, button):
        note_info = self._get_note_info_for_coordinate(button.coordinate)
        return (
         note_info.index, note_info.channel)

    def _set_button_control_properties(self, button):
        super(InstrumentComponent, self)._set_button_control_properties(button)
        button.sensitivity_profile = 'default' if self._takeover_pads else 'instrument'

    def _update_matrix(self):
        self._update_control_from_script()
        self._update_note_translations()
        self._update_led_feedback()

    def _get_pattern(self, first_note=None):
        if first_note is None:
            first_note = int(round(self._first_note))
        interval = self._note_layout.interval
        notes = self._note_layout.notes
        width = None
        height = None
        octave = first_note / self.page_length
        offset = first_note % self.page_length - self._first_scale_note_offset()
        if interval == None:
            if self._note_layout.is_in_key:
                interval = len(self._note_layout.notes)
                if self._note_layout.is_horizontal:
                    width = interval + 1
                else:
                    height = interval + 1
            else:
                interval = 8
        elif not self._note_layout.is_in_key:
            interval = [
             0, 2, 4, 5, 7, 9, 10, 11][interval]
        if self._note_layout.is_horizontal:
            steps = [
             1, interval]
            origin = [offset, 0]
        else:
            steps = [
             interval, 1]
            origin = [0, offset]
        return MelodicPattern(steps=steps, scale=notes, origin=origin, root_note=octave * 12, chromatic_mode=not self._note_layout.is_in_key, width=width, height=height)

    def _update_aftertouch(self):
        if self.is_enabled() and self._aftertouch_control != None:
            self._aftertouch_control.send_value('mono')
        return
예제 #22
0
class FixedLengthComponent(Component, Messenger):
    fixed_length_toggle_button = ButtonControl()

    def __init__(self, fixed_length_setting=None, *a, **k):
        assert fixed_length_setting is not None
        super(FixedLengthComponent, self).__init__(*a, **k)
        self._fixed_length_setting = fixed_length_setting
        self.settings_component = FixedLengthSettingComponent(
            fixed_length_setting=self._fixed_length_setting,
            parent=self,
            is_enabled=False)
        self._length_press_state = None
        self.__on_setting_enabled_changes.subject = fixed_length_setting
        self.__on_setting_enabled_changes(fixed_length_setting.enabled)
        return

    @fixed_length_toggle_button.released_immediately
    def fixed_length_toggle_button(self, button):
        loop_set = self._set_loop()
        if not loop_set:
            enabled = not self._fixed_length_setting.enabled
            self._fixed_length_setting.enabled = enabled
            self.show_notification(consts.MessageBoxText.FIXED_LENGTH %
                                   (b'On' if enabled else b'Off'))

    @fixed_length_toggle_button.pressed_delayed
    def fixed_length_toggle_button(self, button):
        self.settings_component.set_enabled(True)

    @fixed_length_toggle_button.released_delayed
    def fixed_length_toggle_button(self, button):
        self.settings_component.set_enabled(False)
        self._set_loop()

    @fixed_length_toggle_button.pressed
    def fixed_length_toggle_button(self, button):
        song = self.song
        slot = song.view.highlighted_clip_slot
        if slot is None:
            return
        else:
            clip = slot.clip
            if slot.is_recording and not clip.is_overdubbing:
                self._length_press_state = (slot, clip.playing_position)
            return

    def _set_loop(self):
        song = self.song
        slot = song.view.highlighted_clip_slot
        if slot is None:
            return
        else:
            clip = slot.clip
            loop_set = False
            if self._length_press_state is not None:
                press_slot, press_position = self._length_press_state
                if press_slot == slot and slot.is_recording and not clip.is_overdubbing:
                    length, _ = self._fixed_length_setting.get_selected_length(
                        song)
                    one_bar = 4.0 * song.signature_numerator / song.signature_denominator
                    loop_end = int(press_position / one_bar) * one_bar
                    loop_start = loop_end - length
                    if loop_start >= 0.0:
                        clip.loop_end = loop_end
                        clip.end_marker = loop_end
                        clip.loop_start = loop_start
                        clip.start_marker = loop_start
                        self._tasks.add(
                            task.sequence(
                                task.delay(0),
                                task.run(
                                    partial(slot.fire,
                                            force_legato=True,
                                            launch_quantization=Quantization.
                                            q_no_q))))
                        self.song.overdub = False
                    loop_set = True
            self._length_press_state = None
            return loop_set

    @listens(b'enabled')
    def __on_setting_enabled_changes(self, enabled):
        self.fixed_length_toggle_button.color = b'FixedLength.On' if enabled else b'FixedLength.Off'
class SelectPlayingClipComponent(MessengerModesComponent):
    action_button = ButtonControl(color='DefaultButton.Alert')

    def __init__(self,
                 playing_clip_above_layer=None,
                 playing_clip_below_layer=None,
                 *a,
                 **k):
        super(SelectPlayingClipComponent, self).__init__(*a, **k)
        self._update_mode_task = self._tasks.add(
            task.sequence(task.delay(1), task.run(self._update_mode)))
        self._update_mode_task.kill()
        self.add_mode('default', None)
        self.add_mode('above', [AddLayerMode(self, playing_clip_above_layer)],
                      message=MessageBoxText.PLAYING_CLIP_ABOVE_SELECTED_CLIP)
        self.add_mode('below', [AddLayerMode(self, playing_clip_below_layer)],
                      message=MessageBoxText.PLAYING_CLIP_BELOW_SELECTED_CLIP)
        self.selected_mode = 'default'
        self.notify_when_enabled = True
        self._on_detail_clip_changed.subject = self.song.view
        self._on_playing_slot_index_changed.subject = self.song.view.selected_track
        self._notification_reference = partial(nop, None)
        return

    @action_button.pressed
    def action_button(self, button):
        self._go_to_playing_clip()

    @listens('detail_clip')
    def _on_detail_clip_changed(self):
        self._update_mode_task.restart()

    @listens('playing_slot_index')
    def _on_playing_slot_index_changed(self):
        self._update_mode_task.restart()

    def _go_to_playing_clip(self):
        song_view = self.song.view
        playing_clip_slot = self._playing_clip_slot()
        if playing_clip_slot:
            song_view.highlighted_clip_slot = playing_clip_slot
            song_view.detail_clip = playing_clip_slot.clip
            self._hide_notification()

    def _hide_notification(self):
        if self._notification_reference() is not None:
            self._notification_reference().hide()
        return

    def show_notification(self, display_text):
        self._notification_reference = super(
            SelectPlayingClipComponent, self).show_notification(
                display_text,
                blink_text=MessageBoxText.SELECTED_CLIP_BLINK,
                notification_time=-1)

    def _selected_track_clip_is_playing(self):
        playing_clip_slot = self._playing_clip_slot()
        return not (playing_clip_slot
                    and playing_clip_slot.clip != self.song.view.detail_clip)

    def _playing_clip_slot(self):
        track = self.song.view.selected_track
        try:
            playing_slot_index = track.playing_slot_index
            slot = track.clip_slots[
                playing_slot_index] if 0 <= playing_slot_index < len(
                    track.clip_slots) else None
            return slot
        except RuntimeError:
            pass

        return

    def _selected_track_clip_is_above_playing_clip(self):
        song_view = self.song.view
        track = song_view.selected_track
        playing_slot_index = track.playing_slot_index
        selected_index = index_if(
            lambda slot: slot == song_view.highlighted_clip_slot,
            track.clip_slots)
        return playing_slot_index <= selected_index

    def _update_mode(self):
        if not self._selected_track_clip_is_playing():
            if self._selected_track_clip_is_above_playing_clip():
                self.selected_mode = 'above'
            else:
                self.selected_mode = 'below'
        else:
            self.selected_mode = 'default'
            self._hide_notification()
class DrumGroupComponent(SlideableTouchStripComponent, DrumGroupComponent,
                         Messenger):
    """'
    Class representing a drum group pads in a matrix.
    """
    __module__ = __name__
    matrix = control_matrix(PadControl)
    duplicate_button = ButtonControl()

    def __init__(self, quantizer=None, *a, **k):
        super(DrumGroupComponent,
              self).__init__(touch_slideable=self,
                             translation_channel=PAD_FEEDBACK_CHANNEL,
                             dragging_enabled=True,
                             *a,
                             **k)
        self._copy_handler = self._make_copy_handler()
        self._notification_reference = partial(nop, None)
        self._quantizer = quantizer
        self.selected_notes_provider = self.register_disconnectable(
            SelectedNotesProvider())
        self._update_selected_drum_pad()
        return

    position_count = 32
    page_length = 4
    page_offset = 1

    def update(self):
        super(DrumGroupComponent, self).update()
        if self._copy_handler:
            self._copy_handler.stop_copying()

    def set_drum_group_device(self, drum_group_device):
        super(DrumGroupComponent,
              self).set_drum_group_device(drum_group_device)
        self._on_chains_changed.subject = self._drum_group_device
        self.notify_contents()

    def quantize_pitch(self, note):
        self._quantizer.quantize_pitch(note, 'pad')

    def _update_selected_drum_pad(self):
        super(DrumGroupComponent, self)._update_selected_drum_pad()
        if liveobj_valid(self._selected_drum_pad):
            self.selected_notes_provider.selected_notes = [
                self._selected_drum_pad.note
            ]
        self.notify_selected_target_note()

    def _update_assigned_drum_pads(self):
        super(DrumGroupComponent, self)._update_assigned_drum_pads()
        self.notify_selected_target_note()

    def _make_copy_handler(self):
        return DrumPadCopyHandler(self.show_notification)

    @matrix.pressed
    def matrix(self, pad):
        self._on_matrix_pressed(pad)

    @matrix.released
    def matrix(self, pad):
        self._on_matrix_released(pad)

    def _on_matrix_pressed(self, pad):
        super(DrumGroupComponent, self)._on_matrix_pressed(pad)
        if self.duplicate_button.is_pressed:
            self._duplicate_pad(pad)

    def set_select_button(self, button):
        self.select_button.set_control_element(button)

    def set_mute_button(self, button):
        self.mute_button.set_control_element(button)

    def set_solo_button(self, button):
        self.solo_button.set_control_element(button)

    def set_quantize_button(self, button):
        self.quantize_button.set_control_element(button)

    @duplicate_button.pressed
    def duplicate_button(self, button):
        self._set_control_pads_from_script(True)

    @duplicate_button.released
    def duplicate_button(self, button):
        self._set_control_pads_from_script(False)
        if self._copy_handler:
            self._copy_handler.stop_copying()
        if self._notification_reference() is not None:
            self._notification_reference().hide()
        return

    @listens('chains')
    def _on_chains_changed(self):
        self.notify_contents()

    def delete_pitch(self, drum_pad):
        clip = self.song.view.detail_clip
        if clip and any(clip.get_notes(0, drum_pad.note, DISTANT_FUTURE, 1)):
            clip.remove_notes(0, drum_pad.note, DISTANT_FUTURE, 1)
            self.show_notification(MessageBoxText.DELETE_NOTES % drum_pad.name)
        else:
            self.show_notification(MessageBoxText.DELETE_DRUM_RACK_PAD %
                                   drum_pad.name)
            self.delete_drum_pad_content(drum_pad)

    def delete_drum_pad_content(self, drum_pad):
        drum_pad.delete_all_chains()

    def _duplicate_pad(self, pad):
        if self._copy_handler and self._drum_group_device:
            drum_pad = self._pad_for_button(pad)
            self._notification_reference = self._copy_handler.duplicate_pad(
                self._drum_group_device, drum_pad)

    def set_matrix(self, matrix):
        super(DrumGroupComponent, self).set_matrix(matrix)
        self._update_sensitivity_profile()

    def _update_control_from_script(self):
        super(DrumGroupComponent, self)._update_control_from_script()
        self._update_sensitivity_profile()

    def _update_sensitivity_profile(self):
        profile = 'default' if self._takeover_pads or self.pressed_pads else 'drums'
        for button in self.matrix:
            button.sensitivity_profile = profile

    @listenable_property
    def selected_target_note(self):
        note_and_channel = (-1, -1)
        if liveobj_valid(self._drum_group_device) and liveobj_valid(
                self._selected_drum_pad):
            if self._selected_drum_pad in self.assigned_drum_pads:
                predicate = lambda b: self._pad_for_button(
                    b) == self._selected_drum_pad
                button = find_if(predicate, self.matrix)
                if button != None and None not in (button.identifier,
                                                   button.channel):
                    note_and_channel = (button.identifier, button.channel)
            else:
                note_and_channel = (self._selected_drum_pad.note,
                                    NON_FEEDBACK_CHANNEL)
        return NamedTuple(note=note_and_channel[0],
                          channel=note_and_channel[1])
class BrowserComponent(Component):
    """'
    Component for controlling the Live library browser.  It has 4
    browsing columns that are controlled by encoders and state
    buttons.  The contents of these lists are provided by a browser
    model -- see BrowserModel and derivatives.
    """
    __module__ = __name__
    __events__ = (u'load_item', )
    NUM_COLUMNS = 4
    COLUMN_SIZE = 4
    enter_button = ButtonControl(**consts.SIDE_BUTTON_COLORS)
    exit_button = ButtonControl(**consts.SIDE_BUTTON_COLORS)
    shift_button = ButtonControl()
    prehear_button = ToggleButtonControl(toggled_color='Browser.Prehear',
                                         untoggled_color='Browser.PrehearOff')

    def __init__(self,
                 browser=None,
                 make_browser_model=None,
                 preferences=dict(),
                 *a,
                 **k):
        super(BrowserComponent, self).__init__(*a, **k)
        assert make_browser_model is not None
        self._browser = browser or self.application.browser
        self._browser_model = EmptyBrowserModel(browser=self._browser)
        self._make_browser_model = make_browser_model
        num_data_sources = self.NUM_COLUMNS * self.COLUMN_SIZE
        self._data_sources = map(DisplayDataSource, (u'', ) * num_data_sources)
        self._last_loaded_item = None
        self._default_item_formatter = DefaultItemFormatter()
        self._list_components = [
            ListComponent(parent=self) for _ in xrange(self.NUM_COLUMNS)
        ]
        for i, component in enumerate(self._list_components):
            component.do_trigger_action = lambda item: self._do_load_item(item)
            component.last_action_item = lambda: self._last_loaded_item
            component.item_formatter = partial(self._item_formatter, i)

        self._preferences = preferences
        self._select_buttons = []
        self._state_buttons = []
        self._encoder_controls = []
        self._on_selected_item.replace_subjects(self._list_components)
        self._on_list_item_action.replace_subjects(self._list_components)
        self._on_hotswap_target_changed.subject = self._browser
        self._on_filter_type_changed.subject = self._browser
        self._on_browser_full_refresh.subject = self._browser
        self._scroll_offset = 0
        self._max_scroll_offset = 0
        self._max_hierarchy = 0
        self._last_filter_type = None
        self._skip_next_preselection = False
        self._browser_model_dirty = True
        self._on_content_lists_changed()
        self.prehear_button.is_toggled = preferences.setdefault(
            'browser_prehear', True)
        self._last_selected_item = None
        return

    def disconnect(self):
        self._last_selected_item = None
        super(BrowserComponent, self).disconnect()
        return

    def set_display_line1(self, display):
        self.set_display_line_with_index(display, 0)

    def set_display_line2(self, display):
        self.set_display_line_with_index(display, 1)

    def set_display_line3(self, display):
        self.set_display_line_with_index(display, 2)

    def set_display_line4(self, display):
        self.set_display_line_with_index(display, 3)

    def set_display_line_with_index(self, display, index):
        if display:
            sources = self._data_sources[index::self.COLUMN_SIZE]
            display.set_data_sources(sources)

    def set_select_buttons(self, buttons):
        for button in buttons or []:
            if button:
                button.reset()

        self._on_select_matrix_value.subject = buttons or None
        self._select_buttons = buttons
        buttons = buttons or (None, None, None, None, None, None, None, None)
        for component, button in izip(self._list_components, buttons[1::2]):
            self._set_button_if_enabled(component, 'action_button', button)

        for component, button in izip(self._list_components, buttons[::2]):
            if self.shift_button.is_pressed:
                self._set_button_if_enabled(component, 'prev_page_button',
                                            button)
                self._set_button_if_enabled(component, 'select_prev_button',
                                            None)
            else:
                self._set_button_if_enabled(component, 'prev_page_button',
                                            None)
                self._set_button_if_enabled(component, 'select_prev_button',
                                            button)

        return

    def set_state_buttons(self, buttons):
        for button in buttons or []:
            if button:
                button.reset()

        self._on_state_matrix_value.subject = buttons or None
        self._state_buttons = buttons
        buttons = buttons or (None, None, None, None, None, None, None, None)
        for component, button in izip(self._list_components, buttons[::2]):
            if self.shift_button.is_pressed:
                self._set_button_if_enabled(component, 'next_page_button',
                                            button)
                self._set_button_if_enabled(component, 'select_next_button',
                                            None)
            else:
                self._set_button_if_enabled(component, 'next_page_button',
                                            None)
                self._set_button_if_enabled(component, 'select_next_button',
                                            button)

        for button in buttons[1::2]:
            if button and self.is_enabled():
                button.set_light('DefaultButton.Disabled')

        return

    @shift_button.value
    def shift_button(self, value, control):
        self.set_select_buttons(self._select_buttons)
        self.set_state_buttons(self._state_buttons)

    def _set_button_if_enabled(self, component, name, button):
        control = getattr(component, name)
        if component.is_enabled(explicit=True):
            control.set_control_element(button)
        else:
            control.set_control_element(None)
            if button and self.is_enabled():
                button.set_light('DefaultButton.Disabled')
        return

    def set_encoder_controls(self, encoder_controls):
        if encoder_controls:
            num_active_lists = len(
                self._browser_model.content_lists) - self._scroll_offset
            num_assignable_lists = min(num_active_lists,
                                       len(encoder_controls) / 2)
            index = 0
            for component in self._list_components[:num_assignable_lists - 1]:
                component.encoders.set_control_element(
                    encoder_controls[index:index + 2])
                index += 2

            self._list_components[(num_assignable_lists -
                                   1)].encoders.set_control_element(
                                       encoder_controls[index:])
        else:
            for component in self._list_components:
                component.encoders.set_control_element([])

        self._encoder_controls = encoder_controls

    def update(self):
        super(BrowserComponent, self).update()
        if self.is_enabled():
            self.set_state_buttons(self._state_buttons)
            self.set_select_buttons(self._select_buttons)
            self._update_browser_model()
        else:
            self._browser.stop_preview()

    def reset_load_memory(self):
        self._update_load_memory(None)
        return

    def _do_load_item(self, item):
        self.do_load_item(item)
        self._update_load_memory(item)
        self._skip_next_preselection = True

        def reset_skip_next_preselection():
            self._skip_next_preselection = False

        self._tasks.add(task.run(reset_skip_next_preselection))

    def _update_load_memory(self, item):
        self._last_loaded_item = item
        for component in self._list_components:
            component.update()

    def do_load_item(self, item):
        item.action()
        self.notify_load_item(item.content)

    def back_to_top(self):
        self._set_scroll_offset(0)

    def _set_scroll_offset(self, offset):
        self._scroll_offset = offset
        self._on_content_lists_changed()
        scrollable_list = self._list_components[(-1)].scrollable_list
        if scrollable_list:
            scrollable_list.request_notify_item_activated()

    def _update_navigation_button_state(self):
        self.exit_button.enabled = self._scroll_offset > 0
        self.enter_button.enabled = self._scroll_offset < self._max_scroll_offset

    def _shorten_item_name(self, shortening_limit, list_index, item_name):
        u"""
        Creates the name of an item shortened by removing words from the parents name
        """
        def is_short_enough(item_name):
            return len(item_name) <= 9

        content_lists = self._browser_model.content_lists
        parent_lists = reversed(content_lists[max(0, list_index -
                                                  3):list_index])
        for content_list in parent_lists:
            if is_short_enough(item_name):
                break
            parent_name = unicode(content_list.selected_item)
            stems = split_stem(parent_name)
            for stem in stems:
                short_name = make_stem_cleaner(stem)(item_name)
                short_name = full_strip(short_name)
                item_name = short_name if len(short_name) > 4 else item_name
                if is_short_enough(item_name):
                    break

        if len(item_name) >= shortening_limit and item_name[(
                -1)] == consts.CHAR_ELLIPSIS:
            return item_name[:-1]
        return item_name

    def _item_formatter(self, depth, index, item, action_in_progress):
        display_string = ''
        separator_length = len(self._data_sources[(self.COLUMN_SIZE *
                                                   depth)].separator)
        shortening_limit = 16 - separator_length
        if item:
            item_name = 'Loading...' if action_in_progress else self._shorten_item_name(
                shortening_limit, depth + self._scroll_offset, unicode(item))
            display_string = consts.CHAR_SELECT if item and item.is_selected else ' '
            display_string += item_name
            if depth == len(
                    self._list_components
            ) - 1 and item.is_selected and self._scroll_offset < self._max_hierarchy:
                display_string = string.ljust(display_string,
                                              consts.DISPLAY_LENGTH / 4 - 1)
                shortening_limit += 1
                display_string = display_string[:shortening_limit] + consts.CHAR_ARROW_RIGHT
            if depth == 0 and self._scroll_offset > 0:
                prefix = consts.CHAR_ARROW_LEFT if index == 0 else ' '
                display_string = prefix + display_string
        return display_string[:shortening_limit + 1]

    @enter_button.pressed
    def enter_button(self, control):
        self._set_scroll_offset(
            min(self._max_scroll_offset, self._scroll_offset + 1))

    @exit_button.pressed
    def exit_button(self, control):
        self._set_scroll_offset(max(0, self._scroll_offset - 1))

    @prehear_button.toggled
    def prehear_button(self, toggled, button):
        if not toggled:
            self._browser.stop_preview()
        elif self._last_selected_item is not None:
            self._last_selected_item.preview()
        self._preferences['browser_prehear'] = toggled
        return

    @listens('hotswap_target')
    def _on_hotswap_target_changed(self):
        if not self._skip_next_preselection:
            self._set_scroll_offset(0)
        self._update_browser_model()

    @listens('filter_type')
    def _on_filter_type_changed(self):
        self._update_browser_model()

    @listens('full_refresh')
    def _on_browser_full_refresh(self):
        self._browser_model_dirty = True

    def _update_browser_model(self):
        if self.is_enabled():
            self._do_update_browser_model()

    def _create_browser_model_of_type(self, filter_type):
        self._last_filter_type = filter_type
        new_model = self._make_browser_model(self._browser, filter_type)
        if self._browser_model and self._browser_model.can_be_exchanged(
                new_model) and new_model.can_be_exchanged(self._browser_model):
            self._browser_model.exchange_model(new_model)
            new_model.disconnect()
        else:
            self.disconnect_disconnectable(self._browser_model)
            self._browser_model = self.register_disconnectable(new_model)
            self._on_content_lists_changed.subject = self._browser_model
            self._on_selection_updated.subject = self._browser_model
        self._browser_model.update_content()

    def _do_update_browser_model(self):
        filter_type = filter_type_for_browser(self._browser)
        if filter_type != self._last_filter_type:
            self._create_browser_model_of_type(filter_type)
        elif self._browser_model_dirty:
            self._browser_model.update_content()
        elif not self._skip_next_preselection:
            self._browser_model.update_selection()
        self._skip_next_preselection = False
        self._browser_model_dirty = False

    @listens_group('item_action')
    def _on_list_item_action(self, item, _):
        self.notify_load_item(item.content)

    @listens_group('selected_item')
    def _on_selected_item(self, item, _):
        if item is not None and self.prehear_button.is_toggled:
            item.preview()
        self._last_selected_item = item
        return

    @listens('selection_updated')
    def _on_selection_updated(self, index):
        more_content_available = len(self._browser_model.content_lists
                                     ) > self.NUM_COLUMNS + self._scroll_offset
        required_scroll_offset = index - (self.NUM_COLUMNS - 1)
        if more_content_available and required_scroll_offset > self._scroll_offset:
            self._set_scroll_offset(self._scroll_offset + 1)
            self._browser_model.update_selection()

    @listens('content_lists')
    def _on_content_lists_changed(self):
        self._last_selected_item = None
        components = self._list_components
        contents = self._browser_model.content_lists[self._scroll_offset:]
        messages = self._browser_model.empty_list_messages
        scroll_depth = len(self._browser_model.content_lists) - len(
            self._list_components)
        self._max_scroll_offset = max(0, scroll_depth + 2)
        self._max_hierarchy = max(0, scroll_depth)
        for component, content, message in izip_longest(
                components, contents, messages):
            if component != None:
                component.scrollable_list = content
                component.empty_list_message = message

        active_lists = len(contents)
        num_head = clamp(active_lists - 1, 0, self.NUM_COLUMNS - 1)
        head = components[:num_head]
        last = components[num_head:]

        def set_data_sources_with_separator(component, sources, separator):
            for source in sources:
                source.separator = separator

            component.set_data_sources(sources)
            component.set_enabled(True)

        for idx, component in enumerate(head):
            offset = idx * self.COLUMN_SIZE
            sources = self._data_sources[offset:offset + self.COLUMN_SIZE]
            set_data_sources_with_separator(component, sources, '|')

        if last:
            offset = num_head * self.COLUMN_SIZE
            scrollable_list = last[0].scrollable_list
            if scrollable_list and find_if(lambda item: item.content.is_folder,
                                           scrollable_list.items):
                sources = self._data_sources[offset:offset + self.COLUMN_SIZE]
                map(DisplayDataSource.clear,
                    self._data_sources[offset + self.COLUMN_SIZE:])
            else:
                sources = self._data_sources[offset:]
            set_data_sources_with_separator(last[0], sources, '')
            for component in last[1:]:
                component.set_enabled(False)

        self.set_select_buttons(self._select_buttons)
        self.set_state_buttons(self._state_buttons)
        self.set_encoder_controls(self._encoder_controls)
        self._update_navigation_button_state()
        return

    @listens('value')
    def _on_select_matrix_value(self, value, *_):
        pass

    @listens('value')
    def _on_state_matrix_value(self, value, *_):
        pass

    @listens('value')
    def _on_encoder_matrix_value(self, value, *_):
        pass
예제 #26
0
class GenericDeviceComponent(DeviceComponentBase):
    parameter_touch_buttons = control_list(ButtonControl, control_count=8)
    shift_button = ButtonControl()

    def __init__(self, visualisation_real_time_data=None, delete_button=None, *a, **k):
        super(GenericDeviceComponent, self).__init__(*a, **k)
        self._visualisation_real_time_data = visualisation_real_time_data
        self._delete_button = delete_button
        self.default_sensitivity = partial(self._sensitivity, DEFAULT_SENSITIVITY_KEY)
        self.fine_sensitivity = partial(self._sensitivity, FINE_GRAINED_SENSITIVITY_KEY)

    def set_device(self, device):
        self._set_device(device)

    def _set_device(self, device):
        super(GenericDeviceComponent, self)._set_device(device)
        self.notify_options()

    def _on_device_changed(self, device):
        u"""
        Override the base class behaviour, which calls _set_device when a device in
        the device provider changes. It's already donne by the DeviceComponentProvider.
        """
        pass

    @parameter_touch_buttons.pressed
    def parameter_touch_buttons(self, button):
        if button.index < len(self._provided_parameters):
            parameter = self._provided_parameters[button.index].parameter
            self._parameter_touched(parameter)

    @parameter_touch_buttons.released
    def parameter_touch_buttons(self, button):
        if button.index < len(self._provided_parameters):
            parameter = self._provided_parameters[button.index].parameter
            self._parameter_released(parameter)

    def parameters_changed(self):
        pass

    def _parameter_touched(self, parameter):
        pass

    def _parameter_released(self, parameter):
        pass

    @shift_button.pressed
    def shift_button(self, button):
        self._shift_button_pressed(button)

    @shift_button.released
    def shift_button(self, button):
        self._shift_button_released(button)

    def _shift_button_pressed(self, button):
        pass

    def _shift_button_released(self, button):
        pass

    def initialize_visualisation_view_data(self):
        view_data = self._initial_visualisation_view_data()
        if view_data:
            self._update_visualisation_view_data(view_data)

    def _update_visualisation_view_data(self, view_data):
        visualisation = self._visualisation_real_time_data.device_visualisation()
        if liveobj_valid(visualisation):
            visualisation_view_data = visualisation.get_view_data()
            for key, value in view_data.iteritems():
                visualisation_view_data[key] = value

            visualisation.set_view_data(visualisation_view_data)

    def _initial_visualisation_view_data(self):
        return {}

    def _is_parameter_available(self, parameter):
        return True

    def _create_parameter_info(self, parameter, name):
        return ParameterInfo(parameter=parameter if self._is_parameter_available(parameter) else None, name=name, default_encoder_sensitivity=self.default_sensitivity(parameter), fine_grain_encoder_sensitivity=self.fine_sensitivity(parameter))

    def _sensitivity(self, sensitivity_key, parameter):
        device = self.device()
        sensitivity = parameter_sensitivities(device.class_name, parameter)[sensitivity_key]
        if liveobj_valid(parameter):
            sensitivity = self._adjust_parameter_sensitivity(parameter, sensitivity)
        return sensitivity

    def _adjust_parameter_sensitivity(self, parameter, sensitivity):
        return sensitivity

    @listenable_property
    def options(self):
        return getattr(self._bank, 'options', [None] * OPTIONS_PER_BANK)

    @property
    def bank_view_description(self):
        return getattr(self._bank, 'bank_view_description', '')

    @listenable_property
    def visualisation_visible(self):
        return self._visualisation_visible

    @property
    def _visualisation_visible(self):
        return False

    @listenable_property
    def shrink_parameters(self):
        return self._shrink_parameters

    @property
    def _shrink_parameters(self):
        return [
         False] * 8

    @listens('options')
    def __on_options_changed(self):
        self.notify_options()

    def _setup_bank(self, device):
        super(GenericDeviceComponent, self)._setup_bank(device, bank_factory=create_device_bank_with_options)
        try:
            self.__on_options_changed.subject = self._bank
        except EventError:
            pass
class SpecialSessionComponent(SessionComponent):
    """
    Special session subclass that handles ConfigurableButtons
    and has a button to fire the selected clip slot.
    """
    _session_component_ends_initialisation = False
    scene_component_type = SpecialSceneComponent
    duplicate_button = ButtonControl()

    def __init__(self,
                 clip_slot_copy_handler=None,
                 fixed_length_recording=None,
                 *a,
                 **k):
        self._clip_copy_handler = clip_slot_copy_handler or ClipSlotCopyHandler(
        )
        self._fixed_length_recording = fixed_length_recording
        with inject(copy_handler=const(self._clip_copy_handler),
                    fixed_length_recording=const(
                        self._fixed_length_recording)).everywhere():
            super(SpecialSessionComponent, self).__init__(*a, **k)
        self._slot_launch_button = None
        self._duplicate_button = None
        self._duplicate = DuplicateSceneComponent(self._session_ring,
                                                  parent=self)
        self._duplicate_enabler = EnablingModesComponent(
            parent=self, component=self._duplicate)
        self._end_initialisation()
        return

    duplicate_layer = forward_property(b'_duplicate')(b'layer')

    @duplicate_button.pressed
    def duplicate_button(self, button):
        self._duplicate_enabler.selected_mode = b'enabled'

    @duplicate_button.released
    def duplicate_button(self, button):
        self._duplicate_enabler.selected_mode = b'disabled'
        self._clip_copy_handler.stop_copying()

    def set_slot_launch_button(self, button):
        self._slot_launch_button = button
        self._on_slot_launch_value.subject = button

    def set_clip_launch_buttons(self, buttons):
        if buttons:
            buttons.reset()
        super(SpecialSessionComponent, self).set_clip_launch_buttons(buttons)

    def set_touch_strip(self, touch_strip):
        if touch_strip:
            touch_strip.set_mode(TouchStripModes.CUSTOM_FREE)
            touch_strip.send_state([
                TouchStripStates.STATE_OFF
                for _ in xrange(touch_strip.state_count)
            ])
        self._on_touch_strip_value.subject = touch_strip

    @listens(b'value')
    def _on_touch_strip_value(self, value):
        pass

    @listens(b'value')
    def _on_slot_launch_value(self, value):
        if self.is_enabled():
            if value != 0 or not self._slot_launch_button.is_momentary():
                if liveobj_valid(self.song.view.highlighted_clip_slot):
                    self.song.view.highlighted_clip_slot.fire()
                self._slot_launch_button.turn_on()
            else:
                self._slot_launch_button.turn_off()
class MuteSoloStopClipComponent(CompoundComponent, Messenger):
    MESSAGE_FOR_MODE = {
        'mute': 'Mute: %s',
        'solo': 'Solo: %s',
        'stop': 'Stop Clips: %s'
    }
    stop_all_clips_button = ButtonControl()

    def __init__(self,
                 item_provider=None,
                 solo_track_button=None,
                 mute_track_button=None,
                 stop_clips_button=None,
                 track_list_component=None,
                 cancellation_action_performers=[],
                 *a,
                 **k):
        super(MuteSoloStopClipComponent, self).__init__(*a, **k)
        self._currently_locked_button_handler = None
        self._track_list = track_list_component
        self._solo_button_handler = self.register_component(
            GlobalMixerActionComponent(
                track_list_component=track_list_component,
                mode='solo',
                mode_active_color='Mixer.SoloOn',
                mode_locked_color='Mixer.LockedSoloMode',
                default_color_indicator=TrackStateColorIndicator(
                    item_provider=item_provider,
                    track_property='solo',
                    property_active_color='Mixer.SoloOn',
                    song=self.song),
                immediate_action=lambda: toggle_mixable_solo(
                    item_provider.selected_item, self.song)))
        self._solo_button_handler.layer = Layer(
            action_button=solo_track_button)
        self._mute_button_handler = self.register_component(
            GlobalMixerActionComponent(
                track_list_component=track_list_component,
                mode='mute',
                mode_active_color='Mixer.MuteOff',
                mode_locked_color='Mixer.LockedMuteMode',
                default_color_indicator=TrackStateColorIndicator(
                    item_provider=item_provider,
                    track_property='mute',
                    property_active_color='Mixer.MuteOff',
                    song=self.song),
                immediate_action=lambda: toggle_mixable_mute(
                    item_provider.selected_item, self.song)))
        self._mute_button_handler.layer = Layer(
            action_button=mute_track_button)
        self._stop_button_handler = self.register_component(
            GlobalMixerActionComponent(
                track_list_component=track_list_component,
                mode='stop',
                immediate_action=partial(stop_clip_in_selected_track,
                                         self.song),
                mode_locked_color='StopClips.LockedStopMode'))
        self._stop_button_handler.layer = Layer(
            action_button=stop_clips_button)
        self.__on_mute_solo_stop_cancel_action_performed.replace_subjects(
            [track_list_component] + cancellation_action_performers)
        button_handlers = (self._mute_button_handler,
                           self._solo_button_handler,
                           self._stop_button_handler)
        self.__on_mode_locked_changed.replace_subjects(button_handlers,
                                                       button_handlers)
        self.__on_selected_item_changed.subject = item_provider
        return

    @stop_all_clips_button.pressed
    def stop_all_clips_button(self, button):
        self.song.stop_all_clips()

    @listens_group('mute_solo_stop_cancel_action_performed')
    def __on_mute_solo_stop_cancel_action_performed(self, _):
        self.cancel_release_actions()

    @listens_group('mode_locked')
    def __on_mode_locked_changed(self, is_locked, button_handler):
        if is_locked:
            if self._currently_locked_button_handler:
                self._currently_locked_button_handler.cancel_locked_mode()
            self._currently_locked_button_handler = button_handler
        else:
            self._currently_locked_button_handler = None
        self._track_list.locked_mode = self._currently_locked_button_handler.mode if is_locked else None
        self._show_mode_lock_change_notification(is_locked,
                                                 button_handler.mode)
        return

    @listens('selected_item')
    def __on_selected_item_changed(self):
        self.cancel_release_actions()

    def _show_mode_lock_change_notification(self, is_locked, mode):
        message_template = self.MESSAGE_FOR_MODE[mode]
        message_part = 'Locked' if is_locked else 'Unlocked'
        self.show_notification(message_template % message_part)

    def cancel_release_actions(self):
        self._solo_button_handler.cancel_release_action()
        self._mute_button_handler.cancel_release_action()
        self._stop_button_handler.cancel_release_action()
class FixedLengthSessionRecordingComponent(SessionRecordingComponent,
                                           Messenger):
    foot_switch_button = ButtonControl()
    arrangement_record_button = ButtonControl()
    capture_midi_button = ButtonControl()

    def __init__(self, clip_creator=None, fixed_length_setting=None, *a, **k):
        (super(FixedLengthSessionRecordingComponent, self).__init__)(*a, **k)
        self._fixed_length_recording = self.register_disconnectable(
            FixedLengthRecording(song=(self.song),
                                 clip_creator=clip_creator,
                                 fixed_length_setting=fixed_length_setting))
        self.footswitch_toggles_arrangement_recording = False
        self._FixedLengthSessionRecordingComponent__on_record_mode_changed.subject = self.song
        self._FixedLengthSessionRecordingComponent__on_record_mode_changed()
        self.set_trigger_recording_on_release(not any((
            self.record_button.is_pressed,
            self.arrangement_record_button.is_pressed)))

    def set_trigger_recording_on_release(self, trigger_recording):
        self._should_trigger_recording = trigger_recording

    @foot_switch_button.pressed
    def foot_switch_button(self, button):
        if self.footswitch_toggles_arrangement_recording:
            self._toggle_arrangement_recording()
        else:
            self._trigger_recording()

    @capture_midi_button.pressed
    def capture_midi_button(self, button):
        try:
            self.song.capture_midi()
            self.set_trigger_recording_on_release(not any((
                self.record_button.is_pressed,
                self.arrangement_record_button.is_pressed)))
        except RuntimeError:
            pass

    @arrangement_record_button.pressed
    def arrangement_record_button(self, _):
        self._on_arrangement_record_button_pressed()

    @arrangement_record_button.released
    def arrangement_record_button(self, _):
        self._on_arrangement_record_button_released()

    def _toggle_arrangement_recording(self):
        self.song.record_mode = not self.song.record_mode

    def _on_record_button_pressed(self):
        pass

    def _on_record_button_released(self):
        self._trigger_recording_action(self._trigger_recording)

    def _on_arrangement_record_button_pressed(self):
        pass

    def _on_arrangement_record_button_released(self):
        self._trigger_recording_action(self._toggle_arrangement_recording)

    def _trigger_recording_action(self, recording_action):
        if self._should_trigger_recording:
            recording_action()
        self._should_trigger_recording = True

    def _clip_slot_index_to_record_into(self):
        song = self.song
        selected_scene = song.view.selected_scene
        return list(song.scenes).index(selected_scene)

    def _update_record_button(self):
        if self.is_enabled():
            song = self.song
            clip_slot = get_clip_slot_from_index(
                song, song.view.selected_track,
                self._clip_slot_index_to_record_into())
            if liveobj_valid(clip_slot):
                if clip_slot.is_triggered:
                    if song.overdub and not clip_slot.is_recording:
                        self.record_button.color = 'Recording.Transition'
                    elif song.record_mode:
                        self.record_button.color = 'Recording.ArrangementRecordingOn'
                    else:
                        super(FixedLengthSessionRecordingComponent,
                              self)._update_record_button()
                self.arrangement_record_button.color = self.record_button.color

    @listens('record_mode')
    def __on_record_mode_changed(self):
        self._update_record_button()

    def _start_recording(self):
        track = self.song.view.selected_track
        clip_slot_index = self._clip_slot_index_to_record_into()
        self._fixed_length_recording.start_recording_in_slot(
            track, clip_slot_index)
        if track_can_record(track):
            self._ensure_slot_is_visible(self.song.view.selected_track,
                                         clip_slot_index)

    def _ensure_slot_is_visible(self, track, scene_index):
        song = self.song
        if song.view.selected_track == track:
            song.view.selected_scene = song.scenes[scene_index]
        self._view_selected_clip_detail()

    def _handle_limitation_error_on_scene_creation(self):
        self.expect_dialog(MessageBoxText.SCENE_LIMIT_REACHED)
class SimpleDeviceParameterComponent(Component):
    prev_bank_button = ButtonControl()
    next_bank_button = ButtonControl()
    device_lock_button = ToggleButtonControl()
    device_on_off_button = ToggleButtonControl()
    device_name_display = DisplayControl()

    @depends(device_provider=None)
    def __init__(self, device_provider = None, device_bank_registry = None, toggle_lock = None, *a, **k):
        super(SimpleDeviceParameterComponent, self).__init__(*a, **k)
        self._toggle_lock = toggle_lock
        self._device = None
        self._banks = []
        self._bank_index = 0
        self._parameter_controls = None
        self._empty_control_slots = self.register_disconnectable(EventObject())
        self._device_bank_registry = device_bank_registry
        self._device_provider = device_provider
        self._device_name_slot = self.register_slot(None, self._update_device_name_display, u'name')
        self.__on_provided_device_changed.subject = device_provider
        self.__on_provided_device_changed()
        self._display_bank_name_task = self._tasks.add(task.sequence(task.run(self._display_bank_name), task.wait(BANK_NAME_DISPLAY_DURATION), task.run(self._update_device_name_display)))
        self._display_bank_name_task.kill()
        if toggle_lock:
            self.__on_is_locked_to_device_changed.subject = self._device_provider
            self.__on_is_locked_to_device_changed()

    @device_lock_button.toggled
    def device_lock_button(self, *_):
        self._on_device_lock_button_toggled()

    @device_on_off_button.toggled
    def device_on_off_button(self, is_toggled, _):
        parameter = self._on_off_parameter()
        if parameter is not None:
            parameter.value = float(is_toggled)

    def _on_device_lock_button_toggled(self):
        self._toggle_lock()
        self._update_device_lock_button()

    @prev_bank_button.pressed
    def prev_bank_button(self, _):
        self.bank_index = self._bank_index - 1

    @next_bank_button.pressed
    def next_bank_button(self, _):
        self.bank_index = self._bank_index + 1

    @property
    def bank_index(self):
        return self._bank_index

    @bank_index.setter
    def bank_index(self, value):
        prev_bank_index = self._bank_index
        self._bank_index = self._clamp_to_bank_size(value)
        if prev_bank_index != self._bank_index:
            self._display_bank_name_task.restart()
        if self._device_bank_registry:
            self._device_bank_registry.set_device_bank(self._device, self._bank_index)
        self.update()

    def _clamp_to_bank_size(self, value):
        return clamp(value, 0, self.num_banks - 1)

    @property
    def selected_bank(self):
        if self.num_banks:
            return self._banks[self._bank_index or 0]
        return []

    @property
    def num_banks(self):
        return len(self._banks)

    def set_parameter_controls(self, controls):
        for control in self._parameter_controls or []:
            release_control(control)

        self._parameter_controls = controls
        self.update()

    @listens(u'device')
    def __on_provided_device_changed(self):
        for control in self._parameter_controls or []:
            release_control(control)

        self._device = self._device_provider.device
        self.__on_bank_changed.subject = self._device_bank_registry
        if self._device_bank_registry:
            self._bank_index = self._device_bank_registry.get_device_bank(self._device)
            self.update()
        else:
            self.bank_index = 0
        self._device_name_slot.subject = self._device
        self._update_device_name_display()

    @listens(u'device_bank')
    def __on_bank_changed(self, device, bank):
        if device == self._device:
            self.bank_index = bank

    @listens(u'is_locked_to_device')
    def __on_is_locked_to_device_changed(self):
        self._update_device_lock_button()

    @listens(u'value')
    def __on_device_on_off_changed(self):
        self._update_device_on_off_button()

    def update(self):
        super(SimpleDeviceParameterComponent, self).update()
        if self.is_enabled():
            self._update_parameter_banks()
            self._update_bank_navigation_buttons()
            self._empty_control_slots.disconnect()
            self.__on_device_on_off_changed.subject = self._on_off_parameter()
            self._update_device_on_off_button()
            if liveobj_valid(self._device):
                self._connect_parameters()
            else:
                self._disconnect_parameters()
        else:
            self._disconnect_parameters()

    def _disconnect_parameters(self):
        for control in self._parameter_controls or []:
            release_control(control)
            self._empty_control_slots.register_slot(control, nop, u'value')

    def _connect_parameters(self):
        for control, parameter in zip_longest(self._parameter_controls or [], self.selected_bank):
            if liveobj_valid(control):
                if liveobj_valid(parameter):
                    control.connect_to(parameter)
                else:
                    control.release_parameter()
                    self._empty_control_slots.register_slot(control, nop, u'value')

    def _on_off_parameter(self):
        if liveobj_valid(self._device):
            for p in self._device.parameters:
                if p.name.startswith(u'Device On') and liveobj_valid(p) and p.is_enabled:
                    return p

    def _update_parameter_banks(self):
        if liveobj_valid(self._device):
            self._banks = parameter_banks(self._device)
        else:
            self._banks = []
        self._bank_index = self._clamp_to_bank_size(self._bank_index)

    def _update_bank_navigation_buttons(self):
        self.prev_bank_button.enabled = self.bank_index > 0
        self.next_bank_button.enabled = self.bank_index < self.num_banks - 1

    def _update_device_lock_button(self):
        self.device_lock_button.is_toggled = self._device_provider.is_locked_to_device

    def _update_device_on_off_button(self):
        parameter = self._on_off_parameter()
        self.device_on_off_button.enabled = parameter is not None
        if parameter is not None:
            self.device_on_off_button.is_toggled = parameter.value > 0

    def _update_device_name_display(self):
        self.device_name_display.message = self._device.name if liveobj_valid(self._device) else u' - '

    def _display_bank_name(self):
        names = parameter_bank_names(self._device)
        if self.bank_index < len(names):
            self.device_name_display.message = names[self.bank_index]