1 /* ide-application-actions.c
2 *
3 * Copyright 2018-2019 Christian Hergert <chergert@redhat.com>
4 *
5 * This program 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 * SPDX-License-Identifier: GPL-3.0-or-later
19 */
20
21 #define G_LOG_DOMAIN "ide-application-addins"
22 #define DOCS_URI "https://builder.readthedocs.io"
23
24 #include "config.h"
25
26 #include <glib/gi18n.h>
27 #include <ide-build-ident.h>
28 #include <libide-projects.h>
29
30 #include "ide-application.h"
31 #include "ide-application-credits.h"
32 #include "ide-application-private.h"
33 #include "ide-gui-global.h"
34 #include "ide-preferences-window.h"
35 #include "ide-shortcuts-window-private.h"
36
37 static void
ide_application_actions_preferences(GSimpleAction * action,GVariant * parameter,gpointer user_data)38 ide_application_actions_preferences (GSimpleAction *action,
39 GVariant *parameter,
40 gpointer user_data)
41 {
42 IdeApplication *self = user_data;
43 GtkWindow *toplevel = NULL;
44 GtkWindow *window;
45 GList *windows;
46
47 IDE_ENTRY;
48
49 g_assert (G_IS_SIMPLE_ACTION (action));
50 g_assert (IDE_IS_APPLICATION (self));
51
52 /* Locate a toplevel for a transient-for property, or a previous
53 * preferences window to display.
54 */
55 windows = gtk_application_get_windows (GTK_APPLICATION (self));
56 for (; windows != NULL; windows = windows->next)
57 {
58 GtkWindow *win = windows->data;
59
60 if (IDE_IS_PREFERENCES_WINDOW (win))
61 {
62 ide_gtk_window_present (win);
63 return;
64 }
65
66 if (toplevel == NULL && IDE_IS_WORKBENCH (win))
67 toplevel = win;
68 }
69
70 /* Create a new window for preferences, with enough space for
71 * 2 columns of preferences. The window manager will automatically
72 * maximize the window if necessary.
73 */
74 window = g_object_new (IDE_TYPE_PREFERENCES_WINDOW,
75 "transient-for", toplevel,
76 "default-width", 1300,
77 "default-height", 800,
78 "title", _("Builder — Preferences"),
79 "window-position", GTK_WIN_POS_CENTER_ON_PARENT,
80 NULL);
81 gtk_application_add_window (GTK_APPLICATION (self), window);
82 ide_gtk_window_present (window);
83
84 IDE_EXIT;
85 }
86
87 static void
ide_application_actions_quit(GSimpleAction * action,GVariant * param,gpointer user_data)88 ide_application_actions_quit (GSimpleAction *action,
89 GVariant *param,
90 gpointer user_data)
91 {
92 IdeApplication *self = user_data;
93
94 IDE_ENTRY;
95
96 g_assert (IDE_IS_APPLICATION (self));
97
98 /* TODO: Ask all workbenches to cleanup */
99
100 g_application_quit (G_APPLICATION (self));
101
102 IDE_EXIT;
103 }
104
105 static void
ide_application_actions_about(GSimpleAction * action,GVariant * param,gpointer user_data)106 ide_application_actions_about (GSimpleAction *action,
107 GVariant *param,
108 gpointer user_data)
109 {
110 IdeApplication *self = user_data;
111 g_autoptr(GString) version = NULL;
112 GtkDialog *dialog;
113 GtkWindow *parent = NULL;
114 GList *iter;
115 GList *windows;
116
117 g_assert (IDE_IS_APPLICATION (self));
118
119 windows = gtk_application_get_windows (GTK_APPLICATION (self));
120
121 for (iter = windows; iter; iter = iter->next)
122 {
123 if (IDE_IS_WORKSPACE (iter->data))
124 {
125 parent = iter->data;
126 break;
127 }
128 }
129
130 version = g_string_new (PACKAGE_VERSION);
131
132 if (!g_str_equal (IDE_BUILD_TYPE, "release"))
133 g_string_append (version, " (" IDE_BUILD_IDENTIFIER ")");
134
135 if (g_strcmp0 (IDE_BUILD_CHANNEL, "other") != 0)
136 g_string_append (version, "\n" IDE_BUILD_CHANNEL);
137
138 dialog = g_object_new (GTK_TYPE_ABOUT_DIALOG,
139 "artists", ide_application_credits_artists,
140 "authors", ide_application_credits_authors,
141 "comments", _("An IDE for GNOME"),
142 "copyright", "© 2014–2021 Christian Hergert, et al.",
143 "documenters", ide_application_credits_documenters,
144 "license-type", GTK_LICENSE_GPL_3_0,
145 "logo-icon-name", "org.gnome.Builder",
146 "modal", TRUE,
147 "program-name", _("GNOME Builder"),
148 "transient-for", parent,
149 "translator-credits", _("translator-credits"),
150 "use-header-bar", TRUE,
151 "version", version->str,
152 "website", "https://wiki.gnome.org/Apps/Builder",
153 "website-label", _("Learn more about GNOME Builder"),
154 NULL);
155 gtk_about_dialog_add_credit_section (GTK_ABOUT_DIALOG (dialog),
156 _("Funded By"),
157 ide_application_credits_funders);
158
159 g_signal_connect (dialog, "response", G_CALLBACK (gtk_widget_destroy), NULL);
160 ide_gtk_window_present (GTK_WINDOW (dialog));
161 }
162
163 static void
ide_application_actions_help_cb(GObject * object,GAsyncResult * result,gpointer user_data)164 ide_application_actions_help_cb (GObject *object,
165 GAsyncResult *result,
166 gpointer user_data)
167 {
168 GNetworkMonitor *monitor = (GNetworkMonitor *)object;
169 g_autoptr(IdeApplication) self = user_data;
170 GtkWindow *focused_window;
171
172 IDE_ENTRY;
173
174 g_assert (IDE_IS_APPLICATION (self));
175 g_assert (G_IS_ASYNC_RESULT (result));
176
177 focused_window = gtk_application_get_active_window (GTK_APPLICATION (self));
178
179 /*
180 * If we can reach the documentation website, prefer showing up-to-date
181 * documentation from the website.
182 */
183 if (g_network_monitor_can_reach_finish (monitor, result, NULL))
184 {
185 g_autoptr(GError) error = NULL;
186
187 g_debug ("Can reach documentation site, opening online");
188 if (!ide_gtk_show_uri_on_window (focused_window, DOCS_URI, g_get_monotonic_time (), &error))
189 g_warning ("Failed to display documentation: %s", error->message);
190
191 IDE_EXIT;
192 }
193
194 g_debug ("Cannot reach online documentation, trying locally");
195
196 /*
197 * We failed to reach the online site for some reason (offline, transient error, etc),
198 * so instead try to load the local documentation.
199 */
200 if (g_file_test (PACKAGE_DOCDIR"/en/index.html", G_FILE_TEST_IS_REGULAR))
201 {
202 g_autofree gchar *file_base = NULL;
203 g_autofree gchar *uri = NULL;
204 g_autoptr(GError) error = NULL;
205
206 if (ide_is_flatpak ())
207 file_base = ide_get_relocatable_path ("/share/doc/gnome-builder");
208 else
209 file_base = g_strdup (PACKAGE_DOCDIR);
210
211 uri = g_strdup_printf ("file://%s/en/index.html", file_base);
212
213 g_debug ("Documentation URI: %s", uri);
214
215 if (!ide_gtk_show_uri_on_window (focused_window, uri, g_get_monotonic_time (), &error))
216 g_warning ("Failed to load documentation: %s", error->message);
217
218 IDE_EXIT;
219 }
220
221 g_debug ("No locally installed documentation to display");
222
223 IDE_EXIT;
224 }
225
226 static void
ide_application_actions_help(GSimpleAction * action,GVariant * param,gpointer user_data)227 ide_application_actions_help (GSimpleAction *action,
228 GVariant *param,
229 gpointer user_data)
230 {
231 IdeApplication *self = user_data;
232 g_autoptr(GSocketConnectable) network_address = NULL;
233
234 IDE_ENTRY;
235
236 g_assert (G_IS_SIMPLE_ACTION (action));
237 g_assert (IDE_IS_APPLICATION (self));
238
239 /*
240 * Check for access to the internet. Sadly, we cannot use
241 * g_network_monitor_get_network_available() because that does not seem to
242 * act correctly on some systems (Ubuntu appears to be one example). So
243 * instead, we can asynchronously check if we can reach the peer first.
244 */
245 network_address = g_network_address_parse_uri (DOCS_URI, 443, NULL);
246 g_network_monitor_can_reach_async (g_network_monitor_get_default (),
247 network_address,
248 NULL,
249 ide_application_actions_help_cb,
250 g_object_ref (self));
251
252 IDE_EXIT;
253 }
254
255 static void
ide_application_actions_shortcuts(GSimpleAction * action,GVariant * variant,gpointer user_data)256 ide_application_actions_shortcuts (GSimpleAction *action,
257 GVariant *variant,
258 gpointer user_data)
259 {
260 IdeApplication *self = user_data;
261 GtkWindow *window;
262 GtkWindow *parent = NULL;
263 GList *list;
264
265 g_assert (IDE_IS_APPLICATION (self));
266
267 list = gtk_application_get_windows (GTK_APPLICATION (self));
268
269 for (; list; list = list->next)
270 {
271 window = list->data;
272
273 if (IDE_IS_SHORTCUTS_WINDOW (window))
274 {
275 ide_gtk_window_present (window);
276 return;
277 }
278
279 if (IDE_IS_WORKBENCH (window))
280 {
281 parent = window;
282 break;
283 }
284 }
285
286 window = g_object_new (IDE_TYPE_SHORTCUTS_WINDOW,
287 "application", self,
288 "window-position", GTK_WIN_POS_CENTER,
289 "transient-for", parent,
290 NULL);
291
292 ide_gtk_window_present (GTK_WINDOW (window));
293 }
294
295 static void
ide_application_actions_nighthack(GSimpleAction * action,GVariant * variant,gpointer user_data)296 ide_application_actions_nighthack (GSimpleAction *action,
297 GVariant *variant,
298 gpointer user_data)
299 {
300 g_autoptr(GSettings) settings = NULL;
301 g_autofree gchar *name = NULL;
302
303 g_object_set (gtk_settings_get_default (),
304 "gtk-application-prefer-dark-theme", TRUE,
305 NULL);
306
307 settings = g_settings_new ("org.gnome.builder.editor");
308 name = g_settings_get_string (settings, "style-scheme-name");
309
310 if (g_str_has_prefix (name, "Adwaita"))
311 g_settings_set_string (settings, "style-scheme-name", "Adwaita-dark");
312 else
313 g_settings_set_string (settings, "style-scheme-name", "builder-dark");
314 }
315
316 static void
ide_application_actions_dayhack(GSimpleAction * action,GVariant * variant,gpointer user_data)317 ide_application_actions_dayhack (GSimpleAction *action,
318 GVariant *variant,
319 gpointer user_data)
320 {
321 g_autoptr(GSettings) settings = NULL;
322 g_autofree gchar *name = NULL;
323
324 g_object_set (gtk_settings_get_default (),
325 "gtk-application-prefer-dark-theme", FALSE,
326 NULL);
327
328 settings = g_settings_new ("org.gnome.builder.editor");
329 name = g_settings_get_string (settings, "style-scheme-name");
330
331 if (g_str_has_prefix (name, "Adwaita"))
332 g_settings_set_string (settings, "style-scheme-name", "Adwaita");
333 else
334 g_settings_set_string (settings, "style-scheme-name", "builder");
335 }
336
337 static void
ide_application_actions_load_project(GSimpleAction * action,GVariant * args,gpointer user_data)338 ide_application_actions_load_project (GSimpleAction *action,
339 GVariant *args,
340 gpointer user_data)
341 {
342 IdeApplication *self = user_data;
343 g_autoptr(IdeProjectInfo) project_info = NULL;
344 g_autofree gchar *filename = NULL;
345 g_autoptr(GFile) file = NULL;
346 g_autofree gchar *scheme = NULL;
347
348 g_assert (IDE_IS_APPLICATION (self));
349
350 g_variant_get (args, "s", &filename);
351
352 if ((scheme = g_uri_parse_scheme (filename)))
353 file = g_file_new_for_uri (filename);
354 else
355 file = g_file_new_for_path (filename);
356
357 project_info = ide_project_info_new ();
358 ide_project_info_set_file (project_info, file);
359
360 ide_application_open_project_async (self,
361 project_info,
362 G_TYPE_INVALID,
363 NULL, NULL, NULL);
364 }
365
366 static gint
type_compare(gconstpointer a,gconstpointer b)367 type_compare (gconstpointer a,
368 gconstpointer b)
369 {
370 GType *ta = (GType *)a;
371 GType *tb = (GType *)b;
372
373 return g_type_get_instance_count (*ta) - g_type_get_instance_count (*tb);
374 }
375
376 static void
ide_application_actions_stats(GSimpleAction * action,GVariant * args,gpointer user_data)377 ide_application_actions_stats (GSimpleAction *action,
378 GVariant *args,
379 gpointer user_data)
380 {
381 guint n_types = 0;
382 g_autofree GType *types = g_type_children (G_TYPE_OBJECT, &n_types);
383 GtkScrolledWindow *scroller;
384 GtkTextBuffer *buffer;
385 GtkTextView *text_view;
386 GtkWindow *window;
387 gboolean found = FALSE;
388
389 window = g_object_new (GTK_TYPE_WINDOW,
390 "default-width", 1000,
391 "default-height", 600,
392 "title", "about:types",
393 NULL);
394 scroller = g_object_new (GTK_TYPE_SCROLLED_WINDOW,
395 "visible", TRUE,
396 NULL);
397 gtk_container_add (GTK_CONTAINER (window), GTK_WIDGET (scroller));
398 text_view = g_object_new (GTK_TYPE_TEXT_VIEW,
399 "editable", FALSE,
400 "monospace", TRUE,
401 "visible", TRUE,
402 NULL);
403 gtk_container_add (GTK_CONTAINER (scroller), GTK_WIDGET (text_view));
404 buffer = gtk_text_view_get_buffer (text_view);
405
406 gtk_text_buffer_insert_at_cursor (buffer, "Count | Type\n", -1);
407 gtk_text_buffer_insert_at_cursor (buffer, "======+======\n", -1);
408
409 qsort (types, n_types, sizeof (GType), type_compare);
410
411 for (guint i = 0; i < n_types; i++)
412 {
413 gint count = g_type_get_instance_count (types[i]);
414
415 if (count)
416 {
417 gchar str[12];
418
419 found = TRUE;
420
421 g_snprintf (str, sizeof str, "%6d", count);
422 gtk_text_buffer_insert_at_cursor (buffer, str, -1);
423 gtk_text_buffer_insert_at_cursor (buffer, " ", -1);
424 gtk_text_buffer_insert_at_cursor (buffer, g_type_name (types[i]), -1);
425 gtk_text_buffer_insert_at_cursor (buffer, "\n", -1);
426 }
427 }
428
429 if (!found)
430 gtk_text_buffer_insert_at_cursor (buffer, "No stats were found, was GOBJECT_DEBUG=instance-count set?", -1);
431
432 ide_gtk_window_present (window);
433 }
434
435 static const GActionEntry IdeApplicationActions[] = {
436 { "about:types", ide_application_actions_stats },
437 { "about", ide_application_actions_about },
438 { "dayhack", ide_application_actions_dayhack },
439 { "nighthack", ide_application_actions_nighthack },
440 { "load-project", ide_application_actions_load_project, "s"},
441 { "preferences", ide_application_actions_preferences },
442 { "quit", ide_application_actions_quit },
443 { "shortcuts", ide_application_actions_shortcuts },
444 { "help", ide_application_actions_help },
445 };
446
447 void
_ide_application_init_actions(IdeApplication * self)448 _ide_application_init_actions (IdeApplication *self)
449 {
450 g_assert (IDE_IS_MAIN_THREAD ());
451 g_assert (IDE_IS_APPLICATION (self));
452
453 g_action_map_add_action_entries (G_ACTION_MAP (self),
454 IdeApplicationActions,
455 G_N_ELEMENTS (IdeApplicationActions),
456 self);
457 }
458
459 static void
cancellable_weak_notify(gpointer data,GObject * where_object_was)460 cancellable_weak_notify (gpointer data,
461 GObject *where_object_was)
462 {
463 g_autofree char *name = data;
464 g_action_map_remove_action (G_ACTION_MAP (IDE_APPLICATION_DEFAULT), name);
465 }
466
467 char *
ide_application_create_cancel_action(IdeApplication * self,GCancellable * cancellable)468 ide_application_create_cancel_action (IdeApplication *self,
469 GCancellable *cancellable)
470 {
471 static guint cancel_count;
472 g_autofree char *action_name = NULL;
473 g_autofree char *detailed_action_name = NULL;
474 g_autoptr(GSimpleAction) action = NULL;
475 guint count;
476
477 g_return_val_if_fail (IDE_IS_APPLICATION (self), NULL);
478 g_return_val_if_fail (G_IS_CANCELLABLE (cancellable), NULL);
479
480 count = ++cancel_count;
481 action_name = g_strdup_printf ("cancel_%u", count);
482 detailed_action_name = g_strdup_printf ("app.cancel_%u", count);
483 action = g_simple_action_new (action_name, NULL);
484 g_signal_connect_object (action,
485 "activate",
486 G_CALLBACK (g_cancellable_cancel),
487 cancellable,
488 G_CONNECT_SWAPPED);
489 g_object_weak_ref (G_OBJECT (cancellable),
490 cancellable_weak_notify,
491 g_steal_pointer (&action_name));
492 g_action_map_add_action (G_ACTION_MAP (self), G_ACTION (action));
493
494 return g_steal_pointer (&detailed_action_name);
495 }
496