420 lines
16 KiB
Python
420 lines
16 KiB
Python
# This is a part of the external Repeat One Song plugin for Rhythmbox
|
|
#
|
|
# Author: Eduardo Mucelli Rezende Oliveira
|
|
# E-mail: edumucelli@gmail.com or eduardom@dcc.ufmg.br
|
|
# Version: 0.4 (Unstable) for Rhythmbox 3.0.1 or later
|
|
#
|
|
# reworked for alternative-toolbar
|
|
# Author: David Mohammed 2015-2020 <fossfreedom@ubuntu.com>
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
|
|
from alttoolbar_preferences import CoverLocale
|
|
from alttoolbar_preferences import GSetting
|
|
from gi.repository import GLib
|
|
from gi.repository import GObject
|
|
from gi.repository import Gdk
|
|
from gi.repository import Gio
|
|
from gi.repository import Gtk
|
|
|
|
|
|
class Repeat(GObject.Object):
|
|
"""
|
|
Object handling song repeating, with an additional feature of
|
|
repeating one song, one song only.
|
|
"""
|
|
|
|
SONG_CHANGED_MANUAL = 0
|
|
SONG_CHANGED_EOS = 1
|
|
|
|
def __init__(self, shell, toggle_button):
|
|
"""
|
|
:param shell: the plugin object
|
|
:param toggle_button: button that controls the repeat functions
|
|
"""
|
|
GObject.Object.__init__(self)
|
|
|
|
# use this to start the repeat-one-song capability (if True)
|
|
self.repeat_song = False
|
|
self.toggle_button = toggle_button
|
|
self.song_changed = self.SONG_CHANGED_MANUAL
|
|
|
|
player = shell.props.shell_player
|
|
# EOS signal means that the song changed because the song is over.
|
|
# ie. the user did not manually change the song.
|
|
# https://developer.gnome.org/rhythmbox/unstable/RBPlayer.html#RBPlayer-eos
|
|
player.props.player.connect('eos', self.on_gst_player_eos)
|
|
player.connect('playing-song-changed', self.on_song_change)
|
|
# This hack is no longer needed when the above signal handlers
|
|
# work. For more details, refer to the comments above the
|
|
# definition of method on_elapsed_change.
|
|
# player.connect('elapsed-changed', self.on_elapsed_change)
|
|
|
|
try:
|
|
popover = Gtk.Popover.new(toggle_button)
|
|
except AttributeError:
|
|
# use our custom Popover equivalent for Gtk+3.10 folks
|
|
popover = CustomPopover(toggle_button)
|
|
else:
|
|
popover.set_modal(False)
|
|
finally:
|
|
repeat = RepeatPopContainer(popover, toggle_button)
|
|
popover.add(repeat)
|
|
|
|
toggle_button.connect('toggled', self._on_toggle, popover, repeat)
|
|
repeat.connect('repeat-type-changed', self._on_repeat_type_changed)
|
|
|
|
self._on_repeat_type_changed(repeat, repeat.get_repeat_type())
|
|
|
|
def _on_toggle(self, toggle, popover, repeat):
|
|
if toggle.get_active():
|
|
popover.show_all()
|
|
self.repeat_song = \
|
|
repeat.get_repeat_type() == RepeatPopContainer.ONE_SONG
|
|
else:
|
|
popover.hide()
|
|
self.repeat_song = False
|
|
|
|
self._set_toggle_tooltip(repeat)
|
|
|
|
print("on toggle", self.repeat_song)
|
|
|
|
def _set_toggle_tooltip(self, repeat):
|
|
# locale stuff
|
|
cl = CoverLocale()
|
|
cl.switch_locale(cl.Locale.LOCALE_DOMAIN)
|
|
if self.toggle_button.get_has_tooltip():
|
|
if repeat.get_repeat_type() == RepeatPopContainer.ALL_SONGS:
|
|
message = _("Repeat all tracks")
|
|
else:
|
|
message = _("Repeat the current track")
|
|
self.toggle_button.set_tooltip_text(message)
|
|
cl = CoverLocale()
|
|
cl.switch_locale(cl.Locale.RB)
|
|
|
|
def _on_repeat_type_changed(self, repeat, repeat_type):
|
|
if self.toggle_button.get_active():
|
|
if repeat_type == RepeatPopContainer.ONE_SONG:
|
|
self.repeat_song = True
|
|
else:
|
|
self.repeat_song = False
|
|
else:
|
|
self.repeat_song = False
|
|
|
|
self._set_toggle_tooltip(repeat)
|
|
|
|
print("repeat type changed", self.repeat_song)
|
|
|
|
def on_gst_player_eos(self, gst_player, stream_data, early=0):
|
|
"""
|
|
Set song_changed to SONG_CHANGED_EOS so that on_song_change will
|
|
know to repeat the song.
|
|
"""
|
|
if self.repeat_song:
|
|
self.song_changed = self.SONG_CHANGED_EOS
|
|
|
|
def on_song_change(self, player, time):
|
|
"""
|
|
Repeat song that has just been played
|
|
(when called on song change signal).
|
|
"""
|
|
if self.song_changed == self.SONG_CHANGED_EOS:
|
|
self.song_changed = self.SONG_CHANGED_MANUAL
|
|
player.do_previous()
|
|
|
|
# Since seg faults no longer seem to happen when the 'eos' callback
|
|
# is called with GStreamer 1.0, on_gst_player_eos in conjunction
|
|
# with on_song_change are used instead of this method to control the
|
|
# song repetition. The related to GStreamer is described at
|
|
# https://bugs.launchpad.net/ubuntu/+source/rhythmbox/+bug/1239218
|
|
def on_elapsed_change(self, player, time):
|
|
"""
|
|
This is a old method to 'repeat' the current song as soon as
|
|
it reaches the last seconds.
|
|
"""
|
|
if self.repeat_song:
|
|
# This might be improved by keeping a instance variable with
|
|
# the duration and updating it on_song_change in order to
|
|
# avoid querying the duration on every call.
|
|
duration = player.get_playing_song_duration()
|
|
if duration > 0:
|
|
# Repeat on the last two seconds of the song. Previously the
|
|
# last second was used but RB now seems to use the last second
|
|
# to prepare things for the next song of the list.
|
|
if time >= duration - 2:
|
|
player.set_playing_time(0)
|
|
|
|
|
|
class RepeatPopContainer(Gtk.ButtonBox):
|
|
__gsignals__ = {
|
|
"repeat-type-changed": (GObject.SIGNAL_RUN_LAST, None, (int,))
|
|
}
|
|
|
|
# repeat-type-changed is emitted with one of the following values
|
|
ONE_SONG = 1
|
|
ALL_SONGS = 2
|
|
|
|
def __init__(self, parent_container, parent_button, *args, **kwargs):
|
|
super(RepeatPopContainer, self).__init__(*args, **kwargs)
|
|
|
|
self.set_orientation(Gtk.Orientation.HORIZONTAL)
|
|
self.set_layout(Gtk.ButtonBoxStyle.START)
|
|
self.props.margin = 5
|
|
context = self.get_style_context()
|
|
context.add_class('linked')
|
|
|
|
icon_size = 4
|
|
|
|
toggle1 = Gtk.RadioButton.new(None)
|
|
toggle1.set_mode(False)
|
|
fallback = 'media-playlist-repeat-symbolic'
|
|
icon = Gio.ThemedIcon.new_with_default_fallbacks(fallback)
|
|
image = Gtk.Image()
|
|
image.set_from_gicon(icon, icon_size)
|
|
image.props.margin = 5
|
|
toggle1.set_image(image)
|
|
toggle1.connect('leave-notify-event', self._on_popover_mouse_over)
|
|
toggle1.connect('enter-notify-event', self._on_popover_mouse_over)
|
|
toggle1.connect('toggled', self._on_popover_button_toggled)
|
|
|
|
# locale stuff
|
|
cl = CoverLocale()
|
|
cl.switch_locale(cl.Locale.LOCALE_DOMAIN)
|
|
|
|
if parent_button.get_has_tooltip():
|
|
toggle1.set_tooltip_text(_("Repeat all tracks"))
|
|
|
|
self._repeat_button = toggle1
|
|
self.add(toggle1)
|
|
self.child_set_property(toggle1, "non-homogeneous", True)
|
|
toggle1.show_all()
|
|
|
|
self._repeat_image = Gtk.Image()
|
|
self._repeat_image.set_from_gicon(icon, icon_size)
|
|
self._repeat_image.props.margin = 5
|
|
|
|
toggle2 = Gtk.RadioButton.new_from_widget(toggle1)
|
|
toggle2.set_mode(False)
|
|
sym = 'media-playlist-repeat-song-symbolic'
|
|
icon2 = Gio.ThemedIcon.new_with_default_fallbacks(sym)
|
|
image2 = Gtk.Image()
|
|
image2.set_from_gicon(icon2, icon_size)
|
|
image2.props.margin = 5
|
|
toggle2.set_image(image2)
|
|
|
|
if parent_button.get_has_tooltip():
|
|
toggle2.set_tooltip_text(_("Repeat the current track"))
|
|
|
|
self._repeat_song_image = Gtk.Image()
|
|
self._repeat_song_image.set_from_gicon(icon2, icon_size)
|
|
self._repeat_song_image.props.margin = 5
|
|
|
|
toggle2.connect('leave-notify-event', self._on_popover_mouse_over)
|
|
toggle2.connect('enter-notify-event', self._on_popover_mouse_over)
|
|
toggle2.connect('toggled', self._on_popover_button_toggled)
|
|
toggle2.show_all()
|
|
self._repeat_song_button = toggle2
|
|
self.add(toggle2)
|
|
self.child_set_property(toggle2, "non-homogeneous", True)
|
|
|
|
self._popover_inprogress = 0
|
|
parent_container.connect('leave-notify-event',
|
|
self._on_popover_mouse_over)
|
|
parent_container.connect('enter-notify-event',
|
|
self._on_popover_mouse_over)
|
|
parent_button.connect('leave-notify-event',
|
|
self._on_popover_mouse_over)
|
|
parent_button.connect('enter-notify-event',
|
|
self._on_popover_mouse_over)
|
|
|
|
parent_button.set_image(self._repeat_image)
|
|
|
|
self._parent_container = parent_container
|
|
self._parent_button = parent_button
|
|
|
|
# now get the repeat-type saved in gsettings
|
|
# get values from gsettings
|
|
self.gs = GSetting()
|
|
self.plugin_settings = self.gs.get_setting(self.gs.Path.PLUGIN)
|
|
|
|
repeat_type = self.plugin_settings[self.gs.PluginKey.REPEAT_TYPE]
|
|
|
|
if repeat_type == RepeatPopContainer.ONE_SONG:
|
|
self._repeat_song_button.set_active(True)
|
|
|
|
def _on_popover_button_toggled(self, button, *args):
|
|
print("popover toggle")
|
|
if button.get_active():
|
|
if button == self._repeat_button:
|
|
self._parent_button.set_image(self._repeat_image)
|
|
self.emit('repeat-type-changed', RepeatPopContainer.ALL_SONGS)
|
|
self.plugin_settings[self.gs.PluginKey.REPEAT_TYPE] = \
|
|
RepeatPopContainer.ALL_SONGS
|
|
else:
|
|
self._parent_button.set_image(self._repeat_song_image)
|
|
self.emit('repeat-type-changed', RepeatPopContainer.ONE_SONG)
|
|
self.plugin_settings[self.gs.PluginKey.REPEAT_TYPE] = \
|
|
RepeatPopContainer.ONE_SONG
|
|
|
|
def get_repeat_type(self):
|
|
repeat_type = RepeatPopContainer.ALL_SONGS
|
|
if self._repeat_song_button.get_active():
|
|
repeat_type = RepeatPopContainer.ONE_SONG
|
|
|
|
return repeat_type
|
|
|
|
def _on_popover_mouse_over(self, widget, eventcrossing):
|
|
if eventcrossing.type == Gdk.EventType.ENTER_NOTIFY:
|
|
if self._popover_inprogress == 0:
|
|
self._popover_inprogress = 1
|
|
print("enter1")
|
|
else:
|
|
self._popover_inprogress = 2
|
|
print("enter2")
|
|
self._popover_inprogress_count = 0
|
|
|
|
if type(widget) is Gtk.ToggleButton:
|
|
print("here")
|
|
if widget.get_active():
|
|
print(self._parent_container)
|
|
self._parent_container.show_all()
|
|
else:
|
|
print("exit")
|
|
self._popover_inprogress = 3
|
|
|
|
def delayed(*args):
|
|
if self._popover_inprogress == 3:
|
|
self._popover_inprogress_count += 1
|
|
|
|
if self._popover_inprogress_count < 5:
|
|
return True
|
|
|
|
self._parent_container.hide()
|
|
self._popover_inprogress = 0
|
|
print("exit timeout")
|
|
return False
|
|
else:
|
|
return True
|
|
|
|
if self._popover_inprogress == 1:
|
|
print("adding timeout")
|
|
self._popover_inprogress = 2
|
|
GLib.timeout_add(100, delayed)
|
|
|
|
|
|
class CustomPopover(Gtk.Window):
|
|
def __init__(self, parent_button, *args, **kwargs):
|
|
super(CustomPopover, self).__init__(type=Gtk.WindowType.POPUP, *args,
|
|
**kwargs)
|
|
|
|
self.set_decorated(False)
|
|
self.set_resizable(False)
|
|
self.set_type_hint(Gdk.WindowTypeHint.DOCK)
|
|
self.stick()
|
|
self._parent_button = parent_button
|
|
self.connect_after('show', self._on_show)
|
|
# Track movements of the window to move calendar window as well
|
|
self.connect("configure-event", self.on_window_config)
|
|
|
|
def add(self, widget):
|
|
self._frame = Gtk.Frame()
|
|
self._frame.add(widget)
|
|
|
|
super(CustomPopover, self).add(self._frame)
|
|
self._frame.show_all()
|
|
|
|
# Popoverwindow co ordinates without off-screen correction:
|
|
# Window origin (x, y)
|
|
# |
|
|
# V
|
|
# ---------------------------------
|
|
# | Main Window |
|
|
# | |
|
|
# | |
|
|
# |Toggle button's (x, y) |
|
|
# |(relative to parent window) |
|
|
# | | |
|
|
# | V |
|
|
# | ......................... |
|
|
# Popover | | Toggle Button | |
|
|
# window's | | | |
|
|
# (x, y)---+> ......................... |
|
|
# |(window will be here) |
|
|
# | |
|
|
# | |
|
|
# ---------------------------------
|
|
# Popover Window's screen coordinates:
|
|
# x = Window's origin x + Toggle Button's relative x
|
|
# y = Window's origin y + Toggle Button's relative y + Toggle Button's
|
|
# height
|
|
|
|
def _on_show(self, widget):
|
|
rect = self._parent_button.get_allocation()
|
|
main_window = self._parent_button.get_toplevel()
|
|
[val, win_x, win_y] = main_window.get_window().get_origin()
|
|
cal_x = win_x + rect.x
|
|
cal_y = win_y + rect.y + rect.height
|
|
|
|
[x, y] = self.apply_screen_coord_correction(cal_x, cal_y)
|
|
self.move(x, y)
|
|
|
|
# This function "tries" to correct calendar window position so that it is
|
|
# not obscured when
|
|
# a portion of main window is off-screen.
|
|
# Known bug: If the main window is partially off-screen before Calendar
|
|
# window
|
|
# has been realized then get_allocation() will return rect of 1x1 in which
|
|
# case
|
|
# the calculations will fail & correction will not be applied
|
|
def apply_screen_coord_correction(self, x, y):
|
|
corrected_y = y
|
|
corrected_x = x
|
|
rect = self.get_allocation()
|
|
screen_w = Gdk.Screen.width()
|
|
screen_h = Gdk.Screen.height()
|
|
|
|
delta_x = screen_w - (x + rect.width)
|
|
delta_y = screen_h - (y + rect.height)
|
|
if delta_x < 0:
|
|
corrected_x += delta_x
|
|
print("at x")
|
|
if corrected_x < 0:
|
|
corrected_x = 0
|
|
|
|
button_rect = self._parent_button.get_allocation()
|
|
window_width, window_height = \
|
|
self._parent_button.get_toplevel().get_size()
|
|
# print (y, button_rect.y, button_rect.height, )
|
|
|
|
calc = (window_height - (button_rect.y + (button_rect.height * 2)))
|
|
if delta_y < 0 or (calc < 0):
|
|
btn_hgt = self._parent_button.get_allocation().height
|
|
corrected_y = y - rect.height - btn_hgt
|
|
print("at y")
|
|
if corrected_y < 0:
|
|
corrected_y = 0
|
|
return [corrected_x, corrected_y]
|
|
|
|
# "configure-event" callback of main window, try to move calendar window
|
|
# along with main window.
|
|
def on_window_config(self, widget, event):
|
|
# Maybe better way to find the visiblilty
|
|
if self.get_mapped():
|
|
rect = self._parent_button.get_allocation()
|
|
main_window = self._parent_button.get_toplevel()
|
|
[val, win_x, win_y] = main_window.get_window().get_origin()
|
|
cal_x = win_x + rect.x
|
|
cal_y = win_y + rect.y + rect.height
|
|
|
|
self.show_all()
|
|
[x, y] = self.apply_screen_coord_correction(cal_x, cal_y)
|
|
self.move(x, y)
|