Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package goodvibes for openSUSE:Factory 
checked in at 2023-11-08 22:19:07
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/goodvibes (Old)
 and      /work/SRC/openSUSE:Factory/.goodvibes.new.17445 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "goodvibes"

Wed Nov  8 22:19:07 2023 rev:6 rq:1124244 version:0.7.8

Changes:
--------
--- /work/SRC/openSUSE:Factory/goodvibes/goodvibes.changes      2023-10-12 
11:42:01.156838823 +0200
+++ /work/SRC/openSUSE:Factory/.goodvibes.new.17445/goodvibes.changes   
2023-11-08 22:20:29.089249950 +0100
@@ -1,0 +2,7 @@
+Wed Nov  8 13:37:55 UTC 2023 - Andrea Manzini <andrea.manz...@suse.com>
+
+- Update to 0.7.8:
+  * Replace GtkVolumeControl with a custom volume control widget
+  * Add a keyboard shortcut 'm' for mute
+
+-------------------------------------------------------------------

Old:
----
  goodvibes-v0.7.7.tar.gz

New:
----
  goodvibes-v0.7.8.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ goodvibes.spec ++++++
--- /var/tmp/diff_new_pack.RSQrVC/_old  2023-11-08 22:20:29.705272580 +0100
+++ /var/tmp/diff_new_pack.RSQrVC/_new  2023-11-08 22:20:29.705272580 +0100
@@ -17,7 +17,7 @@
 
 
 Name:           goodvibes
-Version:        0.7.7
+Version:        0.7.8
 Release:        0
 Summary:        A lightweight radio player
 License:        GPL-3.0-only

++++++ goodvibes-v0.7.7.tar.gz -> goodvibes-v0.7.8.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/goodvibes-v0.7.7/NEWS new/goodvibes-v0.7.8/NEWS
--- old/goodvibes-v0.7.7/NEWS   2023-10-10 11:17:44.000000000 +0200
+++ new/goodvibes-v0.7.8/NEWS   2023-11-06 10:04:29.000000000 +0100
@@ -1,3 +1,12 @@
+0.7.8 (2023-11-06) - Gorilla / Ze Gran Zeft
+-------------------------------------------
+
+General
+ * Replace GtkVolumeControl with a custom volume control widget. #160
+ * Add a keyboard shortcut ``m`` for mute.
+
+
+
 0.7.7 (2023-10-10) - Öpik-Oort / Psygnosis
 ------------------------------------------
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/goodvibes-v0.7.7/data/io.gitlab.Goodvibes.appdata.xml.in 
new/goodvibes-v0.7.8/data/io.gitlab.Goodvibes.appdata.xml.in
--- old/goodvibes-v0.7.7/data/io.gitlab.Goodvibes.appdata.xml.in        
2023-10-10 11:17:44.000000000 +0200
+++ new/goodvibes-v0.7.8/data/io.gitlab.Goodvibes.appdata.xml.in        
2023-11-06 10:04:29.000000000 +0100
@@ -32,6 +32,7 @@
   </screenshots>
 
   <releases>
+    <release version="0.7.8" date="2023-11-06"/>
     <release version="0.7.7" date="2023-10-09"/>
     <release version="0.7.6" date="2023-01-18"/>
     <release version="0.7.5" date="2022-10-07"/>
@@ -65,6 +66,7 @@
   <url type="bugtracker">https://gitlab.com/goodvibes/goodvibes/issues</url>
   <url type="help">https://goodvibes.readthedocs.io</url>
   <url type="translate">https://hosted.weblate.org/projects/goodvibes</url>
