J’utilise régulièrement Rhythmbox pour écouter ma musique. Hélas, il manque une option que je trouve très intéressante : passer le même morceau en boucle. Voyons si nous pouvons y remédier.

Avant de commencer, je remarque comme souvent un écart entre la version de Rhythmbox actuellement disponible dans les dépôts Debian (3.4.3) et celle, plus avancée, du gitlab de gnome.org (3.4.4). Dans ce qui suit, nous restons en ligne avec la version Debian.

$ rhythmbox --version
rhythmbox 3.4.3

Plan de l’article

Definition du problème

Voici l’interface principale de Rhythmbox lorsqu’on lance l’application.

Rhythmbox

Le bouton de relecture existe en deux états :

  • un état désactivé où le lecteur s’arretera après lecture du dernier morceau (ici “Night Prowler” d’AC/DC)
  • un état activé où, après lecture du dernier morceau, le lecteur reprendra la play-list du début (auquel cas, on passera de “Night Prowler” à “Hells Bells”)

Ce comportement est d’ailleurs bien spécifié dans la documentation : il n’existe actuellement pas d’option pour lire en boucle le même morceau. Mis-à-part peut-être la manipulation qui consisterait à créer une play-list ne contenant qu’un seul morceau.

Des plug-ins ont bien été développés 1 pour remédier à cette situation (voir par exemple ici ou la) mais nous allons voir si on ne peut pas directement modifier le code source.

Récupération des sources de Rhythmbox

Sous Debian, c’est extrémement simple ! Plaçons-nous dans un répertoire de travail et entrons :

$ mkdir ~/rhythmbox
$ cd ~/rhythmbox
$ apt-get source rhythmbox

Avant de regarder le code source, tâchons déjà de compiler l’application en l’état :

$ cd rhythmbox-3.4.3
$ ./configure

A cette étape, ça bloque car certaines bibliothèques ne sont pas installées sur le système et ./configure l’indiquera.

On pourrait traquer ces dépendances une à une et les installer manuellement mais il y a plus direct. Comme je me base sur la version des dépôts Debian, je peux installer ces commandes avec un seul appel à apt-get !

$ apt-get --dry-run build-dep rhythmbox

L’option --dry-run permet de contrôler ce qu’apt-get propose d’installer. Une fois cela vérifié, il faut bien entendu l’enlever.

Si on relance ./configure tout se déroulera désormais sans accroc. Il suffira ensuite de lancer classiquement make pour compiler le projet à l’intérieur du répertoire courant. Pas d’appel à make install car je souhaite uniquement avoir le binaire dans le répertoire de compilation pour le moment.

Modification du code

Ah là on commence à mettre un peu les mains dans le cambouis.

Tout d’abord, on peut voir que Rhythmbox se base sur la bibliothèque gtk+-3.0

$ grep -m 1 GTK_REQS ./configure
GTK_REQS=3.20.0

Nous pouvons contrôler ce retour en examinant les dépendances de l’executable Rhythmbox tel qu’installé par le système

$ ldd /usr/bin/rhythmbox | grep libgtk
	libgtk-3.so.0 => /lib/x86_64-linux-gnu/libgtk-3.so.0 (0x00007f8b343a1000)

Le travail se décompose en trois étapes :

  1. créer un bouton à trois états pour prendre en main les callbacks gtk
  2. modifier les sources de Rhythmbox pour ajouter une fonctionnalité “lire en boucle”
  3. modifier les sources de Rhythmbox pour ajouter un bouton à trois états

Etape un : création d’un bouton à trois états

Ce premier exercice permet de s’échauffer un peu avant de rentrer dans le détail du code de Rhythmbox.

Pour avoir la liste des icones gnome/gtk3 dans le thème actuellement installé, on pourrait aller farfouiller dans /usr/share/icons/. Nénamoins gtk propose une façon simple de trouver notre bonheur en tapant :

$ gtk3-icon-browser

On peut alors facilement parcourir les différentes icones, classées par thématique. Celles qui nous intéressent sont naturellement dans l’onglet “Multimédia”. Nous choisissons de retenir les trois icones suivantes (qui représenteront les trois états de notre bouton).

Etat Nom Icone
Lecture simple media-playlist-consecutive-symbolic Lecture simple
Répeter play-list media-playlist-repeat-symbolic Lecture simple
Répeter morceau media-playlist-repeat-song-symbolic Lecture simple

