1
1

Compare commits

...

15 Commits

Author SHA1 Message Date
ca7079a44e Merge branch 'master' into outliner-cpp-refactor 2020-11-11 17:35:26 +01:00
fdd9cb713e Merge branch 'master' into outliner-cpp-refactor 2020-11-09 13:56:13 +01:00
8c4f121492 Cleanup: Split header for Outliner tree building into C and C++ headers
It's odd to include a C++ header (".hh") in C code, we should avoid that. All
of the Outliner code should be moved to C++, I don't expect this C header to
stay for long.
2020-11-09 13:54:30 +01:00
c796364f05 Cleanup: Rename Outliner "tree-view" types to "tree-display" & update comments
"View" leads to weird names like `TreeViewViewLayer` and after all they are
specific to what we call a "display mode", so "display" is more appropriate.

Also add, update and correct comments.
2020-11-09 13:37:28 +01:00
4a572f06e9 Cleanup: Follow C++ code style for new Outliner building code
* Use C++17 nested namespaces.
* Use `_` suffix rather than prefix for private member variables.

Also: Simplify code visually in `tree_view.cc` with `using namespace`.
2020-11-09 12:21:51 +01:00
fa91f90287 Cleanup: General cleanup of Outliner Blender File display mode building
* Turn functions into member functions (makes API for a type more obvious &
  local, allows implicitly sharing data through member variables, enables order
  independend definition of functions, allows more natural language for
  function names because of the obvious context).
* Prefer references over pointers for passing by reference (makes clear that
  NULL is not a valid value and that the current scope is not the owner).
* Reduce indentation levels, use `continue` in loops to ensure preconditions
  are met.
* Add asserts for sanity checks.
2020-11-09 00:37:41 +01:00
2d60f64960 UI Code Quality: Convert Outliner Blender File mode to new tree buiding design
See https://developer.blender.org/D9499.

Also:
* Add `space_outliner/tree/common.cc` for functions shared between display
  modes.
* Had to add a cast to `ListBaseWrapper` to make it work with ID lists.
* Cleanup: Remove internal `Tree` alias for `ListBase`. That was more confusing
  than helpful.
2020-11-07 22:46:51 +01:00
fc6338d863 Cleanup: Put Outliner C++ namespace into blender::ed namespace, add comments
Also remove unnecessary forward declaration.
2020-11-07 14:23:06 +01:00
d962ca7387 Fix possible null-pointer dereference in new Outliner tree building code 2020-11-07 01:24:18 +01:00
e1a361d287 Cleanup: Remove redundant parameter from new Outliner tree building code 2020-11-07 01:21:47 +01:00
8b787089b3 Merge branch 'master' into outliner-cpp-refactor 2020-11-07 01:09:59 +01:00
126b6f8cc7 Cleanup: Comments & style improvements for new Outliner C++ code
* Add comments to explain the design ideas better.
* Follow code style guide for class layout.
* Avoid uninitialized value after construction (general good practice).
2020-11-07 01:04:17 +01:00
86fe1ec968 UI Code Quality: Use C++ data-structures for Outliner object hierarchy building
* Use `blender::Map` over `GHash`
* Use `blender::Vector` over allocated `ListBase *`

Benefits:
* Significantly reduces the amount of heap allocations in large trees (e.g.
  from O(n) to O(log(n)), where n is number of objects).
* Higher type safety (no `void *`, virtually no casts).
* More optimized (e.g. small buffer optimization).
* More practicable, const-correct APIs with well-defined exception behavior.

Code generally becomes more readable (less lines of code, less boilerplate,
more logic-focused APIs because of greater language flexibility).
2020-11-07 00:15:09 +01:00
6f87489536 UI Code Quality: General refactor of Outliner View Layer display mode creation
* Turn functions into member functions (makes API for a type more obvious &
  local, allows implicitly sharing data through member variables, enables order
  independend definition of functions, allows more natural language for
  function names because of the obvious context).
* Move important variables to classes rather than passing around all the time
  (shorter, more task-focused code, localizes important data names).
* Add helper class for adding object children sub-trees (smaller, more focused
  units are easier to reason about, have higher coherence, better testability,
  can manage own resources easily with RAII).
* Use C++ iterators over C-macros (arguably more readable, less macros is
  generally preferred)
* Add doxygen groups (visually emphasizes the coherence of code sections,
  provide place for higher level comments on sections).
* Prefer references over pointers for passing by reference (makes clear that
  NULL is not a valid value and that the current scope is not the owner).
2020-11-06 22:55:36 +01:00
cf94571762 UI Code Quality: Start refactoring Outliner tree creation (using C++)
The Outliner tree creation was very messy and hard to follow. Hardcoded display
type checks are scattered over many places.
This introduces a new abstraction "tree-view" to help constructing and managing
the tree for the different display types (View Layer, Scene, Blender file,
etc.).

