[gnome-builder: 41/139] libide-tree: add new libide-tree static library
- From: Christian Hergert <chergert src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-builder: 41/139] libide-tree: add new libide-tree static library
- Date: Thu, 10 Jan 2019 04:20:46 +0000 (UTC)
commit 897ddd53e7c99ff8d2bd98672fd7d1f38282af03
Author: Christian Hergert <chergert redhat com>
Date: Wed Jan 9 16:27:37 2019 -0800
libide-tree: add new libide-tree static library
The libide-tree static library contains a model for creating lazy-built
trees and displaying them using content created by addins.
It is similar to DzlTree but more specialized for Builder's use-case.
src/libide/tree/ide-tree-addin.c | 366 +++++++
src/libide/tree/ide-tree-addin.h | 149 +++
src/libide/tree/ide-tree-model.c | 1626 +++++++++++++++++++++++++++++++
src/libide/tree/ide-tree-model.h | 72 ++
src/libide/tree/ide-tree-node.c | 1863 ++++++++++++++++++++++++++++++++++++
src/libide/tree/ide-tree-node.h | 172 ++++
src/libide/tree/ide-tree-private.h | 70 ++
src/libide/tree/ide-tree.c | 764 +++++++++++++++
src/libide/tree/ide-tree.h | 67 ++
src/libide/tree/libide-tree.h | 36 +
src/libide/tree/meson.build | 62 ++
11 files changed, 5247 insertions(+)
---
diff --git a/src/libide/tree/ide-tree-addin.c b/src/libide/tree/ide-tree-addin.c
new file mode 100644
index 000000000..973ce7ffa
--- /dev/null
+++ b/src/libide/tree/ide-tree-addin.c
@@ -0,0 +1,366 @@
+/* ide-tree-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-tree-addin"
+
+#include "config.h"
+
+#include <libide-threading.h>
+
+#include "ide-tree-addin.h"
+
+G_DEFINE_INTERFACE (IdeTreeAddin, ide_tree_addin, G_TYPE_OBJECT)
+
+static void
+ide_tree_addin_real_build_children_async (IdeTreeAddin *self,
+ IdeTreeNode *node,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ g_autoptr(IdeTask) task = NULL;
+
+ g_assert (IDE_IS_MAIN_THREAD ());
+ g_assert (IDE_IS_TREE_ADDIN (self));
+ g_assert (IDE_IS_TREE_NODE (node));
+ g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+ task = ide_task_new (self, cancellable, callback, user_data);
+ ide_task_set_source_tag (task, ide_tree_addin_real_build_children_async);
+
+ if (IDE_TREE_ADDIN_GET_IFACE (self)->build_children)
+ IDE_TREE_ADDIN_GET_IFACE (self)->build_children (self, node);
+
+ ide_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+ide_tree_addin_real_build_children_finish (IdeTreeAddin *self,
+ GAsyncResult *result,
+ GError **error)
+{
+ g_assert (IDE_IS_MAIN_THREAD ());
+ g_assert (IDE_IS_TREE_ADDIN (self));
+ g_assert (IDE_IS_TASK (result));
+
+ return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static void
+ide_tree_addin_real_node_dropped_async (IdeTreeAddin *self,
+ IdeTreeNode *drag_node,
+ IdeTreeNode *drop_node,
+ GtkSelectionData *selection,
+ GdkDragAction actions,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ g_assert (IDE_IS_MAIN_THREAD ());
+ g_assert (IDE_IS_TREE_ADDIN (self));
+ g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+ g_task_report_new_error (self, callback, user_data,
+ ide_tree_addin_real_node_dropped_async,
+ G_IO_ERROR,
+ G_IO_ERROR_NOT_SUPPORTED,
+ "Addin does not support dropping nodes");
+}
+
+static gboolean
+ide_tree_addin_real_node_dropped_finish (IdeTreeAddin *self,
+ GAsyncResult *result,
+ GError **error)
+{
+ g_assert (IDE_IS_MAIN_THREAD ());
+ g_assert (IDE_IS_TREE_ADDIN (self));
+ g_assert (G_IS_TASK (result));
+
+ return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+ide_tree_addin_default_init (IdeTreeAddinInterface *iface)
+{
+ iface->build_children_async = ide_tree_addin_real_build_children_async;
+ iface->build_children_finish = ide_tree_addin_real_build_children_finish;
+ iface->node_dropped_async = ide_tree_addin_real_node_dropped_async;
+ iface->node_dropped_finish = ide_tree_addin_real_node_dropped_finish;
+}
+
+/**
+ * ide_tree_addin_build_children_async:
+ * @self: a #IdeTreeAddin
+ * @node: a #IdeTreeNode
+ * @cancellable: (nullable): a #GCancellable or %NULL
+ * @callback: a #GAsyncReadyCallback or %NULL
+ * @user_data: user data for @callback
+ *
+ * This function is called when building the children of a node. This
+ * happens when expanding an node that might have children, or building the
+ * root node.
+ *
+ * You may want to use ide_tree_node_holds() to determine if the node
+ * contains an item that you are interested in.
+ *
+ * This function will call the synchronous form of
+ * IdeTreeAddin.build_children() if no asynchronous form is available.
+ *
+ * Since: 3.32
+ */
+void
+ide_tree_addin_build_children_async (IdeTreeAddin *self,
+ IdeTreeNode *node,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ g_return_if_fail (IDE_IS_MAIN_THREAD ());
+ g_return_if_fail (IDE_IS_TREE_ADDIN (self));
+ g_return_if_fail (IDE_IS_TREE_NODE (node));
+ g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+ IDE_TREE_ADDIN_GET_IFACE (self)->build_children_async (self, node, cancellable, callback, user_data);
+}
+
+/**
+ * ide_tree_addin_build_children_finish:
+ * @self: a #IdeTreeAddin
+ * @result: result given to callback in ide_tree_addin_build_children_async()
+ * @error: a location for a #GError, or %NULL
+ *
+ * Completes an asynchronous request to ide_tree_addin_build_children_async().
+ *
+ * Returns: %TRUE if successful; otherwise %FALSE and @error is set.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_tree_addin_build_children_finish (IdeTreeAddin *self,
+ GAsyncResult *result,
+ GError **error)
+{
+ g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+ g_return_val_if_fail (IDE_IS_TREE_ADDIN (self), FALSE);
+ g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+ return IDE_TREE_ADDIN_GET_IFACE (self)->build_children_finish (self, result, error);
+}
+
+/**
+ * ide_tree_addin_build_node:
+ * @self: a #IdeTreeAddin
+ * @node: a #IdeTreeNode
+ *
+ * This function is called when preparing a node for display in the tree.
+ *
+ * Addins should adjust any state on the node that makes sense based on the
+ * addin.
+ *
+ * You may want to use ide_tree_node_holds() to determine if the node
+ * contains an item that you are interested in.
+ *
+ * Since: 3.32
+ */
+void
+ide_tree_addin_build_node (IdeTreeAddin *self,
+ IdeTreeNode *node)
+{
+ g_return_if_fail (IDE_IS_MAIN_THREAD ());
+ g_return_if_fail (IDE_IS_TREE_ADDIN (self));
+ g_return_if_fail (IDE_IS_TREE_NODE (node));
+
+ if (IDE_TREE_ADDIN_GET_IFACE (self)->build_node)
+ IDE_TREE_ADDIN_GET_IFACE (self)->build_node (self, node);
+}
+
+/**
+ * ide_tree_addin_activated:
+ * @self: an #IdeTreeAddin
+ * @tree: an #IdeTree
+ * @node: an #IdeTreeNode
+ *
+ * This function is called when a node has been activated in the tree
+ * and allows for the addin to perform any necessary operations in response
+ * to that.
+ *
+ * If the addin performs an action based on the activation request, then it
+ * should return %TRUE from this function so that no further addins may
+ * respond to the action.
+ *
+ * Returns: %TRUE if the activation was handled, otherwise %FALSE
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_tree_addin_node_activated (IdeTreeAddin *self,
+ IdeTree *tree,
+ IdeTreeNode *node)
+{
+ g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+ g_return_val_if_fail (IDE_IS_TREE_ADDIN (self), FALSE);
+ g_return_val_if_fail (IDE_IS_TREE (tree), FALSE);
+ g_return_val_if_fail (IDE_IS_TREE_NODE (node), FALSE);
+
+ if (IDE_TREE_ADDIN_GET_IFACE (self)->node_activated)
+ return IDE_TREE_ADDIN_GET_IFACE (self)->node_activated (self, tree, node);
+
+ return FALSE;
+}
+
+void
+ide_tree_addin_load (IdeTreeAddin *self,
+ IdeTree *tree,
+ IdeTreeModel *model)
+{
+ g_return_if_fail (IDE_IS_TREE_ADDIN (self));
+ g_return_if_fail (IDE_IS_TREE (tree));
+ g_return_if_fail (IDE_IS_TREE_MODEL (model));
+
+ if (IDE_TREE_ADDIN_GET_IFACE (self)->load)
+ IDE_TREE_ADDIN_GET_IFACE (self)->load (self, tree, model);
+}
+
+void
+ide_tree_addin_unload (IdeTreeAddin *self,
+ IdeTree *tree,
+ IdeTreeModel *model)
+{
+ g_return_if_fail (IDE_IS_TREE_ADDIN (self));
+ g_return_if_fail (IDE_IS_TREE (tree));
+ g_return_if_fail (IDE_IS_TREE_MODEL (model));
+
+ if (IDE_TREE_ADDIN_GET_IFACE (self)->unload)
+ IDE_TREE_ADDIN_GET_IFACE (self)->unload (self, tree, model);
+}
+
+void
+ide_tree_addin_selection_changed (IdeTreeAddin *self,
+ IdeTreeNode *selection)
+{
+ g_return_if_fail (IDE_IS_TREE_ADDIN (self));
+ g_return_if_fail (!selection || IDE_IS_TREE_NODE (selection));
+
+ if (IDE_TREE_ADDIN_GET_IFACE (self)->selection_changed)
+ IDE_TREE_ADDIN_GET_IFACE (self)->selection_changed (self, selection);
+}
+
+void
+ide_tree_addin_node_expanded (IdeTreeAddin *self,
+ IdeTreeNode *node)
+{
+ g_return_if_fail (IDE_IS_TREE_ADDIN (self));
+ g_return_if_fail (IDE_IS_TREE_NODE (node));
+
+ if (IDE_TREE_ADDIN_GET_IFACE (self)->node_expanded)
+ IDE_TREE_ADDIN_GET_IFACE (self)->node_expanded (self, node);
+}
+
+void
+ide_tree_addin_node_collapsed (IdeTreeAddin *self,
+ IdeTreeNode *node)
+{
+ g_return_if_fail (IDE_IS_TREE_ADDIN (self));
+ g_return_if_fail (IDE_IS_TREE_NODE (node));
+
+ if (IDE_TREE_ADDIN_GET_IFACE (self)->node_collapsed)
+ IDE_TREE_ADDIN_GET_IFACE (self)->node_collapsed (self, node);
+}
+
+gboolean
+ide_tree_addin_node_draggable (IdeTreeAddin *self,
+ IdeTreeNode *node)
+{
+ g_return_val_if_fail (IDE_IS_TREE_ADDIN (self), FALSE);
+ g_return_val_if_fail (IDE_IS_TREE_NODE (node), FALSE);
+
+ if (IDE_TREE_ADDIN_GET_IFACE (self)->node_draggable)
+ return IDE_TREE_ADDIN_GET_IFACE (self)->node_draggable (self, node);
+
+ return FALSE;
+}
+
+gboolean
+ide_tree_addin_node_droppable (IdeTreeAddin *self,
+ IdeTreeNode *drag_node,
+ IdeTreeNode *drop_node,
+ GtkSelectionData *selection)
+{
+ g_return_val_if_fail (IDE_IS_TREE_ADDIN (self), FALSE);
+ g_return_val_if_fail (!drag_node || IDE_IS_TREE_NODE (drag_node), FALSE);
+ g_return_val_if_fail (!drop_node || IDE_IS_TREE_NODE (drop_node), FALSE);
+
+ if (IDE_TREE_ADDIN_GET_IFACE (self)->node_droppable)
+ return IDE_TREE_ADDIN_GET_IFACE (self)->node_droppable (self, drag_node, drop_node, selection);
+
+ return FALSE;
+}
+
+void
+ide_tree_addin_node_dropped_async (IdeTreeAddin *self,
+ IdeTreeNode *drag_node,
+ IdeTreeNode *drop_node,
+ GtkSelectionData *selection,
+ GdkDragAction actions,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ g_return_if_fail (IDE_IS_MAIN_THREAD ());
+ g_return_if_fail (IDE_IS_TREE_ADDIN (self));
+ g_return_if_fail (!drag_node || IDE_IS_TREE_NODE (drag_node));
+ g_return_if_fail (!drop_node || IDE_IS_TREE_NODE (drop_node));
+ g_return_if_fail (selection != NULL);
+ g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+ IDE_TREE_ADDIN_GET_IFACE (self)->node_dropped_async (self,
+ drag_node,
+ drop_node,
+ selection,
+ actions,
+ cancellable,
+ callback,
+ user_data);
+}
+
+gboolean
+ide_tree_addin_node_dropped_finish (IdeTreeAddin *self,
+ GAsyncResult *result,
+ GError **error)
+{
+ g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+ g_return_val_if_fail (IDE_IS_TREE_ADDIN (self), FALSE);
+ g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+ return IDE_TREE_ADDIN_GET_IFACE (self)->node_dropped_finish (self, result, error);
+}
+
+void
+ide_tree_addin_cell_data_func (IdeTreeAddin *self,
+ IdeTreeNode *node,
+ GtkCellRenderer *cell)
+{
+ g_return_if_fail (IDE_IS_TREE_ADDIN (self));
+ g_return_if_fail (IDE_IS_TREE_NODE (node));
+ g_return_if_fail (GTK_IS_CELL_RENDERER (cell));
+
+ if (IDE_TREE_ADDIN_GET_IFACE (self)->cell_data_func)
+ IDE_TREE_ADDIN_GET_IFACE (self)->cell_data_func (self, node, cell);
+}
diff --git a/src/libide/tree/ide-tree-addin.h b/src/libide/tree/ide-tree-addin.h
new file mode 100644
index 000000000..92275620d
--- /dev/null
+++ b/src/libide/tree/ide-tree-addin.h
@@ -0,0 +1,149 @@
+/* ide-tree-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 <libide-core.h>
+
+#include "ide-tree.h"
+#include "ide-tree-model.h"
+#include "ide-tree-node.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_TREE_ADDIN (ide_tree_addin_get_type ())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeTreeAddin, ide_tree_addin, IDE, TREE_ADDIN, GObject)
+
+struct _IdeTreeAddinInterface
+{
+ GTypeInterface parent;
+
+ void (*load) (IdeTreeAddin *self,
+ IdeTree *tree,
+ IdeTreeModel *model);
+ void (*unload) (IdeTreeAddin *self,
+ IdeTree *tree,
+ IdeTreeModel *model);
+ void (*build_node) (IdeTreeAddin *self,
+ IdeTreeNode *node);
+ void (*build_children) (IdeTreeAddin *self,
+ IdeTreeNode *node);
+ void (*build_children_async) (IdeTreeAddin *self,
+ IdeTreeNode *node,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+ gboolean (*build_children_finish) (IdeTreeAddin *self,
+ GAsyncResult *result,
+ GError **error);
+ void (*cell_data_func) (IdeTreeAddin *self,
+ IdeTreeNode *node,
+ GtkCellRenderer *cell);
+ gboolean (*node_activated) (IdeTreeAddin *self,
+ IdeTree *tree,
+ IdeTreeNode *node);
+ void (*selection_changed) (IdeTreeAddin *self,
+ IdeTreeNode *selection);
+ void (*node_expanded) (IdeTreeAddin *self,
+ IdeTreeNode *node);
+ void (*node_collapsed) (IdeTreeAddin *self,
+ IdeTreeNode *node);
+ gboolean (*node_draggable) (IdeTreeAddin *self,
+ IdeTreeNode *node);
+ gboolean (*node_droppable) (IdeTreeAddin *self,
+ IdeTreeNode *drag_node,
+ IdeTreeNode *drop_node,
+ GtkSelectionData *selection);
+ void (*node_dropped_async) (IdeTreeAddin *self,
+ IdeTreeNode *drag_node,
+ IdeTreeNode *drop_node,
+ GtkSelectionData *selection,
+ GdkDragAction actions,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+ gboolean (*node_dropped_finish) (IdeTreeAddin *self,
+ GAsyncResult *result,
+ GError **error);
+};
+
+IDE_AVAILABLE_IN_3_32
+void ide_tree_addin_load (IdeTreeAddin *self,
+ IdeTree *tree,
+ IdeTreeModel *model);
+IDE_AVAILABLE_IN_3_32
+void ide_tree_addin_unload (IdeTreeAddin *self,
+ IdeTree *tree,
+ IdeTreeModel *model);
+IDE_AVAILABLE_IN_3_32
+void ide_tree_addin_build_node (IdeTreeAddin *self,
+ IdeTreeNode *node);
+IDE_AVAILABLE_IN_3_32
+void ide_tree_addin_build_children_async (IdeTreeAddin *self,
+ IdeTreeNode *node,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_tree_addin_build_children_finish (IdeTreeAddin *self,
+ GAsyncResult *result,
+ GError **error);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_tree_addin_node_activated (IdeTreeAddin *self,
+ IdeTree *tree,
+ IdeTreeNode *node);
+IDE_AVAILABLE_IN_3_32
+void ide_tree_addin_selection_changed (IdeTreeAddin *self,
+ IdeTreeNode *selection);
+IDE_AVAILABLE_IN_3_32
+void ide_tree_addin_node_expanded (IdeTreeAddin *self,
+ IdeTreeNode *node);
+IDE_AVAILABLE_IN_3_32
+void ide_tree_addin_node_collapsed (IdeTreeAddin *self,
+ IdeTreeNode *node);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_tree_addin_node_draggable (IdeTreeAddin *self,
+ IdeTreeNode *node);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_tree_addin_node_droppable (IdeTreeAddin *self,
+ IdeTreeNode *drag_node,
+ IdeTreeNode *drop_node,
+ GtkSelectionData *selection);
+IDE_AVAILABLE_IN_3_32
+void ide_tree_addin_node_dropped_async (IdeTreeAddin *self,
+ IdeTreeNode *drag_node,
+ IdeTreeNode *drop_node,
+ GtkSelectionData *selection,
+ GdkDragAction actions,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_tree_addin_node_dropped_finish (IdeTreeAddin *self,
+ GAsyncResult *result,
+ GError **error);
+IDE_AVAILABLE_IN_3_32
+void ide_tree_addin_cell_data_func (IdeTreeAddin *self,
+ IdeTreeNode *node,
+ GtkCellRenderer *cell);
+
+G_END_DECLS
diff --git a/src/libide/tree/ide-tree-model.c b/src/libide/tree/ide-tree-model.c
new file mode 100644
index 000000000..aa797d645
--- /dev/null
+++ b/src/libide/tree/ide-tree-model.c
@@ -0,0 +1,1626 @@
+/* ide-tree-model.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-tree-model"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libide-plugins.h>
+#include <libide-threading.h>
+#include <string.h>
+
+#include "ide-tree-addin.h"
+#include "ide-tree-model.h"
+#include "ide-tree-node.h"
+#include "ide-tree-private.h"
+#include "ide-tree.h"
+
+struct _IdeTreeModel
+{
+ IdeObject parent_instance;
+ IdeExtensionSetAdapter *addins;
+ gchar *kind;
+ IdeTreeNode *root;
+ IdeTree *tree;
+};
+
+typedef struct
+{
+ IdeTreeNode *drag_node;
+ IdeTreeNode *drop_node;
+ GtkSelectionData *selection;
+ GdkDragAction actions;
+ gint n_active;
+} DragDataReceived;
+
+static void tree_model_iface_init (GtkTreeModelIface *iface);
+static void tree_drag_dest_iface_init (GtkTreeDragDestIface *iface);
+static void tree_drag_source_iface_init (GtkTreeDragSourceIface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (IdeTreeModel, ide_tree_model, IDE_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_TREE_MODEL, tree_model_iface_init)
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_TREE_DRAG_DEST, tree_drag_dest_iface_init)
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_TREE_DRAG_SOURCE, tree_drag_source_iface_init))
+
+enum {
+ PROP_0,
+ PROP_KIND,
+ PROP_ROOT,
+ PROP_TREE,
+ N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+drag_data_received_free (DragDataReceived *data)
+{
+ g_assert (data != NULL);
+ g_assert (!data->drag_node || IDE_IS_TREE_NODE (data->drag_node));
+ g_assert (!data->drop_node || IDE_IS_TREE_NODE (data->drop_node));
+ g_assert (data->n_active == 0);
+
+ g_clear_object (&data->drag_node);
+ g_clear_object (&data->drop_node);
+ g_clear_pointer (&data->selection, gtk_selection_data_free);
+ g_slice_free (DragDataReceived, data);
+}
+
+static IdeTreeNode *
+create_root (void)
+{
+ return g_object_new (IDE_TYPE_TREE_NODE,
+ "children-possible", TRUE,
+ NULL);
+}
+
+static void
+ide_tree_model_build_node_cb (IdeExtensionSetAdapter *set,
+ PeasPluginInfo *plugin_info,
+ PeasExtension *exten,
+ gpointer user_data)
+{
+ IdeTreeAddin *addin = (IdeTreeAddin *)exten;
+ IdeTreeNode *node = user_data;
+
+ g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+ g_assert (plugin_info != NULL);
+ g_assert (IDE_IS_TREE_ADDIN (addin));
+ g_assert (IDE_IS_TREE_NODE (node));
+
+ ide_tree_addin_build_node (addin, node);
+}
+
+void
+_ide_tree_model_build_node (IdeTreeModel *self,
+ IdeTreeNode *node)
+{
+ g_assert (IDE_IS_TREE_MODEL (self));
+ g_assert (IDE_IS_TREE_NODE (node));
+
+ ide_extension_set_adapter_foreach (self->addins,
+ ide_tree_model_build_node_cb,
+ node);
+}
+
+static IdeTreeNodeVisit
+ide_tree_model_addin_added_traverse_cb (IdeTreeNode *node,
+ gpointer user_data)
+{
+ IdeTreeAddin *addin = user_data;
+
+ g_assert (IDE_IS_TREE_ADDIN (addin));
+ g_assert (IDE_IS_TREE_NODE (node));
+
+ if (!ide_tree_node_is_empty (node))
+ {
+ ide_tree_addin_build_node (addin, node);
+
+ if (ide_tree_node_get_children_possible (node))
+ _ide_tree_node_set_needs_build_children (node, TRUE);
+ }
+
+ return IDE_TREE_NODE_VISIT_CHILDREN;
+}
+
+static void
+ide_tree_model_addin_added_cb (IdeExtensionSetAdapter *adapter,
+ PeasPluginInfo *plugin_info,
+ PeasExtension *exten,
+ gpointer user_data)
+{
+ IdeTreeModel *self = user_data;
+ IdeTreeAddin *addin = (IdeTreeAddin *)exten;
+
+ g_assert (IDE_IS_EXTENSION_SET_ADAPTER (adapter));
+ g_assert (plugin_info != NULL);
+ g_assert (IDE_IS_TREE_ADDIN (addin));
+ g_assert (IDE_IS_TREE_MODEL (self));
+ g_assert (IDE_IS_TREE (self->tree));
+
+ ide_tree_addin_load (addin, self->tree, self);
+
+ ide_tree_node_traverse (self->root,
+ G_PRE_ORDER,
+ G_TRAVERSE_ALL,
+ -1,
+ ide_tree_model_addin_added_traverse_cb,
+ addin);
+}
+
+static void
+ide_tree_model_addin_removed_cb (IdeExtensionSetAdapter *adapter,
+ PeasPluginInfo *plugin_info,
+ PeasExtension *exten,
+ gpointer user_data)
+{
+ IdeTreeModel *self = user_data;
+ IdeTreeAddin *addin = (IdeTreeAddin *)exten;
+
+ g_assert (IDE_IS_EXTENSION_SET_ADAPTER (adapter));
+ g_assert (plugin_info != NULL);
+ g_assert (IDE_IS_TREE_ADDIN (addin));
+ g_assert (IDE_IS_TREE_MODEL (self));
+
+ ide_tree_addin_unload (addin, self->tree, self);
+}
+
+static void
+ide_tree_model_parent_set (IdeObject *object,
+ IdeObject *parent)
+{
+ IdeTreeModel *self = (IdeTreeModel *)object;
+ g_autoptr(IdeContext) context = NULL;
+
+ g_assert (IDE_IS_TREE_MODEL (self));
+ g_assert (!parent || IDE_IS_OBJECT (parent));
+
+ if (self->addins != NULL || parent == NULL ||
+ !(context = ide_object_ref_context (IDE_OBJECT (self))))
+ return;
+
+ g_assert (IDE_IS_TREE (self->tree));
+
+ self->addins = ide_extension_set_adapter_new (IDE_OBJECT (self),
+ peas_engine_get_default (),
+ IDE_TYPE_TREE_ADDIN,
+ "Tree-Kind",
+ self->kind);
+
+ g_signal_connect_object (self->addins,
+ "extension-added",
+ G_CALLBACK (ide_tree_model_addin_added_cb),
+ self,
+ 0);
+
+ g_signal_connect_object (self->addins,
+ "extension-removed",
+ G_CALLBACK (ide_tree_model_addin_removed_cb),
+ self,
+ 0);
+
+ ide_extension_set_adapter_foreach (self->addins,
+ ide_tree_model_addin_added_cb,
+ self);
+}
+
+static void
+ide_tree_model_dispose (GObject *object)
+{
+ IdeTreeModel *self = (IdeTreeModel *)object;
+
+ /* Clear the model back-pointer for root so that it cannot emit anu
+ * further signals on our tree model.
+ */
+ if (self->root != NULL)
+ _ide_tree_node_set_model (self->root, NULL);
+
+ g_clear_object (&self->tree);
+ ide_clear_and_destroy_object (&self->addins);
+ g_clear_object (&self->root);
+ g_clear_pointer (&self->kind, g_free);
+
+ G_OBJECT_CLASS (ide_tree_model_parent_class)->dispose (object);
+}
+
+static void
+ide_tree_model_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ IdeTreeModel *self = IDE_TREE_MODEL (object);
+
+ switch (prop_id)
+ {
+ case PROP_KIND:
+ g_value_set_string (value, ide_tree_model_get_kind (self));
+ break;
+
+ case PROP_ROOT:
+ g_value_set_object (value, ide_tree_model_get_root (self));
+ break;
+
+ case PROP_TREE:
+ g_value_set_object (value, self->tree);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+ide_tree_model_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ IdeTreeModel *self = IDE_TREE_MODEL (object);
+
+ switch (prop_id)
+ {
+ case PROP_KIND:
+ ide_tree_model_set_kind (self, g_value_get_string (value));
+ break;
+
+ case PROP_ROOT:
+ ide_tree_model_set_root (self, g_value_get_object (value));
+ break;
+
+ case PROP_TREE:
+ self->tree = g_value_dup_object (value);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+ide_tree_model_class_init (IdeTreeModelClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ IdeObjectClass *i_object_class = IDE_OBJECT_CLASS (klass);
+
+ object_class->dispose = ide_tree_model_dispose;
+ object_class->get_property = ide_tree_model_get_property;
+ object_class->set_property = ide_tree_model_set_property;
+
+ i_object_class->parent_set = ide_tree_model_parent_set;
+
+ properties [PROP_TREE] =
+ g_param_spec_object ("tree",
+ "Tree",
+ "The tree the model belongs to",
+ IDE_TYPE_TREE,
+ (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * IdeTreeModel:root:
+ *
+ * The "root" property contains the root #IdeTreeNode that is used to build
+ * the tree. It should contain an object for the #IdeTreeNode:item property
+ * so that #IdeTreeAddin's may use it to build the node and any children.
+ *
+ * Since: 3.32
+ */
+ properties [PROP_ROOT] =
+ g_param_spec_object ("root",
+ "Root",
+ "The root IdeTreeNode",
+ IDE_TYPE_TREE_NODE,
+ (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * IdeTreeModel:kind:
+ *
+ * The "kind" property is used to determine what #IdeTreeAddin plugins to
+ * load. Only plugins which match the "kind" will be loaded to extend the
+ * tree contents.
+ *
+ * For example, to extend the project-tree, plugins should set
+ * "X-Tree-Kind=project" in their .plugin manifest.
+ *
+ * Since: 3.32
+ */
+ properties [PROP_KIND] =
+ g_param_spec_string ("kind",
+ "Kind",
+ "The kind of tree model that is being generated",
+ 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_tree_model_init (IdeTreeModel *self)
+{
+ self->root = create_root ();
+}
+
+IdeTreeModel *
+_ide_tree_model_new (IdeTree *tree)
+{
+ return g_object_new (IDE_TYPE_TREE_MODEL,
+ "tree", tree,
+ NULL);
+}
+
+void
+_ide_tree_model_release_addins (IdeTreeModel *self)
+{
+ g_assert (IDE_IS_TREE_MODEL (self));
+
+ ide_clear_and_destroy_object (&self->addins);
+}
+
+static GtkTreeModelFlags
+ide_tree_model_get_flags (GtkTreeModel *model)
+{
+ return 0;
+}
+
+static gint
+ide_tree_model_get_n_columns (GtkTreeModel *model)
+{
+ return 1;
+}
+
+static GType
+ide_tree_model_get_column_type (GtkTreeModel *model,
+ gint index_)
+{
+ return IDE_TYPE_TREE_NODE;
+}
+
+static GtkTreePath *
+ide_tree_model_get_path (GtkTreeModel *tree_model,
+ GtkTreeIter *iter)
+{
+ g_autoptr(GArray) indexes = NULL;
+ IdeTreeModel *self = (IdeTreeModel *)tree_model;
+ IdeTreeNode *node;
+
+ g_assert (IDE_IS_TREE_MODEL (self));
+ g_assert (iter != NULL);
+ g_assert (IDE_IS_TREE_NODE (iter->user_data));
+
+ node = iter->user_data;
+
+ if (ide_tree_node_is_root (node))
+ return NULL;
+
+ indexes = g_array_new (FALSE, FALSE, sizeof (gint));
+
+ do
+ {
+ gint position;
+
+ position = ide_tree_node_get_index (node);
+ g_array_prepend_val (indexes, position);
+ }
+ while ((node = ide_tree_node_get_parent (node)) &&
+ !ide_tree_node_is_root (node));
+
+ return gtk_tree_path_new_from_indicesv (&g_array_index (indexes, gint, 0), indexes->len);
+}
+
+static gboolean
+ide_tree_model_get_iter (GtkTreeModel *model,
+ GtkTreeIter *iter,
+ GtkTreePath *path)
+{
+ IdeTreeModel *self = (IdeTreeModel *)model;
+ IdeTreeNode *node;
+ gint *indices;
+ gint depth = 0;
+
+ g_assert (IDE_IS_TREE_MODEL (self));
+ g_assert (iter != NULL);
+ g_assert (path != NULL);
+
+ memset (iter, 0, sizeof *iter);
+
+ if (self->root == NULL)
+ return FALSE;
+
+ indices = gtk_tree_path_get_indices_with_depth (path, &depth);
+
+ node = self->root;
+
+ for (gint i = 0; i < depth; i++)
+ {
+ if (!(node = ide_tree_node_get_nth_child (node, indices[i])))
+ return FALSE;
+ }
+
+ if (ide_tree_node_is_root (node))
+ return FALSE;
+
+ iter->user_data = node;
+ return TRUE;
+}
+
+static void
+ide_tree_model_get_value (GtkTreeModel *model,
+ GtkTreeIter *iter,
+ gint column,
+ GValue *value)
+{
+ g_value_init (value, IDE_TYPE_TREE_NODE);
+ g_value_set_object (value, iter->user_data);
+}
+
+static gboolean
+ide_tree_model_iter_next (GtkTreeModel *model,
+ GtkTreeIter *iter)
+{
+ g_assert (IDE_IS_TREE_MODEL (model));
+ g_assert (!iter->user_data || IDE_IS_TREE_NODE (iter->user_data));
+
+ if (iter->user_data)
+ iter->user_data = ide_tree_node_get_next (iter->user_data);
+
+ return IDE_IS_TREE_NODE (iter->user_data);
+}
+
+static gboolean
+ide_tree_model_iter_previous (GtkTreeModel *model,
+ GtkTreeIter *iter)
+{
+ g_assert (IDE_IS_TREE_MODEL (model));
+ g_assert (!iter->user_data || IDE_IS_TREE_NODE (iter->user_data));
+
+ if (iter->user_data)
+ iter->user_data = ide_tree_node_get_previous (iter->user_data);
+
+ return IDE_IS_TREE_NODE (iter->user_data);
+}
+
+static gboolean
+ide_tree_model_iter_nth_child (GtkTreeModel *model,
+ GtkTreeIter *iter,
+ GtkTreeIter *parent,
+ gint n)
+{
+ IdeTreeModel *self = (IdeTreeModel *)model;
+ IdeTreeNode *pnode;
+
+ g_assert (IDE_IS_TREE_MODEL (model));
+ g_assert (iter != NULL);
+
+ if (self->root == NULL)
+ return FALSE;
+
+ g_assert (parent == NULL || IDE_IS_TREE_NODE (parent->user_data));
+
+ n = CLAMP (n, 0, G_MAXINT);
+
+ memset (iter, 0, sizeof *iter);
+
+ if (parent == NULL)
+ pnode = self->root;
+ else
+ pnode = parent->user_data;
+ g_assert (IDE_IS_TREE_NODE (pnode));
+
+ iter->user_data = ide_tree_node_get_nth_child (pnode, n);
+ g_assert (!iter->user_data || IDE_IS_TREE_NODE (iter->user_data));
+
+ return IDE_IS_TREE_NODE (iter->user_data);
+}
+
+static gboolean
+ide_tree_model_iter_children (GtkTreeModel *model,
+ GtkTreeIter *iter,
+ GtkTreeIter *parent)
+{
+ return ide_tree_model_iter_nth_child (model, iter, parent, 0);
+}
+
+static gboolean
+ide_tree_model_iter_has_child (GtkTreeModel *model,
+ GtkTreeIter *iter)
+{
+ gboolean ret;
+
+ g_assert (IDE_IS_TREE_MODEL (model));
+ g_assert (iter != NULL);
+ g_assert (IDE_IS_TREE_NODE (iter->user_data));
+
+ ret = ide_tree_node_has_child (iter->user_data);
+
+ IDE_TRACE_MSG ("%s has child -> %s",
+ ide_tree_node_get_display_name (iter->user_data),
+ ret ? "yes" : "no");
+
+ return ret;
+}
+
+static gint
+ide_tree_model_iter_n_children (GtkTreeModel *model,
+ GtkTreeIter *iter)
+{
+ IdeTreeModel *self = (IdeTreeModel *)model;
+ gint ret;
+
+ IDE_ENTRY;
+
+ g_assert (IDE_IS_MAIN_THREAD ());
+ g_assert (self != NULL);
+ g_assert (IDE_IS_TREE_MODEL (self));
+ g_assert (IDE_IS_TREE_NODE (self->root));
+ g_assert (iter == NULL || IDE_IS_TREE_NODE (iter->user_data));
+
+ if (iter == NULL)
+ ret = ide_tree_node_get_n_children (self->root);
+ else if (iter->user_data)
+ ret = ide_tree_node_get_n_children (iter->user_data);
+ else
+ ret = 0;
+
+ IDE_RETURN (ret);
+}
+
+static gboolean
+ide_tree_model_iter_parent (GtkTreeModel *model,
+ GtkTreeIter *iter,
+ GtkTreeIter *child)
+{
+ IdeTreeModel *self = (IdeTreeModel *)model;
+
+ g_assert (IDE_IS_TREE_MODEL (self));
+ g_assert (iter != NULL);
+ g_assert (child != NULL);
+ g_assert (IDE_IS_TREE_NODE (child->user_data));
+
+ memset (iter, 0, sizeof *iter);
+
+ iter->user_data = ide_tree_node_get_parent (child->user_data);
+
+ return !ide_tree_node_is_root (iter->user_data);
+}
+
+static void
+ide_tree_model_row_inserted (GtkTreeModel *model,
+ GtkTreePath *path,
+ GtkTreeIter *iter)
+{
+ IdeTreeModel *self = (IdeTreeModel *)model;
+ IdeTreeNode *node;
+
+ g_assert (IDE_IS_TREE_MODEL (self));
+ g_assert (path != NULL);
+ g_assert (iter != NULL);
+
+ node = iter->user_data;
+
+ g_assert (IDE_IS_TREE_NODE (node));
+
+#if 0
+ g_print ("Building %s (child of %s)\n",
+ ide_tree_node_get_display_name (node),
+ ide_tree_node_get_display_name (ide_tree_node_get_parent (node)));
+#endif
+
+ /*
+ * If this node holds an IdeObject which is not rooted on our object
+ * tree, add it to the object tree beneath us so that it can get destroy
+ * propagation and access to the IdeContext.
+ */
+ if (ide_tree_node_holds (node, IDE_TYPE_OBJECT))
+ {
+ IdeObject *object = ide_tree_node_get_item (node);
+
+ if (!ide_object_get_parent (object))
+ ide_object_append (IDE_OBJECT (self), object);
+ }
+
+ _ide_tree_model_build_node (self, node);
+}
+
+static void
+ide_tree_model_ref_node (GtkTreeModel *model,
+ GtkTreeIter *iter)
+{
+ g_assert (IDE_IS_TREE_MODEL (model));
+ g_assert (iter != NULL);
+ g_assert (!iter->user_data || IDE_IS_TREE_NODE (iter->user_data));
+
+ if (iter->user_data)
+ g_object_ref (iter->user_data);
+}
+
+static void
+ide_tree_model_unref_node (GtkTreeModel *model,
+ GtkTreeIter *iter)
+{
+ g_assert (IDE_IS_TREE_MODEL (model));
+ g_assert (iter != NULL);
+ g_assert (!iter->user_data || IDE_IS_TREE_NODE (iter->user_data));
+
+ if (iter->user_data)
+ g_object_unref (iter->user_data);
+}
+
+static void
+tree_model_iface_init (GtkTreeModelIface *iface)
+{
+ iface->get_flags = ide_tree_model_get_flags;
+ iface->get_n_columns = ide_tree_model_get_n_columns;
+ iface->get_column_type = ide_tree_model_get_column_type;
+ iface->get_iter = ide_tree_model_get_iter;
+ iface->get_path = ide_tree_model_get_path;
+ iface->get_value = ide_tree_model_get_value;
+ iface->iter_next = ide_tree_model_iter_next;
+ iface->iter_previous = ide_tree_model_iter_previous;
+ iface->iter_children = ide_tree_model_iter_children;
+ iface->iter_has_child = ide_tree_model_iter_has_child;
+ iface->iter_n_children = ide_tree_model_iter_n_children;
+ iface->iter_nth_child = ide_tree_model_iter_nth_child;
+ iface->iter_parent = ide_tree_model_iter_parent;
+ iface->row_inserted = ide_tree_model_row_inserted;
+ iface->ref_node = ide_tree_model_ref_node;
+ iface->unref_node = ide_tree_model_unref_node;
+}
+
+/**
+ * ide_tree_model_get_path_for_node:
+ * @self: an #IdeTreeModel
+ * @node: an #IdeTreeNode
+ *
+ * Gets the #GtkTreePath pointing at @node.
+ *
+ * Returns: (transfer full) (nullable): a new #GtkTreePath
+ *
+ * Since: 3.32
+ */
+GtkTreePath *
+ide_tree_model_get_path_for_node (IdeTreeModel *self,
+ IdeTreeNode *node)
+{
+ GtkTreeIter iter;
+
+ g_return_val_if_fail (IDE_IS_TREE_MODEL (self), NULL);
+ g_return_val_if_fail (IDE_IS_TREE_NODE (node), NULL);
+
+ if (ide_tree_model_get_iter_for_node (self, &iter, node))
+ return gtk_tree_model_get_path (GTK_TREE_MODEL (self), &iter);
+
+ return NULL;
+}
+
+/**
+ * ide_tree_model_get_iter_for_node:
+ * @self: an #IdeTreeModel
+ * @iter: (out): a #GtkTreeIter
+ * @node: an #IdeTreeNode
+ *
+ * Gets a #GtkTreeIter that points at @node.
+ *
+ * Returns: %TRUE if @iter was set; otherwise %FALSE
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_tree_model_get_iter_for_node (IdeTreeModel *self,
+ GtkTreeIter *iter,
+ IdeTreeNode *node)
+{
+ g_return_val_if_fail (IDE_IS_TREE_MODEL (self), FALSE);
+
+ if (_ide_tree_model_contains_node (self, node))
+ {
+ memset (iter, 0, sizeof *iter);
+ iter->user_data = node;
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+/**
+ * ide_tree_model_get_root:
+ * @self: a #IdeTreeModel
+ *
+ * Gets the root #IdeTreeNode. This node is never visualized in the tree, but
+ * is used to build the immediate children which are displayed in the tree.
+ *
+ * Returns: (transfer none) (not nullable): an #IdeTreeNode
+ *
+ * Since: 3.32
+ */
+IdeTreeNode *
+ide_tree_model_get_root (IdeTreeModel *self)
+{
+ g_return_val_if_fail (IDE_IS_TREE_MODEL (self), NULL);
+
+ return self->root;
+}
+
+static IdeTreeNodeVisit
+ide_tree_model_remove_all_cb (IdeTreeNode *node,
+ gpointer user_data)
+{
+ IdeTreeModel *self = user_data;
+
+ g_assert (IDE_IS_TREE_NODE (node));
+ g_assert (IDE_IS_TREE_MODEL (self));
+
+ if (node != self->root)
+ {
+ GtkTreePath *tree_path;
+
+ tree_path = ide_tree_model_get_path_for_node (self, node);
+ gtk_tree_model_row_deleted (GTK_TREE_MODEL (self), tree_path);
+ gtk_tree_path_free (tree_path);
+ }
+
+ return IDE_TREE_NODE_VISIT_CHILDREN;
+}
+
+static void
+ide_tree_model_remove_all (IdeTreeModel *self)
+{
+ g_return_if_fail (IDE_IS_TREE_MODEL (self));
+
+ ide_tree_node_traverse (self->root,
+ G_POST_ORDER,
+ G_TRAVERSE_ALL,
+ -1,
+ ide_tree_model_remove_all_cb,
+ self);
+}
+
+void
+ide_tree_model_set_root (IdeTreeModel *self,
+ IdeTreeNode *root)
+{
+ g_return_if_fail (IDE_IS_TREE_MODEL (self));
+ g_return_if_fail (!root || IDE_IS_TREE_NODE (root));
+
+ if (root != self->root)
+ {
+ ide_tree_model_remove_all (self);
+ g_clear_object (&self->root);
+
+ if (root != NULL)
+ self->root = g_object_ref (root);
+ else
+ self->root = create_root ();
+
+ _ide_tree_node_set_model (self->root, self);
+
+ /* Root always requires building children */
+ if (!ide_tree_node_get_children_possible (self->root))
+ ide_tree_node_set_children_possible (self->root, TRUE);
+
+ g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ROOT]);
+ }
+}
+
+/**
+ * ide_tree_model_get_kind:
+ * @self: a #IdeTreeModel
+ *
+ * Gets the kind of model that is being generated. See #IdeTreeModel:kind
+ * for more information.
+ *
+ * Returns: (nullable): a string containing the kind, or %NULL
+ *
+ * Since: 3.32
+ */
+const gchar *
+ide_tree_model_get_kind (IdeTreeModel *self)
+{
+ g_return_val_if_fail (IDE_IS_TREE_MODEL (self), NULL);
+
+ return self->kind;
+}
+
+/**
+ * ide_tree_model_set_kind:
+ * @self: a #IdeTreeModel
+ * @kind: a string describing the kind of model
+ *
+ * Sets the kind of model that is being created. This determines what plugins
+ * are used to generate the tree contents.
+ *
+ * This should be set before adding the #IdeTreeModel to an #IdeObject to
+ * ensure the tree builds the proper contents.
+ *
+ * Since: 3.32
+ */
+void
+ide_tree_model_set_kind (IdeTreeModel *self,
+ const gchar *kind)
+{
+ g_return_if_fail (IDE_IS_TREE_MODEL (self));
+
+ if (!ide_str_equal0 (kind, self->kind))
+ {
+ g_free (self->kind);
+ self->kind = g_strdup (kind);
+
+ if (self->addins != NULL)
+ ide_extension_set_adapter_set_value (self->addins, kind);
+
+ g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_KIND]);
+ }
+}
+
+typedef struct
+{
+ IdeTreeNode *node;
+ IdeTree *tree;
+ gboolean handled;
+} RowActivated;
+
+static void
+ide_tree_model_row_activated_cb (IdeExtensionSetAdapter *set,
+ PeasPluginInfo *plugin_info,
+ PeasExtension *exten,
+ gpointer user_data)
+{
+ IdeTreeAddin *addin = (IdeTreeAddin *)exten;
+ RowActivated *state = user_data;
+
+ g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+ g_assert (plugin_info != NULL);
+ g_assert (IDE_IS_TREE_ADDIN (addin));
+ g_assert (state != NULL);
+
+ if (state->handled)
+ return;
+
+ state->handled = ide_tree_addin_node_activated (addin, state->tree, state->node);
+}
+
+gboolean
+_ide_tree_model_row_activated (IdeTreeModel *self,
+ IdeTree *tree,
+ GtkTreePath *path)
+{
+ GtkTreeIter iter;
+
+ g_assert (IDE_IS_TREE_MODEL (self));
+ g_assert (IDE_IS_TREE (tree));
+ g_assert (path != NULL);
+
+ if (gtk_tree_model_get_iter (GTK_TREE_MODEL (self), &iter, path))
+ {
+ RowActivated state = {
+ .node = iter.user_data,
+ .tree = tree,
+ .handled = FALSE,
+ };
+
+ ide_extension_set_adapter_foreach (self->addins,
+ ide_tree_model_row_activated_cb,
+ &state);
+
+ return state.handled;
+ }
+
+ return FALSE;
+}
+
+/**
+ * ide_tree_model_get_node:
+ * @self: a #IdeTreeModel
+ * @iter: a #GtkTreeIter
+ *
+ * Gets the #IdeTreeNode found at @iter.
+ *
+ * Returns: (transfer none) (nullable): an #IdeTreeNode or %NULL
+ *
+ * Since: 3.32
+ */
+IdeTreeNode *
+ide_tree_model_get_node (IdeTreeModel *self,
+ GtkTreeIter *iter)
+{
+ g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+ g_return_val_if_fail (IDE_IS_TREE_MODEL (self), NULL);
+ g_return_val_if_fail (iter != NULL, NULL);
+
+ if (IDE_IS_TREE_NODE (iter->user_data))
+ return iter->user_data;
+
+ return NULL;
+}
+
+gboolean
+_ide_tree_model_contains_node (IdeTreeModel *self,
+ IdeTreeNode *node)
+{
+ g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+ g_return_val_if_fail (IDE_IS_TREE_MODEL (self), FALSE);
+ g_return_val_if_fail (!node || IDE_IS_TREE_NODE (node), FALSE);
+
+ if (node == NULL)
+ return FALSE;
+
+ return self->root == ide_tree_node_get_root (node);
+}
+
+static void
+inc_active (IdeTask *task)
+{
+ gint n_active = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (task), "N_ACTIVE"));
+ n_active++;
+ g_object_set_data (G_OBJECT (task), "N_ACTIVE", GINT_TO_POINTER (n_active));
+}
+
+static gboolean
+dec_active_and_test (IdeTask *task)
+{
+ gint n_active = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (task), "N_ACTIVE"));
+ n_active--;
+ g_object_set_data (G_OBJECT (task), "N_ACTIVE", GINT_TO_POINTER (n_active));
+ return n_active == 0;
+}
+
+static void
+ide_tree_model_addin_build_children_cb (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ IdeTreeAddin *addin = (IdeTreeAddin *)object;
+ g_autoptr(IdeTask) task = user_data;
+ g_autoptr(GError) error = NULL;
+
+ g_assert (IDE_IS_TREE_ADDIN (addin));
+ g_assert (G_IS_ASYNC_RESULT (result));
+ g_assert (IDE_IS_TASK (task));
+
+ ide_tree_addin_build_children_finish (addin, result, &error);
+
+ if (dec_active_and_test (task))
+ {
+#if 0
+ {
+ IdeTreeNode *node = ide_task_get_task_data (task);
+ _ide_tree_node_dump (ide_tree_node_get_root (node));
+ }
+#endif
+
+ ide_task_return_boolean (task, TRUE);
+ }
+}
+
+static void
+ide_tree_model_expand_foreach_cb (IdeExtensionSetAdapter *set,
+ PeasPluginInfo *plugin_info,
+ PeasExtension *exten,
+ gpointer user_data)
+{
+ IdeTreeAddin *addin = (IdeTreeAddin *)exten;
+ IdeTreeNode *node;
+ IdeTask *task = user_data;
+
+ g_assert (IDE_IS_MAIN_THREAD ());
+ g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+ g_assert (plugin_info != NULL);
+ g_assert (IDE_IS_TREE_ADDIN (addin));
+ g_assert (IDE_IS_TASK (task));
+
+ node = ide_task_get_task_data (task);
+
+ g_assert (IDE_IS_TREE_NODE (node));
+
+ inc_active (task);
+
+ ide_tree_addin_build_children_async (addin,
+ node,
+ ide_task_get_cancellable (task),
+ ide_tree_model_addin_build_children_cb,
+ g_object_ref (task));
+
+ _ide_tree_node_set_needs_build_children (node, FALSE);
+}
+
+static void
+ide_tree_model_expand_completed (IdeTreeNode *node,
+ GParamSpec *pspec,
+ IdeTask *task)
+{
+ g_assert (IDE_IS_TREE_NODE (node));
+ g_assert (pspec != NULL);
+ g_assert (IDE_IS_TASK (task));
+
+ _ide_tree_node_set_loading (node, FALSE);
+}
+
+void
+ide_tree_model_expand_async (IdeTreeModel *self,
+ IdeTreeNode *node,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ g_autoptr(IdeTask) task = NULL;
+
+ g_return_if_fail (IDE_IS_TREE_MODEL (self));
+ g_return_if_fail (IDE_IS_TREE_NODE (node));
+ g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+ task = ide_task_new (self, cancellable, callback, user_data);
+ ide_task_set_source_tag (task, ide_tree_model_expand_async);
+ ide_task_set_task_data (task, g_object_ref (node), g_object_unref);
+
+ g_signal_connect_object (task,
+ "notify::completed",
+ G_CALLBACK (ide_tree_model_expand_completed),
+ node,
+ G_CONNECT_SWAPPED);
+
+ /* If no building is necessary, then just skip any work here */
+ if (!_ide_tree_node_get_needs_build_children (node) ||
+ ide_extension_set_adapter_get_n_extensions (self->addins) == 0)
+ {
+ ide_task_return_boolean (task, TRUE);
+ return;
+ }
+
+ _ide_tree_node_set_loading (node, TRUE);
+
+ ide_extension_set_adapter_foreach (self->addins,
+ ide_tree_model_expand_foreach_cb,
+ task);
+}
+
+gboolean
+ide_tree_model_expand_finish (IdeTreeModel *self,
+ GAsyncResult *result,
+ GError **error)
+{
+ g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+ g_return_val_if_fail (IDE_IS_TREE_MODEL (self), FALSE);
+ g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+ return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static IdeTreeNodeVisit
+ide_tree_model_invalidate_traverse_cb (IdeTreeNode *node,
+ gpointer user_data)
+{
+ g_assert (IDE_IS_TREE_NODE (node));
+
+ if (!ide_tree_node_is_root (node))
+ ide_tree_node_remove (ide_tree_node_get_parent (node), node);
+
+ return IDE_TREE_NODE_VISIT_CHILDREN;
+}
+
+/**
+ * ide_tree_model_invalidate:
+ * @self: a #IdeTreeModel
+ * @node: (nullable): an #IdeTreeNode or %NULL
+ *
+ * Invalidates @model starting from @node so that those items
+ * are rebuilt using the configured tree addins.
+ *
+ * If @node is %NULL, the root of the tree is invalidated.
+ *
+ * Since: 3.32
+ */
+void
+ide_tree_model_invalidate (IdeTreeModel *self,
+ IdeTreeNode *node)
+{
+ g_return_if_fail (IDE_IS_TREE_MODEL (self));
+ g_return_if_fail (!node || IDE_IS_TREE_NODE (node));
+
+ if (node == NULL)
+ node = self->root;
+
+ ide_tree_node_traverse (node,
+ G_POST_ORDER,
+ G_TRAVERSE_ALL,
+ -1,
+ ide_tree_model_invalidate_traverse_cb,
+ NULL);
+
+ _ide_tree_node_set_needs_build_children (node, TRUE);
+ ide_tree_model_expand_async (self, node, NULL, NULL, NULL);
+}
+
+static void
+ide_tree_model_propagate_selection_changed_cb (IdeExtensionSetAdapter *set,
+ PeasPluginInfo *plugin_info,
+ PeasExtension *exten,
+ gpointer user_data)
+{
+ IdeTreeNode *node = user_data;
+
+ g_assert (IDE_IS_MAIN_THREAD ());
+ g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+ g_assert (plugin_info != NULL);
+ g_assert (IDE_IS_TREE_ADDIN (exten));
+ g_assert (!node || IDE_IS_TREE_NODE (node));
+
+ ide_tree_addin_selection_changed (IDE_TREE_ADDIN (exten), node);
+}
+
+void
+_ide_tree_model_selection_changed (IdeTreeModel *self,
+ GtkTreeIter *iter)
+{
+ IdeTreeNode *node = NULL;
+
+ g_return_if_fail (IDE_IS_MAIN_THREAD ());
+ g_return_if_fail (IDE_IS_TREE_MODEL (self));
+ g_return_if_fail (!iter || IDE_IS_TREE_NODE (iter->user_data));
+
+ if (self->addins == NULL)
+ return;
+
+ if (iter != NULL)
+ node = ide_tree_model_get_node (self, iter);
+
+ ide_extension_set_adapter_foreach (self->addins,
+ ide_tree_model_propagate_selection_changed_cb,
+ node);
+}
+
+static void
+ide_tree_model_propagate_node_expanded_cb (IdeExtensionSetAdapter *set,
+ PeasPluginInfo *plugin_info,
+ PeasExtension *exten,
+ gpointer user_data)
+{
+ IdeTreeNode *node = user_data;
+
+ g_assert (IDE_IS_MAIN_THREAD ());
+ g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+ g_assert (plugin_info != NULL);
+ g_assert (IDE_IS_TREE_ADDIN (exten));
+ g_assert (!node || IDE_IS_TREE_NODE (node));
+
+ ide_tree_addin_node_expanded (IDE_TREE_ADDIN (exten), node);
+}
+
+void
+_ide_tree_model_row_expanded (IdeTreeModel *self,
+ IdeTree *tree,
+ GtkTreePath *path)
+{
+ GtkTreeIter iter;
+
+ g_return_if_fail (IDE_IS_MAIN_THREAD ());
+ g_return_if_fail (IDE_IS_TREE_MODEL (self));
+ g_return_if_fail (IDE_IS_TREE (tree));
+ g_return_if_fail (path != NULL);
+
+ if (self->addins == NULL)
+ return;
+
+ if (ide_tree_model_get_iter (GTK_TREE_MODEL (self), &iter, path))
+ {
+ IdeTreeNode *node = ide_tree_model_get_node (self, &iter);
+
+ ide_extension_set_adapter_foreach (self->addins,
+ ide_tree_model_propagate_node_expanded_cb,
+ node);
+ }
+}
+
+static void
+ide_tree_model_propagate_node_collapsed_cb (IdeExtensionSetAdapter *set,
+ PeasPluginInfo *plugin_info,
+ PeasExtension *exten,
+ gpointer user_data)
+{
+ IdeTreeNode *node = user_data;
+
+ g_assert (IDE_IS_MAIN_THREAD ());
+ g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+ g_assert (plugin_info != NULL);
+ g_assert (IDE_IS_TREE_ADDIN (exten));
+ g_assert (!node || IDE_IS_TREE_NODE (node));
+
+ ide_tree_addin_node_collapsed (IDE_TREE_ADDIN (exten), node);
+}
+
+void
+_ide_tree_model_row_collapsed (IdeTreeModel *self,
+ IdeTree *tree,
+ GtkTreePath *path)
+{
+ GtkTreeIter iter;
+
+ g_return_if_fail (IDE_IS_MAIN_THREAD ());
+ g_return_if_fail (IDE_IS_TREE_MODEL (self));
+ g_return_if_fail (IDE_IS_TREE (tree));
+ g_return_if_fail (path != NULL);
+
+ if (self->addins == NULL)
+ return;
+
+ if (ide_tree_model_get_iter (GTK_TREE_MODEL (self), &iter, path))
+ {
+ IdeTreeNode *node = ide_tree_model_get_node (self, &iter);
+
+ ide_extension_set_adapter_foreach (self->addins,
+ ide_tree_model_propagate_node_collapsed_cb,
+ node);
+ }
+}
+
+/**
+ * ide_tree_model_get_tree:
+ * @self: a #IdeTreeModel
+ *
+ * Returns: (transfer none): an #IdeTree
+ *
+ * Since: 3.32
+ */
+IdeTree *
+ide_tree_model_get_tree (IdeTreeModel *self)
+{
+ g_return_val_if_fail (IDE_IS_TREE_MODEL (self), NULL);
+
+ return self->tree;
+}
+
+static void
+ide_tree_model_cell_data_func_cb (IdeExtensionSetAdapter *set,
+ PeasPluginInfo *plugin_info,
+ PeasExtension *exten,
+ gpointer user_data)
+{
+ struct {
+ IdeTreeNode *node;
+ GtkCellRenderer *cell;
+ } *state = user_data;
+
+ g_assert (IDE_IS_MAIN_THREAD ());
+ g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+ g_assert (plugin_info != NULL);
+ g_assert (IDE_IS_TREE_ADDIN (exten));
+ g_assert (state != NULL);
+ g_assert (IDE_IS_TREE_NODE (state->node));
+ g_assert (GTK_IS_CELL_RENDERER (state->cell));
+
+ ide_tree_addin_cell_data_func (IDE_TREE_ADDIN (exten), state->node, state->cell);
+}
+
+void
+_ide_tree_model_cell_data_func (IdeTreeModel *self,
+ GtkTreeIter *iter,
+ GtkCellRenderer *cell)
+{
+ struct {
+ IdeTreeNode *node;
+ GtkCellRenderer *cell;
+ } state;
+
+ g_return_if_fail (IDE_IS_MAIN_THREAD ());
+ g_return_if_fail (IDE_IS_TREE_MODEL (self));
+ g_return_if_fail (iter != NULL);
+ g_return_if_fail (GTK_IS_CELL_RENDERER (cell));
+
+ state.node = iter->user_data;
+ state.cell = cell;
+
+ ide_extension_set_adapter_foreach (self->addins,
+ ide_tree_model_cell_data_func_cb,
+ &state);
+}
+
+static void
+ide_tree_model_row_draggable_cb (IdeExtensionSetAdapter *set,
+ PeasPluginInfo *plugin_info,
+ PeasExtension *exten,
+ gpointer user_data)
+{
+ struct {
+ IdeTreeNode *node;
+ gboolean draggable;
+ } *state = user_data;
+
+ g_assert (IDE_IS_MAIN_THREAD ());
+ g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+ g_assert (plugin_info != NULL);
+ g_assert (IDE_IS_TREE_ADDIN (exten));
+ g_assert (state != NULL);
+ g_assert (IDE_IS_TREE_NODE (state->node));
+
+ state->draggable |= ide_tree_addin_node_draggable (IDE_TREE_ADDIN (exten), state->node);
+}
+
+static gboolean
+ide_tree_model_row_draggable (GtkTreeDragSource *source,
+ GtkTreePath *path)
+{
+ IdeTreeModel *self = (IdeTreeModel *)source;
+ GtkTreeIter iter;
+ struct {
+ IdeTreeNode *node;
+ gboolean draggable;
+ } state;
+
+ g_assert (IDE_IS_MAIN_THREAD ());
+ g_assert (IDE_IS_TREE_MODEL (self));
+
+ if (!ide_tree_model_get_iter (GTK_TREE_MODEL (source), &iter, path))
+ return FALSE;
+
+ if (!IDE_IS_TREE_NODE (iter.user_data))
+ return FALSE;
+
+ state.node = iter.user_data;
+ state.draggable = FALSE;
+
+ ide_extension_set_adapter_foreach (self->addins,
+ ide_tree_model_row_draggable_cb,
+ &state);
+
+ return state.draggable;
+}
+
+static gboolean
+ide_tree_model_drag_data_get (GtkTreeDragSource *source,
+ GtkTreePath *path,
+ GtkSelectionData *selection)
+{
+ IdeTreeModel *self = (IdeTreeModel *)source;
+
+ g_assert (IDE_IS_MAIN_THREAD ());
+ g_assert (IDE_IS_TREE_MODEL (self));
+ g_assert (path != NULL);
+ g_assert (selection != NULL);
+
+ return gtk_tree_set_row_drag_data (selection, GTK_TREE_MODEL (self), path);
+}
+
+static gboolean
+ide_tree_model_drag_data_delete (GtkTreeDragSource *source,
+ GtkTreePath *path)
+{
+ IdeTreeModel *self = (IdeTreeModel *)source;
+
+ g_assert (IDE_IS_MAIN_THREAD ());
+ g_assert (IDE_IS_TREE_MODEL (self));
+ g_assert (path != NULL);
+
+ return FALSE;
+}
+
+static void
+tree_drag_source_iface_init (GtkTreeDragSourceIface *iface)
+{
+ iface->row_draggable = ide_tree_model_row_draggable;
+ iface->drag_data_get = ide_tree_model_drag_data_get;
+ iface->drag_data_delete = ide_tree_model_drag_data_delete;
+}
+
+static void
+ide_tree_model_drag_data_received_addin_cb (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ IdeTreeAddin *addin = (IdeTreeAddin *)object;
+ g_autoptr(IdeTask) task = user_data;
+ g_autoptr(GError) error = NULL;
+ DragDataReceived *state;
+
+ g_assert (IDE_IS_MAIN_THREAD ());
+ g_assert (IDE_IS_TREE_ADDIN (addin));
+ g_assert (G_IS_ASYNC_RESULT (result));
+ g_assert (IDE_IS_TASK (task));
+
+ if (!ide_tree_addin_node_dropped_finish (addin, result, &error))
+ {
+ if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED))
+ g_warning ("%s: %s", G_OBJECT_TYPE_NAME (addin), error->message);
+ }
+
+ state = ide_task_get_task_data (task);
+ g_assert (state != NULL);
+ g_assert (!state->drag_node || IDE_IS_TREE_NODE (state->drag_node));
+ g_assert (!state->drop_node || IDE_IS_TREE_NODE (state->drop_node));
+ g_assert (state->n_active > 0);
+
+ state->n_active--;
+
+ if (state->n_active == 0)
+ ide_task_return_boolean (task, TRUE);
+}
+
+static void
+ide_tree_model_drag_data_received_cb (IdeExtensionSetAdapter *set,
+ PeasPluginInfo *plugin_info,
+ PeasExtension *exten,
+ gpointer user_data)
+{
+ IdeTreeAddin *addin = (IdeTreeAddin *)exten;
+ IdeTask *task = user_data;
+ DragDataReceived *state;
+
+ g_assert (IDE_IS_MAIN_THREAD ());
+ g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+ g_assert (plugin_info != NULL);
+ g_assert (IDE_IS_TREE_ADDIN (addin));
+ g_assert (IDE_IS_TASK (task));
+
+ state = ide_task_get_task_data (task);
+ g_assert (state != NULL);
+ g_assert (!state->drag_node || IDE_IS_TREE_NODE (state->drag_node));
+ g_assert (!state->drop_node || IDE_IS_TREE_NODE (state->drop_node));
+
+ state->n_active++;
+
+ ide_tree_addin_node_dropped_async (addin,
+ state->drag_node,
+ state->drop_node,
+ state->selection,
+ state->actions,
+ NULL,
+ ide_tree_model_drag_data_received_addin_cb,
+ g_object_ref (task));
+}
+
+static gboolean
+ide_tree_model_drag_data_received (GtkTreeDragDest *dest,
+ GtkTreePath *path,
+ GtkSelectionData *selection)
+{
+ IdeTreeModel *self = (IdeTreeModel *)dest;
+ g_autoptr(GtkTreePath) source_path = NULL;
+ g_autoptr(IdeTask) task = NULL;
+ GtkTreeModel *source_model = NULL;
+ DragDataReceived *state;
+ IdeTreeNode *drag_node = NULL;
+ IdeTreeNode *drop_node = NULL;
+ GtkTreeIter iter;
+
+ g_assert (IDE_IS_MAIN_THREAD ());
+ g_assert (IDE_IS_TREE_MODEL (self));
+ g_assert (path != NULL);
+ g_assert (selection != NULL);
+
+ if (gtk_tree_get_row_drag_data (selection, &source_model, &source_path))
+ {
+ if (IDE_IS_TREE_MODEL (source_model))
+ {
+ if (ide_tree_model_get_iter (source_model, &iter, source_path))
+ drag_node = IDE_TREE_NODE (iter.user_data);
+ }
+ }
+
+ drop_node = _ide_tree_get_drop_node (self->tree);
+
+ state = g_slice_new0 (DragDataReceived);
+ g_set_object (&state->drag_node, drag_node);
+ g_set_object (&state->drop_node, drop_node);
+ state->selection = gtk_selection_data_copy (selection);
+ state->actions = _ide_tree_get_drop_actions (self->tree);
+
+
+ task = ide_task_new (self, NULL, NULL, NULL);
+ ide_task_set_source_tag (task, ide_tree_model_drag_data_received);
+ ide_task_set_task_data (task, state, drag_data_received_free);
+
+ ide_extension_set_adapter_foreach (self->addins,
+ ide_tree_model_drag_data_received_cb,
+ task);
+
+ if (state->n_active == 0)
+ ide_task_return_boolean (task, TRUE);
+
+ return TRUE;
+}
+
+static void
+ide_tree_model_row_drop_possible_cb (IdeExtensionSetAdapter *set,
+ PeasPluginInfo *plugin_info,
+ PeasExtension *exten,
+ gpointer user_data)
+{
+ struct {
+ IdeTreeNode *drag_node;
+ IdeTreeNode *drop_node;
+ GtkSelectionData *selection;
+ gboolean drop_possible;
+ } *state = user_data;
+
+ g_assert (IDE_IS_MAIN_THREAD ());
+ g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+ g_assert (plugin_info != NULL);
+ g_assert (IDE_IS_TREE_ADDIN (exten));
+ g_assert (state != NULL);
+ g_assert (state->selection != NULL);
+
+ state->drop_possible |= ide_tree_addin_node_droppable (IDE_TREE_ADDIN (exten),
+ state->drag_node,
+ state->drop_node,
+ state->selection);
+}
+
+static gboolean
+ide_tree_model_row_drop_possible (GtkTreeDragDest *dest,
+ GtkTreePath *path,
+ GtkSelectionData *selection)
+{
+ IdeTreeModel *self = (IdeTreeModel *)dest;
+ g_autoptr(GtkTreePath) source_path = NULL;
+ GtkTreeModel *source_model = NULL;
+ IdeTreeNode *drag_node = NULL;
+ IdeTreeNode *drop_node = NULL;
+ GtkTreeIter iter = {0};
+ struct {
+ IdeTreeNode *drag_node;
+ IdeTreeNode *drop_node;
+ GtkSelectionData *selection;
+ gboolean drop_possible;
+ } state;
+
+ g_assert (IDE_IS_MAIN_THREAD ());
+ g_assert (IDE_IS_TREE_MODEL (self));
+ g_assert (path != NULL);
+ g_assert (selection != NULL);
+
+ if (gtk_tree_get_row_drag_data (selection, &source_model, &source_path))
+ {
+ if (IDE_IS_TREE_MODEL (source_model))
+ {
+ if (ide_tree_model_get_iter (source_model, &iter, source_path))
+ drag_node = IDE_TREE_NODE (iter.user_data);
+ }
+ }
+
+ if (ide_tree_model_get_iter (GTK_TREE_MODEL (self), &iter, path))
+ {
+ drop_node = IDE_TREE_NODE (iter.user_data);
+ }
+ else
+ {
+ g_autoptr(GtkTreePath) copy = gtk_tree_path_copy (path);
+
+ gtk_tree_path_up (copy);
+
+ if (ide_tree_model_get_iter (GTK_TREE_MODEL (self), &iter, copy))
+ drop_node = IDE_TREE_NODE (iter.user_data);
+ }
+
+ state.drag_node = drag_node;
+ state.drop_node = drop_node;
+ state.selection = selection;
+ state.drop_possible = FALSE;
+
+ ide_extension_set_adapter_foreach (self->addins,
+ ide_tree_model_row_drop_possible_cb,
+ &state);
+
+ return state.drop_possible;
+}
+
+static void
+tree_drag_dest_iface_init (GtkTreeDragDestIface *iface)
+{
+ iface->drag_data_received = ide_tree_model_drag_data_received;
+ iface->row_drop_possible = ide_tree_model_row_drop_possible;
+}
diff --git a/src/libide/tree/ide-tree-model.h b/src/libide/tree/ide-tree-model.h
new file mode 100644
index 000000000..5590b8943
--- /dev/null
+++ b/src/libide/tree/ide-tree-model.h
@@ -0,0 +1,72 @@
+/* ide-tree-model.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 <gtk/gtk.h>
+#include <libide-core.h>
+
+#include "ide-tree.h"
+#include "ide-tree-node.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_TREE_MODEL (ide_tree_model_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeTreeModel, ide_tree_model, IDE, TREE_MODEL, IdeObject)
+
+IDE_AVAILABLE_IN_3_32
+IdeTree *ide_tree_model_get_tree (IdeTreeModel *self);
+IDE_AVAILABLE_IN_3_32
+IdeTreeNode *ide_tree_model_get_root (IdeTreeModel *self);
+IDE_AVAILABLE_IN_3_32
+void ide_tree_model_set_root (IdeTreeModel *self,
+ IdeTreeNode *root);
+IDE_AVAILABLE_IN_3_32
+const gchar *ide_tree_model_get_kind (IdeTreeModel *self);
+IDE_AVAILABLE_IN_3_32
+void ide_tree_model_set_kind (IdeTreeModel *self,
+ const gchar *kind);
+IDE_AVAILABLE_IN_3_32
+IdeTreeNode *ide_tree_model_get_node (IdeTreeModel *self,
+ GtkTreeIter *iter);
+IDE_AVAILABLE_IN_3_32
+GtkTreePath *ide_tree_model_get_path_for_node (IdeTreeModel *self,
+ IdeTreeNode *node);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_tree_model_get_iter_for_node (IdeTreeModel *self,
+ GtkTreeIter *iter,
+ IdeTreeNode *node);
+IDE_AVAILABLE_IN_3_32
+void ide_tree_model_invalidate (IdeTreeModel *self,
+ IdeTreeNode *node);
+IDE_AVAILABLE_IN_3_32
+void ide_tree_model_expand_async (IdeTreeModel *self,
+ IdeTreeNode *node,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_tree_model_expand_finish (IdeTreeModel *self,
+ GAsyncResult *result,
+ GError **error);
+
+G_END_DECLS
diff --git a/src/libide/tree/ide-tree-node.c b/src/libide/tree/ide-tree-node.c
new file mode 100644
index 000000000..aeda3c25b
--- /dev/null
+++ b/src/libide/tree/ide-tree-node.c
@@ -0,0 +1,1863 @@
+/* ide-tree-node.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-tree-node"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "ide-tree-model.h"
+#include "ide-tree-node.h"
+#include "ide-tree-private.h"
+
+/**
+ * SECTION:ide-tree-node
+ * @title: IdeTreeNode
+ * @short_description: a node within the tree
+ *
+ * The #IdeTreeNode class is used to represent an item that should
+ * be displayed in the tree of the Ide application. The
+ * #IdeTreeAddin plugins create and maintain these nodes during the
+ * lifetime of the program.
+ *
+ * Plugins that want to add items to the tree should implement the
+ * #IdeTreeAddin interface and register it during plugin
+ * initialization.
+ *
+ * Since: 3.32
+ */
+
+struct _IdeTreeNode
+{
+ GObject parent_instance;
+
+ /* A pointer to the model, which is only set on the root node. */
+ IdeTreeModel *model;
+
+ /*
+ * The following are fields containing the values for various properties
+ * on the tree node. Usually, icon, display_name, and item will be set
+ * on all nodes.
+ */
+ GIcon *icon;
+ GIcon *expanded_icon;
+ gchar *display_name;
+ GObject *item;
+ gchar *tag;
+ GList *emblems;
+
+ /*
+ * The following items are used to maintain a tree structure of
+ * nodes for which we can use O(1) operations. The link is inserted
+ * into the parents children queue. The parent pointer is unowned,
+ * and set by the parent (cleared upon removal).
+ *
+ * This also allows maintaining the tree structure with zero additional
+ * allocations beyond the nodes themselves.
+ */
+ IdeTreeNode *parent;
+ GQueue children;
+ GList link;
+
+ /* Foreground and Background colors */
+ GdkRGBA background;
+ GdkRGBA foreground;
+
+ /* When did we start loading? This is used to avoid drawing "Loading..."
+ * when the tree loads really quickly. Otherwise, we risk looking janky
+ * when the loads are quite fast.
+ */
+ gint64 started_loading_at;
+
+ /* If we're currently loading */
+ guint is_loading : 1;
+
+ /* If the node is a header (bold, etc) */
+ guint is_header : 1;
+
+ /* If this is a synthesized empty node */
+ guint is_empty : 1;
+
+ /* If the node maybe has children */
+ guint children_possible : 1;
+
+ /* If this node needs to have the children built */
+ guint needs_build_children : 1;
+
+ /* If true, we remove all children on collapse */
+ guint reset_on_collapse : 1;
+
+ /* If true, we use ide_clear_and_destroy_object() */
+ guint destroy_item : 1;
+
+ /* If colors are set */
+ guint background_set : 1;
+ guint foreground_set : 1;
+};
+
+G_DEFINE_TYPE (IdeTreeNode, ide_tree_node, G_TYPE_OBJECT)
+
+enum {
+ PROP_0,
+ PROP_CHILDREN_POSSIBLE,
+ PROP_DESTROY_ITEM,
+ PROP_DISPLAY_NAME,
+ PROP_EXPANDED_ICON,
+ PROP_EXPANDED_ICON_NAME,
+ PROP_ICON,
+ PROP_ICON_NAME,
+ PROP_IS_HEADER,
+ PROP_ITEM,
+ PROP_RESET_ON_COLLAPSE,
+ PROP_TAG,
+ N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static IdeTreeModel *
+ide_tree_node_get_model (IdeTreeNode *self)
+{
+ return ide_tree_node_get_root (self)->model;
+}
+
+/**
+ * ide_tree_node_new:
+ *
+ * Create a new #IdeTreeNode.
+ *
+ * Returns: (transfer full): a newly created #IdeTreeNode
+ *
+ * Since: 3.32
+ */
+IdeTreeNode *
+ide_tree_node_new (void)
+{
+ return g_object_new (IDE_TYPE_TREE_NODE, NULL);
+}
+
+static void
+ide_tree_node_emit_changed (IdeTreeNode *self)
+{
+ g_autoptr(GtkTreePath) path = NULL;
+ IdeTreeModel *model;
+ GtkTreeIter iter = { .user_data = self };
+
+ g_assert (IDE_IS_TREE_NODE (self));
+
+ if (!(model = ide_tree_node_get_model (self)))
+ return;
+
+ if ((path = ide_tree_model_get_path_for_node (model, self)))
+ gtk_tree_model_row_changed (GTK_TREE_MODEL (model), path, &iter);
+}
+
+static void
+ide_tree_node_remove_with_dispose (IdeTreeNode *self,
+ IdeTreeNode *child)
+{
+ g_object_ref (child);
+ ide_tree_node_remove (self, child);
+ g_object_run_dispose (G_OBJECT (child));
+ g_object_unref (child);
+}
+
+static void
+ide_tree_node_dispose (GObject *object)
+{
+ IdeTreeNode *self = (IdeTreeNode *)object;
+
+ while (self->children.length > 0)
+ ide_tree_node_remove_with_dispose (self, g_queue_peek_nth (&self->children, 0));
+
+ if (self->destroy_item && IDE_IS_OBJECT (self->item))
+ ide_clear_and_destroy_object (&self->item);
+ else
+ g_clear_object (&self->item);
+
+ g_list_free_full (self->emblems, g_object_unref);
+ self->emblems = NULL;
+
+ g_clear_object (&self->icon);
+ g_clear_object (&self->expanded_icon);
+ g_clear_pointer (&self->display_name, g_free);
+ g_clear_pointer (&self->tag, g_free);
+
+ G_OBJECT_CLASS (ide_tree_node_parent_class)->dispose (object);
+}
+
+static void
+ide_tree_node_finalize (GObject *object)
+{
+ IdeTreeNode *self = (IdeTreeNode *)object;
+
+ g_clear_weak_pointer (&self->model);
+
+ g_assert (self->children.head == NULL);
+ g_assert (self->children.tail == NULL);
+ g_assert (self->children.length == 0);
+
+ if (self->destroy_item && IDE_IS_OBJECT (self->item))
+ ide_clear_and_destroy_object (&self->item);
+ else
+ g_clear_object (&self->item);
+
+ g_clear_object (&self->icon);
+ g_clear_object (&self->expanded_icon);
+ g_clear_pointer (&self->display_name, g_free);
+ g_clear_pointer (&self->tag, g_free);
+
+ G_OBJECT_CLASS (ide_tree_node_parent_class)->finalize (object);
+}
+
+static void
+ide_tree_node_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ IdeTreeNode *self = IDE_TREE_NODE (object);
+
+ switch (prop_id)
+ {
+ case PROP_CHILDREN_POSSIBLE:
+ g_value_set_boolean (value, ide_tree_node_get_children_possible (self));
+ break;
+
+ case PROP_DESTROY_ITEM:
+ g_value_set_boolean (value, self->destroy_item);
+ break;
+
+ case PROP_DISPLAY_NAME:
+ g_value_set_string (value, ide_tree_node_get_display_name (self));
+ break;
+
+ case PROP_ICON:
+ g_value_set_object (value, ide_tree_node_get_icon (self));
+ break;
+
+ case PROP_IS_HEADER:
+ g_value_set_boolean (value, ide_tree_node_get_is_header (self));
+ break;
+
+ case PROP_ITEM:
+ g_value_set_object (value, ide_tree_node_get_item (self));
+ break;
+
+ case PROP_RESET_ON_COLLAPSE:
+ g_value_set_boolean (value, ide_tree_node_get_reset_on_collapse (self));
+ break;
+
+ case PROP_TAG:
+ g_value_set_string (value, ide_tree_node_get_tag (self));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+ide_tree_node_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ IdeTreeNode *self = IDE_TREE_NODE (object);
+
+ switch (prop_id)
+ {
+ case PROP_CHILDREN_POSSIBLE:
+ ide_tree_node_set_children_possible (self, g_value_get_boolean (value));
+ break;
+
+ case PROP_DESTROY_ITEM:
+ self->destroy_item = g_value_get_boolean (value);
+ break;
+
+ case PROP_DISPLAY_NAME:
+ ide_tree_node_set_display_name (self, g_value_get_string (value));
+ break;
+
+ case PROP_EXPANDED_ICON:
+ ide_tree_node_set_expanded_icon (self, g_value_get_object (value));
+ break;
+
+ case PROP_EXPANDED_ICON_NAME:
+ ide_tree_node_set_expanded_icon_name (self, g_value_get_string (value));
+ break;
+
+ case PROP_ICON:
+ ide_tree_node_set_icon (self, g_value_get_object (value));
+ break;
+
+ case PROP_ICON_NAME:
+ ide_tree_node_set_icon_name (self, g_value_get_string (value));
+ break;
+
+ case PROP_IS_HEADER:
+ ide_tree_node_set_is_header (self, g_value_get_boolean (value));
+ break;
+
+ case PROP_ITEM:
+ ide_tree_node_set_item (self, g_value_get_object (value));
+ break;
+
+ case PROP_RESET_ON_COLLAPSE:
+ ide_tree_node_set_reset_on_collapse (self, g_value_get_boolean (value));
+ break;
+
+ case PROP_TAG:
+ ide_tree_node_set_tag (self, g_value_get_string (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+ide_tree_node_class_init (IdeTreeNodeClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->dispose = ide_tree_node_dispose;
+ object_class->finalize = ide_tree_node_finalize;
+ object_class->get_property = ide_tree_node_get_property;
+ object_class->set_property = ide_tree_node_set_property;
+
+ /**
+ * IdeTreeNode:children-possible:
+ *
+ * The "children-possible" property denotes if the node may have children
+ * even if it doesn't have children yet. This is useful for delayed loading
+ * of children nodes.
+ *
+ * Since: 3.32
+ */
+ properties [PROP_CHILDREN_POSSIBLE] =
+ g_param_spec_boolean ("children-possible",
+ "Children Possible",
+ "If children are possible for the node",
+ FALSE,
+ (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * IdeTreeNode:destroy-item:
+ *
+ * If %TRUE and #IdeTreeNode:item is an #IdeObject, it will be destroyed
+ * when the node is destroyed.
+ *
+ * Since: 3.32
+ */
+ properties [PROP_DESTROY_ITEM] =
+ g_param_spec_boolean ("destroy-item",
+ "Destroy Item",
+ "If the item should be destroyed with the node.",
+ FALSE,
+ (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * IdeTreeNode:display-name:
+ *
+ * The "display-name" property is the name for the node as it should be
+ * displayed in the tree.
+ *
+ * Since: 3.32
+ */
+ properties [PROP_DISPLAY_NAME] =
+ g_param_spec_string ("display-name",
+ "Display Name",
+ "Display name for the node in the tree",
+ NULL,
+ (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * IdeTreeNode:expanded-icon:
+ *
+ * The "expanded-icon" property is the icon that should be displayed to the
+ * user in the tree for this node.
+ *
+ * Since: 3.32
+ */
+ properties [PROP_EXPANDED_ICON] =
+ g_param_spec_object ("expanded-icon",
+ "Expanded Icon",
+ "The expanded icon to display in the tree",
+ G_TYPE_ICON,
+ (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * IdeTreeNode:expanded-icon-name:
+ *
+ * The "expanded-icon-name" is a convenience property to set the
+ * #IdeTreeNode:expanded-icon property using an icon-name.
+ *
+ * Since: 3.32
+ */
+ properties [PROP_EXPANDED_ICON_NAME] =
+ g_param_spec_string ("expanded-icon-name",
+ "Expanded Icon Name",
+ "The expanded icon-name for the GIcon",
+ NULL,
+ (G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * IdeTreeNode:icon:
+ *
+ * The "icon" property is the icon that should be displayed to the
+ * user in the tree for this node.
+ *
+ * Since: 3.32
+ */
+ properties [PROP_ICON] =
+ g_param_spec_object ("icon",
+ "Icon",
+ "The icon to display in the tree",
+ G_TYPE_ICON,
+ (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * IdeTreeNode:icon-name:
+ *
+ * The "icon-name" is a convenience property to set the #IdeTreeNode:icon
+ * property using an icon-name.
+ *
+ * Since: 3.32
+ */
+ properties [PROP_ICON_NAME] =
+ g_param_spec_string ("icon-name",
+ "Icon Name",
+ "The icon-name for the GIcon",
+ NULL,
+ (G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * IdeTreeNode:is-header:
+ *
+ * The "is-header" property denotes the node should be styled as a group
+ * header.
+ *
+ * Since: 3.32
+ */
+ properties [PROP_IS_HEADER] =
+ g_param_spec_boolean ("is-header",
+ "Is Header",
+ "If the node is a header",
+ FALSE,
+ (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * IdeTreeNode:item:
+ *
+ * The "item" property is an optional #GObject that can be used to
+ * store information about the node, which is sometimes useful when
+ * creating #IdeTreeAddin plugins.
+ *
+ * Since: 3.32
+ */
+ properties [PROP_ITEM] =
+ g_param_spec_object ("item",
+ "Item",
+ "Item",
+ G_TYPE_OBJECT,
+ (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * IdeTreeNode:reset-on-collapse:
+ *
+ * The "reset-on-collapse" denotes that children should be removed when
+ * the node is collapsed.
+ *
+ * Since: 3.32
+ */
+ properties [PROP_RESET_ON_COLLAPSE] =
+ g_param_spec_boolean ("reset-on-collapse",
+ "Reset on Collapse",
+ "If the children are removed when the node is collapsed",
+ TRUE,
+ (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * IdeTreeNode:tag:
+ *
+ * The "tag" property can be used to denote the type of node when you do not have an
+ * object to assign to #IdeTreeNode:item.
+ *
+ * See ide_tree_node_is_tag() to match a tag when building.
+ *
+ * Since: 3.32
+ */
+ properties [PROP_TAG] =
+ g_param_spec_string ("tag",
+ "Tag",
+ "The tag for the node if any",
+ 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_tree_node_init (IdeTreeNode *self)
+{
+ self->reset_on_collapse = TRUE;
+ self->link.data = self;
+}
+
+/**
+ * ide_tree_node_get_display_name:
+ * @self: a #IdeTreeNode
+ *
+ * Gets the #IdeTreeNode:display-name property.
+ *
+ * Returns: (nullable): a string containing the display name
+ *
+ * Since: 3.32
+ */
+const gchar *
+ide_tree_node_get_display_name (IdeTreeNode *self)
+{
+ g_return_val_if_fail (IDE_IS_TREE_NODE (self), NULL);
+
+ return self->display_name;
+}
+
+/**
+ * ide_tree_node_set_display_name:
+ *
+ * Sets the #IdeTreeNode:display-name property, which is the text to
+ * use when displaying the item in the tree.
+ *
+ * Since: 3.32
+ */
+void
+ide_tree_node_set_display_name (IdeTreeNode *self,
+ const gchar *display_name)
+{
+ g_return_if_fail (IDE_IS_TREE_NODE (self));
+
+ if (g_strcmp0 (display_name, self->display_name) != 0)
+ {
+ g_free (self->display_name);
+ self->display_name = g_strdup (display_name);
+ ide_tree_node_emit_changed (self);
+ g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_DISPLAY_NAME]);
+ }
+}
+
+/**
+ * ide_tree_node_get_icon:
+ * @self: a #IdeTree
+ *
+ * Gets the icon associated with the tree node.
+ *
+ * Returns: (transfer none) (nullable): a #GIcon or %NULL
+ *
+ * Since: 3.32
+ */
+GIcon *
+ide_tree_node_get_icon (IdeTreeNode *self)
+{
+ g_return_val_if_fail (IDE_IS_TREE_NODE (self), NULL);
+
+ return self->icon;
+}
+
+/**
+ * ide_tree_node_set_icon:
+ * @self: a @IdeTreeNode
+ * @icon: (nullable): a #GIcon or %NULL
+ *
+ * Sets the icon for the tree node.
+ *
+ * Since: 3.32
+ */
+void
+ide_tree_node_set_icon (IdeTreeNode *self,
+ GIcon *icon)
+{
+ g_return_if_fail (IDE_IS_TREE_NODE (self));
+
+ if (g_set_object (&self->icon, icon))
+ {
+ ide_tree_node_emit_changed (self);
+ g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ICON]);
+ }
+}
+
+/**
+ * ide_tree_node_get_expanded_icon:
+ * @self: a #IdeTree
+ *
+ * Gets the expanded icon associated with the tree node.
+ *
+ * Returns: (transfer none) (nullable): a #GIcon or %NULL
+ *
+ * Since: 3.32
+ */
+GIcon *
+ide_tree_node_get_expanded_icon (IdeTreeNode *self)
+{
+ g_return_val_if_fail (IDE_IS_TREE_NODE (self), NULL);
+
+ return self->expanded_icon ? self->expanded_icon : self->icon;
+}
+
+/**
+ * ide_tree_node_set_expanded_icon:
+ * @self: a @IdeTreeNode
+ * @expanded_icon: (nullable): a #GIcon or %NULL
+ *
+ * Sets the expanded icon for the tree node.
+ *
+ * Since: 3.32
+ */
+void
+ide_tree_node_set_expanded_icon (IdeTreeNode *self,
+ GIcon *expanded_icon)
+{
+ g_return_if_fail (IDE_IS_TREE_NODE (self));
+
+ if (g_set_object (&self->expanded_icon, expanded_icon))
+ {
+ ide_tree_node_emit_changed (self);
+ g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_EXPANDED_ICON]);
+ }
+}
+
+/**
+ * ide_tree_node_get_item:
+ * @self: a #IdeTreeNode
+ *
+ * Gets the item that has been associated with the node.
+ *
+ * Returns: (transfer none) (type GObject.Object) (nullable): a #GObject
+ * if the item has been previously set.
+ *
+ * Since: 3.32
+ */
+gpointer
+ide_tree_node_get_item (IdeTreeNode *self)
+{
+ g_return_val_if_fail (IDE_IS_TREE_NODE (self), NULL);
+ g_return_val_if_fail (!self->item || G_IS_OBJECT (self->item), NULL);
+
+ return self->item;
+}
+
+void
+ide_tree_node_set_item (IdeTreeNode *self,
+ gpointer item)
+{
+ g_return_if_fail (IDE_IS_TREE_NODE (self));
+ g_return_if_fail (!item || G_IS_OBJECT (item));
+
+ if (g_set_object (&self->item, item))
+ {
+ ide_tree_node_emit_changed (self);
+ g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ITEM]);
+ }
+}
+
+static IdeTreeNodeVisit
+ide_tree_node_row_inserted_traverse_cb (IdeTreeNode *node,
+ gpointer user_data)
+{
+ IdeTreeModel *model = user_data;
+ g_autoptr(GtkTreePath) path = NULL;
+ GtkTreeIter iter = { .user_data = node };
+
+ g_assert (IDE_IS_TREE_NODE (node));
+ g_assert (IDE_IS_TREE_MODEL (model));
+
+ /* Ignore the root node, nothing to do with that */
+ if (ide_tree_node_is_root (node))
+ return IDE_TREE_NODE_VISIT_CHILDREN;
+
+ /* It would be faster to create our paths as we traverse the tree,
+ * but that complicates the traversal. Generally this path should get
+ * hit very little (as usually it's only a single "child node").
+ */
+ if ((path = gtk_tree_model_get_path (GTK_TREE_MODEL (model), &iter)))
+ {
+ gtk_tree_model_row_inserted (GTK_TREE_MODEL (model), path, &iter);
+
+ if (ide_tree_node_is_first (node))
+ {
+ IdeTreeNode *parent = ide_tree_node_get_parent (node);
+
+ if (!ide_tree_node_is_root (parent))
+ {
+ iter.user_data = parent;
+ gtk_tree_path_up (path);
+ gtk_tree_model_row_has_child_toggled (GTK_TREE_MODEL (model), path, &iter);
+ }
+ }
+ }
+
+ return IDE_TREE_NODE_VISIT_CHILDREN;
+}
+
+static void
+ide_tree_node_row_inserted (IdeTreeNode *self,
+ IdeTreeNode *child)
+{
+ g_autoptr(GtkTreePath) path = NULL;
+ IdeTreeModel *model;
+ GtkTreeIter iter;
+
+ g_assert (IDE_IS_MAIN_THREAD ());
+ g_assert (IDE_IS_TREE_NODE (self));
+ g_assert (IDE_IS_TREE_NODE (child));
+
+ if (!(model = ide_tree_node_get_model (self)) ||
+ !ide_tree_model_get_iter_for_node (model, &iter, child) ||
+ !(path = ide_tree_model_get_path_for_node (model, child)))
+ return;
+
+ ide_tree_node_traverse (child,
+ G_PRE_ORDER,
+ G_TRAVERSE_ALL,
+ -1,
+ ide_tree_node_row_inserted_traverse_cb,
+ model);
+}
+
+void
+_ide_tree_node_set_model (IdeTreeNode *self,
+ IdeTreeModel *model)
+{
+ g_return_if_fail (IDE_IS_TREE_NODE (self));
+ g_return_if_fail (!model || IDE_IS_TREE_MODEL (model));
+
+ if (g_set_weak_pointer (&self->model, model))
+ {
+ if (self->model != NULL)
+ ide_tree_node_row_inserted (self, self);
+ }
+}
+
+/**
+ * ide_tree_node_prepend:
+ * @self: a #IdeTreeNode
+ * @child: a #IdeTreeNode
+ *
+ * Prepends @child as a child of @self at the 0 index.
+ *
+ * This operation is O(1).
+ *
+ * Since: 3.32
+ */
+void
+ide_tree_node_prepend (IdeTreeNode *self,
+ IdeTreeNode *child)
+{
+ g_return_if_fail (IDE_IS_TREE_NODE (self));
+ g_return_if_fail (IDE_IS_TREE_NODE (child));
+ g_return_if_fail (child->parent == NULL);
+
+ child->parent = self;
+ g_object_ref (child);
+ g_queue_push_head_link (&self->children, &child->link);
+
+ ide_tree_node_row_inserted (self, child);
+}
+
+/**
+ * ide_tree_node_append:
+ * @self: a #IdeTreeNode
+ * @child: a #IdeTreeNode
+ *
+ * Appends @child as a child of @self at the last position.
+ *
+ * This operation is O(1).
+ *
+ * Since: 3.32
+ */
+void
+ide_tree_node_append (IdeTreeNode *self,
+ IdeTreeNode *child)
+{
+ g_return_if_fail (IDE_IS_TREE_NODE (self));
+ g_return_if_fail (IDE_IS_TREE_NODE (child));
+ g_return_if_fail (child->parent == NULL);
+
+ child->parent = self;
+ g_object_ref (child);
+ g_queue_push_tail_link (&self->children, &child->link);
+
+ ide_tree_node_row_inserted (self, child);
+}
+
+/**
+ * ide_tree_node_insert_before:
+ * @self: a #IdeTreeNode
+ * @child: a #IdeTreeNode
+ *
+ * Inserts @child directly before @self by adding it to the parent of @self.
+ *
+ * This operation is O(1).
+ *
+ * Since: 3.32
+ */
+void
+ide_tree_node_insert_before (IdeTreeNode *self,
+ IdeTreeNode *child)
+{
+ g_return_if_fail (IDE_IS_TREE_NODE (self));
+ g_return_if_fail (IDE_IS_TREE_NODE (child));
+ g_return_if_fail (self->parent != NULL);
+ g_return_if_fail (child->parent == NULL);
+
+ child->parent = self->parent;
+ g_object_ref (child);
+ _g_queue_insert_before_link (&self->parent->children, &self->link, &child->link);
+
+ ide_tree_node_row_inserted (self, child);
+}
+
+/**
+ * ide_tree_node_insert_after:
+ * @self: a #IdeTreeNode
+ * @child: a #IdeTreeNode
+ *
+ * Inserts @child directly after @self by adding it to the parent of @self.
+ *
+ * This operation is O(1).
+ *
+ * Since: 3.32
+ */
+void
+ide_tree_node_insert_after (IdeTreeNode *self,
+ IdeTreeNode *child)
+{
+ g_return_if_fail (IDE_IS_TREE_NODE (self));
+ g_return_if_fail (IDE_IS_TREE_NODE (child));
+ g_return_if_fail (self->parent != NULL);
+ g_return_if_fail (child->parent == NULL);
+
+ child->parent = self->parent;
+ g_object_ref (child);
+ _g_queue_insert_after_link (&self->parent->children, &self->link, &child->link);
+
+ ide_tree_node_row_inserted (self, child);
+}
+
+/**
+ * ide_tree_node_remove:
+ * @self: a #IdeTreeNode
+ * @child: a #IdeTreeNode
+ *
+ * Removes the child node @child from @self. @self must be the parent of @child.
+ *
+ * This function is O(1).
+ *
+ * Since: 3.32
+ */
+void
+ide_tree_node_remove (IdeTreeNode *self,
+ IdeTreeNode *child)
+{
+ g_autoptr(GtkTreePath) path = NULL;
+ IdeTreeModel *model;
+
+ g_return_if_fail (IDE_IS_TREE_NODE (self));
+ g_return_if_fail (IDE_IS_TREE_NODE (child));
+ g_return_if_fail (child->parent == self);
+
+ if ((model = ide_tree_node_get_model (self)))
+ path = ide_tree_model_get_path_for_node (model, child);
+
+ child->parent = NULL;
+ g_queue_unlink (&self->children, &child->link);
+
+ if (path != NULL)
+ gtk_tree_model_row_deleted (GTK_TREE_MODEL (model), path);
+
+ g_object_unref (child);
+}
+
+/**
+ * ide_tree_node_get_parent:
+ * @self: a #IdeTreeNode
+ *
+ * Gets the parent node of @self.
+ *
+ * Returns: (transfer none) (nullable): a #IdeTreeNode or %NULL
+ *
+ * Since: 3.32
+ */
+IdeTreeNode *
+ide_tree_node_get_parent (IdeTreeNode *self)
+{
+ g_return_val_if_fail (IDE_IS_TREE_NODE (self), NULL);
+
+ return self->parent;
+}
+
+/**
+ * ide_tree_node_get_root:
+ * @self: a #IdeTreeNode
+ *
+ * Gets the root #IdeTreeNode by following the #IdeTreeNode:parent
+ * properties of each node.
+ *
+ * Returns: (transfer none) (nullable): a #IdeTreeNode or %NULL
+ *
+ * Since: 3.32
+ */
+IdeTreeNode *
+ide_tree_node_get_root (IdeTreeNode *self)
+{
+ g_return_val_if_fail (IDE_IS_TREE_NODE (self), NULL);
+
+ while (self->parent != NULL)
+ self = self->parent;
+
+ g_return_val_if_fail (IDE_IS_TREE_NODE (self), NULL);
+
+ return self;
+}
+
+/**
+ * ide_tree_node_holds:
+ * @self: a #IdeTreeNode
+ * @type: a #GType
+ *
+ * Checks to see if the #IdeTreeNode:item property matches @type
+ * or is a subclass of @type.
+ *
+ * Returns: %TRUE if @self holds a @type item
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_tree_node_holds (IdeTreeNode *self,
+ GType type)
+{
+ g_return_val_if_fail (IDE_IS_TREE_NODE (self), FALSE);
+
+ return G_TYPE_CHECK_INSTANCE_TYPE (self->item, type);
+}
+
+/**
+ * ide_tree_node_get_index:
+ * @self: a #IdeTreeNode
+ *
+ * Gets the position of the @self.
+ *
+ * Returns: the offset of @self with it's siblings.
+ *
+ * Since: 3.32
+ */
+guint
+ide_tree_node_get_index (IdeTreeNode *self)
+{
+ g_return_val_if_fail (IDE_IS_TREE_NODE (self), 0);
+
+ if (self->parent != NULL)
+ return g_list_position (self->parent->children.head, &self->link);
+
+ return 0;
+}
+
+/**
+ * ide_tree_node_get_nth_child:
+ * @self: a #IdeTreeNode
+ * @index_: the index of the child
+ *
+ * Gets the @nth child of the tree node or %NULL if it does not exist.
+ *
+ * Returns: (transfer none) (nullable): a #IdeTreeNode or %NULL
+ *
+ * Since: 3.32
+ */
+IdeTreeNode *
+ide_tree_node_get_nth_child (IdeTreeNode *self,
+ guint index_)
+{
+ g_return_val_if_fail (IDE_IS_TREE_NODE (self), NULL);
+
+ return g_queue_peek_nth (&self->children, index_);
+}
+
+/**
+ * ide_tree_node_get_next:
+ * @self: a #IdeTreeNode
+ *
+ * Gets the next sibling after @self.
+ *
+ * Returns: (transfer none) (nullable): a #IdeTreeNode or %NULL
+ *
+ * Since: 3.32
+ */
+IdeTreeNode *
+ide_tree_node_get_next (IdeTreeNode *self)
+{
+ g_return_val_if_fail (IDE_IS_TREE_NODE (self), NULL);
+
+ if (self->link.next)
+ return self->link.next->data;
+
+ return NULL;
+}
+
+/**
+ * ide_tree_node_get_previous:
+ * @self: a #IdeTreeNode
+ *
+ * Gets the previous sibling before @self.
+ *
+ * Returns: (transfer none) (nullable): a #IdeTreeNode or %NULL
+ *
+ * Since: 3.32
+ */
+IdeTreeNode *
+ide_tree_node_get_previous (IdeTreeNode *self)
+{
+ g_return_val_if_fail (IDE_IS_TREE_NODE (self), NULL);
+
+ if (self->link.prev)
+ return self->link.prev->data;
+
+ return NULL;
+}
+
+/**
+ * ide_tree_node_get_children_possible:
+ * @self: a #IdeTreeNode
+ *
+ * Checks if the node can have children, and if so, returns %TRUE.
+ * It may not actually have children yet.
+ *
+ * Returns: %TRUE if the children may have children
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_tree_node_get_children_possible (IdeTreeNode *self)
+{
+ g_return_val_if_fail (IDE_IS_TREE_NODE (self), FALSE);
+
+ return self->children_possible;
+}
+
+/**
+ * ide_tree_node_set_children_possible:
+ * @self: a #IdeTreeNode
+ * @children_possible: if children are possible
+ *
+ * Sets if the children are possible for the node.
+ *
+ * Since: 3.32
+ */
+void
+ide_tree_node_set_children_possible (IdeTreeNode *self,
+ gboolean children_possible)
+{
+ g_return_if_fail (IDE_IS_TREE_NODE (self));
+
+ children_possible = !!children_possible;
+
+ if (children_possible != self->children_possible)
+ {
+ self->children_possible = children_possible;
+ self->needs_build_children = children_possible;
+
+ if (self->children_possible && self->children.length == 0)
+ {
+ g_autoptr(IdeTreeNode) child = NULL;
+
+ child = g_object_new (IDE_TYPE_TREE_NODE,
+ "display-name", _("(Empty)"),
+ NULL);
+ child->is_empty = TRUE;
+ ide_tree_node_append (self, child);
+
+ g_assert (ide_tree_node_has_child (self) == children_possible);
+ }
+
+ g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CHILDREN_POSSIBLE]);
+ }
+}
+
+/**
+ * ide_tree_node_has_child:
+ * @self: a #IdeTreeNode
+ *
+ * Checks if @self has any children.
+ *
+ * Returns: %TRUE if @self has one or more children.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_tree_node_has_child (IdeTreeNode *self)
+{
+ g_return_val_if_fail (IDE_IS_TREE_NODE (self), FALSE);
+
+ return self->children.length > 0;
+}
+
+/**
+ * ide_tree_node_get_n_children:
+ * @self: a #IdeTreeNode
+ *
+ * Gets the number of children that @self contains.
+ *
+ * Returns: the number of children
+ *
+ * Since: 3.32
+ */
+guint
+ide_tree_node_get_n_children (IdeTreeNode *self)
+{
+ g_return_val_if_fail (IDE_IS_TREE_NODE (self), 0);
+
+ return self->children.length;
+}
+
+/**
+ * ide_tree_node_get_is_header:
+ * @self: a #IdeTreeNode
+ *
+ * Gets the #IdeTreeNode:is-header property.
+ *
+ * If this is %TRUE, then the node will be rendered with alternate
+ * styling for group headers.
+ *
+ * Returns: %TRUE if @self is a header.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_tree_node_get_is_header (IdeTreeNode *self)
+{
+ g_return_val_if_fail (IDE_IS_TREE_NODE (self), FALSE);
+
+ return self->is_header;
+}
+
+/**
+ * ide_tree_node_set_is_header:
+ * @self: a #IdeTreeNode
+ *
+ * Sets the #IdeTreeNode:is-header property.
+ *
+ * Since: 3.32
+ */
+void
+ide_tree_node_set_is_header (IdeTreeNode *self,
+ gboolean is_header)
+{
+ g_return_if_fail (IDE_IS_TREE_NODE (self));
+
+ is_header = !!is_header;
+
+ if (self->is_header != is_header)
+ {
+ self->is_header = is_header;
+ g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_IS_HEADER]);
+ }
+}
+
+typedef struct
+{
+ GTraverseType type;
+ GTraverseFlags flags;
+ gint depth;
+ IdeTreeTraverseFunc callback;
+ gpointer user_data;
+} IdeTreeTraversal;
+
+static inline gboolean
+can_callback_node (IdeTreeNode *node,
+ GTraverseFlags flags)
+{
+ return ((flags & G_TRAVERSE_LEAVES) && node->children.length == 0) ||
+ ((flags & G_TRAVERSE_NON_LEAVES) && node->children.length > 0);
+}
+
+static gboolean
+do_traversal (IdeTreeNode *node,
+ IdeTreeTraversal *traversal)
+{
+ const GList *iter;
+ IdeTreeNodeVisit ret = IDE_TREE_NODE_VISIT_BREAK;
+
+ if (traversal->depth < 0)
+ return IDE_TREE_NODE_VISIT_CONTINUE;
+
+ traversal->depth--;
+
+ if (traversal->type == G_PRE_ORDER && can_callback_node (node, traversal->flags))
+ {
+ ret = traversal->callback (node, traversal->user_data);
+
+ if (!ide_tree_node_is_root (node) &&
+ (ret == IDE_TREE_NODE_VISIT_CONTINUE || ret == IDE_TREE_NODE_VISIT_BREAK))
+ goto finish;
+ }
+
+ iter = node->children.head;
+
+ while (iter != NULL)
+ {
+ IdeTreeNode *child = iter->data;
+
+ iter = iter->next;
+
+ ret = do_traversal (child, traversal);
+
+ if (ret == IDE_TREE_NODE_VISIT_BREAK)
+ goto finish;
+ }
+
+ if (traversal->type == G_POST_ORDER && can_callback_node (node, traversal->flags))
+ ret = traversal->callback (node, traversal->user_data);
+
+finish:
+ traversal->depth++;
+
+ return ret;
+}
+
+/**
+ * ide_tree_node_traverse:
+ * @self: a #IdeTreeNode
+ * @traverse_type: the type of traversal, pre and post supported
+ * @traverse_flags: the flags for what nodes to match
+ * @max_depth: the max depth for the traversal or -1 for all
+ * @traverse_func: (scope call): the callback for each matching node
+ * @user_data: user data for @traverse_func
+ *
+ * Calls @traverse_func for each node that matches the requested
+ * type, flags, and depth.
+ *
+ * Traversal is stopped if @traverse_func returns %TRUE.
+ *
+ * Since: 3.32
+ */
+void
+ide_tree_node_traverse (IdeTreeNode *self,
+ GTraverseType traverse_type,
+ GTraverseFlags traverse_flags,
+ gint max_depth,
+ IdeTreeTraverseFunc traverse_func,
+ gpointer user_data)
+{
+ IdeTreeTraversal traverse;
+
+ g_return_if_fail (IDE_IS_TREE_NODE (self));
+ g_return_if_fail (traverse_type == G_PRE_ORDER ||
+ traverse_type == G_POST_ORDER);
+ g_return_if_fail (traverse_func != NULL);
+
+ traverse.type = traverse_type;
+ traverse.flags = traverse_flags;
+ traverse.depth = max_depth < 0 ? G_MAXINT : max_depth;
+ traverse.callback = traverse_func;
+ traverse.user_data = user_data;
+
+ do_traversal (self, &traverse);
+}
+
+/**
+ * ide_tree_node_is_empty:
+ * @self: a #IdeTreeNode
+ *
+ * This function checks if @self is a synthesized "empty" node.
+ *
+ * Empty nodes are added to #IdeTreeNode that may have children in the
+ * future, but are currently empty. It allows the tree to display the
+ * "(Empty)" contents and show a proper expander arrow.
+ *
+ * Returns: %TRUE if @self is a synthesized empty node.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_tree_node_is_empty (IdeTreeNode *self)
+{
+ g_return_val_if_fail (IDE_IS_TREE_NODE (self), FALSE);
+
+ return self->is_empty;
+}
+
+gboolean
+_ide_tree_node_get_needs_build_children (IdeTreeNode *self)
+{
+ g_return_val_if_fail (IDE_IS_TREE_NODE (self), FALSE);
+
+ return self->needs_build_children;
+}
+
+void
+_ide_tree_node_set_needs_build_children (IdeTreeNode *self,
+ gboolean needs_build_children)
+{
+ g_return_if_fail (IDE_IS_TREE_NODE (self));
+
+ self->needs_build_children = !!needs_build_children;
+}
+
+/**
+ * ide_tree_node_set_icon_name:
+ * @self: a #IdeTreeNode
+ * @icon_name: (nullable): the name of the icon, or %NULL
+ *
+ * Sets the #IdeTreeNode:icon property using an icon-name.
+ *
+ * Since: 3.32
+ */
+void
+ide_tree_node_set_icon_name (IdeTreeNode *self,
+ const gchar *icon_name)
+{
+ g_autoptr(GIcon) icon = NULL;
+
+ g_return_if_fail (IDE_IS_TREE_NODE (self));
+
+ if (icon_name != NULL)
+ icon = g_themed_icon_new (icon_name);
+ ide_tree_node_set_icon (self, icon);
+}
+
+/**
+ * ide_tree_node_set_expanded_icon_name:
+ * @self: a #IdeTreeNode
+ * @expanded_icon_name: (nullable): the name of the icon, or %NULL
+ *
+ * Sets the #IdeTreeNode:icon property using an icon-name.
+ *
+ * Since: 3.32
+ */
+void
+ide_tree_node_set_expanded_icon_name (IdeTreeNode *self,
+ const gchar *expanded_icon_name)
+{
+ g_autoptr(GIcon) icon = NULL;
+
+ g_return_if_fail (IDE_IS_TREE_NODE (self));
+
+ if (expanded_icon_name != NULL)
+ icon = g_themed_icon_new (expanded_icon_name);
+ ide_tree_node_set_expanded_icon (self, icon);
+}
+
+/**
+ * ide_tree_node_is_root:
+ * @self: a #IdeTreeNode
+ *
+ * Checks if @self is the root node, meaning it has no parent.
+ *
+ * Returns: %TRUE if @self has no parent.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_tree_node_is_root (IdeTreeNode *self)
+{
+ g_return_val_if_fail (IDE_IS_TREE_NODE (self), FALSE);
+
+ return self->parent == NULL;
+}
+
+/**
+ * ide_tree_node_is_first:
+ * @self: a #IdeTreeNode
+ *
+ * Checks if @self is the first sibling.
+ *
+ * Returns: %TRUE if @self is the first sibling
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_tree_node_is_first (IdeTreeNode *self)
+{
+ g_return_val_if_fail (IDE_IS_TREE_NODE (self), FALSE);
+
+ return self->link.prev == NULL;
+}
+
+/**
+ * ide_tree_node_is_last:
+ * @self: a #IdeTreeNode
+ *
+ * Checks if @self is the last sibling.
+ *
+ * Returns: %TRUE if @self is the last sibling
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_tree_node_is_last (IdeTreeNode *self)
+{
+ g_return_val_if_fail (IDE_IS_TREE_NODE (self), FALSE);
+
+ return self->link.next == NULL;
+}
+
+static void
+ide_tree_node_dump_internal (IdeTreeNode *self,
+ gint depth)
+{
+ g_autofree gchar *space = g_strnfill (depth * 2, ' ');
+
+ g_print ("%s%s\n", space, ide_tree_node_get_display_name (self));
+
+ g_assert (self->children.length == 0 || self->children.head);
+ g_assert (self->children.length == 0 || self->children.tail);
+ g_assert (self->children.length > 0 || !self->children.head);
+ g_assert (self->children.length > 0 || !self->children.tail);
+
+ for (const GList *iter = self->children.head; iter; iter = iter->next)
+ ide_tree_node_dump_internal (iter->data, depth + 1);
+}
+
+void
+_ide_tree_node_dump (IdeTreeNode *self)
+{
+ ide_tree_node_dump_internal (self, 0);
+}
+
+gboolean
+_ide_tree_node_get_loading (IdeTreeNode *self,
+ gint64 *started_loading_at)
+{
+ g_assert (IDE_IS_TREE_NODE (self));
+ g_assert (started_loading_at != NULL);
+
+ *started_loading_at = self->started_loading_at;
+
+ return self->is_loading;
+}
+
+void
+_ide_tree_node_set_loading (IdeTreeNode *self,
+ gboolean loading)
+{
+ g_return_if_fail (IDE_IS_TREE_NODE (self));
+
+ self->is_loading = !!loading;
+
+ if (self->is_loading)
+ self->started_loading_at = g_get_monotonic_time ();
+
+ for (const GList *iter = self->children.head; iter; iter = iter->next)
+ {
+ IdeTreeNode *child = iter->data;
+
+ if (child->is_empty)
+ {
+ if (loading)
+ ide_tree_node_set_display_name (child, _("Loading…"));
+ else
+ ide_tree_node_set_display_name (child, _("(Empty)"));
+
+ if (self->children.length > 1)
+ ide_tree_node_remove (self, child);
+
+ break;
+ }
+ }
+}
+
+void
+_ide_tree_node_remove_all (IdeTreeNode *self)
+{
+ const GList *iter;
+
+ g_return_if_fail (IDE_IS_TREE_NODE (self));
+
+ iter = self->children.head;
+
+ while (iter != NULL)
+ {
+ IdeTreeNode *child = iter->data;
+ iter = iter->next;
+ ide_tree_node_remove (self, child);
+ }
+
+ if (ide_tree_node_get_children_possible (self))
+ {
+ g_autoptr(IdeTreeNode) child = g_object_new (IDE_TYPE_TREE_NODE,
+ "display-name", _("(Empty)"),
+ NULL);
+ child->is_empty = TRUE;
+ ide_tree_node_append (self, child);
+ _ide_tree_node_set_needs_build_children (self, TRUE);
+ }
+}
+
+/**
+ * ide_tree_node_get_reset_on_collapse:
+ * @self: a #IdeTreeNode
+ *
+ * Checks if the node should have all children removed when collapsed.
+ *
+ * Returns: %TRUE if children are removed on collapse
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_tree_node_get_reset_on_collapse (IdeTreeNode *self)
+{
+ g_return_val_if_fail (IDE_IS_TREE_NODE (self), FALSE);
+
+ return self->reset_on_collapse;
+}
+
+/**
+ * ide_tree_node_set_reset_on_collapse:
+ * @self: a #IdeTreeNode
+ * @reset_on_collapse: if the children should be removed on collapse
+ *
+ * If %TRUE, then children will be removed when the row is collapsed.
+ *
+ * Since: 3.32
+ */
+void
+ide_tree_node_set_reset_on_collapse (IdeTreeNode *self,
+ gboolean reset_on_collapse)
+{
+ g_return_if_fail (IDE_IS_TREE_NODE (self));
+
+ reset_on_collapse = !!reset_on_collapse;
+
+ if (reset_on_collapse != self->reset_on_collapse)
+ {
+ self->reset_on_collapse = reset_on_collapse;
+ g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_RESET_ON_COLLAPSE]);
+ }
+}
+
+/**
+ * ide_tree_node_get_path:
+ * @self: a #IdeTreeNode
+ *
+ * Gets the path for the tree node.
+ *
+ * Returns: (transfer full) (nullable): a path or %NULL
+ *
+ * Since: 3.32
+ */
+GtkTreePath *
+ide_tree_node_get_path (IdeTreeNode *self)
+{
+ IdeTreeModel *model;
+
+ g_return_val_if_fail (IDE_IS_TREE_NODE (self), NULL);
+
+ if ((model = ide_tree_node_get_model (self)))
+ return ide_tree_model_get_path_for_node (model, self);
+
+ return NULL;
+}
+
+static void
+ide_tree_node_get_area (IdeTreeNode *node,
+ IdeTree *tree,
+ GdkRectangle *area)
+{
+ GtkTreeViewColumn *column;
+ g_autoptr(GtkTreePath) path = NULL;
+
+ g_assert (IDE_IS_TREE_NODE (node));
+ g_assert (IDE_IS_TREE (tree));
+ g_assert (area != NULL);
+
+ path = ide_tree_node_get_path (node);
+ column = gtk_tree_view_get_column (GTK_TREE_VIEW (tree), 0);
+ gtk_tree_view_get_cell_area (GTK_TREE_VIEW (tree), path, column, area);
+}
+
+typedef struct
+{
+ IdeTreeNode *self;
+ IdeTree *tree;
+ GtkPopover *popover;
+} PopupRequest;
+
+static gboolean
+ide_tree_node_show_popover_timeout_cb (gpointer data)
+{
+ PopupRequest *popreq = data;
+ GdkRectangle rect;
+ GtkAllocation alloc;
+
+ g_assert (popreq);
+ g_assert (IDE_IS_TREE_NODE (popreq->self));
+ g_assert (GTK_IS_POPOVER (popreq->popover));
+
+ ide_tree_node_get_area (popreq->self, popreq->tree, &rect);
+ gtk_widget_get_allocation (GTK_WIDGET (popreq->tree), &alloc);
+
+ if ((rect.x + rect.width) > (alloc.x + alloc.width))
+ rect.width = (alloc.x + alloc.width) - rect.x;
+
+ /* FIXME: Wouldn't this be better placed in a theme? */
+ switch (gtk_popover_get_position (popreq->popover))
+ {
+ case GTK_POS_BOTTOM:
+ case GTK_POS_TOP:
+ rect.y += 3;
+ rect.height -= 6;
+ break;
+ case GTK_POS_RIGHT:
+ case GTK_POS_LEFT:
+ rect.x += 3;
+ rect.width -= 6;
+ break;
+
+ default:
+ break;
+ }
+
+ gtk_popover_set_relative_to (popreq->popover, GTK_WIDGET (popreq->tree));
+ gtk_popover_set_pointing_to (popreq->popover, &rect);
+ gtk_popover_popup (popreq->popover);
+
+ g_clear_object (&popreq->self);
+ g_clear_object (&popreq->popover);
+ g_slice_free (PopupRequest, popreq);
+
+ return G_SOURCE_REMOVE;
+}
+
+void
+_ide_tree_node_show_popover (IdeTreeNode *self,
+ IdeTree *tree,
+ GtkPopover *popover)
+{
+ GdkRectangle cell_area;
+ GdkRectangle visible_rect;
+ PopupRequest *popreq;
+
+ g_return_if_fail (IDE_IS_TREE_NODE (self));
+ g_return_if_fail (IDE_IS_TREE (tree));
+ g_return_if_fail (GTK_IS_POPOVER (popover));
+
+ gtk_tree_view_get_visible_rect (GTK_TREE_VIEW (tree), &visible_rect);
+ ide_tree_node_get_area (self, tree, &cell_area);
+ gtk_tree_view_convert_bin_window_to_tree_coords (GTK_TREE_VIEW (tree),
+ cell_area.x,
+ cell_area.y,
+ &cell_area.x,
+ &cell_area.y);
+
+ popreq = g_slice_new0 (PopupRequest);
+ popreq->self = g_object_ref (self);
+ popreq->tree = g_object_ref (tree);
+ popreq->popover = g_object_ref (popover);
+
+ /*
+ * If the node is not on screen, we need to animate until we get there.
+ */
+ if ((cell_area.y < visible_rect.y) ||
+ ((cell_area.y + cell_area.height) >
+ (visible_rect.y + visible_rect.height)))
+ {
+ GtkTreePath *path;
+
+ path = ide_tree_node_get_path (self);
+ gtk_tree_view_scroll_to_cell (GTK_TREE_VIEW (tree), path, NULL, FALSE, 0, 0);
+ g_clear_pointer (&path, gtk_tree_path_free);
+
+ /*
+ * FIXME: Time period comes from gtk animation duration.
+ * Not curently available in pubic API.
+ * We need to be greater than the max timeout it
+ * could take to move, since we must have it
+ * on screen by then.
+ *
+ * One alternative might be to check the result
+ * and if we are still not on screen, then just
+ * pin it to a row-height from the top or bottom.
+ */
+ g_timeout_add (300,
+ ide_tree_node_show_popover_timeout_cb,
+ popreq);
+
+ return;
+ }
+
+ ide_tree_node_show_popover_timeout_cb (g_steal_pointer (&popreq));
+}
+
+const gchar *
+ide_tree_node_get_tag (IdeTreeNode *self)
+{
+ g_return_val_if_fail (IDE_IS_TREE_NODE (self), NULL);
+
+ return self->tag;
+}
+
+void
+ide_tree_node_set_tag (IdeTreeNode *self,
+ const gchar *tag)
+{
+ g_return_if_fail (IDE_IS_TREE_NODE (self));
+
+ if (!ide_str_equal0 (self->tag, tag))
+ {
+ g_free (self->tag);
+ self->tag = g_strdup (tag);
+ }
+}
+
+gboolean
+ide_tree_node_is_tag (IdeTreeNode *self,
+ const gchar *tag)
+{
+ g_return_val_if_fail (IDE_IS_TREE_NODE (self), FALSE);
+
+ return tag && ide_str_equal0 (self->tag, tag);
+}
+
+void
+ide_tree_node_add_emblem (IdeTreeNode *self,
+ GEmblem *emblem)
+{
+ g_return_if_fail (IDE_IS_MAIN_THREAD ());
+ g_return_if_fail (IDE_IS_TREE_NODE (self));
+
+ self->emblems = g_list_append (self->emblems, g_object_ref (emblem));
+}
+
+GIcon *
+_ide_tree_node_apply_emblems (IdeTreeNode *self,
+ GIcon *base)
+{
+ g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+ g_return_val_if_fail (IDE_IS_TREE_NODE (self), NULL);
+
+ if (self->emblems != NULL)
+ {
+ g_autoptr(GIcon) emblemed = g_emblemed_icon_new (base, NULL);
+
+ for (const GList *iter = self->emblems; iter; iter = iter->next)
+ g_emblemed_icon_add_emblem (G_EMBLEMED_ICON (emblemed), iter->data);
+
+ return G_ICON (g_steal_pointer (&emblemed));
+ }
+
+ return g_object_ref (base);
+}
+
+const GdkRGBA *
+ide_tree_node_get_foreground_rgba (IdeTreeNode *self)
+{
+ g_return_val_if_fail (IDE_IS_TREE_NODE (self), NULL);
+
+ return self->foreground_set ? &self->foreground : NULL;
+}
+
+void
+ide_tree_node_set_foreground_rgba (IdeTreeNode *self,
+ const GdkRGBA *foreground_rgba)
+{
+ g_return_if_fail (IDE_IS_MAIN_THREAD ());
+ g_return_if_fail (IDE_IS_TREE_NODE (self));
+
+ self->foreground_set = !!foreground_rgba;
+
+ if (foreground_rgba)
+ self->foreground = *foreground_rgba;
+
+ ide_tree_node_emit_changed (self);
+}
+
+const GdkRGBA *
+ide_tree_node_get_background_rgba (IdeTreeNode *self)
+{
+ g_return_val_if_fail (IDE_IS_TREE_NODE (self), NULL);
+
+ return self->background_set ? &self->background : NULL;
+}
+
+void
+ide_tree_node_set_background_rgba (IdeTreeNode *self,
+ const GdkRGBA *background_rgba)
+{
+ g_return_if_fail (IDE_IS_MAIN_THREAD ());
+ g_return_if_fail (IDE_IS_TREE_NODE (self));
+
+ self->background_set = !!background_rgba;
+
+ if (background_rgba)
+ self->background = *background_rgba;
+
+ ide_tree_node_emit_changed (self);
+}
+
+void
+_ide_tree_node_apply_colors (IdeTreeNode *self,
+ GtkCellRenderer *cell)
+{
+ PangoAttrList *attrs = NULL;
+
+ g_return_if_fail (IDE_IS_TREE_NODE (self));
+
+ if (self->foreground_set)
+ {
+ if (!attrs)
+ attrs = pango_attr_list_new ();
+ pango_attr_list_insert (attrs,
+ pango_attr_foreground_new (self->foreground.red * 65535,
+ self->foreground.green * 65535,
+ self->foreground.blue * 65535));
+ }
+
+ if (self->background_set)
+ {
+ if (!attrs)
+ attrs = pango_attr_list_new ();
+ pango_attr_list_insert (attrs,
+ pango_attr_background_new (self->background.red * 65535,
+ self->background.green * 65535,
+ self->background.blue * 65535));
+ }
+
+ g_object_set (cell, "attributes", attrs, NULL);
+ g_clear_pointer (&attrs, pango_attr_list_unref);
+}
+
+gboolean
+ide_tree_node_is_selected (IdeTreeNode *self)
+{
+ g_autoptr(GtkTreePath) path = NULL;
+ GtkTreeSelection *selection;
+ IdeTreeModel *model;
+ IdeTree *tree;
+
+ g_return_val_if_fail (IDE_IS_TREE_NODE (self), FALSE);
+
+ if ((path = ide_tree_node_get_path (self)) &&
+ (model = ide_tree_node_get_model (self)) &&
+ (tree = ide_tree_model_get_tree (model)) &&
+ (selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (tree))))
+ return gtk_tree_selection_path_is_selected (selection, path);
+
+ return FALSE;
+}
diff --git a/src/libide/tree/ide-tree-node.h b/src/libide/tree/ide-tree-node.h
new file mode 100644
index 000000000..2a0339dd2
--- /dev/null
+++ b/src/libide/tree/ide-tree-node.h
@@ -0,0 +1,172 @@
+/* ide-tree-node.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 <libide-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_TREE_NODE (ide_tree_node_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeTreeNode, ide_tree_node, IDE, TREE_NODE, GObject)
+
+typedef enum
+{
+ IDE_TREE_NODE_VISIT_BREAK = 0,
+ IDE_TREE_NODE_VISIT_CONTINUE = 0x1,
+ IDE_TREE_NODE_VISIT_CHILDREN = 0x3,
+} IdeTreeNodeVisit;
+
+/**
+ * IdeTreeTraverseFunc:
+ * @node: an #IdeTreeNode
+ * @user_data: closure data provided to ide_tree_node_traverse()
+ *
+ * This function prototype is used to traverse a tree of #IdeTreeNode.
+ *
+ * Returns: #IdeTreeNodeVisit, %IDE_TREE_NODE_VISIT_BREAK to stop traversal.
+ *
+ * Since: 3.32
+ */
+typedef IdeTreeNodeVisit (*IdeTreeTraverseFunc) (IdeTreeNode *node,
+ gpointer user_data);
+
+IDE_AVAILABLE_IN_3_32
+IdeTreeNode *ide_tree_node_new (void);
+IDE_AVAILABLE_IN_3_32
+const gchar *ide_tree_node_get_tag (IdeTreeNode *self);
+IDE_AVAILABLE_IN_3_32
+void ide_tree_node_set_tag (IdeTreeNode *self,
+ const gchar *tag);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_tree_node_is_tag (IdeTreeNode *self,
+ const gchar *tag);
+IDE_AVAILABLE_IN_3_32
+GtkTreePath *ide_tree_node_get_path (IdeTreeNode *self);
+IDE_AVAILABLE_IN_3_32
+const gchar *ide_tree_node_get_display_name (IdeTreeNode *self);
+IDE_AVAILABLE_IN_3_32
+void ide_tree_node_set_display_name (IdeTreeNode *self,
+ const gchar *display_name);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_tree_node_get_is_header (IdeTreeNode *self);
+IDE_AVAILABLE_IN_3_32
+void ide_tree_node_set_is_header (IdeTreeNode *self,
+ gboolean header);
+IDE_AVAILABLE_IN_3_32
+GIcon *ide_tree_node_get_icon (IdeTreeNode *self);
+IDE_AVAILABLE_IN_3_32
+void ide_tree_node_set_icon (IdeTreeNode *self,
+ GIcon *icon);
+IDE_AVAILABLE_IN_3_32
+void ide_tree_node_set_icon_name (IdeTreeNode *self,
+ const gchar *icon_name);
+IDE_AVAILABLE_IN_3_32
+GIcon *ide_tree_node_get_expanded_icon (IdeTreeNode *self);
+IDE_AVAILABLE_IN_3_32
+void ide_tree_node_set_expanded_icon (IdeTreeNode *self,
+ GIcon *expanded_icon);
+IDE_AVAILABLE_IN_3_32
+void ide_tree_node_set_expanded_icon_name (IdeTreeNode *self,
+ const gchar *expanded_icon_name);
+IDE_AVAILABLE_IN_3_32
+gpointer ide_tree_node_get_item (IdeTreeNode *self);
+IDE_AVAILABLE_IN_3_32
+void ide_tree_node_set_item (IdeTreeNode *self,
+ gpointer item);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_tree_node_get_children_possible (IdeTreeNode *self);
+IDE_AVAILABLE_IN_3_32
+void ide_tree_node_set_children_possible (IdeTreeNode *self,
+ gboolean children_possible);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_tree_node_is_empty (IdeTreeNode *self);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_tree_node_has_child (IdeTreeNode *self);
+IDE_AVAILABLE_IN_3_32
+guint ide_tree_node_get_n_children (IdeTreeNode *self);
+IDE_AVAILABLE_IN_3_32
+IdeTreeNode *ide_tree_node_get_next (IdeTreeNode *self);
+IDE_AVAILABLE_IN_3_32
+IdeTreeNode *ide_tree_node_get_previous (IdeTreeNode *self);
+IDE_AVAILABLE_IN_3_32
+guint ide_tree_node_get_index (IdeTreeNode *self);
+IDE_AVAILABLE_IN_3_32
+IdeTreeNode *ide_tree_node_get_nth_child (IdeTreeNode *self,
+ guint index_);
+IDE_AVAILABLE_IN_3_32
+void ide_tree_node_prepend (IdeTreeNode *self,
+ IdeTreeNode *child);
+IDE_AVAILABLE_IN_3_32
+void ide_tree_node_append (IdeTreeNode *self,
+ IdeTreeNode *child);
+IDE_AVAILABLE_IN_3_32
+void ide_tree_node_insert_before (IdeTreeNode *self,
+ IdeTreeNode *child);
+IDE_AVAILABLE_IN_3_32
+void ide_tree_node_insert_after (IdeTreeNode *self,
+ IdeTreeNode *child);
+IDE_AVAILABLE_IN_3_32
+void ide_tree_node_remove (IdeTreeNode *self,
+ IdeTreeNode *child);
+IDE_AVAILABLE_IN_3_32
+IdeTreeNode *ide_tree_node_get_parent (IdeTreeNode *self);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_tree_node_is_root (IdeTreeNode *self);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_tree_node_is_first (IdeTreeNode *self);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_tree_node_is_last (IdeTreeNode *self);
+IDE_AVAILABLE_IN_3_32
+IdeTreeNode *ide_tree_node_get_root (IdeTreeNode *self);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_tree_node_holds (IdeTreeNode *self,
+ GType type);
+IDE_AVAILABLE_IN_3_32
+void ide_tree_node_traverse (IdeTreeNode *self,
+ GTraverseType traverse_type,
+ GTraverseFlags traverse_flags,
+ gint max_depth,
+ IdeTreeTraverseFunc traverse_func,
+ gpointer user_data);
+IDE_AVAILABLE_IN_3_32
+void ide_tree_node_add_emblem (IdeTreeNode *self,
+ GEmblem *emblem);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_tree_node_get_reset_on_collapse (IdeTreeNode *self);
+IDE_AVAILABLE_IN_3_32
+void ide_tree_node_set_reset_on_collapse (IdeTreeNode *self,
+ gboolean reset_on_collapse);
+IDE_AVAILABLE_IN_3_32
+const GdkRGBA *ide_tree_node_get_background_rgba (IdeTreeNode *self);
+IDE_AVAILABLE_IN_3_32
+void ide_tree_node_set_background_rgba (IdeTreeNode *self,
+ const GdkRGBA *background_rgba);
+IDE_AVAILABLE_IN_3_32
+const GdkRGBA *ide_tree_node_get_foreground_rgba (IdeTreeNode *self);
+IDE_AVAILABLE_IN_3_32
+void ide_tree_node_set_foreground_rgba (IdeTreeNode *self,
+ const GdkRGBA *foreground_rgba);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_tree_node_is_selected (IdeTreeNode *self);
+
+G_END_DECLS
diff --git a/src/libide/tree/ide-tree-private.h b/src/libide/tree/ide-tree-private.h
new file mode 100644
index 000000000..77968a522
--- /dev/null
+++ b/src/libide/tree/ide-tree-private.h
@@ -0,0 +1,70 @@
+/* ide-tree-private.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-tree.h"
+#include "ide-tree-node.h"
+#include "ide-tree-model.h"
+
+G_BEGIN_DECLS
+
+GdkDragAction _ide_tree_get_drop_actions (IdeTree *tree);
+IdeTreeModel *_ide_tree_model_new (IdeTree *tree);
+IdeTreeNode *_ide_tree_get_drop_node (IdeTree *tree);
+void _ide_tree_model_release_addins (IdeTreeModel *self);
+void _ide_tree_model_selection_changed (IdeTreeModel *model,
+ GtkTreeIter *selection);
+void _ide_tree_model_build_node (IdeTreeModel *self,
+ IdeTreeNode *node);
+gboolean _ide_tree_model_row_activated (IdeTreeModel *self,
+ IdeTree *tree,
+ GtkTreePath *path);
+void _ide_tree_model_row_expanded (IdeTreeModel *self,
+ IdeTree *tree,
+ GtkTreePath *path);
+void _ide_tree_model_row_collapsed (IdeTreeModel *self,
+ IdeTree *tree,
+ GtkTreePath *path);
+void _ide_tree_model_cell_data_func (IdeTreeModel *self,
+ GtkTreeIter *iter,
+ GtkCellRenderer *cell);
+gboolean _ide_tree_model_contains_node (IdeTreeModel *self,
+ IdeTreeNode *node);
+gboolean _ide_tree_node_get_loading (IdeTreeNode *self,
+ gint64 *loading_started_at);
+void _ide_tree_node_set_loading (IdeTreeNode *self,
+ gboolean loading);
+void _ide_tree_node_dump (IdeTreeNode *self);
+void _ide_tree_node_remove_all (IdeTreeNode *self);
+void _ide_tree_node_set_model (IdeTreeNode *self,
+ IdeTreeModel *model);
+gboolean _ide_tree_node_get_needs_build_children (IdeTreeNode *self);
+void _ide_tree_node_set_needs_build_children (IdeTreeNode *self,
+ gboolean needs_build_children);
+void _ide_tree_node_show_popover (IdeTreeNode *node,
+ IdeTree *tree,
+ GtkPopover *popover);
+GIcon *_ide_tree_node_apply_emblems (IdeTreeNode *self,
+ GIcon *base);
+void _ide_tree_node_apply_colors (IdeTreeNode *self,
+ GtkCellRenderer *cell);
+
+G_END_DECLS
diff --git a/src/libide/tree/ide-tree.c b/src/libide/tree/ide-tree.c
new file mode 100644
index 000000000..24d557da1
--- /dev/null
+++ b/src/libide/tree/ide-tree.c
@@ -0,0 +1,764 @@
+/* ide-tree.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-tree"
+
+#include "config.h"
+
+#include <libpeas/peas.h>
+#include <libide-core.h>
+#include <libide-threading.h>
+
+#include "ide-tree.h"
+#include "ide-tree-model.h"
+#include "ide-tree-node.h"
+#include "ide-tree-private.h"
+
+typedef struct
+{
+ /* This #GCancellable will be automatically cancelled when the widget is
+ * destroyed. That is usefulf or async operations that you want to be
+ * cleaned up as the workspace is destroyed or the widget in question
+ * removed from the widget tree.
+ */
+ GCancellable *cancellable;
+
+ /* To keep rendering of common styles fast, we share these PangoAttrList
+ * so that we need not re-create them many times.
+ */
+ PangoAttrList *dim_label_attributes;
+ PangoAttrList *header_attributes;
+
+ /* The context menu to use for popups */
+ GMenu *context_menu;
+
+ /* Our context menu popover */
+ GtkPopover *popover;
+
+ /* Stashed drop information to propagate on drop */
+ GdkDragAction drop_action;
+ GtkTreePath *drop_path;
+ GtkTreeViewDropPosition drop_pos;
+} IdeTreePrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdeTree, ide_tree, GTK_TYPE_TREE_VIEW)
+
+static IdeTreeModel *
+ide_tree_get_model (IdeTree *self)
+{
+ GtkTreeModel *model;
+
+ g_assert (IDE_IS_TREE (self));
+
+ if (!(model = gtk_tree_view_get_model (GTK_TREE_VIEW (self))) ||
+ !IDE_IS_TREE_MODEL (model))
+ return NULL;
+
+ return IDE_TREE_MODEL (model);
+}
+
+static void
+ide_tree_selection_changed_cb (IdeTree *self,
+ GtkTreeSelection *selection)
+{
+ IdeTreeModel *model;
+ GtkTreeIter iter;
+
+ g_assert (IDE_IS_TREE (self));
+ g_assert (GTK_IS_TREE_SELECTION (selection));
+
+ if (!(model = ide_tree_get_model (self)))
+ return;
+
+ if (gtk_tree_selection_get_selected (selection, NULL, &iter))
+ _ide_tree_model_selection_changed (model, &iter);
+ else
+ _ide_tree_model_selection_changed (model, NULL);
+}
+
+static void
+ide_tree_unselect (IdeTree *self)
+{
+ g_assert (IDE_IS_TREE (self));
+
+ gtk_tree_selection_unselect_all (gtk_tree_view_get_selection (GTK_TREE_VIEW (self)));
+}
+
+static void
+ide_tree_select (IdeTree *self,
+ IdeTreeNode *node)
+{
+ g_autoptr(GtkTreePath) path = NULL;
+ GtkTreeSelection *selection;
+
+ g_assert (IDE_IS_TREE (self));
+ g_assert (IDE_IS_TREE_NODE (node));
+
+ ide_tree_unselect (self);
+
+ selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (self));
+ path = ide_tree_node_get_path (node);
+ gtk_tree_selection_select_path (selection, path);
+}
+
+static void
+text_cell_func (GtkCellLayout *layout,
+ GtkCellRenderer *cell,
+ GtkTreeModel *model,
+ GtkTreeIter *iter,
+ gpointer user_data)
+{
+ IdeTree *self = user_data;
+ IdeTreePrivate *priv = ide_tree_get_instance_private (self);
+ const gchar *display_name = NULL;
+ IdeTreeNode *node;
+
+ g_assert (IDE_IS_TREE (self));
+ g_assert (IDE_IS_TREE_MODEL (model));
+
+ g_object_set (cell,
+ "attributes", NULL,
+ "foreground-set", FALSE,
+ NULL);
+
+ if (!(node = ide_tree_model_get_node (IDE_TREE_MODEL (model), iter)))
+ return;
+
+ _ide_tree_model_cell_data_func (IDE_TREE_MODEL (model), iter, cell);
+
+ /* If we're loading the node, avoid showing the "Loading..." text for 250
+ * milliseconds, so that we don't flash the user with information they'll
+ * never be able to read.
+ */
+ if (ide_tree_node_is_empty (node))
+ {
+ IdeTreeNode *parent = ide_tree_node_get_parent (node);
+ gint64 started_loading_at;
+
+ if (_ide_tree_node_get_loading (parent, &started_loading_at))
+ {
+ gint64 now = g_get_monotonic_time ();
+
+ if ((now - started_loading_at) < (G_USEC_PER_SEC / 4L))
+ goto set_props;
+ }
+ }
+
+ /* Only apply styling if the node isn't selected */
+ if (!ide_tree_node_is_selected (node))
+ {
+ if (ide_tree_node_get_is_header (node))
+ g_object_set (cell, "attributes", priv->header_attributes, NULL);
+ else if (ide_tree_node_is_empty (node))
+ g_object_set (cell, "attributes", priv->dim_label_attributes, NULL);
+ }
+
+ display_name = ide_tree_node_get_display_name (node);
+
+set_props:
+ g_object_set (cell,
+ "text", display_name,
+ NULL);
+}
+
+static void
+pixbuf_cell_func (GtkCellLayout *layout,
+ GtkCellRenderer *cell,
+ GtkTreeModel *model,
+ GtkTreeIter *iter,
+ gpointer user_data)
+{
+ IdeTree *self = user_data;
+ g_autoptr(GtkTreePath) path = NULL;
+ g_autoptr(GIcon) emblems = NULL;
+ IdeTreeNode *node;
+ GIcon *icon;
+
+ g_assert (IDE_IS_TREE (self));
+ g_assert (IDE_IS_TREE_MODEL (model));
+
+ if (!(node = ide_tree_model_get_node (IDE_TREE_MODEL (model), iter)))
+ return;
+
+ path = gtk_tree_model_get_path (model, iter);
+
+ if (gtk_tree_view_row_expanded (GTK_TREE_VIEW (self), path))
+ icon = ide_tree_node_get_expanded_icon (node);
+ else
+ icon = ide_tree_node_get_icon (node);
+
+ if (icon != NULL)
+ emblems = _ide_tree_node_apply_emblems (node, icon);
+
+ g_object_set (cell, "gicon", emblems, NULL);
+}
+
+static void
+ide_tree_expand_cb (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ IdeTreeModel *model = (IdeTreeModel *)object;
+ g_autoptr(GtkTreePath) path = NULL;
+ g_autoptr(IdeTask) task = user_data;
+ IdeTreeNode *node;
+ IdeTree *self;
+
+ g_assert (IDE_IS_MAIN_THREAD ());
+ g_assert (IDE_IS_TREE_MODEL (model));
+ g_assert (G_IS_ASYNC_RESULT (result));
+ g_assert (IDE_IS_TASK (task));
+
+ self = ide_task_get_source_object (task);
+ node = ide_task_get_task_data (task);
+
+ g_assert (IDE_IS_TREE (self));
+ g_assert (IDE_IS_TREE_NODE (node));
+
+ if ((path = ide_tree_model_get_path_for_node (model, node)))
+ {
+ if (ide_tree_model_expand_finish (model, result, NULL))
+ {
+ /* If node was detached during our async operation, we'll get NULL
+ * back for the GtkTreePath (in which case, we'll just ignore).
+ */
+ gtk_tree_view_expand_row (GTK_TREE_VIEW (self), path, FALSE);
+ }
+
+ _ide_tree_model_row_expanded (model, self, path);
+ }
+
+ ide_task_return_boolean (task, TRUE);
+}
+
+static void
+ide_tree_row_activated (GtkTreeView *tree_view,
+ GtkTreePath *path,
+ GtkTreeViewColumn *column)
+{
+ IdeTree *self = (IdeTree *)tree_view;
+ IdeTreeModel *model;
+ IdeTreeNode *node;
+ GtkTreeIter iter;
+
+ g_assert (IDE_IS_TREE (self));
+ g_assert (path != NULL);
+ g_assert (GTK_IS_TREE_VIEW_COLUMN (column));
+
+ /* Get our model, and the node in question. Ignore everything if this
+ * is a synthesized "Empty" node.
+ */
+ if (!(model = ide_tree_get_model (self)) ||
+ !gtk_tree_model_get_iter (GTK_TREE_MODEL (model), &iter, path) ||
+ !(node = ide_tree_model_get_node (model, &iter)) ||
+ ide_tree_node_is_empty (node))
+ return;
+
+ if (!_ide_tree_model_row_activated (model, self, path))
+ {
+ if (gtk_tree_view_row_expanded (tree_view, path))
+ gtk_tree_view_collapse_row (tree_view, path);
+ else
+ gtk_tree_view_expand_row (tree_view, path, FALSE);
+ }
+}
+
+static void
+ide_tree_row_expanded (GtkTreeView *tree_view,
+ GtkTreeIter *iter,
+ GtkTreePath *path)
+{
+ IdeTree *self = (IdeTree *)tree_view;
+ IdeTreePrivate *priv = ide_tree_get_instance_private (self);
+ g_autoptr(IdeTask) task = NULL;
+ IdeTreeModel *model;
+ IdeTreeNode *node;
+
+ g_assert (IDE_IS_TREE (tree_view));
+ g_assert (iter != NULL);
+ g_assert (IDE_IS_TREE_NODE (iter->user_data));
+ g_assert (path != NULL);
+
+ if (!(model = ide_tree_get_model (self)) ||
+ !(node = ide_tree_model_get_node (model, iter)) ||
+ !ide_tree_node_get_children_possible (node))
+ return;
+
+ task = ide_task_new (self, priv->cancellable, NULL, NULL);
+ ide_task_set_source_tag (task, ide_tree_row_expanded);
+ ide_task_set_task_data (task, g_object_ref (node), g_object_unref);
+
+ /* We want to expand the row if we can, but we need to ensure the
+ * children have been built first (it might only have a fake "empty"
+ * node currently). So we request that the model expand the row and
+ * then expand to the path on the callback. The model will do nothing
+ * more than complete the async request if there is nothing to build.
+ */
+ ide_tree_model_expand_async (IDE_TREE_MODEL (model),
+ node,
+ priv->cancellable,
+ ide_tree_expand_cb,
+ g_steal_pointer (&task));
+}
+
+static void
+ide_tree_row_collapsed (GtkTreeView *tree_view,
+ GtkTreeIter *iter,
+ GtkTreePath *path)
+{
+ IdeTree *self = (IdeTree *)tree_view;
+ IdeTreeModel *model;
+ IdeTreeNode *node;
+
+ g_assert (IDE_IS_TREE (self));
+ g_assert (iter != NULL);
+ g_assert (path != NULL);
+
+ if (!(model = ide_tree_get_model (self)) ||
+ !(node = ide_tree_model_get_node (IDE_TREE_MODEL (model), iter)))
+ return;
+
+ /*
+ * If we are collapsing a row that requests to have its children removed
+ * and the dummy node re-inserted, go ahead and do so now.
+ */
+ if (ide_tree_node_get_reset_on_collapse (node))
+ _ide_tree_node_remove_all (node);
+
+ _ide_tree_model_row_collapsed (model, self, path);
+}
+
+static void
+ide_tree_popup (IdeTree *self,
+ IdeTreeNode *node,
+ GdkEventButton *event,
+ gint target_x,
+ gint target_y)
+{
+ IdeTreePrivate *priv = ide_tree_get_instance_private (self);
+ const GdkRectangle area = { target_x, target_y, 0, 0 };
+ GtkTextDirection dir;
+
+ g_assert (IDE_IS_TREE (self));
+ g_assert (IDE_IS_TREE_NODE (node));
+
+ if (priv->context_menu == NULL)
+ return;
+
+ dir = gtk_widget_get_direction (GTK_WIDGET (self));
+
+ if (priv->popover == NULL)
+ {
+ priv->popover = GTK_POPOVER (gtk_popover_new_from_model (GTK_WIDGET (self),
+ G_MENU_MODEL (priv->context_menu)));
+ g_signal_connect (priv->popover,
+ "destroy",
+ G_CALLBACK (gtk_widget_destroyed),
+ &priv->popover);
+ }
+
+ gtk_popover_set_pointing_to (priv->popover, &area);
+ gtk_popover_set_position (priv->popover, dir == GTK_TEXT_DIR_LTR ? GTK_POS_RIGHT : GTK_POS_LEFT);
+
+ ide_tree_show_popover_at_node (self, node, priv->popover);
+}
+
+static gboolean
+ide_tree_button_press_event (GtkWidget *widget,
+ GdkEventButton *event)
+{
+ IdeTree *self = (IdeTree *)widget;
+ IdeTreeModel *model;
+
+ g_assert (IDE_IS_TREE (self));
+ g_assert (event != NULL);
+
+ if ((model = ide_tree_get_model (self)) &&
+ (event->type == GDK_BUTTON_PRESS) &&
+ (event->button == GDK_BUTTON_SECONDARY))
+ {
+ g_autoptr(GtkTreePath) path = NULL;
+ gint cell_y;
+
+ if (!gtk_widget_has_focus (GTK_WIDGET (self)))
+ gtk_widget_grab_focus (GTK_WIDGET (self));
+
+ gtk_tree_view_get_path_at_pos (GTK_TREE_VIEW (self),
+ event->x,
+ event->y,
+ &path,
+ NULL,
+ NULL,
+ &cell_y);
+
+ if (path == NULL)
+ {
+ ide_tree_unselect (self);
+ }
+ else
+ {
+ GtkAllocation alloc;
+ GtkTreeIter iter;
+
+ gtk_widget_get_allocation (GTK_WIDGET (self), &alloc);
+
+ if (gtk_tree_model_get_iter (GTK_TREE_MODEL (model), &iter, path))
+ {
+ IdeTreeNode *node;
+
+ node = ide_tree_model_get_node (IDE_TREE_MODEL (model), &iter);
+ ide_tree_select (self, node);
+ ide_tree_popup (self, node, event, alloc.x + alloc.width, event->y - cell_y);
+ }
+ }
+
+ return GDK_EVENT_STOP;
+ }
+
+ return GTK_WIDGET_CLASS (ide_tree_parent_class)->button_press_event (widget, event);
+}
+
+static gboolean
+ide_tree_drag_motion (GtkWidget *widget,
+ GdkDragContext *context,
+ gint x,
+ gint y,
+ guint time_)
+{
+ IdeTree *self = (IdeTree *)widget;
+ IdeTreePrivate *priv = ide_tree_get_instance_private (self);
+ gboolean ret;
+
+ g_assert (IDE_IS_MAIN_THREAD ());
+ g_assert (IDE_IS_TREE (self));
+ g_assert (context != NULL);
+
+ ret = GTK_WIDGET_CLASS (ide_tree_parent_class)->drag_motion (widget, context, x, y, time_);
+
+ /*
+ * Cache the current drop position so we can use it
+ * later to determine how to drop on a given node.
+ */
+ g_clear_pointer (&priv->drop_path, gtk_tree_path_free);
+ gtk_tree_view_get_drag_dest_row (GTK_TREE_VIEW (self), &priv->drop_path, &priv->drop_pos);
+
+ /* Save the drag action for builders dispatch */
+ priv->drop_action = gdk_drag_context_get_selected_action (context);
+
+ return ret;
+}
+
+static void
+ide_tree_destroy (GtkWidget *widget)
+{
+ IdeTree *self = (IdeTree *)widget;
+ IdeTreePrivate *priv = ide_tree_get_instance_private (self);
+ IdeTreeModel *model;
+
+ g_assert (IDE_IS_MAIN_THREAD ());
+
+ if ((model = ide_tree_get_model (self)))
+ _ide_tree_model_release_addins (model);
+
+ if (priv->popover != NULL)
+ gtk_widget_destroy (GTK_WIDGET (priv->popover));
+
+ gtk_tree_view_set_model (GTK_TREE_VIEW (self), NULL);
+
+ g_cancellable_cancel (priv->cancellable);
+ g_clear_object (&priv->cancellable);
+
+ g_clear_object (&priv->context_menu);
+
+ g_clear_pointer (&priv->dim_label_attributes, pango_attr_list_unref);
+ g_clear_pointer (&priv->header_attributes, pango_attr_list_unref);
+ g_clear_pointer (&priv->drop_path, gtk_tree_path_free);
+
+ GTK_WIDGET_CLASS (ide_tree_parent_class)->destroy (widget);
+}
+
+static void
+ide_tree_class_init (IdeTreeClass *klass)
+{
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ GtkTreeViewClass *tree_view_class = GTK_TREE_VIEW_CLASS (klass);
+
+ widget_class->destroy = ide_tree_destroy;
+ widget_class->button_press_event = ide_tree_button_press_event;
+ widget_class->drag_motion = ide_tree_drag_motion;
+
+ tree_view_class->row_activated = ide_tree_row_activated;
+ tree_view_class->row_expanded = ide_tree_row_expanded;
+ tree_view_class->row_collapsed = ide_tree_row_collapsed;
+}
+
+static void
+ide_tree_init (IdeTree *self)
+{
+ IdeTreePrivate *priv = ide_tree_get_instance_private (self);
+ GtkCellRenderer *cell;
+ GtkTreeViewColumn *column;
+
+ priv->cancellable = g_cancellable_new ();
+
+ g_signal_connect_object (gtk_tree_view_get_selection (GTK_TREE_VIEW (self)),
+ "changed",
+ G_CALLBACK (ide_tree_selection_changed_cb),
+ self,
+ G_CONNECT_SWAPPED);
+
+ gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (self), FALSE);
+ gtk_tree_view_set_activate_on_single_click (GTK_TREE_VIEW (self), TRUE);
+
+ column = gtk_tree_view_column_new ();
+ cell = g_object_new (GTK_TYPE_CELL_RENDERER_PIXBUF,
+ "xpad", 6,
+ NULL);
+ gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (column), cell, FALSE);
+ gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (column), cell, pixbuf_cell_func, self, NULL);
+
+ cell = gtk_cell_renderer_text_new ();
+ gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (column), cell, TRUE);
+ gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (column), cell, text_cell_func, self, NULL);
+
+ gtk_tree_view_append_column (GTK_TREE_VIEW (self), column);
+
+ priv->dim_label_attributes = pango_attr_list_new ();
+ pango_attr_list_insert (priv->dim_label_attributes,
+ pango_attr_foreground_alpha_new (65535 * 0.55));
+
+ priv->header_attributes = pango_attr_list_new ();
+ pango_attr_list_insert (priv->header_attributes,
+ pango_attr_weight_new (PANGO_WEIGHT_BOLD));
+}
+
+GtkWidget *
+ide_tree_new (void)
+{
+ return g_object_new (IDE_TYPE_TREE, NULL);
+}
+
+void
+ide_tree_set_context_menu (IdeTree *self,
+ GMenu *menu)
+{
+ IdeTreePrivate *priv = ide_tree_get_instance_private (self);
+
+ g_return_if_fail (IDE_IS_TREE (self));
+ g_return_if_fail (!menu || G_IS_MENU (menu));
+
+ if (g_set_object (&priv->context_menu, menu))
+ {
+ if (priv->popover != NULL)
+ gtk_widget_destroy (GTK_WIDGET (priv->popover));
+ }
+
+ g_return_if_fail (priv->popover == NULL);
+}
+
+void
+ide_tree_show_popover_at_node (IdeTree *self,
+ IdeTreeNode *node,
+ GtkPopover *popover)
+{
+ g_return_if_fail (IDE_IS_TREE (self));
+ g_return_if_fail (IDE_IS_TREE_NODE (node));
+ g_return_if_fail (GTK_IS_POPOVER (popover));
+
+ _ide_tree_node_show_popover (node, self, popover);
+}
+
+/**
+ * ide_tree_get_selected_node:
+ * @self: a #IdeTree
+ *
+ * Gets the currently selected node, or %NULL
+ *
+ * Returns: (transfer none) (nullable): an #IdeTreeNode or %NULL
+ *
+ * Since: 3.32
+ */
+IdeTreeNode *
+ide_tree_get_selected_node (IdeTree *self)
+{
+ GtkTreeSelection *selection;
+ GtkTreeModel *model;
+ GtkTreeIter iter;
+
+ g_return_val_if_fail (IDE_IS_TREE (self), NULL);
+
+ selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (self));
+
+ if (gtk_tree_selection_get_selected (selection, &model, &iter) && IDE_IS_TREE_MODEL (model))
+ return ide_tree_model_get_node (IDE_TREE_MODEL (model), &iter);
+
+ return NULL;
+}
+
+void
+ide_tree_select_node (IdeTree *self,
+ IdeTreeNode *node)
+{
+ g_return_if_fail (IDE_IS_TREE (self));
+ g_return_if_fail (!node || IDE_IS_TREE_NODE (node));
+
+ if (node == NULL)
+ ide_tree_unselect (self);
+ else
+ ide_tree_select (self, node);
+}
+
+static void
+ide_tree_expand_node_cb (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ IdeTreeModel *model = (IdeTreeModel *)object;
+ g_autoptr(IdeTask) task = user_data;
+
+ g_assert (IDE_IS_TREE_MODEL (model));
+ g_assert (G_IS_ASYNC_RESULT (result));
+ g_assert (IDE_IS_TASK (task));
+
+ if (ide_tree_model_expand_finish (model, result, NULL))
+ {
+ g_autoptr(GtkTreePath) path = NULL;
+ IdeTreeNode *node;
+ IdeTree *self;
+
+ self = ide_task_get_source_object (task);
+ node = ide_task_get_task_data (task);
+
+ g_assert (IDE_IS_TREE (self));
+ g_assert (IDE_IS_TREE_NODE (node));
+
+ if ((path = ide_tree_node_get_path (node)))
+ gtk_tree_view_expand_row (GTK_TREE_VIEW (self), path, FALSE);
+ }
+
+ ide_task_return_boolean (task, TRUE);
+}
+
+void
+ide_tree_expand_node (IdeTree *self,
+ IdeTreeNode *node)
+{
+ g_autoptr(IdeTask) task = NULL;
+ IdeTreeModel *model;
+
+ g_return_if_fail (IDE_IS_TREE (self));
+
+ if (!(model = ide_tree_get_model (self)))
+ return;
+
+ task = ide_task_new (self, NULL, NULL, NULL);
+ ide_task_set_source_tag (task, ide_tree_expand_node);
+ ide_task_set_task_data (task, g_object_ref (node), g_object_unref);
+
+ ide_tree_model_expand_async (model,
+ node,
+ NULL,
+ ide_tree_expand_node_cb,
+ g_steal_pointer (&task));
+}
+
+gboolean
+ide_tree_node_expanded (IdeTree *self,
+ IdeTreeNode *node)
+{
+ g_autoptr(GtkTreePath) path = NULL;
+
+ g_return_val_if_fail (IDE_IS_TREE (self), FALSE);
+ g_return_val_if_fail (!node || IDE_IS_TREE_NODE (node), FALSE);
+
+ if (node == NULL)
+ return FALSE;
+
+ if (!(path = ide_tree_node_get_path (node)))
+ return FALSE;
+
+ return gtk_tree_view_row_expanded (GTK_TREE_VIEW (self), path);
+}
+
+void
+ide_tree_collapse_node (IdeTree *self,
+ IdeTreeNode *node)
+{
+ IdeTreeModel *model;
+ g_autoptr(GtkTreePath) path = NULL;
+
+ g_return_if_fail (IDE_IS_TREE (self));
+
+ if (!(model = ide_tree_get_model (self)))
+ return;
+
+ if ((path = ide_tree_node_get_path (node)))
+ gtk_tree_view_collapse_row (GTK_TREE_VIEW (self), path);
+}
+
+GdkDragAction
+_ide_tree_get_drop_actions (IdeTree *self)
+{
+ IdeTreePrivate *priv = ide_tree_get_instance_private (self);
+
+ g_return_val_if_fail (IDE_IS_TREE (self), 0);
+
+ return priv->drop_action;
+}
+
+IdeTreeNode *
+_ide_tree_get_drop_node (IdeTree *self)
+{
+ IdeTreePrivate *priv = ide_tree_get_instance_private (self);
+ g_autoptr(GtkTreePath) copy = NULL;
+ GtkTreeModel *model;
+ GtkTreeIter iter;
+
+ g_return_val_if_fail (IDE_IS_TREE (self), NULL);
+
+ if (priv->drop_path == NULL)
+ return NULL;
+
+ copy = gtk_tree_path_copy (priv->drop_path);
+
+ if (priv->drop_pos == GTK_TREE_VIEW_DROP_BEFORE ||
+ priv->drop_pos == GTK_TREE_VIEW_DROP_AFTER)
+ {
+ if (gtk_tree_path_get_depth (copy) > 1)
+ gtk_tree_path_up (copy);
+ }
+
+ model = gtk_tree_view_get_model (GTK_TREE_VIEW (self));
+
+ if (gtk_tree_model_get_iter (model, &iter, copy))
+ {
+ IdeTreeNode *node = iter.user_data;
+
+ if (IDE_IS_TREE_NODE (node))
+ {
+ if (ide_tree_node_is_empty (node))
+ node = ide_tree_node_get_parent (node);
+ }
+
+ return node;
+ }
+
+ return NULL;
+}
diff --git a/src/libide/tree/ide-tree.h b/src/libide/tree/ide-tree.h
new file mode 100644
index 000000000..0b4c16b6e
--- /dev/null
+++ b/src/libide/tree/ide-tree.h
@@ -0,0 +1,67 @@
+/* ide-tree.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 <gtk/gtk.h>
+#include <libide-core.h>
+
+#include "ide-tree-node.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_TREE (ide_tree_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeTree, ide_tree, IDE, TREE, GtkTreeView)
+
+struct _IdeTreeClass
+{
+ GtkTreeViewClass parent_type;
+
+ /*< private >*/
+ gpointer _reserved[16];
+};
+
+IDE_AVAILABLE_IN_3_32
+GtkWidget *ide_tree_new (void);
+IDE_AVAILABLE_IN_3_32
+void ide_tree_set_context_menu (IdeTree *self,
+ GMenu *menu);
+IDE_AVAILABLE_IN_3_32
+void ide_tree_show_popover_at_node (IdeTree *self,
+ IdeTreeNode *node,
+ GtkPopover *popover);
+IDE_AVAILABLE_IN_3_32
+IdeTreeNode *ide_tree_get_selected_node (IdeTree *self);
+IDE_AVAILABLE_IN_3_32
+void ide_tree_select_node (IdeTree *self,
+ IdeTreeNode *node);
+IDE_AVAILABLE_IN_3_32
+void ide_tree_expand_node (IdeTree *self,
+ IdeTreeNode *node);
+IDE_AVAILABLE_IN_3_32
+void ide_tree_collapse_node (IdeTree *self,
+ IdeTreeNode *node);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_tree_node_expanded (IdeTree *self,
+ IdeTreeNode *node);
+
+G_END_DECLS
diff --git a/src/libide/tree/libide-tree.h b/src/libide/tree/libide-tree.h
new file mode 100644
index 000000000..515828d11
--- /dev/null
+++ b/src/libide/tree/libide-tree.h
@@ -0,0 +1,36 @@
+/* libide-tree.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 <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TREE_INSIDE
+
+#include "ide-tree.h"
+#include "ide-tree-addin.h"
+#include "ide-tree-model.h"
+#include "ide-tree-node.h"
+
+#undef IDE_TREE_INSIDE
+
+G_END_DECLS
diff --git a/src/libide/tree/meson.build b/src/libide/tree/meson.build
new file mode 100644
index 000000000..8e47f196d
--- /dev/null
+++ b/src/libide/tree/meson.build
@@ -0,0 +1,62 @@
+libide_tree_header_subdir = join_paths(libide_header_subdir, 'tree')
+libide_include_directories += include_directories('.')
+
+#
+# Public API Headers
+#
+
+libide_tree_public_headers = [
+ 'ide-tree.h',
+ 'ide-tree-addin.h',
+ 'ide-tree-model.h',
+ 'ide-tree-node.h',
+ 'libide-tree.h',
+]
+
+install_headers(libide_tree_public_headers, subdir: libide_tree_header_subdir)
+
+#
+# Sources
+#
+
+libide_tree_public_sources = [
+ 'ide-tree.c',
+ 'ide-tree-addin.c',
+ 'ide-tree-model.c',
+ 'ide-tree-node.c',
+]
+
+libide_tree_sources = libide_tree_public_sources
+
+#
+# Dependencies
+#
+
+libide_tree_deps = [
+ libgtk_dep,
+ libpeas_dep,
+
+ libide_core_dep,
+ libide_plugins_dep,
+ libide_threading_dep,
+]
+
+#
+# Library Definitions
+#
+
+libide_tree = static_library('ide-tree-' + libide_api_version, libide_tree_sources,
+ dependencies: libide_tree_deps,
+ c_args: libide_args + release_args + ['-DIDE_TREE_COMPILATION'],
+)
+
+libide_tree_dep = declare_dependency(
+ dependencies: libide_tree_deps,
+ link_whole: libide_tree,
+ include_directories: include_directories('.'),
+)
+
+gnome_builder_public_sources += files(libide_tree_public_sources)
+gnome_builder_public_headers += files(libide_tree_public_headers)
+gnome_builder_include_subdirs += libide_tree_header_subdir
+gnome_builder_gir_extra_args += ['--c-include=libide-tree.h', '-DIDE_TREE_COMPILATION']
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]