Il nous reste maintenant à créer un bouton et cycler entre les trois états. Notons au passage l’existence du paquet devhelp qui permet d’explorer facilement la documentation gtk. Une petite interface graphique apparaîtra alors, permettant de lister l’ensemble des objets de fonctions des différentes bibliothèques (gtk, GLib, GIO, GObject…).

devhelp

Voici au final un bout de code qui permet de cycler entre ces trois images sur un même bouton. Gardons-le sous le coude, on s’en inspirera plus tard.

#include <gtk/gtk.h>
#define NB_IMAGE 3


GtkWidget *img[NB_IMAGE]; // 3 images img1 img2 et img3


/* Fonction qui switche la couleur du bouton */
static void
switch_color_cb (GtkWidget *widget,
                 gpointer   data)
{
  static int compteur = 0; // compteur pour cycler parmi les trois images


  /* Swith color img1 > img2 > img3 > img1 */
  compteur = (++compteur%3);


  // We increase the reference count of img[compteur]
  // so that it is not nullified by gtk_button_set_image() call
  // see https://stackoverflow.com/questions/21433762/button-with-image-images-disappear

  g_object_ref( (GtkImage *) img[compteur]);

  g_print("- Set image to img[%d]\n", compteur);

  gtk_button_set_image ((GtkButton *) widget, img[compteur]);

}


/* Fonction d'activation de l'application gtk */
static void
activate_cb (GtkApplication *app,
             gpointer        user_data)
{
  GtkWidget *window;
  GtkWidget *button_box;
  GtkWidget *button;

  img[0] = gtk_image_new_from_icon_name ("media-playlist-consecutive-symbolic", GTK_ICON_SIZE_LARGE_TOOLBAR);
  img[1] = gtk_image_new_from_icon_name ("media-playlist-repeat-symbolic", GTK_ICON_SIZE_LARGE_TOOLBAR);
  img[2] = gtk_image_new_from_icon_name ("media-playlist-repeat-song-symbolic", GTK_ICON_SIZE_LARGE_TOOLBAR);

  window = gtk_application_window_new (app);
  gtk_window_set_title (GTK_WINDOW (window), "Window");
  gtk_window_set_default_size (GTK_WINDOW (window), 200, 200);

  button_box = gtk_button_box_new (GTK_ORIENTATION_HORIZONTAL);
  gtk_container_add (GTK_CONTAINER (window), button_box);

  // Bouton principal
  button = gtk_button_new();

  g_object_ref( (GtkImage *) img[0]);
  gtk_button_set_image ((GtkButton *) button, img[0]); // Initialisation avec img1

  g_signal_connect (button, "clicked", G_CALLBACK (switch_color_cb), NULL);

  gtk_container_add (GTK_CONTAINER (button_box), button);

  gtk_widget_show_all (window);

}

int
main (int    argc,
      char **argv)
{

  GtkApplication *app;
  int status;

  app = gtk_application_new ("org.gtk.example", G_APPLICATION_FLAGS_NONE);
  g_signal_connect (app, "activate", G_CALLBACK (activate_cb), NULL);

  status = g_application_run (G_APPLICATION (app), argc, argv);
  g_object_unref (app);

  return status;
}

Etape deux : ajout une fonctionnalité “lire en boucle”

Avec les sources de Rhythmbox, on récupère un fichier très intéressant nommé INTERNALS. Ce fichier présente les grandes structures du programme et permet de se repérer rapidement. HACKING est autre fichier utile, présent sur le gitlab de gnome mais pas dans le dépôt Debian, et qui présente succinctement les principales règles de rédaction du programme.

Le fichier ./data/ui/main-toolbar.ui contient l’interface graphique principale au format xml. Dans ce fichier, le bouton “repéter” apparaît de cette façon :

<object class="GtkToggleButton" id="repeat-button">
  <property name="visible">True</property>
  <property name="can_focus">True</property>
  <property name="receives_default">True</property>
  <property name="action_name">app.play-repeat</property>
  <property name="image">image2</property>
  <style>
    <class name="raised"/>
  </style>
</object>

