/* ide-workbench.c * * Copyright 2014-2019 Christian Hergert * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * SPDX-License-Identifier: GPL-3.0-or-later */ #define G_LOG_DOMAIN "ide-workbench" #include "config.h" #include #include #include #include #include "ide-build-private.h" #include "ide-context-private.h" #include "ide-foundry-init.h" #include "ide-thread-private.h" #include "ide-transfer-manager-private.h" #include "ide-application.h" #include "ide-command-manager.h" #include "ide-gui-global.h" #include "ide-gui-private.h" #include "ide-primary-workspace.h" #include "ide-workbench.h" #include "ide-workbench-addin.h" #include "ide-workspace.h" /** * SECTION:ide-workbench * @title: IdeWorkbench * @short_description: window group for all windows within a project * * The #IdeWorkbench is a #GtkWindowGroup containing the #IdeContext (root * data-structure for a project) and all of the windows associated with the * project. * * Usually, windows within the #IdeWorkbench are an #IdeWorkspace. They can * react to changes in the #IdeContext or its descendants to represent the * project and it's state. * * Since: 3.32 */ struct _IdeWorkbench { GtkWindowGroup parent_instance; /* MRU of workspaces, link embedded in workspace */ GQueue mru_queue; /* Owned references */ PeasExtensionSet *addins; GCancellable *cancellable; IdeContext *context; IdeBuildSystem *build_system; IdeProjectInfo *project_info; IdeVcs *vcs; IdeVcsMonitor *vcs_monitor; IdeSearchEngine *search_engine; /* Various flags */ guint unloaded : 1; }; typedef struct { GPtrArray *addins; IdeWorkbenchAddin *preferred; GFile *file; gchar *hint; gchar *content_type; IdeBufferOpenFlags flags; gint at_line; gint at_line_offset; } Open; typedef struct { IdeProjectInfo *project_info; GPtrArray *addins; GType workspace_type; gint64 present_time; } LoadProject; typedef struct { GPtrArray *roots; gchar *path; } ResolveFile; enum { PROP_0, PROP_CONTEXT, PROP_VCS, N_PROPS }; static void ide_workbench_action_close (IdeWorkbench *self, GVariant *param); static void ide_workbench_action_open (IdeWorkbench *self, GVariant *param); static void ide_workbench_action_dump_tasks (IdeWorkbench *self, GVariant *param); static void ide_workbench_action_object_tree (IdeWorkbench *self, GVariant *param); static void ide_workbench_action_inspector (IdeWorkbench *self, GVariant *param); static void ide_workbench_action_reload_all (IdeWorkbench *self, GVariant *param); DZL_DEFINE_ACTION_GROUP (IdeWorkbench, ide_workbench, { { "close", ide_workbench_action_close }, { "open", ide_workbench_action_open }, { "reload-files", ide_workbench_action_reload_all }, { "-inspector", ide_workbench_action_inspector }, { "-object-tree", ide_workbench_action_object_tree }, { "-dump-tasks", ide_workbench_action_dump_tasks }, }) G_DEFINE_FINAL_TYPE_WITH_CODE (IdeWorkbench, ide_workbench, GTK_TYPE_WINDOW_GROUP, G_IMPLEMENT_INTERFACE (G_TYPE_ACTION_GROUP, ide_workbench_init_action_group)) static GParamSpec *properties [N_PROPS]; static void load_project_free (LoadProject *lp) { g_clear_object (&lp->project_info); g_clear_pointer (&lp->addins, g_ptr_array_unref); g_slice_free (LoadProject, lp); } static void open_free (Open *o) { g_clear_pointer (&o->addins, g_ptr_array_unref); g_clear_object (&o->preferred); g_clear_object (&o->file); g_clear_pointer (&o->hint, g_free); g_clear_pointer (&o->content_type, g_free); g_slice_free (Open, o); } static void resolve_file_free (ResolveFile *rf) { g_clear_pointer (&rf->roots, g_ptr_array_unref); g_clear_pointer (&rf->path, g_free); g_slice_free (ResolveFile, rf); } static gboolean ignore_error (GError *error) { return g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) || g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED); } /** * ide_workbench_from_context: * @context: an #IdeContext * * Helper to get the #IdeWorkbench for a given context. * * Returns: (transfer none) (nullable): an #IdeWorkbench or %NULL * * Since: 3.40 */ IdeWorkbench * ide_workbench_from_context (IdeContext *context) { g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL); return IDE_WORKBENCH (g_object_get_data (G_OBJECT (context), "WORKBENCH")); } static void ide_workbench_set_context (IdeWorkbench *self, IdeContext *context) { g_autoptr(IdeContext) new_context = NULL; g_autoptr(IdeBufferManager) bufmgr = NULL; IdeBuildSystem *build_system; g_return_if_fail (IDE_IS_WORKBENCH (self)); g_return_if_fail (!context || IDE_IS_CONTEXT (context)); if (context == NULL) context = new_context = ide_context_new (); /* backpointer for the workbench */ g_object_set_data (G_OBJECT (context), "WORKBENCH", self); g_set_object (&self->context, context); /* Make sure we have access to buffer manager early */ bufmgr = ide_object_ensure_child_typed (IDE_OBJECT (context), IDE_TYPE_BUFFER_MANAGER); /* And use a fallback build system if one is not already available */ if ((build_system = ide_context_peek_child_typed (context, IDE_TYPE_BUILD_SYSTEM))) self->build_system = g_object_ref (build_system); else self->build_system = ide_object_ensure_child_typed (IDE_OBJECT (context), IDE_TYPE_FALLBACK_BUILD_SYSTEM); } static void ide_workbench_addin_added_workspace_cb (IdeWorkspace *workspace, IdeWorkbenchAddin *addin) { g_assert (IDE_IS_WORKSPACE (workspace)); g_assert (IDE_IS_WORKBENCH_ADDIN (addin)); ide_workbench_addin_workspace_added (addin, workspace); } static void ide_workbench_addin_removed_workspace_cb (IdeWorkspace *workspace, IdeWorkbenchAddin *addin) { g_assert (IDE_IS_WORKSPACE (workspace)); g_assert (IDE_IS_WORKBENCH_ADDIN (addin)); ide_workbench_addin_workspace_removed (addin, workspace); } static void ide_workbench_addin_added_cb (PeasExtensionSet *set, PeasPluginInfo *plugin_info, PeasExtension *exten, gpointer user_data) { IdeWorkbench *self = user_data; IdeWorkbenchAddin *addin = (IdeWorkbenchAddin *)exten; g_assert (PEAS_IS_EXTENSION_SET (set)); g_assert (plugin_info != NULL); g_assert (IDE_IS_WORKBENCH_ADDIN (addin)); g_assert (IDE_IS_WORKBENCH (self)); ide_workbench_addin_load (addin, self); /* Notify of the VCS system up-front */ if (self->vcs != NULL) ide_workbench_addin_vcs_changed (addin, self->vcs); /* * If we already loaded a project, then give the plugin a * chance to handle that, even if it is delayed a bit. */ if (self->project_info != NULL) ide_workbench_addin_load_project_async (addin, self->project_info, NULL, NULL, NULL); ide_workbench_foreach_workspace (self, (GtkCallback)ide_workbench_addin_added_workspace_cb, addin); } static void ide_workbench_addin_removed_cb (PeasExtensionSet *set, PeasPluginInfo *plugin_info, PeasExtension *exten, gpointer user_data) { IdeWorkbench *self = user_data; IdeWorkbenchAddin *addin = (IdeWorkbenchAddin *)exten; g_assert (PEAS_IS_EXTENSION_SET (set)); g_assert (plugin_info != NULL); g_assert (IDE_IS_WORKBENCH_ADDIN (addin)); g_assert (IDE_IS_WORKBENCH (self)); /* Notify of workspace removals so addins don't need to manually * track them for cleanup. */ ide_workbench_foreach_workspace (self, (GtkCallback)ide_workbench_addin_removed_workspace_cb, addin); ide_workbench_addin_unload (addin, self); } static void ide_workbench_notify_context_title (IdeWorkbench *self, GParamSpec *pspec, IdeContext *context) { g_autofree gchar *formatted = NULL; g_autofree gchar *title = NULL; g_assert (IDE_IS_MAIN_THREAD ()); g_assert (IDE_IS_WORKBENCH (self)); g_assert (IDE_IS_CONTEXT (context)); title = ide_context_dup_title (context); formatted = g_strdup_printf (_("Builder — %s"), title); ide_workbench_foreach_workspace (self, (GtkCallback)gtk_window_set_title, formatted); } static void ide_workbench_notify_context_workdir (IdeWorkbench *self, GParamSpec *pspec, IdeContext *context) { g_autoptr(GFile) workdir = NULL; g_assert (IDE_IS_MAIN_THREAD ()); g_assert (IDE_IS_WORKBENCH (self)); g_assert (IDE_IS_CONTEXT (context)); workdir = ide_context_ref_workdir (context); ide_vcs_monitor_set_root (self->vcs_monitor, workdir); } static void ide_workbench_constructed (GObject *object) { IdeWorkbench *self = (IdeWorkbench *)object; g_assert (IDE_IS_WORKBENCH (self)); if (self->context == NULL) self->context = ide_context_new (); g_signal_connect_object (self->context, "notify::title", G_CALLBACK (ide_workbench_notify_context_title), self, G_CONNECT_SWAPPED); g_signal_connect_object (self->context, "notify::workdir", G_CALLBACK (ide_workbench_notify_context_workdir), self, G_CONNECT_SWAPPED); G_OBJECT_CLASS (ide_workbench_parent_class)->constructed (object); self->vcs_monitor = g_object_new (IDE_TYPE_VCS_MONITOR, "parent", self->context, NULL); self->addins = peas_extension_set_new (peas_engine_get_default (), IDE_TYPE_WORKBENCH_ADDIN, NULL); g_signal_connect (self->addins, "extension-added", G_CALLBACK (ide_workbench_addin_added_cb), self); g_signal_connect (self->addins, "extension-removed", G_CALLBACK (ide_workbench_addin_removed_cb), self); peas_extension_set_foreach (self->addins, ide_workbench_addin_added_cb, self); /* Load command providers (which may register shortcuts) */ (void)ide_command_manager_from_context (self->context); } static void ide_workbench_finalize (GObject *object) { IdeWorkbench *self = (IdeWorkbench *)object; g_assert (IDE_IS_MAIN_THREAD ()); if (self->context != NULL) g_object_set_data (G_OBJECT (self->context), "WORKBENCH", NULL); g_clear_object (&self->build_system); g_clear_object (&self->vcs); g_clear_object (&self->search_engine); g_clear_object (&self->project_info); g_clear_object (&self->cancellable); g_clear_object (&self->context); G_OBJECT_CLASS (ide_workbench_parent_class)->finalize (object); } static void ide_workbench_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { IdeWorkbench *self = IDE_WORKBENCH (object); g_assert (IDE_IS_MAIN_THREAD ()); switch (prop_id) { case PROP_CONTEXT: g_value_set_object (value, ide_workbench_get_context (self)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } } static void ide_workbench_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { IdeWorkbench *self = IDE_WORKBENCH (object); g_assert (IDE_IS_MAIN_THREAD ()); switch (prop_id) { case PROP_CONTEXT: ide_workbench_set_context (self, g_value_get_object (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } } static void ide_workbench_class_init (IdeWorkbenchClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->constructed = ide_workbench_constructed; object_class->finalize = ide_workbench_finalize; object_class->get_property = ide_workbench_get_property; object_class->set_property = ide_workbench_set_property; /** * IdeWorkbench:context: * * The "context" property is the #IdeContext for the project. * * The #IdeContext is the root #IdeObject used in the tree of * objects representing the project and the workings of the IDE. * * Since: 3.32 */ properties [PROP_CONTEXT] = g_param_spec_object ("context", "Context", "The IdeContext for the workbench", IDE_TYPE_CONTEXT, (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); /** * IdeWorkbench:vcs: * * The "vcs" property contains an #IdeVcs that represents the version control * system that is currently loaded for the project. * * The #IdeVcs is registered by an #IdeWorkbenchAddin when loading a project. * * Since: 3.32 */ properties [PROP_VCS] = g_param_spec_object ("vcs", "Vcs", "The version control system, if any", IDE_TYPE_VCS, (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); g_object_class_install_properties (object_class, N_PROPS, properties); } static void ide_workbench_init (IdeWorkbench *self) { } static void collect_addins_cb (PeasExtensionSet *set, PeasPluginInfo *plugin_info, PeasExtension *exten, gpointer user_data) { GPtrArray *ar = user_data; g_ptr_array_add (ar, g_object_ref (exten)); } static GPtrArray * ide_workbench_collect_addins (IdeWorkbench *self) { g_autoptr(GPtrArray) ar = NULL; g_assert (IDE_IS_WORKBENCH (self)); ar = g_ptr_array_new_with_free_func (g_object_unref); if (self->addins != NULL) peas_extension_set_foreach (self->addins, collect_addins_cb, ar); return g_steal_pointer (&ar); } static IdeWorkbenchAddin * ide_workbench_find_addin (IdeWorkbench *self, const gchar *hint) { PeasEngine *engine; PeasPluginInfo *plugin_info; PeasExtension *exten = NULL; g_return_val_if_fail (IDE_IS_WORKBENCH (self), NULL); g_return_val_if_fail (hint != NULL, NULL); engine = peas_engine_get_default (); if ((plugin_info = peas_engine_get_plugin_info (engine, hint))) exten = peas_extension_set_get_extension (self->addins, plugin_info); return exten ? g_object_ref (IDE_WORKBENCH_ADDIN (exten)) : NULL; } /** * ide_workbench_new: * * Creates a new #IdeWorkbench. * * This does not create any windows, you'll need to request that a workspace * be created based on the kind of workspace you want to display to the user. * * Returns: an #IdeWorkbench * * Since: 3.32 */ IdeWorkbench * ide_workbench_new (void) { g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL); return g_object_new (IDE_TYPE_WORKBENCH, NULL); } /** * ide_workbench_new_for_context: * * Creates a new #IdeWorkbench using @context for the #IdeWorkbench:context. * * Returns: (transfer full): an #IdeWorkbench * * Since: 3.32 */ IdeWorkbench * ide_workbench_new_for_context (IdeContext *context) { g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL); return g_object_new (IDE_TYPE_CONTEXT, "visible", TRUE, NULL); } /** * ide_workbench_get_context: * @self: an #IdeWorkbench * * Gets the #IdeContext for the workbench. * * Returns: (transfer none): an #IdeContext * * Since: 3.32 */ IdeContext * ide_workbench_get_context (IdeWorkbench *self) { g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL); g_return_val_if_fail (IDE_IS_WORKBENCH (self), NULL); return self->context; } /** * ide_workbench_from_widget: * @widget: a #GtkWidget * * Finds the #IdeWorkbench associated with a widget. * * Returns: (nullable) (transfer none): an #IdeWorkbench or %NULL * * Since: 3.32 */ IdeWorkbench * ide_workbench_from_widget (GtkWidget *widget) { GtkWindowGroup *group; GtkWidget *toplevel; g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL); g_return_val_if_fail (GTK_IS_WIDGET (widget), NULL); /* * The workbench is a window group, and the workspaces belong to us. So we * just need to get the toplevel window group property, and cast. */ if ((toplevel = gtk_widget_get_toplevel (widget)) && GTK_IS_WINDOW (toplevel) && (group = gtk_window_get_group (GTK_WINDOW (toplevel))) && IDE_IS_WORKBENCH (group)) return IDE_WORKBENCH (group); return NULL; } /** * ide_workbench_foreach_workspace: * @self: an #IdeWorkbench * @callback: (scope call): a #GtkCallback to call for each #IdeWorkspace * @user_data: user data for @callback * * Iterates the available workspaces in the workbench. Workspaces are iterated * in most-recently-used order. * * Since: 3.32 */ void ide_workbench_foreach_workspace (IdeWorkbench *self, GtkCallback callback, gpointer user_data) { GList *copy; g_return_if_fail (IDE_IS_MAIN_THREAD ()); g_return_if_fail (IDE_IS_WORKBENCH (self)); g_return_if_fail (callback != NULL); /* Copy for re-entrancy safety */ copy = g_list_copy (self->mru_queue.head); for (const GList *iter = copy; iter; iter = iter->next) { IdeWorkspace *workspace = iter->data; g_assert (IDE_IS_WORKSPACE (workspace)); callback (GTK_WIDGET (workspace), user_data); } g_list_free (copy); } /** * ide_workbench_foreach_page: * @self: a #IdeWorkbench * @callback: (scope call): a callback to execute for each page * @user_data: closure data for @callback * * Calls @callback for every page loaded in the workbench, by iterating * workspaces in order of most-recently-used. * * Since: 3.32 */ void ide_workbench_foreach_page (IdeWorkbench *self, GtkCallback callback, gpointer user_data) { GList *copy; g_return_if_fail (IDE_IS_WORKBENCH (self)); g_return_if_fail (callback != NULL); /* Make a copy to be safe against auto-cleanup removals */ copy = g_list_copy (self->mru_queue.head); for (const GList *iter = copy; iter; iter = iter->next) { IdeWorkspace *workspace = iter->data; g_assert (IDE_IS_WORKSPACE (workspace)); ide_workspace_foreach_page (workspace, callback, user_data); } g_list_free (copy); } static void ide_workbench_workspace_has_toplevel_focus_cb (IdeWorkbench *self, GParamSpec *pspec, IdeWorkspace *workspace) { g_assert (IDE_IS_WORKBENCH (self)); g_assert (IDE_IS_WORKSPACE (workspace)); g_assert (gtk_window_get_group (GTK_WINDOW (workspace)) == GTK_WINDOW_GROUP (self)); if (gtk_window_has_toplevel_focus (GTK_WINDOW (workspace))) { GList *mru_link = _ide_workspace_get_mru_link (workspace); g_queue_unlink (&self->mru_queue, mru_link); g_assert (mru_link->prev == NULL); g_assert (mru_link->next == NULL); g_assert (mru_link->data == (gpointer)workspace); g_queue_push_head_link (&self->mru_queue, mru_link); } } static void insert_action_groups_foreach_cb (IdeWorkspace *workspace, gpointer user_data) { IdeWorkbench *self = user_data; struct { const gchar *name; GType child_type; } groups[] = { { "config-manager", IDE_TYPE_CONFIG_MANAGER }, { "build-manager", IDE_TYPE_BUILD_MANAGER }, { "device-manager", IDE_TYPE_DEVICE_MANAGER }, { "run-manager", IDE_TYPE_RUN_MANAGER }, { "test-manager", IDE_TYPE_TEST_MANAGER }, }; g_assert (IDE_IS_MAIN_THREAD ()); g_assert (IDE_IS_WORKBENCH (self)); g_assert (IDE_IS_WORKSPACE (workspace)); for (guint i = 0; i < G_N_ELEMENTS (groups); i++) { IdeObject *child; if ((child = ide_context_peek_child_typed (self->context, groups[i].child_type))) gtk_widget_insert_action_group (GTK_WIDGET (workspace), groups[i].name, G_ACTION_GROUP (child)); } } /** * ide_workbench_add_workspace: * @self: an #IdeWorkbench * @workspace: an #IdeWorkspace * * Adds @workspace to @workbench. * * Since: 3.32 */ void ide_workbench_add_workspace (IdeWorkbench *self, IdeWorkspace *workspace) { g_autoptr(GPtrArray) addins = NULL; IdeCommandManager *command_manager; GList *mru_link; g_return_if_fail (IDE_IS_MAIN_THREAD ()); g_return_if_fail (IDE_IS_WORKBENCH (self)); g_return_if_fail (IDE_IS_WORKSPACE (workspace)); /* Now add the window to the workspace (which takes no reference, as the * window will take a reference back to us. */ if (gtk_window_get_group (GTK_WINDOW (workspace)) != GTK_WINDOW_GROUP (self)) gtk_window_group_add_window (GTK_WINDOW_GROUP (self), GTK_WINDOW (workspace)); g_assert (gtk_window_has_group (GTK_WINDOW (workspace))); g_assert (gtk_window_get_group (GTK_WINDOW (workspace)) == GTK_WINDOW_GROUP (self)); /* Now place the workspace into our MRU tracking */ mru_link = _ide_workspace_get_mru_link (workspace); /* New workspaces are expected to be displayed right away, so we can * just push the window onto the head. */ g_queue_push_head_link (&self->mru_queue, mru_link); /* Update the context for the workspace, even if we're not loaded, * this IdeContext will be updated later. */ _ide_workspace_set_context (workspace, self->context); /* This causes the workspace to get an additional reference to the group * (which already happens from GtkWindow:group), but IdeWorkspace will * remove itself in IdeWorkspace.destroy. */ gtk_widget_insert_action_group (GTK_WIDGET (workspace), "workbench", G_ACTION_GROUP (self)); /* Give the workspace access to all the action groups of the context that * might be useful for them to access (debug-manager, run-manager, etc). */ if (self->project_info != NULL) insert_action_groups_foreach_cb (workspace, self); /* Track toplevel focus changes to maintain a most-recently-used queue. */ g_signal_connect_object (workspace, "notify::has-toplevel-focus", G_CALLBACK (ide_workbench_workspace_has_toplevel_focus_cb), self, G_CONNECT_SWAPPED); /* Give access to transfer-manager */ gtk_widget_insert_action_group (GTK_WIDGET (workspace), "transfer-manager", _ide_transfer_manager_get_actions (NULL)); /* Notify all the addins about the new workspace. */ if ((addins = ide_workbench_collect_addins (self))) { for (guint i = 0; i < addins->len; i++) { IdeWorkbenchAddin *addin = g_ptr_array_index (addins, i); ide_workbench_addin_workspace_added (addin, workspace); } } if (!gtk_window_get_title (GTK_WINDOW (workspace))) { g_autofree gchar *title = NULL; g_autofree gchar *formatted = NULL; title = ide_context_dup_title (self->context); formatted = g_strdup_printf (_("Builder — %s"), title); gtk_window_set_title (GTK_WINDOW (workspace), formatted); } /* Load shortcuts for commands */ command_manager = ide_command_manager_from_context (self->context); _ide_command_manager_init_shortcuts (command_manager, workspace); } /** * ide_workbench_remove_workspace: * @self: an #IdeWorkbench * @workspace: an #IdeWorkspace * * Removes @workspace from @workbench. * * Since: 3.32 */ void ide_workbench_remove_workspace (IdeWorkbench *self, IdeWorkspace *workspace) { g_autoptr(GPtrArray) addins = NULL; IdeCommandManager *command_manager; GList *list; GList *mru_link; guint count = 0; g_return_if_fail (IDE_IS_MAIN_THREAD ()); g_return_if_fail (IDE_IS_WORKBENCH (self)); g_return_if_fail (IDE_IS_WORKSPACE (workspace)); /* Stop tracking MRU changes */ mru_link = _ide_workspace_get_mru_link (workspace); g_queue_unlink (&self->mru_queue, mru_link); g_signal_handlers_disconnect_by_func (workspace, G_CALLBACK (ide_workbench_workspace_has_toplevel_focus_cb), self); /* Remove any shortcuts that were registered by command providers */ command_manager = ide_command_manager_from_context (self->context); _ide_command_manager_unload_shortcuts (command_manager, workspace); /* Notify all the addins about losing the workspace. */ if ((addins = ide_workbench_collect_addins (self))) { for (guint i = 0; i < addins->len; i++) { IdeWorkbenchAddin *addin = g_ptr_array_index (addins, i); ide_workbench_addin_workspace_removed (addin, workspace); } } /* Clear our action group (which drops an additional back-reference) */ gtk_widget_insert_action_group (GTK_WIDGET (workspace), "workbench", NULL); /* Only cleanup the group if it hasn't already been removed */ if (gtk_window_has_group (GTK_WINDOW (workspace))) gtk_window_group_remove_window (GTK_WINDOW_GROUP (self), GTK_WINDOW (workspace)); /* * If this is our last workspace being closed, then we want to * try to cleanup the workbench and shut things down. */ list = gtk_window_group_list_windows (GTK_WINDOW_GROUP (self)); for (const GList *iter = list; iter; iter = iter->next) { GtkWindow *window = iter->data; if (IDE_IS_WORKSPACE (window) && workspace != IDE_WORKSPACE (window)) count++; } g_list_free (list); /* * If there are no more workspaces left, then we will want to also * unload the workbench opportunistically, so that the application * can exit cleanly. */ if (count == 0 && self->unloaded == FALSE) ide_workbench_unload_async (self, NULL, NULL, NULL); } /** * ide_workbench_focus_workspace: * @self: an #IdeWorkbench * @workspace: an #IdeWorkspace * * Requests that @workspace be raised in the windows of @self, and * displayed to the user. * * Since: 3.32 */ void ide_workbench_focus_workspace (IdeWorkbench *self, IdeWorkspace *workspace) { g_return_if_fail (IDE_IS_MAIN_THREAD ()); g_return_if_fail (IDE_IS_WORKBENCH (self)); g_return_if_fail (IDE_IS_WORKSPACE (workspace)); ide_gtk_window_present (GTK_WINDOW (workspace)); } static void ide_workbench_project_loaded_foreach_cb (PeasExtensionSet *set, PeasPluginInfo *plugin_info, PeasExtension *exten, gpointer user_data) { IdeWorkbenchAddin *addin = (IdeWorkbenchAddin *)exten; IdeWorkbench *self = user_data; g_assert (IDE_IS_MAIN_THREAD ()); g_assert (PEAS_IS_EXTENSION_SET (set)); g_assert (IDE_IS_WORKBENCH_ADDIN (addin)); g_assert (IDE_IS_WORKBENCH (self)); g_assert (IDE_IS_PROJECT_INFO (self->project_info)); ide_workbench_addin_project_loaded (addin, self->project_info); } static void ide_workbench_load_project_completed (IdeWorkbench *self, IdeTask *task) { IdeBuildManager *build_manager; LoadProject *lp; g_assert (IDE_IS_WORKBENCH (self)); g_assert (IDE_IS_TASK (task)); lp = ide_task_get_task_data (task); g_assert (lp != NULL); g_assert (lp->addins != NULL); g_assert (lp->addins->len == 0); /* If we did not get a VCS as part of the loading process, set the * fallback VCS implementation. */ if (self->vcs == NULL) { g_autoptr(GFile) workdir = ide_context_ref_workdir (self->context); g_autoptr(IdeDirectoryVcs) vcs = ide_directory_vcs_new (workdir); ide_workbench_set_vcs (self, IDE_VCS (vcs)); } /* Create the search engine up-front */ if (self->search_engine == NULL) self->search_engine = ide_object_ensure_child_typed (IDE_OBJECT (self->context), IDE_TYPE_SEARCH_ENGINE); if (lp->workspace_type != G_TYPE_INVALID) { IdeWorkspace *workspace; workspace = g_object_new (lp->workspace_type, "application", IDE_APPLICATION_DEFAULT, NULL); ide_workbench_add_workspace (self, IDE_WORKSPACE (workspace)); gtk_window_present_with_time (GTK_WINDOW (workspace), lp->present_time); } /* Give workspaces access to the various GActionGroups */ ide_workbench_foreach_workspace (self, (GtkCallback)insert_action_groups_foreach_cb, self); /* Notify addins that projects have loaded */ peas_extension_set_foreach (self->addins, ide_workbench_project_loaded_foreach_cb, self); /* Now that we have a workspace window for the project, we can allow * the build manager to start. */ build_manager = ide_build_manager_from_context (self->context); _ide_build_manager_start (build_manager); ide_task_return_boolean (task, TRUE); } static void ide_workbench_load_project_cb (GObject *object, GAsyncResult *result, gpointer user_data) { IdeWorkbenchAddin *addin = (IdeWorkbenchAddin *)object; g_autoptr(IdeTask) task = user_data; g_autoptr(GError) error = NULL; IdeWorkbench *self; LoadProject *lp; g_assert (IDE_IS_WORKBENCH_ADDIN (addin)); g_assert (G_IS_ASYNC_RESULT (result)); g_assert (IDE_IS_TASK (task)); self = ide_task_get_source_object (task); lp = ide_task_get_task_data (task); g_assert (IDE_IS_WORKBENCH (self)); g_assert (lp != NULL); g_assert (IDE_IS_PROJECT_INFO (lp->project_info)); g_assert (lp->addins != NULL); g_assert (lp->addins->len > 0); if (!ide_workbench_addin_load_project_finish (addin, result, &error)) { if (!ignore_error (error)) g_warning ("%s addin failed to load project: %s", G_OBJECT_TYPE_NAME (addin), error->message); } g_ptr_array_remove (lp->addins, addin); if (lp->addins->len == 0) ide_workbench_load_project_completed (self, task); } static void ide_workbench_init_foundry_cb (GObject *object, GAsyncResult *result, gpointer user_data) { g_autoptr(IdeTask) task = user_data; g_autoptr(GError) error = NULL; IdeWorkbench *self; GCancellable *cancellable; LoadProject *lp; g_assert (IDE_IS_MAIN_THREAD ()); g_assert (G_IS_ASYNC_RESULT (result)); g_assert (IDE_IS_TASK (task)); if (!_ide_foundry_init_finish (result, &error)) g_critical ("Failed to initialize foundry: %s", error->message); cancellable = ide_task_get_cancellable (task); self = ide_task_get_source_object (task); lp = ide_task_get_task_data (task); g_assert (IDE_IS_WORKBENCH (self)); g_assert (lp != NULL); g_assert (lp->addins != NULL); g_assert (IDE_IS_PROJECT_INFO (lp->project_info)); /* Now, we need to notify all of the workbench addins that we're * opening the project. Once they have all completed, we'll create the * new workspace window and attach it. That saves us the work of * rendering various frames of the during the intensive load process. */ for (guint i = 0; i < lp->addins->len; i++) { IdeWorkbenchAddin *addin = g_ptr_array_index (lp->addins, i); ide_workbench_addin_load_project_async (addin, lp->project_info, cancellable, ide_workbench_load_project_cb, g_object_ref (task)); } if (lp->addins->len == 0) ide_workbench_load_project_completed (self, task); } /** * ide_workbench_load_project_async: * @self: a #IdeWorkbench * @project_info: an #IdeProjectInfo describing the project to open * @cancellable: (nullable): a #GCancellable or %NULL * @callback: (nullable): a #GAsyncReadyCallback to execute upon completion * @user_data: user data for @callback * * Requests that a project be opened in the workbench. * * @project_info should contain enough information to discover and load the * project. Depending on the various fields of the #IdeProjectInfo, * different plugins may become active as part of loading the project. * * Note that this may only be called once for an #IdeWorkbench. If you need * to open a second project, you need to create and register a second * workbench first, and then open using that secondary workbench. * * @callback should call ide_workbench_load_project_finish() to obtain the * result of the open request. * * Since: 3.32 */ void ide_workbench_load_project_async (IdeWorkbench *self, IdeProjectInfo *project_info, GType workspace_type, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { g_autoptr(IdeTask) task = NULL; g_autoptr(GFile) parent = NULL; g_autofree gchar *name = NULL; const gchar *project_id; LoadProject *lp; GFile *directory; GFile *file; IDE_ENTRY; g_return_if_fail (IDE_IS_MAIN_THREAD ()); g_return_if_fail (IDE_IS_WORKBENCH (self)); g_return_if_fail (IDE_IS_PROJECT_INFO (project_info)); g_return_if_fail (workspace_type != IDE_TYPE_WORKSPACE); g_return_if_fail (workspace_type == G_TYPE_INVALID || g_type_is_a (workspace_type, IDE_TYPE_WORKSPACE)); g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); g_return_if_fail (self->unloaded == FALSE); task = ide_task_new (self, cancellable, callback, user_data); ide_task_set_source_tag (task, ide_workbench_load_project_async); if (self->project_info != NULL) { ide_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED, "Cannot load project, a project is already loaded"); IDE_EXIT; } _ide_context_set_has_project (self->context); g_set_object (&self->project_info, project_info); /* Update context project-id based on project-info */ if ((project_id = ide_project_info_get_id (project_info))) { g_autofree gchar *generated = ide_create_project_id (project_id); ide_context_set_project_id (self->context, generated); } if (!ide_project_info_get_directory (project_info) && !ide_project_info_get_file (project_info)) { ide_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, "No file or directory provided to load as project"); IDE_EXIT; } /* Fallback to using directory as file if necessary */ if (!(file = ide_project_info_get_file (project_info))) { file = ide_project_info_get_directory (project_info); g_assert (G_IS_FILE (file)); ide_project_info_set_file (project_info, file); } /* * Track the directory root based on project info. If we didn't get a * directory set, then take the parent of the project file. */ if ((directory = ide_project_info_get_directory (project_info))) { ide_context_set_workdir (self->context, directory); } else { if (g_file_query_file_type (file, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL) == G_FILE_TYPE_DIRECTORY) { ide_context_set_workdir (self->context, file); directory = file; } else { ide_context_set_workdir (self->context, (parent = g_file_get_parent (file))); directory = parent; } ide_project_info_set_directory (project_info, directory); } g_assert (G_IS_FILE (directory)); name = g_file_get_basename (directory); ide_context_set_title (self->context, name); { GFile *pdir = ide_project_info_get_directory (project_info); GFile *pfile = ide_project_info_get_file (project_info); const gchar *pident = ide_project_info_get_id (project_info); const gchar *pname = ide_project_info_get_name (project_info); /* Log some information to help track down project loading issues. */ g_debug ("Loading project"); g_debug (" id = %s", pname); g_debug (" name = %s", pident); g_debug (" dir = %s", g_file_peek_path (pdir)); g_debug (" file = %s", g_file_peek_path (pfile)); } /* If there has not been a project name set, make the default matching * the directory name. A plugin may update the name with more information * based on .doap files, etc. */ if (!ide_project_info_get_name (project_info)) ide_project_info_set_name (project_info, name); /* Setup some information we're going to need later on when loading the * individual workbench addins (and then creating the workspace). */ lp = g_slice_new0 (LoadProject); lp->project_info = g_object_ref (project_info); /* HACK: Workaround for lack of last event time */ lp->present_time = g_get_monotonic_time () / 1000L; lp->addins = ide_workbench_collect_addins (self); lp->workspace_type = workspace_type; ide_task_set_task_data (task, lp, load_project_free); /* * Before we load any addins, we want to register the Foundry subsystems * such as the device manager, diagnostics engine, configurations, etc. * This makes sure that we have some basics setup before addins load. */ _ide_foundry_init_async (self->context, cancellable, ide_workbench_init_foundry_cb, g_steal_pointer (&task)); IDE_EXIT; } /** * ide_workbench_load_project_finish: * @self: a #IdeWorkbench * * Completes an asynchronous request to open a project using * ide_workbench_load_project_async(). * * Returns: %TRUE if the project was successfully opened; otherwise %FALSE * and @error is set. * * Since: 3.32 */ gboolean ide_workbench_load_project_finish (IdeWorkbench *self, GAsyncResult *result, GError **error) { g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE); g_return_val_if_fail (IDE_IS_WORKBENCH (self), FALSE); g_return_val_if_fail (IDE_IS_TASK (result), FALSE); return ide_task_propagate_boolean (IDE_TASK (result), error); } static void print_object_tree (IdeObject *object, gpointer depthptr) { gint depth = GPOINTER_TO_INT (depthptr); g_autofree gchar *space = g_strnfill (depth * 2, ' '); g_autofree gchar *info = ide_object_repr (object); g_print ("%s%s\n", space, info); ide_object_foreach (object, (GFunc)print_object_tree, GINT_TO_POINTER (depth + 1)); } static void ide_workbench_action_object_tree (IdeWorkbench *self, GVariant *param) { g_assert (IDE_IS_WORKBENCH (self)); print_object_tree (IDE_OBJECT (self->context), NULL); } static void ide_workbench_action_dump_tasks (IdeWorkbench *self, GVariant *param) { g_assert (IDE_IS_WORKBENCH (self)); _ide_dump_tasks (); } static void ide_workbench_action_inspector (IdeWorkbench *self, GVariant *param) { gtk_window_set_interactive_debugging (TRUE); } static void ide_workbench_action_close_cb (GObject *object, GAsyncResult *result, gpointer user_data) { IdeWorkbench *self = (IdeWorkbench *)object; g_assert (IDE_IS_MAIN_THREAD ()); g_assert (IDE_IS_WORKBENCH (self)); g_assert (G_IS_ASYNC_RESULT (result)); g_assert (user_data == NULL); if (ide_workbench_unload_finish (self, result, NULL)) { IdeApplication *app = IDE_APPLICATION_DEFAULT; GtkWindow *active; if (!(active = gtk_application_get_active_window (GTK_APPLICATION (app)))) g_application_activate (G_APPLICATION (app)); else ide_gtk_window_present (active); } } static void ide_workbench_action_close (IdeWorkbench *self, GVariant *param) { g_assert (IDE_IS_WORKBENCH (self)); g_assert (param == NULL); if (self->unloaded == FALSE) ide_workbench_unload_async (self, NULL, ide_workbench_action_close_cb, NULL); } static void ide_workbench_action_reload_all (IdeWorkbench *self, GVariant *param) { IdeBufferManager *bufmgr; IdeContext *context; g_assert (IDE_IS_WORKBENCH (self)); g_assert (param == NULL); context = ide_workbench_get_context (self); bufmgr = ide_buffer_manager_from_context (context); ide_buffer_manager_reload_all_async (bufmgr, NULL, NULL, NULL); } static void ide_workbench_action_open (IdeWorkbench *self, GVariant *param) { GtkFileChooserNative *chooser; IdeWorkspace *workspace; gint ret; g_assert (IDE_IS_WORKBENCH (self)); g_assert (param == NULL); workspace = ide_workbench_get_current_workspace (self); chooser = gtk_file_chooser_native_new (_("Open File…"), GTK_WINDOW (workspace), GTK_FILE_CHOOSER_ACTION_OPEN, _("_Open"), _("_Cancel")); gtk_native_dialog_set_modal (GTK_NATIVE_DIALOG (chooser), FALSE); gtk_file_chooser_set_local_only (GTK_FILE_CHOOSER (chooser), FALSE); gtk_file_chooser_set_select_multiple (GTK_FILE_CHOOSER (chooser), TRUE); ret = gtk_native_dialog_run (GTK_NATIVE_DIALOG (chooser)); if (ret == GTK_RESPONSE_ACCEPT) { g_autoslist(GFile) files = gtk_file_chooser_get_files (GTK_FILE_CHOOSER (chooser)); for (const GSList *iter = files; iter; iter = iter->next) { GFile *file = iter->data; g_assert (G_IS_FILE (file)); ide_workbench_open_async (self, file, NULL, 0, NULL, NULL, NULL); } } gtk_native_dialog_destroy (GTK_NATIVE_DIALOG (chooser)); } /** * ide_workbench_get_search_engine: * @self: a #IdeWorkbench * * Gets the search engine for the workbench, if any. * * Returns: (transfer none): an #IdeSearchEngine * * Since: 3.32 */ IdeSearchEngine * ide_workbench_get_search_engine (IdeWorkbench *self) { g_return_val_if_fail (IDE_IS_WORKBENCH (self), NULL); g_return_val_if_fail (self->context != NULL, NULL); if (self->search_engine == NULL) self->search_engine = ide_object_ensure_child_typed (IDE_OBJECT (self->context), IDE_TYPE_SEARCH_ENGINE); return self->search_engine; } /** * ide_workbench_get_project_info: * @self: a #IdeWorkbench * * Gets the #IdeProjectInfo for the workbench, if a project has been or is * currently, loading. * * Returns: (transfer none) (nullable): an #IdeProjectInfo or %NULL * * Since: 3.32 */ IdeProjectInfo * ide_workbench_get_project_info (IdeWorkbench *self) { g_return_val_if_fail (IDE_IS_WORKBENCH (self), NULL); return self->project_info; } static void ide_workbench_unload_foundry_cb (GObject *object, GAsyncResult *result, gpointer user_data) { g_autoptr(IdeTask) task = user_data; g_autoptr(GError) error = NULL; IdeWorkbench *self; g_assert (G_IS_ASYNC_RESULT (result)); g_assert (IDE_IS_TASK (task)); self = ide_task_get_source_object (task); if (!_ide_foundry_unload_finish (result, &error)) ide_task_return_error (task, g_steal_pointer (&error)); else ide_task_return_boolean (task, TRUE); if (self->context != NULL) { ide_object_destroy (IDE_OBJECT (self->context)); g_clear_object (&self->context); } } static void ide_workbench_unload_project_completed (IdeWorkbench *self, IdeTask *task) { g_assert (IDE_IS_WORKBENCH (self)); g_assert (IDE_IS_TASK (task)); g_clear_object (&self->addins); ide_workbench_foreach_workspace (self, (GtkCallback)gtk_widget_destroy, NULL); _ide_foundry_unload_async (self->context, ide_task_get_cancellable (task), ide_workbench_unload_foundry_cb, g_object_ref (task)); } static void ide_workbench_unload_project_cb (GObject *object, GAsyncResult *result, gpointer user_data) { IdeWorkbenchAddin *addin = (IdeWorkbenchAddin *)object; g_autoptr(IdeTask) task = user_data; g_autoptr(GError) error = NULL; IdeWorkbench *self; GPtrArray *addins; g_assert (IDE_IS_WORKBENCH_ADDIN (addin)); g_assert (G_IS_ASYNC_RESULT (result)); g_assert (IDE_IS_TASK (task)); self = ide_task_get_source_object (task); addins = ide_task_get_task_data (task); g_assert (IDE_IS_WORKBENCH (self)); g_assert (addins != NULL); g_assert (addins->len > 0); if (!ide_workbench_addin_unload_project_finish (addin, result, &error)) { if (!ignore_error (error)) g_warning ("%s failed to unload project: %s", G_OBJECT_TYPE_NAME (addin), error->message); } g_ptr_array_remove (addins, addin); if (addins->len == 0) ide_workbench_unload_project_completed (self, task); } /** * ide_workbench_unload_async: * @self: an #IdeWorkbench * @cancellable: (nullable): a #GCancellable * @callback: a #GAsyncReadyCallback to execute upon completion * @user_data: closure data for @callback * * Asynchronously unloads the workbench. * * All #IdeWorkspace windows will be closed after calling this * function. * * Since: 3.32 */ void ide_workbench_unload_async (IdeWorkbench *self, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { g_autoptr(IdeTask) task = NULL; g_autoptr(GPtrArray) addins = NULL; GApplication *app; g_return_if_fail (IDE_IS_WORKBENCH (self)); g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); task = ide_task_new (self, cancellable, callback, user_data); ide_task_set_source_tag (task, ide_workbench_unload_async); if (self->unloaded) { ide_task_return_boolean (task, TRUE); return; } self->unloaded = TRUE; /* Keep the GApplication alive for the lifetime of the task */ app = g_application_get_default (); g_signal_connect_object (task, "notify::completed", G_CALLBACK (g_application_release), app, G_CONNECT_SWAPPED); g_application_hold (app); /* * Remove our workbench from the application, so that no new * open-file requests can keep us alive while we're shutting * down. */ ide_application_remove_workbench (IDE_APPLICATION (app), self); /* If we haven't loaded a project, then there is nothing to * do right now, just let ide_workbench_addin_unload() be called * when the workbench disposes. */ if (self->project_info == NULL) { ide_workbench_unload_project_completed (self, g_steal_pointer (&task)); return; } addins = ide_workbench_collect_addins (self); ide_task_set_task_data (task, g_ptr_array_ref (addins), g_ptr_array_unref); if (addins->len == 0) { ide_workbench_unload_project_completed (self, task); return; } for (guint i = 0; i < addins->len; i++) { IdeWorkbenchAddin *addin = g_ptr_array_index (addins, i); ide_workbench_addin_unload_project_async (addin, self->project_info, ide_task_get_cancellable (task), ide_workbench_unload_project_cb, g_object_ref (task)); } /* Since the g_steal_pointer() just before doesn't always run, ensure the * task isn't freed while it hasn't yet finished running asynchronously. */ task = NULL; } /** * ide_workbench_unload_finish: * @self: an #IdeWorkbench * @result: a #GAsyncResult provided to callback * @error: a location for a #GError, or %NULL * Completes a request to unload the workbench. * * Returns: %TRUE if the workbench was unloaded successfully, * otherwise %FALSE and @error is set. * * Since: 3.32 */ gboolean ide_workbench_unload_finish (IdeWorkbench *self, GAsyncResult *result, GError **error) { g_return_val_if_fail (IDE_IS_WORKBENCH (self), FALSE); g_return_val_if_fail (IDE_IS_TASK (result), FALSE); return ide_task_propagate_boolean (IDE_TASK (result), error); } static void ide_workbench_open_all_cb (GObject *object, GAsyncResult *result, gpointer user_data) { IdeWorkbench *self = (IdeWorkbench *)object; g_autoptr(IdeTask) task = user_data; g_autoptr(GError) error = NULL; gint *n_active; g_assert (IDE_IS_WORKBENCH (self)); g_assert (G_IS_ASYNC_RESULT (result)); g_assert (IDE_IS_TASK (task)); if (!ide_workbench_open_finish (self, result, &error)) g_message ("Failed to open file: %s", error->message); n_active = ide_task_get_task_data (task); g_assert (n_active != NULL); (*n_active)--; if (*n_active == 0) ide_task_return_boolean (task, TRUE); } /** * ide_workbench_open_all_async: * @self: an #IdeWorkbench * @files: (array length=n_files): an array of #GFile * @n_files: number of #GFiles contained in @files * @hint: (nullable): an optional hint about what addin to use * @cancellable: (nullable): a #GCancellable * @callback: a #GAsyncReadyCallback to execute upon completion * @user_data: closure data for @callback * * Requests that the workbench open all of the #GFile denoted by @files. * * If @hint is provided, that will be used to determine what workbench * addin to use when opening the file. The @hint name should match the * module name of the plugin. * * Call ide_workbench_open_finish() from @callback to complete this * operation. * * Since: 3.32 */ void ide_workbench_open_all_async (IdeWorkbench *self, GFile **files, guint n_files, const gchar *hint, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { g_autoptr(IdeTask) task = NULL; g_autoptr(GPtrArray) ar = NULL; gint *n_active; g_return_if_fail (IDE_IS_WORKBENCH (self)); g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); task = ide_task_new (self, cancellable, callback, user_data); ide_task_set_source_tag (task, ide_workbench_open_all_async); if (n_files == 0) { ide_task_return_boolean (task, TRUE); return; } ar = g_ptr_array_new_full (n_files, g_object_unref); for (guint i = 0; i < n_files; i++) g_ptr_array_add (ar, g_object_ref (files[i])); n_active = g_new0 (gint, 1); *n_active = ar->len; ide_task_set_task_data (task, n_active, g_free); for (guint i = 0; i < ar->len; i++) { GFile *file = g_ptr_array_index (ar, i); ide_workbench_open_async (self, file, hint, IDE_BUFFER_OPEN_FLAGS_NONE, cancellable, ide_workbench_open_all_cb, g_object_ref (task)); } } /** * ide_workbench_open_async: * @self: an #IdeWorkbench * @file: a #GFile * @hint: (nullable): an optional hint about what addin to use * @flags: optional flags when opening the file * @cancellable: (nullable): a #GCancellable * @callback: a #GAsyncReadyCallback to execute upon completion * @user_data: closure data for @callback * * Requests that the workbench open @file. * * If @hint is provided, that will be used to determine what workbench * addin to use when opening the file. The @hint name should match the * module name of the plugin. * * @flags may be ignored by some backends. * * Since: 3.32 */ void ide_workbench_open_async (IdeWorkbench *self, GFile *file, const gchar *hint, IdeBufferOpenFlags flags, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { g_return_if_fail (IDE_IS_WORKBENCH (self)); g_return_if_fail (G_IS_FILE (file)); g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); ide_workbench_open_at_async (self, file, hint, -1, -1, flags, cancellable, callback, user_data); } static void ide_workbench_open_cb (GObject *object, GAsyncResult *result, gpointer user_data) { IdeWorkbenchAddin *addin = (IdeWorkbenchAddin *)object; IdeWorkbenchAddin *next; g_autoptr(IdeTask) task = user_data; g_autoptr(GError) error = NULL; GCancellable *cancellable; Open *o; g_assert (IDE_IS_WORKBENCH_ADDIN (addin)); g_assert (G_IS_ASYNC_RESULT (result)); g_assert (IDE_IS_TASK (task)); cancellable = ide_task_get_cancellable (task); o = ide_task_get_task_data (task); g_assert (!cancellable || G_IS_CANCELLABLE (cancellable)); g_assert (o != NULL); g_assert (o->addins != NULL); g_assert (o->addins->len > 0); if (ide_workbench_addin_open_finish (addin, result, &error)) { ide_task_return_boolean (task, TRUE); return; } g_debug ("%s did not open the file, trying next.", G_OBJECT_TYPE_NAME (addin)); g_ptr_array_remove (o->addins, addin); /* * We failed to open the file, try the next addin that is * left which said it supported the content-type. */ if (o->addins->len == 0) { ide_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED, "Failed to locate addin supporting file"); return; } next = g_ptr_array_index (o->addins, 0); ide_workbench_addin_open_at_async (next, o->file, o->content_type, o->at_line, o->at_line_offset, o->flags, cancellable, ide_workbench_open_cb, g_steal_pointer (&task)); } static gint sort_by_priority (gconstpointer a, gconstpointer b, gpointer user_data) { IdeWorkbenchAddin *addin_a = *(IdeWorkbenchAddin **)a; IdeWorkbenchAddin *addin_b = *(IdeWorkbenchAddin **)b; Open *o = user_data; gint prio_a = 0; gint prio_b = 0; if (!ide_workbench_addin_can_open (addin_a, o->file, o->content_type, &prio_a)) return 1; if (!ide_workbench_addin_can_open (addin_b, o->file, o->content_type, &prio_b)) return -1; if (prio_a < prio_b) return -1; else if (prio_a > prio_b) return 1; else return 0; } static void ide_workbench_open_query_info_cb (GObject *object, GAsyncResult *result, gpointer user_data) { GFile *file = (GFile *)object; g_autoptr(IdeTask) task = user_data; g_autoptr(GFileInfo) info = NULL; g_autoptr(GError) error = NULL; IdeWorkbenchAddin *first; GCancellable *cancellable; Open *o; g_assert (G_IS_FILE (file)); g_assert (G_IS_ASYNC_RESULT (result)); g_assert (IDE_IS_TASK (task)); cancellable = ide_task_get_cancellable (task); o = ide_task_get_task_data (task); g_assert (!cancellable || G_IS_CANCELLABLE (cancellable)); g_assert (o != NULL); g_assert (o->addins != NULL); g_assert (o->addins->len > 0); if ((info = g_file_query_info_finish (file, result, &error))) o->content_type = g_strdup (g_file_info_get_content_type (info)); /* Remove unsupported addins while iterating backwards so that * we can preserve the ordering of the array as we go. */ for (guint i = o->addins->len; i > 0; i--) { IdeWorkbenchAddin *addin = g_ptr_array_index (o->addins, i - 1); gint prio = G_MAXINT; if (!ide_workbench_addin_can_open (addin, o->file, o->content_type, &prio)) { g_ptr_array_remove_index_fast (o->addins, i - 1); if (o->preferred == addin) g_clear_object (&o->preferred); } } if (o->addins->len == 0) { ide_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED, "No addins can open the file"); return; } /* * Now sort the addins by priority, so that we can attempt to load them * in the preferred ordering. */ g_ptr_array_sort_with_data (o->addins, sort_by_priority, o); /* * Ensure that we place the preferred at the head of the array, so * that it gets preference over default priorities. */ if (o->preferred != NULL) { g_ptr_array_insert (o->addins, 0, g_object_ref (o->preferred)); for (guint i = 1; i < o->addins->len; i++) { if (g_ptr_array_index (o->addins, i) == (gpointer)o->preferred) { g_ptr_array_remove_index (o->addins, i); break; } } } /* Now start requesting that addins attempt to load the file. */ first = g_ptr_array_index (o->addins, 0); ide_workbench_addin_open_at_async (first, o->file, o->content_type, o->at_line, o->at_line_offset, o->flags, cancellable, ide_workbench_open_cb, g_steal_pointer (&task)); } /** * ide_workbench_open_at_async: * @self: an #IdeWorkbench * @file: a #GFile * @hint: (nullable): an optional hint about what addin to use * @at_line: the line number to open at, or -1 to ignore * @at_line_offset: the line offset to open at, or -1 to ignore * @flags: optional #IdeBufferOpenFlags * @cancellable: (nullable): a #GCancellable * @callback: a #GAsyncReadyCallback to execute upon completion * @user_data: closure data for @callback * * Like ide_workbench_open_async(), this allows opening a file * within the workbench. However, it also allows specifying a * line and column offset within the file to focus. Usually, this * only makes sense for files that can be opened in an editor. * * @at_line and @at_line_offset may be < 0 to ignore the parameters. * * @flags may be ignored by some backends * * Use ide_workbench_open_finish() to receive teh result of this * asynchronous operation. * * Since: 3.32 */ void ide_workbench_open_at_async (IdeWorkbench *self, GFile *file, const gchar *hint, gint at_line, gint at_line_offset, IdeBufferOpenFlags flags, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { g_autoptr(IdeTask) task = NULL; g_autoptr(GPtrArray) addins = NULL; IdeWorkbench *other; Open *o; g_return_if_fail (IDE_IS_WORKBENCH (self)); g_return_if_fail (G_IS_FILE (file)); g_return_if_fail (self->unloaded == FALSE); g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable)); /* Possibly re-route opening the file to another workbench if we * discover the file is a better fit over there. */ other = ide_application_find_workbench_for_file (IDE_APPLICATION_DEFAULT, file); if (other != NULL && other != self) { ide_workbench_open_at_async (other, file, hint, at_line, at_line_offset, flags, cancellable, callback, user_data); return; } /* Canonicalize parameters */ if (at_line < 0) at_line = -1; if (at_line_offset < 0) at_line_offset = -1; task = ide_task_new (self, cancellable, callback, user_data); ide_task_set_source_tag (task, ide_workbench_open_at_async); /* * Make sure we might have an addin to load after discovering * the files content-type. */ if (!(addins = ide_workbench_collect_addins (self)) || addins->len == 0) { ide_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED, "No addins could open the file"); return; } o = g_slice_new0 (Open); o->addins = g_ptr_array_ref (addins); if (hint != NULL) o->preferred = ide_workbench_find_addin (self, hint); o->file = g_object_ref (file); o->hint = g_strdup (hint); o->flags = flags; o->at_line = at_line; o->at_line_offset = at_line_offset; ide_task_set_task_data (task, o, open_free); g_file_query_info_async (file, G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE, G_FILE_QUERY_INFO_NONE, G_PRIORITY_DEFAULT, cancellable, ide_workbench_open_query_info_cb, g_steal_pointer (&task)); } /** * ide_workbench_open_finish: * @self: an #IdeWorkbench * @result: a #GAsyncResult provided to callback * @error: a location for a #GError, or %NULL * * Completes a request to open a file using either * ide_workbench_open_async() or ide_workbench_open_at_async(). * * Returns: %TRUE if the file was successfully opened; otherwise * %FALSE and @error is set. * * Since: 3.32 */ gboolean ide_workbench_open_finish (IdeWorkbench *self, GAsyncResult *result, GError **error) { g_return_val_if_fail (IDE_IS_WORKBENCH (self), FALSE); g_return_val_if_fail (IDE_IS_TASK (result), FALSE); return ide_task_propagate_boolean (IDE_TASK (result), error); } /** * ide_workbench_get_current_workspace: * @self: a #IdeWorkbench * * Gets the most recently focused workspace, which may be used to * deliver events such as opening new pages. * * Returns: (transfer none) (nullable): an #IdeWorkspace or %NULL * * Since: 3.32 */ IdeWorkspace * ide_workbench_get_current_workspace (IdeWorkbench *self) { g_return_val_if_fail (IDE_IS_WORKBENCH (self), NULL); if (self->mru_queue.length > 0) return IDE_WORKSPACE (self->mru_queue.head->data); return NULL; } /** * ide_workbench_activate: * @self: a #IdeWorkbench * * This function will attempt to raise the most recently focused workspace. * * Since: 3.32 */ void ide_workbench_activate (IdeWorkbench *self) { IdeWorkspace *workspace; g_return_if_fail (IDE_IS_WORKBENCH (self)); if ((workspace = ide_workbench_get_current_workspace (self))) ide_workbench_focus_workspace (self, workspace); } static void ide_workbench_propagate_vcs_cb (PeasExtensionSet *set, PeasPluginInfo *plugin_info, PeasExtension *exten, gpointer user_data) { IdeWorkbenchAddin *addin = (IdeWorkbenchAddin *)exten; IdeVcs *vcs = user_data; g_assert (PEAS_IS_EXTENSION_SET (set)); g_assert (plugin_info != NULL); g_assert (IDE_IS_WORKBENCH_ADDIN (addin)); g_assert (!vcs || IDE_IS_VCS (vcs)); ide_workbench_addin_vcs_changed (addin, vcs); } /** * ide_workbench_get_vcs: * @self: a #IdeWorkbench * * Gets the #IdeVcs that has been loaded for the workbench, if any. * * Returns: (transfer none) (nullable): an #IdeVcs or %NULL * * Since: 3.32 */ IdeVcs * ide_workbench_get_vcs (IdeWorkbench *self) { g_return_val_if_fail (IDE_IS_WORKBENCH (self), NULL); return self->vcs; } /** * ide_workbench_get_vcs_monitor: * @self: a #IdeWorkbench * * Gets the #IdeVcsMonitor for the workbench, if any. * * Returns: (transfer none) (nullable): an #IdeVcsMonitor or %NULL * * Since: 3.32 */ IdeVcsMonitor * ide_workbench_get_vcs_monitor (IdeWorkbench *self) { g_return_val_if_fail (IDE_IS_WORKBENCH (self), NULL); return self->vcs_monitor; } static void remove_non_matching_vcs_cb (IdeObject *child, IdeVcs *vcs) { g_assert (IDE_IS_MAIN_THREAD ()); g_assert (IDE_IS_OBJECT (child)); g_assert (IDE_IS_VCS (vcs)); if (IDE_IS_VCS (child) && IDE_VCS (child) != vcs) ide_object_destroy (child); } static void ide_workbench_vcs_notify_branch_name_cb (IdeWorkbench *self, GParamSpec *pspec, IdeVcs *vcs) { IdeBuildManager *build_manager; IDE_ENTRY; g_assert (IDE_IS_WORKBENCH (self)); g_assert (IDE_IS_VCS (vcs)); if (!ide_workbench_has_project (self)) IDE_EXIT; build_manager = ide_build_manager_from_context (self->context); ide_build_manager_invalidate (build_manager); IDE_EXIT; } /** * ide_workbench_set_vcs: * @self: a #IdeWorkbench * @vcs: (nullable): an #IdeVcs * * Sets the #IdeVcs for the workbench. * * Since: 3.32 */ void ide_workbench_set_vcs (IdeWorkbench *self, IdeVcs *vcs) { g_autoptr(IdeVcs) local_vcs = NULL; g_autoptr(GFile) local_workdir = NULL; g_autoptr(GFile) workdir = NULL; g_return_if_fail (IDE_IS_MAIN_THREAD ()); g_return_if_fail (IDE_IS_WORKBENCH (self)); g_return_if_fail (!vcs || IDE_IS_VCS (vcs)); if (self->vcs != NULL && vcs == self->vcs) return; if (vcs == NULL) { local_workdir = ide_context_ref_workdir (self->context); vcs = local_vcs = IDE_VCS (ide_directory_vcs_new (local_workdir)); } g_set_object (&self->vcs, vcs); ide_object_append (IDE_OBJECT (self->context), IDE_OBJECT (vcs)); ide_object_foreach (IDE_OBJECT (self->context), (GFunc)remove_non_matching_vcs_cb, vcs); if ((workdir = ide_vcs_get_workdir (vcs))) ide_context_set_workdir (self->context, workdir); ide_vcs_monitor_set_vcs (self->vcs_monitor, self->vcs); peas_extension_set_foreach (self->addins, ide_workbench_propagate_vcs_cb, self->vcs); g_signal_connect_object (vcs, "notify::branch-name", G_CALLBACK (ide_workbench_vcs_notify_branch_name_cb), self, G_CONNECT_SWAPPED); g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_VCS]); } /** * ide_workbench_get_build_system: * @self: a #IdeWorkbench * * Gets the #IdeBuildSystem for the workbench, if any. * * Returns: (transfer none) (nullable): an #IdeBuildSystem or %NULL * * Since: 3.32 */ IdeBuildSystem * ide_workbench_get_build_system (IdeWorkbench *self) { g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL); g_return_val_if_fail (IDE_IS_WORKBENCH (self), NULL); return self->build_system; } static void remove_non_matching_build_systems_cb (IdeObject *child, IdeBuildSystem *build_system) { g_assert (IDE_IS_MAIN_THREAD ()); g_assert (IDE_IS_OBJECT (child)); g_assert (IDE_IS_BUILD_SYSTEM (build_system)); if (IDE_IS_BUILD_SYSTEM (child) && IDE_BUILD_SYSTEM (child) != build_system) ide_object_destroy (child); } /** * ide_workbench_set_build_system: * @self: a #IdeWorkbench * @build_system: (nullable): an #IdeBuildSystem or %NULL * * Sets the #IdeBuildSystem for the workbench. * * If @build_system is %NULL, then a fallback build system will be used * instead. It does not provide building capabilities, but allows for some * components that require a build system to continue functioning. * * Since: 3.32 */ void ide_workbench_set_build_system (IdeWorkbench *self, IdeBuildSystem *build_system) { g_autoptr(IdeBuildSystem) local_build_system = NULL; IdeBuildManager *build_manager; g_return_if_fail (IDE_IS_WORKBENCH (self)); g_return_if_fail (!build_system || IDE_IS_BUILD_SYSTEM (build_system)); if (build_system == self->build_system) return; /* We want there to always be a build system available so that various * plugins don't need lots of extra code to handle the %NULL case. So * if @build_system is %NULL, then we'll create a fallback build system * and assign that instead. */ if (build_system == NULL) build_system = local_build_system = ide_fallback_build_system_new (); /* We want to add our new build system before removing the old build * system to ensure there is always an #IdeBuildSystem child of the * IdeContext. */ g_set_object (&self->build_system, build_system); ide_object_append (IDE_OBJECT (self->context), IDE_OBJECT (build_system)); /* Now remove any previous build-system from the context */ ide_object_foreach (IDE_OBJECT (self->context), (GFunc)remove_non_matching_build_systems_cb, build_system); /* Ask the build-manager to setup a new pipeline */ if ((build_manager = ide_context_peek_child_typed (self->context, IDE_TYPE_BUILD_MANAGER))) ide_build_manager_invalidate (build_manager); } /** * ide_workbench_get_workspace_by_type: * @self: a #IdeWorkbench * @type: a #GType of a subclass of #IdeWorkspace * * Gets the most-recently-used workspace that matches @type. * * Returns: (transfer none) (nullable): an #IdeWorkspace or %NULL * * Since: 3.32 */ IdeWorkspace * ide_workbench_get_workspace_by_type (IdeWorkbench *self, GType type) { g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL); g_return_val_if_fail (IDE_IS_WORKBENCH (self), NULL); g_return_val_if_fail (g_type_is_a (type, IDE_TYPE_WORKSPACE), NULL); for (const GList *iter = self->mru_queue.head; iter; iter = iter->next) { if (G_TYPE_CHECK_INSTANCE_TYPE (iter->data, type)) return IDE_WORKSPACE (iter->data); } return NULL; } gboolean _ide_workbench_is_last_workspace (IdeWorkbench *self, IdeWorkspace *workspace) { g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE); g_return_val_if_fail (IDE_IS_WORKBENCH (self), FALSE); return self->mru_queue.length == 1 && g_queue_peek_head (&self->mru_queue) == (gpointer)workspace; } /** * ide_workbench_has_project: * @self: a #IdeWorkbench * * Returns %TRUE if a project is loaded (or currently loading) in the * workbench. * * Returns: %TRUE if the workbench has a project * * Since: 3.32 */ gboolean ide_workbench_has_project (IdeWorkbench *self) { g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE); g_return_val_if_fail (IDE_IS_WORKBENCH (self), FALSE); return self->project_info != NULL; } /** * ide_workbench_addin_find_by_module_name: * @workbench: an #IdeWorkbench * @module_name: the name of the addin module * * Finds the addin (if any) matching the plugin's @module_name. * * Returns: (transfer none) (nullable): an #IdeWorkbenchAddin or %NULL * * Since: 3.32 */ IdeWorkbenchAddin * ide_workbench_addin_find_by_module_name (IdeWorkbench *workbench, const gchar *module_name) { PeasPluginInfo *plugin_info; PeasExtension *ret = NULL; PeasEngine *engine; g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL); g_return_val_if_fail (IDE_IS_WORKBENCH (workbench), NULL); g_return_val_if_fail (module_name != NULL, NULL); if (workbench->addins == NULL) return NULL; engine = peas_engine_get_default (); if ((plugin_info = peas_engine_get_plugin_info (engine, module_name))) ret = peas_extension_set_get_extension (workbench->addins, plugin_info); return IDE_WORKBENCH_ADDIN (ret); } static void ide_workbench_resolve_file_worker (IdeTask *task, gpointer source_object, gpointer task_data, GCancellable *cancellable) { ResolveFile *rf = task_data; g_autofree gchar *basename = NULL; g_assert (IDE_IS_TASK (task)); g_assert (IDE_IS_WORKBENCH (source_object)); g_assert (rf != NULL); g_assert (rf->roots != NULL); g_assert (!cancellable || G_IS_CANCELLABLE (cancellable)); basename = g_path_get_basename (rf->path); for (guint i = 0; i < rf->roots->len; i++) { GFile *root = g_ptr_array_index (rf->roots, i); g_autoptr(GFile) child = g_file_get_child (root, rf->path); g_autoptr(GPtrArray) found = NULL; if (g_file_query_exists (child, cancellable)) { ide_task_return_pointer (task, g_steal_pointer (&child), g_object_unref); return; } found = ide_g_file_find_with_depth (root, basename, 0, cancellable); IDE_PTR_ARRAY_SET_FREE_FUNC (found, g_object_unref); if (found != NULL && found->len > 0) { GFile *match = g_ptr_array_index (found, 0); ide_task_return_pointer (task, g_object_ref (match), g_object_unref); return; } } ide_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, "Failed to locate file %s", basename); } /** * ide_workbench_resolve_file_async: * @self: a #IdeWorkbench * @filename: the filename to discover * * This function will try to locate a given file based on the filename, * possibly resolving it from a build directory, or source directory. * * If no file was discovered, some attempt will be made to locate a file * that matches appropriately. * * Since: 3.32 */ void ide_workbench_resolve_file_async (IdeWorkbench *self, const gchar *filename, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { g_autoptr(IdeTask) task = NULL; ResolveFile *rf; IDE_ENTRY; g_return_if_fail (IDE_IS_MAIN_THREAD ()); g_return_if_fail (IDE_IS_WORKBENCH (self)); g_return_if_fail (filename != NULL); task = ide_task_new (self, cancellable, callback, user_data); ide_task_set_source_tag (task, ide_workbench_resolve_file_async); rf = g_slice_new0 (ResolveFile); rf->roots = g_ptr_array_new_with_free_func (g_object_unref); rf->path = g_strdup (filename); g_ptr_array_add (rf->roots, ide_context_ref_workdir (self->context)); if (ide_workbench_has_project (self)) { IdeBuildManager *build_manager = ide_build_manager_from_context (self->context); IdePipeline *pipeline = ide_build_manager_get_pipeline (build_manager); if (pipeline != NULL) { const gchar *builddir = ide_pipeline_get_builddir (pipeline); g_ptr_array_add (rf->roots, g_file_new_for_path (builddir)); } } ide_task_set_task_data (task, rf, resolve_file_free); ide_task_run_in_thread (task, ide_workbench_resolve_file_worker); IDE_EXIT; } /** * ide_workbench_resolve_file_finish: * @self: a #IdeWorkbench * @result: a #GAsyncResult * @error: a location for a #GError * * Completes an asynchronous request to ide_workbench_resolve_file_async(). * * Returns: (transfer full): a #GFile, or %NULL and @error is set * * Since: 3.32 */ GFile * ide_workbench_resolve_file_finish (IdeWorkbench *self, GAsyncResult *result, GError **error) { GFile *ret; IDE_ENTRY; g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL); g_return_val_if_fail (IDE_IS_WORKBENCH (self), NULL); g_return_val_if_fail (IDE_IS_TASK (result), NULL); ret = ide_task_propagate_pointer (IDE_TASK (result), error); IDE_RETURN (g_steal_pointer (&ret)); }