Esempio n. 1
0
 def _create_main_action(self):
     self.actions = ActionGroup(self.shell, 'LooperActionGroup')
     self.actions.add_action(func=self.on_activation,
                             action_name='ActivateLooper',
                             label=_("Looper"),
                             action_state=ActionGroup.TOGGLE,
                             action_type='app',
                             accel="<Ctrl>e",
                             tooltip=_("Loop part of the song"))
     self.appshell.insert_action_group(self.actions)
     self.appshell.add_app_menuitems(self.UI, 'LooperActionGroup', 'view')
     self.action = self.appshell.lookup_action('LooperActionGroup',
                                               'ActivateLooper', 'app')
Esempio n. 2
0
 def create_main_action(self):
     self.actions = ActionGroup(self.shell, 'LooperActionGroup')
     self.actions.add_action(func=self.on_activation,
                             action_name='ActivateLooper',
                             label=_("Looper"),
                             action_state=ActionGroup.TOGGLE,
                             action_type='app', accel="<Ctrl>e",
                             tooltip=_("Loop part of the song"))
     self.appshell.insert_action_group(self.actions)
     self.appshell.add_app_menuitems(self.UI, 'LooperActionGroup', 'view')
     self.action = self.appshell.lookup_action('LooperActionGroup',
                                               'ActivateLooper', 'app')