Plusieurs choses sont à noter ici. Tout d’abord l’identifiant du bouton repeat-button, qui nous servira d’interface dans le code. Le style du bouton est GtkToggleButton qui ne peut être que dans deux états (enclenché ou pas). Le champ action_name possède quant à lui la valeur app.play-repeat : c’est la fonction de callback qui sera appelée lorsqu’on appuiera sur le bouton. Enfin, la propriété image est intialisée à image2 qui est lui-même défini un peu plus haut dans le document, on voit que cela correspond à l’icone media-playlist-repeat-symbolic qui est bien celle attendue :

<object class="GtkImage" id="image2">
  <property name="visible">True</property>
  <property name="can_focus">False</property>
  <property name="pixel_size">24</property>
  <property name="icon_name">media-playlist-repeat-symbolic</property>
</object>

Notre première modification consiste à modifier dans ./data/ui/main-toolbar.ui le type du bouton repeat-button de GtkToggleButton à simplement GtkButton. Ceci nous permettra d’envisager un bouton à trois états (lecture simple, boucle play-list et boucle un morceau). Attention, en faisant cette modification nous sommes susceptible d’introduire des erreurs dans le programme, par exemple dans le cas où le code chercherait à accéder à des propriétés ou méthodes qui existent dans la classe GtkToggleButton mais pas dans la classe GtkButton2.

Si on compile en l’état, on voit que le style du bouton a bien changé et que le reste du programme semble se comporter normalement.

Cherchons à creuser un peu plus et vérifions à quel endroit le callback est défini. Ceci semble être fait dans ./shell/rb-shell-player.c, plus précisement à l’intérieur de la fonction rb_shell_player_constructed() :

