1 /*-
2  * Copyright (c) 2020 - 2021 Rozhuk Ivan <rozhuk.im@gmail.com>
3  * All rights reserved.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions
7  * are met:
8  * 1. Redistributions of source code must retain the above copyright
9  *    notice, this list of conditions and the following disclaimer.
10  * 2. Redistributions in binary form must reproduce the above copyright
11  *    notice, this list of conditions and the following disclaimer in the
12  *    documentation and/or other materials provided with the distribution.
13  *
14  * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
15  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
17  * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
18  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
19  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
20  * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
21  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
22  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
23  * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
24  * SUCH DAMAGE.
25  *
26  * Author: Rozhuk Ivan <rozhuk.im@gmail.com>
27  *
28  */
29 
30 
31 #include <sys/param.h>
32 #include <sys/types.h>
33 #include <err.h>
34 #include <errno.h>
35 #include <inttypes.h>
36 
37 #include "gtk-mixer.h"
38 
39 
40 typedef struct gtk_mixer_app_s {
41 	gm_plugin_p	plugins;
42 	size_t		plugins_count;
43 	gmp_dev_list_t	dev_list;
44 	GtkWidget	*window;
45 	GtkStatusIcon	*status_icon;
46 	GtkWidget	*tray_icon_menu;
47 
48 	gmp_dev_p	dev; /* Current sound device. */
49 
50 	/* GUI update rate scaler. */
51 	size_t update_skip_counter;
52 	size_t update_force_counter;
53 } gm_app_t, *gm_app_p;
54 
55 /* Check updates every 1s if no changes and every 100ms if something was
56  * changes in last 5 second. */
57 #define UPDATE_INTERVAL		100
58 /* If no chenges - check every (UPDATE_INTERVAL * UPDATE_SKIP_MAX_COUNT) ms. */
59 #define UPDATE_SKIP_MAX_COUNT	10
60 /* If was changes - check every UPDATE_INTERVAL in next (UPDATE_INTERVAL * UPDATE_FORCE_MAX_COUNT) ms. */
61 #define UPDATE_FORCE_MAX_COUNT	50
62 
63 
64 static gboolean
gtk_mixer_check_update(gm_app_p app)65 gtk_mixer_check_update(gm_app_p app) {
66 	int error;
67 	size_t changes = 0;
68 	gmp_dev_list_t dev_list;
69 	gmp_dev_p dev = NULL;
70 
71 	/* GUI update rate scaler. */
72 	app->update_skip_counter ++;
73 	if (UPDATE_SKIP_MAX_COUNT > app->update_skip_counter)
74 		return (TRUE);
75 	app->update_skip_counter = 0;
76 
77 	/* Devices list update check. */
78 	if (gmp_is_list_devs_changed(app->plugins, app->plugins_count)) {
79 		changes ++;
80 		memset(&dev_list, 0x00, sizeof(dev_list));
81 		error = gmp_list_devs(app->plugins, app->plugins_count,
82 		    &dev_list);
83 		if (0 == error) {
84 			/* Try to find old current dev in updated dev list. */
85 			dev = gmp_dev_find_same(&dev_list, app->dev);
86 			gtk_mixer_window_dev_list_update(app->window,
87 			    &dev_list);
88 			gmp_dev_list_clear(&app->dev_list);
89 			app->dev_list = dev_list;
90 			/* Select new current device. */
91 			if (NULL == dev) {
92 				dev = gmp_dev_list_get_default(&app->dev_list);
93 			}
94 			gtk_mixer_window_dev_cur_set(app->window, dev);
95 		}
96 	} else if (0 != gmp_is_def_dev_changed(app->plugins,
97 	    app->plugins_count)) { /* Default device changed. */
98 		changes ++;
99 		gtk_mixer_window_dev_list_update(app->window, NULL);
100 	}
101 
102 	/* Check lines update for current device. */
103 	if (NULL != app->dev) {
104 		error = gmp_dev_read(app->dev, 1);
105 		if (0 == error &&
106 		    gmp_dev_is_updated(app->dev)) {
107 			/* GUI update. */
108 			gtk_mixer_window_lines_update(app->window);
109 			gtk_mixer_tray_icon_update(app->status_icon);
110 			changes += gmp_dev_is_updated_clear(app->dev);
111 		}
112 	}
113 
114 	/* GUI update rate scaler. */
115 	/* If something changed than force check updates on next timer fire. */
116 	if (0 != changes) {
117 		app->update_force_counter = UPDATE_FORCE_MAX_COUNT;
118 	}
119 	if (0 != app->update_force_counter) {
120 		app->update_force_counter --;
121 		app->update_skip_counter = UPDATE_SKIP_MAX_COUNT;
122 	}
123 
124 	return (TRUE);
125 }
126 
127 static void
gtk_mixer_soundcard_changed(GtkWidget * combo __unused,gpointer user_data)128 gtk_mixer_soundcard_changed(GtkWidget *combo __unused,
129     gpointer user_data) {
130 	gm_app_p app = user_data;
131 
132 	if (NULL == app)
133 		return;
134 
135 	app->dev = gtk_mixer_window_dev_cur_get(app->window);
136 
137 	/* Tray icon.*/
138 	gtk_mixer_tray_icon_dev_set(app->status_icon, app->dev);
139 	gtk_mixer_tray_icon_update(app->status_icon);
140 }
141 
142 static void
gtk_mixer_status_icon_activate(GtkStatusIcon * status_icon __unused,gpointer user_data)143 gtk_mixer_status_icon_activate(GtkStatusIcon *status_icon __unused,
144     gpointer user_data) {
145 	gm_app_p app = user_data;
146 
147 	if (NULL == app)
148 		return;
149 
150 	if (gtk_widget_get_visible(app->window)) {
151 		gtk_widget_hide(app->window);
152 	} else {
153 		gtk_widget_show(app->window);
154 	}
155 }
156 
157 static void
on_tray_icon_menu_about_click(GtkMenuItem * menuitem __unused,gpointer user_data __unused)158 on_tray_icon_menu_about_click(GtkMenuItem *menuitem __unused,
159     gpointer user_data __unused) {
160 	GtkAboutDialog *dlg = GTK_ABOUT_DIALOG(gtk_about_dialog_new());
161 	const char *authors[] = {
162 		"",
163 		"2020-2021 Rozhuk Ivan",
164 		"",
165 		"Original xfce4-mixer",
166 		"2012 Guido Berhoerster",
167 		"2008 Jannis Pohlmann",
168 		"and others...",
169 		NULL
170 	};
171 
172 	gtk_about_dialog_set_program_name(dlg, "GTK-Mixer");
173 	gtk_about_dialog_set_version(dlg, VERSION);
174 	gtk_about_dialog_set_copyright(dlg,
175 	    "Copyright (c) 2020-2021 Rozhuk Ivan <rozhuk.im@gmail.com>");
176 	gtk_about_dialog_set_comments(dlg, PACKAGE_DESCRIPTION);
177 	gtk_about_dialog_set_license_type(dlg, GTK_LICENSE_GPL_2_0);
178 	gtk_about_dialog_set_website(dlg, PACKAGE_URL);
179 	gtk_about_dialog_set_website_label(dlg, "github.com");
180 	gtk_about_dialog_set_authors(dlg, authors);
181 	gtk_about_dialog_set_translator_credits(dlg, _("translator-credits"));
182 	gtk_about_dialog_set_logo_icon_name(dlg, APP_ICON_NAME);
183 	gtk_dialog_run(GTK_DIALOG(dlg));
184 	gtk_widget_destroy(GTK_WIDGET(dlg));
185 }
186 static void
gtk_mixer_status_icon_menu(GtkStatusIcon * status_icon __unused,guint button,guint activate_time __unused,gpointer user_data)187 gtk_mixer_status_icon_menu(GtkStatusIcon *status_icon __unused,
188     guint button, guint activate_time __unused, gpointer user_data) {
189 	gm_app_p app = user_data;
190 
191 	if (NULL == app ||
192 	    3 != button)
193 		return;
194 	if (NULL == app->tray_icon_menu) {
195 		GtkWidget *mi;
196 		app->tray_icon_menu = gtk_menu_new();
197 
198 		/* About. */
199 		G_GNUC_BEGIN_IGNORE_DEPRECATIONS
200 		mi = gtk_image_menu_item_new_from_stock("gtk-about", NULL);
201 		G_GNUC_END_IGNORE_DEPRECATIONS
202 		g_signal_connect(G_OBJECT(mi), "activate",
203 		    G_CALLBACK(on_tray_icon_menu_about_click), app);
204 		gtk_menu_shell_append(GTK_MENU_SHELL(app->tray_icon_menu),
205 		    mi);
206 		/* Separator. */
207 		gtk_menu_shell_append(GTK_MENU_SHELL(app->tray_icon_menu),
208 		    gtk_separator_menu_item_new());
209 		/* Quit. */
210 		G_GNUC_BEGIN_IGNORE_DEPRECATIONS
211 		mi  = gtk_image_menu_item_new_from_stock("gtk-quit", NULL);
212 		G_GNUC_END_IGNORE_DEPRECATIONS
213 		g_signal_connect(G_OBJECT(mi), "activate",
214 		    G_CALLBACK(gtk_main_quit), NULL);
215 		gtk_menu_shell_append(GTK_MENU_SHELL(app->tray_icon_menu),
216 		    mi);
217 
218 		gtk_widget_show_all(GTK_WIDGET(app->tray_icon_menu));
219 	}
220 	gtk_menu_popup_at_pointer(GTK_MENU(app->tray_icon_menu), NULL);
221 }
222 
223 
224 int
main(int argc,char ** argv)225 main(int argc, char **argv) {
226 	int error;
227 	gm_app_t app;
228 	gmp_dev_p dev = NULL;
229 
230 	memset(&app, 0x00, sizeof(gm_app_t));
231 
232 	error = gmp_init(&app.plugins, &app.plugins_count);
233 	if (0 != error)
234 		return (error);
235 	error = gmp_list_devs(app.plugins, app.plugins_count,
236 	    &app.dev_list);
237 	if (0 != error)
238 		return (error);
239 
240 	gtk_init(&argc, &argv);
241 
242 	/* Set application name. */
243 	g_set_application_name(_("Audio Mixer"));
244 
245 	/* Use volume control icon for all mixer windows. */
246 	gtk_window_set_default_icon_name(APP_ICON_NAME);
247 
248 	/* Main window. */
249 	app.window = gtk_mixer_window_create();
250 	gtk_mixer_window_dev_list_update(app.window, &app.dev_list);
251 #if 0
252 	if (card_name != NULL) {
253 		dev = gtk_mixer_get_card(card_name);
254 	} else {
255 		dev = gtk_mixer_get_default_card();
256 		g_object_set(gm_win->preferences, "sound-card",
257 		    gtk_mixer_get_card_internal_name(dev), NULL);
258 	}
259 	g_free(card_name);
260 #endif
261 	if (NULL == dev) {
262 		dev = gmp_dev_list_get_default(&app.dev_list);
263 	}
264 	gtk_mixer_window_dev_cur_set(app.window, dev);
265 
266 
267 	/* Tray icon. */
268 	app.status_icon = gtk_mixer_tray_icon_create();
269 	g_signal_connect(app.status_icon, "activate",
270 	    G_CALLBACK(gtk_mixer_status_icon_activate), &app);
271 	g_signal_connect(app.status_icon, "popup-menu",
272 	    G_CALLBACK(gtk_mixer_status_icon_menu), &app);
273 
274 	/* Allow monitor selected sound dev. */
275 	gtk_mixer_window_connect_dev_changed(app.window,
276 	    G_CALLBACK(gtk_mixer_soundcard_changed), &app);
277 	/* Force set sound dev. */
278 	gtk_mixer_soundcard_changed(NULL, &app);
279 
280 	/* Display the mixer window. */
281 	gtk_window_present(GTK_WINDOW(app.window));
282 
283 	/* For update, if volume changed from other app. */
284 	g_timeout_add(UPDATE_INTERVAL,
285 	    (GSourceFunc)gtk_mixer_check_update, &app);
286 
287 	gtk_main();
288 
289 	/* Cleanup. */
290 	gmp_dev_list_clear(&app.dev_list);
291 	gmp_uninit(app.plugins, app.plugins_count);
292 
293 	return (error);
294 }
295 
296 const char *
volume_stock_from_level(const int is_mic,const int is_enabled,const int level,const char * cur_icon_name)297 volume_stock_from_level(const int is_mic, const int is_enabled,
298     const int level, const char *cur_icon_name) {
299 	const int levels[] = { -1, 0, 33, 66, 100 };
300 	const char *volume_level[nitems(levels)] = {
301 	    "audio-volume-muted",
302 	    "audio-volume-muted",
303 	    "audio-volume-low",
304 	    "audio-volume-medium",
305 	    "audio-volume-high"
306 	};
307 	const char *mic_sens_level[nitems(levels)] = {
308 	    "microphone-disabled-symbolic",
309 	    "microphone-sensitivity-muted",
310 	    "microphone-sensitivity-low",
311 	    "microphone-sensitivity-medium",
312 	    "microphone-sensitivity-high"
313 	};
314 	const char **stocks = ((0 != is_mic) ? mic_sens_level : volume_level);
315 
316 	for (size_t i = 0; i < nitems(levels); i ++) {
317 		if (levels[i] < level && 0 != is_enabled)
318 			continue;
319 		if (NULL != cur_icon_name &&
320 		    strcmp(cur_icon_name, stocks[i]) == 0)
321 			break; /* No need to update. */
322 		return (stocks[i]);
323 	}
324 
325 	return (NULL);
326 }
327