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