+  <url type="donation">https://liberapay.com/arnaudr/</url>
 
   <launchable type="desktop-id">io.gitlab.Goodvibes.desktop</launchable>
   <translation type="gettext">goodvibes</translation>
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/goodvibes-v0.7.7/meson.build 
new/goodvibes-v0.7.8/meson.build
--- old/goodvibes-v0.7.7/meson.build    2023-10-10 11:17:44.000000000 +0200
+++ new/goodvibes-v0.7.8/meson.build    2023-11-06 10:04:29.000000000 +0100
@@ -1,7 +1,7 @@
 # SPDX-License-Identifier: GPL-3.0-only
 
 project('goodvibes', 'c',
-  version: '0.7.7',
+  version: '0.7.8',
   license: 'GPLv3',
   meson_version: '>= 0.49.0',
   default_options: [
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/goodvibes-v0.7.7/src/gv-graphical-application.c 
new/goodvibes-v0.7.8/src/gv-graphical-application.c
--- old/goodvibes-v0.7.7/src/gv-graphical-application.c 2023-10-10 
11:17:44.000000000 +0200
+++ new/goodvibes-v0.7.8/src/gv-graphical-application.c 2023-11-06 
10:04:29.000000000 +0100
@@ -76,6 +76,14 @@
 }
 
 static void
+mute_action_cb(GSimpleAction *action G_GNUC_UNUSED,
+              GVariant *parameters G_GNUC_UNUSED,
+              gpointer user_data G_GNUC_UNUSED)
+{
+       gv_ui_mute();
+}
+
+static void
 add_station_action_cb(GSimpleAction *action G_GNUC_UNUSED,
                      GVariant *parameters G_GNUC_UNUSED,
                      gpointer user_data G_GNUC_UNUSED)
@@ -135,6 +143,7 @@
 #pragma GCC diagnostic ignored "-Wmissing-field-initializers"
 static const GActionEntry action_entries[] = {
        { "play-stop", play_stop_action_cb },
+       { "mute", mute_action_cb },
        { "add-station", add_station_action_cb },
        { "preferences", preferences_action_cb },
        { "help", help_action_cb },
@@ -171,6 +180,7 @@
 
 static const GvActionAccel action_accels[] = {
        { "app.play-stop", "space" },
+       { "app.mute", "m" },
        { "app.add-station", "<Primary>a" },
        { "app.help", "F1" },
        { "app.close-ui", "<Primary>c" },
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/goodvibes-v0.7.7/src/ui/gv-playlist-view.c 
new/goodvibes-v0.7.8/src/ui/gv-playlist-view.c
--- old/goodvibes-v0.7.7/src/ui/gv-playlist-view.c      2023-10-10 
11:17:44.000000000 +0200
+++ new/goodvibes-v0.7.8/src/ui/gv-playlist-view.c      2023-11-06 
10:04:29.000000000 +0100
@@ -28,6 +28,7 @@
 #include "ui/gtk-additions.h"
 #include "ui/gv-stations-tree-view.h"
 #include "ui/gv-ui-internal.h"
+#include "ui/gv-volume-control.h"
 
 #include "ui/gv-playlist-view.h"
 
@@ -63,12 +64,13 @@
        GtkWidget *playback_status_label;
        GtkWidget *go_next_button;
        /* Controls */
+       GtkWidget *button_box;
        GtkWidget *play_button;
        GtkWidget *prev_button;
        GtkWidget *next_button;
+       GtkWidget *volume_control;
        GtkWidget *repeat_toggle_button;
        GtkWidget *shuffle_toggle_button;
-       GtkWidget *volume_button;
        /* Station list */
        GtkWidget *scrolled_window;
        GtkWidget *stations_tree_view;
@@ -79,7 +81,6 @@
 
        GBinding *repeat_binding;
        GBinding *shuffle_binding;
-       GBinding *volume_binding;
 };
 
 typedef struct _GvPlaylistViewPrivate GvPlaylistViewPrivate;
@@ -162,17 +163,6 @@
        gtk_button_set_image(button, image);
 }
 
-static void
-set_volume_button(GtkVolumeButton *volume_button, guint volume, gboolean mute)
-{
-       GtkScaleButton *scale_button = GTK_SCALE_BUTTON(volume_button);
-
-       if (mute)
-               gtk_scale_button_set_value(scale_button, 0);
-       else
-               gtk_scale_button_set_value(scale_button, volume);
-}
-
 /*
  * Private methods
  */
@@ -208,21 +198,6 @@
        set_play_button(button, state);
 }
 
-static void
-gv_playlist_view_update_volume_button(GvPlaylistView *self, GvPlayer *player)
-{
-       GvPlaylistViewPrivate *priv = self->priv;
-       GtkVolumeButton *volume_button = GTK_VOLUME_BUTTON(priv->volume_button);
-       guint volume = gv_player_get_volume(player);
-       gboolean mute = gv_player_get_mute(player);
-
-       g_binding_unbind(priv->volume_binding);
-       set_volume_button(volume_button, volume, mute);
-       priv->volume_binding = g_object_bind_property(
-               player, "volume", volume_button, "value",
-               G_BINDING_BIDIRECTIONAL);
-}
-
 /*
  * Signal handlers
  */
@@ -243,8 +218,6 @@
                gv_playlist_view_update_play_button(self, player);
        } else if (!g_strcmp0(property_name, "metadata")) {
                gv_playlist_view_update_playback_status_label(self, player);
-       } else if (!g_strcmp0(property_name, "mute")) {
-               gv_playlist_view_update_volume_button(self, player);
        }
 }
 
@@ -290,14 +263,10 @@
        priv->shuffle_binding = g_object_bind_property(
                player, "shuffle", priv->shuffle_toggle_button, "active",
                G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE);
-       priv->volume_binding = g_object_bind_property(
-               player, "volume", priv->volume_button, "value",
-               G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE);
 
        gv_playlist_view_update_station_name_label(self, player);
        gv_playlist_view_update_playback_status_label(self, player);
        gv_playlist_view_update_play_button(self, player);
-       gv_playlist_view_update_volume_button(self, player);
 }
 
 static void
@@ -314,8 +283,6 @@
        priv->repeat_binding = NULL;
        g_binding_unbind(priv->shuffle_binding);
        priv->shuffle_binding = NULL;