static void
rb_shell_player_constructed (GObject *object)
{
[...]
	GActionEntry actions[] = {
		{ "play", play_action_cb },
		{ "play-previous", play_previous_action_cb },
		{ "play-next", play_next_action_cb },
		{ "play-repeat", play_repeat_action_cb, "b", "false" },
		{ "play-shuffle", play_shuffle_action_cb, "b", "false" },
		{ "volume-up", play_volume_up_action_cb },
		{ "volume-down", play_volume_down_action_cb }
[...]
	g_action_map_add_action_entries (G_ACTION_MAP (app),
					 actions,
					 G_N_ELEMENTS (actions),
					 player);
[...]
}

La fonction qui nous intéresse s’appelle play_repeat_action_cb. Voici ci-dessous les modifications que j’ai introduites. La variable repeat n’est désormais plus un booléen mais un entier. C’est la deuxième modification casse-gueule du code puisqu’il faut maintenant penser à répercuter ce changement partout ailleurs dans le code (je ne le montre pas ici). Le fait de boucler entre les trois états à chaque appui sur le bouton se traduit simplement dans le code grâce à un modulo.

static void
play_repeat_action_cb (GSimpleAction *action, GVariant *parameter, gpointer user_data)
{
	g_print("[DONUT] play_repeat_action_cb() in rb-shell-player.c has been called !\n");
	
	RBShellPlayer *player = RB_SHELL_PLAYER (user_data);
	const char *neworder;
	gboolean shuffle = FALSE;
	//gboolean repeat = FALSE;
	gint repeat = 0;
	rb_debug ("repeat changed");

	if (player->priv->syncing_state)
		return;

	rb_shell_player_get_playback_state (player, &shuffle, &repeat);

	//repeat = !repeat; // C'est ici que le statut de repeat est mis-à-jour !!
	repeat = (repeat+1)%3;
	
	neworder = state_to_play_order[shuffle ? 1 : 0][repeat];
	g_settings_set_string (player->priv->settings, "play-order", neworder);
}

Bien. Maintenant que nous savons boucler entre les trois états, il faut pouvoir identifier notre nouvel état “loop sur un morceau”. Ceci est défini par la variable state_to_play_order qui est un tableau dont les lignes indiquent le statut du shuffle et les colonnes celui du repeat. Ces deux variables étant initialement des booléens, nous avions logiquement un tableau 2x2. Désormais, il faudra composer avec un tableau 2x3. Je décide d’appeler ce nouveal état “linear-one” et ne fait aucun distingo de comportement selon l’état du shuffle.

static const char* const state_to_play_order[2][3] =
	{ {"linear",	"linear-loop", "linear-one"},
	 {"shuffle",	"random-by-age-and-rating", "linear-one"} };

Bon maintenant que cet état existe, il faut encore déterminer le comportement du lecteur audio. Toujours dans le même fichier ./shell/rb-shell-player.c se trouve la fonction rb_shell_player_init() qui vient brancher les différentes fonctions de lecture en fonction de l’état. Je rajoute naturellement ma propre fonction dans la liste. Nous remarquons au passage qu’il existait 8 types de lecture différentes ce qui est plus élevé que les 2x2 = 4 états initiaux accessibles par les deux boutons “repeat” et “shuffle”.

static void
rb_shell_player_init (RBShellPlayer *player)
{
	[...]
	
	rb_shell_player_add_play_order (player, "linear", N_("Linear"),
					RB_TYPE_LINEAR_PLAY_ORDER, FALSE);
	rb_shell_player_add_play_order (player, "linear-loop", N_("Linear looping"),
					RB_TYPE_LINEAR_PLAY_ORDER_LOOP, FALSE);
	rb_shell_player_add_play_order (player, "shuffle", N_("Shuffle"),
					RB_TYPE_SHUFFLE_PLAY_ORDER, FALSE);
	rb_shell_player_add_play_order (player, "random-equal-weights", N_("Random with equal weights"),
					RB_TYPE_RANDOM_PLAY_ORDER_EQUAL_WEIGHTS, FALSE);
	rb_shell_player_add_play_order (player, "random-by-age", N_("Random by time since last play"),
					RB_TYPE_RANDOM_PLAY_ORDER_BY_AGE, FALSE);
	rb_shell_player_add_play_order (player, "random-by-rating", N_("Random by rating"),
					RB_TYPE_RANDOM_PLAY_ORDER_BY_RATING, FALSE);
	rb_shell_player_add_play_order (player, "random-by-age-and-rating", N_("Random by time since last play and rating"),
					RB_TYPE_RANDOM_PLAY_ORDER_BY_AGE_AND_RATING, FALSE);
	rb_shell_player_add_play_order (player, "linear-one", N_("Repeat the same song !"), // DONUT
					RB_TYPE_LINEAR_PLAY_ORDER_ONE, FALSE);					
	rb_shell_player_add_play_order (player, "queue", N_("Linear, removing entries once played"),
					RB_TYPE_QUEUE_PLAY_ORDER, TRUE);
	[...]
}

La où ça devient intéressant c’est que, dans la conception du logiciel Rhythmbox, chaque nouvel état de lecture (“linear”, “linear-loop” etc…) est entièrement décrit par un objet de type RBPlayOrder dont la définition se trouve dans un couple de fichier .c et .h qui lui est propre. Par exemple rb-play-order-linear-loop.c et rb-play-order-linear-loop.h.

J’ai suivi cette logique et j’ai donc crée deux nouveaux fichiers qui décriront le comportement du lecteur dans l’état “linear-one”. La construction de ce nouveau objet s’appuie en grande partie sur les macros et les conventions de la bibliothèque GObject (pour plus d’informations, se référer ici ou ). Je ne détaille pas ces conventions et je me suis pour l’essentiel limité à reproduire la structure des autres RBPlayOrder.

A minima, il nous faut décrire le comportement du lecteur lorsqu’on cherche à accéder au morceau précedent ou au morceau suivant, ainsi que l’illustre la définition suivante :

static void
rb_linear_play_order_one_class_init (RBLinearPlayOrderOneClass *klass)
{
	RBPlayOrderClass *porder = RB_PLAY_ORDER_CLASS (klass);
	porder->has_next = rb_play_order_model_not_empty;
	porder->has_previous = rb_play_order_model_not_empty;
	porder->get_next = rb_linear_play_order_one_get_next;
	porder->get_previous = rb_linear_play_order_one_get_previous;
}

Deux structures sont cruciales à ce niveau. RhythmDBEntry est une structure qui représente (très grossièrement) un morceau de musique. RhythmDBQueryModel est quant-à-lui une structure qui représente la play-list. A partir de là, il devient trivial de faire boucler le morceau sur lui-même (c’est vraiment le coeur de notre modification) :

/* GET NEXT */
static RhythmDBEntry *
rb_linear_play_order_one_get_next (RBPlayOrder *porder)
{
	RhythmDBQueryModel *model;
	RhythmDBEntry *entry;

	g_return_val_if_fail (porder != NULL, NULL);
	g_return_val_if_fail (RB_IS_LINEAR_PLAY_ORDER_ONE (porder), NULL);

	model = rb_play_order_get_query_model (porder);
	if (model == NULL)
		return NULL;

	g_object_get (porder, "playing-entry", &entry, NULL);

	return entry;
}


/* GET PREVIOUS */
static RhythmDBEntry *
rb_linear_play_order_one_get_previous (RBPlayOrder *porder)
{
	RhythmDBQueryModel *model;
	RhythmDBEntry *entry;

	g_return_val_if_fail (porder != NULL, NULL);
	g_return_val_if_fail (RB_IS_LINEAR_PLAY_ORDER_ONE (porder), NULL);

	model = rb_play_order_get_query_model (porder);
	if (model == NULL)
		return NULL;

	g_object_get (porder, "playing-entry", &entry, NULL);

	return entry;
}

Encore une fois, je ne détaille pas les autres fonctions ni le fichier header associé, je me suis basé sur une copie des fichiers “linear-loop” déjà existants et je les ai adaptés. Avant de pouvoir espérer compiler tout ça, il faut également penser à mettre à jour le Makefile du répertoire shell afin d’y ajouter nos deux nouveaux fichiers.

Une fois qu’on recompile tout le bousin, on se rend compte que le nouvel état est bien accessible et qu’il boucle comme convenu sur le même morceau de la play-list :)

