/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- * * Copyright (C) 2008 William Jon McCann * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "config.h" #include #include #include #include #include #include #include #if GTK_CHECK_VERSION (2, 21, 8) #include #else #include #endif #include "gvc-mixer-stream.h" #include "gvc-channel-bar.h" #include "gvc-stream-status-icon.h" #if GTK_CHECK_VERSION (3, 0, 0) #define MATE_DESKTOP_USE_UNSTABLE_API #include #define gdk_spawn_command_line_on_screen mate_gdk_spawn_command_line_on_screen #endif #define GVC_STREAM_STATUS_ICON_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), GVC_TYPE_STREAM_STATUS_ICON, GvcStreamStatusIconPrivate)) struct GvcStreamStatusIconPrivate { char **icon_names; GvcMixerStream *mixer_stream; GtkWidget *dock; GtkWidget *bar; guint current_icon; char *display_name; gboolean thaw; }; enum { PROP_0, PROP_DISPLAY_NAME, PROP_MIXER_STREAM, PROP_ICON_NAMES, }; static void gvc_stream_status_icon_class_init (GvcStreamStatusIconClass *klass); static void gvc_stream_status_icon_init (GvcStreamStatusIcon *stream_status_icon); static void gvc_stream_status_icon_finalize (GObject *object); G_DEFINE_TYPE (GvcStreamStatusIcon, gvc_stream_status_icon, GTK_TYPE_STATUS_ICON) static void on_adjustment_value_changed (GtkAdjustment *adjustment, GvcStreamStatusIcon *icon) { gdouble volume; if (icon->priv->thaw) return; volume = gtk_adjustment_get_value (adjustment); /* Only push the volume if it's actually changed */ if (gvc_mixer_stream_set_volume(icon->priv->mixer_stream, (pa_volume_t) round (volume)) != FALSE) { gvc_mixer_stream_push_volume(icon->priv->mixer_stream); } } static void update_dock (GvcStreamStatusIcon *icon) { GtkAdjustment *adj; gboolean is_muted; g_return_if_fail (icon); adj = GTK_ADJUSTMENT (gvc_channel_bar_get_adjustment (GVC_CHANNEL_BAR (icon->priv->bar))); icon->priv->thaw = TRUE; gtk_adjustment_set_value (adj, gvc_mixer_stream_get_volume (icon->priv->mixer_stream)); is_muted = gvc_mixer_stream_get_is_muted (icon->priv->mixer_stream); gvc_channel_bar_set_is_muted (GVC_CHANNEL_BAR (icon->priv->bar), is_muted); icon->priv->thaw = FALSE; } static gboolean popup_dock (GvcStreamStatusIcon *icon, guint time) { GdkRectangle area; GtkOrientation orientation; GdkDisplay *display; GdkScreen *screen; gboolean res; int x; int y; int monitor_num; GdkRectangle monitor; GtkRequisition dock_req; update_dock (icon); screen = gtk_status_icon_get_screen (GTK_STATUS_ICON (icon)); res = gtk_status_icon_get_geometry (GTK_STATUS_ICON (icon), &screen, &area, &orientation); if (! res) { g_warning ("Unable to determine geometry of status icon"); return FALSE; } /* position roughly */ gtk_window_set_screen (GTK_WINDOW (icon->priv->dock), screen); gvc_channel_bar_set_orientation (GVC_CHANNEL_BAR (icon->priv->bar), 1 - orientation); monitor_num = gdk_screen_get_monitor_at_point (screen, area.x, area.y); gdk_screen_get_monitor_geometry (screen, monitor_num, &monitor); gtk_container_foreach (GTK_CONTAINER (icon->priv->dock), (GtkCallback) gtk_widget_show_all, NULL); #if GTK_CHECK_VERSION (3, 0, 0) gtk_widget_get_preferred_size (icon->priv->dock, &dock_req, NULL); #else gtk_widget_size_request (icon->priv->dock, &dock_req); #endif if (orientation == GTK_ORIENTATION_VERTICAL) { if (area.x + area.width + dock_req.width <= monitor.x + monitor.width) { x = area.x + area.width; } else { x = area.x - dock_req.width; } if (area.y + dock_req.height <= monitor.y + monitor.height) { y = area.y; } else { y = monitor.y + monitor.height - dock_req.height; } } else { if (area.y + area.height + dock_req.height <= monitor.y + monitor.height) { y = area.y + area.height; } else { y = area.y - dock_req.height; } if (area.x + dock_req.width <= monitor.x + monitor.width) { x = area.x; } else { x = monitor.x + monitor.width - dock_req.width; } } gtk_window_move (GTK_WINDOW (icon->priv->dock), x, y); /* FIXME: without this, the popup window appears as a square * after changing the orientation */ gtk_window_resize (GTK_WINDOW (icon->priv->dock), 1, 1); gtk_widget_show_all (icon->priv->dock); /* grab focus */ gtk_grab_add (icon->priv->dock); if (gdk_pointer_grab (gtk_widget_get_window (icon->priv->dock), TRUE, GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK | GDK_SCROLL_MASK, NULL, NULL, time) != GDK_GRAB_SUCCESS) { gtk_grab_remove (icon->priv->dock); gtk_widget_hide (icon->priv->dock); return FALSE; } if (gdk_keyboard_grab (gtk_widget_get_window (icon->priv->dock), TRUE, time) != GDK_GRAB_SUCCESS) { display = gtk_widget_get_display (icon->priv->dock); gdk_display_pointer_ungrab (display, time); gtk_grab_remove (icon->priv->dock); gtk_widget_hide (icon->priv->dock); return FALSE; } gtk_widget_grab_focus (icon->priv->dock); return TRUE; } static void on_status_icon_activate (GtkStatusIcon *status_icon, GvcStreamStatusIcon *icon) { popup_dock (icon, GDK_CURRENT_TIME); } static gboolean on_status_icon_button_press (GtkStatusIcon *status_icon, GdkEventButton *event, GvcStreamStatusIcon *icon) { /* middle click acts as mute/unmute */ if (event->button == 2) { gboolean is_muted; is_muted = gvc_mixer_stream_get_is_muted (icon->priv->mixer_stream); gvc_mixer_stream_set_is_muted (icon->priv->mixer_stream, !is_muted); gvc_mixer_stream_change_is_muted (icon->priv->mixer_stream, !is_muted); return TRUE; } return FALSE; } static void on_menu_mute_toggled (GtkMenuItem *item, GvcStreamStatusIcon *icon) { gboolean is_muted; is_muted = gtk_check_menu_item_get_active (GTK_CHECK_MENU_ITEM (item)); gvc_channel_bar_set_is_muted (GVC_CHANNEL_BAR (icon->priv->bar), is_muted); } static void on_menu_activate_open_volume_control (GtkMenuItem *item, GvcStreamStatusIcon *icon) { GError *error; error = NULL; gdk_spawn_command_line_on_screen (gtk_widget_get_screen (icon->priv->dock), "mate-volume-control", &error); if (error != NULL) { GtkWidget *dialog; dialog = gtk_message_dialog_new (NULL, 0, GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, _("Failed to start Sound Preferences: %s"), error->message); g_signal_connect (dialog, "response", G_CALLBACK (gtk_widget_destroy), NULL); gtk_widget_show (dialog); g_error_free (error); } } static void on_status_icon_popup_menu (GtkStatusIcon *status_icon, guint button, guint activate_time, GvcStreamStatusIcon *icon) { GtkWidget *menu; GtkWidget *item; GtkWidget *image; menu = gtk_menu_new (); item = gtk_check_menu_item_new_with_mnemonic (_("_Mute")); gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (item), gvc_mixer_stream_get_is_muted (icon->priv->mixer_stream)); g_signal_connect (item, "toggled", G_CALLBACK (on_menu_mute_toggled), icon); gtk_menu_shell_append (GTK_MENU_SHELL (menu), item); item = gtk_image_menu_item_new_with_mnemonic (_("_Sound Preferences")); image = gtk_image_new_from_icon_name ("multimedia-volume-control", GTK_ICON_SIZE_MENU); gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (item), image); g_signal_connect (item, "activate", G_CALLBACK (on_menu_activate_open_volume_control), icon); gtk_menu_shell_append (GTK_MENU_SHELL (menu), item); gtk_widget_show_all (menu); gtk_menu_popup (GTK_MENU (menu), NULL, NULL, gtk_status_icon_position_menu, status_icon, button, activate_time); } static gboolean on_status_icon_scroll_event (GtkStatusIcon *status_icon, GdkEventScroll *event, GvcStreamStatusIcon *icon) { return gvc_channel_bar_scroll (GVC_CHANNEL_BAR (icon->priv->bar), event->direction); } static void gvc_icon_release_grab (GvcStreamStatusIcon *icon, GdkEventButton *event) { GdkDisplay *display; /* ungrab focus */ display = gtk_widget_get_display (GTK_WIDGET (icon->priv->dock)); gdk_display_keyboard_ungrab (display, event->time); gdk_display_pointer_ungrab (display, event->time); gtk_grab_remove (icon->priv->dock); /* hide again */ gtk_widget_hide (icon->priv->dock); } static gboolean on_dock_button_press (GtkWidget *widget, GdkEventButton *event, GvcStreamStatusIcon *icon) { if (event->type == GDK_BUTTON_PRESS) { gvc_icon_release_grab (icon, event); return TRUE; } return FALSE; } static void popdown_dock (GvcStreamStatusIcon *icon) { GdkDisplay *display; /* ungrab focus */ display = gtk_widget_get_display (icon->priv->dock); gdk_display_keyboard_ungrab (display, GDK_CURRENT_TIME); gdk_display_pointer_ungrab (display, GDK_CURRENT_TIME); gtk_grab_remove (icon->priv->dock); /* hide again */ gtk_widget_hide (icon->priv->dock); } /* This is called when the grab is broken for * either the dock, or the scale itself */ static void gvc_icon_grab_notify (GvcStreamStatusIcon *icon, gboolean was_grabbed) { if (was_grabbed != FALSE) { return; } if (!gtk_widget_has_grab (icon->priv->dock)) { return; } if (gtk_widget_is_ancestor (gtk_grab_get_current (), icon->priv->dock)) { return; } popdown_dock (icon); } static void on_dock_grab_notify (GtkWidget *widget, gboolean was_grabbed, GvcStreamStatusIcon *icon) { gvc_icon_grab_notify (icon, was_grabbed); } static gboolean on_dock_grab_broken_event (GtkWidget *widget, gboolean was_grabbed, GvcStreamStatusIcon *icon) { gvc_icon_grab_notify (icon, FALSE); return FALSE; } static gboolean on_dock_key_release (GtkWidget *widget, GdkEventKey *event, GvcStreamStatusIcon *icon) { if (event->keyval == GDK_Escape) { popdown_dock (icon); return TRUE; } #if 0 if (!gtk_bindings_activate_event (GTK_OBJECT (widget), event)) { /* The popup hasn't managed the event, pass onto the button */ gtk_bindings_activate_event (GTK_OBJECT (user_data), event); } #endif return TRUE; } static gboolean on_dock_scroll_event (GtkWidget *widget, GdkEventScroll *event, GvcStreamStatusIcon *icon) { /* Forward event to the status icon */ on_status_icon_scroll_event (NULL, event, icon); return TRUE; } static void update_icon (GvcStreamStatusIcon *icon) { guint volume; gboolean is_muted; guint n; char *markup; gboolean can_decibel; gdouble db; if (icon->priv->mixer_stream == NULL) { return; } volume = gvc_mixer_stream_get_volume (icon->priv->mixer_stream); is_muted = gvc_mixer_stream_get_is_muted (icon->priv->mixer_stream); db = gvc_mixer_stream_get_decibel (icon->priv->mixer_stream); can_decibel = gvc_mixer_stream_get_can_decibel (icon->priv->mixer_stream); /* select image */ if (volume <= 0 || is_muted) { n = 0; } else { n = 3 * volume / PA_VOLUME_NORM + 1; if (n < 1) { n = 1; } else if (n > 3) { n = 3; } } /* apparently status icon will reset icon even if * if doesn't change */ if (icon->priv->current_icon != n) { gtk_status_icon_set_from_icon_name (GTK_STATUS_ICON (icon), icon->priv->icon_names [n]); icon->priv->current_icon = n; } if (is_muted) { markup = g_strdup_printf ( "%s: %s\n%s", icon->priv->display_name, _("Muted"), gvc_mixer_stream_get_description (icon->priv->mixer_stream)); } else if (can_decibel && (db > PA_DECIBEL_MININFTY)) { markup = g_strdup_printf ( "%s: %.0f%%\n%0.2f dB\n%s", icon->priv->display_name, 100 * (float)volume / PA_VOLUME_NORM, db, gvc_mixer_stream_get_description (icon->priv->mixer_stream)); } else if (can_decibel) { markup = g_strdup_printf ( "%s: %.0f%%\n-∞ dB\n%s", icon->priv->display_name, 100 * (float)volume / PA_VOLUME_NORM, gvc_mixer_stream_get_description (icon->priv->mixer_stream)); } else { markup = g_strdup_printf ( "%s: %.0f%%\n%s", icon->priv->display_name, 100 * (float)volume / PA_VOLUME_NORM, gvc_mixer_stream_get_description (icon->priv->mixer_stream)); } gtk_status_icon_set_tooltip_markup (GTK_STATUS_ICON (icon), markup); g_free (markup); } void gvc_stream_status_icon_set_icon_names (GvcStreamStatusIcon *icon, const char **names) { g_return_if_fail (GVC_IS_STREAM_STATUS_ICON (icon)); g_strfreev (icon->priv->icon_names); icon->priv->icon_names = g_strdupv ((char **)names); update_icon (icon); g_object_notify (G_OBJECT (icon), "icon-names"); } static void on_stream_volume_notify (GObject *object, GParamSpec *pspec, GvcStreamStatusIcon *icon) { update_icon (icon); update_dock (icon); } static void on_stream_is_muted_notify (GObject *object, GParamSpec *pspec, GvcStreamStatusIcon *icon) { update_icon (icon); update_dock (icon); } void gvc_stream_status_icon_set_display_name (GvcStreamStatusIcon *icon, const char *name) { g_return_if_fail (GVC_STREAM_STATUS_ICON (icon)); g_free (icon->priv->display_name); icon->priv->display_name = g_strdup (name); update_icon (icon); g_object_notify (G_OBJECT (icon), "display-name"); } void gvc_stream_status_icon_set_mixer_stream (GvcStreamStatusIcon *icon, GvcMixerStream *stream) { g_return_if_fail (GVC_STREAM_STATUS_ICON (icon)); if (stream != NULL) { g_object_ref (stream); } if (icon->priv->mixer_stream != NULL) { g_signal_handlers_disconnect_by_func (icon->priv->mixer_stream, G_CALLBACK (on_stream_volume_notify), icon); g_signal_handlers_disconnect_by_func (icon->priv->mixer_stream, G_CALLBACK (on_stream_is_muted_notify), icon); g_object_unref (icon->priv->mixer_stream); icon->priv->mixer_stream = NULL; } icon->priv->mixer_stream = stream; if (icon->priv->mixer_stream != NULL) { GtkAdjustment *adj; g_object_ref (icon->priv->mixer_stream); icon->priv->thaw = TRUE; adj = GTK_ADJUSTMENT (gvc_channel_bar_get_adjustment (GVC_CHANNEL_BAR (icon->priv->bar))); gtk_adjustment_set_value (adj, gvc_mixer_stream_get_volume (icon->priv->mixer_stream)); icon->priv->thaw = FALSE; g_signal_connect (icon->priv->mixer_stream, "notify::volume", G_CALLBACK (on_stream_volume_notify), icon); g_signal_connect (icon->priv->mixer_stream, "notify::is-muted", G_CALLBACK (on_stream_is_muted_notify), icon); } update_icon (icon); g_object_notify (G_OBJECT (icon), "mixer-stream"); } static void gvc_stream_status_icon_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { GvcStreamStatusIcon *self = GVC_STREAM_STATUS_ICON (object); switch (prop_id) { case PROP_MIXER_STREAM: gvc_stream_status_icon_set_mixer_stream (self, g_value_get_object (value)); break; case PROP_DISPLAY_NAME: gvc_stream_status_icon_set_display_name (self, g_value_get_string (value)); break; case PROP_ICON_NAMES: gvc_stream_status_icon_set_icon_names (self, g_value_get_boxed (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void gvc_stream_status_icon_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { GvcStreamStatusIcon *self = GVC_STREAM_STATUS_ICON (object); GvcStreamStatusIconPrivate *priv = self->priv; switch (prop_id) { case PROP_MIXER_STREAM: g_value_set_object (value, priv->mixer_stream); break; case PROP_DISPLAY_NAME: g_value_set_string (value, priv->display_name); break; case PROP_ICON_NAMES: g_value_set_boxed (value, priv->icon_names); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void on_bar_is_muted_notify (GObject *object, GParamSpec *pspec, GvcStreamStatusIcon *icon) { gboolean is_muted; is_muted = gvc_channel_bar_get_is_muted (GVC_CHANNEL_BAR (object)); if (gvc_mixer_stream_get_is_muted (icon->priv->mixer_stream) != is_muted) { /* Update the stream before pushing the change */ gvc_mixer_stream_set_is_muted (icon->priv->mixer_stream, is_muted); gvc_mixer_stream_change_is_muted (icon->priv->mixer_stream, is_muted); } } static GObject * gvc_stream_status_icon_constructor (GType type, guint n_construct_properties, GObjectConstructParam *construct_params) { GObject *object; GvcStreamStatusIcon *icon; GtkWidget *frame; GtkWidget *box; GtkAdjustment *adj; object = G_OBJECT_CLASS (gvc_stream_status_icon_parent_class)->constructor (type, n_construct_properties, construct_params); icon = GVC_STREAM_STATUS_ICON (object); gtk_status_icon_set_from_icon_name (GTK_STATUS_ICON (icon), icon->priv->icon_names[0]); /* window */ icon->priv->dock = gtk_window_new (GTK_WINDOW_POPUP); gtk_widget_set_name (icon->priv->dock, "gvc-stream-status-icon-popup-window"); g_signal_connect (icon->priv->dock, "button-press-event", G_CALLBACK (on_dock_button_press), icon); g_signal_connect (icon->priv->dock, "key-release-event", G_CALLBACK (on_dock_key_release), icon); g_signal_connect (icon->priv->dock, "scroll-event", G_CALLBACK (on_dock_scroll_event), icon); g_signal_connect (icon->priv->dock, "grab-notify", G_CALLBACK (on_dock_grab_notify), icon); g_signal_connect (icon->priv->dock, "grab-broken-event", G_CALLBACK (on_dock_grab_broken_event), icon); gtk_window_set_decorated (GTK_WINDOW (icon->priv->dock), FALSE); frame = gtk_frame_new (NULL); gtk_frame_set_shadow_type (GTK_FRAME (frame), GTK_SHADOW_OUT); gtk_container_add (GTK_CONTAINER (icon->priv->dock), frame); box = gtk_vbox_new (FALSE, 6); gtk_container_set_border_width (GTK_CONTAINER (box), 2); gtk_container_add (GTK_CONTAINER (frame), box); icon->priv->bar = gvc_channel_bar_new (); gvc_channel_bar_set_orientation (GVC_CHANNEL_BAR (icon->priv->bar), GTK_ORIENTATION_VERTICAL); gtk_box_pack_start (GTK_BOX (box), icon->priv->bar, TRUE, FALSE, 0); g_signal_connect (icon->priv->bar, "notify::is-muted", G_CALLBACK (on_bar_is_muted_notify), icon); adj = GTK_ADJUSTMENT (gvc_channel_bar_get_adjustment (GVC_CHANNEL_BAR (icon->priv->bar))); g_signal_connect (adj, "value-changed", G_CALLBACK (on_adjustment_value_changed), icon); return object; } static void gvc_stream_status_icon_dispose (GObject *object) { GvcStreamStatusIcon *icon = GVC_STREAM_STATUS_ICON (object); if (icon->priv->dock != NULL) { gtk_widget_destroy (icon->priv->dock); icon->priv->dock = NULL; } if (icon->priv->mixer_stream != NULL) { g_object_unref (icon->priv->mixer_stream); icon->priv->mixer_stream = NULL; } G_OBJECT_CLASS (gvc_stream_status_icon_parent_class)->dispose (object); } static void gvc_stream_status_icon_class_init (GvcStreamStatusIconClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->constructor = gvc_stream_status_icon_constructor; object_class->finalize = gvc_stream_status_icon_finalize; object_class->dispose = gvc_stream_status_icon_dispose; object_class->set_property = gvc_stream_status_icon_set_property; object_class->get_property = gvc_stream_status_icon_get_property; g_object_class_install_property (object_class, PROP_MIXER_STREAM, g_param_spec_object ("mixer-stream", "mixer stream", "mixer stream", GVC_TYPE_MIXER_STREAM, G_PARAM_READWRITE|G_PARAM_CONSTRUCT)); g_object_class_install_property (object_class, PROP_DISPLAY_NAME, g_param_spec_string ("display-name", "Display Name", "Name to display for this stream", NULL, G_PARAM_READWRITE|G_PARAM_CONSTRUCT)); g_object_class_install_property (object_class, PROP_ICON_NAMES, g_param_spec_boxed ("icon-names", "Icon Names", "Name of icon to display for this stream", G_TYPE_STRV, G_PARAM_READWRITE|G_PARAM_CONSTRUCT)); g_type_class_add_private (klass, sizeof (GvcStreamStatusIconPrivate)); } static void on_status_icon_visible_notify (GvcStreamStatusIcon *icon) { gboolean visible; g_object_get (icon, "visible", &visible, NULL); if (! visible) { if (icon->priv->dock != NULL) { gtk_widget_hide (icon->priv->dock); } } } static void gvc_stream_status_icon_init (GvcStreamStatusIcon *icon) { icon->priv = GVC_STREAM_STATUS_ICON_GET_PRIVATE (icon); g_signal_connect (icon, "activate", G_CALLBACK (on_status_icon_activate), icon); g_signal_connect (icon, "button-press-event", G_CALLBACK (on_status_icon_button_press), icon); g_signal_connect (icon, "popup-menu", G_CALLBACK (on_status_icon_popup_menu), icon); g_signal_connect (icon, "scroll-event", G_CALLBACK (on_status_icon_scroll_event), icon); g_signal_connect (icon, "notify::visible", G_CALLBACK (on_status_icon_visible_notify), NULL); icon->priv->thaw = FALSE; } static void gvc_stream_status_icon_finalize (GObject *object) { GvcStreamStatusIcon *stream_status_icon; g_return_if_fail (object != NULL); g_return_if_fail (GVC_IS_STREAM_STATUS_ICON (object)); stream_status_icon = GVC_STREAM_STATUS_ICON (object); g_return_if_fail (stream_status_icon->priv != NULL); g_strfreev (stream_status_icon->priv->icon_names); G_OBJECT_CLASS (gvc_stream_status_icon_parent_class)->finalize (object); } GvcStreamStatusIcon * gvc_stream_status_icon_new (GvcMixerStream *stream, const char **icon_names) { GObject *icon; icon = g_object_new (GVC_TYPE_STREAM_STATUS_ICON, "mixer-stream", stream, "icon-names", icon_names, NULL); return GVC_STREAM_STATUS_ICON (icon); }