Esempio n. 3
0
class LooperPlugin(GObject.Object, Peas.Activatable):
    """
    Loops part of the song defined by Start and End Gtk sliders.
    """
    object = GObject.property(type=GObject.Object)

    # Available positions in the RB GUI.
    # They are chosen through user settings.
    POSITIONS = {
        'TOP': RB.ShellUILocation.MAIN_TOP,
        'BOTTOM': RB.ShellUILocation.MAIN_BOTTOM,
        'SIDEBAR': RB.ShellUILocation.SIDEBAR,
        'RIGHT SIDEBAR': RB.ShellUILocation.RIGHT_SIDEBAR,
    }

    STATUS_TPL = Template('[Loop Duration: $duration] ' +
                          '[Current time: $time]')

    # Number of seconds that the End slider is less than a song
    # duration. Its needed because in that period Rhythmbox would
    # change to the next song. Thats why we dont go there.
    SEC_BEFORE_END = 3

    LOOPS_FILENAME = '.loops.json'

    LOOPS_PER_ROW = 8

    MAX_LOOPS_NUM = 32

    UI = """
    <ui>
        <menubar name="MenuBar">
            <menu name="ViewMenu" action="View">
                <menuitem name="LooperPlugin" action="ActivateLooper" />
            </menu>
        </menubar>
    </ui>
    """

    def __init__(self):
        super(LooperPlugin, self).__init__()

    def do_activate(self):
        self.load_css()
        self.settings = Gio.Settings("org.gnome.rhythmbox.plugins.looper")
        # old value will be needed when removing/adding(moving) GUI
        self.gui_position = self.POSITIONS[self.settings['position']]
        self.shell = self.object
        self.shell_player = self.shell.props.shell_player
        self.player = self.shell_player.props.player
        self.db = self.shell.props.db

        self.appshell = ApplicationShell(self.shell)
        self.main_box = Gtk.Box()
        self.main_box.set_orientation(Gtk.Orientation.VERTICAL)

        self.controls_box = Gtk.Box()
        self.controls_box.set_orientation(Gtk.Orientation.VERTICAL)

        self._create_main_action()

        self.controls = Controls(self)
        controls_frame = Gtk.Frame()
        controls_frame.add(self.controls)
        controls_frame.set_property('margin-left', 2)
        controls_frame.set_property('margin-right', 2)
        self.rbpitch = RbPitch(self)
        rbpitch_frame = Gtk.Frame()
        rbpitch_frame.add(self.rbpitch)
        rbpitch_frame.set_property('margin-left', 2)
        rbpitch_frame.set_property('margin-right', 2)
        self.controls_box.pack_start(rbpitch_frame, True, True, 5)
        self.controls_box.pack_start(controls_frame, True, True, 5)

        self.loops_box = Gtk.Grid()
        self.loops_box.set_row_spacing(2)
        self.loops_box.set_column_spacing(2)
        self.loops_box.set_column_homogeneous(True)
        self.loops_box.set_row_homogeneous(True)
        self.loops_box.set_border_width(0)

        self.rb_slider = self.find_rb_slider()

        self.main_box.pack_start(self.controls_box, True, True, 10)
        self.main_box.pack_start(
            Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL), True, True,
            0)
        self.main_box.pack_start(self.loops_box, True, True, 10)

        # position = self.POSITIONS[self.settings['position']]
        self.shell.add_widget(self.main_box, self.gui_position, True, False)

        self.rbpitch.tempo.slider.set_value(self.rbpitch.tempo_val)
        self.rbpitch.pitch.slider.set_value(self.rbpitch.pitch_val)
        self.rbpitch.rate.slider.set_value(self.rbpitch.rate_val)

        self.save_crossfade_settings()

        self.settings_changed_sigid = self.settings.connect(
            'changed', self.on_settings_changed)

        self.song_changed_sigid = self.shell_player.connect(
            "playing-song-changed", self.on_playing_song_changed)

        # a song COULD be playing, so refresh sliders
        self.refresh_widgets()
        self.refresh_status_label()

        if self.settings['always-show']:
            self.refresh_rb_position_slider()
            self.main_box.show_all()

        self.loops = {}
        self.load_loops_file()
        self.loops_box.hide()

    def load_css(self):
        cssProvider = Gtk.CssProvider()
        css_path = rb.find_plugin_file(self, 'looper.css')
        cssProvider.load_from_path(css_path)
        screen = Gdk.Screen.get_default()
        styleContext = Gtk.StyleContext()
        styleContext.add_provider_for_screen(screen, cssProvider,
                                             Gtk.STYLE_PROVIDER_PRIORITY_USER)

    def on_activation(self, *args):
        action = self.actions.get_action('ActivateLooper')
        if action.get_active():
            # connect elapsed handler/signal to handle the loop
            self.elapsed_changed_sigid = self.shell_player.connect(
                "elapsed-changed", self.loop)

            # Disable cross fade. It interferes at the edges of the song ..
            if (self.crossfade and self.crossfade.get_active()):
                self.crossfade.set_active(False)
            self.refresh_rb_position_slider()
            self.controls_box.show_all()
            if self.shell_player.get_playing_song_duration() > -1:
                self.loops_box.show_all()
        else:
            # disconnect the elapsed handler from hes duty
            self.shell_player.disconnect(self.elapsed_changed_sigid)
            del self.elapsed_changed_sigid
            # Restore users crossfade if it was enabled
            if self.crossfade and self.was_crossfade_active:
                self.crossfade.set_active(True)
            if not self.settings['always-show']:
                self.main_box.hide()
                if hasattr(self, 'rb_slider'):
                    self.rb_slider.clear_marks()
            self.loops_box.hide()

        self.refresh_status_label()

    def _create_main_action(self):
        self.actions = ActionGroup(self.shell, 'LooperActionGroup')
        self.actions.add_action(func=self.on_activation,
                                action_name='ActivateLooper',
                                label=_("Looper"),
                                action_state=ActionGroup.TOGGLE,
                                action_type='app',
                                accel="<Ctrl>e",
                                tooltip=_("Loop part of the song"))
        self.appshell.insert_action_group(self.actions)
        self.appshell.add_app_menuitems(self.UI, 'LooperActionGroup', 'view')
        self.action = self.appshell.lookup_action('LooperActionGroup',
                                                  'ActivateLooper', 'app')

    def find_rb_slider(self):
        rb_toolbar = self.find(self.shell.props.window, 'ToolBar', 'by_name')
        if not rb_toolbar:
            rb_toolbar = self.find(self.shell.props.window, 'main-toolbar',
                                   'by_id')
        return self.find(rb_toolbar, 'GtkScale', 'by_name')

    # Couldn't find better way to find widgets than loop through them
    def find(self, node, search_id, search_type):
        if isinstance(node, Gtk.Buildable):
            if search_type == 'by_id':
                if Gtk.Buildable.get_name(node) == search_id:
                    return node
            elif search_type == 'by_name':
                if node.get_name() == search_id:
                    return node

        if isinstance(node, Gtk.Container):
            for child in node.get_children():
                ret = self.find(child, search_id, search_type)
                if ret:
                    return ret
        return None

    def save_crossfade_settings(self):
        # We need to disable cross fade while Looper is active. So store
        # RB's xfade widget and user preference for later use.
        # Next line throws error in the Rhythmbox's console. Dont know why.
        prefs = self.shell.props.prefs  # <-- error. Unreleased refs maybe.
        self.crossfade = self.find(prefs, 'use_xfade_backend', 'by_id')
        self.was_crossfade_active = False
        if self.crossfade and self.crossfade.get_active():
            self.was_crossfade_active = True

    def on_settings_changed(self, settings, setting):
        """Handles changes to settings."""
        if setting == 'position':
            self.shell.remove_widget(self.main_box, self.gui_position)
            new_gui_position = self.POSITIONS[self.settings['position']]
            self.shell.add_widget(self.main_box, new_gui_position, True, False)
            self.gui_position = new_gui_position
        elif setting == 'always-show':
            action = self.actions.get_action('ActivateLooper')
            if settings['always-show']:
                self.main_box.show_all()
                if action.get_active() is not True:
                    self.loops_box.hide()
            else:
                if action.get_active() is not True:
                    self.main_box.hide()

    def on_playing_song_changed(self, source, user_data):
        """Refresh sliders and RB's position marks."""
        self.refresh_widgets()
        self.clear_loops()
        self.load_song_loops()
        action = self.actions.get_action('ActivateLooper')
        if action.get_active() is True:
            self.refresh_rb_position_slider()
            self.loops_box.show_all()
        else:
            self.loops_box.hide()

    def refresh_song_duration(self):
        duration = self.shell_player.get_playing_song_duration()
        if duration != -1 and duration >= (self.SEC_BEFORE_END + 2):
            self.duration = duration - self.SEC_BEFORE_END
        else:
            self.duration = None

    def refresh_rb_position_slider(self):
        """
        Add marks to RB's position slider with Looper's start/end time values.
        """
        # Add start and end marks to the position slider
        action = self.actions.get_action('ActivateLooper')
        if self.rb_slider and (action.get_active()
                               or self.settings['always-show']):
            self.rb_slider.clear_marks()

            start_time = seconds_to_time(
                self.controls.start_slider.get_value())
            end_time = seconds_to_time(self.controls.end_slider.get_value())

            self.rb_slider.add_mark(self.controls.start_slider.get_value(),
                                    Gtk.PositionType.TOP, start_time)
            self.rb_slider.add_mark(self.controls.end_slider.get_value(),
                                    Gtk.PositionType.TOP, end_time)

    def clear_loops(self):
        for child in self.loops_box.get_children():
            child.deactivate()
            self.loops_box.remove(child)

    def load_song_loops(self):
        song_id = self.get_song_id()
        if song_id:
            if song_id and song_id in self.loops:
                self.load_loops(self.loops[song_id])

    def on_save_loop(self, button):
        song_id = self.get_song_id()
        if song_id:
            if song_id not in self.loops:
                self.loops[song_id] = []
            if len(self.loops[song_id]) >= self.MAX_LOOPS_NUM:
                return
            name = '{} - {}'.format(
                seconds_to_time(self.controls.start_slider.get_value()),
                seconds_to_time(self.controls.end_slider.get_value()),
            )
            loop = {
                'end': self.controls.end_slider.get_value(),
                'start': self.controls.start_slider.get_value(),
                'name': name
            }
            self.loops[song_id].append(loop)
            self.save_loops()
            self.clear_loops()
            self.load_loops(self.loops[song_id])

    def refresh_status_label(self):
        status_vars = {'duration': '00:00', 'time': '00:00'}
        status_text = self.STATUS_TPL.substitute(status_vars)
        self.controls.status_label.set_text(status_text)
        self.controls.status_label.set_fraction(0)

    def refresh_widgets(self):
        self.refresh_song_duration()
        self.controls.refresh_min_range_button()
        self.controls.refresh_sliders()

    def load_loops_file(self):
        loops_file = self.get_loops_file_path()
        if loops_file:
            with open(loops_file, 'r') as f:
                try:
                    self.loops = json.loads(f.read())
                except ValueError as e:
                    sys.stderr.write('Error on loading %s: %s\n' %
                                     (loops_file, e))

    def get_loops_file_path(self):
        home = os.path.expanduser('~')
        loops_file = os.path.join(home, self.LOOPS_FILENAME)
        if not os.path.isfile(loops_file):
            with open(loops_file, 'w') as f:
                data = {}
                f.write(json.dumps(data))
        return loops_file

    def get_grid_column_and_row(self):
        number_of_children = len(self.loops_box.get_children())
        row, column = divmod(number_of_children, self.LOOPS_PER_ROW)
        return column, row

    def get_song_id(self):
        if not self.entry:
            return None
        song_id = u'{0}-{1}'.format(self.song_artist, self.song_title)
        if not song_id:
            song_id = self.entry.get_playback_uri()
        return hashlib.md5(song_id.encode('utf8')).hexdigest()

    @property
    def entry(self):
        return self.shell_player.get_playing_entry()

    @property
    def song_title(self):
        if self.entry:
            return self.entry.get_string(RB.RhythmDBPropType.TITLE)
        return ''

    @property
    def song_artist(self):
        if self.entry:
            return self.entry.get_string(RB.RhythmDBPropType.ARTIST)
        return ''

    @property
    def song_path(self):
        if self.entry:
            return self.entry.get_string(RB.RhythmDBPropType.LOCATION)
        return ''

    def load_loops(self, loops):
        for index, loop in enumerate(loops):
            loop = loops[index]
            loop_control = LoopControl(self, index, loop['name'],
                                       loop['start'], loop['end'])
            # TODO:
            # Use ScrolledWindow instead of limited numbers of loops per grid row.
            # For now we will use limited number of loops per row as the
            # Gtk.ScrolledWindow is not working for some reason.
            row, column = self.get_grid_column_and_row()
            self.loops_box.attach(loop_control, row, column, 1, 1)
        action = self.actions.get_action('ActivateLooper')
        if action.get_active():
            self.loops_box.show_all()

    def save_loops(self):
        Gdk.threads_add_idle(GLib.PRIORITY_DEFAULT_IDLE,
                             self.save_loops_to_file)

    def save_loops_to_file(self):
        if self.loops:
            loops_file = self.get_loops_file_path()
            with open(loops_file, 'w') as f:
                f.write(json.dumps(self.loops))

    @property
    def start_slider_max(self):
        if self.duration:
            return self.duration - MIN_RANGE
        return 0

    @property
    def start_slider_min(self):
        return 0

    @property
    def end_slider_max(self):
        if self.duration:
            return self.duration
        return 0

    @property
    def end_slider_min(self):
        if self.duration:
            return MIN_RANGE
        return 0

    def loop(self, player, elapsed):
        """
        Signal handler called every second of the current playing song.
        Forces the song to stay inside Looper's slider limits.
        """
        # Start and End sliders values
        start = int(self.controls.start_slider.get_value())
        end = int(self.controls.end_slider.get_value())

        if self.rbpitch.gst_pitch and self.controls.rbpitch_btn.get_active(
        ) is True:
            tempo = self.rbpitch.tempo.slider.get_value()
            rate = self.rbpitch.rate.slider.get_value()
            start = math.floor(start / (tempo / 100) / (rate / 100))
            end = math.ceil(end / (tempo / 100) / (rate / 100))

        if elapsed < start:
            # current time is bellow Start slider so fast forward
            seek_time = start - elapsed
        elif elapsed >= end:
            # current time is above End slider so rewind
            seek_time = start - elapsed
        else:
            # current position is within sliders, so chill
            seek_time = False

        # Sometimes song change event interferes with seeking. Therefore
        # dont do anything if elapsed time is 0 (less than 1).
        if seek_time and elapsed > 0:
            try:
                self.shell_player.seek(seek_time)
            except GObject.GError:
                sys.stderr.write('Seek to ' + str(seek_time) + 's failed\n')
        self.update_label(elapsed, start, end)

    def update_label(self, elapsed, start, end):
        """Update label based on current song time and sliders positions."""
        current_loop_seconds = elapsed - start
        loop_duration_seconds = end - start
        if current_loop_seconds > 0 and (current_loop_seconds <=
                                         loop_duration_seconds):
            current_loop_time = seconds_to_time(current_loop_seconds)
            loop_duration = seconds_to_time(loop_duration_seconds)

            label = self.STATUS_TPL.substitute(duration=loop_duration,
                                               time=current_loop_time)
            self.controls.status_label.set_text(label)
            fraction = current_loop_seconds / loop_duration_seconds
            self.controls.status_label.set_fraction(fraction)

    def do_deactivate(self):
        self.save_loops_to_file()

        self.controls.destroy_widgets()
        self.rbpitch.destroy_widgets()

        # Restore users crossfade preference
        if self.crossfade and self.was_crossfade_active:
            self.crossfade.set_active(True)

        self.settings.disconnect(self.settings_changed_sigid)
        self.shell_player.disconnect(self.song_changed_sigid)
        if hasattr(self, 'elapsed_changed_sigid'):
            self.shell_player.disconnect(self.elapsed_changed_sigid)

        self.appshell.cleanup()

        self.main_box.set_visible(False)
        self.shell.remove_widget(self.main_box, RB.ShellUILocation.MAIN_TOP)

        if hasattr(self, 'rb_slider'):
            self.rb_slider.clear_marks()
            del self.rb_slider

        del self.controls
        del self.loops_box
        del self.crossfade
        del self.was_crossfade_active
        del self.shell_player
        del self.shell
        del self.player
        del self.db
        del self.appshell
        del self.main_box
        del self.controls_box
        del self.rbpitch
        del self.actions
        del self.action

    def log(self, *args):
        output = ', '.join([str(arg) for arg in args]) + '\n'
        sys.stdout.write(output)