Etape trois : ajout d’un bouton à trois états

La dernière étape consiste à ajouter le bouclage de l’icone tel qu’expliqué au début de l’article à l’endroit idoine du code de Rhythmbox.

Je pensais initialement que sa place était naturellement au sein de la fonction play_repeat_action_cb() du fichier ./shell/rb-shell-player.c. Néanmoins, à ce stade une difficulté est de suite apparue : il ne me semble pas que le Widget du bouton de loop soit propagé au sein de cette fonction.

En y réflechissant un peu, on constate que le même problème est susceptible d’affecter le bouton “play/pause” : quand on clique dessus la musique s’arrête et l’icone du bouton est modifiée. Il peut donc être instructif de regarder comment ce dernier est codé. Le callback de ce dernier n’est pas défini dans rb-shell-player.c mais dans rb-shell.c :

static void
rb_shell_playing_changed_cb (RBShellPlayer *player, gboolean playing, RBShell *shell)
{
	const char *tooltip;
	const char *icon_name;
	GtkWidget *image;

	image = gtk_button_get_image (GTK_BUTTON (shell->priv->play_button));
	if (playing) {
		if (rb_source_can_pause (rb_shell_player_get_active_source (shell->priv->player_shell))) {
			icon_name = "media-playback-pause-symbolic";
			tooltip = _("Pause playback");
		} else {
			icon_name = "media-playback-stop-symbolic";
			tooltip = _("Stop playback");
		}
	} else {
		icon_name = "media-playback-start-symbolic";
		tooltip = _("Start playback");
	}
	gtk_image_set_from_icon_name (GTK_IMAGE (image), icon_name, GTK_ICON_SIZE_LARGE_TOOLBAR);

	gtk_widget_set_tooltip_text (GTK_WIDGET (shell->priv->play_button), tooltip);
}

On remarque que la fonction accède au bouton play grâce à shell->priv->play_button, il nous faut donc rajouter dans cette structure un nouveau champ pour stocker le bouton repeat.

Cela se fait au sein de la structure _RBShellPrivate du fichier rb-shell.c :

struct _RBShellPrivate
{
	[...]
	GtkWidget *play_button;
	GtkWidget *repeat_button; // DONUT
	[...]
};

Toujours dans le même fichier, nous rajoutons ensuite le widget du bouton repeat lors de sa création, dans la fonction construct_load_ui :

static void
construct_load_ui (RBShell *shell)
{
	[...]
	shell->priv->play_button = GTK_WIDGET (gtk_builder_get_object (builder, "play-button"));
	shell->priv->repeat_button = GTK_WIDGET (gtk_builder_get_object (builder, "repeat-button")); // DONUT
	[...]
}

Toujours dans le même fichier, on ajoute le callback au sein de la fonction construct_widgets. Je comprends cet appel de cette façon : chaque fois que le signal “repeat-changed” sera perçu au sein de l’objet shell->priv->player_shell, on exécutera la fonction rb_shell_repeat_changed_cb en lui passant l’argument shell.

