1 /*
2  * alarm-applet.c -- Alarm Clock applet bootstrap
3  *
4  * Copyright (C) 2007-2008 Johannes H. Jensen <joh@pseudoberries.com>
5  *
6  * This program is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU General Public License
8  * as published by the Free Software Foundation; either version 2
9  * of the License, or (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program; if not, write to the Free Software
18  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19  *
20  * Authors:
21  * 		Johannes H. Jensen <joh@pseudoberries.com>
22  */
23 
24 #include <stdlib.h>
25 
26 #include "alarm-applet.h"
27 
28 #include "alarm.h"
29 #include "alarm-settings.h"
30 
31 /*
32  * DEFINTIIONS {{
33  */
34 
35 static const gchar *supported_sound_mime_types [] = {
36     "audio",
37     "video",
38     "application/ogg",
39     NULL
40 };
41 
42 GHashTable *app_command_map = NULL;
43 
44 /*
45  * }} DEFINTIIONS
46  */
47 
48 /*
49  * Snooze any triggered alarms.
50  *
51  * Returns the number of snoozed alarms
52  */
53 guint
alarm_applet_alarms_snooze(AlarmApplet * applet)54 alarm_applet_alarms_snooze (AlarmApplet *applet)
55 {
56 	GList *l;
57 	Alarm *a;
58     guint n_snoozed = 0;
59 
60 	g_debug ("Snoozing alarms...");
61 
62 	// Loop through alarms and snooze all triggered ones
63 	for (l = applet->alarms; l; l = l->next) {
64 		a = ALARM (l->data);
65 
66         if (a->triggered) {
67             alarm_applet_alarm_snooze (applet, a);
68             n_snoozed++;
69         }
70 	}
71 
72     // Reset the triggered counter
73     applet->n_triggered = 0;
74 
75     // Update status icon
76     alarm_applet_status_update (applet);
77 
78     return n_snoozed;
79 }
80 
81 /*
82  * Stop any running (read: playing sound) alarms.
83  */
84 guint
alarm_applet_alarms_stop(AlarmApplet * applet)85 alarm_applet_alarms_stop (AlarmApplet *applet)
86 {
87 	GList *l;
88 	Alarm *a;
89     guint n_stopped = 0;
90 
91 	g_debug ("Stopping alarms...");
92 
93 	// Loop through alarms and clear all of 'em
94 	for (l = applet->alarms; l; l = l->next) {
95 		a = ALARM (l->data);
96 
97 		if (a->triggered) {
98     		alarm_clear (a);
99             n_stopped++;
100         }
101 	}
102 
103     // Reset the triggered counter
104     applet->n_triggered = 0;
105 
106     // Update status icon
107     alarm_applet_status_update (applet);
108 
109     return n_stopped;
110 }
111 
112 /**
113  * Snooze an alarm, according to UI settings
114  */
115 void
alarm_applet_alarm_snooze(AlarmApplet * applet,Alarm * alarm)116 alarm_applet_alarm_snooze (AlarmApplet *applet, Alarm *alarm)
117 {
118     guint mins = applet->snooze_mins;
119 
120     if (alarm->type == ALARM_TYPE_CLOCK) {
121         // Clocks always snooze for 9 minutes
122         mins = ALARM_STD_SNOOZE;
123     }
124 
125     g_debug ("AlarmApplet: snooze '%s' for %d minutes", alarm->message, mins);
126 
127     alarm_snooze (alarm, mins * 60);
128 
129     // Update status icon
130     alarm_applet_status_update (applet);
131 }
132 
133 /**
134  * Stop an alarm, keeping UI consistent
135  */
136 void
alarm_applet_alarm_stop(AlarmApplet * applet,Alarm * alarm)137 alarm_applet_alarm_stop (AlarmApplet *applet, Alarm *alarm)
138 {
139     g_debug ("Stopping alarm #%d...", alarm->id);
140 
141     // Stop the alarm
142     alarm_clear (alarm);
143 
144     // Update status icon
145     alarm_applet_status_update (applet);
146 }
147 
148 
149 
150 /*
151  * Sounds list {{
152  */
153 
154 // Load sounds into list
155 // TODO: Refactor to use a GHashTable with string hash
156 void
alarm_applet_sounds_load(AlarmApplet * applet)157 alarm_applet_sounds_load (AlarmApplet *applet)
158 {
159 	Alarm *alarm;
160 	AlarmListEntry *entry;
161 	GList *l, *l2;
162 	gboolean found;
163 
164 	const gchar* const *sysdirs;
165 	gchar *sounds_dir = NULL;
166 	gchar *tmp;
167 	gint i;
168 
169 	//g_assert (applet->alarms);
170 
171 	// Free old list
172 	if (applet->sounds != NULL)
173 		alarm_list_entry_list_free (&(applet->sounds));
174 
175 	// Locate gnome sounds
176 	sysdirs = g_get_system_data_dirs ();
177 	for (i = 0; !sounds_dir && sysdirs[i] != NULL; i++) {
178 		tmp = g_build_filename(sysdirs[i], "sounds/gnome/default/alerts", NULL);
179 		if (g_file_test (tmp, G_FILE_TEST_IS_DIR)) {
180 			// Load stock sounds
181 			g_debug ("AlarmApplet: sounds_load: Found %s!", tmp);
182 			sounds_dir = g_strdup_printf ("file://%s", tmp);
183 			applet->sounds = alarm_list_entry_list_new (sounds_dir, supported_sound_mime_types);
184 			g_free (sounds_dir);
185 		}
186 		g_free(tmp);
187 	}
188 
189 	if (!sounds_dir) {
190 		g_warning ("AlarmApplet: Could not locate sounds!");
191 	}
192 
193 	// Load custom sounds from alarms
194 	for (l = applet->alarms; l != NULL; l = l->next) {
195 		alarm = ALARM (l->data);
196 		found = FALSE;
197 		for (l2 = applet->sounds; l2 != NULL; l2 = l2->next) {
198 			entry = (AlarmListEntry *)l2->data;
199 			if (strcmp (alarm->sound_file, entry->data) == 0) {
200 				// FOUND
201 				found = TRUE;
202 				break;
203 			}
204 		}
205 
206 		if (!found) {
207 			// Add to list
208 			entry = alarm_list_entry_new_file (alarm->sound_file, NULL, NULL);
209 			if (entry) {
210 				applet->sounds = g_list_append (applet->sounds, entry);
211 			}
212 		}
213 	}
214 }
215 
216 // Notify callback for changes to an alarm's sound_file
217 static void
alarm_sound_file_changed(GObject * object,GParamSpec * param,gpointer data)218 alarm_sound_file_changed (GObject *object,
219 						  GParamSpec *param,
220 						  gpointer data)
221 {
222 	Alarm *alarm		= ALARM (object);
223 	AlarmApplet *applet = (AlarmApplet *)data;
224 
225 	g_debug ("alarm_sound_file_changed: #%d", alarm->id);
226 
227 	// Reload sounds list
228 	alarm_applet_sounds_load (applet);
229 }
230 
231 
232 /*
233  * }} Sounds list
234  */
235 
236 
237 /*
238  * Apps list {{
239  */
240 
241 static gchar *
gnome_da_xml_get_string(const xmlNode * parent,const gchar * val_name)242 gnome_da_xml_get_string (const xmlNode *parent, const gchar *val_name)
243 {
244     const gchar * const *sys_langs;
245     xmlChar *node_lang;
246     xmlNode *element;
247     gchar *ret_val = NULL;
248     xmlChar *xml_val_name;
249     gint len;
250     gint i;
251 
252     g_return_val_if_fail (parent != NULL, ret_val);
253     g_return_val_if_fail (parent->children != NULL, ret_val);
254     g_return_val_if_fail (val_name != NULL, ret_val);
255 
256 #if GLIB_CHECK_VERSION (2, 6, 0)
257     sys_langs = g_get_language_names ();
258 #endif
259 
260     xml_val_name = xmlCharStrdup (val_name);
261     len = xmlStrlen (xml_val_name);
262 
263     for (element = parent->children; element != NULL; element = element->next) {
264 		if (!xmlStrncmp (element->name, xml_val_name, len)) {
265 		    node_lang = xmlNodeGetLang (element);
266 
267 		    if (node_lang == NULL) {
268 		    	ret_val = (gchar *) xmlNodeGetContent (element);
269 		    } else {
270 				for (i = 0; sys_langs[i] != NULL; i++) {
271 				    if (!strcmp (sys_langs[i], (const char *)node_lang)) {
272 						ret_val = (gchar *) xmlNodeGetContent (element);
273 						// since sys_langs is sorted from most desirable to
274 						// least desirable, exit at first match
275 						break;
276 				    }
277 				}
278 		    }
279 		    xmlFree (node_lang);
280 		}
281     }
282 
283     xmlFree (xml_val_name);
284     return ret_val;
285 }
286 
287 static const gchar *
get_app_command(const gchar * app)288 get_app_command (const gchar *app)
289 {
290 	// TODO: Shouldn't be a global variable
291 	if (app_command_map == NULL) {
292 		app_command_map = g_hash_table_new (g_str_hash, g_str_equal);
293 
294 		// `rhythmbox-client --play' doesn't actually start playing unless
295 		// Rhythmbox is already running. Sounds like a Bug.
296 		g_hash_table_insert (app_command_map,
297 							 "rhythmbox", "rhythmbox-client --play");
298 
299 		g_hash_table_insert (app_command_map,
300 							 "banshee", "banshee --play");
301 
302 		// Note that totem should already be open with a file for this to work.
303 		g_hash_table_insert (app_command_map,
304 							 "totem", "totem --play");
305 
306 		// Muine crashes and doesn't seem to have any play command
307 		/*g_hash_table_insert (app_command_map,
308 							 "muine", "muine");*/
309 	}
310 
311 	return g_hash_table_lookup (app_command_map, app);
312 }
313 
314 // Load stock apps into list
315 void
alarm_applet_apps_load(AlarmApplet * applet)316 alarm_applet_apps_load (AlarmApplet *applet)
317 {
318 	AlarmListEntry *entry;
319 	gchar *filename, *name, *icon, *command;
320 	xmlDoc *xml_doc;
321 	xmlNode *root, *section, *element;
322     gchar *executable;
323     const gchar *tmp;
324 	const gchar* const *sysdirs;
325 	gint i;
326 
327 	if (applet->apps != NULL)
328 		alarm_list_entry_list_free (&(applet->apps));
329 
330 	// Locate g-d-a.xml
331 	sysdirs = g_get_system_data_dirs ();
332 	for (i = 0; sysdirs[i] != NULL; i++) {
333 		// We'll get the default media players from g-d-a.xml
334 		// from gnome-control-center
335 		filename = g_build_filename (sysdirs[i],
336 									 "gnome-control-center",
337 									 "default-apps",
338 									 "gnome-default-applications.xml",
339 									 NULL);
340 
341 		if (g_file_test (filename, G_FILE_TEST_EXISTS)) {
342 			xml_doc = xmlParseFile (filename);
343 
344 			if (!xml_doc) {
345 				g_warning ("Could not load %s.", filename);
346 				continue;
347 			}
348 
349 			root = xmlDocGetRootElement (xml_doc);
350 
351 			for (section = root->children; section != NULL; section = section->next) {
352 				if (!xmlStrncmp (section->name, (const xmlChar *)"media-players", 13)) {
353 					for (element = section->children; element != NULL; element = element->next) {
354 						if (!xmlStrncmp (element->name, (const xmlChar *)"media-player", 12)) {
355 							executable = gnome_da_xml_get_string (element, "executable");
356 							if (is_executable_valid (executable)) {
357 								name = gnome_da_xml_get_string (element, "name");
358 								icon = gnome_da_xml_get_string (element, "icon-name");
359 
360 								// Lookup executable in app command map
361 								tmp = get_app_command (executable);
362 								if (tmp)
363 									command = g_strdup (tmp);
364 								else {
365 									// Fall back to command specified in XML
366 									command = gnome_da_xml_get_string (element, "command");
367 								}
368 
369 
370 
371 								g_debug ("LOAD-APPS: Adding '%s': %s [%s]", name, command, icon);
372 
373 								entry = alarm_list_entry_new (name, command, icon);
374 
375 								g_free (name);
376 								g_free (command);
377 								g_free (icon);
378 
379 								applet->apps = g_list_append (applet->apps, entry);
380 							}
381 
382 							if (executable)
383 								g_free (executable);
384 						}
385 					}
386 				}
387 			}
388 
389 			g_free(filename);
390 			break;
391 		}
392 
393 		g_free(filename);
394 	}
395 
396 //	entry = alarm_list_entry_new("Rhythmbox Music Player", "rhythmbox", "rhythmbox");
397 //	applet->apps = g_list_append (applet->apps, entry);
398 }
399 
400 /*
401  * Alarms list {{
402  */
403 
404 // TODO: Refactor to use a GHashTable instead?
405 void
alarm_applet_alarms_load(AlarmApplet * applet)406 alarm_applet_alarms_load (AlarmApplet *applet)
407 {
408 	GList *list = NULL;
409     GList *l = NULL;
410 
411 	if (applet->alarms != NULL) {
412 		// Free old alarm objects
413 		for (l = applet->alarms; l != NULL; l = l->next) {
414 			g_object_unref (ALARM (l->data));
415 		}
416 
417 		// Free list
418 		g_list_free (applet->alarms);
419 	}
420 
421 	// Fetch list of alarms and add them
422     applet->alarms = NULL;
423 	list = alarm_get_list (ALARM_GCONF_DIR);
424 
425     for (l = list; l != NULL; l = l->next) {
426         alarm_applet_alarms_add (applet, ALARM (l->data));
427     }
428 }
429 
430 void
alarm_applet_alarms_add(AlarmApplet * applet,Alarm * alarm)431 alarm_applet_alarms_add (AlarmApplet *applet, Alarm *alarm)
432 {
433 	applet->alarms = g_list_append (applet->alarms, alarm);
434 
435     g_signal_connect (alarm, "notify", G_CALLBACK (alarm_applet_alarm_changed), applet);
436 	g_signal_connect (alarm, "notify::sound-file", G_CALLBACK (alarm_sound_file_changed), applet);
437 
438 	g_signal_connect (alarm, "alarm", G_CALLBACK (alarm_applet_alarm_triggered), applet);
439 	g_signal_connect (alarm, "cleared", G_CALLBACK (alarm_applet_alarm_cleared), applet);
440 
441     // Update alarm list window model
442     if (applet->list_window) {
443         alarm_list_window_alarm_add (applet->list_window, alarm);
444     }
445 }
446 
447 void
alarm_applet_alarms_remove(AlarmApplet * applet,Alarm * alarm)448 alarm_applet_alarms_remove (AlarmApplet *applet, Alarm *alarm)
449 {
450 	// Remove from list
451 	applet->alarms = g_list_remove (applet->alarms, alarm);
452 
453 	// Clear list store. This will decrease the refcount of our alarms by 1.
454 	/*if (applet->list_alarms_store)
455 		gtk_list_store_clear (applet->list_alarms_store);*/
456 
457 	g_debug ("alarm_applet_alarms_remove (..., %p): refcount = %d", alarm, G_OBJECT (alarm)->ref_count);
458 
459 	// Remove any signal handlers for this alarm instance.
460 	g_signal_handlers_disconnect_matched (alarm, 0, 0, 0, NULL, NULL, NULL);
461 
462     // Update alarm list window model
463     if (applet->list_window) {
464         alarm_list_window_alarm_remove (applet->list_window, alarm);
465     }
466 
467 	// Dereference alarm
468 	g_object_unref (alarm);
469 }
470 
471 /*
472  * }} Alarms list
473  */
474 
475 // TODO: Is this function needed?
476 /*void
477 alarm_applet_destroy (AlarmApplet *applet)
478 {
479 	GList *l;
480 	Alarm *a;
481 	AlarmSettingsDialog *dialog;
482 
483 	g_debug ("AlarmApplet DESTROY");
484 
485 	// TODO: Destroy alarms list
486 //	if (applet->list_alarms_dialog) {
487 //		list_alarms_dialog_close (applet);
488 //	}
489 
490 	// Destroy preferences dialog
491 	if (applet->prefs_dialog) {
492 		gtk_widget_destroy (GTK_WIDGET (applet->prefs_dialog));
493 	}
494 
495 	// Loop through all alarms and free like a mad man!
496 	for (l = applet->alarms; l; l = l->next) {
497 		a = ALARM (l->data);
498 
499 		// Check if a dialog is open for this alarm
500 		//dialog = (AlarmSettingsDialog *)g_hash_table_lookup (applet->edit_alarm_dialogs, (gconstpointer)a->id);
501 
502 		g_object_unref (a);
503 	}
504 
505 	// Remove sounds list
506 	if (applet->sounds) {
507 		alarm_list_entry_list_free(&(applet->sounds));
508 	}
509 
510 	// Remove apps list
511 	if (applet->apps) {
512 		alarm_list_entry_list_free(&(applet->apps));
513 	}
514 
515 	if (app_command_map) {
516 		g_hash_table_destroy (app_command_map);
517 		app_command_map = NULL;
518 	}
519 
520 	// Free GConf dir
521 	//g_free (applet->gconf_dir);
522 
523 	// Finally free the AlarmApplet struct itself
524 	g_free (applet);
525 }*/
526 
527 
528 static UniqueResponse
unique_app_message_cb(UniqueApp * app,UniqueCommand command,UniqueMessageData * message,guint time_,gpointer user_data)529 unique_app_message_cb (UniqueApp         *app,
530                        UniqueCommand      command,
531                        UniqueMessageData *message,
532                        guint              time_,
533                        gpointer           user_data)
534 {
535     AlarmApplet *applet = (AlarmApplet *)user_data;
536 
537     UniqueResponse res;
538 
539     switch (command) {
540         case UNIQUE_ACTIVATE:
541             g_debug ("AlarmApplet: unique_app_message: ACTIVATE");
542             if (gtk_toggle_action_get_active (GTK_TOGGLE_ACTION (applet->action_toggle_list_win))) {
543                 // Already visible, present to user
544                 alarm_list_window_show (applet->list_window);
545             } else {
546                 // Toggle list window visibility
547                 gtk_action_activate (GTK_ACTION (applet->action_toggle_list_win));
548             }
549 
550             res = UNIQUE_RESPONSE_OK;
551             break;
552         default:
553             g_warning ("AlarmApplet: unique_app_message: Unknown command %d",
554                 command);
555 
556             res = UNIQUE_RESPONSE_INVALID;
557             break;
558     }
559 
560     return res;
561 }
562 
563 
564 
565 /*
566  * INIT {{
567  */
568 
569 static AlarmApplet*
alarm_applet_init(int * argc,char *** argv)570 alarm_applet_init (int *argc, char ***argv)
571 {
572 	AlarmApplet *applet;
573 	UniqueApp *unique_app;
574 
575 	GError *error = NULL;
576 	GOptionContext *context;
577 
578 	gboolean hidden = FALSE;     // Start hidden
579 
580     // Command line options
581 	GOptionEntry entries[] =
582 	{
583 		{ "hidden", 0, 0, G_OPTION_ARG_NONE, &hidden, "Start hidden", NULL },
584 		{ NULL }
585 	};
586 
587 	// Initialize unique app
588 	unique_app = unique_app_new ("com.pseudoberries.AlarmClock", NULL);
589 
590 	// Check if we're already running
591 	if (unique_app_is_running (unique_app)) {
592 		g_printerr(_("%s is already running, exiting...\n"), PACKAGE);
593 
594 		// Send activate message
595 		UniqueMessageData *message = unique_message_data_new ();
596 
597 		unique_app_send_message (unique_app, UNIQUE_ACTIVATE, message);
598 
599 		unique_message_data_free (message);
600 		g_object_unref (unique_app);
601 
602 		exit (EXIT_SUCCESS);
603 	}
604 
605 	// Parse command-line arguments
606 	context = g_option_context_new (NULL);
607 	g_option_context_add_main_entries (context, entries, GETTEXT_PACKAGE);
608 	g_option_context_add_group (context, gtk_get_option_group (TRUE));
609 
610 	if (!g_option_context_parse (context, argc, argv, &error)) {
611 		g_printerr ("%s\n", error->message);
612 		exit (EXIT_FAILURE);
613 	}
614 
615 	// Initialize applet struct
616 	applet = g_new0 (AlarmApplet, 1);
617 
618 	// Set up unique app
619 	applet->unique_app = unique_app;
620 
621 	g_signal_connect (unique_app, "message-received",
622 		G_CALLBACK (unique_app_message_cb), applet);
623 
624 	//applet->edit_alarm_dialogs = g_hash_table_new (NULL, NULL);
625 
626 	// Preferences (defaults).
627 	// ...gconf_get_string can return NULL if the key is not found. We can't
628 	// assume the schema provides the default values for strings.
629 
630 	// TODO: Add to gconf
631 	applet->snooze_mins = 5;
632 
633 	// Initialize gconf
634 	alarm_applet_gconf_init (applet);
635 
636 	// Load alarms
637 	alarm_applet_alarms_load (applet);
638 
639 	// Load sounds from alarms
640 	alarm_applet_sounds_load (applet);
641 
642 	// Load apps for alarms
643 	alarm_applet_apps_load (applet);
644 
645 	// Set up applet UI
646 	alarm_applet_ui_init (applet);
647 
648 	// Show alarms window, unless --hidden
649 	if (!hidden) {
650 		gtk_action_activate (GTK_ACTION (applet->action_toggle_list_win));
651 	}
652 
653 	return applet;
654 }
655 
656 /**
657  * Cleanup
658  */
659 static void
alarm_applet_quit(AlarmApplet * applet)660 alarm_applet_quit (AlarmApplet *applet)
661 {
662     g_debug ("AlarmApplet: Quitting...");
663 
664     g_object_unref (applet->unique_app);
665 }
666 
667 /**
668  * Alarm Clock main()
669  */
670 int
main(int argc,char * argv[])671 main (int argc, char *argv[])
672 {
673 	AlarmApplet *applet;
674 
675     // Internationalization
676     bindtextdomain (GETTEXT_PACKAGE, ALARM_CLOCK_DATADIR "/locale");
677     bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
678     textdomain (GETTEXT_PACKAGE);
679 
680     // Terminate on critical errors
681     //g_log_set_always_fatal (G_LOG_LEVEL_CRITICAL);
682 
683 	// Initialize GTK+
684 	gtk_init (&argc, &argv);
685 
686 	// Initialize applet
687 	applet = alarm_applet_init (&argc, &argv);
688 
689 	// Start main loop
690 	gtk_main ();
691 
692 	// Clean up
693 	alarm_applet_quit (applet);
694 
695 	return 0;
696 }
697 
698 /*
699  * }} INIT
700  */
701