1 /* EasyTAG - tag editor for audio files
2  * Copyright (C) 2014-2015  David King <amigadave@amigadave.com>
3  *
4  * This program is free software; you can redistribute it and/or modify it
5  * under the terms of the GNU General Public License as published by the Free
6  * Software Foundation; either version 2 of the License, or (at your option)
7  * any later version.
8  *
9  * This program is distributed in the hope that it will be useful, but WITHOUT
10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
12  * more details.
13  *
14  * You should have received a copy of the GNU General Public License along with
15  * this program; if not, write to the Free Software Foundation, Inc., 51
16  * Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17  */
18 
19 #include "config.h"
20 
21 #include "application.h"
22 
23 #include <glib/gi18n.h>
24 #include <stdlib.h>
25 
26 #include "about.h"
27 #include "charset.h"
28 #include "easytag.h"
29 #include "log.h"
30 #include "misc.h"
31 #include "setting.h"
32 
33 typedef struct
34 {
35     guint idle_handler;
36     GFile *init_directory;
37 } EtApplicationPrivate;
38 
39 G_DEFINE_TYPE_WITH_PRIVATE (EtApplication, et_application, GTK_TYPE_APPLICATION)
40 
41 static const GOptionEntry entries[] =
42 {
43     { "version", 'v', 0, G_OPTION_ARG_NONE, NULL,
44       N_("Print the version and exit"), NULL },
45     { NULL }
46 };
47 
48 static void
on_help(GSimpleAction * action,GVariant * parameter,gpointer user_data)49 on_help (GSimpleAction *action,
50          GVariant *parameter,
51          gpointer user_data)
52 {
53     GError *error = NULL;
54 
55     /* TODO: Link to locally-installed help on Windows. */
56     gtk_show_uri (gtk_window_get_screen (GTK_WINDOW (MainWindow)),
57                   "help:easytag", GDK_CURRENT_TIME, &error);
58 
59     if (error)
60     {
61         g_debug ("Error while opening help: %s", error->message);
62         g_clear_error (&error);
63     }
64     else
65     {
66         return;
67     }
68 
69     gtk_show_uri (gtk_window_get_screen (GTK_WINDOW (MainWindow)),
70                   "https://help.gnome.org/users/easytag/stable/",
71                   GDK_CURRENT_TIME, &error);
72 
73     if (error)
74     {
75         g_debug ("Error while opening online help: %s", error->message);
76         g_error_free (error);
77     }
78 }
79 
80 static void
on_about(GSimpleAction * action,GVariant * parameter,gpointer user_data)81 on_about (GSimpleAction *action,
82           GVariant *parameter,
83           gpointer user_data)
84 {
85     et_show_about_dialog (gtk_application_get_active_window (GTK_APPLICATION (user_data)));
86 }
87 
88 static void
on_quit(GSimpleAction * action,GVariant * parameter,gpointer user_data)89 on_quit (GSimpleAction *action,
90          GVariant *parameter,
91          gpointer user_data)
92 {
93     et_application_window_quit (ET_APPLICATION_WINDOW (gtk_application_get_active_window (GTK_APPLICATION (user_data))));
94 }
95 
96 static const GActionEntry actions[] =
97 {
98     { "help", on_help },
99     { "about", on_about },
100     { "quit", on_quit }
101 };
102 
103 /*
104  * Load the default directory when the user interface is completely displayed
105  * to avoid bad visualization effect at startup.
106  */
107 static gboolean
on_idle_init(EtApplication * self)108 on_idle_init (EtApplication *self)
109 {
110     EtApplicationPrivate *priv;
111 
112     priv = et_application_get_instance_private (self);
113 
114     ET_Core_Free ();
115     ET_Core_Create ();
116 
117     if (g_settings_get_boolean (MainSettings, "scan-startup"))
118     {
119         g_action_group_activate_action (G_ACTION_GROUP (MainWindow), "scanner",
120                                         NULL);
121     }
122 
123     if (priv->init_directory)
124     {
125         et_application_window_select_dir (ET_APPLICATION_WINDOW (MainWindow),
126                                           priv->init_directory);
127     }
128     else
129     {
130         et_application_window_status_bar_message (ET_APPLICATION_WINDOW (MainWindow),
131                                                   _("Select a directory to browse"),
132                                                   FALSE);
133         g_action_group_activate_action (G_ACTION_GROUP (MainWindow),
134                                         "go-default", NULL);
135     }
136 
137     /* Set sensitivity of buttons if the default directory is invalid. */
138     et_application_window_update_actions (ET_APPLICATION_WINDOW (MainWindow));
139 
140     priv->idle_handler = 0;
141 
142     return G_SOURCE_REMOVE;
143 }
144 
145 /*
146  * common_init:
147  * @application: the application
148  *
149  * Create and show the main window. Common to all actions which may occur after
150  * startup, such as "activate" and "open".
151  */
152 static void
common_init(EtApplication * self)153 common_init (EtApplication *self)
154 {
155     EtApplicationPrivate *priv;
156     gboolean settings_warning;
157     EtApplicationWindow *window;
158 
159     priv = et_application_get_instance_private (self);
160 
161     /* Create all config files. */
162     settings_warning = !Setting_Create_Files ();
163 
164     /* Load Config */
165     Init_Config_Variables ();
166 
167     /* Initialization */
168     ET_Core_Create ();
169     Main_Stop_Button_Pressed = FALSE;
170 
171     /* The main window */
172     window = et_application_window_new (GTK_APPLICATION (self));
173     MainWindow = GTK_WIDGET (window);
174 
175     gtk_widget_show (MainWindow);
176 
177     /* Starting messages */
178     Log_Print (LOG_OK, _("Starting EasyTAG version %s…"), PACKAGE_VERSION);
179 #ifdef G_OS_WIN32
180     if (g_getenv ("LANG"))
181     {
182         Log_Print (LOG_OK, _("Setting locale: ‘%s’"), g_getenv ("LANG"));
183     }
184 #endif /* G_OS_WIN32 */
185 
186     if (get_locale ())
187         Log_Print (LOG_OK,
188                    _("System locale is ‘%s’, using ‘%s’"),
189                    get_locale (), get_encoding_from_locale (get_locale ()));
190 
191     if (settings_warning)
192     {
193         Log_Print (LOG_WARNING, _("Unable to create setting directories"));
194     }
195 
196     /* Load the default dir when the UI is created and displayed
197      * to the screen and open also the scanner window */
198     priv->idle_handler = g_idle_add ((GSourceFunc)on_idle_init, self);
199     g_source_set_name_by_id (priv->idle_handler, "Init idle function");
200 }
201 
202 /*
203  * check_for_hidden_path_in_tree:
204  * @arg: the path to check
205  *
206  * Recursively check for a hidden path in the directory tree given by @arg. If
207  * a hidden path is found, set browse-show-hidden to TRUE.
208  */
209 static void
check_for_hidden_path_in_tree(GFile * arg)210 check_for_hidden_path_in_tree (GFile *arg)
211 {
212     GFile *file = NULL;
213     GFile *parent;
214     GFileInfo *info;
215     GError *err = NULL;
216 
217     /* Not really the parent until an iteration through the loop below. */
218     parent = g_file_dup (arg);
219 
220     do
221     {
222         info = g_file_query_info (parent,
223                                   G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN,
224                                   G_FILE_QUERY_INFO_NONE, NULL, &err);
225 
226         if (info == NULL)
227         {
228             g_message ("Error querying file information (%s)", err->message);
229             g_clear_error (&err);
230 
231             if (file)
232             {
233                 g_clear_object (&file);
234             }
235             g_object_unref (parent);
236             break;
237         }
238         else
239         {
240             if (g_file_info_get_is_hidden (info))
241             {
242                 g_settings_set_boolean (MainSettings, "browse-show-hidden",
243                                         TRUE);
244             }
245         }
246 
247         g_object_unref (info);
248 
249         if (file)
250         {
251             g_clear_object (&file);
252         }
253 
254         file = parent;
255     }
256     while ((parent = g_file_get_parent (file)) != NULL);
257 
258     g_clear_object (&file);
259 }
260 
261 /*
262  * et_application_activate:
263  * @application: the application
264  *
265  * Handle the application being activated, which occurs after startup and if
266  * no files are opened.
267  */
268 static void
et_application_activate(GApplication * application)269 et_application_activate (GApplication *application)
270 {
271     GtkWindow *window;
272 
273     window = gtk_application_get_active_window (GTK_APPLICATION (application));
274 
275     if (window != NULL)
276     {
277         gtk_window_present (window);
278     }
279     else
280     {
281         common_init (ET_APPLICATION (application));
282     }
283 }
284 
285 /*
286  * et_application_local_command_line:
287  * @application: the application
288  * @arguments: pointer to the argument string array
289  * @exit_status: pointer to the returned exit status
290  *
291  * Parse the local instance command-line arguments.
292  *
293  * Returns: %TRUE to indicate that the command-line arguments were completely
294  * handled in the local instance
295  */
296 static gboolean
et_application_local_command_line(GApplication * application,gchar ** arguments[],gint * exit_status)297 et_application_local_command_line (GApplication *application,
298                                    gchar **arguments[],
299                                    gint *exit_status)
300 {
301     GError *error = NULL;
302     guint n_args;
303     gchar **argv;
304 
305     /* Try to register. */
306     if (!g_application_register (application, NULL, &error))
307     {
308         g_critical ("Error registering EtApplication: %s", error->message);
309         g_error_free (error);
310         *exit_status = 1;
311         return TRUE;
312     }
313 
314     argv = *arguments;
315     n_args = g_strv_length (argv);
316     *exit_status = 0;
317 
318     g_debug ("Received %u commandline arguments", n_args);
319 
320     if (n_args <= 1)
321     {
322         g_application_activate (application);
323         return TRUE;
324     }
325     else
326     {
327         const gsize i = 1;
328 
329         /* Exit the local instance for --help and --version. */
330         if ((strcmp (argv[i], "--version") == 0)
331             || (strcmp (argv[i], "-v") == 0))
332         {
333             g_print (PACKAGE_TARNAME " " PACKAGE_VERSION "\n");
334             g_print (_("Website: %s"), PACKAGE_URL "\n");
335             exit (0);
336         }
337         else if ((strcmp (argv[i], "--help") == 0)
338                  || (strcmp (argv[i], "-h") == 0))
339         {
340             GOptionContext *context;
341             gchar *help;
342 
343             context = g_option_context_new (_("- Tag and rename audio files"));
344             g_option_context_add_main_entries (context, entries,
345                                                GETTEXT_PACKAGE);
346             help = g_option_context_get_help (context, TRUE, NULL);
347             g_print ("%s", help);
348             exit (0);
349         }
350         else
351         {
352             /* Assume a filename otherwise, and allow the primary instance to
353              * handle it. */
354             GFile **files;
355             gsize n_files;
356             gsize j;
357 
358             n_files = n_args - 1;
359             files = g_new (GFile *, n_files);
360 
361             for (j = 0; j < n_files; j++)
362             {
363                 files[j] = g_file_new_for_commandline_arg (argv[j + 1]);
364             }
365 
366             g_application_open (application, files, n_files, "");
367 
368             for (j = 0; j < n_files; j++)
369             {
370                 g_object_unref (files[j]);
371             }
372 
373             g_free (files);
374         }
375     }
376 
377     return TRUE;
378 }
379 
380 /*
381  * et_application_open:
382  * @application: the application
383  * @files: array of files to open
384  * @n_files: the number of files
385  * @hint: hint of method to open files, currently empty
386  *
387  * Handle the files passed to the primary instance.
388  *
389  * Returns: the exit status to be passed to the calling process
390  */
391 static void
et_application_open(GApplication * self,GFile ** files,gint n_files,const gchar * hint)392 et_application_open (GApplication *self,
393                      GFile **files,
394                      gint n_files,
395                      const gchar *hint)
396 {
397     EtApplicationPrivate *priv;
398     GtkWindow *window;
399     gboolean activated;
400     GFile *arg;
401     GFile *parent;
402     GFileInfo *info;
403     GError *err = NULL;
404     GFileType type;
405     gchar *path;
406     gchar *display_path;
407 
408     priv = et_application_get_instance_private (ET_APPLICATION (self));
409 
410     window = gtk_application_get_active_window (GTK_APPLICATION (self));
411     activated = window ? TRUE : FALSE;
412 
413     /* Only take the first file; ignore the rest. */
414     arg = files[0];
415 
416     check_for_hidden_path_in_tree (arg);
417 
418     path = g_file_get_path (arg);
419     display_path = g_filename_display_name (path);
420     info = g_file_query_info (arg, G_FILE_ATTRIBUTE_STANDARD_TYPE,
421                               G_FILE_QUERY_INFO_NONE, NULL, &err);
422 
423     g_free (path);
424 
425     if (info == NULL)
426     {
427         if (activated)
428         {
429             Log_Print (LOG_ERROR, _("Error while querying information for file ‘%s’: %s"),
430                        display_path, err->message);
431 
432         }
433         else
434         {
435             g_warning ("Error while querying information for file: '%s' (%s)",
436                        display_path, err->message);
437         }
438 
439         g_free (display_path);
440         g_error_free (err);
441         return;
442     }
443 
444     type = g_file_info_get_file_type (info);
445     g_object_unref (info);
446 
447     if (type == G_FILE_TYPE_DIRECTORY)
448     {
449         if (activated)
450         {
451             et_application_window_select_dir (ET_APPLICATION_WINDOW (window),
452                                               arg);
453         }
454         else
455         {
456             priv->init_directory = g_object_ref (arg);
457         }
458     }
459     else if (type == G_FILE_TYPE_REGULAR)
460     {
461         /* When given a file, load the parent directory. */
462         parent = g_file_get_parent (arg);
463 
464         if (parent)
465         {
466             if (activated)
467             {
468                 et_application_window_select_dir (ET_APPLICATION_WINDOW (window),
469                                                   parent);
470             }
471             else
472             {
473                 priv->init_directory = parent;
474             }
475         }
476         else
477         {
478             Log_Print (LOG_WARNING, _("Cannot open path ‘%s’"), display_path);
479             g_free (display_path);
480             return;
481         }
482     }
483     else
484     {
485         Log_Print (LOG_WARNING, _("Cannot open path ‘%s’"), display_path);
486         g_free (display_path);
487         return;
488     }
489 
490     g_free (display_path);
491 
492     if (!activated)
493     {
494         common_init (ET_APPLICATION (self));
495     }
496 }
497 
498 /*
499  * et_application_shutdown:
500  * @application: the application
501  * @user_data: user data set when the signal handler was connected
502  *
503  * Handle the application being shutdown, which occurs after the main loop has
504  * exited and before returning from g_application_run().
505  */
506 static void
et_application_shutdown(GApplication * application)507 et_application_shutdown (GApplication *application)
508 {
509     Charset_Insert_Locales_Destroy ();
510 
511     G_APPLICATION_CLASS (et_application_parent_class)->shutdown (application);
512 }
513 
514 static void
et_application_startup(GApplication * application)515 et_application_startup (GApplication *application)
516 {
517     GtkBuilder *builder;
518     GMenuModel *appmenu;
519     GMenuModel *menubar;
520 
521     g_action_map_add_action_entries (G_ACTION_MAP (application), actions,
522                                      G_N_ELEMENTS (actions), application);
523 
524     G_APPLICATION_CLASS (et_application_parent_class)->startup (application);
525 
526     /* gtk_init() calls setlocale(), so gettext must be called after that. */
527     g_set_application_name (_(PACKAGE_NAME));
528     builder = gtk_builder_new_from_resource ("/org/gnome/EasyTAG/menus.ui");
529 
530     appmenu = G_MENU_MODEL (gtk_builder_get_object (builder, "app-menu"));
531     gtk_application_set_app_menu (GTK_APPLICATION (application), appmenu);
532 
533     menubar = G_MENU_MODEL (gtk_builder_get_object (builder, "menubar"));
534     gtk_application_set_menubar (GTK_APPLICATION (application), menubar);
535 
536     g_object_unref (builder);
537 
538     Charset_Insert_Locales_Init ();
539 }
540 
541 static void
et_application_dispose(GObject * object)542 et_application_dispose (GObject *object)
543 {
544     EtApplication *self;
545     EtApplicationPrivate *priv;
546 
547     self = ET_APPLICATION (object);
548     priv = et_application_get_instance_private (self);
549 
550     if (priv->idle_handler)
551     {
552         g_source_remove (priv->idle_handler);
553         priv->idle_handler = 0;
554     }
555 }
556 
557 static void
et_application_finalize(GObject * object)558 et_application_finalize (GObject *object)
559 {
560     EtApplication *self;
561     EtApplicationPrivate *priv;
562 
563     self = ET_APPLICATION (object);
564     priv = et_application_get_instance_private (self);
565 
566     if (priv->init_directory)
567     {
568         g_object_unref (priv->init_directory);
569         priv->init_directory = NULL;
570     }
571 
572     G_OBJECT_CLASS (et_application_parent_class)->finalize (object);
573 }
574 
575 static void
et_application_init(EtApplication * self)576 et_application_init (EtApplication *self)
577 {
578 }
579 
580 static void
et_application_class_init(EtApplicationClass * klass)581 et_application_class_init (EtApplicationClass *klass)
582 {
583     GObjectClass *gobject_class;
584     GApplicationClass *gapplication_class;
585 
586     gobject_class = G_OBJECT_CLASS (klass);
587     gapplication_class = G_APPLICATION_CLASS (klass);
588 
589     gobject_class->dispose = et_application_dispose;
590     gobject_class->finalize = et_application_finalize;
591     gapplication_class->activate = et_application_activate;
592     gapplication_class->local_command_line = et_application_local_command_line;
593     gapplication_class->open = et_application_open;
594     gapplication_class->shutdown = et_application_shutdown;
595     gapplication_class->startup = et_application_startup;
596 }
597 
598 /*
599  * et_application_new:
600  *
601  * Create a new EtApplication instance and initialise internationalization.
602  *
603  * Returns: a new #EtApplication
604  */
605 EtApplication *
et_application_new(void)606 et_application_new (void)
607 {
608     return g_object_new (ET_TYPE_APPLICATION, "application-id",
609                          "org.gnome.EasyTAG", "flags",
610                          G_APPLICATION_HANDLES_OPEN, NULL);
611 }
612