static void
construct_widgets (RBShell *shell)
{
	[...]
	g_print("[DONUT] construct_widgets() from rb_shell.c is about to connect rb_shell_playing_changed_cb() function...\n");
	g_signal_connect_object (shell->priv->player_shell,
				 "playing-changed",
				 G_CALLBACK (rb_shell_playing_changed_cb),
				 shell, 0);

	g_print("[DONUT] construct_widgets() from rb_shell.c is about to connect rb_shell_repeat_changed_cb() function...\n");
	g_signal_connect_object (shell->priv->player_shell,
				 "repeat-changed",
				 G_CALLBACK (rb_shell_repeat_changed_cb),
				 shell, 0);
	[...]
}

Il nous reste maintenant à écrire cette fameuse fonction rb_shell_repeat_changed_cb (toujours dans rb-shell.c) dont le seul but est de modifier l’icone du bouton (à l’instar de son alter ego rb_shell_playing_changed_cb).

static void
rb_shell_repeat_changed_cb (RBShellPlayer *player, gint repeat, RBShell *shell)
{

GtkWidget *image;
	image = gtk_button_get_image (GTK_BUTTON (shell->priv->repeat_button));
	const char *icon_name;
	
if (repeat==0) // Linear
		icon_name = "media-playlist-consecutive-symbolic";
	else if (repeat ==1) // Loop all
		icon_name = "media-playlist-repeat-symbolic";
	else
		icon_name = "media-playlist-repeat-song-symbolic";
		
gtk_image_set_from_icon_name (GTK_IMAGE (image), icon_name, GTK_ICON_SIZE_LARGE_TOOLBAR);

}

A ce stade tout est en place et il nous reste juste à gérer l’émission du signal “repeat-changed” au bon moment.

Dans rb-shell-player.h on ajoute une nouvelle fonction repeat_changed au sein de la classe RBShellPlayerClass

struct _RBShellPlayerClass
{
	GObjectClass parent_class;

	void (*window_title_changed) (RBShellPlayer *player, const char *window_title);
	void (*elapsed_changed) (RBShellPlayer *player, guint elapsed);
	void (*elapsed_nano_changed) (RBShellPlayer *player, gint64 elapsed);
	void (*playing_changed) (RBShellPlayer *player, gboolean playing);
	void (*playing_source_changed) (RBShellPlayer *player, RBSource *source);
	void (*playing_uri_changed) (RBShellPlayer *player, const char *uri);
	void (*playing_song_changed) (RBShellPlayer *player, RhythmDBEntry *entry);
	void (*playing_song_property_changed) (RBShellPlayer *player,
					       const char *uri,
					       const char *property,
					       GValue *old,
					       GValue *newValue);
	void (*repeat_changed) (RBShellPlayer *player, gint repeat); // DONUT
};

Dans rb-shell-player.c on rajoute un signal

enum
{
	WINDOW_TITLE_CHANGED,
	ELAPSED_CHANGED,
	PLAYING_SOURCE_CHANGED,
	PLAYING_CHANGED,
	PLAYING_SONG_CHANGED,
	PLAYING_URI_CHANGED,
	PLAYING_SONG_PROPERTY_CHANGED,
	ELAPSED_NANO_CHANGED,
	REPEAT_CHANGED, // DONUT
	LAST_SIGNAL
};

Toujours dans le même fichier, on connecte enfin le signal (c’est ici que le lien avec rb-shell.c s’effectue)

static void
rb_shell_player_class_init (RBShellPlayerClass *klass)
{
	[...]
	
	rb_shell_player_signals[PLAYING_CHANGED] =
		g_signal_new ("playing-changed",
			      G_OBJECT_CLASS_TYPE (object_class),
			      G_SIGNAL_RUN_LAST,
			      G_STRUCT_OFFSET (RBShellPlayerClass, playing_changed),
			      NULL, NULL,
			      NULL,
			      G_TYPE_NONE,
			      1,
			      G_TYPE_BOOLEAN);
	[...]
	
	rb_shell_player_signals[REPEAT_CHANGED] =
		g_signal_new ("repeat-changed",
			      G_OBJECT_CLASS_TYPE (object_class),
			      G_SIGNAL_RUN_LAST,
			      G_STRUCT_OFFSET (RBShellPlayerClass, repeat_changed),
			      NULL, NULL,
			      NULL,
			      G_TYPE_NONE,
			      1,
			      G_TYPE_INT);	

	[...]
}