Idea is to have an abstract base class to define an interface
(`AbstractTreeView`), and then subclasses with the implementation of each
display type (e.g. `TreeViewViewLayer`, `TreeViewBlenderFile`, etc). The
tree-viewer is kept alive until tree-rebuild as runtime data of the space, so
that further queries based on the display type can be executed (e.g. "does the
view support selection syncing?", "does it support restriction toggle
columns?", etc.).

I may still change the names a bit, not sure yet if "tree-view" is the right
term for this helper.
2020-11-06 20:54:20 +01:00
13 changed files with 844 additions and 378 deletions

View File

@@ -1640,6 +1640,7 @@ static void direct_link_area(BlendDataReader *reader, ScrArea *area)
}
space_outliner->treehash = NULL;
space_outliner->tree.first = space_outliner->tree.last = NULL;
space_outliner->runtime = NULL;
}
else if (sl->spacetype == SPACE_IMAGE) {
SpaceImage *sima = (SpaceImage *)sl;

View File

@@ -56,7 +56,8 @@ template<typename T> class ListBaseWrapper {
Iterator &operator++()
{
current_ = current_->next;
/* Some types store next/prev using `void *`, so cast is necessary. */
current_ = static_cast<T *>(current_->next);
return *this;
}

View File

@@ -44,8 +44,14 @@ set(SRC
outliner_tree.c
outliner_utils.c
space_outliner.c
tree/common.cc
tree/tree_display.cc
tree/tree_display_libraries.cc
tree/tree_display_view_layer.cc
outliner_intern.h
tree/tree_display.h
tree/tree_display.hh
)
set(LIB

View File

@@ -25,6 +25,10 @@
#include "RNA_types.h"
#ifdef __cplusplus
extern "C" {
#endif
/* internal exports only */
struct ARegion;
@@ -42,6 +46,13 @@ struct bPoseChannel;
struct wmKeyConfig;
struct wmOperatorType;
typedef struct SpaceOutliner_Runtime {
/**
* Internal C++ object to create and manage the tree for a specific display type (View Layers,
* Scenes, Blender File, etc.). */
struct TreeDisplay *tree_display;
} SpaceOutliner_Runtime;
typedef enum TreeElementInsertType {
TE_INSERT_BEFORE,
TE_INSERT_AFTER,
@@ -534,3 +545,7 @@ void outliner_tag_redraw_avoid_rebuild_on_open_change(const struct SpaceOutliner
/* outliner_sync.c ---------------------------------------------- */
void outliner_sync_selection(const struct bContext *C, struct SpaceOutliner *space_outliner);
#ifdef __cplusplus
}
#endif

View File

@@ -85,6 +85,7 @@
#include "UI_interface.h"
#include "outliner_intern.h"
#include "tree/tree_display.h"
#ifdef WIN32
# include "BLI_math_base.h" /* M_PI */
@@ -94,7 +95,6 @@
static TreeElement *outliner_add_collection_recursive(SpaceOutliner *space_outliner,
Collection *collection,
TreeElement *ten);
static void outliner_make_object_parent_hierarchy(ListBase *lb);
static int outliner_exclude_filter_get(const SpaceOutliner *space_outliner);
/* ********************************************************* */
@@ -237,14 +237,6 @@ void outliner_free_tree_element(TreeElement *element, ListBase *parent_subtree)
/* ********************************************************* */
/* Prototype, see functions below */
static TreeElement *outliner_add_element(SpaceOutliner *space_outliner,
ListBase *lb,
void *idv,
TreeElement *parent,
short type,
short index);
/* -------------------------------------------------------- */
bool outliner_requires_rebuild_on_select_or_active_change(const SpaceOutliner *space_outliner)
@@ -920,12 +912,12 @@ static void outliner_add_id_contents(SpaceOutliner *space_outliner,
* \note: If child items are only added to the tree if the item is open, the TSE_ type _must_ be
* added to #outliner_element_needs_rebuild_on_open_change().
*/
static TreeElement *outliner_add_element(SpaceOutliner *space_outliner,
ListBase *lb,
void *idv,
TreeElement *parent,
short type,
short index)
TreeElement *outliner_add_element(SpaceOutliner *space_outliner,
ListBase *lb,
void *idv,
TreeElement *parent,
short type,
short index)
{
TreeElement *te;
TreeStoreElem *tselem;
@@ -1390,110 +1382,6 @@ static void outliner_add_seq_dup(SpaceOutliner *space_outliner,
/* ----------------------------------------------- */
static const char *outliner_idcode_to_plural(short idcode)
{
const char *propname = BKE_idtype_idcode_to_name_plural(idcode);
PropertyRNA *prop = RNA_struct_type_find_property(&RNA_BlendData, propname);
return (prop) ? RNA_property_ui_name(prop) : "UNKNOWN";
}
static bool outliner_library_id_show(Library *lib, ID *id, short filter_id_type)
{
if (id->lib != lib) {
return false;
}
if (filter_id_type == ID_GR) {
/* Don't show child collections of non-scene master collection,
* they are already shown as children. */
Collection *collection = (Collection *)id;
bool has_non_scene_parent = false;
LISTBASE_FOREACH (CollectionParent *, cparent, &collection->parents) {
if (!(cparent->collection->flag & COLLECTION_IS_MASTER)) {
has_non_scene_parent = true;
}
}
if (has_non_scene_parent) {
return false;
}
}
return true;
}
static TreeElement *outliner_add_library_contents(Main *mainvar,
SpaceOutliner *space_outliner,
ListBase *lb,
Library *lib)
{
TreeElement *ten, *tenlib = NULL;
ListBase *lbarray[MAX_LIBARRAY];
int a, tot;
short filter_id_type = (space_outliner->filter & SO_FILTER_ID_TYPE) ?
space_outliner->filter_id_type :
0;
if (filter_id_type) {
lbarray[0] = which_libbase(mainvar, space_outliner->filter_id_type);
tot = 1;
}
else {
tot = set_listbasepointers(mainvar, lbarray);
}
for (a = 0; a < tot; a++) {
if (lbarray[a] && lbarray[a]->first) {
ID *id = lbarray[a]->first;
const bool is_library = (GS(id->name) == ID_LI) && (lib != NULL);
/* check if there's data in current lib */
for (; id; id = id->next) {
if (id->lib == lib) {
break;
}
}
/* We always want to create an entry for libraries, even if/when we have no more IDs from
* them. This invalid state is important to show to user as well.*/
if (id != NULL || is_library) {
if (!tenlib) {
/* Create library tree element on demand, depending if there are any data-blocks. */
if (lib) {
tenlib = outliner_add_element(space_outliner, lb, lib, NULL, 0, 0);
}
else {
tenlib = outliner_add_element(space_outliner, lb, mainvar, NULL, TSE_ID_BASE, 0);
tenlib->name = IFACE_("Current File");
}
}
/* Create data-block list parent element on demand. */
if (id != NULL) {
if (filter_id_type) {
ten = tenlib;
}
else {
ten = outliner_add_element(
space_outliner, &tenlib->subtree, lbarray[a], NULL, TSE_ID_BASE, 0);
ten->directdata = lbarray[a];
ten->name = outliner_idcode_to_plural(GS(id->name));
}
for (id = lbarray[a]->first; id; id = id->next) {
if (outliner_library_id_show(lib, id, filter_id_type)) {
outliner_add_element(space_outliner, &ten->subtree, id, ten, 0, 0);
}
}
}
}
}
}
return tenlib;
}
static void outliner_add_orphaned_datablocks(Main *mainvar, SpaceOutliner *space_outliner)
{
TreeElement *ten;
@@ -1546,82 +1434,6 @@ static void outliner_add_orphaned_datablocks(Main *mainvar, SpaceOutliner *space
}
}
static void outliner_add_layer_collection_objects(SpaceOutliner *space_outliner,
ListBase *tree,
ViewLayer *layer,
LayerCollection *lc,
TreeElement *ten)
{
LISTBASE_FOREACH (CollectionObject *, cob, &lc->collection->gobject) {
Base *base = BKE_view_layer_base_find(layer, cob->ob);
TreeElement *te_object = outliner_add_element(space_outliner, tree, base->object, ten, 0, 0);
te_object->directdata = base;
if (!(base->flag & BASE_VISIBLE_VIEWLAYER)) {
te_object->flag |= TE_DISABLED;
}
}
}
static void outliner_add_layer_collections_recursive(SpaceOutliner *space_outliner,
ListBase *tree,
ViewLayer *layer,
ListBase *layer_collections,
TreeElement *parent_ten,
const bool show_objects)
{
LISTBASE_FOREACH (LayerCollection *, lc, layer_collections) {
const bool exclude = (lc->flag & LAYER_COLLECTION_EXCLUDE) != 0;
TreeElement *ten;
if (exclude && ((space_outliner->show_restrict_flags & SO_RESTRICT_ENABLE) == 0)) {
ten = parent_ten;
}
else {
ID *id = &lc->collection->id;
ten = outliner_add_element(space_outliner, tree, id, parent_ten, TSE_LAYER_COLLECTION, 0);
ten->name = id->name + 2;
ten->directdata = lc;
/* Open by default, except linked collections, which may contain many elements. */
TreeStoreElem *tselem = TREESTORE(ten);
if (!(tselem->used || ID_IS_LINKED(id) || ID_IS_OVERRIDE_LIBRARY(id))) {
tselem->flag &= ~TSE_CLOSED;
}
if (exclude || (lc->runtime_flag & LAYER_COLLECTION_VISIBLE_VIEW_LAYER) == 0) {
ten->flag |= TE_DISABLED;
}
}
outliner_add_layer_collections_recursive(
space_outliner, &ten->subtree, layer, &lc->layer_collections, ten, show_objects);
if (!exclude && show_objects) {
outliner_add_layer_collection_objects(space_outliner, &ten->subtree, layer, lc, ten);
}
}
}
static void outliner_add_view_layer(SpaceOutliner *space_outliner,
ListBase *tree,
TreeElement *parent,
ViewLayer *layer,
const bool show_objects)
{
/* First layer collection is for master collection, don't show it. */
LayerCollection *lc = layer->layer_collections.first;
if (lc == NULL) {
return;
}
outliner_add_layer_collections_recursive(
space_outliner, tree, layer, &lc->layer_collections, parent, show_objects);
if (show_objects) {
outliner_add_layer_collection_objects(space_outliner, tree, layer, lc, parent);
}
}
BLI_INLINE void outliner_add_collection_init(TreeElement *te, Collection *collection)
{
te->name = BKE_collection_ui_name_get(collection);
@@ -1661,7 +1473,7 @@ static TreeElement *outliner_add_collection_recursive(SpaceOutliner *space_outli
/* Hierarchy --------------------------------------------- */
/* make sure elements are correctly nested */
static void outliner_make_object_parent_hierarchy(ListBase *lb)
void outliner_make_object_parent_hierarchy(ListBase *lb)
{
TreeElement *te, *ten, *tep;
TreeStoreElem *tselem;
@@ -1686,103 +1498,6 @@ static void outliner_make_object_parent_hierarchy(ListBase *lb)
}
}
/**
* For all objects in the tree, lookup the parent in this map,
* and move or add tree elements as needed.
*/
static void outliner_make_object_parent_hierarchy_collections(SpaceOutliner *space_outliner,
GHash *object_tree_elements_hash)
{
GHashIterator gh_iter;
GHASH_ITER (gh_iter, object_tree_elements_hash) {
Object *child = BLI_ghashIterator_getKey(&gh_iter);
if (child->parent == NULL) {
continue;
}
ListBase *child_ob_tree_elements = BLI_ghashIterator_getValue(&gh_iter);
ListBase *parent_ob_tree_elements = BLI_ghash_lookup(object_tree_elements_hash, child->parent);
if (parent_ob_tree_elements == NULL) {
continue;
}
LISTBASE_FOREACH (LinkData *, link, parent_ob_tree_elements) {
TreeElement *parent_ob_tree_element = link->data;
TreeElement *parent_ob_collection_tree_element = NULL;
bool found = false;
/* We always want to remove the child from the direct collection its parent is nested under.
* This is particularly important when dealing with multi-level nesting (grandchildren). */
parent_ob_collection_tree_element = parent_ob_tree_element->parent;
while (!ELEM(TREESTORE(parent_ob_collection_tree_element)->type,
TSE_VIEW_COLLECTION_BASE,
TSE_LAYER_COLLECTION)) {
parent_ob_collection_tree_element = parent_ob_collection_tree_element->parent;
}
LISTBASE_FOREACH (LinkData *, link_iter, child_ob_tree_elements) {
TreeElement *child_ob_tree_element = link_iter->data;
if (child_ob_tree_element->parent == parent_ob_collection_tree_element) {
/* Move from the collection subtree into the parent object subtree. */
BLI_remlink(&parent_ob_collection_tree_element->subtree, child_ob_tree_element);
BLI_addtail(&parent_ob_tree_element->subtree, child_ob_tree_element);
child_ob_tree_element->parent = parent_ob_tree_element;
found = true;
break;
}
}
if (!found) {
/* We add the child in the tree even if it is not in the collection.
* We deliberately clear its sub-tree though, to make it less prominent. */
TreeElement *child_ob_tree_element = outliner_add_element(
space_outliner, &parent_ob_tree_element->subtree, child, parent_ob_tree_element, 0, 0);
outliner_free_tree(&child_ob_tree_element->subtree);
child_ob_tree_element->flag |= TE_CHILD_NOT_IN_COLLECTION;
BLI_addtail(child_ob_tree_elements, BLI_genericNodeN(child_ob_tree_element));
}
}
}
}
/**
* Build a map from Object* to a list of TreeElement* matching the object.
*/
static void outliner_object_tree_elements_lookup_create_recursive(GHash *object_tree_elements_hash,
TreeElement *te_parent)
{
LISTBASE_FOREACH (TreeElement *, te, &te_parent->subtree) {
TreeStoreElem *tselem = TREESTORE(te);
if (tselem->type == TSE_LAYER_COLLECTION) {
outliner_object_tree_elements_lookup_create_recursive(object_tree_elements_hash, te);
}
else if (tselem->type == 0 && te->idcode == ID_OB) {
Object *ob = (Object *)tselem->id;
ListBase *tree_elements = BLI_ghash_lookup(object_tree_elements_hash, ob);
if (tree_elements == NULL) {
tree_elements = MEM_callocN(sizeof(ListBase), __func__);
BLI_ghash_insert(object_tree_elements_hash, ob, tree_elements);
}
BLI_addtail(tree_elements, BLI_genericNodeN(te));
outliner_object_tree_elements_lookup_create_recursive(object_tree_elements_hash, te);
}
}
}
static void outliner_object_tree_elements_lookup_free(GHash *object_tree_elements_hash)
{
GHASH_FOREACH_BEGIN (ListBase *, tree_elements, object_tree_elements_hash) {
BLI_freelistN(tree_elements);
MEM_freeN(tree_elements);
}
GHASH_FOREACH_END();
}
/* Sorting ------------------------------------------------------ */
typedef struct tTreeSort {
@@ -2499,59 +2214,23 @@ void outliner_build_tree(Main *mainvar,
outliner_free_tree(&space_outliner->tree);
outliner_storage_cleanup(space_outliner);
outliner_tree_display_destroy(&space_outliner->runtime->tree_display);
space_outliner->runtime->tree_display = outliner_tree_display_create(space_outliner->outlinevis,
space_outliner);
if (space_outliner->runtime->tree_display) {
TreeSourceData source_data = {.bmain = mainvar, .scene = scene, .view_layer = view_layer};
space_outliner->tree = outliner_tree_display_build_tree(space_outliner->runtime->tree_display,
&source_data);
}
if (space_outliner->runtime->tree_display) {
/* Skip if there's a tree-display that's responsible for adding all elements. */
}
/* options */
if (space_outliner->outlinevis == SO_LIBRARIES) {
Library *lib;
/* current file first - mainvar provides tselem with unique pointer - not used */
ten = outliner_add_library_contents(mainvar, space_outliner, &space_outliner->tree, NULL);
if (ten) {
tselem = TREESTORE(ten);
if (!tselem->used) {
tselem->flag &= ~TSE_CLOSED;
}
}
for (lib = mainvar->libraries.first; lib; lib = lib->id.next) {
ten = outliner_add_library_contents(mainvar, space_outliner, &space_outliner->tree, lib);
/* NULL-check matters, due to filtering there may not be a new element. */
if (ten) {
lib->id.newid = (ID *)ten;
}
}
/* make hierarchy */
ten = space_outliner->tree.first;
if (ten != NULL) {
ten = ten->next; /* first one is main */
while (ten) {
TreeElement *nten = ten->next, *par;
tselem = TREESTORE(ten);
lib = (Library *)tselem->id;
if (lib && lib->parent) {
par = (TreeElement *)lib->parent->id.newid;
if (tselem->id->tag & LIB_TAG_INDIRECT) {
/* Only remove from 'first level' if lib is not also directly used. */
BLI_remlink(&space_outliner->tree, ten);
BLI_addtail(&par->subtree, ten);
ten->parent = par;
}
else {
/* Else, make a new copy of the libtree for our parent. */
TreeElement *dupten = outliner_add_library_contents(
mainvar, space_outliner, &par->subtree, lib);
if (dupten) {
dupten->parent = par;
}
}
}
ten = nten;
}
}
/* restore newid pointers */
for (lib = mainvar->libraries.first; lib; lib = lib->id.next) {
lib->id.newid = NULL;
}
else if (space_outliner->outlinevis == SO_LIBRARIES) {
/* Ported to new tree-display, should be built there already. */
BLI_assert(false);
}
else if (space_outliner->outlinevis == SO_SCENES) {
Scene *sce;
@@ -2612,38 +2291,8 @@ void outliner_build_tree(Main *mainvar,
outliner_add_orphaned_datablocks(mainvar, space_outliner);
}
else if (space_outliner->outlinevis == SO_VIEW_LAYER) {
if (space_outliner->filter & SO_FILTER_NO_COLLECTION) {
/* Show objects in the view layer. */
LISTBASE_FOREACH (Base *, base, &view_layer->object_bases) {
TreeElement *te_object = outliner_add_element(
space_outliner, &space_outliner->tree, base->object, NULL, 0, 0);
te_object->directdata = base;
}
if ((space_outliner->filter & SO_FILTER_NO_CHILDREN) == 0) {
outliner_make_object_parent_hierarchy(&space_outliner->tree);
}
}
else {
/* Show collections in the view layer. */
ten = outliner_add_element(
space_outliner, &space_outliner->tree, scene, NULL, TSE_VIEW_COLLECTION_BASE, 0);
ten->name = IFACE_("Scene Collection");
TREESTORE(ten)->flag &= ~TSE_CLOSED;
bool show_objects = !(space_outliner->filter & SO_FILTER_NO_OBJECT);
outliner_add_view_layer(space_outliner, &ten->subtree, ten, view_layer, show_objects);
if ((space_outliner->filter & SO_FILTER_NO_CHILDREN) == 0) {
GHash *object_tree_elements_hash = BLI_ghash_new(
BLI_ghashutil_ptrhash, BLI_ghashutil_ptrcmp, __func__);
outliner_object_tree_elements_lookup_create_recursive(object_tree_elements_hash, ten);
outliner_make_object_parent_hierarchy_collections(space_outliner,
object_tree_elements_hash);
outliner_object_tree_elements_lookup_free(object_tree_elements_hash);
BLI_ghash_free(object_tree_elements_hash, NULL, NULL);
}
}
/* Ported to new tree-display, should be built there already. */
BLI_assert(false);
}
if ((space_outliner->flag & SO_SKIP_SORT_ALPHA) == 0) {

View File

@@ -351,11 +351,21 @@ static void outliner_free(SpaceLink *sl)
if (space_outliner->treehash) {
BKE_outliner_treehash_free(space_outliner->treehash);
}
if (space_outliner->runtime) {
MEM_freeN(space_outliner->runtime);
}
}
/* spacetype; init callback */
static void outliner_init(wmWindowManager *UNUSED(wm), ScrArea *UNUSED(area))
static void outliner_init(wmWindowManager *UNUSED(wm), ScrArea *area)
{
SpaceOutliner *space_outliner = area->spacedata.first;
if (space_outliner->runtime == NULL) {
space_outliner->runtime = MEM_callocN(sizeof(*space_outliner->runtime),
"SpaceOutliner_Runtime");
}
}
static SpaceLink *outliner_duplicate(SpaceLink *sl)
@@ -369,6 +379,10 @@ static SpaceLink *outliner_duplicate(SpaceLink *sl)
space_outliner_new->sync_select_dirty = WM_OUTLINER_SYNC_SELECT_FROM_ALL;
if (space_outliner->runtime) {
space_outliner_new->runtime = MEM_dupallocN(space_outliner->runtime);
}
return (SpaceLink *)space_outliner_new;
}

View File

@@ -0,0 +1,41 @@
/*
* 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 2
* 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, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
/** \file
* \ingroup spoutliner
*
* Functions and helpers shared between tree-display types or other tree related code.
*/
#include "BKE_idtype.h"
#include "RNA_access.h"
#include "tree_display.hh"
/* -------------------------------------------------------------------- */
/** \name ID Helpers.
*
* \{ */
const char *outliner_idcode_to_plural(short idcode)
{
const char *propname = BKE_idtype_idcode_to_name_plural(idcode);
PropertyRNA *prop = RNA_struct_type_find_property(&RNA_BlendData, propname);
return (prop) ? RNA_property_ui_name(prop) : "UNKNOWN";
}
/** \} */

View File

@@ -0,0 +1,60 @@
/*
* 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 2
* 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, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
/** \file
* \ingroup spoutliner
*/
#include "BLI_listbase.h"
#include "DNA_listBase.h"
#include "tree_display.hh"
using namespace blender::ed::outliner;
TreeDisplay *outliner_tree_display_create(eSpaceOutliner_Mode mode, SpaceOutliner *space_outliner)
{
AbstractTreeDisplay *tree_display = nullptr;
switch (mode) {
case SO_SCENES:
break;
case SO_LIBRARIES:
tree_display = new TreeDisplayLibraries(*space_outliner);
break;
case SO_SEQUENCE:
case SO_DATA_API:
case SO_ID_ORPHANS:
break;
case SO_VIEW_LAYER:
tree_display = new TreeDisplayViewLayer(*space_outliner);
break;
}
return reinterpret_cast<TreeDisplay *>(tree_display);
}
void outliner_tree_display_destroy(TreeDisplay **tree_display)
{
delete reinterpret_cast<AbstractTreeDisplay *>(*tree_display);
*tree_display = nullptr;
}
ListBase outliner_tree_display_build_tree(TreeDisplay *tree_display, TreeSourceData *source_data)
{
return reinterpret_cast<AbstractTreeDisplay *>(tree_display)->buildTree(*source_data);
}

View File

@@ -0,0 +1,64 @@
/*
* 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 2
* 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, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
/** \file
* \ingroup spoutliner
*
* C-API for the Tree-Display types.
*/
#pragma once
#include "DNA_space_types.h"
struct ListBase;
#ifdef __cplusplus
extern "C" {
#endif
/** C alias for an #AbstractTreeDisplay handle. */
typedef struct TreeDisplay TreeDisplay;
/**
* \brief The data to build the tree from.
*/
typedef struct TreeSourceData {
struct Main *bmain;
struct Scene *scene;
struct ViewLayer *view_layer;
} TreeSourceData;
TreeDisplay *outliner_tree_display_create(eSpaceOutliner_Mode mode, SpaceOutliner *space_outliner);
void outliner_tree_display_destroy(TreeDisplay **tree_display);
ListBase outliner_tree_display_build_tree(TreeDisplay *tree_display, TreeSourceData *source_data);
/* The following functions are needed to build the tree. They are calls back into C; the way
* elements are created should be refactored and ported to C++ with a new design/API too. */
struct TreeElement *outliner_add_element(SpaceOutliner *space_outliner,
ListBase *lb,
void *idv,
struct TreeElement *parent,
short type,
short index);
void outliner_make_object_parent_hierarchy(ListBase *lb);
const char *outliner_idcode_to_plural(short idcode);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,113 @@
/*
* 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 2
* 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, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
/** \file
* \ingroup spoutliner
*
* \brief Establish and manage Outliner trees for different display modes.
*
* Each Outliner display mode (e.g View Layer, Scenes, Blender File) is implemented as a
* tree-display class with the #AbstractTreeDisplay interface.
*
* Their main responsibility is building the Outliner tree for a display mode. For that, they
* implement the #buildTree() function, which based on Blender data (#TreeSourceData), builds a
* custom tree of whatever data it wants to visualize.
* Further, they can implement display mode dependent queries and general operations using the
* #AbstractTreeDisplay abstraction as common interface.
*
* Outliners keep the current tree-display object alive until the next full tree rebuild to keep
* access to it.
*/
#pragma once
#include "tree_display.h"
struct ListBase;
struct Main;
struct SpaceOutliner;
struct TreeElement;
struct TreeSourceData;
namespace blender::ed::outliner {
/* -------------------------------------------------------------------- */
/* Tree-Display Interface */
/**
* \brief Base Class For Tree-Displays
*
* Abstract base class defining the interface for tree-display variants.
*/
class AbstractTreeDisplay {
public:
AbstractTreeDisplay(SpaceOutliner &space_outliner) : space_outliner_(space_outliner)
{
}
virtual ~AbstractTreeDisplay() = default;
/**
* Build a tree for this display mode with the Blender context data given in \a source_data and
* the view settings in \a space_outliner.
*/
virtual ListBase buildTree(const TreeSourceData &source_data) = 0;
protected:
/** All derived classes will need a handle to this, so storing it in the base for convenience. */
SpaceOutliner &space_outliner_;
};
/* -------------------------------------------------------------------- */
/* View Layer Tree-Display */
/**
* \brief Tree-Display for the View Layer display mode.
*/
class TreeDisplayViewLayer final : public AbstractTreeDisplay {
ViewLayer *view_layer_ = nullptr;
bool show_objects_ = true;
public:
TreeDisplayViewLayer(SpaceOutliner &space_outliner);
ListBase buildTree(const TreeSourceData &source_data) override;
private:
void add_view_layer(ListBase &, TreeElement &);
void add_layer_collections_recursive(ListBase &, ListBase &, TreeElement &);
void add_layer_collection_objects(ListBase &, LayerCollection &, TreeElement &);
void add_layer_collection_objects_children(TreeElement &);
};
/* -------------------------------------------------------------------- */
/* Library Tree-Display */
/**
* \brief Tree-Display for the Libraries display mode.
*/
class TreeDisplayLibraries final : public AbstractTreeDisplay {
public:
TreeDisplayLibraries(SpaceOutliner &space_outliner);
ListBase buildTree(const TreeSourceData &source_data) override;
private:
TreeElement *add_library_contents(Main &, ListBase &, Library *) const;
bool library_id_filter_poll(Library *lib, ID *id) const;
short id_filter_get() const;
};
} // namespace blender::ed::outliner

View File

@@ -0,0 +1,213 @@
/*
* 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 2
* 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, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
/** \file
* \ingroup spoutliner
*/
#include "BLI_listbase.h"
#include "BLI_listbase_wrapper.hh"
#include "BKE_collection.h"
#include "BKE_main.h"
#include "BLT_translation.h"
#include "../outliner_intern.h"
#include "tree_display.hh"
namespace blender::ed::outliner {
/* Convenience/readability. */
template<typename T> using List = ListBaseWrapper<T>;
TreeDisplayLibraries::TreeDisplayLibraries(SpaceOutliner &space_outliner)
: AbstractTreeDisplay(space_outliner)
{
}
ListBase TreeDisplayLibraries::buildTree(const TreeSourceData &source_data)
{
ListBase tree = {nullptr};
{
/* current file first - mainvar provides tselem with unique pointer - not used */
TreeElement *ten = add_library_contents(*source_data.bmain, tree, nullptr);
TreeStoreElem *tselem;
if (ten) {
tselem = TREESTORE(ten);
if (!tselem->used) {
tselem->flag &= ~TSE_CLOSED;
}
}
}
for (ID *id : List<ID>(source_data.bmain->libraries)) {
Library *lib = reinterpret_cast<Library *>(id);
TreeElement *ten = add_library_contents(*source_data.bmain, tree, lib);
/* NULL-check matters, due to filtering there may not be a new element. */
if (ten) {
lib->id.newid = (ID *)ten;
}
}
/* make hierarchy */
for (TreeElement *ten : List<TreeElement>(tree)) {
if (ten == tree.first) {
/* First item is main, skip. */
continue;
}
TreeStoreElem *tselem = TREESTORE(ten);
Library *lib = (Library *)tselem->id;
BLI_assert(!lib || (GS(lib->id.name) == ID_LI));
if (!lib || !lib->parent) {
continue;
}
TreeElement *parent = (TreeElement *)lib->parent->id.newid;
if (tselem->id->tag & LIB_TAG_INDIRECT) {
/* Only remove from 'first level' if lib is not also directly used. */
BLI_remlink(&tree, ten);
BLI_addtail(&parent->subtree, ten);
ten->parent = parent;
}
else {
/* Else, make a new copy of the libtree for our parent. */
TreeElement *dupten = add_library_contents(*source_data.bmain, parent->subtree, lib);
if (dupten) {
dupten->parent = parent;
}
}
}
/* restore newid pointers */
for (ID *library_id : List<ID>(source_data.bmain->libraries)) {
library_id->newid = nullptr;
}
return tree;
}
TreeElement *TreeDisplayLibraries::add_library_contents(Main &mainvar,
ListBase &lb,
Library *lib) const
{
const short filter_id_type = id_filter_get();
ListBase *lbarray[MAX_LIBARRAY];
int tot;
if (filter_id_type) {
lbarray[0] = which_libbase(&mainvar, space_outliner_.filter_id_type);
tot = 1;
}
else {
tot = set_listbasepointers(&mainvar, lbarray);
}
TreeElement *tenlib = nullptr;
for (int a = 0; a < tot; a++) {
if (!lbarray[a] || !lbarray[a]->first) {
continue;
}
ID *id = static_cast<ID *>(lbarray[a]->first);
const bool is_library = (GS(id->name) == ID_LI) && (lib != nullptr);
/* check if there's data in current lib */
for (ID *id_iter : List<ID>(lbarray[a])) {
if (id_iter->lib == lib) {
id = id_iter;
break;
}
}
/* We always want to create an entry for libraries, even if/when we have no more IDs from
* them. This invalid state is important to show to user as well.*/
if (id != nullptr || is_library) {
if (!tenlib) {
/* Create library tree element on demand, depending if there are any data-blocks. */
if (lib) {
tenlib = outliner_add_element(&space_outliner_, &lb, lib, nullptr, 0, 0);
}
else {
tenlib = outliner_add_element(&space_outliner_, &lb, &mainvar, nullptr, TSE_ID_BASE, 0);
tenlib->name = IFACE_("Current File");
}
}
/* Create data-block list parent element on demand. */
if (id != nullptr) {
TreeElement *ten;
if (filter_id_type) {
ten = tenlib;
}
else {
ten = outliner_add_element(
&space_outliner_, &tenlib->subtree, lbarray[a], nullptr, TSE_ID_BASE, 0);
ten->directdata = lbarray[a];
ten->name = outliner_idcode_to_plural(GS(id->name));
}
for (ID *id : List<ID>(lbarray[a])) {
if (library_id_filter_poll(lib, id)) {
outliner_add_element(&space_outliner_, &ten->subtree, id, ten, 0, 0);
}
}
}
}
}
return tenlib;
}
short TreeDisplayLibraries::id_filter_get() const
{
if (space_outliner_.filter & SO_FILTER_ID_TYPE) {
return space_outliner_.filter_id_type;
}
return 0;
}
bool TreeDisplayLibraries::library_id_filter_poll(Library *lib, ID *id) const
{
if (id->lib != lib) {
return false;
}
if (id_filter_get() == ID_GR) {
/* Don't show child collections of non-scene master collection,
* they are already shown as children. */
Collection *collection = (Collection *)id;
bool has_non_scene_parent = false;
for (CollectionParent *cparent : List<CollectionParent>(collection->parents)) {
if (!(cparent->collection->flag & COLLECTION_IS_MASTER)) {
has_non_scene_parent = true;
}
}
if (has_non_scene_parent) {
return false;
}
}
return true;
}
} // namespace blender::ed::outliner

View File

@@ -0,0 +1,284 @@
/*
* 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 2
* 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, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
/** \file
* \ingroup spoutliner
*/
#include <iostream>
#include "DNA_scene_types.h"
#include "BKE_layer.h"
#include "BLI_listbase.h"
#include "BLI_listbase_wrapper.hh"
#include "BLI_map.hh"
#include "BLI_vector.hh"
#include "BLT_translation.h"
#include "../outliner_intern.h"
#include "tree_display.hh"
namespace blender::ed::outliner {
/* Convenience/readability. */
template<typename T> using List = ListBaseWrapper<T>;
class ObjectsChildrenBuilder {
using TreeChildren = Vector<TreeElement *>;
using ObjectTreeElementsMap = Map<Object *, TreeChildren>;
SpaceOutliner &outliner_;
ObjectTreeElementsMap object_tree_elements_map_;
public:
ObjectsChildrenBuilder(SpaceOutliner &);
~ObjectsChildrenBuilder() = default;
void operator()(TreeElement &collection_tree_elem);
private:
void object_tree_elements_lookup_create_recursive(TreeElement *);
void make_object_parent_hierarchy_collections();
};
/* -------------------------------------------------------------------- */
/** \name Tree-Display for a View Layer.
*
* \{ */
TreeDisplayViewLayer::TreeDisplayViewLayer(SpaceOutliner &space_outliner)
: AbstractTreeDisplay(space_outliner)
{
}
ListBase TreeDisplayViewLayer::buildTree(const TreeSourceData &source_data)
{
ListBase tree = {nullptr};
view_layer_ = source_data.view_layer;
show_objects_ = !(space_outliner_.filter & SO_FILTER_NO_OBJECT);
const bool show_children = (space_outliner_.filter & SO_FILTER_NO_CHILDREN) == 0;
if (space_outliner_.filter & SO_FILTER_NO_COLLECTION) {
/* Show objects in the view layer. */
for (Base *base : List<Base>(view_layer_->object_bases)) {
TreeElement *te_object = outliner_add_element(
&space_outliner_, &tree, base->object, nullptr, 0, 0);
te_object->directdata = base;
}
if (show_children) {
outliner_make_object_parent_hierarchy(&tree);
}
}
else {
/* Show collections in the view layer. */
TreeElement &ten = *outliner_add_element(
&space_outliner_, &tree, source_data.scene, nullptr, TSE_VIEW_COLLECTION_BASE, 0);
ten.name = IFACE_("Scene Collection");
TREESTORE(&ten)->flag &= ~TSE_CLOSED;
add_view_layer(ten.subtree, ten);
if (show_children) {
add_layer_collection_objects_children(ten);
}
}
return tree;
}
void TreeDisplayViewLayer::add_view_layer(ListBase &tree, TreeElement &parent)
{
/* First layer collection is for master collection, don't show it. */
LayerCollection *lc = static_cast<LayerCollection *>(view_layer_->layer_collections.first);
if (lc == nullptr) {
return;
}
add_layer_collections_recursive(tree, lc->layer_collections, parent);
if (show_objects_) {
add_layer_collection_objects(tree, *lc, parent);
}
}
void TreeDisplayViewLayer::add_layer_collections_recursive(ListBase &tree,
ListBase &layer_collections,
TreeElement &parent_ten)
{
for (LayerCollection *lc : List<LayerCollection>(layer_collections)) {
const bool exclude = (lc->flag & LAYER_COLLECTION_EXCLUDE) != 0;
TreeElement *ten;
if (exclude && ((space_outliner_.show_restrict_flags & SO_RESTRICT_ENABLE) == 0)) {
ten = &parent_ten;
}
else {
ID *id = &lc->collection->id;
ten = outliner_add_element(
&space_outliner_, &tree, id, &parent_ten, TSE_LAYER_COLLECTION, 0);
ten->name = id->name + 2;
ten->directdata = lc;
/* Open by default, except linked collections, which may contain many elements. */
TreeStoreElem *tselem = TREESTORE(ten);
if (!(tselem->used || ID_IS_LINKED(id) || ID_IS_OVERRIDE_LIBRARY(id))) {
tselem->flag &= ~TSE_CLOSED;
}
if (exclude || (lc->runtime_flag & LAYER_COLLECTION_VISIBLE_VIEW_LAYER) == 0) {
ten->flag |= TE_DISABLED;
}
}
add_layer_collections_recursive(ten->subtree, lc->layer_collections, *ten);
if (!exclude && show_objects_) {
add_layer_collection_objects(ten->subtree, *lc, *ten);
}
}
}
void TreeDisplayViewLayer::add_layer_collection_objects(ListBase &tree,
LayerCollection &lc,
TreeElement &ten)
{
for (CollectionObject *cob : List<CollectionObject>(lc.collection->gobject)) {
Base *base = BKE_view_layer_base_find(view_layer_, cob->ob);
TreeElement *te_object = outliner_add_element(
&space_outliner_, &tree, base->object, &ten, 0, 0);
te_object->directdata = base;
if (!(base->flag & BASE_VISIBLE_VIEWLAYER)) {
te_object->flag |= TE_DISABLED;
}
}
}
void TreeDisplayViewLayer::add_layer_collection_objects_children(TreeElement &collection_tree_elem)
{
/* Call helper to add children. */
ObjectsChildrenBuilder child_builder{space_outliner_};
child_builder(collection_tree_elem);
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Object Children helper.
*
* Helper to add child objects to the sub-tree of their parent, recursively covering all nested
* collections.
*
* \{ */
ObjectsChildrenBuilder::ObjectsChildrenBuilder(SpaceOutliner &outliner) : outliner_(outliner)
{
}
void ObjectsChildrenBuilder::operator()(TreeElement &collection_tree_elem)
{
object_tree_elements_lookup_create_recursive(&collection_tree_elem);
make_object_parent_hierarchy_collections();
}
/**
* Build a map from Object* to a list of TreeElement* matching the object.
*/
void ObjectsChildrenBuilder::object_tree_elements_lookup_create_recursive(TreeElement *te_parent)
{
for (TreeElement *te : List<TreeElement>(te_parent->subtree)) {
TreeStoreElem *tselem = TREESTORE(te);
if (tselem->type == TSE_LAYER_COLLECTION) {
object_tree_elements_lookup_create_recursive(te);
continue;
}
if (tselem->type == 0 && te->idcode == ID_OB) {
Object *ob = (Object *)tselem->id;
/* Lookup children or add new, empty children vector. */
Vector<TreeElement *> &tree_elements = object_tree_elements_map_.lookup_or_add(ob, {});
tree_elements.append(te);
object_tree_elements_lookup_create_recursive(te);
}
}
}
/**
* For all objects in the tree, lookup the parent in this map,
* and move or add tree elements as needed.
*/
void ObjectsChildrenBuilder::make_object_parent_hierarchy_collections()
{
for (ObjectTreeElementsMap::MutableItem item : object_tree_elements_map_.items()) {
Object *child = item.key;
if (child->parent == nullptr) {
continue;
}
Vector<TreeElement *> &child_ob_tree_elements = item.value;
Vector<TreeElement *> *parent_ob_tree_elements = object_tree_elements_map_.lookup_ptr(
child->parent);
if (parent_ob_tree_elements == nullptr) {
continue;
}
for (TreeElement *parent_ob_tree_element : *parent_ob_tree_elements) {
TreeElement *parent_ob_collection_tree_element = nullptr;
bool found = false;
/* We always want to remove the child from the direct collection its parent is nested under.
* This is particularly important when dealing with multi-level nesting (grandchildren). */
parent_ob_collection_tree_element = parent_ob_tree_element->parent;
while (!ELEM(TREESTORE(parent_ob_collection_tree_element)->type,
TSE_VIEW_COLLECTION_BASE,
TSE_LAYER_COLLECTION)) {
parent_ob_collection_tree_element = parent_ob_collection_tree_element->parent;
}
for (TreeElement *child_ob_tree_element : child_ob_tree_elements) {
if (child_ob_tree_element->parent == parent_ob_collection_tree_element) {
/* Move from the collection subtree into the parent object subtree. */
BLI_remlink(&parent_ob_collection_tree_element->subtree, child_ob_tree_element);
BLI_addtail(&parent_ob_tree_element->subtree, child_ob_tree_element);
child_ob_tree_element->parent = parent_ob_tree_element;
found = true;
break;
}
}
if (!found) {
/* We add the child in the tree even if it is not in the collection.
* We deliberately clear its sub-tree though, to make it less prominent. */
TreeElement *child_ob_tree_element = outliner_add_element(
&outliner_, &parent_ob_tree_element->subtree, child, parent_ob_tree_element, 0, 0);
outliner_free_tree(&child_ob_tree_element->subtree);
child_ob_tree_element->flag |= TE_CHILD_NOT_IN_COLLECTION;
child_ob_tree_elements.append(child_ob_tree_element);
}
}
}
}
/** \} */
} // namespace blender::ed::outliner

View File

@@ -234,6 +234,9 @@ typedef enum eSpaceButtons_Flag {
/** \name Outliner
* \{ */
/* Defined in `outliner_intern.h`. */
typedef struct SpaceOutliner_Runtime SpaceOutliner_Runtime;
/* Outliner */
typedef struct SpaceOutliner {
SpaceLink *next, *prev;
@@ -276,6 +279,8 @@ typedef struct SpaceOutliner {
* Pointers to treestore elements, grouped by (id, type, nr)
* in hashtable for faster searching */
void *treehash;
SpaceOutliner_Runtime *runtime;
} SpaceOutliner;
/* SpaceOutliner.flag */