Esempio n. 4
0
class LooperPlugin(GObject.Object, Peas.Activatable):
    """
    Loops part of the song defined by Start and End Gtk sliders.
    """
    object = GObject.property(type=GObject.Object)

    # Available positions in the RB GUI.
    # They are chosen through user settings.
    POSITIONS = {
        'TOP': RB.ShellUILocation.MAIN_TOP,
        'BOTTOM': RB.ShellUILocation.MAIN_BOTTOM,
        'SIDEBAR': RB.ShellUILocation.SIDEBAR,
        'RIGHT SIDEBAR': RB.ShellUILocation.RIGHT_SIDEBAR,
    }

    STATUS_TPL = Template('[Loop Duration: $duration] ' +
                          '[Current time: $time]')

    # Number of seconds that the End slider is less than a song
    # duration. Its needed because in that period Rhythmbox would
    # change to the next song. Thats why we dont go there.
    SEC_BEFORE_END = 3

    # Minimal allowed range in seconds.
    # (1 second is too small for meaningful sound)
    MIN_RANGE = 2

    LOOPS_FILENAME = 'loops.json'

    LOOPS_PER_ROW = 8

    MAX_LOOPS_NUM = 32

    ON_LABEL = 'Enabled'

    OFF_LABEL = 'Disabled'

    UI = """
    <ui>
        <menubar name="MenuBar">
            <menu name="ViewMenu" action="View">
                <menuitem name="LooperPlugin" action="ActivateLooper" />
            </menu>
        </menubar>
    </ui>
    """

    def __init__(self):
        super(LooperPlugin, self).__init__()

    def do_activate(self):
        self.settings = Gio.Settings("org.gnome.rhythmbox.plugins.looper")
        # old value will be needed when removing/adding(moving) GUI
        self.gui_position = self.POSITIONS[self.settings['position']]
        self.shell = self.object
        self.player = self.shell.props.shell_player
        self.db = self.shell.props.db

        self.create_widgets()
        self.create_main_action()
        self.rb_slider = self.find_rb_slider()
        self.pack_widgets()
        self.save_crossfade_settings()
        self.connect_signals()

        # a song COULD be playing, so refresh sliders
        self.refresh_widgets()
        self.refresh_status_label()

        if self.settings['always-show']:
            self.refresh_rb_position_slider()
            self.main_box.show_all()

        self.load_loops_conf()
        self.loops_box.hide()
        # srv = LooperServer(self)
        # srv.daemon = True
        # srv.start()
        # GObject.threads_init()

    def create_widgets(self):
        """Create Looper's GTK widgets."""
        self.appshell = ApplicationShell(self.shell)
        self.main_box = Gtk.VBox()
        self.controls_box = Gtk.HBox()

        self.loops_box = Gtk.Grid()
        self.loops_box.set_row_spacing(2)
        self.loops_box.set_column_spacing(2)
        self.loops_box.set_column_homogeneous(True)
        self.loops_box.set_row_homogeneous(True)
        self.loops_box.set_border_width(0)

        self.save_loop_btn = Gtk.Button(label='Save loop')

        self.min_range_label = Gtk.Label()
        self.min_range_label.set_text('Min range ')
        adj = Gtk.Adjustment(self.MIN_RANGE, self.MIN_RANGE,
                             self.MIN_RANGE, 1, 10, 0)
        self.min_range = Gtk.SpinButton(adjustment=adj)

        if is_rb3(self.shell):
            self.activation_btn = Gtk.Button(self.OFF_LABEL)
        else:
            self.activation_btn = Gtk.CheckButton()
            self.activation_btn.set_related_action(self.action.action)

        self.status_label = Gtk.Label()

        self.start_slider = self.create_slider()
        self.end_slider = self.create_slider()

    def on_activation(self, *args):
        action = self.actions.get_action('ActivateLooper')
        if action.get_active():
            # connect elapsed handler/signal to handle the loop
            self.elapsed_changed_sigid = self.player.connect(
                "elapsed-changed", self.loop)

            # Disable cross fade. It interferes at the edges of the song ..
            if (self.crossfade and self.crossfade.get_active()):
                    self.crossfade.set_active(False)
            self.refresh_rb_position_slider()
            self.main_box.show_all()
            self.loops_box.show_all()
        else:
            # disconnect the elapsed handler from hes duty
            self.player.disconnect(self.elapsed_changed_sigid)
            del self.elapsed_changed_sigid
            # Restore users crossfade if it was enabled
            if self.crossfade and self.was_crossfade_active:
                self.crossfade.set_active(True)
            if not self.settings['always-show']:
                self.main_box.hide()
                if hasattr(self, 'rb_slider'):
                    self.rb_slider.clear_marks()
            self.loops_box.hide()

        self.refresh_status_label()

    def create_slider(self):
        adj = Gtk.Adjustment(0, 0, 0, 1, 1, 0)
        slider = Gtk.Scale(orientation=Gtk.Orientation.HORIZONTAL,
                           adjustment=adj)
        slider.set_digits(0)
        return slider

    def create_main_action(self):
        self.actions = ActionGroup(self.shell, 'LooperActionGroup')
        self.actions.add_action(func=self.on_activation,
                                action_name='ActivateLooper',
                                label=_("Looper"),
                                action_state=ActionGroup.TOGGLE,
                                action_type='app', accel="<Ctrl>e",
                                tooltip=_("Loop part of the song"))
        self.appshell.insert_action_group(self.actions)
        self.appshell.add_app_menuitems(self.UI, 'LooperActionGroup', 'view')
        self.action = self.appshell.lookup_action('LooperActionGroup',
                                                  'ActivateLooper', 'app')

    def find_rb_slider(self):
        rb_toolbar = self.find(self.shell.props.window, 'ToolBar', 'by_name')
        if not rb_toolbar:
            rb_toolbar = self.find(self.shell.props.window,
                                   'main-toolbar', 'by_id')
        return self.find(rb_toolbar, 'GtkScale', 'by_name')

    # Couldn't find better way to find widgets than loop through them
    def find(self, node, search_id, search_type):
        if isinstance(node, Gtk.Buildable):
            if search_type == 'by_id':
                if Gtk.Buildable.get_name(node) == search_id:
                    return node
            elif search_type == 'by_name':
                if node.get_name() == search_id:
                    return node

        if isinstance(node, Gtk.Container):
            for child in node.get_children():
                ret = self.find(child, search_id, search_type)
                if ret:
                    return ret
        return None

    def pack_widgets(self):
        """Pack Looper's widgets and add Looper to Rhythmbox."""
        self.controls_box.pack_start(self.min_range_label, False, False, 5)
        self.controls_box.pack_start(self.min_range, False, False, 0)
        self.controls_box.pack_start(self.status_label, True, False, 0)
        self.controls_box.pack_start(self.start_slider, True, True, 0)
        self.controls_box.pack_start(self.end_slider, True, True, 0)
        self.controls_box.pack_start(self.save_loop_btn, True, True, 5)
        self.controls_box.pack_start(self.activation_btn, True, True, 0)
        self.main_box.pack_start(self.controls_box, True, True, 0)
        self.main_box.pack_start(self.loops_box, True, True, 0)
        position = self.POSITIONS[self.settings['position']]
        self.shell.add_widget(self.main_box, position, True, False)

    def save_crossfade_settings(self):
        # We need to disable cross fade while Looper is active. So store
        # RB's xfade widget and user preference for later use.
        # Next line throws error in the Rhythmbox's console. Dont know why.
        prefs = self.shell.props.prefs  # <-- error. Unreleased refs maybe.
        self.crossfade = self.find(prefs, 'use_xfade_backend', 'by_id')
        self.was_crossfade_active = False
        if self.crossfade and self.crossfade.get_active():
            self.was_crossfade_active = True

    def connect_signals(self):
        """Connects all needed GTK signals."""
        self.settings_changed_sigid = self.settings.connect(
            'changed', self.on_settings_changed)

        if is_rb3(self.shell):
            # In RB3 plugins cannot be activated from custom buttons so
            # we need to hack it with dummy button that will emit activation
            # on click. If Looper is activated through Rhythmbox just change
            # activation_btn label else if Looper is activated through
            # our button, change the button label and emit a signal for
            # Rhythmbox.
            self.action_sigid = self.action.connect(
                'change-state', self.on_rb_activation, '')
            self.activation_btn_sigid = self.activation_btn.connect(
                'clicked', self.on_btn_activation)

        self.min_range_sigid = self.min_range.connect(
            'value-changed', self.on_min_range_changed)

        self.song_changed_sigid = self.player.connect(
            "playing-song-changed", self.on_playing_song_changed)

        self.start_slider_changed_sigid = self.start_slider.connect(
            "value-changed", self.on_slider_moved, 'start')
        self.end_slider_changed_sigid = self.end_slider.connect(
            "value-changed", self.on_slider_moved, 'end')

        # Start and End slider values need to be formated from
        # seconds to (MM:SS)
        self.start_slider_value_sigid = self.start_slider.connect(
            "format-value", self.on_format_slider_value)
        self.end_slider_value_sigid = self.end_slider.connect(
            "format-value", self.on_format_slider_value)

        self.save_loop_btn_sigid = self.save_loop_btn.connect(
            'clicked', self.on_save_loop)

    def on_settings_changed(self, settings, setting):
        """Handles changes to settings."""
        if setting == 'position':
            self.shell.remove_widget(self.main_box, self.gui_position)
            position = self.POSITIONS[self.settings['position']]
            self.shell.add_widget(self.main_box, position, True, False)
            self.gui_position = self.POSITIONS[self.settings['position']]
        elif setting == 'always-show':
            if settings['always-show']:
                self.main_box.show_all()
            else:
                action = self.actions.get_action('ActivateLooper')
                if action.get_active() is not True:
                    self.main_box.hide()

    def on_rb_activation(self, action, state, data):
        """
        Change our custom activation button label as appropriate for the
        current activation state.
        """
        if state:
            self.activation_btn.set_label(self.ON_LABEL)
        else:
            self.activation_btn.set_label(self.OFF_LABEL)

    def on_btn_activation(self, button):
        """
        Change our custom activation button label as appropriate for the
        next activation state and send a signal to Rhythmbox to change
        the active state.
        """
        label = self.activation_btn.get_label()
        # It seems that the value of state parameter (True/False) has no
        # effect on self.action.action.emit('activate', state) result, But
        # it's required.
        if label == self.ON_LABEL:
            state = GLib.Variant('b', False)
            label = self.OFF_LABEL
        else:
            state = GLib.Variant('b', True)
            label = self.ON_LABEL
        self.action.action.emit('activate', state)

    def on_min_range_changed(self, spinner):
        # simulate slider moved event so sliders obey new min_range value
        self.on_slider_moved(self.start_slider, 'start')

    def on_slider_moved(self, slider, moving_slider):
        """Dont let Start slider be greater than End or vice versa."""
        start_value = self.start_slider.get_value()
        end_value = self.end_slider.get_value()
        min_range = self.min_range.get_value_as_int()
        if moving_slider == 'start':
            slider_start_max = end_value - min_range
            if start_value > slider_start_max:
                if self.duration and end_value >= self.duration:
                    new_value = end_value - min_range
                    self.start_slider.set_value(new_value)
                else:
                    new_value = start_value + min_range
                    self.end_slider.set_value(new_value)
        else:
            slider_end_min = start_value + min_range
            if end_value < slider_end_min:
                if start_value == 0.0:
                    new_value = start_value + min_range
                    self.end_slider.set_value(new_value)
                else:
                    new_value = end_value - min_range
                    self.start_slider.set_value(new_value)

        self.refresh_rb_position_slider()

    def on_playing_song_changed(self, source, user_data):
        """Refresh sliders and RB's position marks."""
        self.refresh_widgets()
        self.clear_loops()
        self.load_song_loops()
        action = self.actions.get_action('ActivateLooper')
        if action.get_active() is True:
            self.refresh_rb_position_slider()
            self.loops_box.show_all()
        else:
            self.loops_box.hide()

    def refresh_song_duration(self):
        duration = self.player.get_playing_song_duration()
        if duration != -1 and duration >= (self.SEC_BEFORE_END + 2):
            self.duration = duration - self.SEC_BEFORE_END
        else:
            self.duration = None

    def refresh_min_range_button(self):
        current_value = self.min_range.get_value_as_int()
        if self.duration is None:
            lower_limit = self.MIN_RANGE
            upper_limit = self.MIN_RANGE
            current_value = self.MIN_RANGE
        else:
            lower_limit = self.MIN_RANGE
            upper_limit = self.duration

        if current_value > upper_limit:
            current_value = upper_limit

        adj = self.min_range.get_adjustment()
        adj.set_lower(lower_limit)
        adj.set_upper(upper_limit)
        adj.set_value(current_value)
        self.min_range.set_numeric(True)
        self.min_range.set_update_policy(1)

    def refresh_sliders(self):
        """Set the Looper's slider boundries to the current song duration."""
        if self.duration:
            start_adj = self.start_slider.get_adjustment()
            start_adj.set_lower(self.start_slider_min)
            start_adj.set_upper(self.start_slider_max)
            start_adj.set_value(0)

            end_adj = self.end_slider.get_adjustment()
            end_adj.set_lower(self.end_slider_min)
            end_adj.set_upper(self.end_slider_max)
            end_adj.set_value(self.duration)

    def refresh_rb_position_slider(self):
        """
        Add marks to RB's position slider with Looper's start/end time values.
        """
        # Add start and end marks to the position slider
        action = self.actions.get_action('ActivateLooper')
        if self.rb_slider and (action.get_active() or self.settings['always-show']):
            self.rb_slider.clear_marks()

            start_time = self.seconds_to_time(self.start_slider.get_value())
            end_time = self.seconds_to_time(self.end_slider.get_value())

            self.rb_slider.add_mark(self.start_slider.get_value(),
                                             Gtk.PositionType.TOP, start_time)
            self.rb_slider.add_mark(self.end_slider.get_value(),
                                             Gtk.PositionType.TOP, end_time)

    def clear_loops(self):
        for child in self.loops_box.get_children():
            child.deactivate()
            self.loops_box.remove(child)

    def load_song_loops(self):
        song_id = self.get_song_id()
        if song_id:
            if song_id and song_id in self.loops:
                self.load_loops(self.loops[song_id])

    def on_format_slider_value(self, scale, value):
        return self.seconds_to_time(value)

    def seconds_to_time(self, seconds):
        """Converts seconds to time format (MM:SS)."""
        m, s = divmod(int(seconds), 60)
        return "%02d:%02d" % (m, s)

    def on_save_loop(self, button):
        song_id = self.get_song_id()
        if song_id:
            if song_id not in self.loops:
                self.loops[song_id] = []
            if len(self.loops[song_id]) >= self.MAX_LOOPS_NUM:
                return
            name = '{} - {}'.format(
                self.seconds_to_time(self.start_slider.get_value()),
                self.seconds_to_time(self.end_slider.get_value()),
            )
            loop = {
                'end': self.end_slider.get_value(),
                'start': self.start_slider.get_value(),
                'name': name
            }
            self.loops[song_id].append(loop)
            self.save_loops()
            self.clear_loops()
            self.load_loops(self.loops[song_id])

    def refresh_status_label(self):
        status_vars = {'duration': '00:00', 'time': '00:00'}
        status_text = self.STATUS_TPL.substitute(status_vars)
        self.status_label.set_text(status_text)

    def refresh_widgets(self):
        self.refresh_song_duration()
        self.refresh_min_range_button()
        self.refresh_sliders()

    def load_loops_conf(self):
        self.loops = {}
        loops_file = self.get_loops_file()
        if loops_file:
            with open(loops_file, 'r') as f:
                try:
                    self.loops = json.loads(f.read())
                except ValueError as e:
                    sys.stderr.write('Error on loading %s: %s\n' % (
                        loops_file, e))

    def get_loops_file(self):
        loops_file = rb.find_plugin_file(self, self.LOOPS_FILENAME)
        if loops_file is None:
            loops_file = os.path.join(rb.find_plugin_file(self, ''), self.LOOPS_FILENAME)
            with open(loops_file, 'w') as f:
                data = {}
                f.write(json.dumps(data))
        return rb.find_plugin_file(self, self.LOOPS_FILENAME)

    def get_grid_column_and_row(self):
        number_of_children = len(self.loops_box.get_children())
        row, column  = divmod(number_of_children, self.LOOPS_PER_ROW)
        return column, row

    def get_song_id(self):
        if not self.entry:
            return None
        song_id = u'{0}-{1}'.format(self.song_artist, self.song_title)
        if not song_id:
            song_id =  self.entry.get_playback_uri()
        return hashlib.md5(song_id.encode('utf8')).hexdigest()

    @property
    def entry(self):
        return self.player.get_playing_entry()

    @property
    def song_title(self):
        if self.entry:
            return self.entry.get_string(RB.RhythmDBPropType.TITLE)
        return ''

    @property
    def song_artist(self):
        if self.entry:
            return self.entry.get_string(RB.RhythmDBPropType.ARTIST)
        return ''

    def load_loops(self, loops):
        for index, loop in enumerate(loops):
            loop = loops[index]
            loop_control = LoopControl(self, index, loop['name'], loop['start'], loop['end'])
            # TODO:
            # Use ScrolledWindow instead of limited numbers of loops per grid row.
            # For now we will use limited number of loops per row as the
            # Gtk.ScrolledWindow is not working for some reason.
            row, column = self.get_grid_column_and_row()
            self.loops_box.attach(loop_control, row, column, 1, 1)
        action = self.actions.get_action('ActivateLooper')
        if action.get_active():
            self.loops_box.show_all()

    def save_loops(self):
        Gdk.threads_add_idle(GLib.PRIORITY_DEFAULT_IDLE, self.save_loops_to_file)

    def save_loops_to_file(self):
        loops_file = rb.find_plugin_file(self, self.LOOPS_FILENAME)
        with open(loops_file, 'w') as f:
            f.write(json.dumps(self.loops))

    @property
    def start_slider_max(self):
        if self.duration:
            return self.duration - self.MIN_RANGE
        return 0

    @property
    def start_slider_min(self):
        return 0

    @property
    def end_slider_max(self):
        if self.duration:
            return self.duration
        return 0

    @property
    def end_slider_min(self):
        if self.duration:
            return self.MIN_RANGE
        return 0

    def loop(self, player, elapsed):
        """
        Signal handler called every second of the current playing song.
        Forces the song to stay inside Looper's slider limits.
        """
        # Start and End sliders values
        start = int(self.start_slider.get_value())
        end = int(self.end_slider.get_value())

        if elapsed < start:
            # current time is bellow Start slider so fast forward
            seek_time = start - elapsed
        elif elapsed >= end:
            # current time is above End slider so rewind
            seek_time = start - elapsed
        else:
            # current position is within sliders, so chill
            seek_time = False

        # Sometimes song change event interferes with seeking. Therefore
        # dont do anything if elapsed time is 0 (less than 1).
        if seek_time and elapsed > 0:
            try:
                self.player.seek(seek_time)
            except GObject.GError:
                sys.stderr.write('Seek to ' + str(seek_time) + 's failed\n')
        self.update_label(elapsed, start, end)

    def update_label(self, elapsed, start, end):
        """Update label based on current song time and sliders positions."""
        current_loop_seconds = elapsed - start
        loop_duration_seconds = end - start
        if current_loop_seconds > 0 and (current_loop_seconds <=
                                         loop_duration_seconds):
            current_loop_time = self.seconds_to_time(current_loop_seconds)
            loop_duration = self.seconds_to_time(loop_duration_seconds)

            label = self.STATUS_TPL.substitute(duration=loop_duration,
                                               time=current_loop_time)
            self.status_label.set_text(label)

    def do_deactivate(self):
        self.save_loops_to_file()
        # Restore users crossfade preference
        if self.crossfade and self.was_crossfade_active:
            self.crossfade.set_active(True)

        self.disconnect_signals()
        self.destroy_widgets()
        del self.shell
        del self.player

    def disconnect_signals(self):
        self.settings.disconnect(self.settings_changed_sigid)
        self.activation_btn.disconnect(self.activation_btn_sigid)
        self.min_range.disconnect(self.min_range_sigid)
        self.player.disconnect(self.song_changed_sigid)
        if hasattr(self, 'elapsed_changed_sigid'):
            self.player.disconnect(self.elapsed_changed_sigid)
        self.start_slider.disconnect(self.start_slider_changed_sigid)
        self.start_slider.disconnect(self.start_slider_value_sigid)
        self.end_slider.disconnect(self.end_slider_changed_sigid)
        self.end_slider.disconnect(self.end_slider_value_sigid)

    def destroy_widgets(self):
        self.appshell.cleanup()
        self.main_box.set_visible(False)
        self.shell.remove_widget(self.main_box, RB.ShellUILocation.MAIN_TOP)
        if hasattr(self, 'rb_slider'):
            self.rb_slider.clear_marks()
            del self.rb_slider
        del self.main_box
        del self.controls_box
        del self.loops_box
        del self.save_loop_btn
        del self.min_range_label
        del self.min_range
        del self.start_slider
        del self.end_slider
        del self.status_label
        del self.activation_btn
        del self.crossfade
        del self.was_crossfade_active

    def log(self, *args):
        output = ', '.join([str(arg) for arg in args]) + '\n'
        sys.stdout.write(output)