-       g_binding_unbind(priv->volume_binding);
-       priv->volume_binding = NULL;
 }
 
 /*
@@ -333,23 +300,6 @@
  */
 
 static void
-setup_adjustment(GtkScaleButton *scale_button, GObject *obj, const gchar 
*obj_prop)
-{
-       GtkAdjustment *adjustment;
-       guint minimum, maximum;
-       guint range;
-
-       g_object_get_property_uint_bounds(obj, obj_prop, &minimum, &maximum);
-       range = maximum - minimum;
-
-       adjustment = gtk_scale_button_get_adjustment(scale_button);
-       gtk_adjustment_set_lower(adjustment, minimum);
-       gtk_adjustment_set_upper(adjustment, maximum);
-       gtk_adjustment_set_step_increment(adjustment, range / 100);
-       gtk_adjustment_set_page_increment(adjustment, range / 10);
-}
-
-static void
 gv_playlist_view_populate_widgets(GvPlaylistView *self)
 {
        GvPlaylistViewPrivate *priv = self->priv;
@@ -370,12 +320,16 @@
        GTK_BUILDER_SAVE_WIDGET(builder, priv, go_next_button);
 
        /* Button box */
+       GTK_BUILDER_SAVE_WIDGET(builder, priv, button_box);
        GTK_BUILDER_SAVE_WIDGET(builder, priv, play_button);
        GTK_BUILDER_SAVE_WIDGET(builder, priv, prev_button);
        GTK_BUILDER_SAVE_WIDGET(builder, priv, next_button);
        GTK_BUILDER_SAVE_WIDGET(builder, priv, repeat_toggle_button);
        GTK_BUILDER_SAVE_WIDGET(builder, priv, shuffle_toggle_button);
-       GTK_BUILDER_SAVE_WIDGET(builder, priv, volume_button);
+
+       /* Create the volume control, add it to the button box */
+       priv->volume_control = gv_volume_control_new();
+       gtk_container_add(GTK_CONTAINER(priv->button_box), 
priv->volume_control);
 
        /* Stations tree view */
        GTK_BUILDER_SAVE_WIDGET(builder, priv, scrolled_window);
@@ -410,16 +364,12 @@
 gv_playlist_view_setup_widgets(GvPlaylistView *self)
 {
        GvPlaylistViewPrivate *priv = self->priv;
-       GObject *player_obj = G_OBJECT(gv_core_player);
 
        /* Give a name to some widgets, for the status icon window */
        gtk_widget_set_name(priv->go_next_button, "go_next_button");
        gtk_widget_set_name(priv->station_name_label, "station_name_label");
        gtk_widget_set_name(priv->stations_tree_view, "stations_tree_view");
 
-       /* Setup adjustment for the volume button */
-       setup_adjustment(GTK_SCALE_BUTTON(priv->volume_button), player_obj, 
"volume");
-
        /* Connect next button */
        g_signal_connect_object(priv->go_next_button, "clicked",
                                G_CALLBACK(on_go_next_button_clicked), self, 0);
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/goodvibes-v0.7.7/src/ui/gv-ui.c 
new/goodvibes-v0.7.8/src/ui/gv-ui.c
--- old/goodvibes-v0.7.7/src/ui/gv-ui.c 2023-10-10 11:17:44.000000000 +0200
+++ new/goodvibes-v0.7.8/src/ui/gv-ui.c 2023-11-06 10:04:29.000000000 +0100
@@ -148,6 +148,14 @@
 }
 
 void
+gv_ui_mute(void)
+{
+       GvPlayer *player = gv_core_player;
+
+       gv_player_toggle_mute(player);
+}
+
+void
 gv_ui_configure(void)
 {
        GList *item;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/goodvibes-v0.7.7/src/ui/gv-ui.h 
new/goodvibes-v0.7.8/src/ui/gv-ui.h
--- old/goodvibes-v0.7.7/src/ui/gv-ui.h 2023-10-10 11:17:44.000000000 +0200
+++ new/goodvibes-v0.7.8/src/ui/gv-ui.h 2023-11-06 10:04:29.000000000 +0100
@@ -37,6 +37,7 @@
 void gv_ui_configure(void);
 
 void gv_ui_play_stop(void);
+void gv_ui_mute(void);
 void gv_ui_present_add_station(void);
 void gv_ui_present_main       (void);
 void gv_ui_present_preferences(void);
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/goodvibes-v0.7.7/src/ui/gv-volume-control.c 
new/goodvibes-v0.7.8/src/ui/gv-volume-control.c
--- old/goodvibes-v0.7.7/src/ui/gv-volume-control.c     1970-01-01 
01:00:00.000000000 +0100
+++ new/goodvibes-v0.7.8/src/ui/gv-volume-control.c     2023-11-06 
10:04:29.000000000 +0100
@@ -0,0 +1,560 @@
+/*
+ * Goodvibes Radio Player
+ *
+ * Copyright (C) 2023 Arnaud Rebillout
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ *
+ * 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, version 3.
+ *
+ * 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, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <math.h>
+
+#include <glib-object.h>
+#include <glib.h>
+#include <gtk/gtk.h>
+
+#include "base/glib-object-additions.h"
+#include "base/log.h"
+#include "core/gv-core.h"
+#include "ui/gv-volume-control.h"
+
+#define VOLUME_SCALE_WIDTH 90
+#define SCROLLING_DELAY 500 // ms
+
+/**
+ * GvVolumeControl:
+ *
+ * A widget to change volume, with a mute button and a volume bar.
+ *
+ * `GvVolumeControl` is similar to the volume controls that one can find in
+ * most web media players out there, eg. YouTube, Vimeo, Bandcamp, etc... It
+ * consists of a mute button next to a volume bar (a scale in Gtk-speak). Users
+ * click the button to mute or unmute, and click the volume bar to set the
+ * volume. So far so good, but there's a bit more to it.
+ *
+ * When mute is clicked, the volume bar goes to zero, so that (1) it's more
+ * explicit, visually, that there's no sound, and (2) it's possible to unmute
+ * at any volume, by clicking anywhere on the volume bar. Of course, clicking
+ * mute again will unmute at the previous volume value.
+ *
+ * When the volume is brought to zero (by dragging the slider, or scrolling on
+ * the widget), the button is set to to "mute", so that it's possible to
+ * restore a non-zero volume value by clicking again on the mute button. This
+ * is actually tricky, because it means that we must remember such a non-zero
+ * volume value (we call it "fallback volume" in the code). How to exactly keep
+ * track of the last non-zero volume value differs depending on how the volume
+ * is modified. For a simple click, or when dragging the slider, we can record
+ * the volume when the click is released.  However when scrolling, we have to
+ * be a bit smarter, and rely on a timeout, so that we record the volume when
+ * it settles. If we didn't do that, we'd record the last non-zero volume value
+ * before it reaches zero (ie. almost zero), so unmuting would take us to
+ * "almost muted".
+ *
+ * ## Test check list
+ *
+ * Because of the behavior described above, the list of things to check in
+ * order to test this widget is a bit daunting.
+ *
+ * Test setting the volume with:
+ * - a simple click
+ * - click then drag the slider (go to zero and back, without releasing)
+ * - scroll on the slider (go to zero and back, don't stop scrolling)
+ * - externally from cmdline
+ *
+ * The volume scale must move gracefully (no jittering), the icon for the mute
+ * button must change to "muted" when the volume reaches zero.
+ *
+ * Test mute and unmute:
+ * - by clicking the button twice
+ * - click then drag the slider down to zero, release click, then click the
+ *   button to unmute
+ * - scroll on the slider down to zero, then click the button to unmute
+ * - externally from cmdline
+ *
+ * The volume scale must be shown as zero when muted. Then unmuting should
+ * restore the correct volume value.
+ *
+ * Additionally:
+ * - test that, when muted, a click on the volume scale is enough to unmute
+ * - on fresh start, and when the volume was never modified yet:
+ *   - what about mute/unmute, does that work?
+ *   - what about dragging/scrolling the slider to zero, and then unmute?
+ *
+ * Finally, and as long as there are two "views" in GoodVibes (playlist view
+ * and station view):
+ * - switch the views back and forth, make sure that the volume/mute states
+ *   don't change
+ * - switch to the station view, change mute/volume via cmdline, switch back,
+ *   make sure the states are correct.
+ */
+
+/*
+ * GObject definitions
+ */
+
+struct _GvVolumeControlPrivate {
+       /* Widgets */
+       GtkWidget *mute_button;
+       GtkWidget *volume_scale;
+       /* Internal */
+       gboolean volume_scale_clicked;
+       gboolean volume_scale_scrolling;
+       guint scrolling_timeout_id;
+       guint fallback_volume;
+};
+
+typedef struct _GvVolumeControlPrivate GvVolumeControlPrivate;
+
+struct _GvVolumeControl {
+       /* Parent instance structure */
+       GtkBox parent_instance;
+       /* Private data */
+       GvVolumeControlPrivate *priv;
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE(GvVolumeControl, gv_volume_control, GTK_TYPE_BOX)
+
+static void on_volume_scale_value_changed(GtkRange *range, GvVolumeControl 
*self);
+
+/*
+ * Helpers
+ */
+
+static void
+set_mute_button(GtkButton *button, gboolean mute, guint volume)
+{
+       GtkWidget *image;
+       const gchar *icon_name;
+
+       if (mute == TRUE || volume == 0)
+               icon_name = "audio-volume-muted";
+       else if (volume <= 33)
+               icon_name = "audio-volume-low";
+       else if (volume <= 66)
+               icon_name = "audio-volume-medium";
+       else
+               icon_name = "audio-volume-high";
+
+       image = gtk_image_new_from_icon_name(icon_name, GTK_ICON_SIZE_BUTTON);
+       gtk_button_set_image(button, image);
+}
+
+static void
+set_volume_scale(GtkScale *scale, guint volume)
+{
+       GtkRange *range = GTK_RANGE(scale);
+       gdouble value = (gdouble) volume;
+
+       gtk_range_set_value(range, value);
+}
+
+/*
+ * Private methods
+ */
+
+static void
+gv_volume_control_update(GvVolumeControl *self, gboolean update_mute, gboolean 
update_volume)
+{
+       GvVolumeControlPrivate *priv = self->priv;
+       GvPlayer *player = gv_core_player;
+       gboolean mute;
+       guint volume;
+
+       TRACE("%p, %p", self, player);
+
+       mute = gv_player_get_mute(player);
+       volume = gv_player_get_volume(player);
+
+       if (update_mute == TRUE) {
+               GtkButton *button = GTK_BUTTON(priv->mute_button);
+
+               set_mute_button(button, mute, volume);
+       }
+
+       /* When updating the volume scale, make sure to block signal handlers.
+        * Otherwise it will emit signals that will be picked up by our signal
+        * handlers, that will then set the volume. But we're not setting the
+        * volume, we're just moving the slider to reflect the current volume.
+        */
+       if (update_volume == TRUE) {
+               GtkScale *scale = GTK_SCALE(priv->volume_scale);
+
+               g_signal_handlers_block_by_func(scale, 
on_volume_scale_value_changed, self);
+
+               /* When muted, we display the volume as zero. This is a special
+                * case where we don't want the GtkScale to reflect the real
+                * volume value.
+                */
+               if (mute == TRUE)
+                       set_volume_scale(scale, 0);
+               else
+                       set_volume_scale(scale, volume);
+
+               g_signal_handlers_unblock_by_func(scale, 
on_volume_scale_value_changed, self);
+       }
+}
+
+/*
+ * Signal handlers
+ */
+
+static void
+on_player_notify_mute(GvPlayer *player, GParamSpec *pspec, GvVolumeControl 
*self)
+{
+       GvVolumeControlPrivate *priv = self->priv;
+       const gchar *property_name = g_param_spec_get_name(pspec);
+       gboolean mute;
+       guint volume;
+
+       TRACE("%p, %s, %p", player, property_name, self);
+
+       mute = gv_player_get_mute(player);
+       volume = gv_player_get_volume(player);
+
+       /* Handle a corner-case: what if user wants to unmute, however the
+        * volume is zero? Going from "muted" to "unmuted with volume=0"
+        * doesn't get us very far. That's why we have a "fallback volume",
+        * which is basically the last non-zero volume that we know of.
+        *
+        * This situation happens if ever users drags (or scrolls) the slider
+        * down to zero: it results in mute=true and volume=0. That's why we
+        * need to have a fallback volume.
+        *
+        * If even this fallback volume is zero, set the volume to 50%.
+        */
+       if (mute == FALSE && volume == 0) {
+               volume = priv->fallback_volume;
+               if (volume == 0) {
+                       GtkRange *range = GTK_RANGE(priv->volume_scale);
+                       GtkAdjustment *adj = gtk_range_get_adjustment(range);
+                       volume = (guint) (gtk_adjustment_get_upper(adj) / 2);
+               }
+               DEBUG("Setting volume from fallback: %u", volume);
+               gv_player_set_volume(player, volume);
+       }
+
+       /* Update widgets */
+       gv_volume_control_update(self, TRUE, TRUE);
+}
+
+static void
+on_player_notify_volume(GvPlayer *player, GParamSpec *pspec, GvVolumeControl 
*self)
+{
+       GvVolumeControlPrivate *priv = self->priv;
+       const gchar *property_name = g_param_spec_get_name(pspec);
+       gboolean update_mute, update_volume;
+       guint volume;
+
+       TRACE("%p, %s, %p", player, property_name, self);
+
+       volume = gv_player_get_volume(player);
+
+       /* We always want to update the mute widget, as the icon depends on
+        * the volume. We might or might not want to update the volume widget,
+        * see below.
+        */
+       update_mute = TRUE;
+       update_volume = FALSE;
+
+       /* When the volume is set due to a click or scrolling event:
+        * - Don't save the fallback volume now. It will be done later, when
+        *   the operation is finished, ie. click is released, or scrolling is
+        *   over (we have a timeout to decide when it is over).
+        * - Don't update the widget either. Moving the slider programmatically
+        *   while user is moving it at the same time causes some jittering,
+        *   and it's pointless anyway, there's no need for this feedback loop.
+        */
+       if (priv->volume_scale_clicked == TRUE) {
+               ;
+       } else if (priv->volume_scale_scrolling == TRUE) {
+               ;
+       } else {
+               update_volume = TRUE;
+               if (volume != 0)
+                       priv->fallback_volume = volume;
+       }
+
+       /* When the volume goes to zero, we *mute*, so that clicking the mute
+        * button afterwards results in *unmuting*, and restores the fallback
+        * volume.
+        *
+        * When the volume goes to non-zero, make sure to also *unmute*, so
+        * that if ever sound was muted, clicking on the volume scale doesn't
+        * only set the volume, it also unmutes.
+        */
+       if (volume == 0)
+               gv_player_set_mute(player, TRUE);
+       else
+               gv_player_set_mute(player, FALSE);
+
+       /* Update widgets */
+       gv_volume_control_update(self, update_mute, update_volume);
+}
+
+static void
+on_mute_button_clicked(GtkButton *button G_GNUC_UNUSED, GvVolumeControl *self 
G_GNUC_UNUSED)
+{
+       GvPlayer *player = gv_core_player;
+
+       gv_player_toggle_mute(player);
+}
+
+static gboolean
+on_volume_scale_button_press_event(GtkWidget *widget G_GNUC_UNUSED, GdkEvent 
*event G_GNUC_UNUSED, GvVolumeControl *self)
+{
+       GvVolumeControlPrivate *priv = self->priv;
+
+       priv->volume_scale_clicked = TRUE;
+
+       return GDK_EVENT_PROPAGATE;
+}
+
+static gboolean
+on_volume_scale_button_release_event(GtkWidget *widget G_GNUC_UNUSED, GdkEvent 
*event G_GNUC_UNUSED, GvVolumeControl *self)
+{
+       GvVolumeControlPrivate *priv = self->priv;
+       GvPlayer *player = gv_core_player;
+       guint volume;
+
+       priv->volume_scale_clicked = FALSE;
+
+       /* Update the fallback volume */
+       volume = gv_player_get_volume(player);
+       if (volume != 0)
+               priv->fallback_volume = volume;
+
+       return GDK_EVENT_PROPAGATE;
+}
+
+static gboolean
+when_scrolling_timeout(GvVolumeControl *self)
+{
+       GvVolumeControlPrivate *priv = self->priv;
+       GvPlayer *player = gv_core_player;
+       guint volume;
+
+       priv->volume_scale_scrolling = FALSE;
+
+       /* Update the fallback volume */
+       volume = gv_player_get_volume(player);
+       if (volume != 0)
+               priv->fallback_volume = volume;
+
+       priv->scrolling_timeout_id = 0;
+
+       return G_SOURCE_REMOVE;
+}
+
+static gboolean
+on_volume_scale_scroll_event(GtkWidget *widget G_GNUC_UNUSED, GdkEvent *event 
G_GNUC_UNUSED, GvVolumeControl *self)
+{
+       GvVolumeControlPrivate *priv = self->priv;
+
+       priv->volume_scale_scrolling = TRUE;
+
+       /* Schedule a callback to record the fallback volume */
+       g_clear_handle_id(&priv->scrolling_timeout_id, g_source_remove);
+       priv->scrolling_timeout_id = g_timeout_add(SCROLLING_DELAY,
+                       (GSourceFunc) when_scrolling_timeout, self);
+
+       return GDK_EVENT_PROPAGATE;
+}
+
+static void
+on_volume_scale_value_changed(GtkRange *range, GvVolumeControl *self 
G_GNUC_UNUSED)
+{
+       GvPlayer *player = gv_core_player;
+       gdouble value = round(gtk_range_get_value(range));
+
+       gv_player_set_volume(player, (guint) value);
+}
+
+static void
+on_map(GvVolumeControl *self, gpointer user_data)
+{
+       TRACE("%p, %p", self, user_data);
+
+       /* Make sure widgets have the right values */
+       gv_volume_control_update(self, TRUE, TRUE);
+}
+
+/*
+ * Public methods
+ */
+
+GtkWidget *
+gv_volume_control_new(void)
+{
+       return g_object_new(GV_TYPE_VOLUME_CONTROL, NULL);
+}
+
+/*
+ * Construct helpers
+ */
+
+static GParamSpecUInt *
+get_param_spec_uint(GObject *object, const gchar *property_name)
+{
+       GObjectClass *object_class;
+       GParamSpec *pspec;
+
+       object_class = G_OBJECT_GET_CLASS(object);
+       pspec = g_object_class_find_property(object_class, property_name);
+       g_assert(pspec != NULL);
+
+       return G_PARAM_SPEC_UINT(pspec);
+}
+
+static void
+setup_scale_adjustment(GtkScale *scale, GObject *obj, const gchar 
*obj_prop_name)
+{
+       GtkAdjustment *adjustment;
+       GParamSpecUInt *pspec;
+       guint range;
+
+       pspec = get_param_spec_uint(obj, obj_prop_name);
+       range = pspec->maximum - pspec->minimum;
+
+       adjustment = gtk_range_get_adjustment(GTK_RANGE(scale));
+       gtk_adjustment_set_lower(adjustment, pspec->minimum);
+       gtk_adjustment_set_upper(adjustment, pspec->maximum);
+       gtk_adjustment_set_step_increment(adjustment, range / 100);
+       gtk_adjustment_set_page_increment(adjustment, range / 10);
+}
+
+static void
+gv_volume_control_populate_widgets(GvVolumeControl *self)
+{
+       GvVolumeControlPrivate *priv = self->priv;
+       GtkWidget *w;
+
+       /* Add the mute button */
+       w = gtk_button_new_from_icon_name("audio-volume-high", 
GTK_ICON_SIZE_BUTTON);
+       gtk_button_set_relief(GTK_BUTTON(w), GTK_RELIEF_NONE);
+       gtk_button_set_always_show_image(GTK_BUTTON(w), TRUE);
+       gtk_container_add(GTK_CONTAINER(self), w);
+       priv->mute_button = w;
+
+       /* Add the volume scale */
+       w = gtk_scale_new(GTK_ORIENTATION_HORIZONTAL, NULL);
+       gtk_scale_set_draw_value(GTK_SCALE(w), FALSE);
+       gtk_widget_set_size_request(w, VOLUME_SCALE_WIDTH, -1);
+       gtk_container_add(GTK_CONTAINER(self), w);
+       priv->volume_scale = w;
+}
+
+static void
+gv_volume_control_setup_appearance(GvVolumeControl *self)
+{
+       GvVolumeControlPrivate *priv = self->priv;
+       GtkCssProvider *provider;
+       GtkStyleContext *context;
+       const gchar *css;
+
+       /* For a GTK3 Scale, the default padding is 12px. Let's reduce that a
+        * little on the left, to bring the mute icon and the volume scale
+        * closer to each other, and increase that a bit on the right,
+        * otherwise the button that come next almost touch the knob.
+        */
+       css = "scale {padding-left: 6px; padding-right: 24px;}";
+       provider = gtk_css_provider_new();
+       gtk_css_provider_load_from_data(provider, css, -1, NULL);
+
+       context = gtk_widget_get_style_context(priv->volume_scale);
+       gtk_style_context_add_provider(context, GTK_STYLE_PROVIDER(provider),
+                       GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+
+       g_object_unref(provider);
+}
+
+static void
+gv_volume_control_setup_widgets(GvVolumeControl *self)
+{
+       GvVolumeControlPrivate *priv = self->priv;
+       GvPlayer *player = gv_core_player;
+
+       /* Setup adjustment for the volume scale */
+       setup_scale_adjustment(GTK_SCALE(priv->volume_scale), G_OBJECT(player), 
"volume");
+
+       /* Connect widgets signal handlers */
+       g_signal_connect_object(priv->mute_button, "clicked",
+                               G_CALLBACK(on_mute_button_clicked), self, 0);
+       g_signal_connect_object(priv->volume_scale, "button-press-event",
+                               G_CALLBACK(on_volume_scale_button_press_event), 
self, 0);
+       g_signal_connect_object(priv->volume_scale, "button-release-event",
+                               
G_CALLBACK(on_volume_scale_button_release_event), self, 0);
+       g_signal_connect_object(priv->volume_scale, "scroll-event",
+                               G_CALLBACK(on_volume_scale_scroll_event), self, 
0);
+       g_signal_connect_object(priv->volume_scale, "value-changed",
+                               G_CALLBACK(on_volume_scale_value_changed), 
self, 0);
+
+       /* Connect self signal handlers */
+       g_signal_connect_object(self, "map", G_CALLBACK(on_map), NULL, 0);
+
+       /* Connect player signal handlers */
+       g_signal_connect_object(player, "notify::mute",
+                               G_CALLBACK(on_player_notify_mute), self, 0);
+       g_signal_connect_object(player, "notify::volume",
+                               G_CALLBACK(on_player_notify_volume), self, 0);
+}
+
+/*
+ * GObject methods
+ */
+
+static void
+gv_volume_control_finalize(GObject *object)
+{
+       TRACE("%p", object);
+
+       /* Chain up */
+       G_OBJECT_CHAINUP_FINALIZE(gv_volume_control, object);
+}
+
+static void
+gv_volume_control_constructed(GObject *object)
+{
+       GvVolumeControl *self = GV_VOLUME_CONTROL(object);
+
+       TRACE("%p", object);
+
+       /* Build widget */
+       gv_volume_control_populate_widgets(self);
+       gv_volume_control_setup_appearance(self);
+       gv_volume_control_setup_widgets(self);
+
+       /* Chain up */
+       G_OBJECT_CHAINUP_CONSTRUCTED(gv_volume_control, object);
+}
+
+static void
+gv_volume_control_init(GvVolumeControl *self)
+{
+       TRACE("%p", self);
+
+       /* Initialize private pointer */
+       self->priv = gv_volume_control_get_instance_private(self);
+}
+
+static void
+gv_volume_control_class_init(GvVolumeControlClass *class)
+{
+       GObjectClass *object_class = G_OBJECT_CLASS(class);
+
+       TRACE("%p", class);
+
+       /* Override GObject methods */
+       object_class->finalize = gv_volume_control_finalize;
+       object_class->constructed = gv_volume_control_constructed;
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/goodvibes-v0.7.7/src/ui/gv-volume-control.h 
new/goodvibes-v0.7.8/src/ui/gv-volume-control.h
--- old/goodvibes-v0.7.7/src/ui/gv-volume-control.h     1970-01-01 
01:00:00.000000000 +0100
+++ new/goodvibes-v0.7.8/src/ui/gv-volume-control.h     2023-11-06 
10:04:29.000000000 +0100
@@ -0,0 +1,33 @@
+/*
+ * Goodvibes Radio Player
+ *
+ * Copyright (C) 2023 Arnaud Rebillout
+ *
+ * SPDX-License-Identifier: GPL-3.0-only
+ *
+ * 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, version 3.
+ *
+ * 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, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <glib-object.h>
+
+/* GObject declarations */
+
+#define GV_TYPE_VOLUME_CONTROL gv_volume_control_get_type()
+
+G_DECLARE_FINAL_TYPE(GvVolumeControl, gv_volume_control, GV, VOLUME_CONTROL, 
GtkBox)
+
+/* Methods */
+
+GtkWidget *gv_volume_control_new(void);
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/goodvibes-v0.7.7/src/ui/meson.build 
new/goodvibes-v0.7.8/src/ui/meson.build
--- old/goodvibes-v0.7.7/src/ui/meson.build     2023-10-10 11:17:44.000000000 
+0200
+++ new/goodvibes-v0.7.8/src/ui/meson.build     2023-11-06 10:04:29.000000000 
+0100
@@ -23,6 +23,7 @@
   'gv-status-icon.c',
   'gv-ui.c',
   'gv-ui-helpers.c',
+  'gv-volume-control.c',
 ]
 
 ui_dependencies = [
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/goodvibes-v0.7.7/src/ui/resources/playlist-view.glade 
new/goodvibes-v0.7.8/src/ui/resources/playlist-view.glade
--- old/goodvibes-v0.7.7/src/ui/resources/playlist-view.glade   2023-10-10 
11:17:44.000000000 +0200
+++ new/goodvibes-v0.7.8/src/ui/resources/playlist-view.glade   2023-11-06 
10:04:29.000000000 +0100
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<!-- Generated with glade 3.38.2 -->
+<!-- Generated with glade 3.40.0 -->
 <interface>
   <requires lib="gtk+" version="3.16"/>
   <object class="GtkImage" id="go_next_image">
@@ -145,35 +145,7 @@
           </packing>
         </child>
         <child>
-          <object class="GtkVolumeButton" id="volume_button">
-            <property name="visible">True</property>
-            <property name="can-focus">True</property>
-            <property name="receives-default">True</property>
-            <property name="relief">none</property>
-            <property name="orientation">vertical</property>
-            <property name="icons">audio-volume-muted-symbolic
-audio-volume-high-symbolic
-audio-volume-low-symbolic
-audio-volume-medium-symbolic</property>
-            <child internal-child="plus_button">
-              <object class="GtkButton">
-                <property name="can-focus">False</property>
-                <property name="receives-default">False</property>
-              </object>
-            </child>
-            <child internal-child="minus_button">
-              <object class="GtkButton">
-                <property name="can-focus">False</property>
-                <property name="receives-default">False</property>
-              </object>
-            </child>
-          </object>
-          <packing>
-            <property name="expand">False</property>
-            <property name="fill">True</property>
-            <property name="pack-type">end</property>
-            <property name="position">3</property>
-          </packing>
+          <placeholder/>
         </child>
         <child>
           <object class="GtkToggleButton" id="shuffle_toggle_button">
@@ -188,7 +160,7 @@
             <property name="expand">False</property>
             <property name="fill">True</property>
             <property name="pack-type">end</property>
-            <property name="position">4</property>
+            <property name="position">5</property>
           </packing>
         </child>
         <child>
@@ -204,7 +176,7 @@
             <property name="expand">False</property>
             <property name="fill">True</property>
             <property name="pack-type">end</property>
-            <property name="position">5</property>
+            <property name="position">6</property>
           </packing>
         </child>
       </object>
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/goodvibes-v0.7.7/src/ui/resources/shortcuts-window.ui 
new/goodvibes-v0.7.8/src/ui/resources/shortcuts-window.ui
--- old/goodvibes-v0.7.7/src/ui/resources/shortcuts-window.ui   2023-10-10 
11:17:44.000000000 +0200
+++ new/goodvibes-v0.7.8/src/ui/resources/shortcuts-window.ui   2023-11-06 
10:04:29.000000000 +0100
@@ -17,6 +17,13 @@
               </object>
             </child>
             <child>
+              <object class="GtkShortcutsShortcut" id="pause">
+                <property name="visible">1</property>
+                <property name="accelerator">m</property>
+               <property name="title" translatable="yes">Mute</property>
+              </object>
+            </child>
+            <child>
               <object class="GtkShortcutsShortcut" id="add-station">
                 <property name="visible">1</property>
                 <property name="accelerator">&lt;Primary&gt;a</property>

Reply via email to