1 /* Copyright (C) 2016-2017 Shengyu Zhang <i@silverrainz.me>
2  *
3  * This file is part of Srain.
4  *
5  * Srain is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 
19 /**
20  * @file sui_app.c
21  * @brief Sui module application class implementation
22  * @author Shengyu Zhang <i@silverrainz.me>
23  * @version 0.06.2
24  * @date 2016-03-01
25  */
26 
27 #include <gtk/gtk.h>
28 
29 #include "sui/sui.h"
30 #include "meta.h"
31 #include "log.h"
32 #include "i18n.h"
33 
34 #include "sui_theme.h"
35 #include "sui_common.h"
36 #include "sui_event_hdr.h"
37 #include "sui_app.h"
38 #include "sui_window.h"
39 #include "sui_prefs_dialog.h"
40 
41 struct _SuiApplication {
42     GtkApplication parent;
43 
44     GtkStatusIcon *tray_icon;
45     // GtkPopover can not shown at outside of GtkWindow on X11,
46     // so we need another traditional menu as tray icon menu.
47     GtkMenu *menu;
48     GtkPopoverMenu *popover_menu;
49     // The parsed startup commandline options, should be valid after
50     // "handle-local-options" signal.
51 
52     SuiApplicationEvents *events;
53     SuiApplicationConfig *cfg;
54     SuiApplicationOptions *opts;
55     SuiThemeManager *theme;
56     void *ctx;
57 };
58 
59 struct _SuiApplicationClass {
60     GtkApplicationClass parent_class;
61 };
62 
63 /* Only one SuiApplication instance in one application */
64 static SuiApplication *app_instance = NULL;
65 
66 static void sui_application_set_ctx(SuiApplication *self, void *ctx);
67 static void sui_application_set_events(SuiApplication *self,
68         SuiApplicationEvents *events);
69 
70 static void show_about_dialog(SuiApplication *self);
71 
72 static void on_startup(SuiApplication *self);
73 static void on_activate(SuiApplication *self);
74 static void on_shutdown(SuiApplication *self);
75 static int on_handle_local_options(SuiApplication *self, GVariantDict *options,
76         gpointer user_data);
77 static int on_command_line(SuiApplication *self,
78         GApplicationCommandLine *cmdline, gpointer user_data);
79 static void on_activate_about(GSimpleAction *action, GVariant  *parameter,
80         gpointer user_data);
81 static void on_activate_prefs(GSimpleAction *action, GVariant  *parameter,
82         gpointer user_data);
83 static void on_activate_exit(GSimpleAction *action, GVariant  *parameter,
84         gpointer user_data);
85 static void tray_icon_on_click(GtkStatusIcon *status_icon, gpointer user_data);
86 static void tray_icon_on_popup_menu(GtkStatusIcon *status_icon, guint button,
87        guint activate_time, gpointer user_data);
88 
89 /*****************************************************************************
90  * GObject functions
91  *****************************************************************************/
92 
93 enum
94 {
95   // 0 for PROP_NOME
96   PROP_CTX = 1,
97   PROP_EVENTS,
98   PROP_CONFIG,
99   N_PROPERTIES
100 };
101 
102 G_DEFINE_TYPE(SuiApplication, sui_application, GTK_TYPE_APPLICATION);
103 
104 static GParamSpec *obj_properties[N_PROPERTIES] = { NULL, };
105 
106 static const GOptionEntry option_entries[] = {
107     {
108         .long_name = "version",
109         .short_name = 'v',
110         .flags = 0,
111         .arg = G_OPTION_ARG_NONE,
112         .arg_data = NULL,
113         .description = N_("Show version information"),
114         .arg_description = NULL,
115     },
116     {
117         .long_name = "no-auto",
118         .short_name = 'a',
119         .flags = 0,
120         .arg = G_OPTION_ARG_NONE,
121         .arg_data = NULL,
122         .description = N_("Don't auto connect to servers"),
123         .arg_description = NULL,
124     },
125     {
126         .long_name = G_OPTION_REMAINING,
127         .short_name = '\0',
128         .flags = 0,
129         .arg = G_OPTION_ARG_STRING_ARRAY,
130         .arg_data = NULL,
131         .description = N_("Open one or more IRC URLs"),
132         .arg_description = N_("[URL…]")
133     },
134     {NULL}
135 };
136 
137 static const GActionEntry action_entries[] = {
138     {
139         .name = "about",
140         .activate = on_activate_about,
141     },
142     {
143         .name = "preferences",
144         .activate = on_activate_prefs,
145     },
146     {
147         .name = "exit",
148         .activate = on_activate_exit,
149     },
150     {NULL}
151 };
152 
sui_application_set_property(GObject * object,guint property_id,const GValue * value,GParamSpec * pspec)153 static void sui_application_set_property(GObject *object, guint property_id,
154         const GValue *value, GParamSpec *pspec){
155   SuiApplication *self = SUI_APPLICATION(object);
156 
157   switch (property_id){
158     case PROP_CTX:
159       sui_application_set_ctx(self, g_value_get_pointer(value));
160       break;
161     case PROP_EVENTS:
162       sui_application_set_events(self, g_value_get_pointer(value));
163       break;
164     case PROP_CONFIG:
165       sui_application_set_config(self, g_value_get_pointer(value));
166       break;
167     default:
168       /* We don't have any other property... */
169       G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
170       break;
171     }
172 }
173 
sui_application_get_property(GObject * object,guint property_id,GValue * value,GParamSpec * pspec)174 static void sui_application_get_property(GObject *object, guint property_id,
175         GValue *value, GParamSpec *pspec){
176   SuiApplication *self = SUI_APPLICATION(object);
177 
178   switch (property_id){
179     case PROP_CTX:
180       g_value_set_pointer(value, sui_application_get_ctx(self));
181       break;
182     case PROP_EVENTS:
183       g_value_set_pointer(value, sui_application_get_events(self));
184       break;
185     case PROP_CONFIG:
186       g_value_set_pointer(value, sui_application_get_config(self));
187       break;
188     default:
189       /* We don't have any other property... */
190       G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
191       break;
192     }
193 }
194 
sui_application_init(SuiApplication * self)195 static void sui_application_init(SuiApplication *self){
196     self->opts = sui_application_options_new();
197     self->theme = sui_theme_manager_new();
198 
199     g_application_add_main_option_entries(G_APPLICATION(self), option_entries);
200 
201     g_action_map_add_action_entries(G_ACTION_MAP(self), action_entries,
202             -1, self);
203 
204     g_signal_connect(self, "startup", G_CALLBACK(on_startup), NULL);
205     g_signal_connect(self, "activate", G_CALLBACK(on_activate), NULL);
206     g_signal_connect(self, "shutdown", G_CALLBACK(on_shutdown), NULL);
207     g_signal_connect(self, "command-line", G_CALLBACK(on_command_line), NULL);
208     g_signal_connect(self, "handle-local-options",
209             G_CALLBACK(on_handle_local_options), NULL);
210 
211 }
212 
sui_application_constructed(GObject * object)213 static void sui_application_constructed(GObject *object){
214     G_OBJECT_CLASS(sui_application_parent_class)->constructed(object);
215 }
216 
sui_application_finalize(GObject * object)217 static void sui_application_finalize(GObject *object){
218     SuiApplication *self;
219 
220     self = SUI_APPLICATION(object);
221 
222     g_object_unref(self->tray_icon);
223     g_object_unref(self->menu);
224     g_object_unref(self->popover_menu);
225 
226     // NOTE: SuiApplicationConfig is hold via SrnApplicationConfig so
227     // should not be freed here.
228     // sui_application_config_free(self->opts);
229     sui_application_options_free(self->opts);
230     sui_theme_manager_free(self->theme);
231 
232     G_OBJECT_CLASS(sui_application_parent_class)->finalize(object);
233 }
234 
sui_application_class_init(SuiApplicationClass * class)235 static void sui_application_class_init(SuiApplicationClass *class){
236     GObjectClass *object_class;
237 
238     /* Overwrite callbacks */
239     object_class = G_OBJECT_CLASS(class);
240     object_class->constructed = sui_application_constructed;
241     object_class->finalize = sui_application_finalize;
242     object_class->set_property = sui_application_set_property;
243     object_class->get_property = sui_application_get_property;
244 
245     /* Install properties */
246     obj_properties[PROP_CTX] =
247         g_param_spec_pointer("context",
248                 "Context",
249                 "Context of application.",
250                 G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
251 
252     obj_properties[PROP_EVENTS] =
253         g_param_spec_pointer("events",
254                 "Events",
255                 "Event callbacks of application.",
256                 G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
257 
258     obj_properties[PROP_CONFIG] =
259         g_param_spec_pointer("config",
260                 "Config",
261                 "Configuration of application.",
262                 G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
263 
264     g_object_class_install_properties(object_class,
265             N_PROPERTIES,
266             obj_properties);
267 }
268 
269 /*****************************************************************************
270  * Exported functions
271  *****************************************************************************/
272 
sui_application_new(const char * id,void * ctx,SuiApplicationEvents * events,SuiApplicationConfig * cfg)273 SuiApplication* sui_application_new(const char *id, void *ctx,
274         SuiApplicationEvents *events, SuiApplicationConfig *cfg){
275     if (app_instance == NULL) {
276         app_instance = g_object_new(SUI_TYPE_APPLICATION,
277                 "application-id", id,
278                 "flags", G_APPLICATION_HANDLES_COMMAND_LINE,
279                 "context", ctx,
280                 "events", events,
281                 "config", cfg,
282                 NULL);
283     }
284 
285     return app_instance;
286 }
287 
sui_application_run(SuiApplication * self,int argc,char * argv[])288 void sui_application_run(SuiApplication *self, int argc, char *argv[]){
289     g_return_if_fail(SUI_IS_APPLICATION(self));
290 
291     g_application_run(G_APPLICATION(self), argc, argv);
292 }
293 
sui_application_exit(SuiApplication * self)294 void sui_application_exit(SuiApplication *self){
295     g_return_if_fail(SUI_IS_APPLICATION(self));
296     /*
297     GtkWidget *win;
298     GList *list, *next;
299 
300     list = gtk_application_get_windows(GTK_APPLICATION(self));
301     while (list){
302         win = list->data;
303         next = list->next;
304         gtk_widget_destroy (GTK_WIDGET (win));
305         list = next;
306     }
307     */
308     g_application_quit(G_APPLICATION(self));
309 }
310 
311 /**
312  * @brief ``sui_application_send_notification``
313  *
314  * @param self
315  * @param msg
316  */
sui_application_send_notification(SuiApplication * self,SuiNotification * notif)317 void sui_application_send_notification(SuiApplication *self,
318         SuiNotification *notif){
319     GIcon *icon;
320     GNotification *gnotif;
321 
322     g_return_if_fail(SUI_IS_APPLICATION(self));
323     g_return_if_fail(notif);
324 
325     icon = g_themed_icon_new(notif->icon);
326     g_return_if_fail(icon);
327 
328     gnotif = g_notification_new(notif->title);
329     g_notification_set_body(gnotif, notif->body);
330     g_notification_set_icon(gnotif, icon);
331 
332     g_application_send_notification(G_APPLICATION(self), notif->id, gnotif);
333 
334     g_object_unref(gnotif);
335     g_object_unref(icon);
336 }
337 
sui_application_highlight_tray_icon(SuiApplication * self,bool highlight)338 void sui_application_highlight_tray_icon(SuiApplication *self, bool highlight){
339     gtk_status_icon_set_from_icon_name(self->tray_icon,
340             highlight ? "srain-red": PACKAGE);
341 }
342 
sui_application_get_instance()343 SuiApplication* sui_application_get_instance(){
344     return app_instance;
345 }
346 
sui_application_get_cur_window(SuiApplication * self)347 SuiWindow* sui_application_get_cur_window(SuiApplication *self){
348     GtkWindow *win;
349 
350     win = gtk_application_get_active_window(GTK_APPLICATION(self));
351     while (win && !SUI_IS_WINDOW(win)) {
352         // If active window is not a SuiWindow, try its transient parent
353         win = gtk_window_get_transient_for(win);
354     }
355 
356     return SUI_WINDOW(win);
357 }
358 
sui_application_get_popover_menu(SuiApplication * self)359 GtkPopover* sui_application_get_popover_menu(SuiApplication *self){
360     return GTK_POPOVER(self->popover_menu);
361 }
362 
sui_application_get_ctx(SuiApplication * self)363 void* sui_application_get_ctx(SuiApplication *self){
364     return self->ctx;
365 }
366 
sui_application_get_events(SuiApplication * self)367 SuiApplicationEvents* sui_application_get_events(SuiApplication *self){
368     return self->events;
369 }
370 
sui_application_set_config(SuiApplication * self,SuiApplicationConfig * cfg)371 void sui_application_set_config(SuiApplication *self, SuiApplicationConfig *cfg){
372     GList *wins;
373 
374     self->cfg = cfg;
375 
376     /* Update config of all SuiWindow */
377     wins = gtk_application_get_windows(GTK_APPLICATION(self));
378     for (GList *lst = wins; lst; lst = g_list_next(lst)){
379         SuiWindow *win;
380 
381         if (!SUI_IS_WINDOW(lst->data)) {
382             continue;
383         }
384         win = SUI_WINDOW(lst->data);
385         sui_window_set_config(win, &cfg->window);
386     }
387 }
388 
sui_application_get_config(SuiApplication * self)389 SuiApplicationConfig* sui_application_get_config(SuiApplication *self){
390     return self->cfg;
391 }
392 
sui_application_get_options(SuiApplication * self)393 SuiApplicationOptions* sui_application_get_options(SuiApplication *self){
394     return self->opts;
395 }
396 
397 /*****************************************************************************
398  * Static functions
399  *****************************************************************************/
400 
sui_application_set_ctx(SuiApplication * self,void * ctx)401 static void sui_application_set_ctx(SuiApplication *self, void *ctx){
402     self->ctx = ctx;
403 }
404 
sui_application_set_events(SuiApplication * self,SuiApplicationEvents * events)405 static void sui_application_set_events(SuiApplication *self,
406         SuiApplicationEvents *events){
407     self->events = events;
408 }
409 
show_about_dialog(SuiApplication * self)410 static void show_about_dialog(SuiApplication *self){
411     GtkWindow *window = gtk_application_get_active_window(
412             GTK_APPLICATION(self));
413     const gchar *authors[] = { PACKAGE_AUTHOR " <" PACKAGE_EMAIL ">", NULL };
414     const gchar **documentors = authors;
415     const gchar *version = g_strdup_printf(_("%1$s%2$s\nRunning against GTK+ %3$d.%4$d.%5$d"),
416             PACKAGE_VERSION,
417             PACKAGE_BUILD,
418             gtk_get_major_version(),
419             gtk_get_minor_version(),
420             gtk_get_micro_version());
421     const char *translators =
422         "Heimen Stoffels (nl)\n" \
423         "Artem Polishchuk (ru)\n" \
424         "Shengyu Zhang (zh_CN)\n" \
425         "Jianqiu Zhang (zh_CN)";
426 
427     gtk_show_about_dialog(window,
428             "program-name", PACKAGE_NAME,
429             "version", version,
430             "copyright", "(C) " PACKAGE_COPYRIGHT_DATES " " PACKAGE_AUTHOR,
431             "license-type", GTK_LICENSE_GPL_3_0,
432             "website", PACKAGE_WEBSITE,
433             "comments", PACKAGE_DESC,
434             "authors", authors,
435             "documenters", documentors,
436             "logo-icon-name", PACKAGE,
437             "title", _("About Srain"),
438             "translator-credits", translators,
439             NULL);
440 }
441 
on_startup(SuiApplication * self)442 static void on_startup(SuiApplication *self){
443     SrnRet ret;
444     GtkBuilder *builder;
445 
446     builder = gtk_builder_new_from_resource("/im/srain/Srain/app.glade");
447     self->tray_icon = GTK_STATUS_ICON(g_object_ref_sink(
448                 gtk_builder_get_object(builder, "tray_icon")));
449     self->menu = GTK_MENU(g_object_ref_sink(
450             gtk_builder_get_object(builder, "menu")));
451     self->popover_menu = GTK_POPOVER_MENU(g_object_ref_sink(
452         gtk_builder_get_object(builder, "popover_menu")));
453     g_object_unref(builder);
454 
455     // Attach to any widget to connect to action
456     gtk_menu_attach_to_widget(self->menu, GTK_WIDGET(self->popover_menu), NULL);
457 
458     // Add resource to icon search path
459     gtk_icon_theme_add_resource_path(gtk_icon_theme_get_default(),
460             "/im/srain/Srain/icons");
461 
462     g_signal_connect(self->tray_icon, "activate",
463             G_CALLBACK(tray_icon_on_click), self);
464     g_signal_connect(self->tray_icon, "popup-menu",
465             G_CALLBACK(tray_icon_on_popup_menu), self);
466 
467     ret = sui_theme_manager_apply(self->theme, self->cfg->theme);
468     if (!RET_IS_OK(ret)){
469         sui_message_box(_("Error"), RET_MSG(ret));
470     }
471 }
472 
on_activate(SuiApplication * self)473 static void on_activate(SuiApplication *self){
474     SrnRet ret;
475     GList *wins;
476 
477     /* Always show window when application activated */
478     wins = gtk_application_get_windows(GTK_APPLICATION(self));
479     for (GList *lst = wins; lst; lst = g_list_next(lst)){
480         GtkWidget *win;
481 
482         win = lst->data;
483         gtk_widget_set_visible(win, TRUE);
484     }
485 
486     ret = sui_application_event_hdr(self, SUI_EVENT_ACTIVATE, NULL);
487     if (!RET_IS_OK(ret)){
488         sui_message_box(_("Error"), RET_MSG(ret));
489     }
490 }
491 
on_shutdown(SuiApplication * self)492 static void on_shutdown(SuiApplication *self){
493     sui_application_event_hdr(self, SUI_EVENT_SHUTDOWN, NULL);
494 }
495 
on_handle_local_options(SuiApplication * self,GVariantDict * options,gpointer user_data)496 static int on_handle_local_options(SuiApplication *self, GVariantDict *options,
497         gpointer user_data){
498     if (g_variant_dict_lookup(options, "version", "b", NULL)){
499         g_print("%s %s%s\n", PACKAGE_NAME, PACKAGE_VERSION, PACKAGE_BUILD);
500         return 0; // Exit
501     }
502 
503     self->opts->no_auto_connect =
504         g_variant_dict_lookup(options, "no-auto", "b", NULL);
505 
506     return -1; // Return -1 to let the default option processing continue.
507 }
508 
on_command_line(SuiApplication * self,GApplicationCommandLine * cmdline,gpointer user_data)509 static int on_command_line(SuiApplication *self,
510         GApplicationCommandLine *cmdline, gpointer user_data){
511     char **urls;
512     GVariantDict *options;
513     GVariantDict* params;
514     SrnRet ret;
515 
516     // Activate application firstly, it will create window if not exist
517     g_application_activate(G_APPLICATION(self));
518 
519     options = g_application_command_line_get_options_dict(cmdline);
520     if (g_variant_dict_lookup(options, G_OPTION_REMAINING, "^as", &urls)){
521         params = g_variant_dict_new(NULL);
522         g_variant_dict_insert(params, "urls", SUI_EVENT_PARAM_STRINGS,
523                 urls, g_strv_length(urls));
524 
525         ret = sui_application_event_hdr(self, SUI_EVENT_OPEN, params);
526         if (!RET_IS_OK(ret)){
527             sui_message_box(_("Error"), RET_MSG(ret));
528         }
529 
530         g_variant_dict_unref(params);
531         g_strfreev(urls);
532     }
533 
534     return 0;
535 }
536 
on_activate_about(GSimpleAction * action,GVariant * parameter,gpointer user_data)537 static void on_activate_about(GSimpleAction *action, GVariant  *parameter,
538         gpointer user_data){
539     SuiApplication *self;
540 
541     self = user_data;
542     show_about_dialog(self);
543 }
544 
on_activate_prefs(GSimpleAction * action,GVariant * parameter,gpointer user_data)545 static void on_activate_prefs(GSimpleAction *action, GVariant  *parameter,
546         gpointer user_data){
547     SuiApplication *self;
548     SuiPrefsDialog *dialog;
549 
550     self = SUI_APPLICATION(user_data);
551 
552     dialog = sui_prefs_dialog_new(self, sui_application_get_cur_window(self));
553     switch (gtk_dialog_run(GTK_DIALOG(dialog))){
554         // TODO(SilverRainZ): Determine whether to write the configuration
555         // back to the file based on the GTK response.
556         default:
557             break;
558     }
559     gtk_widget_destroy(GTK_WIDGET(dialog));
560 }
561 
on_activate_exit(GSimpleAction * action,GVariant * parameter,gpointer user_data)562 static void on_activate_exit(GSimpleAction *action, GVariant  *parameter,
563         gpointer user_data){
564     SuiApplication *self;
565 
566     self = user_data;
567     sui_application_exit(self);
568 }
569 
tray_icon_on_click(GtkStatusIcon * status_icon,gpointer user_data)570 static void tray_icon_on_click(GtkStatusIcon *status_icon, gpointer user_data){
571     GList *wins;
572     SuiApplication *self;
573 
574     self = user_data;
575     wins = gtk_application_get_windows(GTK_APPLICATION(self));
576 
577     for (GList *lst = wins; lst; lst = g_list_next(lst)){
578         GtkWidget *win;
579 
580         win = lst->data;
581         gtk_widget_set_visible(win, !gtk_widget_get_visible(win));
582     }
583 }
584 
tray_icon_on_popup_menu(GtkStatusIcon * status_icon,guint button,guint activate_time,gpointer user_data)585 static void tray_icon_on_popup_menu(GtkStatusIcon *status_icon, guint button,
586        guint activate_time, gpointer user_data){
587     SuiApplication *self;
588 
589     self = user_data;
590 
591     gtk_menu_popup(self->menu, NULL, NULL, NULL, NULL, button, activate_time);
592 }
593