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"><Primary>a</property>