Finalement, toujours dans le même fichier on ajoute l’émission du signal à la fin de la fonction play_repeat_action_cb :

static void
play_repeat_action_cb (GSimpleAction *action, GVariant *parameter, gpointer user_data) // DONUT
{
	[...]
	g_signal_emit (player, rb_shell_player_signals[REPEAT_CHANGED], 0,
		       repeat);	
}

Désormais, l’affichage du bouton se mettra à jour à chaque clic ! Une dernière étape reste cependant à effectuer : lors du lancement de l’application, le bouton replay est initialisé en dur à “répéter la play-list” ainsi que le décrit le fichier ./data/ui/main-toolbar.ui dont je parlais au début de l’article.

<object class="GtkImage" id="image2">
  <property name="visible">True</property>
  <property name="can_focus">False</property>
  <property name="pixel_size">24</property>
  <property name="icon_name">media-playlist-repeat-symbolic</property>
</object>

Ceci correspond dans notre code à une valeur de repeat de 1. Dernière petite subtilité, on rajoute l’initialisation à 1 de la variable repeat. J’ai placé cette dernière au sein de la fonction rb_shell_player_constructed.

static void
rb_shell_player_constructed (GObject *object)
{
	[...]
	
	gboolean shuffle;
	gint repeat;
	rb_shell_player_get_playback_state (player, &shuffle, &repeat);
	rb_shell_player_set_playback_state (player, shuffle, 1);
		
}

Conclusion

Nous arrivons enfin à la fin de cet article. Nous avons obtenu un résultat satisfaisant bien que je ne sois pas sûr d’avoir réussi à respecter du début à la fin la “logique Rhythmbox”.

L’ajout d’un nouveau “play-order” me paraît propre et en ligne avec le reste du code même si je ne maîtrise pas toutes les subtilités de la création d’objets GObject.

La propagation du changement de type de la variable repeat (de gboolean à gint) a demandé quelques précautions mais au final ça s’est mieux passé que ce que je craignais.

En revanche, je nourris quelques doutes sur la propreté de l’ajout du nouveau signal “repeat-changed”. J’ai également complétement ignoré la possible sauvegarde d’état du logiciel lorsqu’on le quitte. Ainsi la variable “repeat” est toujours initialisé à 1 (de façon un peu grossière j’en conviens). Je n’ai d’ailleurs pas trouvé où ces différentes variables étaient initialisées (je suppute une subtilité de GObject et des fonctions *_init).

Mais enfin comme disait D. J. Barrett dans son livre sur SSH “Enough is enough” !

Lectures utiles

gtk se base principalement sur les trois bibliothèques GLib, GIO et GObject ainsi que l’indique la documentation officielle.

GLib redéfinit certains aspects de la libc comme le traitement des chaînes de caractères et introduit également des structures de données plus élaborées comme les arbres ou les tables de hash.

GIO quant à elle fournit un système de fichier virtuel permettant d’accéder à des fichiers locaux ou distant avec la même API ainsi qu’un système de communication sur DBus.

GObject pour terminer introduit la notion de programmation orientée objet dans le C et est au coeur des objets manipulés par gtk (widgets, fenêtres etc…). GObject facilite également l’export des fonctions C ce qui rend sa “syntaxe” parfois un peu étrange, de l’aveu même de ses concepteurs :

A lot of programmers are used to working with compiled-only or dynamically interpreted-only languages and do not understand the >challenges associated with cross-language interoperability. This introduction tries to provide an insight into these challenges and >briefly describes the solution chosen by GLib.

The following chapters go into greater detail into how GType and GObject work and how you can use them as a C programmer. It is useful >to keep in mind that allowing access to C objects from other interpreted languages was one of the major design goals: this can often >explain the sometimes rather convoluted APIs and features present in this library.

Documentation officielle

Documentation GLib

Documentation GIO

Documentation GObject

Documentation gtk3

Liens utiles

Une introduction à GLib/GObject

  1. Je garde en tête qu’une documentation assez complète existe sur l’écriture des plugins dans Rhythmbox. Ce n’est pas le propos de cet article, donc je ne détaille pas plus. 

  2. Bien que développée en C, la bibliothèque gtk utilise le paradigme de la programmation orientée objet.