586 lines
21 KiB
Python
586 lines
21 KiB
Python
# -*- Mode: python; coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*-
|
|
#
|
|
# Copyright (C) 2015 - 2020 - David Mohammed <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, 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.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software
|
|
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
|
|
# define plugin
|
|
|
|
import gi
|
|
import rb
|
|
import os
|
|
from gi.repository import GObject
|
|
from gi.repository import Gio
|
|
from gi.repository import Gtk
|
|
|
|
gi.require_version('Peas', '1.0')
|
|
from gi.repository import Peas
|
|
from gi.repository import RB
|
|
|
|
from alttoolbar_plugins import PluginDialog
|
|
from alttoolbar_preferences import CoverLocale
|
|
from alttoolbar_preferences import GSetting
|
|
from alttoolbar_preferences import Preferences
|
|
from alttoolbar_rb3compat import ActionGroup
|
|
from alttoolbar_rb3compat import ApplicationShell
|
|
from alttoolbar_rb3compat import gtk_version
|
|
from alttoolbar_type import AltToolbarCompact
|
|
from alttoolbar_type import AltToolbarHeaderBar
|
|
from alttoolbar_type import AltToolbarStandard
|
|
|
|
view_menu_ui = """
|
|
<ui>
|
|
<menubar name="MenuBar">
|
|
<menu name="ViewMenu" action="View">
|
|
<menuitem name="Show Toolbar" action="ToggleToolbar" />
|
|
<menuitem name="Show Source Toolbar"
|
|
action="ToggleSourceMediaToolbar" />
|
|
</menu>
|
|
</menubar>
|
|
</ui>
|
|
"""
|
|
|
|
view_seek_menu_ui = """
|
|
<ui>
|
|
<menubar name="MenuBar">
|
|
<menu name="ViewMenu" action="View">
|
|
<menuitem name="SeekBackward" action="SeekBackward" />
|
|
<menuitem name="SeekForward" action="SeekForward" />
|
|
</menu>
|
|
</menubar>
|
|
</ui>
|
|
"""
|
|
|
|
seek_backward_time = 5
|
|
seek_forward_time = 10
|
|
|
|
|
|
class AltToolbarPlugin(GObject.Object, Peas.Activatable):
|
|
"""
|
|
Main class of the plugin. Manages the activation and deactivation of the
|
|
plugin.
|
|
"""
|
|
__gtype_name = 'AltToolbarPlugin'
|
|
object = GObject.property(type=GObject.Object)
|
|
display_page_tree_visible = GObject.property(type=bool, default=False)
|
|
show_album_art = GObject.property(type=bool, default=False)
|
|
show_song_position_slider = GObject.property(type=bool, default=False)
|
|
playing_label = GObject.property(type=bool, default=False)
|
|
|
|
# signals
|
|
# toolbar-visibility - bool parameter True = visible, False = not visible
|
|
__gsignals__ = {
|
|
'toolbar-visibility': (GObject.SIGNAL_RUN_LAST, None, (bool,))
|
|
}
|
|
|
|
def __init__(self):
|
|
"""
|
|
Initialises the plugin object.
|
|
"""
|
|
GObject.Object.__init__(self)
|
|
self.appshell = None
|
|
self.sh_psc = self.sh_op = self.sh_pc = None
|
|
|
|
def do_activate(self):
|
|
"""
|
|
Called by Rhythmbox when the plugin is activated. It creates the
|
|
plugin's source and connects signals to manage the plugin's
|
|
preferences.
|
|
"""
|
|
|
|
self.shell = self.object
|
|
self.db = self.shell.props.db
|
|
self.shell_player = self.shell.props.shell_player
|
|
|
|
# Prepare internal variables
|
|
self.song_duration = 0
|
|
self.entry = None
|
|
self._plugin_dialog_width = 760
|
|
self._plugin_dialog_height = 550
|
|
|
|
# locale stuff
|
|
cl = CoverLocale()
|
|
cl.switch_locale(cl.Locale.LOCALE_DOMAIN)
|
|
|
|
# for custom icons ensure we start looking in the plugin img folder
|
|
# as a fallback
|
|
theme = Gtk.IconTheme.get_default()
|
|
theme.append_search_path(rb.find_plugin_file(self, 'img'))
|
|
|
|
# Find the Rhythmbox Toolbar
|
|
self.rb_toolbar = AltToolbarPlugin.find(self.shell.props.window,
|
|
'main-toolbar', 'by_id')
|
|
|
|
# get values from gsettings
|
|
self.gs = GSetting()
|
|
self.plugin_settings = self.gs.get_setting(self.gs.Path.PLUGIN)
|
|
|
|
display_type = self.plugin_settings[self.gs.PluginKey.DISPLAY_TYPE]
|
|
self.volume_control = self.plugin_settings[
|
|
self.gs.PluginKey.VOLUME_CONTROL]
|
|
self.show_compact_toolbar = self.plugin_settings[
|
|
self.gs.PluginKey.SHOW_COMPACT]
|
|
self.compact_toolbar_pos = self.plugin_settings[
|
|
self.gs.PluginKey.COMPACT_POS]
|
|
self.start_hidden = self.plugin_settings[
|
|
self.gs.PluginKey.START_HIDDEN]
|
|
self.inline_label = self.plugin_settings[
|
|
self.gs.PluginKey.INLINE_LABEL]
|
|
self.enhanced_sidebar = self.plugin_settings[
|
|
self.gs.PluginKey.ENHANCED_SIDEBAR]
|
|
self.show_tooltips = self.plugin_settings[
|
|
self.gs.PluginKey.SHOW_TOOLTIPS]
|
|
self.enhanced_plugins = self.plugin_settings[
|
|
self.gs.PluginKey.ENHANCED_PLUGINS]
|
|
self.horiz_categories = self.plugin_settings[
|
|
self.gs.PluginKey.HORIZ_CATEGORIES]
|
|
self.app_menu = self.plugin_settings[
|
|
self.gs.PluginKey.APP_MENU]
|
|
self.prefer_dark_theme = \
|
|
self.plugin_settings[self.gs.PluginKey.DARK_THEME]
|
|
|
|
# Add the various application view menus
|
|
self.appshell = ApplicationShell(self.shell)
|
|
self._add_menu_options()
|
|
|
|
# Determine what type of toolbar is to be displayed
|
|
if display_type == 0:
|
|
if 'gnome' in os.environ['XDG_CURRENT_DESKTOP'].lower():
|
|
display_type = 1
|
|
else:
|
|
display_type = 2
|
|
|
|
self.plugin_settings[self.gs.PluginKey.DISPLAY_TYPE] = display_type
|
|
|
|
self.toolbar_type = None
|
|
if display_type == 1:
|
|
self.toolbar_type = AltToolbarHeaderBar()
|
|
elif self.show_compact_toolbar:
|
|
self.toolbar_type = AltToolbarCompact()
|
|
else:
|
|
self.toolbar_type = AltToolbarStandard()
|
|
|
|
self.toolbar_type.initialise(self)
|
|
self.toolbar_type.post_initialise()
|
|
|
|
try:
|
|
process = Gio.Subprocess.new(['rhythmbox', '--version'],
|
|
Gio.SubprocessFlags.STDOUT_PIPE)
|
|
passval, buf, err = process.communicate_utf8(None)
|
|
|
|
if passval:
|
|
buf = buf[:-1]
|
|
ver = buf.split(' ')[1]
|
|
except:
|
|
ver = "999.99.99"
|
|
|
|
if self.enhanced_plugins and ver <= "3.4.3":
|
|
# redirect plugins action to our implementation
|
|
# after v3.4.3 plugins has been moved into
|
|
# preferences so no need to activate our own
|
|
# implementation
|
|
|
|
action = Gio.SimpleAction.new('plugins', None)
|
|
action.connect('activate', self._display_plugins)
|
|
self.shell.props.application.add_action(action)
|
|
|
|
self._connect_signals()
|
|
self._connect_properties()
|
|
|
|
# allow other plugins access to this toolbar
|
|
self.shell.alternative_toolbar = self
|
|
|
|
cl.switch_locale(cl.Locale.RB)
|
|
|
|
def _display_plugins(self, *args):
|
|
"""
|
|
display our implementation of the LibPeas Plugin window
|
|
"""
|
|
|
|
has_headerbar = isinstance(self.toolbar_type, AltToolbarHeaderBar)
|
|
|
|
if gtk_version() < 3.12:
|
|
has_headerbar = False
|
|
|
|
dlg = PluginDialog(self.shell.props.window, has_headerbar)
|
|
response = 0
|
|
dlg.set_default_size(self._plugin_dialog_width,
|
|
self._plugin_dialog_height)
|
|
|
|
while response >= 0:
|
|
response = dlg.run()
|
|
print(response)
|
|
|
|
self._plugin_dialog_width, self._plugin_dialog_height = dlg.get_size()
|
|
dlg.destroy()
|
|
|
|
def on_search(self, *args):
|
|
self.toolbar_type.on_search_toggle()
|
|
|
|
def _add_menu_options(self):
|
|
"""
|
|
add the various menu options to the application
|
|
"""
|
|
self.search_action_group = ActionGroup(self.shell,
|
|
'AltToolbarPluginSearchActions')
|
|
self.search_action_group.add_action(func=self.on_search,
|
|
action_name='Search',
|
|
label=_("Search"),
|
|
action_type='app', accel="<Ctrl>f",
|
|
tooltip=_(
|
|
"Search"))
|
|
self.appshell.insert_action_group(self.search_action_group)
|
|
|
|
self.seek_action_group = ActionGroup(self.shell,
|
|
'AltToolbarPluginSeekActions')
|
|
self.seek_action_group.add_action(func=self.on_skip_backward,
|
|
action_name='SeekBackward',
|
|
label=_("Seek Backward"),
|
|
action_type='app', accel="<Alt>Left",
|
|
tooltip=_(
|
|
"Seek backward, in current "
|
|
"track, by 5 seconds."))
|
|
self.seek_action_group.add_action(func=self.on_skip_forward,
|
|
action_name='SeekForward',
|
|
label=_("Seek Forward"),
|
|
action_type='app',
|
|
accel="<Alt>Right",
|
|
tooltip=_(
|
|
"Seek forward, in current "
|
|
"track, by 10 seconds."))
|
|
|
|
self.appshell.insert_action_group(self.seek_action_group)
|
|
self.appshell.add_app_menuitems(view_seek_menu_ui,
|
|
'AltToolbarPluginSeekActions', 'view')
|
|
|
|
self.toggle_action_group = ActionGroup(self.shell,
|
|
'AltToolbarPluginActions')
|
|
self.toggle_action_group.add_action(func=self.toggle_visibility,
|
|
action_name='ToggleToolbar',
|
|
label=_(
|
|
"Show Play-Controls Toolbar"),
|
|
action_state=ActionGroup.TOGGLE,
|
|
action_type='app',
|
|
tooltip=_(
|
|
"Show or hide the "
|
|
"play-controls toolbar"))
|
|
self.toggle_action_group.add_action(
|
|
func=self.toggle_sourcemedia_visibility,
|
|
action_name='ToggleSourceMediaToolbar',
|
|
label=_("Show Source Toolbar"),
|
|
action_state=ActionGroup.TOGGLE,
|
|
action_type='app', accel="<Ctrl>t",
|
|
tooltip=_("Show or hide the source toolbar"))
|
|
|
|
self.appshell.insert_action_group(self.toggle_action_group)
|
|
self.appshell.add_app_menuitems(view_menu_ui,
|
|
'AltToolbarPluginActions', 'view')
|
|
|
|
def _connect_properties(self):
|
|
"""
|
|
bind plugin properties to various gsettings that we dynamically
|
|
interact with
|
|
"""
|
|
self.plugin_settings.bind(self.gs.PluginKey.PLAYING_LABEL, self,
|
|
'playing_label',
|
|
Gio.SettingsBindFlags.GET)
|
|
|
|
def _connect_signals(self):
|
|
"""
|
|
connect to various rhythmbox signals that the toolbars need
|
|
"""
|
|
self.sh_display_page_tree = self.shell.props.display_page_tree.connect(
|
|
"selected", self.on_page_change
|
|
)
|
|
|
|
self.sh_psc = self.shell_player.connect("playing-song-changed",
|
|
self._sh_on_song_change)
|
|
|
|
self.sh_op = self.shell_player.connect("elapsed-changed",
|
|
self._sh_on_playing)
|
|
|
|
self.sh_pc = self.shell_player.connect("playing-changed",
|
|
self._sh_on_playing_change)
|
|
|
|
self.sh_pspc = self.shell_player.connect(
|
|
"playing-song-property-changed",
|
|
self._sh_on_song_property_changed)
|
|
|
|
self.rb_settings = Gio.Settings.new('org.gnome.rhythmbox')
|
|
|
|
self.rb_settings.bind('show-album-art', self, 'show_album_art',
|
|
Gio.SettingsBindFlags.GET)
|
|
self.connect('notify::show-album-art',
|
|
self.show_album_art_settings_changed)
|
|
self.show_album_art_settings_changed(None)
|
|
|
|
self.rb_settings.bind('show-song-position-slider', self,
|
|
'show_song_position_slider',
|
|
Gio.SettingsBindFlags.GET)
|
|
self.connect('notify::show-song-position-slider',
|
|
self.show_song_position_slider_settings_changed)
|
|
self.show_song_position_slider_settings_changed(None)
|
|
|
|
def _sh_on_song_property_changed(self, sp, uri, property, old, new):
|
|
"""
|
|
shell-player "playing-song-property-changed" signal handler
|
|
"""
|
|
if sp.get_playing() and property in \
|
|
('artist',
|
|
'album',
|
|
'title',
|
|
RB.RHYTHMDB_PROP_STREAM_SONG_ARTIST,
|
|
RB.RHYTHMDB_PROP_STREAM_SONG_ALBUM,
|
|
RB.RHYTHMDB_PROP_STREAM_SONG_TITLE):
|
|
entry = sp.get_playing_entry()
|
|
self.toolbar_type.display_song(entry)
|
|
|
|
def _sh_on_playing_change(self, player, playing):
|
|
"""
|
|
Shell-player 'playing-change' signal handler.
|
|
"""
|
|
self.toolbar_type.play_control_change(player, playing)
|
|
if (self.song_duration != 0):
|
|
self.toolbar_type.enable_slider(True)
|
|
else:
|
|
self.toolbar_type.enable_slider(False)
|
|
if (hasattr(self.toolbar_type, "total_time_label")):
|
|
label = ""
|
|
self.toolbar_type.total_time_label.set_markup(label)
|
|
|
|
def _sh_on_song_change(self, player, entry):
|
|
"""
|
|
Shell-player 'playing-song-changed' signal handler.
|
|
"""
|
|
if (entry is not None):
|
|
self.song_duration = entry.get_ulong(RB.RhythmDBPropType.DURATION)
|
|
else:
|
|
self.song_duration = 0
|
|
|
|
if hasattr(self.toolbar_type, 'song_progress'):
|
|
self.toolbar_type.song_progress.adjustment.set_upper(
|
|
self.song_duration or 1)
|
|
self.toolbar_type.display_song(entry)
|
|
|
|
def _sh_on_playing(self, player, seconds):
|
|
"""
|
|
Shell-player 'elapsed-changed' signal handler.
|
|
"""
|
|
if self.song_duration == 0:
|
|
return
|
|
try:
|
|
slider = self.toolbar_type.song_progress
|
|
except AttributeError:
|
|
return
|
|
with slider.handler_block(slider.changed_callback_id):
|
|
slider.adjustment.set_value(seconds)
|
|
|
|
minutes, seconds = divmod(seconds, 60)
|
|
hours, minutes = divmod(minutes, 60)
|
|
total_minutes, total_seconds = divmod(self.song_duration, 60)
|
|
total_hours, total_minutes = divmod(total_minutes, 60)
|
|
|
|
if total_hours:
|
|
label = "<small>{}:{:02}:{:02} / {}:{:02}:{:02}</small>".format(
|
|
hours, minutes, seconds,
|
|
total_hours, total_minutes, total_seconds)
|
|
else:
|
|
label = "<small>{:02}:{:02} / {:02}:{:02}</small>".format(
|
|
minutes, seconds, total_minutes, total_seconds)
|
|
self.toolbar_type.total_time_label.set_markup(label)
|
|
|
|
def on_skip_backward(self, *args):
|
|
"""
|
|
keyboard seek backwards signal handler
|
|
"""
|
|
sp = self.object.props.shell_player
|
|
if (sp.get_playing()[1]):
|
|
seek_time = sp.get_playing_time()[1] - seek_backward_time
|
|
print(seek_time)
|
|
if (seek_time < 0):
|
|
seek_time = 0
|
|
|
|
print(seek_time)
|
|
sp.set_playing_time(seek_time)
|
|
|
|
def on_skip_forward(self, *args):
|
|
"""
|
|
keyboard seek forwards signal handler
|
|
"""
|
|
sp = self.object.props.shell_player
|
|
if (sp.get_playing()[1]):
|
|
seek_time = sp.get_playing_time()[1] + seek_forward_time
|
|
song_duration = sp.get_playing_song_duration()
|
|
if (song_duration > 0): # sanity check
|
|
if (seek_time > song_duration):
|
|
seek_time = song_duration
|
|
|
|
sp.set_playing_time(seek_time)
|
|
|
|
def show_song_position_slider_settings_changed(self, *args):
|
|
"""
|
|
rhythmbox show-slider signal handler
|
|
"""
|
|
self.toolbar_type.show_slider(self.show_song_position_slider)
|
|
|
|
def show_album_art_settings_changed(self, *args):
|
|
"""
|
|
rhythmbox show-album-art signal handler
|
|
"""
|
|
self.toolbar_type.show_cover(self.show_album_art)
|
|
|
|
def on_page_change(self, display_page_tree, page):
|
|
"""
|
|
sources display-tree signal handler
|
|
"""
|
|
print("page changed", page)
|
|
self.toolbar_type.reset_categories_pos(page)
|
|
self.toolbar_type.reset_toolbar(page)
|
|
self.toolbar_type.reset_entryview(page)
|
|
|
|
@staticmethod
|
|
def find(node, search_id, search_type, button_label=None):
|
|
"""
|
|
find various GTK Widgets
|
|
:param node: node is the starting container to find from
|
|
:param search_id: search_id is the GtkWidget type string or
|
|
GtkWidget name
|
|
:param search_type: search_type is the type of search
|
|
"by_name" to search by the type of GtkWidget
|
|
e.g. GtkButton
|
|
"by_id" to search by the GtkWidget (glade name)
|
|
e.g. box_1
|
|
:param button_label: button_label to find specific buttons where we
|
|
cannot use by_id
|
|
:return:N/A
|
|
"""
|
|
|
|
# Couldn't find better way to find widgets than loop through them
|
|
# print("by_name %s by_id %s" % (node.get_name(),
|
|
# Gtk.Buildable.get_name(node)))
|
|
|
|
def extract_label(button):
|
|
label = button.get_label()
|
|
if label:
|
|
return label
|
|
|
|
child = button.get_child()
|
|
if child and child.get_name() == "GtkLabel":
|
|
return child.get_text()
|
|
|
|
return None
|
|
|
|
if isinstance(node, Gtk.Buildable):
|
|
if search_type == 'by_id':
|
|
if Gtk.Buildable.get_name(node) == search_id:
|
|
if button_label is None or (
|
|
'Button' in node.get_name() and extract_label(
|
|
node) == button_label):
|
|
return node
|
|
elif search_type == 'by_name':
|
|
if node.get_name() == search_id:
|
|
if button_label is None or (
|
|
'Button' in node.get_name() and extract_label(
|
|
node) == button_label):
|
|
return node
|
|
|
|
if isinstance(node, Gtk.Container):
|
|
for child in node.get_children():
|
|
ret = AltToolbarPlugin.find(child, search_id, search_type,
|
|
button_label)
|
|
if ret:
|
|
return ret
|
|
|
|
return None
|
|
|
|
def do_deactivate(self):
|
|
"""
|
|
Called by Rhythmbox when the plugin is deactivated. It makes sure to
|
|
free all the resources used by the plugin.
|
|
"""
|
|
del self.db
|
|
|
|
if self.sh_op:
|
|
self.shell_player.disconnect(self.sh_op)
|
|
self.shell_player.disconnect(self.sh_psc)
|
|
self.shell_player.disconnect(self.sh_pc)
|
|
self.shell_player.disconnect(self.sh_pspc)
|
|
# self.disconnect(self.sh_display_page)
|
|
self.shell.props.display_page_tree.disconnect(
|
|
self.sh_display_page_tree)
|
|
del self.shell_player
|
|
|
|
if self.appshell:
|
|
self.appshell.cleanup()
|
|
|
|
self.rb_toolbar.set_visible(True)
|
|
|
|
self.toolbar_type.cleanup()
|
|
|
|
del self.shell
|
|
|
|
def toggle_visibility(self, action, param=None, data=None):
|
|
"""
|
|
Display or Hide PlayControls signal handler
|
|
:param action:
|
|
:param param:
|
|
:param data:
|
|
:return:
|
|
"""
|
|
action = self.toggle_action_group.get_action('ToggleToolbar')
|
|
|
|
self.toolbar_type.set_visible(action.get_active())
|
|
|
|
def toggle_sourcemedia_visibility(self, action, param=None, data=None):
|
|
"""
|
|
Display or Hide the source toolbar
|
|
:param action:
|
|
:param param:
|
|
:param data:
|
|
:return:
|
|
"""
|
|
action = self.toggle_action_group.get_action(
|
|
'ToggleSourceMediaToolbar')
|
|
|
|
self.toolbar_type.source_toolbar_visibility(action.get_active())
|
|
|
|
def _translation_helper(self):
|
|
"""
|
|
a method just to help out with translation strings
|
|
it is not meant to be called by itself
|
|
"""
|
|
|
|
# define .plugin text strings used for translation
|
|
plugin = _('Alternative Toolbar')
|
|
plugin += "dummy"
|
|
desc = _(
|
|
'Replace the Rhythmbox large toolbar with a Client-Side '
|
|
'Decorated or Compact Toolbar which can be hidden')
|
|
|
|
desc += "dummy"
|
|
# stop PyCharm removing the Preference import on optimisation
|
|
pref = Preferences()
|
|
return pref
|
|
|
|
def get_toolbar(self, callback):
|
|
"""
|
|
a method to return the toolbar itself
|
|
:param callback: function callback - func(AT.ToolbarCallback)
|
|
passed
|
|
:return:
|
|
"""
|
|
|
|
self.toolbar_type.setup_completed_async(callback)
|