[gnome-builder: 28/139] core: add new libide-core library
- From: Christian Hergert <chergert src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-builder: 28/139] core: add new libide-core library
- Date: Thu, 10 Jan 2019 04:19:40 +0000 (UTC)
commit a9b12408642839dd9dd2e57771901c575d2f1c34
Author: Christian Hergert <chergert redhat com>
Date: Wed Jan 9 15:38:04 2019 -0800
core: add new libide-core library
This is the center-most library that all the new static libraries will
be based upon. It provides our core objects, and some new ways to work
with things going forward.
IdeObject is now a bit of a directed graph (without cycles). It provides
a mechanism similar to GtkWidget to destroy hierarchies of objects without
relying on the GObject.dispose directly upon final-unref. Doing so helps
us ensure that we can cleanup objects even if a leak occurs somewhere and
therefore reduce how much we leak.
It also provides us a number of debugging tools going forward to get info
about the object graph.
You can encapsulate a non-IdeObject in an IdeObjectBox which allows
associating it with a tree. IdeBuffer does this so that we can attach
sub-objects to the buffer and have it cleaned up with the buffer. Various
extension points use this for auto-cleanup.
IdeNotification(s) is also used all over in the new design to track
progress of operations. It has convenient UI components in libide-gui to
make visualizing them easier in the carousel and elsewhere.
The IdeContext is now just the root IdeObject. We no longer require loading
a project into the context, which is useful in future scenarios like a
project-less editor window.
IdeObject provides more safety-checks around threading as well as built
in support for locking parts of the graph. Plugins utilizing
multi-threading should take advantage of this feature.
src/libide/{ => core}/ide-build-ident.h.in | 0
src/libide/core/ide-context-addin.c | 207 ++
src/libide/core/ide-context-addin.h | 73 +
.../{ide-global.h => core/ide-context-private.h} | 11 +-
src/libide/core/ide-context.c | 855 ++++++
src/libide/core/ide-context.h | 91 +
src/libide/{ => core}/ide-debug.h.in | 0
src/libide/core/ide-global.c | 234 ++
src/libide/core/ide-global.h | 66 +
src/libide/core/ide-log.c | 380 +++
src/libide/core/ide-log.h | 45 +
src/libide/core/ide-macros.h | 249 ++
src/libide/core/ide-notification.c | 1187 ++++++++
src/libide/core/ide-notification.h | 143 +
src/libide/core/ide-notifications.c | 516 ++++
src/libide/core/ide-notifications.h | 48 +
src/libide/core/ide-object-box.c | 289 ++
src/libide/core/ide-object-box.h | 46 +
src/libide/core/ide-object-notify.c | 114 +
src/libide/core/ide-object.c | 1367 +++++++++
src/libide/core/ide-object.h | 156 +
src/libide/core/ide-settings.c | 589 ++++
src/libide/core/ide-settings.h | 111 +
src/libide/core/ide-transfer-manager.c | 493 ++++
src/libide/core/ide-transfer-manager.h | 58 +
src/libide/core/ide-transfer.c | 522 ++++
src/libide/core/ide-transfer.h | 101 +
src/libide/{ => core}/ide-version-macros.h | 15 +-
src/libide/{ => core}/ide-version.h.in | 0
src/libide/core/libide-core.h | 43 +
src/libide/core/meson.build | 124 +
src/libide/ide-context.c | 3057 --------------------
src/libide/ide-context.h | 158 -
src/libide/ide-enums.c.in | 64 -
src/libide/ide-enums.h.in | 26 -
src/libide/ide-object.c | 877 ------
src/libide/ide-object.h | 90 -
src/libide/ide-pausable.c | 255 --
src/libide/ide-pausable.h | 56 -
src/libide/ide-service.c | 152 -
src/libide/ide-service.h | 67 -
src/libide/ide-types.h | 150 -
src/libide/ide.c | 89 -
src/libide/ide.h | 232 --
44 files changed, 8117 insertions(+), 5289 deletions(-)
---
diff --git a/src/libide/ide-build-ident.h.in b/src/libide/core/ide-build-ident.h.in
similarity index 100%
rename from src/libide/ide-build-ident.h.in
rename to src/libide/core/ide-build-ident.h.in
diff --git a/src/libide/core/ide-context-addin.c b/src/libide/core/ide-context-addin.c
new file mode 100644
index 000000000..3f3071ffc
--- /dev/null
+++ b/src/libide/core/ide-context-addin.c
@@ -0,0 +1,207 @@
+/* ide-context-addin.c
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-context-addin"
+
+#include "config.h"
+
+#include "ide-context-addin.h"
+
+G_DEFINE_INTERFACE (IdeContextAddin, ide_context_addin, G_TYPE_OBJECT)
+
+enum {
+ PROJECT_LOADED,
+ N_SIGNALS
+};
+
+static guint signals [N_SIGNALS];
+
+static void
+ide_context_addin_real_load_project_async (IdeContextAddin *addin,
+ IdeContext *context,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ g_autoptr(GTask) task = g_task_new (addin, cancellable, callback, user_data);
+ g_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+ide_context_addin_real_load_project_finish (IdeContextAddin *addin,
+ GAsyncResult *result,
+ GError **error)
+{
+ return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+ide_context_addin_default_init (IdeContextAddinInterface *iface)
+{
+ iface->load_project_async = ide_context_addin_real_load_project_async;
+ iface->load_project_finish = ide_context_addin_real_load_project_finish;
+
+ /**
+ * IdeContextAddin::project-loaded:
+ * @self: an #IdeContextAddin
+ * @context: an #IdeContext
+ *
+ * The "project-loaded" signal is emitted after a project has been loaded
+ * in the #IdeContext.
+ *
+ * You might use this to setup any runtime features that rely on the project
+ * being successfully loaded first. Every addin's
+ * ide_context_addin_load_project_async() will have been called and completed
+ * before this signal is emitted.
+ *
+ * Since: 3.32
+ */
+ signals [PROJECT_LOADED] =
+ g_signal_new ("project-loaded",
+ G_TYPE_FROM_INTERFACE (iface),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (IdeContextAddinInterface, project_loaded),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__OBJECT,
+ G_TYPE_NONE, 1, IDE_TYPE_CONTEXT);
+ g_signal_set_va_marshaller (signals [PROJECT_LOADED],
+ G_TYPE_FROM_INTERFACE (iface),
+ g_cclosure_marshal_VOID__OBJECTv);
+}
+
+/**
+ * ide_context_addin_load_project_async:
+ * @self: an #IdeContextAddin
+ * @context: an #IdeContext
+ * @cancellable: (nullable): a #GCancellable or %NULL
+ * @callback: a callback to execute upon completion
+ * @user_data: closure data for @callback
+ *
+ * Requests to load a project with the #IdeContextAddin.
+ *
+ * This function is called when the #IdeContext requests loading a project.
+ *
+ * Since: 3.32
+ */
+void
+ide_context_addin_load_project_async (IdeContextAddin *self,
+ IdeContext *context,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ g_return_if_fail (IDE_IS_CONTEXT_ADDIN (self));
+ g_return_if_fail (IDE_IS_CONTEXT (context));
+ g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+ IDE_CONTEXT_ADDIN_GET_IFACE (self)->load_project_async (self, context, cancellable, callback, user_data);
+}
+
+/**
+ * ide_context_addin_load_project_finish:
+ * @self: an #IdeContextAddin
+ * @result: a #GAsyncResult
+ * @error: a location for a #GError, or %NULL
+ *
+ * Completes a request to load a project with the #IdeContextAddin.
+ *
+ * This function will be called from the callback provided to
+ * ide_context_addin_load_project_async().
+ *
+ * Returns: %TRUE if successful; otherwise %FALSE and @error is set.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_context_addin_load_project_finish (IdeContextAddin *self,
+ GAsyncResult *result,
+ GError **error)
+{
+ g_return_val_if_fail (IDE_IS_CONTEXT_ADDIN (self), FALSE);
+ g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+ return IDE_CONTEXT_ADDIN_GET_IFACE (self)->load_project_finish (self, result, error);
+}
+
+/**
+ * ide_context_addin_load:
+ * @self: an #IdeContextAddin
+ * @context: an #IdeContext
+ *
+ * Requests that the #IdeContextAddin loads any necessary runtime features.
+ *
+ * This is called when the #IdeContext is created. If you would rather wait
+ * until a project is loaded, then use #IdeContextAddin::project-loaded to
+ * load runtime features.
+ *
+ * Since: 3.32
+ */
+void
+ide_context_addin_load (IdeContextAddin *self,
+ IdeContext *context)
+{
+ g_return_if_fail (IDE_IS_CONTEXT_ADDIN (self));
+ g_return_if_fail (IDE_IS_CONTEXT (context));
+
+ if (IDE_CONTEXT_ADDIN_GET_IFACE (self)->load)
+ IDE_CONTEXT_ADDIN_GET_IFACE (self)->load (self, context);
+}
+
+/**
+ * ide_context_addin_unload:
+ * @self: an #IdeContextAddin
+ * @context: an #IdeContext
+ *
+ * Requests that the #IdeContextAddin unloads any previously loaded
+ * resources.
+ *
+ * Since: 3.32
+ */
+void
+ide_context_addin_unload (IdeContextAddin *self,
+ IdeContext *context)
+{
+ g_return_if_fail (IDE_IS_CONTEXT_ADDIN (self));
+ g_return_if_fail (IDE_IS_CONTEXT (context));
+
+ if (IDE_CONTEXT_ADDIN_GET_IFACE (self)->unload)
+ IDE_CONTEXT_ADDIN_GET_IFACE (self)->unload (self, context);
+}
+
+/**
+ * ide_context_addin_project_loaded:
+ * @self: an #IdeContextAddin
+ * @context: an #IdeContext
+ *
+ * Emits the #IdeContextAddin::project-loaded signal.
+ *
+ * This is called when the context has completed loading a project.
+ *
+ * Since: 3.32
+ */
+void
+ide_context_addin_project_loaded (IdeContextAddin *self,
+ IdeContext *context)
+{
+ g_return_if_fail (IDE_IS_CONTEXT_ADDIN (self));
+ g_return_if_fail (IDE_IS_CONTEXT (context));
+
+ g_signal_emit (self, signals [PROJECT_LOADED], 0, context);
+}
diff --git a/src/libide/core/ide-context-addin.h b/src/libide/core/ide-context-addin.h
new file mode 100644
index 000000000..ea4e3b575
--- /dev/null
+++ b/src/libide/core/ide-context-addin.h
@@ -0,0 +1,73 @@
+/* ide-context-addin.h
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include "ide-context.h"
+#include "ide-version-macros.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_CONTEXT_ADDIN (ide_context_addin_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeContextAddin, ide_context_addin, IDE, CONTEXT_ADDIN, GObject)
+
+struct _IdeContextAddinInterface
+{
+ GTypeInterface parent_iface;
+
+ void (*load) (IdeContextAddin *self,
+ IdeContext *context);
+ void (*unload) (IdeContextAddin *self,
+ IdeContext *context);
+ void (*load_project_async) (IdeContextAddin *self,
+ IdeContext *context,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+ gboolean (*load_project_finish) (IdeContextAddin *self,
+ GAsyncResult *result,
+ GError **error);
+ void (*project_loaded) (IdeContextAddin *self,
+ IdeContext *context);
+};
+
+IDE_AVAILABLE_IN_3_32
+void ide_context_addin_load_project_async (IdeContextAddin *self,
+ IdeContext *context,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_context_addin_load_project_finish (IdeContextAddin *self,
+ GAsyncResult *result,
+ GError **error);
+IDE_AVAILABLE_IN_3_32
+void ide_context_addin_load (IdeContextAddin *self,
+ IdeContext *context);
+IDE_AVAILABLE_IN_3_32
+void ide_context_addin_unload (IdeContextAddin *self,
+ IdeContext *context);
+IDE_AVAILABLE_IN_3_32
+void ide_context_addin_project_loaded (IdeContextAddin *self,
+ IdeContext *context);
+
+G_END_DECLS
diff --git a/src/libide/ide-global.h b/src/libide/core/ide-context-private.h
similarity index 74%
rename from src/libide/ide-global.h
rename to src/libide/core/ide-context-private.h
index a9b422157..e554b9952 100644
--- a/src/libide/ide-global.h
+++ b/src/libide/core/ide-context-private.h
@@ -1,6 +1,6 @@
-/* ide-global.h
+/* ide-context-private.h
*
- * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
*
* 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
@@ -20,13 +20,10 @@
#pragma once
-#include "ide-version-macros.h"
+#include "ide-context.h"
G_BEGIN_DECLS
-IDE_AVAILABLE_IN_3_32
-const gchar *ide_get_program_name (void);
-IDE_AVAILABLE_IN_3_32
-void ide_set_program_name (const gchar *program_name);
+void _ide_context_set_has_project (IdeContext *self);
G_END_DECLS
diff --git a/src/libide/core/ide-context.c b/src/libide/core/ide-context.c
new file mode 100644
index 000000000..4f6cc3144
--- /dev/null
+++ b/src/libide/core/ide-context.c
@@ -0,0 +1,855 @@
+/* ide-context.c
+ *
+ * Copyright 2014-2019 Christian Hergert <chergert redhat com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-context"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libpeas/peas.h>
+
+#include "ide-context.h"
+#include "ide-context-private.h"
+#include "ide-context-addin.h"
+#include "ide-macros.h"
+#include "ide-notifications.h"
+
+/**
+ * SECTION:ide-context
+ * @title: IdeContext
+ * @short_description: the root object for a project
+ *
+ * The #IdeContext object is the root object for a project. Everything
+ * in a project is contained by this object.
+ *
+ * Since: 3.32
+ */
+
+struct _IdeContext
+{
+ IdeObject parent_instance;
+ PeasExtensionSet *addins;
+ gchar *project_id;
+ gchar *title;
+ GFile *workdir;
+ guint project_loaded : 1;
+};
+
+enum {
+ PROP_0,
+ PROP_PROJECT_ID,
+ PROP_TITLE,
+ PROP_WORKDIR,
+ N_PROPS
+};
+
+enum {
+ LOG,
+ N_SIGNALS
+};
+
+G_DEFINE_TYPE (IdeContext, ide_context, IDE_TYPE_OBJECT)
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+static void
+ide_context_addin_load_project_cb (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ IdeContextAddin *addin = (IdeContextAddin *)object;
+ g_autoptr(IdeContext) self = user_data;
+ g_autoptr(GError) error = NULL;
+
+ g_assert (IDE_IS_CONTEXT_ADDIN (addin));
+ g_assert (G_IS_ASYNC_RESULT (result));
+ g_assert (IDE_IS_CONTEXT (self));
+
+ if (ide_context_addin_load_project_finish (addin, result, &error))
+ ide_context_addin_project_loaded (addin, self);
+ else
+ g_warning ("%s context addin failed to load project: %s",
+ G_OBJECT_TYPE_NAME (addin), error->message);
+}
+
+static void
+ide_context_addin_added_cb (PeasExtensionSet *set,
+ PeasPluginInfo *plugin_info,
+ PeasExtension *exten,
+ gpointer user_data)
+{
+ IdeContextAddin *addin = (IdeContextAddin *)exten;
+ IdeContext *self = user_data;
+ g_autoptr(GCancellable) cancellable = NULL;
+
+ g_assert (PEAS_IS_EXTENSION_SET (set));
+ g_assert (plugin_info != NULL);
+ g_assert (IDE_IS_CONTEXT_ADDIN (addin));
+
+ /* Ignore any request during shutdown */
+ cancellable = ide_object_ref_cancellable (IDE_OBJECT (self));
+ if (g_cancellable_is_cancelled (cancellable))
+ return;
+
+ ide_context_addin_load (addin, self);
+
+ if (self->project_loaded)
+ ide_context_addin_load_project_async (addin,
+ self,
+ cancellable,
+ ide_context_addin_load_project_cb,
+ g_object_ref (self));
+}
+
+static void
+ide_context_addin_removed_cb (PeasExtensionSet *set,
+ PeasPluginInfo *plugin_info,
+ PeasExtension *exten,
+ gpointer user_data)
+{
+ IdeContextAddin *addin = (IdeContextAddin *)exten;
+ IdeContext *self = user_data;
+
+ g_assert (PEAS_IS_EXTENSION_SET (set));
+ g_assert (plugin_info != NULL);
+ g_assert (IDE_IS_CONTEXT_ADDIN (addin));
+
+ ide_context_addin_unload (addin, self);
+}
+
+static void
+ide_context_real_log (IdeContext *self,
+ GLogLevelFlags level,
+ const gchar *domain,
+ const gchar *message)
+{
+ g_log (domain, level, "%s", message);
+}
+
+static gchar *
+ide_context_repr (IdeObject *object)
+{
+ IdeContext *self = IDE_CONTEXT (object);
+
+ return g_strdup_printf ("%s workdir=\"%s\" has_project=%d",
+ G_OBJECT_TYPE_NAME (self),
+ g_file_peek_path (self->workdir),
+ self->project_loaded);
+}
+
+static void
+ide_context_constructed (GObject *object)
+{
+ IdeContext *self = (IdeContext *)object;
+
+ g_assert (IDE_IS_OBJECT (object));
+
+ self->addins = peas_extension_set_new (peas_engine_get_default (),
+ IDE_TYPE_CONTEXT_ADDIN,
+ NULL);
+
+ g_signal_connect (self->addins,
+ "extension-added",
+ G_CALLBACK (ide_context_addin_added_cb),
+ self);
+
+ g_signal_connect (self->addins,
+ "extension-removed",
+ G_CALLBACK (ide_context_addin_removed_cb),
+ self);
+
+ peas_extension_set_foreach (self->addins,
+ ide_context_addin_added_cb,
+ self);
+
+ G_OBJECT_CLASS (ide_context_parent_class)->constructed (object);
+}
+
+static void
+ide_context_destroy (IdeObject *object)
+{
+ IdeContext *self = (IdeContext *)object;
+
+ g_assert (IDE_IS_OBJECT (object));
+
+ g_clear_object (&self->addins);
+
+ IDE_OBJECT_CLASS (ide_context_parent_class)->destroy (object);
+}
+
+static void
+ide_context_finalize (GObject *object)
+{
+ IdeContext *self = (IdeContext *)object;
+
+ g_clear_object (&self->workdir);
+ g_clear_pointer (&self->project_id, g_free);
+ g_clear_pointer (&self->title, g_free);
+
+ G_OBJECT_CLASS (ide_context_parent_class)->finalize (object);
+}
+
+static void
+ide_context_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ IdeContext *self = IDE_CONTEXT (object);
+
+ switch (prop_id)
+ {
+ case PROP_PROJECT_ID:
+ g_value_take_string (value, ide_context_dup_project_id (self));
+ break;
+
+ case PROP_TITLE:
+ g_value_take_string (value, ide_context_dup_title (self));
+ break;
+
+ case PROP_WORKDIR:
+ g_value_take_object (value, ide_context_ref_workdir (self));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+ide_context_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ IdeContext *self = IDE_CONTEXT (object);
+
+ switch (prop_id)
+ {
+ case PROP_PROJECT_ID:
+ ide_context_set_project_id (self, g_value_get_string (value));
+ break;
+
+ case PROP_TITLE:
+ ide_context_set_title (self, g_value_get_string (value));
+ break;
+
+ case PROP_WORKDIR:
+ ide_context_set_workdir (self, g_value_get_object (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+ide_context_class_init (IdeContextClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ IdeObjectClass *i_object_class = IDE_OBJECT_CLASS (klass);
+
+ object_class->constructed = ide_context_constructed;
+ object_class->finalize = ide_context_finalize;
+ object_class->get_property = ide_context_get_property;
+ object_class->set_property = ide_context_set_property;
+
+ i_object_class->destroy = ide_context_destroy;
+ i_object_class->repr = ide_context_repr;
+
+ /**
+ * IdeContext:project-id:
+ *
+ * The "project-id" property is the identifier to use when creating
+ * files and folders for this project. It has a mutated form of either
+ * the directory or some other discoverable trait of the project.
+ *
+ * It has also been modified to remove spaces and other unsafe
+ * characters for file-systems.
+ *
+ * This may change during runtime, but usually only once when the
+ * project has been initialize loaded.
+ *
+ * Before any project has loaded, this is "empty" to allow flexibility
+ * for non-project use.
+ *
+ * Since: 3.32
+ */
+ properties [PROP_PROJECT_ID] =
+ g_param_spec_string ("project-id",
+ "Project Id",
+ "The project identifier used when creating files and folders",
+ "empty",
+ (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * IdeContext:title:
+ *
+ * The "title" property is a descriptive name for the project.
+ *
+ * Since: 3.32
+ */
+ properties [PROP_TITLE] =
+ g_param_spec_string ("title",
+ "Title",
+ "The title of the project",
+ NULL,
+ (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * IdeContext:workdir:
+ *
+ * The "workdir" property is the best guess at the working directory for the
+ * context. This may be discovered using a common parent if multiple files
+ * are opened without a project.
+ *
+ * Since: 3.32
+ */
+ properties [PROP_WORKDIR] =
+ g_param_spec_object ("workdir",
+ "Working Directory",
+ "The working directory for the project",
+ G_TYPE_FILE,
+ (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_properties (object_class, N_PROPS, properties);
+
+ /**
+ * IdeContext::log:
+ * @self: an #IdeContext
+ * @severity: the log severity
+ * @domain: the log domain
+ * @message: the log message
+ *
+ * This signal is emitted when a log item has been added for the context.
+ *
+ * Since: 3.32
+ */
+ signals [LOG] =
+ g_signal_new_class_handler ("log",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ G_CALLBACK (ide_context_real_log),
+ NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 3,
+ G_TYPE_UINT,
+ G_TYPE_STRING | G_SIGNAL_TYPE_STATIC_SCOPE,
+ G_TYPE_STRING | G_SIGNAL_TYPE_STATIC_SCOPE);
+}
+
+static void
+ide_context_init (IdeContext *self)
+{
+ g_autoptr(IdeNotifications) notifs = NULL;
+
+ self->workdir = g_file_new_for_path (g_get_home_dir ());
+ self->project_id = g_strdup ("empty");
+ self->title = g_strdup (_("Untitled"));
+
+ notifs = ide_notifications_new ();
+ ide_object_append (IDE_OBJECT (self), IDE_OBJECT (notifs));
+}
+
+/**
+ * ide_context_new:
+ *
+ * Creates a new #IdeContext.
+ *
+ * This only creates the context object. After creating the object you need
+ * to set a number of properties and then initialize asynchronously using
+ * g_async_initable_init_async().
+ *
+ * Returns: (transfer full): an #IdeContext
+ *
+ * Since: 3.32
+ */
+IdeContext *
+ide_context_new (void)
+{
+ return ide_object_new (IDE_TYPE_CONTEXT, NULL);
+}
+
+static void
+ide_context_peek_child_typed_cb (IdeObject *object,
+ gpointer user_data)
+{
+ struct {
+ IdeObject *ret;
+ GType type;
+ } *lookup = user_data;
+
+ g_assert (IDE_IS_MAIN_THREAD ());
+
+ if (lookup->ret != NULL)
+ return;
+
+ /* Take a borrowed instance, we're in the main thread so
+ * we can ensure it's not fully destroyed.
+ */
+ if (G_TYPE_CHECK_INSTANCE_TYPE (object, lookup->type))
+ lookup->ret = object;
+}
+
+/**
+ * ide_context_peek_child_typed:
+ * @self: a #IdeContext
+ * @type: the #GType of the child
+ *
+ * Looks for the first child matching @type, and returns it. No reference is
+ * taken to the child, so you should avoid using this except as used by
+ * compatability functions.
+ *
+ * This may only be called from the main thread or you risk the objects
+ * being finalized before your caller has a chance to reference them.
+ *
+ * Returns: (transfer none) (type IdeObject) (nullable): an #IdeObject that
+ * matches @type if successful; otherwise %NULL
+ *
+ * Since: 3.32
+ */
+gpointer
+ide_context_peek_child_typed (IdeContext *self,
+ GType type)
+{
+ struct {
+ IdeObject *ret;
+ GType type;
+ } lookup = { NULL, type };
+
+ g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+ g_return_val_if_fail (IDE_IS_CONTEXT (self), NULL);
+
+ ide_object_lock (IDE_OBJECT (self));
+ ide_object_foreach (IDE_OBJECT (self), (GFunc)ide_context_peek_child_typed_cb, &lookup);
+ ide_object_unlock (IDE_OBJECT (self));
+
+ return lookup.ret;
+}
+
+/**
+ * ide_context_dup_project_id:
+ * @self: a #IdeContext
+ *
+ * Copies the project-id and returns it to the caller.
+ *
+ * Returns: (transfer full): a project-id as a string
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_context_dup_project_id (IdeContext *self)
+{
+ gchar *ret;
+
+ g_return_val_if_fail (IDE_IS_CONTEXT (self), NULL);
+
+ ide_object_lock (IDE_OBJECT (self));
+ ret = g_strdup (self->project_id);
+ ide_object_unlock (IDE_OBJECT (self));
+
+ g_return_val_if_fail (ret != NULL, NULL);
+
+ return g_steal_pointer (&ret);
+}
+
+/**
+ * ide_context_set_project_id:
+ * @self: a #IdeContext
+ *
+ * Sets the project-id for the context.
+ *
+ * Generally, this should only be done once after loading a project.
+ *
+ * Since: 3.32
+ */
+void
+ide_context_set_project_id (IdeContext *self,
+ const gchar *project_id)
+{
+ g_return_if_fail (IDE_IS_CONTEXT (self));
+
+ if (ide_str_empty0 (project_id))
+ project_id = "empty";
+
+ ide_object_lock (IDE_OBJECT (self));
+ if (!ide_str_equal0 (self->project_id, project_id))
+ {
+ g_free (self->project_id);
+ self->project_id = g_strdup (project_id);
+ ide_object_notify_by_pspec (IDE_OBJECT (self), properties [PROP_PROJECT_ID]);
+ }
+ ide_object_unlock (IDE_OBJECT (self));
+}
+
+/**
+ * ide_context_ref_workdir:
+ * @self: a #IdeContext
+ *
+ * Gets the working-directory of the context and increments the
+ * reference count by one.
+ *
+ * Returns: (transfer full): a #GFile
+ *
+ * Since: 3.32
+ */
+GFile *
+ide_context_ref_workdir (IdeContext *self)
+{
+ GFile *ret;
+
+ g_return_val_if_fail (IDE_IS_CONTEXT (self), NULL);
+
+ ide_object_lock (IDE_OBJECT (self));
+ ret = g_object_ref (self->workdir);
+ ide_object_unlock (IDE_OBJECT (self));
+
+ return g_steal_pointer (&ret);
+}
+
+/**
+ * ide_context_set_workdir:
+ * @self: a #IdeContext
+ * @workdir: a #GFile
+ *
+ * Sets the working directory for the project.
+ *
+ * This should generally only be set once after checking out the project.
+ *
+ * In future releases, changes may be made to change this in support of
+ * git-worktrees or similar workflows.
+ *
+ * Since: 3.32
+ */
+void
+ide_context_set_workdir (IdeContext *self,
+ GFile *workdir)
+{
+ g_return_if_fail (IDE_IS_CONTEXT (self));
+ g_return_if_fail (G_IS_FILE (workdir));
+
+ ide_object_lock (IDE_OBJECT (self));
+ if (g_set_object (&self->workdir, workdir))
+ ide_object_notify_by_pspec (G_OBJECT (self), properties [PROP_WORKDIR]);
+ ide_object_unlock (IDE_OBJECT (self));
+}
+
+/**
+ * ide_context_cache_file:
+ * @self: a #IdeContext
+ * @first_part: The first part of the path
+ *
+ * Like ide_context_cache_filename() but returns a #GFile.
+ *
+ * Returns: (transfer full): a #GFile for the cache file
+ *
+ * Since: 3.32
+ */
+GFile *
+ide_context_cache_file (IdeContext *self,
+ const gchar *first_part,
+ ...)
+{
+ g_autoptr(GPtrArray) ar = NULL;
+ g_autofree gchar *path = NULL;
+ g_autofree gchar *project_id = NULL;
+ const gchar *part = first_part;
+ va_list args;
+
+ g_return_val_if_fail (IDE_IS_CONTEXT (self), NULL);
+ g_return_val_if_fail (first_part != NULL, NULL);
+
+ project_id = ide_context_dup_project_id (self);
+
+ ar = g_ptr_array_new ();
+ g_ptr_array_add (ar, (gchar *)g_get_user_cache_dir ());
+ g_ptr_array_add (ar, (gchar *)ide_get_program_name ());
+ g_ptr_array_add (ar, (gchar *)"projects");
+ g_ptr_array_add (ar, (gchar *)project_id);
+
+ va_start (args, first_part);
+ do
+ {
+ g_ptr_array_add (ar, (gchar *)part);
+ part = va_arg (args, const gchar *);
+ }
+ while (part != NULL);
+ va_end (args);
+
+ g_ptr_array_add (ar, NULL);
+
+ path = g_build_filenamev ((gchar **)ar->pdata);
+
+ return g_file_new_for_path (path);
+}
+
+/**
+ * ide_context_cache_filename:
+ * @self: a #IdeContext
+ * @first_part: the first part of the filename
+ *
+ * Creates a new filename that will be located in the projects cache directory.
+ * This makes it convenient to remove files when a project is deleted as all
+ * cache files will share a unified parent directory.
+ *
+ * The file will be located in a directory similar to
+ * ~/.cache/gnome-builder/project_name. This may change based on the value
+ * of g_get_user_cache_dir().
+ *
+ * Returns: (transfer full): A new string containing the cache filename
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_context_cache_filename (IdeContext *self,
+ const gchar *first_part,
+ ...)
+{
+ g_autofree gchar *project_id = NULL;
+ g_autofree gchar *base = NULL;
+ va_list args;
+ gchar *ret;
+
+ g_return_val_if_fail (IDE_IS_CONTEXT (self), NULL);
+ g_return_val_if_fail (first_part != NULL, NULL);
+
+ project_id = ide_context_dup_project_id (self);
+
+ g_return_val_if_fail (project_id != NULL, NULL);
+
+ base = g_build_filename (g_get_user_cache_dir (),
+ ide_get_program_name (),
+ "projects",
+ project_id,
+ first_part,
+ NULL);
+
+ va_start (args, first_part);
+ ret = g_build_filename_valist (base, &args);
+ va_end (args);
+
+ return g_steal_pointer (&ret);
+}
+
+/**
+ * ide_context_build_file:
+ * @self: a #IdeContext
+ * @path: (nullable): a path to the file
+ *
+ * Creates a new #GFile for the path.
+ *
+ * - If @path is %NULL, #IdeContext:workdir is returned.
+ * - If @path is absolute, a new #GFile to the absolute path is returned.
+ * - Otherwise, a #GFile child of #IdeContext:workdir is returned.
+ *
+ * Returns: (transfer full): a #GFile
+ *
+ * Since: 3.32
+ */
+GFile *
+ide_context_build_file (IdeContext *self,
+ const gchar *path)
+{
+ g_autoptr(GFile) ret = NULL;
+
+ g_return_val_if_fail (IDE_IS_CONTEXT (self), NULL);
+
+ if (path == NULL)
+ ret = g_file_dup (self->workdir);
+ else if (g_path_is_absolute (path))
+ ret = g_file_new_for_path (path);
+ else
+ ret = g_file_get_child (self->workdir, path);
+
+ return g_steal_pointer (&ret);
+}
+
+/**
+ * ide_context_build_filename:
+ * @self: a #IdeContext
+ * @first_part: first path part
+ *
+ * Creates a new path that starts from the working directory of the
+ * loaded project.
+ *
+ * Returns: (transfer full): a string containing the new path
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_context_build_filename (IdeContext *self,
+ const gchar *first_part,
+ ...)
+{
+ g_autoptr(GPtrArray) ar = NULL;
+ g_autoptr(GFile) workdir = NULL;
+ const gchar *part = first_part;
+ const gchar *base;
+ va_list args;
+
+ g_return_val_if_fail (IDE_IS_CONTEXT (self), NULL);
+ g_return_val_if_fail (first_part != NULL, NULL);
+
+ workdir = ide_context_ref_workdir (self);
+ base = g_file_peek_path (workdir);
+
+ ar = g_ptr_array_new ();
+
+ /* If first part is absolute, just use that as our root */
+ if (!g_path_is_absolute (first_part))
+ g_ptr_array_add (ar, (gchar *)base);
+
+ va_start (args, first_part);
+ do
+ {
+ g_ptr_array_add (ar, (gchar *)part);
+ part = va_arg (args, const gchar *);
+ }
+ while (part != NULL);
+ va_end (args);
+
+ g_ptr_array_add (ar, NULL);
+
+ return g_build_filenamev ((gchar **)ar->pdata);
+}
+
+/**
+ * ide_context_ref_project_settings:
+ * @self: a #IdeContext
+ *
+ * Gets an org.gnome.builder.project #GSettings.
+ *
+ * This creates a new #GSettings instance for the project.
+ *
+ * Returns: (transfer full): a #GSettings
+ *
+ * Since: 3.32
+ */
+GSettings *
+ide_context_ref_project_settings (IdeContext *self)
+{
+ g_autofree gchar *path = NULL;
+
+ g_return_val_if_fail (IDE_IS_CONTEXT (self), NULL);
+
+ ide_object_lock (IDE_OBJECT (self));
+ path = g_strdup_printf ("/org/gnome/builder/projects/%s/", self->project_id);
+ ide_object_unlock (IDE_OBJECT (self));
+
+ return g_settings_new_with_path ("org.gnome.builder.project", path);
+}
+
+/**
+ * ide_context_dup_title:
+ * @self: a #IdeContext
+ *
+ * Returns: (transfer full): a string containing the title
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_context_dup_title (IdeContext *self)
+{
+ gchar *ret;
+
+ g_return_val_if_fail (IDE_IS_CONTEXT (self), NULL);
+
+ ide_object_lock (IDE_OBJECT (self));
+ ret = g_strdup (self->title);
+ ide_object_unlock (IDE_OBJECT (self));
+
+ return g_steal_pointer (&ret);
+}
+
+/**
+ * ide_context_set_title:
+ * @self: an #IdeContext
+ * @title: (nullable): the title for the project or %NULL
+ *
+ * Sets the #IdeContext:title property. This is used by various
+ * components to show the user the name of the project. This may
+ * include the omnibar and the window title.
+ *
+ * Since: 3.32
+ */
+void
+ide_context_set_title (IdeContext *self,
+ const gchar *title)
+{
+ g_return_if_fail (IDE_IS_CONTEXT (self));
+
+ if (ide_str_empty0 (title))
+ title = _("Untitled");
+
+ ide_object_lock (IDE_OBJECT (self));
+ if (!ide_str_equal0 (self->title, title))
+ {
+ g_free (self->title);
+ self->title = g_strdup (title);
+ ide_object_notify_by_pspec (IDE_OBJECT (self), properties [PROP_TITLE]);
+ }
+ ide_object_unlock (IDE_OBJECT (self));
+}
+
+void
+ide_context_log (IdeContext *self,
+ GLogLevelFlags level,
+ const gchar *domain,
+ const gchar *message)
+{
+ g_assert (IDE_IS_CONTEXT (self));
+
+ g_signal_emit (self, signals [LOG], 0, level, domain, message);
+}
+
+/**
+ * ide_context_has_project:
+ * @self: a #IdeContext
+ *
+ * Checks to see if a project has been loaded in @context.
+ *
+ * Returns: %TRUE if a project has been, or is currently, loading.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_context_has_project (IdeContext *self)
+{
+ gboolean ret;
+
+ g_return_val_if_fail (IDE_IS_CONTEXT (self), FALSE);
+
+ ide_object_lock (IDE_OBJECT (self));
+ ret = self->project_loaded;
+ ide_object_unlock (IDE_OBJECT (self));
+
+ return ret;
+}
+
+void
+_ide_context_set_has_project (IdeContext *self)
+{
+ g_return_if_fail (IDE_IS_CONTEXT (self));
+
+ ide_object_lock (IDE_OBJECT (self));
+ self->project_loaded = TRUE;
+ ide_object_unlock (IDE_OBJECT (self));
+}
diff --git a/src/libide/core/ide-context.h b/src/libide/core/ide-context.h
new file mode 100644
index 000000000..065ac4563
--- /dev/null
+++ b/src/libide/core/ide-context.h
@@ -0,0 +1,91 @@
+/* ide-context.h
+ *
+ * Copyright 2014-2019 Christian Hergert <chergert redhat com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#if !defined (IDE_CORE_INSIDE) && !defined (IDE_CORE_COMPILATION)
+# error "Only <libide-core.h> can be included directly."
+#endif
+
+#include "ide-object.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_CONTEXT (ide_context_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeContext, ide_context, IDE, CONTEXT, IdeObject)
+
+IDE_AVAILABLE_IN_3_32
+IdeContext *ide_context_new (void);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_context_has_project (IdeContext *self);
+IDE_AVAILABLE_IN_3_32
+gpointer ide_context_peek_child_typed (IdeContext *self,
+ GType type);
+IDE_AVAILABLE_IN_3_32
+gchar *ide_context_dup_project_id (IdeContext *self);
+IDE_AVAILABLE_IN_3_32
+void ide_context_set_project_id (IdeContext *self,
+ const gchar *project_id);
+IDE_AVAILABLE_IN_3_32
+gchar *ide_context_dup_title (IdeContext *self);
+IDE_AVAILABLE_IN_3_32
+void ide_context_set_title (IdeContext *self,
+ const gchar *title);
+IDE_AVAILABLE_IN_3_32
+GFile *ide_context_ref_workdir (IdeContext *self);
+IDE_AVAILABLE_IN_3_32
+void ide_context_set_workdir (IdeContext *self,
+ GFile *workdir);
+IDE_AVAILABLE_IN_3_32
+GFile *ide_context_build_file (IdeContext *self,
+ const gchar *path);
+IDE_AVAILABLE_IN_3_32
+gchar *ide_context_build_filename (IdeContext *self,
+ const gchar *first_part,
+ ...) G_GNUC_NULL_TERMINATED;
+IDE_AVAILABLE_IN_3_32
+GFile *ide_context_cache_file (IdeContext *self,
+ const gchar *first_part,
+ ...) G_GNUC_NULL_TERMINATED;
+IDE_AVAILABLE_IN_3_32
+gchar *ide_context_cache_filename (IdeContext *self,
+ const gchar *first_part,
+ ...) G_GNUC_NULL_TERMINATED;
+IDE_AVAILABLE_IN_3_32
+GSettings *ide_context_ref_project_settings (IdeContext *self);
+IDE_AVAILABLE_IN_3_32
+IdeContext *ide_object_ref_context (IdeObject *self);
+IDE_AVAILABLE_IN_3_32
+IdeContext *ide_object_get_context (IdeObject *object);
+IDE_AVAILABLE_IN_3_32
+void ide_object_set_context (IdeObject *object,
+ IdeContext *context);
+IDE_AVAILABLE_IN_3_32
+void ide_context_log (IdeContext *self,
+ GLogLevelFlags level,
+ const gchar *domain,
+ const gchar *message);
+
+#define ide_context_warning(instance, format, ...) \
+ ide_object_log(instance, G_LOG_LEVEL_WARNING, G_LOG_DOMAIN, format __VA_OPT__(,) __VA_ARGS__)
+
+G_END_DECLS
diff --git a/src/libide/ide-debug.h.in b/src/libide/core/ide-debug.h.in
similarity index 100%
rename from src/libide/ide-debug.h.in
rename to src/libide/core/ide-debug.h.in
diff --git a/src/libide/core/ide-global.c b/src/libide/core/ide-global.c
new file mode 100644
index 000000000..46133cc56
--- /dev/null
+++ b/src/libide/core/ide-global.c
@@ -0,0 +1,234 @@
+/* ide-global.c
+ *
+ * Copyright 2014-2019 Christian Hergert <chergert redhat com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-global"
+
+#include "config.h"
+
+#include <gio/gio.h>
+#include <glib/gi18n.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/user.h>
+#include <sys/utsname.h>
+#include <unistd.h>
+
+#include "../../gconstructor.h"
+
+#include "ide-macros.h"
+#include "ide-global.h"
+
+static GThread *main_thread;
+static const gchar *application_id = "org.gnome.Builder";
+static IdeProcessKind kind = IDE_PROCESS_KIND_HOST;
+
+#if defined (G_HAS_CONSTRUCTORS)
+# ifdef G_DEFINE_CONSTRUCTOR_NEEDS_PRAGMA
+# pragma G_DEFINE_CONSTRUCTOR_PRAGMA_ARGS(ide_init_ctor)
+# endif
+G_DEFINE_CONSTRUCTOR(ide_init_ctor)
+#else
+# error Your platform/compiler is missing constructor support
+#endif
+
+static void
+ide_init_ctor (void)
+{
+ main_thread = g_thread_self ();
+
+ if (g_file_test ("/.flatpak-info", G_FILE_TEST_EXISTS))
+ kind = IDE_PROCESS_KIND_FLATPAK;
+}
+
+/**
+ * ide_get_main_thread
+ *
+ * Gets #GThread of the main thread.
+ *
+ * Generally this is used by macros to determine what thread they code is
+ * currently running within.
+ *
+ * Returns: (transfer none): a #GThread
+ *
+ * Since: 3.32
+ */
+GThread *
+ide_get_main_thread (void)
+{
+ return main_thread;
+}
+
+/**
+ * ide_get_process_kind:
+ *
+ * Gets the kind of process we're running as.
+ *
+ * Returns: an #IdeProcessKind
+ *
+ * Since: 3.32
+ */
+IdeProcessKind
+ide_get_process_kind (void)
+{
+ return kind;
+}
+
+const gchar *
+ide_get_application_id (void)
+{
+ return application_id;
+}
+
+/**
+ * ide_set_application_id:
+ * @app_id: the application id
+ *
+ * Sets the application id that will be used.
+ *
+ * This must be set at application startup before any GApplication
+ * has connected to the D-Bus.
+ *
+ * The default is "org.gnome.Builder".
+ *
+ * Since: 3.32
+ */
+void
+ide_set_application_id (const gchar *app_id)
+{
+ g_return_if_fail (app_id != NULL);
+
+ application_id = g_intern_string (app_id);
+}
+
+const gchar *
+ide_get_program_name (void)
+{
+ return "gnome-builder";
+}
+
+gchar *
+ide_create_host_triplet (const gchar *arch,
+ const gchar *kernel,
+ const gchar *system)
+{
+ if (arch == NULL || kernel == NULL)
+ return g_strdup (ide_get_system_type ());
+ else if (system == NULL)
+ return g_strdup_printf ("%s-%s", arch, kernel);
+ else
+ return g_strdup_printf ("%s-%s-%s", arch, kernel, system);
+}
+
+const gchar *
+ide_get_system_type (void)
+{
+ static gchar *system_type;
+ g_autofree gchar *os_lower = NULL;
+ const gchar *machine = NULL;
+ struct utsname u;
+
+ if (system_type != NULL)
+ return system_type;
+
+ if (uname (&u) < 0)
+ return g_strdup ("unknown");
+
+ os_lower = g_utf8_strdown (u.sysname, -1);
+
+ /* config.sub doesn't accept amd64-OS */
+ machine = strcmp (u.machine, "amd64") ? u.machine : "x86_64";
+
+ /*
+ * TODO: Clearly we want to discover "gnu", but that should be just fine
+ * for a default until we try to actually run on something non-gnu.
+ * Which seems unlikely at the moment. If you run FreeBSD, you can
+ * probably fix this for me :-) And while you're at it, make the
+ * uname() call more portable.
+ */
+
+#ifdef __GLIBC__
+ system_type = g_strdup_printf ("%s-%s-%s", machine, os_lower, "gnu");
+#else
+ system_type = g_strdup_printf ("%s-%s", machine, os_lower);
+#endif
+
+ return system_type;
+}
+
+gchar *
+ide_get_system_arch (void)
+{
+ struct utsname u;
+ const char *machine;
+
+ if (uname (&u) < 0)
+ return g_strdup ("unknown");
+
+ /* config.sub doesn't accept amd64-OS */
+ machine = strcmp (u.machine, "amd64") ? u.machine : "x86_64";
+
+ return g_strdup (machine);
+}
+
+gsize
+ide_get_system_page_size (void)
+{
+ return sysconf (_SC_PAGE_SIZE);
+}
+
+static gchar *
+get_base_path (const gchar *name)
+{
+ g_autoptr(GKeyFile) keyfile = g_key_file_new ();
+
+ if (g_key_file_load_from_file (keyfile, "/.flatpak-info", 0, NULL))
+ return g_key_file_get_string (keyfile, "Instance", name, NULL);
+
+ return NULL;
+}
+
+/**
+ * ide_get_relocatable_path:
+ * @path: a relocatable path
+ *
+ * Gets the path to a resource that may be relocatable at runtime.
+ *
+ * Returns: (transfer full): a new string containing the path
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_get_relocatable_path (const gchar *path)
+{
+ static gchar *base_path;
+
+ if G_UNLIKELY (base_path == NULL)
+ base_path = get_base_path ("app-path");
+
+ return g_build_filename (base_path, path, NULL);
+}
+
+const gchar *
+ide_gettext (const gchar *message)
+{
+ if (message != NULL)
+ return g_dgettext (GETTEXT_PACKAGE, message);
+ return NULL;
+}
diff --git a/src/libide/core/ide-global.h b/src/libide/core/ide-global.h
new file mode 100644
index 000000000..28adbdfb8
--- /dev/null
+++ b/src/libide/core/ide-global.h
@@ -0,0 +1,66 @@
+/* ide-global.h
+ *
+ * Copyright 2014-2019 Christian Hergert <chergert redhat com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#if !defined (IDE_CORE_INSIDE) && !defined (IDE_CORE_COMPILATION)
+# error "Only <libide-core.h> can be included directly."
+#endif
+
+#include <glib.h>
+
+#include "ide-version-macros.h"
+
+G_BEGIN_DECLS
+
+typedef enum
+{
+ IDE_PROCESS_KIND_HOST = 0,
+ IDE_PROCESS_KIND_FLATPAK = 1,
+} IdeProcessKind;
+
+#define ide_is_flatpak() (ide_get_process_kind() == IDE_PROCESS_KIND_FLATPAK)
+
+IDE_AVAILABLE_IN_3_32
+const gchar *ide_gettext (const gchar *message);
+IDE_AVAILABLE_IN_3_32
+GThread *ide_get_main_thread (void);
+IDE_AVAILABLE_IN_3_32
+IdeProcessKind ide_get_process_kind (void);
+IDE_AVAILABLE_IN_3_32
+const gchar *ide_get_application_id (void);
+IDE_AVAILABLE_IN_3_32
+void ide_set_application_id (const gchar *app_id);
+IDE_AVAILABLE_IN_3_32
+const gchar *ide_get_program_name (void);
+IDE_AVAILABLE_IN_3_32
+gchar *ide_get_system_arch (void);
+IDE_AVAILABLE_IN_3_32
+const gchar *ide_get_system_type (void);
+IDE_AVAILABLE_IN_3_32
+gchar *ide_create_host_triplet (const gchar *arch,
+ const gchar *kernel,
+ const gchar *system);
+IDE_AVAILABLE_IN_3_32
+gsize ide_get_system_page_size (void) G_GNUC_CONST;
+IDE_AVAILABLE_IN_3_32
+gchar *ide_get_relocatable_path (const gchar *path);
+
+G_END_DECLS
diff --git a/src/libide/core/ide-log.c b/src/libide/core/ide-log.c
new file mode 100644
index 000000000..69a992df6
--- /dev/null
+++ b/src/libide/core/ide-log.c
@@ -0,0 +1,380 @@
+/* ide-log.c
+ *
+ * Copyright 2015-2019 Christian Hergert <chergert redhat com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-log"
+
+#include "config.h"
+
+#ifndef _GNU_SOURCE
+# define _GNU_SOURCE
+#endif
+
+#ifdef __linux__
+# include <sys/types.h>
+# include <sys/syscall.h>
+#endif
+
+#include <glib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "ide-debug.h"
+#include "ide-log.h"
+#include "ide-macros.h"
+
+/**
+ * SECTION:ide-log
+ * @title: Logging
+ * @short_description: Standard logging facilities for Builder
+ *
+ * This module manages the logging facilities in Builder. It involves
+ * formatting the standard output and error logs as well as filtering
+ * logs based on their #GLogLevelFlags.
+ *
+ * Generally speaking, you want to continue using the GLib logging API
+ * such as g_debug(), g_warning(), g_message(), or g_error(). These functions
+ * will redirect their logging information to this module who will format
+ * the log message appropriately.
+ *
+ * If you are writing code for Builder that is in C, you want to ensure you
+ * set the %G_LOG_DOMAIN define at the top of your file (after the license)
+ * as such:
+ *
+ * ## Logging from C
+ *
+ * |[
+ * #define G_LOG_DOMAIN "my-module"
+ * ...
+ * static void
+ * some_function (void)
+ * {
+ * g_debug ("Use normal logging facilities");
+ * }
+ * ]|
+ *
+ * ## Logging from Python
+ *
+ * If you are writing an extension to Builder from Python, you may use the
+ * helper functions provided by our Ide python module.
+ *
+ * |[<!-- Language="py" -->
+ * from gi.repository import Ide
+ *
+ * Ide.warning("This is a warning")
+ * Ide.debug("This is a debug")
+ * Ide.error("This is a fatal error")
+ * ]|
+ *
+ * Since: 3.32
+ */
+
+typedef const gchar *(*IdeLogLevelStrFunc) (GLogLevelFlags log_level);
+
+static GPtrArray *channels;
+static GLogFunc last_handler;
+static int log_verbosity;
+static IdeLogLevelStrFunc log_level_str_func;
+static gchar *domains;
+static gboolean has_domains;
+
+G_LOCK_DEFINE (channels_lock);
+
+/**
+ * ide_log_get_thread:
+ *
+ * Retrieves task id for the current thread. This is only supported on Linux.
+ * On other platforms, the current thread pointer is retrieved.
+ *
+ * Returns: The task id.
+ *
+ * Since: 3.32
+ */
+static inline gint
+ide_log_get_thread (void)
+{
+#ifdef __linux__
+ return (gint) syscall (SYS_gettid);
+#else
+ return GPOINTER_TO_INT (g_thread_self ());
+#endif /* __linux__ */
+}
+
+/**
+ * ide_log_level_str:
+ * @log_level: a #GLogLevelFlags.
+ *
+ * Retrieves the log level as a string.
+ *
+ * Returns: A string which shouldn't be modified or freed.
+ * Side effects: None.
+ *
+ * Since: 3.32
+ */
+static const gchar *
+ide_log_level_str (GLogLevelFlags log_level)
+{
+ switch (((gulong)log_level & G_LOG_LEVEL_MASK))
+ {
+ case G_LOG_LEVEL_ERROR: return " ERROR";
+ case G_LOG_LEVEL_CRITICAL: return "CRITICAL";
+ case G_LOG_LEVEL_WARNING: return " WARNING";
+ case G_LOG_LEVEL_MESSAGE: return " MESSAGE";
+ case G_LOG_LEVEL_INFO: return " INFO";
+ case G_LOG_LEVEL_DEBUG: return " DEBUG";
+ case IDE_LOG_LEVEL_TRACE: return " TRACE";
+
+ default:
+ return " UNKNOWN";
+ }
+}
+
+static const gchar *
+ide_log_level_str_with_color (GLogLevelFlags log_level)
+{
+ switch (((gulong)log_level & G_LOG_LEVEL_MASK))
+ {
+ case G_LOG_LEVEL_ERROR: return " \033[1;31mERROR\033[0m";
+ case G_LOG_LEVEL_CRITICAL: return "\033[1;35mCRITICAL\033[0m";
+ case G_LOG_LEVEL_WARNING: return " \033[1;33mWARNING\033[0m";
+ case G_LOG_LEVEL_MESSAGE: return " \033[1;32mMESSAGE\033[0m";
+ case G_LOG_LEVEL_INFO: return " \033[1;32mINFO\033[0m";
+ case G_LOG_LEVEL_DEBUG: return " \033[1;32mDEBUG\033[0m";
+ case IDE_LOG_LEVEL_TRACE: return " \033[1;36mTRACE\033[0m";
+
+ default:
+ return " UNKNOWN";
+ }
+}
+
+/**
+ * ide_log_write_to_channel:
+ * @channel: a #GIOChannel.
+ * @message: A string log message.
+ *
+ * Writes @message to @channel and flushes the channel.
+ *
+ * Since: 3.32
+ */
+static void
+ide_log_write_to_channel (GIOChannel *channel,
+ const gchar *message)
+{
+ g_io_channel_write_chars (channel, message, -1, NULL, NULL);
+ g_io_channel_flush (channel, NULL);
+}
+
+/**
+ * ide_log_handler:
+ * @log_domain: A string containing the log section.
+ * @log_level: a #GLogLevelFlags.
+ * @message: The string message.
+ * @user_data: User data supplied to g_log_set_default_handler().
+ *
+ * Default log handler that will dispatch log messages to configured logging
+ * destinations.
+ *
+ * Since: 3.32
+ */
+static void
+ide_log_handler (const gchar *log_domain,
+ GLogLevelFlags log_level,
+ const gchar *message,
+ gpointer user_data)
+{
+ GTimeVal tv;
+ struct tm tt;
+ time_t t;
+ const gchar *level;
+ gchar ftime[32];
+ gchar *buffer;
+ gboolean is_debug_level;
+
+ if (G_LIKELY (channels->len))
+ {
+ is_debug_level = (log_level == G_LOG_LEVEL_DEBUG || log_level == IDE_LOG_LEVEL_TRACE);
+ if (is_debug_level &&
+ has_domains &&
+ (log_domain == NULL || strstr (domains, log_domain) == NULL))
+ return;
+
+ switch ((int)log_level)
+ {
+ case G_LOG_LEVEL_MESSAGE:
+ if (log_verbosity < 1)
+ return;
+ break;
+
+ case G_LOG_LEVEL_INFO:
+ if (log_verbosity < 2)
+ return;
+ break;
+
+ case G_LOG_LEVEL_DEBUG:
+ if (log_verbosity < 3)
+ return;
+ break;
+
+ case IDE_LOG_LEVEL_TRACE:
+ if (log_verbosity < 4)
+ return;
+ break;
+
+ default:
+ break;
+ }
+
+ level = log_level_str_func (log_level);
+ g_get_current_time (&tv);
+ t = (time_t) tv.tv_sec;
+ tt = *localtime (&t);
+ strftime (ftime, sizeof (ftime), "%H:%M:%S", &tt);
+ buffer = g_strdup_printf ("%s.%04ld %40s[% 5d]: %s: %s\n",
+ ftime,
+ tv.tv_usec / 1000,
+ log_domain,
+ ide_log_get_thread (),
+ level,
+ message);
+ G_LOCK (channels_lock);
+ g_ptr_array_foreach (channels, (GFunc) ide_log_write_to_channel, buffer);
+ G_UNLOCK (channels_lock);
+ g_free (buffer);
+ }
+}
+
+/**
+ * ide_log_init:
+ * @stdout_: Indicates logging should be written to stdout.
+ * @filename: An optional file in which to store logs.
+ *
+ * Initializes the logging subsystem. This should be called from
+ * the application entry point only. Secondary calls to this function
+ * will do nothing.
+ *
+ * Since: 3.32
+ */
+void
+ide_log_init (gboolean stdout_,
+ const gchar *filename)
+{
+ static gsize initialized = FALSE;
+ GIOChannel *channel;
+
+ if (g_once_init_enter (&initialized))
+ {
+ log_level_str_func = ide_log_level_str;
+ channels = g_ptr_array_new ();
+ if (filename)
+ {
+ channel = g_io_channel_new_file (filename, "a", NULL);
+ g_ptr_array_add (channels, channel);
+ }
+ if (stdout_)
+ {
+ channel = g_io_channel_unix_new (STDOUT_FILENO);
+ g_ptr_array_add (channels, channel);
+ if ((filename == NULL) && isatty (STDOUT_FILENO))
+ log_level_str_func = ide_log_level_str_with_color;
+ }
+
+ domains = g_strdup (g_getenv ("G_MESSAGES_DEBUG"));
+ if (!ide_str_empty0 (domains) && strcmp (domains, "all") != 0)
+ has_domains = TRUE;
+
+ g_log_set_default_handler (ide_log_handler, NULL);
+ g_once_init_leave (&initialized, TRUE);
+ }
+}
+
+/**
+ * ide_log_shutdown:
+ *
+ * Cleans up after the logging subsystem and restores the original
+ * log handler.
+ *
+ * Since: 3.32
+ */
+void
+ide_log_shutdown (void)
+{
+ if (last_handler)
+ {
+ g_log_set_default_handler (last_handler, NULL);
+ last_handler = NULL;
+ }
+
+ g_clear_pointer (&domains, g_free);
+}
+
+/**
+ * ide_log_increase_verbosity:
+ *
+ * Increases the amount of logging that will occur. By default, only
+ * warning and above will be displayed.
+ *
+ * Calling this once will cause %G_LOG_LEVEL_MESSAGE to be displayed.
+ * Calling this twice will cause %G_LOG_LEVEL_INFO to be displayed.
+ * Calling this thrice will cause %G_LOG_LEVEL_DEBUG to be displayed.
+ * Calling this four times will cause %IDE_LOG_LEVEL_TRACE to be displayed.
+ *
+ * Note that many DEBUG and TRACE level log messages are only compiled into
+ * debug builds, and therefore will not be available in release builds.
+ *
+ * This method is meant to be called for every -v provided on the command
+ * line.
+ *
+ * Calling this method more than four times is acceptable.
+ *
+ * Since: 3.32
+ */
+void
+ide_log_increase_verbosity (void)
+{
+ log_verbosity++;
+}
+
+/**
+ * ide_log_get_verbosity:
+ *
+ * Retrieves the log verbosity, which is the number of times -v was
+ * provided on the command line.
+ *
+ * Since: 3.32
+ */
+gint
+ide_log_get_verbosity (void)
+{
+ return log_verbosity;
+}
+
+/**
+ * ide_log_set_verbosity:
+ *
+ * Sets the explicit verbosity. Generally you want to use
+ * ide_log_increase_verbosity() instead of this function.
+ *
+ * Since: 3.32
+ */
+void
+ide_log_set_verbosity (gint level)
+{
+ log_verbosity = level;
+}
diff --git a/src/libide/core/ide-log.h b/src/libide/core/ide-log.h
new file mode 100644
index 000000000..c8052dca4
--- /dev/null
+++ b/src/libide/core/ide-log.h
@@ -0,0 +1,45 @@
+/* ide-log.h
+ *
+ * Copyright 2015-2019 Christian Hergert <chergert redhat com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#if !defined (IDE_CORE_INSIDE) && !defined (IDE_CORE_COMPILATION)
+# error "Only <libide-core.h> can be included directly."
+#endif
+
+#include <glib.h>
+
+#include "ide-version-macros.h"
+
+G_BEGIN_DECLS
+
+IDE_AVAILABLE_IN_3_32
+void ide_log_init (gboolean stdout_,
+ const gchar *filename);
+IDE_AVAILABLE_IN_3_32
+void ide_log_increase_verbosity (void);
+IDE_AVAILABLE_IN_3_32
+gint ide_log_get_verbosity (void);
+IDE_AVAILABLE_IN_3_32
+void ide_log_set_verbosity (gint level);
+IDE_AVAILABLE_IN_3_32
+void ide_log_shutdown (void);
+
+G_END_DECLS
diff --git a/src/libide/core/ide-macros.h b/src/libide/core/ide-macros.h
new file mode 100644
index 000000000..6519df04b
--- /dev/null
+++ b/src/libide/core/ide-macros.h
@@ -0,0 +1,249 @@
+/* ide-macros.h
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#if !defined (IDE_CORE_INSIDE) && !defined (IDE_CORE_COMPILATION)
+# error "Only <libide-core.h> can be included directly."
+#endif
+
+#ifndef __GI_SCANNER__
+
+#include <glib.h>
+
+#include "ide-global.h"
+#include "ide-object.h"
+#include "ide-version-macros.h"
+
+G_BEGIN_DECLS
+
+#define ide_str_empty0(str) (!(str) || !*(str))
+#define ide_str_equal0(str1,str2) (g_strcmp0(str1,str2)==0)
+#define ide_strv_empty0(strv) (((strv) == NULL) || ((strv)[0] == NULL))
+#define ide_set_string(ptr,str) (ide_take_string((ptr), g_strdup(str)))
+
+#define ide_clear_param(pptr, pval) \
+ G_STMT_START { if (pptr) { *(pptr) = pval; }; } G_STMT_END
+
+#define IDE_IS_MAIN_THREAD() (g_thread_self() == ide_get_main_thread())
+
+#define IDE_PTR_ARRAY_CLEAR_FREE_FUNC(ar) \
+ IDE_PTR_ARRAY_SET_FREE_FUNC(ar, NULL)
+#define IDE_PTR_ARRAY_SET_FREE_FUNC(ar, func) \
+ G_STMT_START { \
+ if ((ar) != NULL) \
+ g_ptr_array_set_free_func ((ar), (GDestroyNotify)(func)); \
+ } G_STMT_END
+#define IDE_PTR_ARRAY_STEAL_FULL(arptr) \
+ ({ IDE_PTR_ARRAY_CLEAR_FREE_FUNC (*(arptr)); \
+ g_steal_pointer ((arptr)); })
+
+static inline void
+_g_object_unref0 (gpointer instance)
+{
+ if (instance)
+ g_object_unref (instance);
+}
+
+static inline gboolean
+ide_take_string (gchar **ptr,
+ gchar *str)
+{
+ if (*ptr != str)
+ {
+ g_free (*ptr);
+ *ptr = str;
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static inline void
+ide_clear_string (gchar **ptr)
+{
+ g_free (*ptr);
+ *ptr = NULL;
+}
+
+static inline GList *
+_g_list_insert_before_link (GList *list,
+ GList *sibling,
+ GList *link_)
+{
+ g_return_val_if_fail (link_ != NULL, list);
+
+ if (!list)
+ {
+ g_return_val_if_fail (sibling == NULL, list);
+ return link_;
+ }
+ else if (sibling)
+ {
+ link_->prev = sibling->prev;
+ link_->next = sibling;
+ sibling->prev = link_;
+ if (link_->prev)
+ {
+ link_->prev->next = link_;
+ return list;
+ }
+ else
+ {
+ g_return_val_if_fail (sibling == list, link_);
+ return link_;
+ }
+ }
+ else
+ {
+ GList *last;
+
+ last = list;
+ while (last->next)
+ last = last->next;
+
+ last->next = link_;
+ last->next->prev = last;
+ last->next->next = NULL;
+
+ return list;
+ }
+}
+
+static inline void
+_g_queue_insert_before_link (GQueue *queue,
+ GList *sibling,
+ GList *link_)
+{
+ g_return_if_fail (queue != NULL);
+ g_return_if_fail (link_ != NULL);
+
+ if G_UNLIKELY (sibling == NULL)
+ {
+ /* We don't use g_list_insert_before_link() with a NULL sibling because it
+ * would be a O(n) operation and we would need to update manually the tail
+ * pointer.
+ */
+ g_queue_push_tail_link (queue, link_);
+ }
+ else
+ {
+ queue->head = _g_list_insert_before_link (queue->head, sibling, link_);
+ queue->length++;
+ }
+}
+
+static inline void
+_g_queue_insert_after_link (GQueue *queue,
+ GList *sibling,
+ GList *link_)
+{
+ g_return_if_fail (queue != NULL);
+ g_return_if_fail (link_ != NULL);
+
+ if (sibling == NULL)
+ g_queue_push_head_link (queue, link_);
+ else
+ _g_queue_insert_before_link (queue, sibling->next, link_);
+}
+
+static inline GPtrArray *
+_g_ptr_array_copy_objects (GPtrArray *ar)
+{
+ if (ar != NULL)
+ {
+ GPtrArray *copy = g_ptr_array_new_full (ar->len, g_object_unref);
+ for (guint i = 0; i < ar->len; i++)
+ g_ptr_array_add (copy, g_object_ref (g_ptr_array_index (ar, i)));
+ return g_steal_pointer (©);
+ }
+
+ return NULL;
+}
+
+static void
+ide_object_unref_and_destroy (IdeObject *object)
+{
+ if (object != NULL)
+ {
+ if (!ide_object_in_destruction (object))
+ ide_object_destroy (object);
+ g_object_unref (object);
+ }
+}
+
+typedef GPtrArray IdeObjectArray;
+
+static inline void
+ide_clear_and_destroy_object (gpointer pptr)
+{
+ IdeObject **ptr = pptr;
+
+ if (ptr && *ptr)
+ {
+ if (!ide_object_in_destruction (*ptr))
+ ide_object_destroy (*ptr);
+ g_clear_object (ptr);
+ }
+}
+
+static inline GPtrArray *
+ide_object_array_new (void)
+{
+ return g_ptr_array_new_with_free_func ((GDestroyNotify)ide_object_unref_and_destroy);
+}
+
+static inline gpointer
+ide_object_array_steal_index (IdeObjectArray *array,
+ guint position)
+{
+ gpointer ret = g_ptr_array_index (array, position);
+ g_ptr_array_index (array, position) = NULL;
+ g_ptr_array_remove_index (array, position);
+ return ret;
+}
+
+static inline gpointer
+ide_object_array_index (IdeObjectArray *array,
+ guint position)
+{
+ return g_ptr_array_index (array, position);
+}
+
+static inline void
+ide_object_array_add (IdeObjectArray *ar,
+ gpointer instance)
+{
+ g_ptr_array_add (ar, g_object_ref (IDE_OBJECT (instance)));
+}
+
+static inline void
+ide_object_array_unref (IdeObjectArray *ar)
+{
+ g_ptr_array_unref (ar);
+}
+
+#define IDE_OBJECT_ARRAY_STEAL_FULL(ar) IDE_PTR_ARRAY_STEAL_FULL(ar)
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (IdeObjectArray, g_ptr_array_unref)
+
+G_END_DECLS
+
+#endif /* __GI_SCANNER__ */
diff --git a/src/libide/core/ide-notification.c b/src/libide/core/ide-notification.c
new file mode 100644
index 000000000..f41c4ed29
--- /dev/null
+++ b/src/libide/core/ide-notification.c
@@ -0,0 +1,1187 @@
+/* ide-notification.c
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-notification"
+
+#include "config.h"
+
+#include "ide-macros.h"
+#include "ide-notification.h"
+#include "ide-notifications.h"
+
+typedef struct
+{
+ gchar *id;
+ gchar *title;
+ gchar *body;
+ GIcon *icon;
+ gchar *default_action;
+ GVariant *default_target;
+ GArray *buttons;
+ gdouble progress;
+ gint priority;
+ guint has_progress : 1;
+ guint progress_is_imprecise : 1;
+ guint urgent : 1;
+} IdeNotificationPrivate;
+
+typedef struct
+{
+ gchar *label;
+ GIcon *icon;
+ gchar *action;
+ GVariant *target;
+} Button;
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdeNotification, ide_notification, IDE_TYPE_OBJECT)
+
+enum {
+ PROP_0,
+ PROP_BODY,
+ PROP_HAS_PROGRESS,
+ PROP_ICON,
+ PROP_ICON_NAME,
+ PROP_ID,
+ PROP_PRIORITY,
+ PROP_PROGRESS,
+ PROP_PROGRESS_IS_IMPRECISE,
+ PROP_TITLE,
+ PROP_URGENT,
+ N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+clear_button (Button *button)
+{
+ g_clear_pointer (&button->label, g_free);
+ g_clear_pointer (&button->action, g_free);
+ g_clear_pointer (&button->target, g_variant_unref);
+ g_clear_object (&button->icon);
+}
+
+static void
+ide_notification_destroy (IdeObject *object)
+{
+ IdeNotification *self = (IdeNotification *)object;
+ IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+
+ g_clear_pointer (&priv->title, g_free);
+ g_clear_pointer (&priv->body, g_free);
+ g_clear_pointer (&priv->default_action, g_free);
+ g_clear_pointer (&priv->default_target, g_variant_unref);
+ g_clear_pointer (&priv->buttons, g_array_unref);
+ g_clear_object (&priv->icon);
+
+ IDE_OBJECT_CLASS (ide_notification_parent_class)->destroy (object);
+}
+
+static void
+ide_notification_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ IdeNotification *self = IDE_NOTIFICATION (object);
+
+ switch (prop_id)
+ {
+ case PROP_BODY:
+ g_value_take_string (value, ide_notification_dup_body (self));
+ break;
+
+ case PROP_HAS_PROGRESS:
+ g_value_set_boolean (value, ide_notification_get_has_progress (self));
+ break;
+
+ case PROP_ICON:
+ g_value_take_object (value, ide_notification_ref_icon (self));
+ break;
+
+ case PROP_ID:
+ g_value_take_string (value, ide_notification_dup_id (self));
+ break;
+
+ case PROP_PRIORITY:
+ g_value_set_int (value, ide_notification_get_priority (self));
+ break;
+
+ case PROP_PROGRESS:
+ g_value_set_double (value, ide_notification_get_progress (self));
+ break;
+
+ case PROP_PROGRESS_IS_IMPRECISE:
+ g_value_set_boolean (value, ide_notification_get_progress_is_imprecise (self));
+ break;
+
+ case PROP_TITLE:
+ g_value_take_string (value, ide_notification_dup_title (self));
+ break;
+
+ case PROP_URGENT:
+ g_value_set_boolean (value, ide_notification_get_urgent (self));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+ide_notification_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ IdeNotification *self = IDE_NOTIFICATION (object);
+
+ switch (prop_id)
+ {
+ case PROP_BODY:
+ ide_notification_set_body (self, g_value_get_string (value));
+ break;
+
+ case PROP_HAS_PROGRESS:
+ ide_notification_set_has_progress (self, g_value_get_boolean (value));
+ break;
+
+ case PROP_ICON:
+ ide_notification_set_icon (self, g_value_get_object (value));
+ break;
+
+ case PROP_ICON_NAME:
+ ide_notification_set_icon_name (self, g_value_get_string (value));
+ break;
+
+ case PROP_ID:
+ ide_notification_set_id (self, g_value_get_string (value));
+ break;
+
+ case PROP_PRIORITY:
+ ide_notification_set_priority (self, g_value_get_int (value));
+ break;
+
+ case PROP_PROGRESS:
+ ide_notification_set_progress (self, g_value_get_double (value));
+ break;
+
+ case PROP_PROGRESS_IS_IMPRECISE:
+ ide_notification_set_progress_is_imprecise (self, g_value_get_boolean (value));
+ break;
+
+ case PROP_TITLE:
+ ide_notification_set_title (self, g_value_get_string (value));
+ break;
+
+ case PROP_URGENT:
+ ide_notification_set_urgent (self, g_value_get_boolean (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+ide_notification_class_init (IdeNotificationClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ IdeObjectClass *ide_object_class = IDE_OBJECT_CLASS (klass);
+
+ object_class->get_property = ide_notification_get_property;
+ object_class->set_property = ide_notification_set_property;
+
+ ide_object_class->destroy = ide_notification_destroy;
+
+ /**
+ * IdeNotification:body:
+ *
+ * The "body" property is the main body of text for the notification.
+ * Not all notifications need this, but more complex notifications might.
+ *
+ * Since: 3.32
+ */
+ properties [PROP_BODY] =
+ g_param_spec_string ("body",
+ "Body",
+ "The body of the notification",
+ NULL,
+ (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * IdeNotification:has-progress:
+ *
+ * The "has-progress" property denotes the notification will receive
+ * updates to the #IdeNotification:progress property.
+ *
+ * Since: 3.32
+ */
+ properties [PROP_HAS_PROGRESS] =
+ g_param_spec_boolean ("has-progress",
+ "Has Progress",
+ "If the notification supports progress updates",
+ FALSE,
+ (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * IdeNotification:icon:
+ *
+ * The "icon" property is an optional icon that may be shown next to
+ * the notification title and body under certain senarios.
+ *
+ * Since: 3.32
+ */
+ properties [PROP_ICON] =
+ g_param_spec_object ("icon",
+ "Icon",
+ "The icon for the notification, if any",
+ G_TYPE_ICON,
+ (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * IdeNotification:icon-name:
+ *
+ * The "icon-name" property is a helper to make setting #IdeNotification:icon
+ * more convenient.
+ *
+ * Since: 3.32
+ */
+ properties [PROP_ICON_NAME] =
+ g_param_spec_string ("icon-name",
+ "Icon Name",
+ "An icon-name to use to set IdeNotification:icon",
+ NULL,
+ (G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * IdeNotification:id:
+ *
+ * The "id" property is an optional identifier that can be used to locate
+ * the notification later.
+ *
+ * Since: 3.32
+ */
+ properties [PROP_ID] =
+ g_param_spec_string ("id",
+ "Id",
+ "An optional identifier for the notification",
+ NULL,
+ (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * IdeNotification:priority:
+ *
+ * The "priority" property is used to sort the notification in order of
+ * importance when displaying to the user.
+ *
+ * You may also use the #IdeNotification:urgent property to raise the
+ * importance of a message to the user.
+ *
+ * Since: 3.32
+ */
+ properties [PROP_PRIORITY] =
+ g_param_spec_int ("priority",
+ "Priority",
+ "The priority of the notification",
+ G_MININT, G_MAXINT, 0,
+ (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * IdeNotification:progress:
+ *
+ * The "progress" property is a value between 0.0 and 1.0 describing the progress of
+ * the operation for which the notification represents.
+ *
+ * This property is ignored if #IdeNotification:has-progress is unset.
+ *
+ * Since: 3.32
+ */
+ properties [PROP_PROGRESS] =
+ g_param_spec_double ("progress",
+ "Progress",
+ "The progress for the notification, if any",
+ 0.0, 1.0, 0.0,
+ (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * IdeNotification:progress-is-imprecise:
+ *
+ * The "progress-is-imprecise" property indicates that the notification has
+ * progress, but it is imprecise.
+ *
+ * The UI may show a bouncing progress bar if set.
+ *
+ * Since: 3.32
+ */
+ properties [PROP_PROGRESS_IS_IMPRECISE] =
+ g_param_spec_boolean ("progress-is-imprecise",
+ "Progress is Imprecise",
+ "If the notification supports progress, but is imprecise",
+ FALSE,
+ (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * IdeNotification:title:
+ *
+ * The "title" property is the main text to show the user. It may be
+ * displayed more prominently such as in the titlebar.
+ *
+ * Since: 3.32
+ */
+ properties [PROP_TITLE] =
+ g_param_spec_string ("title",
+ "Title",
+ "The title of the notification",
+ NULL,
+ (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * IdeNotification:urgent:
+ *
+ * If the notification is urgent. These notifications will be displayed with
+ * higher priority than those without the urgent property set.
+ *
+ * Since: 3.32
+ */
+ properties [PROP_URGENT] =
+ g_param_spec_boolean ("urgent",
+ "Urgent",
+ "If it is urgent the user see the notification",
+ FALSE,
+ (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_notification_init (IdeNotification *self)
+{
+}
+
+/**
+ * ide_notification_new:
+ *
+ * Creates a new #IdeNotification.
+ *
+ * To "send" the notification, you should attach it to the #IdeNotifications
+ * object which can be found under the root #IdeObject. To simplify this,
+ * the ide_notification_attach() function is provided to locate the
+ * #IdeNotifications object using any #IdeObject you have access to.
+ *
+ * ```
+ * IdeNotification *notif = ide_notification_new ();
+ * setup_notification (notify);
+ * ide_notification_attach (notif, IDE_OBJECT (some_object));
+ * ```
+ *
+ * Since: 3.32
+ */
+IdeNotification *
+ide_notification_new (void)
+{
+ return g_object_new (IDE_TYPE_NOTIFICATION, NULL);
+}
+
+/**
+ * ide_notification_attach:
+ * @self: an #IdeNotifications
+ * @object: an #IdeObject
+ *
+ * This function will locate the #IdeNotifications object starting from
+ * @object and attach @self as a child to that object.
+ *
+ * Since: 3.32
+ */
+void
+ide_notification_attach (IdeNotification *self,
+ IdeObject *object)
+{
+ g_autoptr(IdeObject) root = NULL;
+ g_autoptr(IdeObject) child = NULL;
+
+ g_return_if_fail (IDE_IS_NOTIFICATION (self));
+ g_return_if_fail (IDE_IS_OBJECT (object));
+
+ root = ide_object_ref_root (object);
+ child = ide_object_get_child_typed (root, IDE_TYPE_NOTIFICATIONS);
+
+ if (child != NULL)
+ ide_notifications_add_notification (IDE_NOTIFICATIONS (child), self);
+ else
+ g_warning ("Failed to locate IdeNotifications from %s", G_OBJECT_TYPE_NAME (object));
+}
+
+/**
+ * ide_notification_dup_id:
+ *
+ * Copies the id of the notification and returns it to the caller after locking
+ * the object. A copy is used to avoid thread-races.
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_notification_dup_id (IdeNotification *self)
+{
+ IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+ gchar *ret;
+
+ g_return_val_if_fail (IDE_IS_NOTIFICATION (self), NULL);
+
+ ide_object_lock (IDE_OBJECT (self));
+ ret = g_strdup (priv->id);
+ ide_object_unlock (IDE_OBJECT (self));
+
+ return g_steal_pointer (&ret);
+}
+
+/**
+ * ide_notification_set_id:
+ * @self: an #IdeNotification
+ * @id: (nullable): a string containing the id, or %NULL
+ *
+ * Sets the #IdeNotification:id property.
+ *
+ * Since: 3.32
+ */
+void
+ide_notification_set_id (IdeNotification *self,
+ const gchar *id)
+{
+ IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+
+ g_return_if_fail (IDE_IS_NOTIFICATION (self));
+
+ ide_object_lock (IDE_OBJECT (self));
+ if (!ide_str_equal0 (priv->id, id))
+ {
+ g_free (priv->id);
+ priv->id = g_strdup (id);
+ ide_object_notify_by_pspec (IDE_OBJECT (self), properties [PROP_ID]);
+ }
+ ide_object_unlock (IDE_OBJECT (self));
+}
+
+/**
+ * ide_notification_dup_title:
+ *
+ * Copies the current title and returns it to the caller after locking the
+ * object. A copy is used to avoid thread-races.
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_notification_dup_title (IdeNotification *self)
+{
+ IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+ gchar *ret;
+
+ g_return_val_if_fail (IDE_IS_NOTIFICATION (self), NULL);
+
+ ide_object_lock (IDE_OBJECT (self));
+ ret = g_strdup (priv->title);
+ ide_object_unlock (IDE_OBJECT (self));
+
+ return g_steal_pointer (&ret);
+}
+
+/**
+ * ide_notification_set_title:
+ * @self: an #IdeNotification
+ * @title: (nullable): a string containing the title text, or %NULL
+ *
+ * Sets the #IdeNotification:title property.
+ *
+ * Since: 3.32
+ */
+void
+ide_notification_set_title (IdeNotification *self,
+ const gchar *title)
+{
+ IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+
+ g_return_if_fail (IDE_IS_NOTIFICATION (self));
+
+ ide_object_lock (IDE_OBJECT (self));
+ if (!ide_str_equal0 (priv->title, title))
+ {
+ g_free (priv->title);
+ priv->title = g_strdup (title);
+ ide_object_notify_by_pspec (IDE_OBJECT (self), properties [PROP_TITLE]);
+ }
+ ide_object_unlock (IDE_OBJECT (self));
+}
+
+/**
+ * ide_notification_dup_body:
+ *
+ * Copies the current body and returns it to the caller after locking the
+ * object. A copy is used to avoid thread-races.
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_notification_dup_body (IdeNotification *self)
+{
+ IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+ gchar *ret;
+
+ g_return_val_if_fail (IDE_IS_NOTIFICATION (self), NULL);
+
+ ide_object_lock (IDE_OBJECT (self));
+ ret = g_strdup (priv->body);
+ ide_object_unlock (IDE_OBJECT (self));
+
+ return g_steal_pointer (&ret);
+}
+
+/**
+ * ide_notification_set_body:
+ * @self: an #IdeNotification
+ * @body: (nullable): a string containing the body text, or %NULL
+ *
+ * Sets the #IdeNotification:body property.
+ *
+ * Since: 3.32
+ */
+void
+ide_notification_set_body (IdeNotification *self,
+ const gchar *body)
+{
+ IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+
+ g_return_if_fail (IDE_IS_NOTIFICATION (self));
+
+ ide_object_lock (IDE_OBJECT (self));
+ if (!ide_str_equal0 (priv->body, body))
+ {
+ g_free (priv->body);
+ priv->body = g_strdup (body);
+ ide_object_notify_by_pspec (IDE_OBJECT (self), properties [PROP_BODY]);
+ }
+ ide_object_unlock (IDE_OBJECT (self));
+}
+
+/**
+ * ide_notification_ref_icon:
+ *
+ * Gets the icon for the notification, and returns a new reference
+ * to the #GIcon.
+ *
+ * Returns: (transfer full) (nullable): a #GIcon or %NULL
+ *
+ * Since: 3.32
+ */
+GIcon *
+ide_notification_ref_icon (IdeNotification *self)
+{
+ IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+ GIcon *ret = NULL;
+
+ g_return_val_if_fail (IDE_IS_NOTIFICATION (self), NULL);
+
+ ide_object_lock (IDE_OBJECT (self));
+ g_set_object (&ret, priv->icon);
+ ide_object_unlock (IDE_OBJECT (self));
+
+ return g_steal_pointer (&ret);
+}
+
+void
+ide_notification_set_icon (IdeNotification *self,
+ GIcon *icon)
+{
+ IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+
+ g_return_if_fail (IDE_IS_NOTIFICATION (self));
+ g_return_if_fail (!icon || G_IS_ICON (icon));
+
+ ide_object_lock (IDE_OBJECT (self));
+ if (g_set_object (&priv->icon, icon))
+ ide_object_notify_by_pspec (self, properties [PROP_ICON]);
+ ide_object_unlock (IDE_OBJECT (self));
+}
+
+void
+ide_notification_set_icon_name (IdeNotification *self,
+ const gchar *icon_name)
+{
+ g_autoptr(GIcon) icon = NULL;
+
+ g_return_if_fail (IDE_IS_NOTIFICATION (self));
+ g_return_if_fail (!icon || G_IS_ICON (icon));
+
+ if (icon_name != NULL)
+ icon = g_themed_icon_new (icon_name);
+ ide_notification_set_icon (self, icon);
+}
+
+gint
+ide_notification_get_priority (IdeNotification *self)
+{
+ IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+ gint ret;
+
+ g_return_val_if_fail (IDE_IS_NOTIFICATION (self), 0);
+
+ ide_object_lock (IDE_OBJECT (self));
+ ret = priv->priority;
+ ide_object_unlock (IDE_OBJECT (self));
+
+ return ret;
+}
+
+void
+ide_notification_set_priority (IdeNotification *self,
+ gint priority)
+{
+ IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+
+ g_return_if_fail (IDE_IS_NOTIFICATION (self));
+
+ ide_object_lock (IDE_OBJECT (self));
+ if (priv->priority != priority)
+ {
+ priv->priority = priority;
+ ide_object_notify_by_pspec (self, properties [PROP_PRIORITY]);
+ }
+ ide_object_unlock (IDE_OBJECT (self));
+}
+
+gboolean
+ide_notification_get_urgent (IdeNotification *self)
+{
+ IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+ gboolean ret;
+
+ g_return_val_if_fail (IDE_IS_NOTIFICATION (self), FALSE);
+
+ ide_object_lock (IDE_OBJECT (self));
+ ret = priv->urgent;
+ ide_object_unlock (IDE_OBJECT (self));
+
+ return ret;
+}
+
+void
+ide_notification_set_urgent (IdeNotification *self,
+ gboolean urgent)
+{
+ IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+
+ g_return_if_fail (IDE_IS_NOTIFICATION (self));
+
+ urgent = !!urgent;
+
+ ide_object_lock (IDE_OBJECT (self));
+ if (priv->urgent != urgent)
+ {
+ priv->urgent = urgent;
+ ide_object_notify_by_pspec (self, properties [PROP_URGENT]);
+ }
+ ide_object_unlock (IDE_OBJECT (self));
+}
+
+guint
+ide_notification_get_n_buttons (IdeNotification *self)
+{
+ IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+ guint ret;
+
+ g_return_val_if_fail (IDE_IS_NOTIFICATION (self), FALSE);
+
+ ide_object_lock (IDE_OBJECT (self));
+ if (priv->buttons != NULL)
+ ret = priv->buttons->len;
+ else
+ ret = 0;
+ ide_object_unlock (IDE_OBJECT (self));
+
+ return ret;
+}
+
+/**
+ * ide_notification_get_button:
+ * @self: an #IdeNotification
+ * @label: (out) (optional): a location for the button label
+ * @icon: (out) (optional): a location for the button icon
+ * @action: (out) (optional): a location for the button action name
+ * @target: (out) (optional): a location for the button action target
+ *
+ * Gets the button indexed by @button, and stores information about the
+ * button into the various out parameters @label, @icon, @action, and @target.
+ *
+ * Caller should check for the number of buttons using
+ * ide_notification_get_n_buttons() to determine the numerical range of
+ * indexes to provide for @button.
+ *
+ * To avoid racing with threads modifying notifications, the caller can
+ * hold a recursive lock across the function calls using ide_object_lock()
+ * and ide_object_unlock().
+ *
+ * Returns: %TRUE if @button was found; otherwise %FALSE
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_notification_get_button (IdeNotification *self,
+ guint button,
+ gchar **label,
+ GIcon **icon,
+ gchar **action,
+ GVariant **target)
+{
+ IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+ gboolean ret = FALSE;
+
+ g_return_val_if_fail (IDE_IS_NOTIFICATION (self), FALSE);
+
+ ide_object_lock (IDE_OBJECT (self));
+ if (priv->buttons != NULL)
+ {
+ if (button < priv->buttons->len)
+ {
+ Button *b = &g_array_index (priv->buttons, Button, button);
+
+ if (label)
+ *label = g_strdup (b->label);
+ if (icon)
+ g_set_object (icon, b->icon);
+ if (action)
+ *action = g_strdup (b->action);
+ if (target)
+ *target = b->target ? g_variant_ref (b->target) : NULL;
+ ret = TRUE;
+ }
+ }
+ ide_object_unlock (IDE_OBJECT (self));
+
+ return ret;
+}
+
+/**
+ * ide_notification_add_button:
+ * @self: an #IdeNotification
+ * @label: the label for the button
+ * @icon: (nullable): an optional icon for the button
+ * @detailed_action: a detailed action name (See #GAction)
+ *
+ * Adds a new button that may be displayed with the notification.
+ *
+ * See also: ide_notification_add_button_with_target_value().
+ *
+ * Since: 3.32
+ */
+void
+ide_notification_add_button (IdeNotification *self,
+ const gchar *label,
+ GIcon *icon,
+ const gchar *detailed_action)
+{
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GVariant) target_value = NULL;
+ g_autofree gchar *action_name = NULL;
+
+ g_return_if_fail (IDE_IS_NOTIFICATION (self));
+ g_return_if_fail (label || icon);
+ g_return_if_fail (!icon || G_IS_ICON (icon));
+ g_return_if_fail (detailed_action != NULL);
+
+ if (!g_action_parse_detailed_name (detailed_action, &action_name, &target_value, &error))
+ g_warning ("Failed to parse detailed_action: %s", error->message);
+ else
+ ide_notification_add_button_with_target_value (self, label, icon, action_name, target_value);
+}
+
+/**
+ * ide_notification_add_button_with_target_value:
+ * @self: an #IdeNotification
+ * @label: the label for the button
+ * @icon: (nullable): an optional icon for the button
+ * @action: an action name (See #GAction)
+ * @target: (nullable): an optional #GVariant for the action target
+ *
+ * Adds a new button, used the parsed #GVariant format for the action
+ * target.
+ *
+ * Since: 3.32
+ */
+void
+ide_notification_add_button_with_target_value (IdeNotification *self,
+ const gchar *label,
+ GIcon *icon,
+ const gchar *action,
+ GVariant *target)
+{
+ IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+ Button b = {0};
+
+ g_return_if_fail (IDE_IS_NOTIFICATION (self));
+ g_return_if_fail (label || icon);
+ g_return_if_fail (action != NULL);
+
+ b.label = g_strdup (label);
+ g_set_object (&b.icon, icon);
+ b.action = g_strdup (action);
+ b.target = target ? g_variant_ref (target) : NULL;
+
+ ide_object_lock (IDE_OBJECT (self));
+ if (priv->buttons == NULL)
+ {
+ priv->buttons = g_array_new (FALSE, FALSE, sizeof b);
+ g_array_set_clear_func (priv->buttons, (GDestroyNotify)clear_button);
+ }
+ g_array_append_val (priv->buttons, b);
+ ide_object_unlock (IDE_OBJECT (self));
+}
+
+gboolean
+ide_notification_get_default_action (IdeNotification *self,
+ gchar **action,
+ GVariant **target)
+{
+ IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+ gboolean ret = FALSE;
+
+ g_return_val_if_fail (IDE_IS_NOTIFICATION (self), FALSE);
+
+
+ ide_object_lock (IDE_OBJECT (self));
+ if (priv->default_action != NULL)
+ {
+ if (action)
+ *action = g_strdup (priv->default_action);
+ if (target)
+ *target = priv->default_target ? g_variant_ref (priv->default_target) : NULL;
+ ret = TRUE;
+ }
+ ide_object_unlock (IDE_OBJECT (self));
+
+ return ret;
+}
+
+void
+ide_notification_set_default_action (IdeNotification *self,
+ const gchar *detailed_action)
+{
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GVariant) target_value = NULL;
+ g_autofree gchar *action_name = NULL;
+
+ g_return_if_fail (IDE_IS_NOTIFICATION (self));
+ g_return_if_fail (detailed_action != NULL);
+
+ if (!g_action_parse_detailed_name (detailed_action, &action_name, &target_value, &error))
+ g_warning ("Failed to parse detailed_action: %s", error->message);
+ else
+ ide_notification_set_default_action_and_target_value (self, action_name, target_value);
+}
+
+void
+ide_notification_set_default_action_and_target_value (IdeNotification *self,
+ const gchar *action,
+ GVariant *target)
+{
+ IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+
+ g_return_if_fail (IDE_IS_NOTIFICATION (self));
+ g_return_if_fail (action != NULL);
+
+ ide_object_lock (IDE_OBJECT (self));
+
+ if (!ide_str_equal0 (priv->default_action, action))
+ {
+ g_free (priv->default_action);
+ priv->default_action = g_strdup (action);
+ }
+
+ if (priv->default_target != NULL &&
+ target != NULL &&
+ g_variant_equal (priv->default_target, target))
+ goto unlock;
+
+ g_clear_pointer (&priv->default_target, g_variant_unref);
+ priv->default_target = target ? g_variant_ref (target) : NULL;
+
+unlock:
+ ide_object_unlock (IDE_OBJECT (self));
+}
+
+gint
+ide_notification_compare (IdeNotification *a,
+ IdeNotification *b)
+{
+ IdeNotificationPrivate *a_priv = ide_notification_get_instance_private (a);
+ IdeNotificationPrivate *b_priv = ide_notification_get_instance_private (b);
+
+ if (a_priv->urgent)
+ {
+ if (!b_priv->urgent)
+ return -1;
+ }
+
+ if (b_priv->urgent)
+ {
+ if (!a_priv->urgent)
+ return 1;
+ }
+
+ return a_priv->priority - b_priv->priority;
+}
+
+/**
+ * ide_notification_get_progress:
+ * @self: a #IdeNotification
+ *
+ * Gets the progress for the notification.
+ *
+ * Returns: a value between 0.0 and 1.0
+ *
+ * Since: 3.32
+ */
+gdouble
+ide_notification_get_progress (IdeNotification *self)
+{
+ IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+ gdouble ret;
+
+ g_return_val_if_fail (IDE_IS_NOTIFICATION (self), 0.0);
+
+ ide_object_lock (IDE_OBJECT (self));
+ ret = priv->progress;
+ ide_object_unlock (IDE_OBJECT (self));
+
+ return ret;
+}
+
+/**
+ * ide_notification_set_progress:
+ * @self: a #IdeNotification
+ * @progress: a value between 0.0 and 1.0
+ *
+ * Sets the progress for the notification.
+ *
+ * Since: 3.32
+ */
+void
+ide_notification_set_progress (IdeNotification *self,
+ gdouble progress)
+{
+ IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+
+ g_return_if_fail (IDE_IS_NOTIFICATION (self));
+
+ progress = CLAMP (progress, 0.0, 1.0);
+
+ ide_object_lock (IDE_OBJECT (self));
+ if (priv->progress != progress)
+ {
+ priv->progress = progress;
+ ide_object_notify_by_pspec (IDE_OBJECT (self), properties [PROP_PROGRESS]);
+ }
+ ide_object_unlock (IDE_OBJECT (self));
+}
+
+/**
+ * ide_notification_get_has_progress:
+ * @self: a #IdeNotification
+ *
+ * Gets if the notification supports progress updates.
+ *
+ * Returns: %TRUE if progress updates are supported.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_notification_get_has_progress (IdeNotification *self)
+{
+ IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+ gboolean ret;
+
+ g_return_val_if_fail (IDE_IS_NOTIFICATION (self), 0.0);
+
+ ide_object_lock (IDE_OBJECT (self));
+ ret = priv->has_progress;
+ ide_object_unlock (IDE_OBJECT (self));
+
+ return ret;
+}
+
+/**
+ * ide_notification_set_has_progress:
+ * @self: a #IdeNotification
+ * @has_progress: if @notification supports progress
+ *
+ * Set to %TRUE if the notification supports progress updates.
+ *
+ * Since: 3.32
+ */
+void
+ide_notification_set_has_progress (IdeNotification *self,
+ gboolean has_progress)
+{
+ IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+
+ g_return_if_fail (IDE_IS_NOTIFICATION (self));
+
+ has_progress = !!has_progress;
+
+ ide_object_lock (IDE_OBJECT (self));
+ if (priv->has_progress != has_progress)
+ {
+ priv->has_progress = has_progress;
+ ide_object_notify_by_pspec (IDE_OBJECT (self), properties [PROP_HAS_PROGRESS]);
+ }
+ ide_object_unlock (IDE_OBJECT (self));
+}
+
+gboolean
+ide_notification_get_progress_is_imprecise (IdeNotification *self)
+{
+ IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+ gboolean ret;
+
+ g_return_val_if_fail (IDE_IS_NOTIFICATION (self), FALSE);
+
+ ide_object_lock (IDE_OBJECT (self));
+ ret = priv->progress_is_imprecise;
+ ide_object_unlock (IDE_OBJECT (self));
+
+ return ret;
+}
+
+void
+ide_notification_set_progress_is_imprecise (IdeNotification *self,
+ gboolean progress_is_imprecise)
+{
+ IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+
+ g_return_if_fail (IDE_IS_NOTIFICATION (self));
+
+ progress_is_imprecise = !!progress_is_imprecise;
+
+ ide_object_lock (IDE_OBJECT (self));
+ if (priv->progress_is_imprecise != progress_is_imprecise)
+ {
+ priv->progress_is_imprecise = progress_is_imprecise;
+ ide_object_notify_by_pspec (IDE_OBJECT (self), properties [PROP_PROGRESS_IS_IMPRECISE]);
+ }
+ ide_object_unlock (IDE_OBJECT (self));
+}
+
+/**
+ * ide_notification_withdraw:
+ * @self: a #IdeNotification
+ *
+ * Withdraws the notification by removing it from the #IdeObject parent it
+ * belongs to.
+ *
+ * Since: 3.32
+ */
+void
+ide_notification_withdraw (IdeNotification *self)
+{
+ g_autoptr(IdeObject) parent = NULL;
+
+ g_return_if_fail (IDE_IS_NOTIFICATION (self));
+
+ g_object_ref (self);
+ ide_object_lock (IDE_OBJECT (self));
+
+ if ((parent = ide_object_ref_parent (IDE_OBJECT (self))))
+ ide_object_remove (parent, IDE_OBJECT (self));
+
+ ide_object_unlock (IDE_OBJECT (self));
+ g_object_unref (self);
+}
+
+static gboolean
+do_withdrawal (gpointer data)
+{
+ ide_notification_withdraw (data);
+ return FALSE;
+}
+
+/**
+ * ide_notification_withdraw_in_seconds:
+ * @self: a #IdeNotification
+ * @seconds: number of seconds to withdraw after, or less than zero for a
+ * sensible default.
+ *
+ * Withdraws @self from it's #IdeObject parent after @seconds have passed.
+ *
+ * Since: 3.32
+ */
+void
+ide_notification_withdraw_in_seconds (IdeNotification *self,
+ gint seconds)
+{
+ g_return_if_fail (IDE_IS_NOTIFICATION (self));
+
+ if (seconds < 0)
+ seconds = 15;
+
+ g_timeout_add_seconds_full (G_PRIORITY_DEFAULT,
+ seconds,
+ do_withdrawal,
+ g_object_ref (self),
+ g_object_unref);
+}
+
+/**
+ * ide_notification_file_progress_callback:
+ *
+ * This function is a #GFileProgressCallback helper that will update the
+ * #IdeNotification:fraction property. @user_data must be an #IdeNotification.
+ *
+ * Remember to make sure to unref the #IdeNotification instance with
+ * g_object_unref() during the #GDestroyNotify.
+ *
+ * Since: 3.32
+ */
+void
+ide_notification_file_progress_callback (goffset current_num_bytes,
+ goffset total_num_bytes,
+ gpointer user_data)
+{
+ IdeNotification *self = user_data;
+ gdouble fraction = 0.0;
+
+ g_return_if_fail (IDE_IS_NOTIFICATION (self));
+
+ if (total_num_bytes)
+ fraction = (gdouble)current_num_bytes / (gdouble)total_num_bytes;
+
+ ide_notification_set_progress (self, fraction);
+}
+
+void
+ide_notification_flatpak_progress_callback (const char *status,
+ guint notification,
+ gboolean estimating,
+ gpointer user_data)
+{
+ IdeNotification *self = user_data;
+
+ g_return_if_fail (IDE_IS_NOTIFICATION (self));
+
+ ide_notification_set_body (self, status);
+ ide_notification_set_progress (self, (gdouble)notification / 100.0);
+}
diff --git a/src/libide/core/ide-notification.h b/src/libide/core/ide-notification.h
new file mode 100644
index 000000000..fdb763a67
--- /dev/null
+++ b/src/libide/core/ide-notification.h
@@ -0,0 +1,143 @@
+/* ide-notification.h
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include "ide-object.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_NOTIFICATION (ide_notification_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeNotification, ide_notification, IDE, NOTIFICATION, IdeObject)
+
+struct _IdeNotificationClass
+{
+ IdeObjectClass parent_class;
+
+ /*< private */
+ gpointer _reserved[16];
+};
+
+IDE_AVAILABLE_IN_3_32
+IdeNotification *ide_notification_new (void);
+IDE_AVAILABLE_IN_3_32
+void ide_notification_attach (IdeNotification *self,
+ IdeObject *object);
+IDE_AVAILABLE_IN_3_32
+gchar *ide_notification_dup_id (IdeNotification *self);
+IDE_AVAILABLE_IN_3_32
+void ide_notification_set_id (IdeNotification *self,
+ const gchar *id);
+IDE_AVAILABLE_IN_3_32
+gchar *ide_notification_dup_title (IdeNotification *self);
+IDE_AVAILABLE_IN_3_32
+void ide_notification_set_title (IdeNotification *self,
+ const gchar *title);
+IDE_AVAILABLE_IN_3_32
+GIcon *ide_notification_ref_icon (IdeNotification *self);
+IDE_AVAILABLE_IN_3_32
+void ide_notification_set_icon (IdeNotification *self,
+ GIcon *icon);
+IDE_AVAILABLE_IN_3_32
+void ide_notification_set_icon_name (IdeNotification *self,
+ const gchar *icon_name);
+IDE_AVAILABLE_IN_3_32
+gchar *ide_notification_dup_body (IdeNotification *self);
+IDE_AVAILABLE_IN_3_32
+void ide_notification_set_body (IdeNotification *self,
+ const gchar *body);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_notification_get_has_progress (IdeNotification *self);
+IDE_AVAILABLE_IN_3_32
+void ide_notification_set_has_progress (IdeNotification *self,
+ gboolean has_progress);
+IDE_AVAILABLE_IN_3_32
+gint ide_notification_get_priority (IdeNotification *self);
+IDE_AVAILABLE_IN_3_32
+void ide_notification_set_priority (IdeNotification *self,
+ gint priority);
+IDE_AVAILABLE_IN_3_32
+gdouble ide_notification_get_progress (IdeNotification *self);
+IDE_AVAILABLE_IN_3_32
+void ide_notification_set_progress (IdeNotification *self,
+ gdouble progress);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_notification_get_progress_is_imprecise (IdeNotification *self);
+IDE_AVAILABLE_IN_3_32
+void ide_notification_set_progress_is_imprecise (IdeNotification *self,
+ gboolean
progress_is_imprecise);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_notification_get_urgent (IdeNotification *self);
+IDE_AVAILABLE_IN_3_32
+void ide_notification_set_urgent (IdeNotification *self,
+ gboolean urgent);
+IDE_AVAILABLE_IN_3_32
+guint ide_notification_get_n_buttons (IdeNotification *self);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_notification_get_button (IdeNotification *self,
+ guint button,
+ gchar **label,
+ GIcon **icon,
+ gchar **action,
+ GVariant **target);
+IDE_AVAILABLE_IN_3_32
+void ide_notification_add_button (IdeNotification *self,
+ const gchar *label,
+ GIcon *icon,
+ const gchar *detailed_action);
+IDE_AVAILABLE_IN_3_32
+void ide_notification_add_button_with_target_value (IdeNotification *self,
+ const gchar *label,
+ GIcon *icon,
+ const gchar *action,
+ GVariant *target);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_notification_get_default_action (IdeNotification *self,
+ gchar **action,
+ GVariant **target);
+IDE_AVAILABLE_IN_3_32
+void ide_notification_set_default_action (IdeNotification *self,
+ const gchar *detailed_action);
+IDE_AVAILABLE_IN_3_32
+void ide_notification_set_default_action_and_target_value (IdeNotification *self,
+ const gchar *action,
+ GVariant *target);
+IDE_AVAILABLE_IN_3_32
+gint ide_notification_compare (IdeNotification *a,
+ IdeNotification *b);
+IDE_AVAILABLE_IN_3_32
+void ide_notification_withdraw (IdeNotification *self);
+IDE_AVAILABLE_IN_3_32
+void ide_notification_withdraw_in_seconds (IdeNotification *self,
+ gint seconds);
+IDE_AVAILABLE_IN_3_32
+void ide_notification_file_progress_callback (goffset current_num_bytes,
+ goffset total_num_bytes,
+ gpointer user_data);
+IDE_AVAILABLE_IN_3_32
+void ide_notification_flatpak_progress_callback (const char *status,
+ guint notification,
+ gboolean estimating,
+ gpointer user_data);
+
+
+G_END_DECLS
diff --git a/src/libide/core/ide-notifications.c b/src/libide/core/ide-notifications.c
new file mode 100644
index 000000000..9c09da832
--- /dev/null
+++ b/src/libide/core/ide-notifications.c
@@ -0,0 +1,516 @@
+/* ide-notifications.c
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-notifications"
+
+#include "config.h"
+
+#include "ide-macros.h"
+#include "ide-notifications.h"
+
+struct _IdeNotifications
+{
+ IdeObject parent_instance;
+};
+
+typedef struct
+{
+ gdouble progress;
+ guint total;
+ guint imprecise;
+} Progress;
+
+typedef struct
+{
+ const gchar *id;
+ IdeNotification *notif;
+} Find;
+
+static void list_model_iface_init (GListModelInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (IdeNotifications, ide_notifications, IDE_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init))
+
+enum {
+ PROP_0,
+ PROP_HAS_PROGRESS,
+ PROP_PROGRESS,
+ PROP_PROGRESS_IS_IMPRECISE,
+ N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_notifications_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ IdeNotifications *self = IDE_NOTIFICATIONS (object);
+
+ switch (prop_id)
+ {
+ case PROP_HAS_PROGRESS:
+ g_value_set_boolean (value, ide_notifications_get_has_progress (self));
+ break;
+
+ case PROP_PROGRESS:
+ g_value_set_double (value, ide_notifications_get_progress (self));
+ break;
+
+ case PROP_PROGRESS_IS_IMPRECISE:
+ g_value_set_boolean (value, ide_notifications_get_progress_is_imprecise (self));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+ide_notifications_child_notify_progress_cb (IdeNotifications *self,
+ GParamSpec *pspec,
+ IdeNotification *child)
+{
+ g_assert (IDE_IS_NOTIFICATIONS (self));
+ g_assert (IDE_IS_NOTIFICATION (child));
+
+ ide_object_notify_by_pspec (self, properties [PROP_PROGRESS]);
+}
+
+static void
+ide_notifications_add (IdeObject *object,
+ IdeObject *sibling,
+ IdeObject *child,
+ IdeObjectLocation location)
+{
+ IdeNotifications *self = (IdeNotifications *)object;
+
+ g_assert (IDE_IS_MAIN_THREAD ());
+ g_assert (IDE_IS_NOTIFICATIONS (object));
+ g_assert (IDE_IS_OBJECT (child));
+
+ if (!IDE_IS_NOTIFICATION (child))
+ {
+ g_warning ("Attempt to add something other than an IdeNotification is not allowed");
+ return;
+ }
+
+ g_signal_connect_object (child,
+ "notify::progress",
+ G_CALLBACK (ide_notifications_child_notify_progress_cb),
+ self,
+ G_CONNECT_SWAPPED);
+
+ IDE_OBJECT_CLASS (ide_notifications_parent_class)->add (object, sibling, child, location);
+
+ g_list_model_items_changed (G_LIST_MODEL (object), ide_object_get_position (child), 0, 1);
+ ide_object_notify_by_pspec (self, properties [PROP_HAS_PROGRESS]);
+ ide_object_notify_by_pspec (self, properties [PROP_PROGRESS_IS_IMPRECISE]);
+ ide_object_notify_by_pspec (self, properties [PROP_PROGRESS]);
+}
+
+static void
+ide_notifications_remove (IdeObject *object,
+ IdeObject *child)
+{
+ IdeNotifications *self = (IdeNotifications *)object;
+ guint position;
+
+ g_assert (IDE_IS_NOTIFICATIONS (self));
+ g_assert (IDE_IS_OBJECT (child));
+
+ g_signal_handlers_disconnect_by_func (child,
+ G_CALLBACK (ide_notifications_child_notify_progress_cb),
+ self);
+
+ position = ide_object_get_position (child);
+
+ IDE_OBJECT_CLASS (ide_notifications_parent_class)->remove (object, child);
+
+ g_list_model_items_changed (G_LIST_MODEL (object), position, 1, 0);
+ ide_object_notify_by_pspec (self, properties [PROP_HAS_PROGRESS]);
+ ide_object_notify_by_pspec (self, properties [PROP_PROGRESS_IS_IMPRECISE]);
+ ide_object_notify_by_pspec (self, properties [PROP_PROGRESS]);
+}
+
+static void
+ide_notifications_class_init (IdeNotificationsClass *klass)
+{
+ GObjectClass *g_object_class = G_OBJECT_CLASS (klass);
+ IdeObjectClass *object_class = IDE_OBJECT_CLASS (klass);
+
+ g_object_class->get_property = ide_notifications_get_property;
+
+ object_class->add = ide_notifications_add;
+ object_class->remove = ide_notifications_remove;
+
+ /**
+ * IdeNotifications:has-progress:
+ *
+ * The "has-progress" property denotes if any of the notifications
+ * have progress supported.
+ *
+ * Since: 3.32
+ */
+ properties [PROP_HAS_PROGRESS] =
+ g_param_spec_boolean ("has-progress",
+ "Has Progress",
+ "If any of the notifications have progress",
+ FALSE,
+ (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * IdeNotifications:progress:
+ *
+ * The "progress" property is the combination of all of the notifications
+ * currently monitored. It is updated when child notifications progress
+ * changes.
+ *
+ * Since: 3.32
+ */
+ properties [PROP_PROGRESS] =
+ g_param_spec_double ("progress",
+ "Progress",
+ "The combined process of all child notifications",
+ 0.0, 1.0, 0.0,
+ (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * IdeNotifications:progress-is-imprecise:
+ *
+ * The "progress-is-imprecise" property indicates that all progress-bearing
+ * notifications are imprecise.
+ *
+ * Since: 3.32
+ */
+ properties [PROP_PROGRESS_IS_IMPRECISE] =
+ g_param_spec_boolean ("progress-is-imprecise",
+ "Progress is Imprecise",
+ "If all of the notifications have imprecise progress",
+ FALSE,
+ (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_properties (g_object_class, N_PROPS, properties);
+}
+
+static void
+ide_notifications_init (IdeNotifications *self)
+{
+#if 0
+ g_autoptr(IdeNotification) notif = NULL;
+ g_autoptr(IdeNotification) notif2 = NULL;
+ g_autoptr(IdeNotification) notif3 = NULL;
+ g_autoptr(IdeNotification) notif4 = NULL;
+ g_autoptr(IdeNotification) notif5 = NULL;
+ g_autoptr(IdeNotification) notif6 = NULL;
+ g_autoptr(IdeNotification) notif7 = NULL;
+ g_autoptr(GIcon) icon1 = NULL;
+ g_autoptr(GIcon) icon2 = NULL;
+ g_autoptr(GIcon) icon4 = NULL;
+ g_autoptr(GIcon) icon5 = NULL;
+ g_autoptr(GIcon) icon6 = NULL;
+ g_autoptr(GIcon) icon7 = NULL;
+
+ notif = ide_notification_new ();
+ ide_notification_set_title (notif, "Builder ready.");
+ ide_notification_set_has_progress (notif, FALSE);
+ ide_notification_add_button (notif, "Foo", (icon1 = g_icon_new_for_string
("media-playback-pause-symbolic", NULL)), "debugger.pause");
+ ide_notifications_add_notification (self, notif);
+
+ notif2 = ide_notification_new ();
+ ide_notification_set_title (notif2, "Downloading libdazzle…");
+ ide_notification_set_has_progress (notif2, TRUE);
+ ide_notification_set_progress (notif2, .75);
+ ide_notification_set_default_action (notif2, "win.close");
+ ide_notification_add_button (notif2, "Foo", (icon2 = g_icon_new_for_string ("process-stop-symbolic",
NULL)), "build-manager.stop");
+ ide_notifications_add_notification (self, notif2);
+
+ notif3 = ide_notification_new ();
+ ide_notification_set_title (notif3, "SDK Not Installed");
+ ide_notification_set_body (notif3, "The org.gnome.Calculator.json build profile requires the
org.gnome.Platform runtime. Install it to allow this project to be built.");
+ ide_notification_set_has_progress (notif3, FALSE);
+ ide_notification_set_progress (notif3, 0);
+ ide_notification_add_button (notif3, "Download and Install", NULL, "win.close");
+ ide_notification_set_default_action (notif3, "win.close");
+ ide_notification_set_urgent (notif3, TRUE);
+ ide_notifications_add_notification (self, notif3);
+
+ notif4 = ide_notification_new ();
+ ide_notification_set_title (notif4, "Code Analytics Unavailable");
+ ide_notification_set_body (notif4, "Code highlighting, error detection, and macros are not fully
available, due to this project not being built recently. Rebuild to fully enable these features.");
+ ide_notification_set_has_progress (notif4, FALSE);
+ ide_notification_set_progress (notif4, 0);
+ ide_notification_set_default_action (notif4, "win.close");
+ ide_notifications_add_notification (self, notif4);
+
+ notif5 = ide_notification_new ();
+ ide_notification_set_title (notif5, "Running Partial Build");
+ ide_notification_set_body (notif5, "Diagnostics and autocompletion may be limited until complete.");
+ ide_notification_set_has_progress (notif5, TRUE);
+ ide_notification_set_progress_is_imprecise (notif5, TRUE);
+ ide_notification_set_progress (notif5, 0);
+ ide_notification_add_button (notif5, NULL, (icon5 = g_icon_new_for_string ("process-stop-symbolic",
NULL)), "win.close");
+ ide_notifications_add_notification (self, notif5);
+
+ notif6 = ide_notification_new ();
+ ide_notification_set_title (notif6, "Indexing Source Code");
+ ide_notification_set_body (notif6, "Search, diagnostics, and autocompletion may be limited until
complete.");
+ ide_notification_set_has_progress (notif6, TRUE);
+ ide_notification_set_progress (notif6, 0);
+ ide_notification_set_progress_is_imprecise (notif6, TRUE);
+ ide_notification_add_button (notif6, NULL, (icon6 = g_icon_new_for_string
("media-playback-pause-symbolic", NULL)), "win.close");
+ ide_notifications_add_notification (self, notif6);
+
+ notif7 = ide_notification_new ();
+ ide_notification_set_title (notif7, "Downloading org.gnome.Platform");
+ ide_notification_set_body (notif7, "3 minutes remaining");
+ ide_notification_set_has_progress (notif7, TRUE);
+ ide_notification_set_progress (notif7, 0);
+ ide_notification_add_button (notif7, NULL, (icon7 = g_icon_new_for_string ("process-stop-symbolic",
NULL)), "win.close");
+ ide_notifications_add_notification (self, notif7);
+
+ ide_notification_withdraw_in_seconds (notif, 10);
+ ide_notification_withdraw_in_seconds (notif2, 12);
+ ide_notification_withdraw_in_seconds (notif3, 14);
+ ide_notification_withdraw_in_seconds (notif4, 16);
+ ide_notification_withdraw_in_seconds (notif5, 18);
+#endif
+}
+
+/**
+ * ide_notifications_new:
+ *
+ * Create a new #IdeNotifications.
+ *
+ * Usually, creating this is not necessary, as the #IdeContext root
+ * #IdeObject will create it automatically.
+ *
+ * Returns: (transfer full): a newly created #IdeNotifications
+ *
+ * Since: 3.32
+ */
+IdeNotifications *
+ide_notifications_new (void)
+{
+ return g_object_new (IDE_TYPE_NOTIFICATIONS, NULL);
+}
+
+/**
+ * ide_notifications_add_notification:
+ * @self: an #IdeNotifications
+ * @notification: an #IdeNotification
+ *
+ * Adds @notification as a child of @self, sorting it by priority
+ * and urgency.
+ *
+ * Since: 3.32
+ */
+void
+ide_notifications_add_notification (IdeNotifications *self,
+ IdeNotification *notification)
+{
+ g_return_if_fail (IDE_IS_MAIN_THREAD ());
+ g_return_if_fail (IDE_IS_NOTIFICATIONS (self));
+ g_return_if_fail (IDE_IS_NOTIFICATION (notification));
+
+ ide_object_insert_sorted (IDE_OBJECT (self),
+ IDE_OBJECT (notification),
+ (GCompareDataFunc)ide_notification_compare,
+ NULL);
+}
+
+static GType
+ide_notifications_get_item_type (GListModel *model)
+{
+ return IDE_TYPE_NOTIFICATION;
+}
+
+static guint
+ide_notifications_get_n_items (GListModel *model)
+{
+ return ide_object_get_n_children (IDE_OBJECT (model));
+}
+
+static gpointer
+ide_notifications_get_item (GListModel *model,
+ guint position)
+{
+ return ide_object_get_nth_child (IDE_OBJECT (model), position);
+}
+
+static void
+list_model_iface_init (GListModelInterface *iface)
+{
+ iface->get_item_type = ide_notifications_get_item_type;
+ iface->get_n_items = ide_notifications_get_n_items;
+ iface->get_item = ide_notifications_get_item;
+}
+
+static void
+collect_progress_cb (gpointer item,
+ gpointer user_data)
+{
+ IdeNotification *notif = item;
+ Progress *prog = user_data;
+
+ g_assert (IDE_IS_NOTIFICATION (notif));
+ g_assert (prog != NULL);
+
+ if (ide_notification_get_has_progress (notif))
+ {
+ if (ide_notification_get_progress_is_imprecise (notif))
+ prog->imprecise++;
+ else
+ prog->progress += ide_notification_get_progress (notif);
+
+ prog->total++;
+ }
+}
+
+/**
+ * ide_notifications_get_progress:
+ * @self: a #IdeNotifications
+ *
+ * Gets the combined progress of the notifications contained in this
+ * #IdeNotifications object.
+ *
+ * Returns: A double between 0.0 and 1.0
+ *
+ * Since: 3.32
+ */
+gdouble
+ide_notifications_get_progress (IdeNotifications *self)
+{
+ Progress prog = {0};
+
+ g_return_val_if_fail (IDE_IS_NOTIFICATIONS (self), 0.0);
+
+ ide_object_lock (IDE_OBJECT (self));
+ ide_object_foreach (IDE_OBJECT (self), collect_progress_cb, &prog);
+ ide_object_unlock (IDE_OBJECT (self));
+
+ if (prog.total > 0)
+ {
+ if (prog.imprecise != prog.total)
+ return prog.progress / (gdouble)(prog.total - prog.imprecise);
+ else
+ return prog.progress / (gdouble)prog.total;
+ }
+
+ return 0.0;
+}
+
+/**
+ * ide_notifications_get_has_progress:
+ * @self: a #IdeNotifications
+ *
+ * Gets if any of the notification support progress updates.
+ *
+ * Returns: %TRUE if any notification has progress
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_notifications_get_has_progress (IdeNotifications *self)
+{
+ Progress prog = {0};
+
+ g_return_val_if_fail (IDE_IS_NOTIFICATIONS (self), 0.0);
+
+ ide_object_lock (IDE_OBJECT (self));
+ ide_object_foreach (IDE_OBJECT (self), collect_progress_cb, &prog);
+ ide_object_unlock (IDE_OBJECT (self));
+
+ return prog.total > 0;
+}
+
+/**
+ * ide_notifications_get_progress_is_imprecise:
+ * @self: a #IdeNotifications
+ *
+ * Checks if all of the notifications with progress are imprecise.
+ *
+ * Returns: %TRUE if all progress-supporting notifications are imprecise.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_notifications_get_progress_is_imprecise (IdeNotifications *self)
+{
+ Progress prog = {0};
+
+ g_return_val_if_fail (IDE_IS_NOTIFICATIONS (self), 0.0);
+
+ ide_object_lock (IDE_OBJECT (self));
+ ide_object_foreach (IDE_OBJECT (self), collect_progress_cb, &prog);
+ ide_object_unlock (IDE_OBJECT (self));
+
+ if (prog.total > 0)
+ return prog.imprecise == prog.total;
+
+ return FALSE;
+}
+
+static void
+find_by_id (gpointer item,
+ gpointer user_data)
+{
+ IdeNotification *notif = item;
+ Find *find = user_data;
+ g_autofree gchar *id = NULL;
+
+ if (find->notif)
+ return;
+
+ id = ide_notification_dup_id (notif);
+
+ if (ide_str_equal0 (find->id, id))
+ find->notif = g_object_ref (notif);
+}
+
+/**
+ * ide_notifications_find_by_id:
+ * @self: a #IdeNotifications
+ * @id: the id of the notification
+ *
+ * Finds the first #IdeNotification registered with @self with
+ * #IdeNotification:id of @id.
+ *
+ * Returns: (transfer full) (nullable): an #IdeNotification or %NULL
+ *
+ * Since: 3.32
+ */
+IdeNotification *
+ide_notifications_find_by_id (IdeNotifications *self,
+ const gchar *id)
+{
+ Find find = { id, NULL };
+
+ g_return_val_if_fail (IDE_IS_NOTIFICATIONS (self), NULL);
+ g_return_val_if_fail (id != NULL, NULL);
+
+ ide_object_lock (IDE_OBJECT (self));
+ ide_object_foreach (IDE_OBJECT (self), find_by_id, &find);
+ ide_object_unlock (IDE_OBJECT (self));
+
+ return g_steal_pointer (&find.notif);
+}
diff --git a/src/libide/core/ide-notifications.h b/src/libide/core/ide-notifications.h
new file mode 100644
index 000000000..fc482cfe4
--- /dev/null
+++ b/src/libide/core/ide-notifications.h
@@ -0,0 +1,48 @@
+/* ide-notifications.h
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include "ide-object.h"
+#include "ide-notification.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_NOTIFICATIONS (ide_notifications_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeNotifications, ide_notifications, IDE, NOTIFICATIONS, IdeObject)
+
+IDE_AVAILABLE_IN_3_32
+IdeNotifications *ide_notifications_new (void);
+IDE_AVAILABLE_IN_3_32
+void ide_notifications_add_notification (IdeNotifications *self,
+ IdeNotification *notification);
+IDE_AVAILABLE_IN_3_32
+gdouble ide_notifications_get_progress (IdeNotifications *self);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_notifications_get_has_progress (IdeNotifications *self);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_notifications_get_progress_is_imprecise (IdeNotifications *self);
+IDE_AVAILABLE_IN_3_32
+IdeNotification *ide_notifications_find_by_id (IdeNotifications *self,
+ const gchar *id);
+
+G_END_DECLS
diff --git a/src/libide/core/ide-object-box.c b/src/libide/core/ide-object-box.c
new file mode 100644
index 000000000..3a3ad2383
--- /dev/null
+++ b/src/libide/core/ide-object-box.c
@@ -0,0 +1,289 @@
+/* ide-object-box.c
+ *
+ * Copyright 2018 Christian Hergert <unknown domain org>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-object-box"
+
+#include "config.h"
+
+#include "ide-object-box.h"
+#include "ide-macros.h"
+
+struct _IdeObjectBox
+{
+ IdeObject parent_instance;
+ GObject *object;
+ guint propagate_disposal : 1;
+};
+
+G_DEFINE_TYPE (IdeObjectBox, ide_object_box, IDE_TYPE_OBJECT)
+
+enum {
+ PROP_0,
+ PROP_OBJECT,
+ PROP_PROPAGATE_DISPOSAL,
+ N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_object_box_set_object (IdeObjectBox *self,
+ GObject *object)
+{
+ g_return_if_fail (IDE_IS_OBJECT_BOX (self));
+ g_return_if_fail (G_IS_OBJECT (object));
+ g_return_if_fail (g_object_get_data (object, "IDE_OBJECT_BOX") == NULL);
+
+ self->object = g_object_ref (object);
+ g_object_set_data (self->object, "IDE_OBJECT_BOX", self);
+}
+
+/**
+ * ide_object_box_new:
+ *
+ * Create a new #IdeObjectBox.
+ *
+ * Returns: (transfer full): a newly created #IdeObjectBox
+ *
+ * Since: 3.32
+ */
+IdeObjectBox *
+ide_object_box_new (GObject *object)
+{
+ g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+
+ return g_object_new (IDE_TYPE_OBJECT_BOX,
+ "object", object,
+ NULL);
+}
+
+static gchar *
+ide_object_box_repr (IdeObject *object)
+{
+ g_autoptr(GObject) obj = ide_object_box_ref_object (IDE_OBJECT_BOX (object));
+
+ if (obj != NULL)
+ return g_strdup_printf ("%s object=\"%s\"",
+ G_OBJECT_TYPE_NAME (object),
+ G_OBJECT_TYPE_NAME (obj));
+ else
+ return IDE_OBJECT_CLASS (ide_object_box_parent_class)->repr (object);
+}
+
+static void
+ide_object_box_destroy (IdeObject *object)
+{
+ IdeObjectBox *self = (IdeObjectBox *)object;
+
+ g_assert (IDE_IS_MAIN_THREAD ());
+ g_assert (IDE_IS_OBJECT (self));
+
+ g_object_ref (self);
+
+ /* Clear the backpointer before any disposal to the object, since that
+ * will possibly result in the object calling back into this peer object.
+ */
+ if (self->object)
+ {
+ g_object_set_data (G_OBJECT (self->object), "IDE_OBJECT_BOX", NULL);
+ if (self->propagate_disposal)
+ g_object_run_dispose (G_OBJECT (self->object));
+ }
+
+ IDE_OBJECT_CLASS (ide_object_box_parent_class)->destroy (object);
+
+ g_clear_object (&self->object);
+
+ g_object_unref (self);
+}
+
+static void
+ide_object_box_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ IdeObjectBox *self = IDE_OBJECT_BOX (object);
+
+ switch (prop_id)
+ {
+ case PROP_OBJECT:
+ g_value_take_object (value, ide_object_box_ref_object (self));
+ break;
+
+ case PROP_PROPAGATE_DISPOSAL:
+ g_value_set_boolean (value, self->propagate_disposal);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+ide_object_box_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ IdeObjectBox *self = IDE_OBJECT_BOX (object);
+
+ switch (prop_id)
+ {
+ case PROP_OBJECT:
+ ide_object_box_set_object (self, g_value_get_object (value));
+ break;
+
+ case PROP_PROPAGATE_DISPOSAL:
+ self->propagate_disposal = g_value_get_boolean (value);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+ide_object_box_class_init (IdeObjectBoxClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ IdeObjectClass *i_object_class = IDE_OBJECT_CLASS (klass);
+
+ object_class->get_property = ide_object_box_get_property;
+ object_class->set_property = ide_object_box_set_property;
+
+ i_object_class->destroy = ide_object_box_destroy;
+ i_object_class->repr = ide_object_box_repr;
+
+ /**
+ * IdeObjectBox:object:
+ *
+ * The "object" property contains the object that is boxed and
+ * placed onto the object graph using this box.
+ *
+ * Since: 3.32
+ */
+ properties [PROP_OBJECT] =
+ g_param_spec_object ("object",
+ "Object",
+ "The boxed object",
+ G_TYPE_OBJECT,
+ (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * IdeObjectBox:propagate-disposal:
+ *
+ * The "propagate-disposal" property denotes if the #IdeObject:object
+ * property contents should have g_object_run_dispose() called when the
+ * #IdeObjectBox is destroyed.
+ *
+ * This is useful when you want to force disposal of an external object
+ * when @self is removed from the object tree.
+ *
+ * Since: 3.32
+ */
+ properties [PROP_PROPAGATE_DISPOSAL] =
+ g_param_spec_boolean ("propagate-disposal",
+ "Propagate Disposal",
+ "If the object should be disposed when the box is destroyed",
+ TRUE,
+ (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_object_box_init (IdeObjectBox *self)
+{
+ self->propagate_disposal = TRUE;
+}
+
+/**
+ * ide_object_box_ref_object:
+ * @self: an #IdeObjectBox
+ *
+ * Gets the boxed object.
+ *
+ * Returns: (transfer full) (nullable) (type GObject): a #GObject or %NULL
+ *
+ * Since: 3.32
+ */
+gpointer
+ide_object_box_ref_object (IdeObjectBox *self)
+{
+ GObject *ret;
+
+ g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+ g_return_val_if_fail (IDE_IS_OBJECT_BOX (self), NULL);
+
+ ide_object_lock (IDE_OBJECT (self));
+ ret = self->object ? g_object_ref (self->object) : NULL;
+ ide_object_unlock (IDE_OBJECT (self));
+
+ return g_steal_pointer (&ret);
+}
+
+/**
+ * ide_object_box_from_object:
+ * @object: a #GObject
+ *
+ * Gets the #IdeObjectBox that contains @object, if any.
+ *
+ * This function may only be called from the main thread.
+ *
+ * Returns: (transfer none): an #IdeObjectBox
+ *
+ * Since: 3.32
+ */
+IdeObjectBox *
+ide_object_box_from_object (GObject *object)
+{
+ g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+ g_return_val_if_fail (G_IS_OBJECT (object), NULL);
+
+ return g_object_get_data (G_OBJECT (object), "IDE_OBJECT_BOX");
+}
+
+/**
+ * ide_object_box_contains:
+ * @self: a #IdeObjectBox
+ * @instance: (type GObject) (nullable): a #GObject or %NULL
+ *
+ * Checks if @self contains @instance.
+ *
+ * Returns: %TRUE if #IdeObjectBox:object matches @instance
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_object_box_contains (IdeObjectBox *self,
+ gpointer instance)
+{
+ gboolean ret;
+
+ g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+ g_return_val_if_fail (IDE_IS_OBJECT_BOX (self), FALSE);
+
+ ide_object_lock (IDE_OBJECT (self));
+ ret = (instance == (gpointer)self->object);
+ ide_object_unlock (IDE_OBJECT (self));
+
+ return ret;
+}
diff --git a/src/libide/core/ide-object-box.h b/src/libide/core/ide-object-box.h
new file mode 100644
index 000000000..eb81085fb
--- /dev/null
+++ b/src/libide/core/ide-object-box.h
@@ -0,0 +1,46 @@
+/* ide-object-box.h
+ *
+ * Copyright 2018 Christian Hergert <unknown domain org>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#if !defined (IDE_CORE_INSIDE) && !defined (IDE_CORE_COMPILATION)
+# error "Only <libide-core.h> can be included directly."
+#endif
+
+#include "ide-object.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_OBJECT_BOX (ide_object_box_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeObjectBox, ide_object_box, IDE, OBJECT_BOX, IdeObject)
+
+IDE_AVAILABLE_IN_3_32
+IdeObjectBox *ide_object_box_new (GObject *object);
+IDE_AVAILABLE_IN_3_32
+gpointer ide_object_box_ref_object (IdeObjectBox *self);
+IDE_AVAILABLE_IN_3_32
+IdeObjectBox *ide_object_box_from_object (GObject *object);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_object_box_contains (IdeObjectBox *self,
+ gpointer instance);
+
+G_END_DECLS
diff --git a/src/libide/core/ide-object-notify.c b/src/libide/core/ide-object-notify.c
new file mode 100644
index 000000000..42cfe92ed
--- /dev/null
+++ b/src/libide/core/ide-object-notify.c
@@ -0,0 +1,114 @@
+/* ide-object-notify.c
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-object-notify"
+
+#include "config.h"
+
+#include "ide-object.h"
+#include "ide-macros.h"
+
+typedef struct
+{
+ GObject *object;
+ GParamSpec *pspec;
+} NotifyInMain;
+
+static gboolean
+ide_object_notify_in_main_cb (gpointer data)
+{
+ NotifyInMain *notify = data;
+
+ g_assert (notify != NULL);
+ g_assert (G_IS_OBJECT (notify->object));
+ g_assert (notify->pspec != NULL);
+
+ g_object_notify_by_pspec (notify->object, notify->pspec);
+
+ g_object_unref (notify->object);
+ g_param_spec_unref (notify->pspec);
+ g_slice_free (NotifyInMain, notify);
+
+ return G_SOURCE_REMOVE;
+}
+
+/**
+ * ide_object_notify_by_pspec:
+ * @instance: a #IdeObjectNotify
+ * @pspec: a #GParamSpec
+ *
+ * Like g_object_notify_by_pspec() if the caller is in the main-thread.
+ * Otherwise, the request is deferred to the main thread.
+ *
+ * Since: 3.32
+ */
+void
+ide_object_notify_by_pspec (gpointer instance,
+ GParamSpec *pspec)
+{
+ NotifyInMain *notify;
+
+ g_return_if_fail (G_IS_OBJECT (instance));
+ g_return_if_fail (G_IS_PARAM_SPEC (pspec));
+
+ if G_LIKELY (IDE_IS_MAIN_THREAD ())
+ {
+ g_object_notify_by_pspec (instance, pspec);
+ return;
+ }
+
+ notify = g_slice_new0 (NotifyInMain);
+ notify->pspec = g_param_spec_ref (pspec);
+ notify->object = g_object_ref (instance);
+
+ g_timeout_add (0, ide_object_notify_in_main_cb, g_steal_pointer (¬ify));
+}
+
+/**
+ * ide_object_notify_in_main:
+ * @instance: (type GObject.Object): a #GObject
+ * @pspec: a #GParamSpec
+ *
+ * This helper will perform a g_object_notify_by_pspec() with the
+ * added requirement that it is run from the applications main thread.
+ *
+ * You may want to do this when modifying state from a thread, but only
+ * notify from the Gtk+ thread.
+ *
+ * This will *always* return to the default main context, and never
+ * emit ::notify immediately.
+ *
+ * Since: 3.32
+ */
+void
+ide_object_notify_in_main (gpointer instance,
+ GParamSpec *pspec)
+{
+ NotifyInMain *notify;
+
+ g_return_if_fail (G_IS_OBJECT (instance));
+ g_return_if_fail (G_IS_PARAM_SPEC (pspec));
+
+ notify = g_slice_new0 (NotifyInMain);
+ notify->pspec = g_param_spec_ref (pspec);
+ notify->object = g_object_ref (instance);
+
+ g_timeout_add (0, ide_object_notify_in_main_cb, g_steal_pointer (¬ify));
+}
diff --git a/src/libide/core/ide-object.c b/src/libide/core/ide-object.c
new file mode 100644
index 000000000..ffabce957
--- /dev/null
+++ b/src/libide/core/ide-object.c
@@ -0,0 +1,1367 @@
+/* ide-object.c
+ *
+ * Copyright 2014-2019 Christian Hergert <chergert redhat com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-object"
+
+#include "config.h"
+
+#include "ide-context.h"
+#include "ide-object.h"
+#include "ide-macros.h"
+
+/**
+ * SECTION:ide-object
+ * @title: IdeObject
+ * @short_description: Base object with support for object trees
+ *
+ * #IdeObject is a specialized #GObject for use in Builder. It provides a
+ * hierarchy of objects using a specialized tree similar to a DOM. You can
+ * insert/append/prepend objects to a parent node, and track their lifetime
+ * as part of the tree.
+ *
+ * When an object is removed from the tree, it can automatically be destroyed
+ * via the #IdeObject::destroy signal. This is useful as it may cause the
+ * children of that object to be removed, recursively destroying the objects
+ * descendants. This behavior is ideal when you want a large amount of objects
+ * to be reclaimed once an ancestor is no longer necessary.
+ *
+ * #IdeObject's may also have a #GCancellable associated with them. The
+ * cancellable is created on demand when ide_object_ref_cancellable() is
+ * called. When the object is destroyed, the #GCancellable::cancel signal
+ * is emitted. This allows automatic cleanup of asynchronous operations
+ * when used properly.
+ *
+ * Since: 3.32
+ */
+
+typedef struct
+{
+ GRecMutex mutex;
+ GCancellable *cancellable;
+ IdeObject *parent;
+ GQueue children;
+ GList link;
+ guint in_destruction : 1;
+ guint destroyed : 1;
+} IdeObjectPrivate;
+
+typedef struct
+{
+ GType type;
+ IdeObject *child;
+} GetChildTyped;
+
+typedef struct
+{
+ GType type;
+ GPtrArray *array;
+} GetChildrenTyped;
+
+enum {
+ PROP_0,
+ PROP_CANCELLABLE,
+ PROP_PARENT,
+ N_PROPS
+};
+
+enum {
+ DESTROY,
+ N_SIGNALS
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdeObject, ide_object, G_TYPE_OBJECT)
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+static inline void
+ide_object_private_lock (IdeObjectPrivate *priv)
+{
+ g_rec_mutex_lock (&priv->mutex);
+}
+
+static inline void
+ide_object_private_unlock (IdeObjectPrivate *priv)
+{
+ g_rec_mutex_unlock (&priv->mutex);
+}
+
+static gboolean
+check_disposition (IdeObject *child,
+ IdeObject *parent,
+ IdeObjectPrivate *sibling_priv)
+{
+ IdeObjectPrivate *priv = ide_object_get_instance_private (child);
+
+ if (priv->parent != NULL)
+ {
+ g_critical ("Attempt to add %s to %s, but it already has a parent",
+ G_OBJECT_TYPE_NAME (child),
+ G_OBJECT_TYPE_NAME (parent));
+ return FALSE;
+ }
+
+ if (sibling_priv && sibling_priv->parent != parent)
+ {
+ g_critical ("Attempt to add child relative to sibling of another parent");
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static gchar *
+ide_object_real_repr (IdeObject *self)
+{
+ return g_strdup (G_OBJECT_TYPE_NAME (self));
+}
+
+static void
+ide_object_real_add (IdeObject *self,
+ IdeObject *sibling,
+ IdeObject *child,
+ IdeObjectLocation location)
+{
+ IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+ IdeObjectPrivate *child_priv = ide_object_get_instance_private (child);
+ IdeObjectPrivate *sibling_priv = ide_object_get_instance_private (sibling);
+
+ g_assert (IDE_IS_OBJECT (self));
+ g_assert (IDE_IS_OBJECT (child));
+ g_assert (!sibling || IDE_IS_OBJECT (sibling));
+
+ if (location == IDE_OBJECT_BEFORE_SIBLING ||
+ location == IDE_OBJECT_AFTER_SIBLING)
+ g_return_if_fail (IDE_IS_OBJECT (sibling));
+
+ ide_object_private_lock (priv);
+ ide_object_private_lock (child_priv);
+
+ if (sibling)
+ ide_object_private_lock (sibling_priv);
+
+ if (!check_disposition (child, self, NULL))
+ goto unlock;
+
+ switch (location)
+ {
+ case IDE_OBJECT_START:
+ g_queue_push_head_link (&priv->children, &child_priv->link);
+ break;
+
+ case IDE_OBJECT_END:
+ g_queue_push_tail_link (&priv->children, &child_priv->link);
+ break;
+
+ case IDE_OBJECT_BEFORE_SIBLING:
+ _g_queue_insert_before_link (&priv->children, &sibling_priv->link, &child_priv->link);
+ break;
+
+ case IDE_OBJECT_AFTER_SIBLING:
+ _g_queue_insert_after_link (&priv->children, &sibling_priv->link, &child_priv->link);
+ break;
+
+ default:
+ g_critical ("Invalid location to add object child");
+ goto unlock;
+ }
+
+ child_priv->parent = self;
+ g_object_ref (child);
+
+ if (IDE_OBJECT_GET_CLASS (child)->parent_set)
+ IDE_OBJECT_GET_CLASS (child)->parent_set (child, self);
+
+unlock:
+ if (sibling)
+ ide_object_private_unlock (sibling_priv);
+ ide_object_private_unlock (child_priv);
+ ide_object_private_unlock (priv);
+}
+
+static void
+ide_object_real_remove (IdeObject *self,
+ IdeObject *child)
+{
+ IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+ IdeObjectPrivate *child_priv = ide_object_get_instance_private (child);
+
+ g_assert (IDE_IS_OBJECT (self));
+ g_assert (IDE_IS_OBJECT (child));
+
+ ide_object_private_lock (priv);
+ ide_object_private_lock (child_priv);
+
+ g_assert (child_priv->parent == self);
+
+ if (child_priv->parent != self)
+ {
+ g_critical ("Attempt to remove child object from incorrect parent");
+ ide_object_private_unlock (child_priv);
+ ide_object_private_unlock (priv);
+ return;
+ }
+
+ g_queue_unlink (&priv->children, &child_priv->link);
+ child_priv->parent = NULL;
+
+ if (IDE_OBJECT_GET_CLASS (child)->parent_set)
+ IDE_OBJECT_GET_CLASS (child)->parent_set (child, NULL);
+
+ ide_object_private_unlock (child_priv);
+ ide_object_private_unlock (priv);
+
+ g_object_unref (child);
+}
+
+static void
+ide_object_real_destroy (IdeObject *self)
+{
+ IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+ IdeObject *hold = NULL;
+
+ g_assert (IDE_IS_OBJECT (self));
+
+ /* We already hold the instance lock, for destroy */
+
+ g_cancellable_cancel (priv->cancellable);
+
+ if (priv->parent != NULL)
+ {
+ hold = g_object_ref (self);
+ ide_object_remove (priv->parent, self);
+ }
+
+ g_assert (priv->parent == NULL);
+ g_assert (priv->link.prev == NULL);
+ g_assert (priv->link.next == NULL);
+
+ while (priv->children.head != NULL)
+ {
+ IdeObject *child = priv->children.head->data;
+
+ ide_object_destroy (child);
+ }
+
+ g_assert (priv->children.tail == NULL);
+ g_assert (priv->children.head == NULL);
+ g_assert (priv->children.length == 0);
+
+ g_assert (priv->parent == NULL);
+ g_assert (priv->link.prev == NULL);
+ g_assert (priv->link.next == NULL);
+
+ priv->destroyed = TRUE;
+
+ if (hold != NULL)
+ g_object_unref (hold);
+}
+
+static gboolean
+ide_object_destroy_in_main_cb (IdeObject *object)
+{
+ g_assert (IDE_IS_MAIN_THREAD ());
+ g_assert (IDE_IS_OBJECT (object));
+
+ ide_object_destroy (object);
+
+ return G_SOURCE_REMOVE;
+}
+
+void
+ide_object_destroy (IdeObject *self)
+{
+ IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+
+ g_return_if_fail (IDE_IS_OBJECT (self));
+
+ g_object_ref (self);
+ ide_object_private_lock (priv);
+
+ /* If we are not on the main thread, we want to detach from the
+ * object tree and then dispatch the rest of the destroy to the
+ * main thread (so no threaded cleanup can occur).
+ */
+
+ if (IDE_IS_MAIN_THREAD ())
+ {
+ g_cancellable_cancel (priv->cancellable);
+ if (!priv->in_destruction && !priv->destroyed)
+ g_object_run_dispose (G_OBJECT (self));
+ }
+ else
+ {
+ g_autoptr(IdeObject) parent = NULL;
+
+ if ((parent = ide_object_ref_parent (self)))
+ ide_object_remove (parent, self);
+
+ g_idle_add_full (G_PRIORITY_LOW + 1000,
+ (GSourceFunc)ide_object_destroy_in_main_cb,
+ g_object_ref (self),
+ g_object_unref);
+ }
+
+ ide_object_private_unlock (priv);
+ g_object_unref (self);
+}
+
+static gboolean
+ide_object_dispose_from_main_cb (gpointer user_data)
+{
+ IdeObject *self = user_data;
+ g_object_run_dispose (G_OBJECT (self));
+ return G_SOURCE_REMOVE;
+}
+
+static void
+ide_object_dispose (GObject *object)
+{
+ IdeObject *self = (IdeObject *)object;
+ IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+
+ if (!IDE_IS_MAIN_THREAD ())
+ {
+ /* We are not on the main thread and might lose our last reference count.
+ * Pass this object to the main thread for disposal. This usually only
+ * happens when an object was temporarily created/destroyed on a thread.
+ */
+ g_idle_add_full (G_PRIORITY_LOW + 1000,
+ ide_object_dispose_from_main_cb,
+ g_object_ref (self),
+ g_object_unref);
+ return;
+ }
+
+ g_assert (IDE_IS_OBJECT (object));
+
+ ide_object_private_lock (priv);
+
+ if (!priv->in_destruction)
+ {
+ priv->in_destruction = TRUE;
+ g_signal_emit (self, signals [DESTROY], 0);
+ priv->in_destruction = FALSE;
+ }
+
+ ide_object_private_unlock (priv);
+
+ G_OBJECT_CLASS (ide_object_parent_class)->dispose (object);
+}
+
+static void
+ide_object_finalize (GObject *object)
+{
+ IdeObject *self = (IdeObject *)object;
+ IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+
+ if (!IDE_IS_MAIN_THREAD ())
+ {
+ g_critical ("Attempt to finalize %s on a thread which is not allowed. Leaking instead.",
+ G_OBJECT_TYPE_NAME (object));
+ return;
+ }
+
+ g_assert (priv->parent == NULL);
+ g_assert (priv->children.length == 0);
+ g_assert (priv->children.head == NULL);
+ g_assert (priv->children.tail == NULL);
+ g_assert (priv->link.prev == NULL);
+ g_assert (priv->link.next == NULL);
+
+ g_clear_object (&priv->cancellable);
+ g_rec_mutex_clear (&priv->mutex);
+
+ G_OBJECT_CLASS (ide_object_parent_class)->finalize (object);
+}
+
+static void
+ide_object_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ IdeObject *self = IDE_OBJECT (object);
+
+ switch (prop_id)
+ {
+ case PROP_PARENT:
+ g_value_take_object (value, ide_object_ref_parent (self));
+ break;
+
+ case PROP_CANCELLABLE:
+ g_value_take_object (value, ide_object_ref_cancellable (self));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+ide_object_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ IdeObject *self = IDE_OBJECT (object);
+ IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+
+ switch (prop_id)
+ {
+ case PROP_CANCELLABLE:
+ priv->cancellable = g_value_dup_object (value);
+ break;
+
+ case PROP_PARENT:
+ {
+ IdeObject *parent = g_value_get_object (value);
+ if (parent != NULL)
+ ide_object_append (parent, IDE_OBJECT (self));
+ }
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+ide_object_class_init (IdeObjectClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->dispose = ide_object_dispose;
+ object_class->finalize = ide_object_finalize;
+ object_class->get_property = ide_object_get_property;
+ object_class->set_property = ide_object_set_property;
+
+ klass->add = ide_object_real_add;
+ klass->remove = ide_object_real_remove;
+ klass->destroy = ide_object_real_destroy;
+ klass->repr = ide_object_real_repr;
+
+ /**
+ * IdeObject:parent:
+ *
+ * The parent #IdeObject, if any.
+ *
+ * Since: 3.32
+ */
+ properties [PROP_PARENT] =
+ g_param_spec_object ("parent",
+ "Parent",
+ "The parent IdeObject",
+ IDE_TYPE_OBJECT,
+ (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * IdeObject:cancellable:
+ *
+ * The "cancellable" property is a #GCancellable that can be used by operations
+ * that will be cancelled when the #IdeObject::destroy signal is emitted on @self.
+ *
+ * This is convenient when you want operations to automatically be cancelled when
+ * part of teh object tree is segmented.
+ *
+ * Since: 3.32
+ */
+ properties [PROP_CANCELLABLE] =
+ g_param_spec_object ("cancellable",
+ "Cancellable",
+ "A GCancellable for the object to use in operations",
+ G_TYPE_CANCELLABLE,
+ (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_properties (object_class, N_PROPS, properties);
+
+ /**
+ * IdeObject::destroy:
+ *
+ * The "destroy" signal is emitted when the object should destroy itself
+ * and cleanup any state that is no longer necessary. This happens when
+ * the object has been removed from the because it was requested to be
+ * destroyed, or because a parent object is being destroyed.
+ *
+ * If you do not want to receive the "destroy" signal, then you must
+ * manually remove the object from the tree using ide_object_remove()
+ * while holding a reference to the object.
+ *
+ * Since: 3.32
+ */
+ signals [DESTROY] =
+ g_signal_new ("destroy",
+ G_TYPE_FROM_CLASS (klass),
+ (G_SIGNAL_RUN_CLEANUP | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS),
+ G_STRUCT_OFFSET (IdeObjectClass, destroy),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE, 0);
+ g_signal_set_va_marshaller (signals [DESTROY],
+ G_TYPE_FROM_CLASS (klass),
+ g_cclosure_marshal_VOID__VOIDv);
+}
+
+static void
+ide_object_init (IdeObject *self)
+{
+ IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+
+ priv->link.data = self;
+
+ g_rec_mutex_init (&priv->mutex);
+}
+
+/**
+ * ide_object_new:
+ * @type: a #GType of an #IdeObject derived object
+ * @parent: (nullable): an optional #IdeObject parent
+ *
+ * This is a convenience function for creating an #IdeObject and appending it
+ * to a parent.
+ *
+ * This function may only be called from the main-thread, as calling from any
+ * other thread would potentially risk being disposed before returning.
+ *
+ * Returns: (transfer full) (type IdeObject): a new #IdeObject
+ *
+ * Since: 3.32
+ */
+gpointer
+ide_object_new (GType type,
+ IdeObject *parent)
+{
+ IdeObject *ret;
+
+ g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+ g_return_val_if_fail (g_type_is_a (type, IDE_TYPE_OBJECT), NULL);
+ g_return_val_if_fail (!parent || IDE_IS_OBJECT (parent), NULL);
+
+ ret = g_object_new (type, NULL);
+ if (parent != NULL)
+ ide_object_append (parent, ret);
+
+ return g_steal_pointer (&ret);
+}
+
+/**
+ * ide_object_get_n_children:
+ * @self: a #IdeObject
+ *
+ * Gets the number of children for an object.
+ *
+ * Returns: the number of children
+ *
+ * Since: 3.32
+ */
+guint
+ide_object_get_n_children (IdeObject *self)
+{
+ IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+ guint ret;
+
+ g_return_val_if_fail (IDE_IS_OBJECT (self), 0);
+
+ ide_object_private_lock (priv);
+ ret = priv->children.length;
+ ide_object_private_unlock (priv);
+
+ return ret;
+}
+
+/**
+ * ide_object_get_nth_child:
+ * @self: a #IdeObject
+ * @nth: position of child to fetch
+ *
+ * Gets the @nth child of @self.
+ *
+ * A full reference to the child is returned.
+ *
+ * Returns: (transfer full) (nullable): an #IdeObject or %NULL
+ *
+ * Since: 3.32
+ */
+IdeObject *
+ide_object_get_nth_child (IdeObject *self,
+ guint nth)
+{
+ IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+ IdeObject *ret;
+
+ g_return_val_if_fail (IDE_IS_OBJECT (self), 0);
+
+ ide_object_private_lock (priv);
+ ret = g_list_nth_data (priv->children.head, nth);
+ if (ret != NULL)
+ g_object_ref (ret);
+ ide_object_private_unlock (priv);
+
+ g_return_val_if_fail (!ret || IDE_IS_OBJECT (ret), NULL);
+
+ return g_steal_pointer (&ret);
+}
+
+/**
+ * ide_object_get_position:
+ * @self: a #IdeObject
+ *
+ * Gets the position of @self within the parent node.
+ *
+ * Returns: the position, starting from 0
+ *
+ * Since: 3.32
+ */
+guint
+ide_object_get_position (IdeObject *self)
+{
+ IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+ guint ret = 0;
+
+ g_return_val_if_fail (IDE_IS_OBJECT (self), 0);
+
+ ide_object_private_lock (priv);
+
+ if (priv->parent != NULL)
+ {
+ IdeObjectPrivate *parent_priv = ide_object_get_instance_private (priv->parent);
+ ret = g_list_position (parent_priv->children.head, &priv->link);
+ }
+
+ ide_object_private_unlock (priv);
+
+ return ret;
+}
+
+/**
+ * ide_object_lock:
+ * @self: a #IdeObject
+ *
+ * Acquires the lock for @self. This can be useful when you need to do
+ * multi-threaded work with @self and want to ensure exclusivity.
+ *
+ * Call ide_object_unlock() to release the lock.
+ *
+ * The synchronization used is a #GRecMutex.
+ *
+ * Since: 3.32
+ */
+void
+ide_object_lock (IdeObject *self)
+{
+ IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+
+ g_return_if_fail (IDE_IS_OBJECT (self));
+
+ ide_object_private_lock (priv);
+}
+
+/**
+ * ide_object_unlock:
+ * @self: a #IdeObject
+ *
+ * Releases a previously acuiqred lock from ide_object_lock().
+ *
+ * The synchronization used is a #GRecMutex.
+ *
+ * Since: 3.32
+ */
+void
+ide_object_unlock (IdeObject *self)
+{
+ IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+
+ g_return_if_fail (IDE_IS_OBJECT (self));
+
+ ide_object_private_unlock (priv);
+}
+
+/**
+ * ide_object_ref_cancellable:
+ * @self: a #IdeObject
+ *
+ * Gets a #GCancellable for the object.
+ *
+ * Returns: (transfer none) (not nullable): a #GCancellable
+ *
+ * Since: 3.32
+ */
+GCancellable *
+ide_object_ref_cancellable (IdeObject *self)
+{
+ IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+ GCancellable *ret;
+
+ g_return_val_if_fail (IDE_IS_OBJECT (self), NULL);
+
+ ide_object_private_lock (priv);
+ if (priv->cancellable == NULL)
+ priv->cancellable = g_cancellable_new ();
+ ret = g_object_ref (priv->cancellable);
+ ide_object_private_unlock (priv);
+
+ return g_steal_pointer (&ret);
+}
+
+/**
+ * ide_object_get_parent:
+ * @self: a #IdeObject
+ *
+ * Gets the parent #IdeObject, if any.
+ *
+ * This function may only be called from the main thread.
+ *
+ * Returns: (transfer none) (nullable): an #IdeObject or %NULL
+ *
+ * Since: 3.32
+ */
+IdeObject *
+ide_object_get_parent (IdeObject *self)
+{
+ IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+ IdeObject *ret;
+
+ g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+ g_return_val_if_fail (IDE_IS_OBJECT (self), NULL);
+
+ ide_object_private_lock (priv);
+ ret = priv->parent;
+ ide_object_private_unlock (priv);
+
+ return g_steal_pointer (&ret);
+}
+
+/**
+ * ide_object_ref_parent:
+ * @self: a #IdeObject
+ *
+ * Gets the parent #IdeObject, if any.
+ *
+ * Returns: (transfer full) (nullable): an #IdeObject or %NULL
+ *
+ * Since: 3.32
+ */
+IdeObject *
+ide_object_ref_parent (IdeObject *self)
+{
+ IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+ IdeObject *ret;
+
+ g_return_val_if_fail (IDE_IS_OBJECT (self), NULL);
+
+ ide_object_private_lock (priv);
+ ret = priv->parent ? g_object_ref (priv->parent) : NULL;
+ ide_object_private_unlock (priv);
+
+ return g_steal_pointer (&ret);
+}
+
+/**
+ * ide_object_is_root:
+ * @self: a #IdeObject
+ *
+ * Checks if @self is root, meaning it has no parent.
+ *
+ * Returns: %TRUE if @self has no parent
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_object_is_root (IdeObject *self)
+{
+ IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+ gboolean ret;
+
+ g_return_val_if_fail (IDE_IS_OBJECT (self), FALSE);
+
+ ide_object_private_lock (priv);
+ ret = priv->parent == NULL;
+ ide_object_private_unlock (priv);
+
+ return ret;
+}
+
+/**
+ * ide_object_add:
+ * @self: an #IdeObject
+ * @sibling: (nullable): an #IdeObject or %NULL
+ * @child: an #IdeObject
+ * @location: location for child
+ *
+ * Adds @child to @self, with location dependent on @location.
+ *
+ * Generally, it is simpler to use the helper functions such as
+ * ide_object_append(), ide_object_prepend(), ide_object_insert_before(),
+ * or ide_object_insert_after().
+ *
+ * This function is primarily meant for consumers that don't know the
+ * relative position they need until runtime.
+ *
+ * Since: 3.32
+ */
+void
+ide_object_add (IdeObject *self,
+ IdeObject *sibling,
+ IdeObject *child,
+ IdeObjectLocation location)
+{
+ g_return_if_fail (IDE_IS_OBJECT (self));
+ g_return_if_fail (IDE_IS_OBJECT (child));
+
+ if (location == IDE_OBJECT_BEFORE_SIBLING ||
+ location == IDE_OBJECT_AFTER_SIBLING)
+ g_return_if_fail (IDE_IS_OBJECT (sibling));
+ else
+ g_return_if_fail (sibling == NULL);
+
+ IDE_OBJECT_GET_CLASS (self)->add (self, sibling, child, location);
+}
+
+/**
+ * ide_object_remove:
+ * @self: an #IdeObject
+ * @child: an #IdeObject
+ *
+ * Removes @child from @self.
+ *
+ * If @child is a borrowed reference, it may be finalized before this
+ * function returns.
+ *
+ * Since: 3.32
+ */
+void
+ide_object_remove (IdeObject *self,
+ IdeObject *child)
+{
+ g_return_if_fail (IDE_IS_OBJECT (self));
+ g_return_if_fail (IDE_IS_OBJECT (child));
+
+ IDE_OBJECT_GET_CLASS (self)->remove (self, child);
+}
+
+/**
+ * ide_object_append:
+ * @self: an #IdeObject
+ * @child: an #IdeObject
+ *
+ * Inserts @child as the last child of @self.
+ *
+ * Since: 3.32
+ */
+void
+ide_object_append (IdeObject *self,
+ IdeObject *child)
+{
+ ide_object_add (self, NULL, child, IDE_OBJECT_END);
+}
+
+/**
+ * ide_object_prepend:
+ * @self: an #IdeObject
+ * @child: an #IdeObject
+ *
+ * Inserts @child as the first child of @self.
+ *
+ * Since: 3.32
+ */
+void
+ide_object_prepend (IdeObject *self,
+ IdeObject *child)
+{
+ ide_object_add (self, NULL, child, IDE_OBJECT_START);
+}
+
+/**
+ * ide_object_insert_before:
+ * @self: an #IdeObject
+ * @sibling: an #IdeObject
+ * @child: an #IdeObject
+ *
+ * Inserts @child into @self's children, directly before @sibling.
+ *
+ * @sibling MUST BE a child of @self.
+ *
+ * Since: 3.32
+ */
+void
+ide_object_insert_before (IdeObject *self,
+ IdeObject *sibling,
+ IdeObject *child)
+{
+ ide_object_add (self, sibling, child, IDE_OBJECT_BEFORE_SIBLING);
+}
+
+/**
+ * ide_object_insert_after:
+ * @self: an #IdeObject
+ * @sibling: an #IdeObject
+ * @child: an #IdeObject
+ *
+ * Inserts @child into @self's children, directly after @sibling.
+ *
+ * @sibling MUST BE a child of @self.
+ *
+ * Since: 3.32
+ */
+void
+ide_object_insert_after (IdeObject *self,
+ IdeObject *sibling,
+ IdeObject *child)
+{
+ ide_object_add (self, sibling, child, IDE_OBJECT_AFTER_SIBLING);
+}
+
+/**
+ * ide_object_insert_sorted:
+ * @self: a #IdeObject
+ * @child: an #IdeObject
+ * @func: (scope call): a #GCompareDataFunc that can be used to locate the
+ * proper sibling
+ * @user_data: user data for @func
+ *
+ * Locates the proper sibling for @child by using @func amongst @self's
+ * children #IdeObject. Those objects must already be sorted.
+ *
+ * Since: 3.32
+ */
+void
+ide_object_insert_sorted (IdeObject *self,
+ IdeObject *child,
+ GCompareDataFunc func,
+ gpointer user_data)
+{
+ IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+
+ g_return_if_fail (IDE_IS_OBJECT (self));
+ g_return_if_fail (IDE_IS_OBJECT (child));
+ g_return_if_fail (func != NULL);
+
+ ide_object_lock (self);
+
+ if (priv->children.length == 0)
+ {
+ ide_object_prepend (self, child);
+ goto unlock;
+ }
+
+ g_assert (priv->children.head != NULL);
+ g_assert (priv->children.tail != NULL);
+
+ for (GList *iter = priv->children.tail; iter; iter = iter->prev)
+ {
+ IdeObject *other = iter->data;
+
+ g_assert (IDE_IS_OBJECT (other));
+
+ if (func (child, other, user_data) <= 0)
+ {
+ ide_object_insert_after (self, other, child);
+ goto unlock;
+ }
+ }
+
+ ide_object_append (self, child);
+
+unlock:
+ ide_object_unlock (self);
+}
+
+/**
+ * ide_object_foreach:
+ * @self: a #IdeObject
+ * @callback: (scope call): a #GFunc to call for each child
+ * @user_data: closure data for @callback
+ *
+ * Calls @callback for each child of @self.
+ *
+ * @callback is allowed to remove children from @self, but only as long as they are
+ * the child passed to callback (or child itself). See g_queue_foreach() for more
+ * details about what is allowed.
+ *
+ * Since: 3.32
+ */
+void
+ide_object_foreach (IdeObject *self,
+ GFunc callback,
+ gpointer user_data)
+{
+ IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+
+ g_return_if_fail (IDE_IS_OBJECT (self));
+ g_return_if_fail (callback != NULL);
+
+ ide_object_private_lock (priv);
+ g_queue_foreach (&priv->children, callback, user_data);
+ ide_object_private_unlock (priv);
+}
+
+static void
+get_child_typed_cb (gpointer data,
+ gpointer user_data)
+{
+ IdeObject *child = data;
+ GetChildTyped *q = user_data;
+
+ if (q->child != NULL)
+ return;
+
+ if (G_TYPE_CHECK_INSTANCE_TYPE (child, q->type))
+ q->child = g_object_ref (child);
+}
+
+/**
+ * ide_object_get_child_typed:
+ * @self: a #IdeObject
+ * @type: the #GType of the child to match
+ *
+ * Finds the first child of @self that is of @type.
+ *
+ * Returns: (transfer full) (type IdeObject) (nullable): an #IdeObject or %NULL
+ *
+ * Since: 3.32
+ */
+gpointer
+ide_object_get_child_typed (IdeObject *self,
+ GType type)
+{
+ GetChildTyped q = { type, NULL };
+
+ g_return_val_if_fail (IDE_IS_OBJECT (self), NULL);
+ g_return_val_if_fail (g_type_is_a (type, IDE_TYPE_OBJECT), NULL);
+
+ ide_object_foreach (self, get_child_typed_cb, &q);
+
+ return g_steal_pointer (&q.child);
+}
+
+static void
+get_children_typed_cb (gpointer data,
+ gpointer user_data)
+{
+ IdeObject *child = data;
+ GetChildrenTyped *q = user_data;
+
+ if (G_TYPE_CHECK_INSTANCE_TYPE (child, q->type))
+ g_ptr_array_add (q->array, g_object_ref (child));
+}
+
+/**
+ * ide_object_get_children_typed:
+ * @self: a #IdeObject
+ * @type: a #GType
+ *
+ * Gets all children matching @type.
+ *
+ * Returns: (transfer full) (element-type IdeObject): a #GPtrArray of
+ * #IdeObject matching @type.
+ *
+ * Since: 3.32
+ */
+GPtrArray *
+ide_object_get_children_typed (IdeObject *self,
+ GType type)
+{
+ g_autoptr(GPtrArray) ar = NULL;
+ GetChildrenTyped q;
+
+ g_return_val_if_fail (IDE_IS_OBJECT (self), NULL);
+ g_return_val_if_fail (g_type_is_a (type, IDE_TYPE_OBJECT), NULL);
+
+ ar = g_ptr_array_new ();
+
+ q.type = type;
+ q.array = ar;
+
+ ide_object_foreach (self, get_children_typed_cb, &q);
+
+ return g_steal_pointer (&ar);
+}
+
+/**
+ * ide_object_ref_root:
+ * @self: a #IdeObject
+ *
+ * Finds and returns the toplevel object in the tree.
+ *
+ * Returns: (transfer full): an #IdeObject
+ *
+ * Since: 3.32
+ */
+IdeObject *
+ide_object_ref_root (IdeObject *self)
+{
+ IdeObject *cur;
+
+ g_return_val_if_fail (IDE_IS_OBJECT (self), NULL);
+
+ cur = g_object_ref (self);
+
+ while (!ide_object_is_root (cur))
+ {
+ IdeObject *tmp = cur;
+ cur = ide_object_ref_parent (tmp);
+ g_object_unref (tmp);
+ }
+
+ return g_steal_pointer (&cur);
+}
+
+static void
+ide_object_async_init_cb (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GAsyncInitable *initable = (GAsyncInitable *)object;
+ g_autoptr(IdeObject) self = user_data;
+ g_autoptr(GError) error = NULL;
+
+ g_assert (G_IS_ASYNC_INITABLE (initable));
+ g_assert (G_IS_ASYNC_RESULT (result));
+ g_assert (IDE_IS_OBJECT (self));
+
+ if (!g_async_initable_init_finish (initable, result, &error))
+ {
+ g_warning ("Failed to initialize %s: %s",
+ G_OBJECT_TYPE_NAME (initable),
+ error->message);
+ ide_object_destroy (IDE_OBJECT (initable));
+ }
+}
+
+/**
+ * ide_object_ensure_child_typed:
+ * @self: a #IdeObject
+ * @type: the #GType of the child
+ *
+ * Like ide_object_get_child_typed() except that it creates an object of
+ * @type if it is missing.
+ *
+ * Returns: (transfer full) (nullable) (type IdeObject): an #IdeObject or %NULL
+ *
+ * Since: 3.32
+ */
+gpointer
+ide_object_ensure_child_typed (IdeObject *self,
+ GType type)
+{
+ IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+ g_autoptr(IdeObject) ret = NULL;
+
+ g_return_val_if_fail (IDE_IS_OBJECT (self), NULL);
+ g_return_val_if_fail (g_type_is_a (type, IDE_TYPE_OBJECT), NULL);
+ g_return_val_if_fail (!ide_object_in_destruction (self), NULL);
+
+ ide_object_private_lock (priv);
+ if (!(ret = ide_object_get_child_typed (self, type)))
+ {
+ g_autoptr(GError) error = NULL;
+
+ ret = ide_object_new (type, self);
+
+ if (G_IS_INITABLE (ret))
+ {
+ if (!g_initable_init (G_INITABLE (ret), NULL, &error))
+ g_warning ("Failed to initialize %s: %s",
+ G_OBJECT_TYPE_NAME (ret), error->message);
+ }
+ else if (G_IS_ASYNC_INITABLE (ret))
+ {
+ g_async_initable_init_async (G_ASYNC_INITABLE (ret),
+ G_PRIORITY_DEFAULT,
+ priv->cancellable,
+ ide_object_async_init_cb,
+ g_object_ref (self));
+ }
+ }
+ ide_object_private_unlock (priv);
+
+ return g_steal_pointer (&ret);
+}
+
+/**
+ * ide_object_destroyed:
+ * @self: a #IdeObject
+ *
+ * This function sets *object_pointer to NULL if object_pointer != NULL. It's
+ * intended to be used as a callback connected to the "destroy" signal of a
+ * object. You connect ide_object_destroyed() as a signal handler, and pass the
+ * address of your object variable as user data. Then when the object is
+ * destroyed, the variable will be set to NULL. Useful for example to avoid
+ * multiple copies of the same dialog.
+ *
+ * Since: 3.32
+ */
+void
+ide_object_destroyed (IdeObject **object_pointer)
+{
+ if (object_pointer != NULL)
+ *object_pointer = NULL;
+}
+
+/* compat for now to ease porting */
+void
+ide_object_set_context (IdeObject *object,
+ IdeContext *context)
+{
+ ide_object_append (IDE_OBJECT (context), object);
+}
+
+static gboolean dummy (gpointer p) { return G_SOURCE_REMOVE; }
+
+/**
+ * ide_object_get_context:
+ * @object: a #IdeObject
+ *
+ * Gets the #IdeContext for the object.
+ *
+ * Returns: (transfer none) (nullable): an #IdeContext
+ *
+ * Since: 3.32
+ */
+IdeContext *
+ide_object_get_context (IdeObject *object)
+{
+ g_autoptr(IdeObject) root = ide_object_ref_root (object);
+ IdeContext *ret = NULL;
+ GSource *source;
+
+ if (IDE_IS_CONTEXT (root))
+ ret = IDE_CONTEXT (root);
+
+ /* We can just return a borrowed instance if in main thread,
+ * otherwise we need to queue the object to the main loop.
+ */
+ if (IDE_IS_MAIN_THREAD ())
+ return ret;
+
+ source = g_idle_source_new ();
+ g_source_set_name (source, "context-release");
+ g_source_set_callback (source, dummy, g_steal_pointer (&root), g_object_unref);
+ g_source_attach (source, g_main_context_get_thread_default ());
+ g_source_unref (source);
+
+ return ret;
+}
+
+/**
+ * ide_object_ref_context:
+ * @self: a #IdeContext
+ *
+ * Gets the root #IdeContext for the object, if any.
+ *
+ * Returns: (transfer full) (nullable): an #IdeContext or %NULL
+ *
+ * Since: 3.32
+ */
+IdeContext *
+ide_object_ref_context (IdeObject *self)
+{
+ g_autoptr(IdeObject) root = NULL;
+
+ g_return_val_if_fail (IDE_IS_OBJECT (self), NULL);
+
+ if ((root = ide_object_ref_root (self)) && IDE_IS_CONTEXT (root))
+ return IDE_CONTEXT (g_steal_pointer (&root));
+
+ return NULL;
+}
+
+gboolean
+ide_object_in_destruction (IdeObject *self)
+{
+ IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+ gboolean ret;
+
+ g_return_val_if_fail (IDE_IS_OBJECT (self), FALSE);
+
+ ide_object_lock (self);
+ ret = priv->in_destruction || priv->destroyed;
+ ide_object_unlock (self);
+
+ return ret;
+}
+
+/**
+ * ide_object_repr:
+ * @self: a #IdeObject
+ *
+ * This function is similar to Python's `repr()` which gives a string
+ * representation for the object. It is useful when debugging Builder
+ * or when writing plugins.
+ *
+ * Returns: (transfer full): a string containing the string representation
+ * of the #IdeObject
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_object_repr (IdeObject *self)
+{
+ g_autofree gchar *str = NULL;
+
+ g_return_val_if_fail (IDE_IS_OBJECT (self), NULL);
+
+ str = IDE_OBJECT_GET_CLASS (self)->repr (self);
+
+ return g_strdup_printf ("<%s at %p>", str, self);
+}
+
+gboolean
+ide_object_set_error_if_destroyed (IdeObject *self,
+ GError **error)
+{
+ g_return_val_if_fail (IDE_IS_OBJECT (self), FALSE);
+
+ if (ide_object_in_destruction (self))
+ {
+ g_set_error (error,
+ G_IO_ERROR,
+ G_IO_ERROR_CANCELLED,
+ "The object was destroyed");
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+void
+ide_object_log (gpointer instance,
+ GLogLevelFlags level,
+ const gchar *domain,
+ const gchar *format,
+ ...)
+{
+ g_autoptr(IdeObject) root = NULL;
+ va_list args;
+
+ g_assert (IDE_IS_OBJECT (instance));
+
+ root = ide_object_ref_root (instance);
+
+ if (IDE_IS_CONTEXT (root))
+ {
+ g_autofree gchar *message = NULL;
+
+ va_start (args, format);
+ message = g_strdup_vprintf (format, args);
+ ide_context_log (IDE_CONTEXT (root), level, domain, message);
+ va_end (args);
+ }
+}
diff --git a/src/libide/core/ide-object.h b/src/libide/core/ide-object.h
new file mode 100644
index 000000000..a0ec78c78
--- /dev/null
+++ b/src/libide/core/ide-object.h
@@ -0,0 +1,156 @@
+/* ide-object.h
+ *
+ * Copyright 2014-2019 Christian Hergert <chergert redhat com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#if !defined (IDE_CORE_INSIDE) && !defined (IDE_CORE_COMPILATION)
+# error "Only <libide-core.h> can be included directly."
+#endif
+
+#include <gio/gio.h>
+
+#include "ide-version-macros.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_OBJECT (ide_object_get_type())
+
+typedef enum
+{
+ IDE_OBJECT_START,
+ IDE_OBJECT_END,
+ IDE_OBJECT_BEFORE_SIBLING,
+ IDE_OBJECT_AFTER_SIBLING,
+} IdeObjectLocation;
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeObject, ide_object, IDE, OBJECT, GObject)
+
+struct _IdeObjectClass
+{
+ GObjectClass parent_class;
+
+ void (*destroy) (IdeObject *self);
+ void (*add) (IdeObject *self,
+ IdeObject *sibling,
+ IdeObject *child,
+ IdeObjectLocation location);
+ void (*remove) (IdeObject *self,
+ IdeObject *child);
+ void (*parent_set) (IdeObject *self,
+ IdeObject *parent);
+ gchar *(*repr) (IdeObject *self);
+
+ /*< private */
+ gpointer _reserved[16];
+};
+
+IDE_AVAILABLE_IN_3_32
+gpointer ide_object_new (GType type,
+ IdeObject *parent) G_GNUC_WARN_UNUSED_RESULT;
+IDE_AVAILABLE_IN_3_32
+GCancellable *ide_object_ref_cancellable (IdeObject *self) G_GNUC_WARN_UNUSED_RESULT;
+IDE_AVAILABLE_IN_3_32
+IdeObject *ide_object_get_parent (IdeObject *self) G_GNUC_WARN_UNUSED_RESULT;
+IDE_AVAILABLE_IN_3_32
+IdeObject *ide_object_ref_parent (IdeObject *self) G_GNUC_WARN_UNUSED_RESULT;
+IDE_AVAILABLE_IN_3_32
+IdeObject *ide_object_ref_root (IdeObject *self) G_GNUC_WARN_UNUSED_RESULT;
+IDE_AVAILABLE_IN_3_32
+gboolean ide_object_is_root (IdeObject *self);
+IDE_AVAILABLE_IN_3_32
+void ide_object_lock (IdeObject *self);
+IDE_AVAILABLE_IN_3_32
+void ide_object_unlock (IdeObject *self);
+IDE_AVAILABLE_IN_3_32
+void ide_object_add (IdeObject *self,
+ IdeObject *sibling,
+ IdeObject *child,
+ IdeObjectLocation location);
+IDE_AVAILABLE_IN_3_32
+void ide_object_append (IdeObject *self,
+ IdeObject *child);
+IDE_AVAILABLE_IN_3_32
+void ide_object_prepend (IdeObject *self,
+ IdeObject *child);
+IDE_AVAILABLE_IN_3_32
+void ide_object_insert_before (IdeObject *self,
+ IdeObject *sibling,
+ IdeObject *child);
+IDE_AVAILABLE_IN_3_32
+void ide_object_insert_after (IdeObject *self,
+ IdeObject *sibling,
+ IdeObject *child);
+IDE_AVAILABLE_IN_3_32
+void ide_object_insert_sorted (IdeObject *self,
+ IdeObject *child,
+ GCompareDataFunc func,
+ gpointer user_data);
+IDE_AVAILABLE_IN_3_32
+void ide_object_remove (IdeObject *self,
+ IdeObject *child);
+IDE_AVAILABLE_IN_3_32
+void ide_object_foreach (IdeObject *self,
+ GFunc callback,
+ gpointer user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_object_set_error_if_destroyed (IdeObject *self,
+ GError **error);
+IDE_AVAILABLE_IN_3_32
+void ide_object_destroy (IdeObject *self);
+IDE_AVAILABLE_IN_3_32
+void ide_object_destroyed (IdeObject **self);
+IDE_AVAILABLE_IN_3_32
+guint ide_object_get_position (IdeObject *self);
+IDE_AVAILABLE_IN_3_32
+guint ide_object_get_n_children (IdeObject *self);
+IDE_AVAILABLE_IN_3_32
+IdeObject *ide_object_get_nth_child (IdeObject *self,
+ guint nth);
+IDE_AVAILABLE_IN_3_32
+gpointer ide_object_get_child_typed (IdeObject *self,
+ GType type);
+IDE_AVAILABLE_IN_3_32
+GPtrArray *ide_object_get_children_typed (IdeObject *self,
+ GType type);
+IDE_AVAILABLE_IN_3_32
+gpointer ide_object_ensure_child_typed (IdeObject *self,
+ GType type);
+IDE_AVAILABLE_IN_3_32
+void ide_object_notify_in_main (gpointer instance,
+ GParamSpec *pspec);
+IDE_AVAILABLE_IN_3_32
+void ide_object_notify_by_pspec (gpointer instance,
+ GParamSpec *pspec);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_object_in_destruction (IdeObject *self);
+IDE_AVAILABLE_IN_3_32
+gchar *ide_object_repr (IdeObject *self);
+IDE_AVAILABLE_IN_3_32
+void ide_object_log (gpointer instance,
+ GLogLevelFlags level,
+ const gchar *domain,
+ const gchar *format,
+ ...) G_GNUC_PRINTF (4, 5);
+
+#define ide_object_message(instance, format, ...) ide_object_log(instance, G_LOG_LEVEL_MESSAGE,
G_LOG_DOMAIN, format __VA_OPT__(,) __VA_ARGS__)
+#define ide_object_warning(instance, format, ...) ide_object_log(instance, G_LOG_LEVEL_WARNING,
G_LOG_DOMAIN, format __VA_OPT__(,) __VA_ARGS__)
+
+G_END_DECLS
diff --git a/src/libide/core/ide-settings.c b/src/libide/core/ide-settings.c
new file mode 100644
index 000000000..80b851da7
--- /dev/null
+++ b/src/libide/core/ide-settings.c
@@ -0,0 +1,589 @@
+/* ide-settings.c
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-settings"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <glib/gi18n.h>
+#include <stdlib.h>
+
+#include "ide-settings.h"
+
+/**
+ * SECTION:ide-settings
+ * @title: IdeSettings
+ * @short_description: Settings with per-project overrides
+ *
+ * In Builder, we need support for settings at the user level (their chosen
+ * defaults) as well as defaults for a project. #IdeSettings attempts to
+ * simplify this by providing a layered approach to settings.
+ *
+ * If a setting has been set for the current project, it will be returned. If
+ * not, the users preference will be returned. Setting a preference via
+ * #IdeSettings will always modify the projects setting, not the users default
+ * settings.
+ *
+ * Since: 3.32
+ */
+
+struct _IdeSettings
+{
+ GObject parent_instance;
+
+ DzlSettingsSandwich *settings_sandwich;
+ gchar *relative_path;
+ gchar *schema_id;
+ gchar *project_id;
+ guint ignore_project_settings : 1;
+};
+
+G_DEFINE_TYPE (IdeSettings, ide_settings, G_TYPE_OBJECT)
+
+enum {
+ PROP_0,
+ PROP_RELATIVE_PATH,
+ PROP_SCHEMA_ID,
+ PROP_IGNORE_PROJECT_SETTINGS,
+ PROP_PROJECT_ID,
+ N_PROPS
+};
+
+enum {
+ CHANGED,
+ N_SIGNALS
+};
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+static void
+ide_settings_set_ignore_project_settings (IdeSettings *self,
+ gboolean ignore_project_settings)
+{
+ g_return_if_fail (IDE_IS_SETTINGS (self));
+
+ ignore_project_settings = !!ignore_project_settings;
+
+ if (ignore_project_settings != self->ignore_project_settings)
+ {
+ self->ignore_project_settings = ignore_project_settings;
+ g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_IGNORE_PROJECT_SETTINGS]);
+ }
+}
+
+static void
+ide_settings_set_relative_path (IdeSettings *self,
+ const gchar *relative_path)
+{
+ g_assert (IDE_IS_SETTINGS (self));
+ g_assert (relative_path != NULL);
+
+ if (*relative_path == '/')
+ relative_path++;
+
+ if (!ide_str_equal0 (relative_path, self->relative_path))
+ {
+ g_free (self->relative_path);
+ self->relative_path = g_strdup (relative_path);
+ g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_RELATIVE_PATH]);
+ }
+}
+
+static void
+ide_settings_set_schema_id (IdeSettings *self,
+ const gchar *schema_id)
+{
+ g_assert (IDE_IS_SETTINGS (self));
+ g_assert (schema_id != NULL);
+
+ if (!ide_str_equal0 (schema_id, self->schema_id))
+ {
+ g_free (self->schema_id);
+ self->schema_id = g_strdup (schema_id);
+ g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SCHEMA_ID]);
+ }
+}
+
+static void
+ide_settings_constructed (GObject *object)
+{
+ IdeSettings *self = (IdeSettings *)object;
+ g_autofree gchar *full_path = NULL;
+ GSettings *settings;
+ gchar *path;
+
+ IDE_ENTRY;
+
+ G_OBJECT_CLASS (ide_settings_parent_class)->constructed (object);
+
+ if (self->schema_id == NULL)
+ {
+ g_error ("You must provide IdeSettings:schema-id");
+ abort ();
+ }
+
+ if (self->relative_path == NULL)
+ {
+ g_autoptr(GSettingsSchema) schema = NULL;
+ GSettingsSchemaSource *source;
+ const gchar *schema_path;
+
+ source = g_settings_schema_source_get_default ();
+ schema = g_settings_schema_source_lookup (source, self->schema_id, TRUE);
+
+ if (schema == NULL)
+ {
+ g_error ("Could not locate schema %s", self->schema_id);
+ abort ();
+ }
+
+ schema_path = g_settings_schema_get_path (schema);
+
+ if ((schema_path != NULL) && !g_str_has_prefix (schema_path, "/org/gnome/builder/"))
+ {
+ g_error ("Schema path MUST be under /org/gnome/builder/");
+ abort ();
+ }
+ else if (schema_path == NULL)
+ {
+ self->relative_path = g_strdup ("");
+ }
+ else
+ {
+ self->relative_path = g_strdup (schema_path + strlen ("/org/gnome/builder/"));
+ }
+ }
+
+ g_assert (self->relative_path != NULL);
+ g_assert (self->relative_path [0] != '/');
+ g_assert ((self->relative_path [0] == 0) || g_str_has_suffix (self->relative_path, "/"));
+
+ full_path = g_strdup_printf ("/org/gnome/builder/%s", self->relative_path);
+ self->settings_sandwich = dzl_settings_sandwich_new (self->schema_id, full_path);
+
+ /* Add our project relative settings */
+ if (self->ignore_project_settings == FALSE)
+ {
+ path = g_strdup_printf ("/org/gnome/builder/projects/%s/%s",
+ self->project_id, self->relative_path);
+ settings = g_settings_new_with_path (self->schema_id, path);
+ dzl_settings_sandwich_append (self->settings_sandwich, settings);
+ g_clear_object (&settings);
+ g_free (path);
+ }
+
+ /* Add our application global (user defaults) settings */
+ settings = g_settings_new_with_path (self->schema_id, full_path);
+ dzl_settings_sandwich_append (self->settings_sandwich, settings);
+ g_clear_object (&settings);
+
+ IDE_EXIT;
+}
+
+static void
+ide_settings_finalize (GObject *object)
+{
+ IdeSettings *self = (IdeSettings *)object;
+
+ g_clear_object (&self->settings_sandwich);
+ g_clear_pointer (&self->relative_path, g_free);
+ g_clear_pointer (&self->schema_id, g_free);
+ g_clear_pointer (&self->project_id, g_free);
+
+ G_OBJECT_CLASS (ide_settings_parent_class)->finalize (object);
+}
+
+static void
+ide_settings_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ IdeSettings *self = IDE_SETTINGS (object);
+
+ switch (prop_id)
+ {
+ case PROP_PROJECT_ID:
+ g_value_set_string (value, self->project_id);
+ break;
+
+ case PROP_SCHEMA_ID:
+ g_value_set_string (value, ide_settings_get_schema_id (self));
+ break;
+
+ case PROP_RELATIVE_PATH:
+ g_value_set_string (value, ide_settings_get_relative_path (self));
+ break;
+
+ case PROP_IGNORE_PROJECT_SETTINGS:
+ g_value_set_boolean (value, ide_settings_get_ignore_project_settings (self));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+ide_settings_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ IdeSettings *self = IDE_SETTINGS (object);
+
+ switch (prop_id)
+ {
+ case PROP_PROJECT_ID:
+ self->project_id = g_value_dup_string (value);
+ break;
+
+ case PROP_SCHEMA_ID:
+ ide_settings_set_schema_id (self, g_value_get_string (value));
+ break;
+
+ case PROP_RELATIVE_PATH:
+ ide_settings_set_relative_path (self, g_value_get_string (value));
+ break;
+
+ case PROP_IGNORE_PROJECT_SETTINGS:
+ ide_settings_set_ignore_project_settings (self, g_value_get_boolean (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+ide_settings_class_init (IdeSettingsClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->constructed = ide_settings_constructed;
+ object_class->finalize = ide_settings_finalize;
+ object_class->get_property = ide_settings_get_property;
+ object_class->set_property = ide_settings_set_property;
+
+ properties [PROP_PROJECT_ID] =
+ g_param_spec_string ("project-id",
+ "Project Id",
+ "The identifier for the project",
+ NULL,
+ (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+ properties [PROP_IGNORE_PROJECT_SETTINGS] =
+ g_param_spec_boolean ("ignore-project-settings",
+ "Ignore Project Settings",
+ "If project settings should be ignored.",
+ FALSE,
+ (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+ properties [PROP_RELATIVE_PATH] =
+ g_param_spec_string ("relative-path",
+ "Relative Path",
+ "Relative Path",
+ NULL,
+ (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+ properties [PROP_SCHEMA_ID] =
+ g_param_spec_string ("schema-id",
+ "Schema ID",
+ "Schema ID",
+ NULL,
+ (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_properties (object_class, N_PROPS, properties);
+
+ signals [CHANGED] =
+ g_signal_new ("changed",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 1,
+ G_TYPE_STRING);
+}
+
+static void
+ide_settings_init (IdeSettings *self)
+{
+}
+
+IdeSettings *
+ide_settings_new (const gchar *project_id,
+ const gchar *schema_id,
+ const gchar *relative_path,
+ gboolean ignore_project_settings)
+{
+ IdeSettings *ret;
+
+ IDE_ENTRY;
+
+ g_assert (project_id != NULL);
+ g_assert (schema_id != NULL);
+ g_assert (relative_path != NULL);
+
+ ret = g_object_new (IDE_TYPE_SETTINGS,
+ "project-id", project_id,
+ "ignore-project-settings", ignore_project_settings,
+ "relative-path", relative_path,
+ "schema-id", schema_id,
+ NULL);
+
+ IDE_RETURN (ret);
+}
+
+const gchar *
+ide_settings_get_schema_id (IdeSettings *self)
+{
+ g_return_val_if_fail (IDE_IS_SETTINGS (self), NULL);
+
+ return self->schema_id;
+}
+
+const gchar *
+ide_settings_get_relative_path (IdeSettings *self)
+{
+ g_return_val_if_fail (IDE_IS_SETTINGS (self), NULL);
+
+ return self->relative_path;
+}
+
+gboolean
+ide_settings_get_ignore_project_settings (IdeSettings *self)
+{
+ g_return_val_if_fail (IDE_IS_SETTINGS (self), FALSE);
+
+ return self->ignore_project_settings;
+}
+
+GVariant *
+ide_settings_get_default_value (IdeSettings *self,
+ const gchar *key)
+{
+ g_return_val_if_fail (IDE_IS_SETTINGS (self), NULL);
+ g_return_val_if_fail (key != NULL, NULL);
+
+ return dzl_settings_sandwich_get_default_value (self->settings_sandwich, key);
+}
+
+GVariant *
+ide_settings_get_user_value (IdeSettings *self,
+ const gchar *key)
+{
+ g_return_val_if_fail (IDE_IS_SETTINGS (self), NULL);
+ g_return_val_if_fail (key != NULL, NULL);
+
+ return dzl_settings_sandwich_get_user_value (self->settings_sandwich, key);
+}
+
+GVariant *
+ide_settings_get_value (IdeSettings *self,
+ const gchar *key)
+{
+ g_return_val_if_fail (IDE_IS_SETTINGS (self), NULL);
+ g_return_val_if_fail (key != NULL, NULL);
+
+ return dzl_settings_sandwich_get_value (self->settings_sandwich, key);
+}
+
+void
+ide_settings_set_value (IdeSettings *self,
+ const gchar *key,
+ GVariant *value)
+{
+ g_return_if_fail (IDE_IS_SETTINGS (self));
+ g_return_if_fail (key != NULL);
+
+ return dzl_settings_sandwich_set_value (self->settings_sandwich, key, value);
+}
+
+gboolean
+ide_settings_get_boolean (IdeSettings *self,
+ const gchar *key)
+{
+ g_return_val_if_fail (IDE_IS_SETTINGS (self), FALSE);
+ g_return_val_if_fail (key != NULL, FALSE);
+
+ return dzl_settings_sandwich_get_boolean (self->settings_sandwich, key);
+}
+
+gdouble
+ide_settings_get_double (IdeSettings *self,
+ const gchar *key)
+{
+ g_return_val_if_fail (IDE_IS_SETTINGS (self), 0.0);
+ g_return_val_if_fail (key != NULL, 0.0);
+
+ return dzl_settings_sandwich_get_double (self->settings_sandwich, key);
+}
+
+gint
+ide_settings_get_int (IdeSettings *self,
+ const gchar *key)
+{
+ g_return_val_if_fail (IDE_IS_SETTINGS (self), 0);
+ g_return_val_if_fail (key != NULL, 0);
+
+ return dzl_settings_sandwich_get_int (self->settings_sandwich, key);
+}
+
+gchar *
+ide_settings_get_string (IdeSettings *self,
+ const gchar *key)
+{
+ g_return_val_if_fail (IDE_IS_SETTINGS (self), NULL);
+ g_return_val_if_fail (key != NULL, NULL);
+
+ return dzl_settings_sandwich_get_string (self->settings_sandwich, key);
+}
+
+guint
+ide_settings_get_uint (IdeSettings *self,
+ const gchar *key)
+{
+ g_return_val_if_fail (IDE_IS_SETTINGS (self), 0);
+ g_return_val_if_fail (key != NULL, 0);
+
+ return dzl_settings_sandwich_get_uint (self->settings_sandwich, key);
+}
+
+void
+ide_settings_set_boolean (IdeSettings *self,
+ const gchar *key,
+ gboolean val)
+{
+ g_return_if_fail (IDE_IS_SETTINGS (self));
+ g_return_if_fail (key != NULL);
+
+ dzl_settings_sandwich_set_boolean (self->settings_sandwich, key, val);
+}
+
+void
+ide_settings_set_double (IdeSettings *self,
+ const gchar *key,
+ gdouble val)
+{
+ g_return_if_fail (IDE_IS_SETTINGS (self));
+ g_return_if_fail (key != NULL);
+
+ dzl_settings_sandwich_set_double (self->settings_sandwich, key, val);
+}
+
+void
+ide_settings_set_int (IdeSettings *self,
+ const gchar *key,
+ gint val)
+{
+ g_return_if_fail (IDE_IS_SETTINGS (self));
+ g_return_if_fail (key != NULL);
+
+ dzl_settings_sandwich_set_int (self->settings_sandwich, key, val);
+}
+
+void
+ide_settings_set_string (IdeSettings *self,
+ const gchar *key,
+ const gchar *val)
+{
+ g_return_if_fail (IDE_IS_SETTINGS (self));
+ g_return_if_fail (key != NULL);
+
+ dzl_settings_sandwich_set_string (self->settings_sandwich, key, val);
+}
+
+void
+ide_settings_set_uint (IdeSettings *self,
+ const gchar *key,
+ guint val)
+{
+ g_return_if_fail (IDE_IS_SETTINGS (self));
+ g_return_if_fail (key != NULL);
+
+ dzl_settings_sandwich_set_uint (self->settings_sandwich, key, val);
+}
+
+void
+ide_settings_bind (IdeSettings *self,
+ const gchar *key,
+ gpointer object,
+ const gchar *property,
+ GSettingsBindFlags flags)
+{
+ g_return_if_fail (IDE_IS_SETTINGS (self));
+ g_return_if_fail (key != NULL);
+ g_return_if_fail (G_IS_OBJECT (object));
+ g_return_if_fail (property != NULL);
+
+ dzl_settings_sandwich_bind (self->settings_sandwich, key, object, property, flags);
+}
+
+/**
+ * ide_settings_bind_with_mapping:
+ * @self: An #IdeSettings
+ * @key: The settings key
+ * @object: the object to bind to
+ * @property: the property of @object to bind to
+ * @flags: flags for the binding
+ * @get_mapping: (allow-none) (scope notified): variant to value mapping
+ * @set_mapping: (allow-none) (scope notified): value to variant mapping
+ * @user_data: user data for @get_mapping and @set_mapping
+ * @destroy: destroy function to cleanup @user_data.
+ *
+ * Like ide_settings_bind() but allows transforming to and from settings storage using
+ * @get_mapping and @set_mapping transformation functions.
+ *
+ * Call ide_settings_unbind() to unbind the mapping.
+ *
+ * Since: 3.32
+ */
+void
+ide_settings_bind_with_mapping (IdeSettings *self,
+ const gchar *key,
+ gpointer object,
+ const gchar *property,
+ GSettingsBindFlags flags,
+ GSettingsBindGetMapping get_mapping,
+ GSettingsBindSetMapping set_mapping,
+ gpointer user_data,
+ GDestroyNotify destroy)
+{
+ g_return_if_fail (IDE_IS_SETTINGS (self));
+ g_return_if_fail (key != NULL);
+ g_return_if_fail (G_IS_OBJECT (object));
+ g_return_if_fail (property != NULL);
+
+ dzl_settings_sandwich_bind_with_mapping (self->settings_sandwich, key, object, property, flags,
+ get_mapping, set_mapping, user_data, destroy);
+}
+
+void
+ide_settings_unbind (IdeSettings *self,
+ const gchar *property)
+{
+ g_return_if_fail (IDE_IS_SETTINGS (self));
+ g_return_if_fail (property != NULL);
+
+ dzl_settings_sandwich_unbind (self->settings_sandwich, property);
+}
diff --git a/src/libide/core/ide-settings.h b/src/libide/core/ide-settings.h
new file mode 100644
index 000000000..ab61a0043
--- /dev/null
+++ b/src/libide/core/ide-settings.h
@@ -0,0 +1,111 @@
+/* ide-settings.h
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_SETTINGS (ide_settings_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeSettings, ide_settings, IDE, SETTINGS, GObject)
+
+IDE_AVAILABLE_IN_3_32
+IdeSettings *ide_settings_new (const gchar *project_id,
+ const gchar *schema_id,
+ const gchar *relative_path,
+ gboolean ignore_project_settings);
+IDE_AVAILABLE_IN_3_32
+const gchar *ide_settings_get_relative_path (IdeSettings *self);
+IDE_AVAILABLE_IN_3_32
+const gchar *ide_settings_get_schema_id (IdeSettings *self);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_settings_get_ignore_project_settings (IdeSettings *self);
+IDE_AVAILABLE_IN_3_32
+GVariant *ide_settings_get_default_value (IdeSettings *self,
+ const gchar *key);
+IDE_AVAILABLE_IN_3_32
+GVariant *ide_settings_get_user_value (IdeSettings *self,
+ const gchar *key);
+IDE_AVAILABLE_IN_3_32
+GVariant *ide_settings_get_value (IdeSettings *self,
+ const gchar *key);
+IDE_AVAILABLE_IN_3_32
+void ide_settings_set_value (IdeSettings *self,
+ const gchar *key,
+ GVariant *value);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_settings_get_boolean (IdeSettings *self,
+ const gchar *key);
+IDE_AVAILABLE_IN_3_32
+gdouble ide_settings_get_double (IdeSettings *self,
+ const gchar *key);
+IDE_AVAILABLE_IN_3_32
+gint ide_settings_get_int (IdeSettings *self,
+ const gchar *key);
+IDE_AVAILABLE_IN_3_32
+gchar *ide_settings_get_string (IdeSettings *self,
+ const gchar *key);
+IDE_AVAILABLE_IN_3_32
+guint ide_settings_get_uint (IdeSettings *self,
+ const gchar *key);
+IDE_AVAILABLE_IN_3_32
+void ide_settings_set_boolean (IdeSettings *self,
+ const gchar *key,
+ gboolean val);
+IDE_AVAILABLE_IN_3_32
+void ide_settings_set_double (IdeSettings *self,
+ const gchar *key,
+ gdouble val);
+IDE_AVAILABLE_IN_3_32
+void ide_settings_set_int (IdeSettings *self,
+ const gchar *key,
+ gint val);
+IDE_AVAILABLE_IN_3_32
+void ide_settings_set_string (IdeSettings *self,
+ const gchar *key,
+ const gchar *val);
+IDE_AVAILABLE_IN_3_32
+void ide_settings_set_uint (IdeSettings *self,
+ const gchar *key,
+ guint val);
+IDE_AVAILABLE_IN_3_32
+void ide_settings_bind (IdeSettings *self,
+ const gchar *key,
+ gpointer object,
+ const gchar *property,
+ GSettingsBindFlags flags);
+IDE_AVAILABLE_IN_3_32
+void ide_settings_bind_with_mapping (IdeSettings *self,
+ const gchar *key,
+ gpointer object,
+ const gchar *property,
+ GSettingsBindFlags flags,
+ GSettingsBindGetMapping get_mapping,
+ GSettingsBindSetMapping set_mapping,
+ gpointer user_data,
+ GDestroyNotify destroy);
+IDE_AVAILABLE_IN_3_32
+void ide_settings_unbind (IdeSettings *self,
+ const gchar *property);
+
+G_END_DECLS
diff --git a/src/libide/core/ide-transfer-manager.c b/src/libide/core/ide-transfer-manager.c
new file mode 100644
index 000000000..66011bf71
--- /dev/null
+++ b/src/libide/core/ide-transfer-manager.c
@@ -0,0 +1,493 @@
+/* ide-transfer-manager.c
+ *
+ * Copyright 2016-2019 Christian Hergert <chergert redhat com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-transfer-manager"
+
+#include "config.h"
+
+#include "ide-context.h"
+#include "ide-debug.h"
+#include "ide-macros.h"
+
+#include "ide-transfer.h"
+#include "ide-transfer-manager.h"
+
+struct _IdeTransferManager
+{
+ GObject parent_instance;
+ GPtrArray *transfers;
+};
+
+static void list_model_iface_init (GListModelInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (IdeTransferManager, ide_transfer_manager, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init))
+
+enum {
+ PROP_0,
+ PROP_HAS_ACTIVE,
+ PROP_PROGRESS,
+ N_PROPS
+};
+
+enum {
+ TRANSFER_COMPLETED,
+ TRANSFER_FAILED,
+ ALL_TRANSFERS_COMPLETED,
+ N_SIGNALS
+};
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+/**
+ * ide_transfer_manager_get_has_active:
+ *
+ * Gets if there are active transfers.
+ *
+ * Returns: %TRUE if there are active transfers.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_transfer_manager_get_has_active (IdeTransferManager *self)
+{
+ g_return_val_if_fail (IDE_IS_TRANSFER_MANAGER (self), FALSE);
+
+ for (guint i = 0; i < self->transfers->len; i++)
+ {
+ IdeTransfer *transfer = g_ptr_array_index (self->transfers, i);
+
+ if (ide_transfer_get_active (transfer))
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static void
+ide_transfer_manager_finalize (GObject *object)
+{
+ IdeTransferManager *self = (IdeTransferManager *)object;
+
+ g_clear_pointer (&self->transfers, g_ptr_array_unref);
+
+ G_OBJECT_CLASS (ide_transfer_manager_parent_class)->finalize (object);
+}
+
+static void
+ide_transfer_manager_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ IdeTransferManager *self = IDE_TRANSFER_MANAGER (object);
+
+ switch (prop_id)
+ {
+ case PROP_HAS_ACTIVE:
+ g_value_set_boolean (value, ide_transfer_manager_get_has_active (self));
+ break;
+
+ case PROP_PROGRESS:
+ g_value_set_double (value, ide_transfer_manager_get_progress (self));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+ide_transfer_manager_class_init (IdeTransferManagerClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->finalize = ide_transfer_manager_finalize;
+ object_class->get_property = ide_transfer_manager_get_property;
+
+ /**
+ * IdeTransferManager:has-active:
+ *
+ * If there are transfers active, this will be set.
+ *
+ * Since: 3.32
+ */
+ properties [PROP_HAS_ACTIVE] =
+ g_param_spec_boolean ("has-active",
+ "Has Active",
+ "Has Active",
+ FALSE,
+ (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * IdeTransferManager:progress:
+ *
+ * A double between and including 0.0 and 1.0 describing the progress of
+ * all tasks.
+ *
+ * Since: 3.32
+ */
+ properties [PROP_PROGRESS] =
+ g_param_spec_double ("progress",
+ "Progress",
+ "Progress",
+ 0.0,
+ 1.0,
+ 0.0,
+ (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_properties (object_class, N_PROPS, properties);
+
+ /**
+ * IdeTransferManager::all-transfers-completed:
+ *
+ * This signal is emitted when all of the transfers have completed or failed.
+ *
+ * Since: 3.32
+ */
+ signals [ALL_TRANSFERS_COMPLETED] =
+ g_signal_new ("all-transfers-completed",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ 0, NULL, NULL, NULL, G_TYPE_NONE, 0);
+
+ /**
+ * IdeTransferManager::transfer-completed:
+ * @self: An #IdeTransferManager
+ * @transfer: An #IdeTransfer
+ *
+ * This signal is emitted when a transfer has completed successfully.
+ *
+ * Since: 3.32
+ */
+ signals [TRANSFER_COMPLETED] =
+ g_signal_new ("transfer-completed",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE, 1, IDE_TYPE_TRANSFER);
+
+ /**
+ * IdeTransferManager::transfer-failed:
+ * @self: An #IdeTransferManager
+ * @transfer: An #IdeTransfer
+ * @reason: (in): The reason for the failure.
+ *
+ * This signal is emitted when a transfer has failed to complete
+ * successfully.
+ *
+ * Since: 3.32
+ */
+ signals [TRANSFER_FAILED] =
+ g_signal_new ("transfer-failed",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE, 2, IDE_TYPE_TRANSFER, G_TYPE_ERROR);
+}
+
+static void
+ide_transfer_manager_init (IdeTransferManager *self)
+{
+ self->transfers = g_ptr_array_new_with_free_func (g_object_unref);
+}
+
+static void
+ide_transfer_manager_notify_progress (IdeTransferManager *self,
+ GParamSpec *pspec,
+ IdeTransfer *transfer)
+{
+ g_assert (IDE_IS_TRANSFER_MANAGER (self));
+ g_assert (IDE_IS_TRANSFER (transfer));
+
+ g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PROGRESS]);
+}
+
+static gboolean
+ide_transfer_manager_append (IdeTransferManager *self,
+ IdeTransfer *transfer)
+{
+ guint position;
+
+ IDE_ENTRY;
+
+ g_return_val_if_fail (IDE_IS_TRANSFER_MANAGER (self), FALSE);
+ g_return_val_if_fail (IDE_IS_TRANSFER (transfer), FALSE);
+
+ for (guint i = 0; i < self->transfers->len; i++)
+ {
+ if (transfer == (IdeTransfer *)g_ptr_array_index (self->transfers, i))
+ IDE_RETURN (FALSE);
+ }
+
+ g_signal_connect_object (transfer,
+ "notify::progress",
+ G_CALLBACK (ide_transfer_manager_notify_progress),
+ self,
+ G_CONNECT_SWAPPED);
+
+ position = self->transfers->len;
+ g_ptr_array_add (self->transfers, g_object_ref (transfer));
+ g_list_model_items_changed (G_LIST_MODEL (self), position, 0, 1);
+
+ IDE_RETURN (TRUE);
+}
+
+void
+ide_transfer_manager_cancel_all (IdeTransferManager *self)
+{
+ IDE_ENTRY;
+
+ g_return_if_fail (IDE_IS_TRANSFER_MANAGER (self));
+
+ for (guint i = 0; i < self->transfers->len; i++)
+ {
+ IdeTransfer *transfer = g_ptr_array_index (self->transfers, i);
+
+ ide_transfer_cancel (transfer);
+ }
+
+ IDE_EXIT;
+}
+
+/**
+ * ide_transfer_manager_clear:
+ *
+ * Removes all transfers from the manager that are completed.
+ *
+ * Since: 3.32
+ */
+void
+ide_transfer_manager_clear (IdeTransferManager *self)
+{
+ IDE_ENTRY;
+
+ g_return_if_fail (IDE_IS_TRANSFER_MANAGER (self));
+
+ for (guint i = self->transfers->len; i > 0; i--)
+ {
+ IdeTransfer *transfer = g_ptr_array_index (self->transfers, i - 1);
+
+ if (!ide_transfer_get_active (transfer))
+ {
+ g_ptr_array_remove_index (self->transfers, i - 1);
+ g_list_model_items_changed (G_LIST_MODEL (self), i - 1, 1, 0);
+ }
+ }
+
+ IDE_EXIT;
+}
+
+static GType
+ide_transfer_manager_get_item_type (GListModel *model)
+{
+ return IDE_TYPE_TRANSFER;
+}
+
+static guint
+ide_transfer_manager_get_n_items (GListModel *model)
+{
+ IdeTransferManager *self = (IdeTransferManager *)model;
+
+ g_assert (IDE_IS_TRANSFER_MANAGER (self));
+
+ return self->transfers->len;
+}
+
+static gpointer
+ide_transfer_manager_get_item (GListModel *model,
+ guint position)
+{
+ IdeTransferManager *self = (IdeTransferManager *)model;
+
+ g_assert (IDE_IS_TRANSFER_MANAGER (self));
+
+ if G_UNLIKELY (position >= self->transfers->len)
+ return NULL;
+
+ return g_object_ref (g_ptr_array_index (self->transfers, position));
+}
+
+static void
+list_model_iface_init (GListModelInterface *iface)
+{
+ iface->get_item_type = ide_transfer_manager_get_item_type;
+ iface->get_n_items = ide_transfer_manager_get_n_items;
+ iface->get_item = ide_transfer_manager_get_item;
+}
+
+gdouble
+ide_transfer_manager_get_progress (IdeTransferManager *self)
+{
+ gdouble total = 0.0;
+
+ g_return_val_if_fail (IDE_IS_TRANSFER_MANAGER (self), 0.0);
+
+ if (self->transfers->len > 0)
+ {
+ guint count = 0;
+
+ for (guint i = 0; i < self->transfers->len; i++)
+ {
+ IdeTransfer *transfer = g_ptr_array_index (self->transfers, i);
+ gdouble progress = ide_transfer_get_progress (transfer);
+
+ if (ide_transfer_get_completed (transfer) || ide_transfer_get_active (transfer))
+ {
+ total += MAX (0.0, MIN (1.0, progress));
+ count++;
+ }
+ }
+
+ if (count != 0)
+ total /= (gdouble)count;
+ }
+
+ return total;
+}
+
+static void
+ide_transfer_manager_execute_cb (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ IdeTransfer *transfer = (IdeTransfer *)object;
+ IdeTransferManager *self;
+ g_autoptr(GTask) task = user_data;
+ g_autoptr(GError) error = NULL;
+
+ IDE_ENTRY;
+
+ g_assert (IDE_IS_TRANSFER (transfer));
+ g_assert (G_IS_TASK (task));
+
+ self = g_task_get_source_object (task);
+
+ if (!ide_transfer_execute_finish (transfer, result, &error))
+ {
+ g_signal_emit (self, signals[TRANSFER_FAILED], 0, transfer, error);
+ g_task_return_error (task, g_steal_pointer (&error));
+ IDE_GOTO (notify_properties);
+ }
+ else
+ {
+ g_signal_emit (self, signals[TRANSFER_COMPLETED], 0, transfer);
+ g_task_return_boolean (task, TRUE);
+ }
+
+ if (!ide_transfer_manager_get_has_active (self))
+ g_signal_emit (self, signals[ALL_TRANSFERS_COMPLETED], 0);
+
+notify_properties:
+ g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_HAS_ACTIVE]);
+ g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PROGRESS]);
+
+ IDE_EXIT;
+}
+
+/**
+ * ide_transfer_manager_execute_async:
+ * @self: An #IdeTransferManager
+ * @cancellable: (nullable): a #GCancellable
+ * @callback: (nullable): A callback or %NULL
+ * @user_data: user data for @callback
+ *
+ * This is a convenience function that will queue @transfer into the transfer
+ * manager and execute callback upon completion of the transfer. The success
+ * or failure #GError will be propagated to the caller via
+ * ide_transfer_manager_execute_finish().
+ *
+ * Since: 3.32
+ */
+void
+ide_transfer_manager_execute_async (IdeTransferManager *self,
+ IdeTransfer *transfer,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ g_autoptr(GTask) task = NULL;
+
+ IDE_ENTRY;
+
+ g_return_if_fail (!self || IDE_IS_TRANSFER_MANAGER (self));
+ g_return_if_fail (IDE_IS_TRANSFER (transfer));
+ g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+ if (self == NULL)
+ self = ide_transfer_manager_get_default ();
+
+ task = g_task_new (self, cancellable, callback, user_data);
+ g_task_set_source_tag (task, ide_transfer_manager_execute_async);
+
+ if (!ide_transfer_manager_append (self, transfer))
+ {
+ if (ide_transfer_get_active (transfer))
+ {
+ g_warning ("%s is already active, ignoring transfer request",
+ G_OBJECT_TYPE_NAME (transfer));
+ IDE_EXIT;
+ }
+ }
+
+ ide_transfer_execute_async (transfer,
+ cancellable,
+ ide_transfer_manager_execute_cb,
+ g_steal_pointer (&task));
+
+ IDE_EXIT;
+}
+
+gboolean
+ide_transfer_manager_execute_finish (IdeTransferManager *self,
+ GAsyncResult *result,
+ GError **error)
+{
+ g_return_val_if_fail (IDE_IS_TRANSFER_MANAGER (self), FALSE);
+ g_return_val_if_fail (G_IS_TASK (result), FALSE);
+
+ return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+/**
+ * ide_transfer_manager_get_default:
+ *
+ * Gets the #IdeTransferManager singleton.
+ *
+ * Returns: (transfer none): an #IdeTransferManager
+ *
+ * Since: 3.32
+ */
+IdeTransferManager *
+ide_transfer_manager_get_default (void)
+{
+ static IdeTransferManager *instance;
+
+ g_assert (IDE_IS_MAIN_THREAD ());
+ g_assert (!instance || IDE_IS_TRANSFER_MANAGER (instance));
+
+ if (g_once_init_enter (&instance))
+ g_once_init_leave (&instance, g_object_new (IDE_TYPE_TRANSFER_MANAGER, NULL));
+
+ return instance;
+}
diff --git a/src/libide/core/ide-transfer-manager.h b/src/libide/core/ide-transfer-manager.h
new file mode 100644
index 000000000..a3adccb54
--- /dev/null
+++ b/src/libide/core/ide-transfer-manager.h
@@ -0,0 +1,58 @@
+/* ide-transfer-manager.h
+ *
+ * Copyright 2016-2019 Christian Hergert <chergert redhat com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#if !defined (IDE_CORE_INSIDE) && !defined (IDE_CORE_COMPILATION)
+# error "Only <libide-core.h> can be included directly."
+#endif
+
+#include "ide-transfer.h"
+#include "ide-version-macros.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_TRANSFER_MANAGER (ide_transfer_manager_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeTransferManager, ide_transfer_manager, IDE, TRANSFER_MANAGER, GObject)
+
+IDE_AVAILABLE_IN_3_32
+IdeTransferManager *ide_transfer_manager_get_default (void);
+IDE_AVAILABLE_IN_3_32
+gdouble ide_transfer_manager_get_progress (IdeTransferManager *self);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_transfer_manager_get_has_active (IdeTransferManager *self);
+IDE_AVAILABLE_IN_3_32
+void ide_transfer_manager_cancel_all (IdeTransferManager *self);
+IDE_AVAILABLE_IN_3_32
+void ide_transfer_manager_clear (IdeTransferManager *self);
+IDE_AVAILABLE_IN_3_32
+void ide_transfer_manager_execute_async (IdeTransferManager *self,
+ IdeTransfer *transfer,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_transfer_manager_execute_finish (IdeTransferManager *self,
+ GAsyncResult *result,
+ GError **error);
+
+G_END_DECLS
diff --git a/src/libide/core/ide-transfer.c b/src/libide/core/ide-transfer.c
new file mode 100644
index 000000000..a3c35b3e0
--- /dev/null
+++ b/src/libide/core/ide-transfer.c
@@ -0,0 +1,522 @@
+/* ide-transfer.c
+ *
+ * Copyright 2016-2019 Christian Hergert <chergert redhat com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-transfer"
+
+#include "config.h"
+
+#include "ide-debug.h"
+#include "ide-macros.h"
+#include "ide-transfer.h"
+
+typedef struct
+{
+ gchar *icon_name;
+ gchar *status;
+ gchar *title;
+ GCancellable *cancellable;
+ gdouble progress;
+ guint active : 1;
+ guint completed : 1;
+} IdeTransferPrivate;
+
+enum {
+ PROP_0,
+ PROP_ACTIVE,
+ PROP_COMPLETED,
+ PROP_ICON_NAME,
+ PROP_PROGRESS,
+ PROP_STATUS,
+ PROP_TITLE,
+ N_PROPS
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdeTransfer, ide_transfer, IDE_TYPE_OBJECT)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_transfer_real_execute_async (IdeTransfer *self,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ g_autoptr(GTask) task = NULL;
+
+ g_assert (IDE_IS_TRANSFER (self));
+ g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+ task = g_task_new (self, cancellable, callback, user_data);
+ g_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+ide_transfer_real_execute_finish (IdeTransfer *self,
+ GAsyncResult *result,
+ GError **error)
+{
+ g_assert (IDE_IS_TRANSFER (self));
+ g_assert (G_IS_TASK (result));
+
+ return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+ide_transfer_finalize (GObject *object)
+{
+ IdeTransfer *self = (IdeTransfer *)object;
+ IdeTransferPrivate *priv = ide_transfer_get_instance_private (self);
+
+ g_clear_pointer (&priv->icon_name, g_free);
+ g_clear_pointer (&priv->status, g_free);
+ g_clear_pointer (&priv->title, g_free);
+ g_clear_object (&priv->cancellable);
+
+ G_OBJECT_CLASS (ide_transfer_parent_class)->finalize (object);
+}
+
+static void
+ide_transfer_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ IdeTransfer *self = IDE_TRANSFER (object);
+
+ switch (prop_id)
+ {
+ case PROP_ACTIVE:
+ g_value_set_boolean (value, ide_transfer_get_active (self));
+ break;
+
+ case PROP_COMPLETED:
+ g_value_set_boolean (value, ide_transfer_get_completed (self));
+ break;
+
+ case PROP_ICON_NAME:
+ g_value_set_string (value, ide_transfer_get_icon_name (self));
+ break;
+
+ case PROP_PROGRESS:
+ g_value_set_double (value, ide_transfer_get_progress (self));
+ break;
+
+ case PROP_STATUS:
+ g_value_set_string (value, ide_transfer_get_status (self));
+ break;
+
+ case PROP_TITLE:
+ g_value_set_string (value, ide_transfer_get_title (self));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+ide_transfer_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ IdeTransfer *self = IDE_TRANSFER (object);
+
+ switch (prop_id)
+ {
+ case PROP_ICON_NAME:
+ ide_transfer_set_icon_name (self, g_value_get_string (value));
+ break;
+
+ case PROP_PROGRESS:
+ ide_transfer_set_progress (self, g_value_get_double (value));
+ break;
+
+ case PROP_STATUS:
+ ide_transfer_set_status (self, g_value_get_string (value));
+ break;
+
+ case PROP_TITLE:
+ ide_transfer_set_title (self, g_value_get_string (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+ide_transfer_class_init (IdeTransferClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->finalize = ide_transfer_finalize;
+ object_class->get_property = ide_transfer_get_property;
+ object_class->set_property = ide_transfer_set_property;
+
+ klass->execute_async = ide_transfer_real_execute_async;
+ klass->execute_finish = ide_transfer_real_execute_finish;
+
+ properties [PROP_ACTIVE] =
+ g_param_spec_boolean ("active",
+ "Active",
+ "If the transfer is active",
+ FALSE,
+ (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+ properties [PROP_COMPLETED] =
+ g_param_spec_boolean ("completed",
+ "Completed",
+ "If the transfer has completed successfully",
+ FALSE,
+ (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+ properties [PROP_ICON_NAME] =
+ g_param_spec_string ("icon-name",
+ "Icon Name",
+ "The icon to display next to the transfer",
+ "folder-download-symbolic",
+ (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+ properties [PROP_PROGRESS] =
+ g_param_spec_double ("progress",
+ "Progress",
+ "The progress for the transfer between 0 adn 1",
+ 0.0,
+ 1.0,
+ 0.0,
+ (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+ properties [PROP_STATUS] =
+ g_param_spec_string ("status",
+ "Status",
+ "The status message for the transfer",
+ NULL,
+ (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+ properties [PROP_TITLE] =
+ g_param_spec_string ("title",
+ "Title",
+ "The title of the transfer",
+ NULL,
+ (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_transfer_init (IdeTransfer *self)
+{
+}
+
+static void
+ide_transfer_execute_cb (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ IdeTransfer *self = (IdeTransfer *)object;
+ IdeTransferPrivate *priv = ide_transfer_get_instance_private (self);
+ g_autoptr(GTask) task = user_data;
+ g_autoptr(GError) error = NULL;
+
+ IDE_ENTRY;
+
+ g_assert (IDE_IS_TRANSFER (self));
+ g_assert (G_IS_TASK (task));
+
+ priv->active = FALSE;
+ g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ACTIVE]);
+
+ ide_transfer_set_progress (self, 1.0);
+
+ if (!IDE_TRANSFER_GET_CLASS (self)->execute_finish (self, result, &error))
+ {
+ g_task_return_error (task, g_steal_pointer (&error));
+ IDE_EXIT;
+ }
+
+ priv->completed = TRUE;
+ g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_COMPLETED]);
+
+ g_task_return_boolean (task, TRUE);
+
+ IDE_EXIT;
+}
+
+void
+ide_transfer_execute_async (IdeTransfer *self,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ IdeTransferPrivate *priv = ide_transfer_get_instance_private (self);
+ g_autoptr(GTask) task = NULL;
+
+ IDE_ENTRY;
+
+ g_assert (IDE_IS_TRANSFER (self));
+ g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+ /*
+ * We already create our own wrapper task so that we can track completion
+ * cleanly from the subclass implementation. It also allows us to ensure
+ * that the subclasses execute_finish() is guaranteed to be called.
+ */
+ task = g_task_new (self, cancellable, callback, user_data);
+ g_task_set_source_tag (task, ide_transfer_execute_async);
+
+ /*
+ * Wrap our own cancellable so that we can gracefully control
+ * the cancellation of the underlying transfer without affecting
+ * the callers cancellation state.
+ */
+ g_clear_object (&priv->cancellable);
+ priv->cancellable = g_cancellable_new ();
+
+ if (cancellable != NULL)
+ g_signal_connect_object (cancellable,
+ "cancelled",
+ G_CALLBACK (g_cancellable_cancel),
+ priv->cancellable,
+ G_CONNECT_SWAPPED);
+
+ priv->active = TRUE;
+ priv->completed = FALSE;
+
+ IDE_TRANSFER_GET_CLASS (self)->execute_async (self,
+ priv->cancellable,
+ ide_transfer_execute_cb,
+ g_steal_pointer (&task));
+
+ g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ACTIVE]);
+ g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_COMPLETED]);
+
+ IDE_EXIT;
+}
+
+gboolean
+ide_transfer_execute_finish (IdeTransfer *self,
+ GAsyncResult *result,
+ GError **error)
+{
+ gboolean ret;
+
+ IDE_ENTRY;
+
+ g_return_val_if_fail (IDE_IS_TRANSFER (self), FALSE);
+ g_return_val_if_fail (G_IS_TASK (result), FALSE);
+
+ ret = g_task_propagate_boolean (G_TASK (result), error);
+
+ IDE_RETURN (ret);
+}
+
+const gchar *
+ide_transfer_get_icon_name (IdeTransfer *self)
+{
+ IdeTransferPrivate *priv = ide_transfer_get_instance_private (self);
+
+ g_return_val_if_fail (IDE_IS_TRANSFER (self), NULL);
+
+ return priv->icon_name ?: "folder-download-symbolic";
+}
+
+void
+ide_transfer_set_icon_name (IdeTransfer *self,
+ const gchar *icon_name)
+{
+ IdeTransferPrivate *priv = ide_transfer_get_instance_private (self);
+
+ g_return_if_fail (IDE_IS_TRANSFER (self));
+
+ if (g_strcmp0 (priv->icon_name, icon_name) != 0)
+ {
+ g_free (priv->icon_name);
+ priv->icon_name = g_strdup (icon_name);
+ g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ICON_NAME]);
+ }
+}
+
+gdouble
+ide_transfer_get_progress (IdeTransfer *self)
+{
+ IdeTransferPrivate *priv = ide_transfer_get_instance_private (self);
+
+ g_return_val_if_fail (IDE_IS_TRANSFER (self), 0.0);
+
+ return priv->progress;
+}
+
+void
+ide_transfer_set_progress (IdeTransfer *self,
+ gdouble progress)
+{
+ IdeTransferPrivate *priv = ide_transfer_get_instance_private (self);
+
+ g_return_if_fail (IDE_IS_TRANSFER (self));
+
+ if (progress != priv->progress)
+ {
+ priv->progress = progress;
+ g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PROGRESS]);
+ }
+}
+
+const gchar *
+ide_transfer_get_status (IdeTransfer *self)
+{
+ IdeTransferPrivate *priv = ide_transfer_get_instance_private (self);
+
+ g_return_val_if_fail (IDE_IS_TRANSFER (self), NULL);
+
+ return priv->status;
+}
+
+void
+ide_transfer_set_status (IdeTransfer *self,
+ const gchar *status)
+{
+ IdeTransferPrivate *priv = ide_transfer_get_instance_private (self);
+
+ g_return_if_fail (IDE_IS_TRANSFER (self));
+
+ if (g_strcmp0 (priv->status, status) != 0)
+ {
+ g_free (priv->status);
+ priv->status = g_strdup (status);
+ g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_STATUS]);
+ }
+}
+
+const gchar *
+ide_transfer_get_title (IdeTransfer *self)
+{
+ IdeTransferPrivate *priv = ide_transfer_get_instance_private (self);
+
+ g_return_val_if_fail (IDE_IS_TRANSFER (self), NULL);
+
+ return priv->title;
+}
+
+void
+ide_transfer_set_title (IdeTransfer *self,
+ const gchar *title)
+{
+ IdeTransferPrivate *priv = ide_transfer_get_instance_private (self);
+
+ g_return_if_fail (IDE_IS_TRANSFER (self));
+
+ if (g_strcmp0 (priv->title, title) != 0)
+ {
+ g_free (priv->title);
+ priv->title = g_strdup (title);
+ g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TITLE]);
+ }
+}
+
+void
+ide_transfer_cancel (IdeTransfer *self)
+{
+ IdeTransferPrivate *priv = ide_transfer_get_instance_private (self);
+
+ g_return_if_fail (IDE_IS_TRANSFER (self));
+
+ if (!g_cancellable_is_cancelled (priv->cancellable))
+ g_cancellable_cancel (priv->cancellable);
+}
+
+gboolean
+ide_transfer_get_completed (IdeTransfer *self)
+{
+ IdeTransferPrivate *priv = ide_transfer_get_instance_private (self);
+
+ g_return_val_if_fail (IDE_IS_TRANSFER (self), FALSE);
+
+ return priv->completed;
+}
+
+gboolean
+ide_transfer_get_active (IdeTransfer *self)
+{
+ IdeTransferPrivate *priv = ide_transfer_get_instance_private (self);
+
+ g_return_val_if_fail (IDE_IS_TRANSFER (self), FALSE);
+
+ return priv->active;
+}
+
+GQuark
+ide_transfer_error_quark (void)
+{
+ return g_quark_from_static_string ("ide-transfer-error-quark");
+}
+
+static void
+ide_transfer_notification_notify_completed (IdeTransfer *self,
+ GParamSpec *pspec,
+ IdeNotification *notif)
+{
+ g_assert (IDE_IS_MAIN_THREAD ());
+ g_assert (IDE_IS_TRANSFER (self));
+ g_assert (IDE_IS_NOTIFICATION (notif));
+
+ ide_notification_withdraw_in_seconds (notif, 10);
+}
+
+/**
+ * ide_transfer_create_notification:
+ * @self: a #IdeTransfer
+ *
+ * Creates a new #IdeNotification that is updated with the progress
+ * of the #IdeTransfer. This is useful when you need to bridge an
+ * #IdeTransfer into something that can be displayed to the user.
+ *
+ * If the transfer has completed, %NULL is returned.
+ *
+ * Returns: (transfer full) (nullable): an #IdeNotification or %NULL
+ *
+ * Since: 3.32
+ */
+IdeNotification *
+ide_transfer_create_notification (IdeTransfer *self)
+{
+ IdeTransferPrivate *priv = ide_transfer_get_instance_private (self);
+ g_autoptr(IdeNotification) notif = NULL;
+
+ g_return_val_if_fail (IDE_IS_TRANSFER (self), NULL);
+
+ if (priv->completed)
+ return NULL;
+
+ notif = ide_notification_new ();
+ ide_notification_set_has_progress (notif, TRUE);
+ g_object_bind_property (self, "title", notif, "title", G_BINDING_SYNC_CREATE);
+ g_object_bind_property (self, "status", notif, "body", G_BINDING_SYNC_CREATE);
+ g_object_bind_property (self, "progress", notif, "progress", G_BINDING_SYNC_CREATE);
+ g_object_bind_property (self, "icon-name", notif, "icon-name", G_BINDING_SYNC_CREATE);
+
+ g_signal_connect_object (self,
+ "notify::completed",
+ G_CALLBACK (ide_transfer_notification_notify_completed),
+ notif,
+ 0);
+
+ return g_steal_pointer (¬if);
+}
diff --git a/src/libide/core/ide-transfer.h b/src/libide/core/ide-transfer.h
new file mode 100644
index 000000000..380161773
--- /dev/null
+++ b/src/libide/core/ide-transfer.h
@@ -0,0 +1,101 @@
+/* ide-transfer.h
+ *
+ * Copyright 2016-2019 Christian Hergert <chergert redhat com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#if !defined (IDE_CORE_INSIDE) && !defined (IDE_CORE_COMPILATION)
+# error "Only <libide-core.h> can be included directly."
+#endif
+
+#include "ide-notification.h"
+#include "ide-object.h"
+#include "ide-version-macros.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_TRANSFER (ide_transfer_get_type())
+#define IDE_TRANSFER_ERROR (ide_transfer_error_quark())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeTransfer, ide_transfer, IDE, TRANSFER, IdeObject)
+
+struct _IdeTransferClass
+{
+ IdeObjectClass parent_class;
+
+ void (*execute_async) (IdeTransfer *self,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+ gboolean (*execute_finish) (IdeTransfer *self,
+ GAsyncResult *result,
+ GError **error);
+
+ /*< private >*/
+ gpointer _reserved[8];
+};
+
+typedef enum
+{
+ IDE_TRANSFER_ERROR_UNKNOWN = 0,
+ IDE_TRANSFER_ERROR_CONNECTION_IS_METERED = 1,
+} IdeTransferError;
+
+IDE_AVAILABLE_IN_3_32
+GQuark ide_transfer_error_quark (void);
+IDE_AVAILABLE_IN_3_32
+void ide_transfer_cancel (IdeTransfer *self);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_transfer_get_completed (IdeTransfer *self);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_transfer_get_active (IdeTransfer *self);
+IDE_AVAILABLE_IN_3_32
+const gchar *ide_transfer_get_icon_name (IdeTransfer *self);
+IDE_AVAILABLE_IN_3_32
+void ide_transfer_set_icon_name (IdeTransfer *self,
+ const gchar *icon_name);
+IDE_AVAILABLE_IN_3_32
+gdouble ide_transfer_get_progress (IdeTransfer *self);
+IDE_AVAILABLE_IN_3_32
+void ide_transfer_set_progress (IdeTransfer *self,
+ gdouble progress);
+IDE_AVAILABLE_IN_3_32
+const gchar *ide_transfer_get_status (IdeTransfer *self);
+IDE_AVAILABLE_IN_3_32
+void ide_transfer_set_status (IdeTransfer *self,
+ const gchar *status);
+IDE_AVAILABLE_IN_3_32
+const gchar *ide_transfer_get_title (IdeTransfer *self);
+IDE_AVAILABLE_IN_3_32
+void ide_transfer_set_title (IdeTransfer *self,
+ const gchar *title);
+IDE_AVAILABLE_IN_3_32
+void ide_transfer_execute_async (IdeTransfer *self,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_transfer_execute_finish (IdeTransfer *self,
+ GAsyncResult *result,
+ GError **error);
+IDE_AVAILABLE_IN_3_32
+IdeNotification *ide_transfer_create_notification (IdeTransfer *self);
+
+G_END_DECLS
diff --git a/src/libide/ide-version-macros.h b/src/libide/core/ide-version-macros.h
similarity index 94%
rename from src/libide/ide-version-macros.h
rename to src/libide/core/ide-version-macros.h
index 95ef7b632..4e7af41a3 100644
--- a/src/libide/ide-version-macros.h
+++ b/src/libide/core/ide-version-macros.h
@@ -1,6 +1,6 @@
/* ide-version-macros.h
*
- * Copyright 2017-2019 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
*
* 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
@@ -18,11 +18,10 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
-#ifndef IDE_VERSION_MACROS_H
-#define IDE_VERSION_MACROS_H
+#pragma once
-#if !defined(IDE_INSIDE) && !defined(IDE_COMPILATION)
-# error "Only <ide.h> can be included directly."
+#if !defined (IDE_CORE_INSIDE) && !defined (IDE_CORE_COMPILATION)
+# error "Only <libide-core.h> can be included directly."
#endif
#include <glib.h>
@@ -78,7 +77,7 @@
* it is possible to use this symbol to avoid the compiler warnings
* without disabling warning for every deprecated function.
*
- * Since: 3.28
+ * Since: 3.32
*/
#ifndef IDE_VERSION_MIN_REQUIRED
# define IDE_VERSION_MIN_REQUIRED (IDE_VERSION_CUR_STABLE)
@@ -99,7 +98,7 @@
* it is possible to use this symbol to get compiler warnings when
* trying to use that function.
*
- * Since: 3.28
+ * Since: 3.32
*/
#ifndef IDE_VERSION_MAX_ALLOWED
# if IDE_VERSION_MIN_REQUIRED > IDE_VERSION_PREV_STABLE
@@ -159,5 +158,3 @@
#else
# define IDE_AVAILABLE_IN_3_32 _IDE_EXTERN
#endif
-
-#endif /* IDE_VERSION_MACROS_H */
diff --git a/src/libide/ide-version.h.in b/src/libide/core/ide-version.h.in
similarity index 100%
rename from src/libide/ide-version.h.in
rename to src/libide/core/ide-version.h.in
diff --git a/src/libide/core/libide-core.h b/src/libide/core/libide-core.h
new file mode 100644
index 000000000..42d4373ea
--- /dev/null
+++ b/src/libide/core/libide-core.h
@@ -0,0 +1,43 @@
+/* ide-core.h
+ *
+ * Copyright 2018-2019 Christian Hergert <chergert redhat com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gio/gio.h>
+
+#define IDE_CORE_INSIDE
+
+#include "ide-build-ident.h"
+#include "ide-context.h"
+#include "ide-debug.h"
+#include "ide-global.h"
+#include "ide-log.h"
+#include "ide-macros.h"
+#include "ide-notification.h"
+#include "ide-notifications.h"
+#include "ide-object.h"
+#include "ide-object-box.h"
+#include "ide-settings.h"
+#include "ide-transfer.h"
+#include "ide-transfer-manager.h"
+#include "ide-version.h"
+#include "ide-version-macros.h"
+
+#undef IDE_CORE_INSIDE
diff --git a/src/libide/core/meson.build b/src/libide/core/meson.build
new file mode 100644
index 000000000..155d9ea6c
--- /dev/null
+++ b/src/libide/core/meson.build
@@ -0,0 +1,124 @@
+libide_core_header_dir = join_paths(libide_header_dir, 'core')
+libide_core_header_subdir = join_paths(libide_header_subdir, 'core')
+libide_include_directories += include_directories('.')
+
+#
+# Versioning that all libide libraries (re)use
+#
+
+version_data = configuration_data()
+version_data.set('MAJOR_VERSION', MAJOR_VERSION)
+version_data.set('MINOR_VERSION', MINOR_VERSION)
+version_data.set('MICRO_VERSION', MICRO_VERSION)
+version_data.set('VERSION', meson.project_version())
+version_data.set_quoted('BUILD_CHANNEL', get_option('with_channel'))
+version_data.set_quoted('BUILD_TYPE', get_option('buildtype'))
+
+libide_core_version_h = configure_file(
+ input: 'ide-version.h.in',
+ output: 'ide-version.h',
+ install_dir: libide_core_header_dir,
+ install: true,
+ configuration: version_data)
+
+libide_core_generated_headers = [libide_core_version_h]
+
+libide_build_ident_h = vcs_tag(
+ fallback: meson.project_version(),
+ input: 'ide-build-ident.h.in',
+ output: 'ide-build-ident.h',
+)
+libide_core_generated_headers += [libide_build_ident_h]
+
+#
+# Debugging and Tracing Support
+#
+
+libide_core_conf = configuration_data()
+libide_core_conf.set10('ENABLE_TRACING', get_option('tracing'))
+libide_core_conf.set('BUGREPORT_URL', 'https://gitlab.gnome.org/GNOME/gnome-builder/issues')
+
+libide_debug_h = configure_file(
+ input: 'ide-debug.h.in',
+ output: 'ide-debug.h',
+ configuration: libide_core_conf,
+ install: true,
+ install_dir: libide_core_header_dir,
+)
+
+libide_core_generated_headers += [libide_debug_h]
+
+#
+# Public API Headers
+#
+
+libide_core_public_headers = [
+ 'ide-context.h',
+ 'ide-context-addin.h',
+ 'ide-global.h',
+ 'ide-log.h',
+ 'ide-macros.h',
+ 'ide-notification.h',
+ 'ide-notifications.h',
+ 'ide-object.h',
+ 'ide-object-box.h',
+ 'ide-settings.h',
+ 'ide-transfer.h',
+ 'ide-transfer-manager.h',
+ 'ide-version-macros.h',
+ 'libide-core.h',
+]
+
+install_headers(libide_core_public_headers, subdir: libide_core_header_subdir)
+
+#
+# Sources
+#
+
+libide_core_public_sources = [
+ 'ide-context.c',
+ 'ide-context-addin.c',
+ 'ide-global.c',
+ 'ide-log.c',
+ 'ide-notification.c',
+ 'ide-notifications.c',
+ 'ide-object.c',
+ 'ide-object-box.c',
+ 'ide-object-notify.c',
+ 'ide-settings.c',
+ 'ide-transfer.c',
+ 'ide-transfer-manager.c',
+]
+
+libide_core_sources = []
+libide_core_sources += libide_core_generated_headers
+libide_core_sources += libide_core_public_sources
+
+#
+# Library Definitions
+#
+
+libide_core_deps = [
+ libgio_dep,
+ libgtk_dep,
+ libdazzle_dep,
+ libpeas_dep,
+]
+
+libide_core = static_library('ide-core-' + libide_api_version, libide_core_sources,
+ dependencies: libide_core_deps,
+ c_args: libide_args + release_args + ['-DIDE_CORE_COMPILATION'],
+)
+
+libide_core_dep = declare_dependency(
+ sources: libide_core_generated_headers,
+ dependencies: libide_core_deps,
+ link_whole: libide_core,
+ include_directories: include_directories('.'),
+)
+
+gnome_builder_public_sources += files(libide_core_public_sources)
+gnome_builder_public_headers += files(libide_core_public_headers)
+gnome_builder_generated_headers += libide_core_generated_headers
+gnome_builder_include_subdirs += libide_core_header_subdir
+gnome_builder_gir_extra_args += ['--c-include=libide-core.h', '-DIDE_CORE_COMPILATION']
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]