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