libgnome-volume-control/gvc-mixer-ui-device.c
Bastien Nocera 20b130c17f mixer-ui-device: Make debug for port available useful
The debug output would print the same value twice, instead of showing
the before and after values when setting the property.
2024-02-04 00:22:37 +01:00

749 lines
27 KiB
C

/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */
/*
* gvc-mixer-ui-device.c
* Copyright (C) Conor Curran 2011 <conor.curran@canonical.com>
* Copyright (C) 2012 David Henningsson, Canonical Ltd. <david.henningsson@canonical.com>
*
* gvc-mixer-ui-device.c 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.
*
* gvc-mixer-ui-device.c 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 <http://www.gnu.org/licenses/>.
*/
#include <string.h>
#include "gvc-mixer-ui-device.h"
#include "gvc-mixer-card.h"
struct GvcMixerUIDevicePrivate
{
gchar *first_line_desc;
gchar *second_line_desc;
GvcMixerCard *card;
gchar *port_name;
char *icon_name;
guint stream_id;
guint id;
gboolean port_available;
/* These two lists contain pointers to GvcMixerCardProfile objects. Those objects are owned by GvcMixerCard. *
* TODO: Do we want to add a weak reference to the GvcMixerCard for this reason? */
GList *supported_profiles; /* all profiles supported by this port.*/
GList *profiles; /* profiles to be added to combobox, subset of supported_profiles. */
GvcMixerUIDeviceDirection type;
gboolean disable_profile_swapping;
gchar *user_preferred_profile;
};
enum
{
PROP_0,
PROP_DESC_LINE_1,
PROP_DESC_LINE_2,
PROP_CARD,
PROP_PORT_NAME,
PROP_STREAM_ID,
PROP_UI_DEVICE_TYPE,
PROP_PORT_AVAILABLE,
PROP_ICON_NAME,
N_PROPS
};
static GParamSpec *obj_props[N_PROPS] = { NULL, };
static void gvc_mixer_ui_device_finalize (GObject *object);
static void gvc_mixer_ui_device_set_icon_name (GvcMixerUIDevice *device,
const char *icon_name);
G_DEFINE_TYPE_WITH_PRIVATE (GvcMixerUIDevice, gvc_mixer_ui_device, G_TYPE_OBJECT)
static guint32
get_next_output_serial (void)
{
static guint32 output_serial = 1;
guint32 serial;
serial = output_serial++;
if ((gint32)output_serial < 0)
output_serial = 1;
return serial;
}
static void
gvc_mixer_ui_device_get_property (GObject *object,
guint property_id,
GValue *value,
GParamSpec *pspec)
{
GvcMixerUIDevice *self = GVC_MIXER_UI_DEVICE (object);
switch (property_id) {
case PROP_DESC_LINE_1:
g_value_set_string (value, self->priv->first_line_desc);
break;
case PROP_DESC_LINE_2:
g_value_set_string (value, self->priv->second_line_desc);
break;
case PROP_CARD:
g_value_set_pointer (value, self->priv->card);
break;
case PROP_PORT_NAME:
g_value_set_string (value, self->priv->port_name);
break;
case PROP_STREAM_ID:
g_value_set_uint (value, self->priv->stream_id);
break;
case PROP_UI_DEVICE_TYPE:
g_value_set_uint (value, (guint)self->priv->type);
break;
case PROP_PORT_AVAILABLE:
g_value_set_boolean (value, self->priv->port_available);
break;
case PROP_ICON_NAME:
g_value_set_string (value, gvc_mixer_ui_device_get_icon_name (self));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
static void
gvc_mixer_ui_device_set_property (GObject *object,
guint property_id,
const GValue *value,
GParamSpec *pspec)
{
GvcMixerUIDevice *self = GVC_MIXER_UI_DEVICE (object);
switch (property_id) {
case PROP_DESC_LINE_1:
g_free (self->priv->first_line_desc);
self->priv->first_line_desc = g_value_dup_string (value);
g_debug ("gvc-mixer-output-set-property - 1st line: %s",
self->priv->first_line_desc);
break;
case PROP_DESC_LINE_2:
g_free (self->priv->second_line_desc);
self->priv->second_line_desc = g_value_dup_string (value);
g_debug ("gvc-mixer-output-set-property - 2nd line: %s",
self->priv->second_line_desc);
break;
case PROP_CARD:
self->priv->card = g_value_get_pointer (value);
g_debug ("gvc-mixer-output-set-property - card: %p",
self->priv->card);
break;
case PROP_PORT_NAME:
g_free (self->priv->port_name);
self->priv->port_name = g_value_dup_string (value);
g_debug ("gvc-mixer-output-set-property - card port name: %s",
self->priv->port_name);
break;
case PROP_STREAM_ID:
self->priv->stream_id = g_value_get_uint (value);
g_debug ("gvc-mixer-output-set-property - sink/source id: %i",
self->priv->stream_id);
break;
case PROP_UI_DEVICE_TYPE:
self->priv->type = (GvcMixerUIDeviceDirection) g_value_get_uint (value);
g_debug ("gvc-mixer-output-set-property - device type: %s",
self->priv->type == UIDeviceInput ? "input" : "output");
break;
case PROP_PORT_AVAILABLE:
g_debug ("gvc-mixer-output-set-property - old port available %i, value passed in %i",
self->priv->port_available, g_value_get_boolean (value));
self->priv->port_available = g_value_get_boolean (value);
break;
case PROP_ICON_NAME:
gvc_mixer_ui_device_set_icon_name (self, g_value_get_string (value));
g_debug ("gvc-mixer-output-set-property - icon name: %s",
self->priv->icon_name);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
static GObject *
gvc_mixer_ui_device_constructor (GType type,
guint n_construct_properties,
GObjectConstructParam *construct_params)
{
GObject *object;
GvcMixerUIDevice *self;
object = G_OBJECT_CLASS (gvc_mixer_ui_device_parent_class)->constructor (type, n_construct_properties, construct_params);
self = GVC_MIXER_UI_DEVICE (object);
self->priv->id = get_next_output_serial ();
self->priv->stream_id = GVC_MIXER_UI_DEVICE_INVALID;
return object;
}
static void
gvc_mixer_ui_device_init (GvcMixerUIDevice *device)
{
device->priv = gvc_mixer_ui_device_get_instance_private (device);
}
static void
gvc_mixer_ui_device_dispose (GObject *object)
{
GvcMixerUIDevice *device;
g_return_if_fail (object != NULL);
g_return_if_fail (GVC_MIXER_UI_DEVICE (object));
device = GVC_MIXER_UI_DEVICE (object);
g_clear_pointer (&device->priv->port_name, g_free);
g_clear_pointer (&device->priv->icon_name, g_free);
g_clear_pointer (&device->priv->first_line_desc, g_free);
g_clear_pointer (&device->priv->second_line_desc, g_free);
g_clear_pointer (&device->priv->profiles, g_list_free);
g_clear_pointer (&device->priv->supported_profiles, g_list_free);
g_clear_pointer (&device->priv->user_preferred_profile, g_free);
G_OBJECT_CLASS (gvc_mixer_ui_device_parent_class)->dispose (object);
}
static void
gvc_mixer_ui_device_finalize (GObject *object)
{
G_OBJECT_CLASS (gvc_mixer_ui_device_parent_class)->finalize (object);
}
static void
gvc_mixer_ui_device_class_init (GvcMixerUIDeviceClass *klass)
{
GObjectClass* object_class = G_OBJECT_CLASS (klass);
object_class->constructor = gvc_mixer_ui_device_constructor;
object_class->dispose = gvc_mixer_ui_device_dispose;
object_class->finalize = gvc_mixer_ui_device_finalize;
object_class->set_property = gvc_mixer_ui_device_set_property;
object_class->get_property = gvc_mixer_ui_device_get_property;
obj_props[PROP_DESC_LINE_1] =
g_param_spec_string ("description",
"Description construct prop",
"Set first line description",
"no-name-set",
G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS);
obj_props[PROP_DESC_LINE_2] =
g_param_spec_string ("origin",
"origin construct prop",
"Set second line description name",
"no-name-set",
G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS);
obj_props[PROP_CARD] =
g_param_spec_pointer ("card",
"Card from pulse",
"Set/Get card",
G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS);
obj_props[PROP_PORT_NAME] =
g_param_spec_string ("port-name",
"port-name construct prop",
"Set port-name",
NULL,
G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS);
obj_props[PROP_STREAM_ID] =
g_param_spec_uint ("stream-id",
"stream id assigned by gvc-stream",
"Set/Get stream id",
0,
G_MAXUINT,
GVC_MIXER_UI_DEVICE_INVALID,
G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS);
obj_props[PROP_UI_DEVICE_TYPE] =
g_param_spec_uint ("type",
"ui-device type",
"determine whether its an input and output",
0, 1, 0,
G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS);
obj_props[PROP_PORT_AVAILABLE] =
g_param_spec_boolean ("port-available",
"available",
"determine whether this port is available",
FALSE,
G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS);
obj_props[PROP_ICON_NAME] =
g_param_spec_string ("icon-name",
"Icon Name",
"Name of icon to display for this card",
NULL,
G_PARAM_READWRITE|G_PARAM_CONSTRUCT|G_PARAM_STATIC_STRINGS);
g_object_class_install_properties (object_class, N_PROPS, obj_props);
}
/* Removes the part of the string that starts with skip_prefix
* ie. corresponding to the other direction.
* Normally either "input:" or "output:"
*
* Example: if given the input string "output:hdmi-stereo+input:analog-stereo" and
* skip_prefix "input:", the resulting string is "output:hdmi-stereo".
*
* The returned string must be freed with g_free().
*/
static gchar *
get_profile_canonical_name (const gchar *profile_name, const gchar *skip_prefix)
{
gchar *result = NULL;
gchar **s;
guint i;
/* optimisation for the simple case. */
if (strstr (profile_name, skip_prefix) == NULL)
return g_strdup (profile_name);
s = g_strsplit (profile_name, "+", 0);
for (i = 0; i < g_strv_length (s); i++) {
if (g_str_has_prefix (s[i], skip_prefix))
continue;
if (result == NULL)
result = g_strdup (s[i]);
else {
gchar *c = g_strdup_printf("%s+%s", result, s[i]);
g_free(result);
result = c;
}
}
g_strfreev(s);
if (!result)
return g_strdup("off");
return result;
}
const gchar *
gvc_mixer_ui_device_get_matching_profile (GvcMixerUIDevice *device, const gchar *profile)
{
const gchar *skip_prefix = device->priv->type == UIDeviceInput ? "output:" : "input:";
gchar *target_cname = get_profile_canonical_name (profile, skip_prefix);
GList *l;
gchar *result = NULL;
g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL);
g_return_val_if_fail (profile != NULL, NULL);
for (l = device->priv->profiles; l != NULL; l = l->next) {
gchar *canonical_name;
GvcMixerCardProfile* p = l->data;
canonical_name = get_profile_canonical_name (p->profile, skip_prefix);
if (strcmp (canonical_name, target_cname) == 0)
result = p->profile;
g_free (canonical_name);
}
g_free (target_cname);
g_debug ("Matching profile for '%s' is '%s'", profile, result ? result : "(null)");
return result;
}
static void
add_canonical_names_of_profiles (GvcMixerUIDevice *device,
const GList *in_profiles,
GHashTable *added_profiles,
const gchar *skip_prefix,
gboolean only_canonical)
{
const GList *l;
for (l = in_profiles; l != NULL; l = l->next) {
gchar *canonical_name;
GvcMixerCardProfile* p = l->data;
canonical_name = get_profile_canonical_name (p->profile, skip_prefix);
g_debug ("The canonical name for '%s' is '%s'", p->profile, canonical_name);
/* Have we already added the canonical version of this profile? */
if (g_hash_table_contains (added_profiles, canonical_name)) {
g_free (canonical_name);
continue;
}
if (only_canonical && strcmp (p->profile, canonical_name) != 0) {
g_free (canonical_name);
continue;
}
g_free (canonical_name);
/* https://bugzilla.gnome.org/show_bug.cgi?id=693654
* Don't add a profile that will make the UI device completely disappear */
if (p->n_sinks == 0 && p->n_sources == 0)
continue;
g_debug ("Adding profile to combobox: '%s' - '%s'", p->profile, p->human_profile);
g_hash_table_insert (added_profiles, g_strdup (p->profile), p);
device->priv->profiles = g_list_append (device->priv->profiles, p);
}
}
/**
* gvc_mixer_ui_device_set_profiles:
* @in_profiles: (element-type Gvc.MixerCardProfile): a list of GvcMixerCardProfile
*
* Assigns value to
* - device->priv->profiles (profiles to be added to combobox)
* - device->priv->supported_profiles (all profiles of this port)
* - device->priv->disable_profile_swapping (whether to show the combobox)
*
* This method attempts to reduce the list of profiles visible to the user by figuring out
* from the context of that device (whether it's an input or an output) what profiles
* actually provide an alternative.
*
* It does this by the following.
* - It ignores off profiles.
* - It takes the canonical name of the profile. That name is what you get when you
* ignore the other direction.
* - In the first iteration, it only adds the names of canonical profiles - i e
* when the other side is turned off.
* - Normally the first iteration covers all cases, but sometimes (e g bluetooth)
* it doesn't, so add other profiles whose canonical name isn't already added
* in a second iteration.
*/
void
gvc_mixer_ui_device_set_profiles (GvcMixerUIDevice *device,
const GList *in_profiles)
{
GHashTable *added_profiles;
const gchar *skip_prefix = device->priv->type == UIDeviceInput ? "output:" : "input:";
g_return_if_fail (GVC_IS_MIXER_UI_DEVICE (device));
g_debug ("Set profiles for '%s'", gvc_mixer_ui_device_get_description(device));
if (in_profiles == NULL)
return;
device->priv->supported_profiles = g_list_copy ((GList*) in_profiles);
added_profiles = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
/* Run two iterations: First, add profiles which are canonical themselves,
* Second, add profiles for which the canonical name is not added already. */
add_canonical_names_of_profiles(device, in_profiles, added_profiles, skip_prefix, TRUE);
add_canonical_names_of_profiles(device, in_profiles, added_profiles, skip_prefix, FALSE);
/* TODO: Consider adding the "Off" profile here */
device->priv->disable_profile_swapping = g_hash_table_size (added_profiles) <= 1;
g_hash_table_destroy (added_profiles);
}
/**
* gvc_mixer_ui_device_get_best_profile:
* @selected: (allow-none): The selected profile or its canonical name or %NULL for any profile
* @current: The currently selected profile
*
* Returns: (transfer none): a profile name, valid as long as the UI device profiles are.
*/
const gchar *
gvc_mixer_ui_device_get_best_profile (GvcMixerUIDevice *device,
const gchar *selected,
const gchar *current)
{
GList *candidates, *l;
const gchar *result;
const gchar *skip_prefix;
gchar *canonical_name_selected;
g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL);
g_return_val_if_fail (current != NULL, NULL);
if (device->priv->type == UIDeviceInput)
skip_prefix = "output:";
else
skip_prefix = "input:";
/* First make a list of profiles acceptable to switch to */
canonical_name_selected = NULL;
if (selected)
canonical_name_selected = get_profile_canonical_name (selected, skip_prefix);
candidates = NULL;
for (l = device->priv->supported_profiles; l != NULL; l = l->next) {
gchar *canonical_name;
GvcMixerCardProfile* p = l->data;
canonical_name = get_profile_canonical_name (p->profile, skip_prefix);
if (!canonical_name_selected || strcmp (canonical_name, canonical_name_selected) == 0) {
candidates = g_list_append (candidates, p);
g_debug ("Candidate for profile switching: '%s'", p->profile);
}
g_free (canonical_name);
}
if (!candidates) {
g_warning ("No suitable profile candidates for '%s'", selected ? selected : "(null)");
g_free (canonical_name_selected);
return current;
}
/* 1) Maybe we can skip profile switching altogether? */
result = NULL;
for (l = candidates; (result == NULL) && (l != NULL); l = l->next) {
GvcMixerCardProfile* p = l->data;
if (strcmp (current, p->profile) == 0)
result = p->profile;
}
/* 2) Try to keep the other side unchanged if possible */
if (result == NULL) {
guint prio = 0;
const gchar *skip_prefix_reverse = device->priv->type == UIDeviceInput ? "input:" : "output:";
gchar *current_reverse = get_profile_canonical_name (current, skip_prefix_reverse);
for (l = candidates; l != NULL; l = l->next) {
gchar *p_reverse;
GvcMixerCardProfile* p = l->data;
p_reverse = get_profile_canonical_name (p->profile, skip_prefix_reverse);
g_debug ("Comparing '%s' (from '%s') with '%s', prio %d", p_reverse, p->profile, current_reverse, p->priority);
if (strcmp (p_reverse, current_reverse) == 0 && (!result || p->priority > prio)) {
result = p->profile;
prio = p->priority;
}
g_free (p_reverse);
}
g_free (current_reverse);
}
/* 3) All right, let's just pick the profile with highest priority.
* TODO: We could consider asking a GUI question if this stops streams
* in the other direction */
if (result == NULL) {
guint prio = 0;
for (l = candidates; l != NULL; l = l->next) {
GvcMixerCardProfile* p = l->data;
if ((p->priority > prio) || !result) {
result = p->profile;
prio = p->priority;
}
}
}
g_list_free (candidates);
g_free (canonical_name_selected);
return result;
}
const gchar *
gvc_mixer_ui_device_get_active_profile (GvcMixerUIDevice* device)
{
GvcMixerCardProfile *profile;
g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL);
if (device->priv->card == NULL) {
g_warning ("Device did not have an appropriate card");
return NULL;
}
profile = gvc_mixer_card_get_profile (device->priv->card);
return gvc_mixer_ui_device_get_matching_profile (device, profile->profile);
}
gboolean
gvc_mixer_ui_device_should_profiles_be_hidden (GvcMixerUIDevice *device)
{
g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), FALSE);
return device->priv->disable_profile_swapping;
}
/**
* gvc_mixer_ui_device_get_profiles:
* @device:
*
* Returns: (transfer none) (element-type Gvc.MixerCardProfile):
*/
GList*
gvc_mixer_ui_device_get_profiles (GvcMixerUIDevice *device)
{
g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL);
return device->priv->profiles;
}
/**
* gvc_mixer_ui_device_get_supported_profiles:
* @device:
*
* Returns: (transfer none) (element-type Gvc.MixerCardProfile):
*/
GList*
gvc_mixer_ui_device_get_supported_profiles (GvcMixerUIDevice *device)
{
g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL);
return device->priv->supported_profiles;
}
guint
gvc_mixer_ui_device_get_id (GvcMixerUIDevice *device)
{
g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), 0);
return device->priv->id;
}
guint
gvc_mixer_ui_device_get_stream_id (GvcMixerUIDevice *device)
{
g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), 0);
return device->priv->stream_id;
}
void
gvc_mixer_ui_device_invalidate_stream (GvcMixerUIDevice *self)
{
g_return_if_fail (GVC_IS_MIXER_UI_DEVICE (self));
self->priv->stream_id = GVC_MIXER_UI_DEVICE_INVALID;
}
const gchar *
gvc_mixer_ui_device_get_description (GvcMixerUIDevice *device)
{
g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL);
return device->priv->first_line_desc;
}
const char *
gvc_mixer_ui_device_get_icon_name (GvcMixerUIDevice *device)
{
g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL);
if (device->priv->icon_name)
return device->priv->icon_name;
if (device->priv->card)
return gvc_mixer_card_get_icon_name (device->priv->card);
return NULL;
}
static void
gvc_mixer_ui_device_set_icon_name (GvcMixerUIDevice *device,
const char *icon_name)
{
g_return_if_fail (GVC_IS_MIXER_UI_DEVICE (device));
g_free (device->priv->icon_name);
device->priv->icon_name = g_strdup (icon_name);
g_object_notify_by_pspec (G_OBJECT (device), obj_props[PROP_ICON_NAME]);
}
/**
* gvc_mixer_ui_device_get_gicon:
* @device:
*
* Returns: (transfer full):
*/
GIcon *
gvc_mixer_ui_device_get_gicon (GvcMixerUIDevice *device)
{
const char *icon_name;
g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL);
icon_name = gvc_mixer_ui_device_get_icon_name (device);
if (icon_name != NULL)
return g_themed_icon_new_with_default_fallbacks (icon_name);
else
return NULL;
}
const gchar *
gvc_mixer_ui_device_get_origin (GvcMixerUIDevice *device)
{
g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL);
return device->priv->second_line_desc;
}
const gchar*
gvc_mixer_ui_device_get_user_preferred_profile (GvcMixerUIDevice *device)
{
g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL);
return device->priv->user_preferred_profile;
}
const gchar *
gvc_mixer_ui_device_get_top_priority_profile (GvcMixerUIDevice *device)
{
GList *last;
GvcMixerCardProfile *profile;
g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL);
last = g_list_last (device->priv->supported_profiles);
profile = last->data;
return profile->profile;
}
void
gvc_mixer_ui_device_set_user_preferred_profile (GvcMixerUIDevice *device,
const gchar *profile)
{
g_return_if_fail (GVC_IS_MIXER_UI_DEVICE (device));
g_return_if_fail (profile != NULL);
g_free (device->priv->user_preferred_profile);
device->priv->user_preferred_profile = g_strdup (profile);
}
const gchar *
gvc_mixer_ui_device_get_port (GvcMixerUIDevice *device)
{
g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL);
return device->priv->port_name;
}
gboolean
gvc_mixer_ui_device_has_ports (GvcMixerUIDevice *device)
{
g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), FALSE);
return (device->priv->port_name != NULL);
}
gboolean
gvc_mixer_ui_device_is_output (GvcMixerUIDevice *device)
{
g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), FALSE);
return (device->priv->type == UIDeviceOutput);
}