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