Add an ObjectDB Profiling Tool

A new tab is added to the debugger that can help profile a game's memory usage.

Specifically, this lets you save a snapshot of all the objects in a running
game's ObjectDB to disk. It then lets you view the snapshot and diff two
snapshots against each other. This is meant to work similarly to Chrome's
heap snapshot tool or Unity's memory profiler.
This commit is contained in:
Aleksander Litynski
2025-05-06 10:30:52 +02:00
committed by Mikael Hermansson
parent 4d1f26e1fd
commit 78f1543e35
40 changed files with 4262 additions and 109 deletions
@@ -0,0 +1,274 @@
/**************************************************************************/
/* class_view.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#include "class_view.h"
#include "editor/editor_node.h"
#include "editor/themes/editor_scale.h"
#include "scene/gui/panel_container.h"
#include "scene/gui/split_container.h"
#include "shared_controls.h"
int ClassData::instance_count(GameStateSnapshot *p_snapshot) {
int count = 0;
for (const SnapshotDataObject *instance : instances) {
if (!p_snapshot || instance->snapshot == p_snapshot) {
count += 1;
}
}
return count;
}
int ClassData::get_recursive_instance_count(HashMap<String, ClassData> &p_all_classes, GameStateSnapshot *p_snapshot) {
if (!recursive_instance_count_cache.has(p_snapshot)) {
recursive_instance_count_cache[p_snapshot] = instance_count(p_snapshot);
for (const String &child : child_classes) {
recursive_instance_count_cache[p_snapshot] += p_all_classes[child].get_recursive_instance_count(p_all_classes, p_snapshot);
}
}
return recursive_instance_count_cache[p_snapshot];
}
SnapshotClassView::SnapshotClassView() {
set_name(TTR("Classes"));
class_tree = nullptr;
object_list = nullptr;
diff_object_list = nullptr;
}
void SnapshotClassView::show_snapshot(GameStateSnapshot *p_data, GameStateSnapshot *p_diff_data) {
SnapshotView::show_snapshot(p_data, p_diff_data);
set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
HSplitContainer *classes_view = memnew(HSplitContainer);
add_child(classes_view);
classes_view->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);
classes_view->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
classes_view->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
classes_view->set_split_offset(0);
VBoxContainer *class_list_column = memnew(VBoxContainer);
class_list_column->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
class_list_column->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
classes_view->add_child(class_list_column);
class_tree = memnew(Tree);
TreeSortAndFilterBar *filter_bar = memnew(TreeSortAndFilterBar(class_tree, TTR("Filter Classes")));
filter_bar->add_sort_option(TTR("Name"), TreeSortAndFilterBar::SortType::ALPHA_SORT, 0);
TreeSortAndFilterBar::SortOptionIndexes default_sort;
if (!diff_data) {
default_sort = filter_bar->add_sort_option(TTR("Count"), TreeSortAndFilterBar::SortType::NUMERIC_SORT, 1);
} else {
filter_bar->add_sort_option(TTR("A Count"), TreeSortAndFilterBar::SortType::NUMERIC_SORT, 1);
filter_bar->add_sort_option(TTR("B Count"), TreeSortAndFilterBar::SortType::NUMERIC_SORT, 2);
default_sort = filter_bar->add_sort_option(TTR("Delta"), TreeSortAndFilterBar::SortType::NUMERIC_SORT, 3);
}
class_list_column->add_child(filter_bar);
class_tree->set_select_mode(Tree::SelectMode::SELECT_ROW);
class_tree->set_custom_minimum_size(Size2(200 * EDSCALE, 0));
class_tree->set_hide_folding(false);
class_list_column->add_child(class_tree);
class_tree->set_hide_root(true);
class_tree->set_columns(diff_data ? 4 : 2);
class_tree->set_column_titles_visible(true);
class_tree->set_column_title(0, TTR("Object Class"));
class_tree->set_column_expand(0, true);
class_tree->set_column_custom_minimum_width(0, 200 * EDSCALE);
class_tree->set_column_title(1, diff_data ? TTR("A Count") : TTR("Count"));
class_tree->set_column_expand(1, false);
if (diff_data) {
class_tree->set_column_title(2, TTR("B Count"));
class_tree->set_column_expand(2, false);
class_tree->set_column_title(3, TTR("Delta"));
class_tree->set_column_expand(3, false);
// Add tooltip with the names of snapshot A and B
class_tree->set_column_title_tooltip_text(1, TTR("A: ") + snapshot_data->name);
class_tree->set_column_title_tooltip_text(2, TTR("B: ") + diff_data->name);
}
class_tree->connect(SceneStringName(item_selected), callable_mp(this, &SnapshotClassView::_class_selected));
class_tree->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
class_tree->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
class_tree->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);
VSplitContainer *object_lists = memnew(VSplitContainer);
classes_view->add_child(object_lists);
object_lists->set_custom_minimum_size(Size2(150 * EDSCALE, 0));
object_lists->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
object_lists->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
if (!diff_data) {
object_lists->add_child(object_list = _make_object_list_tree(TTR("Objects")));
} else {
object_lists->add_child(object_list = _make_object_list_tree(TTR("A Objects")));
object_lists->add_child(diff_object_list = _make_object_list_tree(TTR("B Objects")));
}
HashMap<String, ClassData> grouped_by_class;
grouped_by_class["Object"] = ClassData("Object", "");
_add_objects_to_class_map(grouped_by_class, snapshot_data);
if (diff_data != nullptr) {
_add_objects_to_class_map(grouped_by_class, diff_data);
}
grouped_by_class[""].tree_node = class_tree->create_item();
List<String> classes_todo;
for (const String &c : grouped_by_class[""].child_classes) {
classes_todo.push_front(c);
}
while (classes_todo.size() > 0) {
String next_class_name = classes_todo.get(0);
classes_todo.pop_front();
ClassData &next = grouped_by_class[next_class_name];
ClassData &nexts_parent = grouped_by_class[next.parent_class_name];
next.tree_node = class_tree->create_item(nexts_parent.tree_node);
next.tree_node->set_text(0, next_class_name + " (" + String::num_int64(next.instance_count(snapshot_data)) + ")");
int a_count = next.get_recursive_instance_count(grouped_by_class, snapshot_data);
next.tree_node->set_text(1, String::num_int64(a_count));
if (diff_data) {
int b_count = next.get_recursive_instance_count(grouped_by_class, diff_data);
next.tree_node->set_text(2, String::num_int64(b_count));
next.tree_node->set_text(3, String::num_int64(a_count - b_count));
}
next.tree_node->set_metadata(0, next_class_name);
for (const String &c : next.child_classes) {
classes_todo.push_front(c);
}
}
// Icons won't load until the frame after show_snapshot is called. Not sure why, but just defer the load.
callable_mp(this, &SnapshotClassView::_notification).call_deferred(NOTIFICATION_THEME_CHANGED);
// Default to sort by descending count. Putting the biggest groups at the top is generally pretty interesting.
filter_bar->select_sort(default_sort.descending);
filter_bar->apply();
}
Tree *SnapshotClassView::_make_object_list_tree(const String &p_column_name) {
Tree *list = memnew(Tree);
list->set_select_mode(Tree::SelectMode::SELECT_ROW);
list->set_hide_folding(true);
list->set_hide_root(true);
list->set_columns(1);
list->set_column_titles_visible(true);
list->set_column_title(0, p_column_name);
list->set_column_expand(0, true);
list->connect(SceneStringName(item_selected), callable_mp(this, &SnapshotClassView::_object_selected).bind(list));
list->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
list->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
return list;
}
void SnapshotClassView::_add_objects_to_class_map(HashMap<String, ClassData> &p_class_map, GameStateSnapshot *p_objects) {
for (const KeyValue<ObjectID, SnapshotDataObject *> &pair : p_objects->objects) {
StringName class_name = StringName(pair.value->type_name);
StringName parent_class_name = class_name != StringName() && ClassDB::class_exists(class_name) ? ClassDB::get_parent_class(class_name) : "";
p_class_map[class_name].instances.push_back(pair.value);
// Go up the tree and insert all parents/grandparents.
while (class_name != StringName()) {
if (!p_class_map.has(class_name)) {
p_class_map[class_name] = ClassData(class_name, parent_class_name);
}
if (!p_class_map.has(parent_class_name)) {
// Leave our grandparent blank for now. Next iteration of the while loop will fill it in.
p_class_map[parent_class_name] = ClassData(parent_class_name, "");
}
p_class_map[class_name].parent_class_name = parent_class_name;
p_class_map[parent_class_name].child_classes.insert(class_name);
class_name = parent_class_name;
parent_class_name = class_name != StringName() ? ClassDB::get_parent_class(class_name) : "";
}
}
}
void SnapshotClassView::_object_selected(Tree *p_tree) {
GameStateSnapshot *snapshot = snapshot_data;
if (diff_data) {
Tree *other = p_tree == diff_object_list ? object_list : diff_object_list;
TreeItem *selected = other->get_selected();
if (selected) {
selected->deselect(0);
}
if (p_tree == diff_object_list) {
snapshot = diff_data;
}
}
ObjectID object_id = p_tree->get_selected()->get_metadata(0);
EditorNode::get_singleton()->push_item((Object *)snapshot->objects[object_id]);
}
void SnapshotClassView::_class_selected() {
if (!diff_data) {
_populate_object_list(snapshot_data, object_list, TTR("Objects"));
} else {
_populate_object_list(snapshot_data, object_list, TTR("A Objects"));
_populate_object_list(diff_data, diff_object_list, TTR("B Objects"));
}
}
void SnapshotClassView::_populate_object_list(GameStateSnapshot *p_snapshot, Tree *p_list, const String &p_name_base) {
p_list->clear();
String class_name = class_tree->get_selected()->get_metadata(0);
TreeItem *root = p_list->create_item();
int object_count = 0;
for (const KeyValue<ObjectID, SnapshotDataObject *> &pair : p_snapshot->objects) {
if (pair.value->type_name == class_name) {
TreeItem *item = p_list->create_item(root);
item->set_text(0, pair.value->get_name());
item->set_metadata(0, pair.value->remote_object_id);
item->set_text_overrun_behavior(0, TextServer::OverrunBehavior::OVERRUN_NO_TRIMMING);
object_count++;
}
}
p_list->set_column_title(0, p_name_base + " (" + itos(object_count) + ")");
}
void SnapshotClassView::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_ENTER_TREE:
case NOTIFICATION_LAYOUT_DIRECTION_CHANGED:
case NOTIFICATION_THEME_CHANGED:
case NOTIFICATION_TRANSLATION_CHANGED: {
for (TreeItem *item : _get_children_recursive(class_tree)) {
item->set_icon(0, EditorNode::get_singleton()->get_class_icon(item->get_metadata(0), ""));
}
} break;
}
}
@@ -0,0 +1,73 @@
/**************************************************************************/
/* class_view.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "../snapshot_data.h"
#include "snapshot_view.h"
class Tree;
class TreeItem;
struct ClassData {
ClassData() {}
ClassData(const String &p_name, const String &p_parent) :
class_name(p_name), parent_class_name(p_parent) {}
String class_name;
String parent_class_name;
HashSet<String> child_classes;
List<SnapshotDataObject *> instances;
TreeItem *tree_node = nullptr;
HashMap<GameStateSnapshot *, int> recursive_instance_count_cache;
int instance_count(GameStateSnapshot *p_snapshot = nullptr);
int get_recursive_instance_count(HashMap<String, ClassData> &p_all_classes, GameStateSnapshot *p_snapshot = nullptr);
};
class SnapshotClassView : public SnapshotView {
GDCLASS(SnapshotClassView, SnapshotView);
protected:
Tree *class_tree = nullptr;
Tree *object_list = nullptr;
Tree *diff_object_list = nullptr;
void _object_selected(Tree *p_tree);
void _class_selected();
void _add_objects_to_class_map(HashMap<String, ClassData> &p_class_map, GameStateSnapshot *p_objects);
void _notification(int p_what);
Tree *_make_object_list_tree(const String &p_column_name);
void _populate_object_list(GameStateSnapshot *p_snapshot, Tree *p_list, const String &p_name_base);
public:
SnapshotClassView();
virtual void show_snapshot(GameStateSnapshot *p_data, GameStateSnapshot *p_diff_data) override;
};
@@ -0,0 +1,163 @@
/**************************************************************************/
/* json_view.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#include "json_view.h"
#include "core/io/json.h"
#include "scene/gui/center_container.h"
#include "scene/gui/panel_container.h"
#include "scene/gui/split_container.h"
#include "shared_controls.h"
SnapshotJsonView::SnapshotJsonView() {
set_name(TTR("JSON"));
}
void SnapshotJsonView::show_snapshot(GameStateSnapshot *p_data, GameStateSnapshot *p_diff_data) {
// Lock isn't released until the data processing background thread has finished running
// and the json has been passed back to the main thread and displayed.
SnapshotView::show_snapshot(p_data, p_diff_data);
HSplitContainer *box = memnew(HSplitContainer);
box->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);
add_child(box);
loading_panel = memnew(DarkPanelContainer);
CenterContainer *loading_center = memnew(CenterContainer);
Label *loading_label = memnew(Label(TTR("Loading")));
add_child(loading_panel);
loading_panel->add_child(loading_center);
loading_center->add_child(loading_label);
loading_panel->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);
loading_center->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);
VBoxContainer *json_box = memnew(VBoxContainer);
json_box->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
json_box->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
box->add_child(json_box);
String hdr_a_text = diff_data ? TTR("Snapshot A JSON") : TTR("Snapshot JSON");
SpanningHeader *hdr_a = memnew(SpanningHeader(hdr_a_text));
if (diff_data) {
hdr_a->set_tooltip_text(TTR("Snapshot A: ") + snapshot_data->name);
}
json_box->add_child(hdr_a);
Ref<EditorJsonVisualizerSyntaxHighlighter> syntax_highlighter;
syntax_highlighter.instantiate(List<String>());
json_content = memnew(EditorJsonVisualizer);
json_content->load_theme(syntax_highlighter);
json_content->set_name(hdr_a_text);
json_content->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
json_content->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
json_box->add_child(json_content);
if (diff_data) {
VBoxContainer *diff_json_box = memnew(VBoxContainer);
diff_json_box->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
diff_json_box->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
box->add_child(diff_json_box);
String hrd_b_text = TTR("Snapshot B JSON");
SpanningHeader *hdr_b = memnew(SpanningHeader(hrd_b_text));
hdr_b->set_tooltip_text(TTR("Snapshot B: ") + diff_data->name);
diff_json_box->add_child(hdr_b);
diff_json_content = memnew(EditorJsonVisualizer);
diff_json_content->load_theme(syntax_highlighter);
diff_json_content->set_name(hrd_b_text);
diff_json_content->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
diff_json_content->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
diff_json_box->add_child(diff_json_content);
}
WorkerThreadPool::get_singleton()->add_native_task(&SnapshotJsonView::_serialization_worker, this);
}
String SnapshotJsonView::_snapshot_to_json(GameStateSnapshot *p_snapshot) {
if (p_snapshot == nullptr) {
return "";
}
Dictionary json_data;
json_data["name"] = p_snapshot->name;
Dictionary objects;
for (const KeyValue<ObjectID, SnapshotDataObject *> &obj : p_snapshot->objects) {
Dictionary obj_data;
obj_data["type_name"] = obj.value->type_name;
Array prop_list;
for (const PropertyInfo &prop : obj.value->prop_list) {
prop_list.push_back((Dictionary)prop);
}
objects["prop_list"] = prop_list;
Dictionary prop_values;
for (const KeyValue<StringName, Variant> &prop : obj.value->prop_values) {
// should only ever be one entry in this context
prop_values[prop.key] = prop.value;
}
obj_data["prop_values"] = prop_values;
objects[obj.key] = obj_data;
}
json_data["objects"] = objects;
return JSON::stringify(json_data, " ", true, true);
}
void SnapshotJsonView::_serialization_worker(void *p_ud) {
// About 0.3s to serialize snapshots in a small game.
SnapshotJsonView *self = static_cast<SnapshotJsonView *>(p_ud);
GameStateSnapshot *snapshot_data = self->snapshot_data;
GameStateSnapshot *diff_data = self->diff_data;
// let the message queue figure out if self is still a valid object or if it's been destroyed.
MessageQueue::get_singleton()->push_call(self, "_update_text",
snapshot_data, diff_data,
_snapshot_to_json(snapshot_data),
_snapshot_to_json(diff_data));
}
void SnapshotJsonView::_update_text(GameStateSnapshot *p_data_ptr, GameStateSnapshot *p_diff_ptr, const String &p_data_str, const String &p_diff_data_str) {
if (p_data_ptr != snapshot_data || p_diff_ptr != diff_data) {
// If the GameStateSnapshots we generated strings for no longer match the snapshots we asked for,
// throw these results away. We'll get more from a different worker process.
return;
}
// About 5s to insert the string into the editor.
json_content->set_text(p_data_str);
if (diff_data) {
diff_json_content->set_text(p_diff_data_str);
}
loading_panel->queue_free();
// Loading json done, release the lock.
}
void SnapshotJsonView::_bind_methods() {
ClassDB::bind_method(D_METHOD("_update_text", "p_data_ptr", "p_diff_ptr", "p_data_str", "p_diff_data_str"), &SnapshotJsonView::_update_text);
}
@@ -0,0 +1,57 @@
/**************************************************************************/
/* json_view.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "../snapshot_data.h"
#include "editor/editor_json_visualizer.h"
#include "snapshot_view.h"
class SnapshotJsonView : public SnapshotView {
GDCLASS(SnapshotJsonView, SnapshotView);
protected:
static void _serialization_worker(void *p_ud);
void _update_text(GameStateSnapshot *p_data_ptr, GameStateSnapshot *p_diff_ptr, const String &p_data_str, const String &p_diff_data_str);
static void _bind_methods();
EditorJsonVisualizer *json_content = nullptr;
EditorJsonVisualizer *diff_json_content = nullptr;
Control *loading_panel = nullptr;
void _load_theme_settings();
static String _snapshot_to_json(GameStateSnapshot *p_snapshot);
public:
SnapshotJsonView();
virtual void show_snapshot(GameStateSnapshot *p_data, GameStateSnapshot *p_diff_data) override;
};
@@ -0,0 +1,262 @@
/**************************************************************************/
/* node_view.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#include "node_view.h"
#include "editor/editor_node.h"
#include "editor/themes/editor_scale.h"
#include "scene/gui/check_button.h"
#include "scene/gui/split_container.h"
SnapshotNodeView::SnapshotNodeView() {
set_name("Nodes");
}
void SnapshotNodeView::show_snapshot(GameStateSnapshot *p_data, GameStateSnapshot *p_diff_data) {
SnapshotView::show_snapshot(p_data, p_diff_data);
set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
HSplitContainer *diff_sides = memnew(HSplitContainer);
diff_sides->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);
add_child(diff_sides);
bool show_diff_label = diff_data && combined_diff_view;
main_tree = _make_node_tree(diff_data && !combined_diff_view ? TTR("A Nodes") : TTR("Nodes"), snapshot_data);
diff_sides->add_child(main_tree.root);
_add_snapshot_to_tree(main_tree.tree, snapshot_data, show_diff_label ? "-" : "");
if (diff_data) {
CheckButton *diff_mode_toggle = memnew(CheckButton(TTR("Combine Diff")));
diff_mode_toggle->set_pressed(combined_diff_view);
diff_mode_toggle->connect(SceneStringName(toggled), callable_mp(this, &SnapshotNodeView::_toggle_diff_mode));
main_tree.filter_bar->add_child(diff_mode_toggle);
main_tree.filter_bar->move_child(diff_mode_toggle, 0);
if (combined_diff_view) {
// Merge the snapshots together and add a diff.
_add_snapshot_to_tree(main_tree.tree, diff_data, "+");
} else {
// Add a second column with the diff snapshot.
diff_tree = _make_node_tree(TTR("B Nodes"), diff_data);
diff_sides->add_child(diff_tree.root);
_add_snapshot_to_tree(diff_tree.tree, diff_data, "");
}
}
_refresh_icons();
main_tree.filter_bar->apply();
if (diff_tree.filter_bar) {
diff_tree.filter_bar->apply();
diff_sides->set_split_offset(diff_sides->get_size().x * 0.5);
}
choose_object_menu = memnew(PopupMenu);
add_child(choose_object_menu);
choose_object_menu->connect(SceneStringName(id_pressed), callable_mp(this, &SnapshotNodeView::_choose_object_pressed).bind(false));
}
NodeTreeElements SnapshotNodeView::_make_node_tree(const String &p_tree_name, GameStateSnapshot *p_snapshot) {
NodeTreeElements elements;
elements.root = memnew(VBoxContainer);
elements.root->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);
elements.tree = memnew(Tree);
elements.filter_bar = memnew(TreeSortAndFilterBar(elements.tree, TTR("Filter Nodes")));
elements.root->add_child(elements.filter_bar);
elements.tree->set_select_mode(Tree::SelectMode::SELECT_ROW);
elements.tree->set_custom_minimum_size(Size2(150, 0) * EDSCALE);
elements.tree->set_hide_folding(false);
elements.root->add_child(elements.tree);
elements.tree->set_hide_root(true);
elements.tree->set_allow_reselect(true);
elements.tree->set_columns(1);
elements.tree->set_column_titles_visible(true);
elements.tree->set_column_title(0, p_tree_name);
elements.tree->set_column_expand(0, true);
elements.tree->set_column_clip_content(0, false);
elements.tree->set_column_custom_minimum_width(0, 150 * EDSCALE);
elements.tree->connect(SceneStringName(item_selected), callable_mp(this, &SnapshotNodeView::_node_selected).bind(elements.tree));
elements.tree->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
elements.tree->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
elements.tree->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);
elements.tree->create_item();
return elements;
}
void SnapshotNodeView::_node_selected(Tree *p_tree_selected_from) {
active_tree = p_tree_selected_from;
if (diff_tree.tree) {
// Deselect nodes in non-active tree, if needed.
if (active_tree == main_tree.tree) {
diff_tree.tree->deselect_all();
}
if (active_tree == diff_tree.tree) {
main_tree.tree->deselect_all();
}
}
List<SnapshotDataObject *> &objects = tree_item_owners[p_tree_selected_from->get_selected()];
if (objects.is_empty()) {
return;
}
if (objects.size() == 1) {
EditorNode::get_singleton()->push_item((Object *)(objects.get(0)));
}
if (objects.size() == 2) {
// This happens if we're in the combined diff view and the node exists in both trees
// The user has to specify which version of the node they want to see in the inspector.
_show_choose_object_menu();
}
}
void SnapshotNodeView::_toggle_diff_mode(bool p_state) {
combined_diff_view = p_state;
show_snapshot(snapshot_data, diff_data); // Redraw everything when we toggle views.
}
void SnapshotNodeView::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_ENTER_TREE:
case NOTIFICATION_LAYOUT_DIRECTION_CHANGED:
case NOTIFICATION_THEME_CHANGED:
case NOTIFICATION_TRANSLATION_CHANGED: {
_refresh_icons();
} break;
}
}
void SnapshotNodeView::_add_snapshot_to_tree(Tree *p_tree, GameStateSnapshot *p_snapshot, const String &p_diff_group_name) {
for (const KeyValue<ObjectID, SnapshotDataObject *> &kv : p_snapshot->objects) {
if (kv.value->is_node() && !kv.value->extra_debug_data.has("node_parent")) {
TreeItem *root_item = _add_child_named(p_tree, p_tree->get_root(), kv.value, p_diff_group_name);
_add_object_to_tree(root_item, kv.value, p_diff_group_name);
}
}
}
void SnapshotNodeView::_add_object_to_tree(TreeItem *p_parent_item, SnapshotDataObject *p_data, const String &p_diff_group_name) {
for (const Variant &v : (Array)p_data->extra_debug_data["node_children"]) {
SnapshotDataObject *child_object = p_data->snapshot->objects[ObjectID((uint64_t)v)];
TreeItem *child_item = _add_child_named(p_parent_item->get_tree(), p_parent_item, child_object, p_diff_group_name);
_add_object_to_tree(child_item, child_object, p_diff_group_name);
}
}
TreeItem *SnapshotNodeView::_add_child_named(Tree *p_tree, TreeItem *p_item, SnapshotDataObject *p_item_owner, const String &p_diff_group_name) {
bool has_group = !p_diff_group_name.is_empty();
const String &item_name = p_item_owner->extra_debug_data["node_name"];
// Find out if this node already exists.
TreeItem *child_item = nullptr;
if (has_group) {
for (int idx = 0; idx < p_item->get_child_count(); idx++) {
TreeItem *child = p_item->get_child(idx);
if (child->get_text(0) == item_name) {
child_item = child;
break;
}
}
}
if (child_item) {
// If it exists, clear the background color because we now know it exists in both trees.
child_item->clear_custom_bg_color(0);
} else {
// Add the new node and set it's background color to green or red depending on which snapshot it's a part of.
if (p_item_owner->extra_debug_data["node_is_scene_root"]) {
child_item = p_tree->get_root() ? p_tree->get_root() : p_tree->create_item();
} else {
child_item = p_tree->create_item(p_item);
}
if (has_group) {
if (p_diff_group_name == "+") {
child_item->set_custom_bg_color(0, Color(0, 1, 0, 0.1));
}
if (p_diff_group_name == "-") {
child_item->set_custom_bg_color(0, Color(1, 0, 0, 0.1));
}
}
}
child_item->set_text(0, item_name);
_add_tree_item_owner(child_item, p_item_owner);
return child_item;
}
// Each node in the tree may be part of one or two snapshots. This tracks that relationship
// so we can display the correct data in the inspector if a node is clicked.
void SnapshotNodeView::_add_tree_item_owner(TreeItem *p_item, SnapshotDataObject *p_owner) {
if (!tree_item_owners.has(p_item)) {
tree_item_owners.insert(p_item, List<SnapshotDataObject *>());
}
tree_item_owners[p_item].push_back(p_owner);
}
void SnapshotNodeView::_refresh_icons() {
for (TreeItem *item : _get_children_recursive(main_tree.tree)) {
item->set_icon(0, EditorNode::get_singleton()->get_class_icon(tree_item_owners[item].get(0)->type_name, ""));
}
if (diff_tree.tree) {
for (TreeItem *item : _get_children_recursive(diff_tree.tree)) {
item->set_icon(0, EditorNode::get_singleton()->get_class_icon(tree_item_owners[item].get(0)->type_name, ""));
}
}
}
void SnapshotNodeView::clear_snapshot() {
SnapshotView::clear_snapshot();
tree_item_owners.clear();
main_tree.tree = nullptr;
main_tree.filter_bar = nullptr;
main_tree.root = nullptr;
diff_tree.tree = nullptr;
diff_tree.filter_bar = nullptr;
diff_tree.root = nullptr;
active_tree = nullptr;
}
void SnapshotNodeView::_choose_object_pressed(int p_object_idx, bool p_confirm_override) {
List<SnapshotDataObject *> &objects = tree_item_owners[active_tree->get_selected()];
EditorNode::get_singleton()->push_item((Object *)objects.get(p_object_idx));
}
void SnapshotNodeView::_show_choose_object_menu() {
remove_child(choose_object_menu);
add_child(choose_object_menu);
choose_object_menu->clear(false);
choose_object_menu->add_item(TTR("Snapshot A"), 0);
choose_object_menu->add_item(TTR("Snapshot B"), 1);
choose_object_menu->reset_size();
choose_object_menu->set_position(get_screen_position() + get_local_mouse_position());
choose_object_menu->popup();
}
@@ -0,0 +1,85 @@
/**************************************************************************/
/* node_view.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "../snapshot_data.h"
#include "shared_controls.h"
#include "snapshot_view.h"
class Tree;
// When diffing in split view, we have two trees/filters
// so this struct is used to group their properties together.
struct NodeTreeElements {
NodeTreeElements() {
tree = nullptr;
filter_bar = nullptr;
root = nullptr;
}
Tree *tree = nullptr;
TreeSortAndFilterBar *filter_bar = nullptr;
VBoxContainer *root = nullptr;
};
class SnapshotNodeView : public SnapshotView {
GDCLASS(SnapshotNodeView, SnapshotView);
protected:
NodeTreeElements main_tree;
NodeTreeElements diff_tree;
Tree *active_tree = nullptr;
PopupMenu *choose_object_menu = nullptr;
bool combined_diff_view = true;
HashMap<TreeItem *, List<SnapshotDataObject *>> tree_item_owners;
void _node_selected(Tree *p_tree_selected_from);
void _notification(int p_what);
NodeTreeElements _make_node_tree(const String &p_tree_name, GameStateSnapshot *p_snapshot);
void _apply_filters();
void _refresh_icons();
void _toggle_diff_mode(bool p_state);
void _choose_object_pressed(int p_object_idx, bool p_confirm_override);
void _show_choose_object_menu();
// `_add_snapshot_to_tree`, `_add_object_to_tree`, and `_add_child_named` work together to add items to the node tree.
// They support adding two snapshots to the same tree, and will highlight rows to show additions and removals.
// `_add_snapshot_to_tree` walks the root items in the tree and adds them first, then `_add_object_to_tree` recursively
// adds all the child items. `_add_child_named` is used by both to add each individual items.
void _add_snapshot_to_tree(Tree *p_tree, GameStateSnapshot *p_snapshot, const String &p_diff_group_name = "");
void _add_object_to_tree(TreeItem *p_parent_item, SnapshotDataObject *p_data, const String &p_diff_group_name = "");
TreeItem *_add_child_named(Tree *p_tree, TreeItem *p_item, SnapshotDataObject *p_item_owner, const String &p_diff_group_name = "");
void _add_tree_item_owner(TreeItem *p_item, SnapshotDataObject *p_owner);
public:
SnapshotNodeView();
virtual void show_snapshot(GameStateSnapshot *p_data, GameStateSnapshot *p_diff_data) override;
virtual void clear_snapshot() override;
};
@@ -0,0 +1,251 @@
/**************************************************************************/
/* object_view.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#include "object_view.h"
#include "editor/editor_node.h"
#include "editor/themes/editor_scale.h"
#include "scene/gui/rich_text_label.h"
#include "scene/gui/split_container.h"
SnapshotObjectView::SnapshotObjectView() {
set_name(TTR("Objects"));
}
void SnapshotObjectView::show_snapshot(GameStateSnapshot *p_data, GameStateSnapshot *p_diff_data) {
SnapshotView::show_snapshot(p_data, p_diff_data);
item_data_map.clear();
data_item_map.clear();
set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
objects_view = memnew(HSplitContainer);
add_child(objects_view);
objects_view->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);
VBoxContainer *object_column = memnew(VBoxContainer);
object_column->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);
objects_view->add_child(object_column);
object_column->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
object_column->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
object_list = memnew(Tree);
filter_bar = memnew(TreeSortAndFilterBar(object_list, TTR("Filter Objects")));
object_column->add_child(filter_bar);
int sort_idx = 0;
if (diff_data) {
filter_bar->add_sort_option(TTR("Snapshot"), TreeSortAndFilterBar::SortType::ALPHA_SORT, sort_idx++);
}
filter_bar->add_sort_option(TTR("Class"), TreeSortAndFilterBar::SortType::ALPHA_SORT, sort_idx++);
filter_bar->add_sort_option(TTR("Name"), TreeSortAndFilterBar::SortType::ALPHA_SORT, sort_idx++);
filter_bar->add_sort_option(TTR("Inbound References"), TreeSortAndFilterBar::SortType::NUMERIC_SORT, sort_idx++);
TreeSortAndFilterBar::SortOptionIndexes default_sort = filter_bar->add_sort_option(
TTR("Outbound References"), TreeSortAndFilterBar::SortType::NUMERIC_SORT, sort_idx++);
// Tree of objects.
object_list->set_select_mode(Tree::SelectMode::SELECT_ROW);
object_list->set_custom_minimum_size(Size2(200, 0) * EDSCALE);
object_list->set_hide_folding(false);
object_column->add_child(object_list);
object_list->set_hide_root(true);
object_list->set_columns(diff_data ? 5 : 4);
object_list->set_column_titles_visible(true);
int offset = 0;
if (diff_data) {
object_list->set_column_title(0, TTR("Snapshot"));
object_list->set_column_expand(0, false);
object_list->set_column_title_tooltip_text(0, "A: " + snapshot_data->name + ", B: " + diff_data->name);
offset++;
}
object_list->set_column_title(offset + 0, TTR("Class"));
object_list->set_column_expand(offset + 0, true);
object_list->set_column_title_tooltip_text(offset + 0, TTR("Object's class"));
object_list->set_column_title(offset + 1, TTR("Object"));
object_list->set_column_expand(offset + 1, true);
object_list->set_column_expand_ratio(offset + 1, 2);
object_list->set_column_title_tooltip_text(offset + 1, TTR("Object's name"));
object_list->set_column_title(offset + 2, TTR("In"));
object_list->set_column_expand(offset + 2, false);
object_list->set_column_clip_content(offset + 2, false);
object_list->set_column_title_tooltip_text(offset + 2, TTR("Number of inbound references"));
object_list->set_column_custom_minimum_width(offset + 2, 30 * EDSCALE);
object_list->set_column_title(offset + 3, TTR("Out"));
object_list->set_column_expand(offset + 3, false);
object_list->set_column_clip_content(offset + 3, false);
object_list->set_column_title_tooltip_text(offset + 3, TTR("Number of outbound references"));
object_list->set_column_custom_minimum_width(offset + 2, 30 * EDSCALE);
object_list->connect(SceneStringName(item_selected), callable_mp(this, &SnapshotObjectView::_object_selected));
object_list->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
object_list->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
object_details = memnew(VBoxContainer);
object_details->set_custom_minimum_size(Size2(200, 0) * EDSCALE);
objects_view->add_child(object_details);
object_details->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
object_details->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
object_list->create_item();
_insert_data(snapshot_data, TTR("A"));
if (diff_data) {
_insert_data(diff_data, TTR("B"));
}
filter_bar->select_sort(default_sort.descending);
filter_bar->apply();
object_list->set_selected(object_list->get_root()->get_first_child());
// Expand the left panel as wide as we can. Passing `INT_MAX` or any very large int will have the opposite effect
// and shrink the left panel as small as it can go. So, pass an int we know is larger than the current panel, but not
// 'very' large (whatever that exact number is).
objects_view->set_split_offset(get_viewport_rect().size.x);
}
void SnapshotObjectView::_insert_data(GameStateSnapshot *p_snapshot, const String &p_name) {
for (const KeyValue<ObjectID, SnapshotDataObject *> &pair : p_snapshot->objects) {
TreeItem *item = object_list->create_item(object_list->get_root());
int offset = 0;
if (diff_data) {
item->set_text(0, p_name);
item->set_tooltip_text(0, p_snapshot->name);
offset = 1;
}
item->set_text(offset + 0, pair.value->type_name);
item->set_text(offset + 1, pair.value->get_name());
item->set_text(offset + 2, String::num_uint64(pair.value->inbound_references.size()));
item->set_text(offset + 3, String::num_uint64(pair.value->outbound_references.size()));
item_data_map[item] = pair.value;
data_item_map[pair.value] = item;
}
}
void SnapshotObjectView::_object_selected() {
reference_item_map.clear();
for (int i = 0; i < object_details->get_child_count(); i++) {
object_details->get_child(i)->queue_free();
}
SnapshotDataObject *d = item_data_map[object_list->get_selected()];
EditorNode::get_singleton()->push_item((Object *)d);
DarkPanelContainer *object_panel = memnew(DarkPanelContainer);
VBoxContainer *object_panel_content = memnew(VBoxContainer);
object_panel_content->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
object_panel_content->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
object_details->add_child(object_panel);
object_panel->add_child(object_panel_content);
object_panel_content->add_child(memnew(SpanningHeader(d->get_name())));
ScrollContainer *properties_scroll = memnew(ScrollContainer);
properties_scroll->set_horizontal_scroll_mode(ScrollContainer::SCROLL_MODE_DISABLED);
properties_scroll->set_vertical_scroll_mode(ScrollContainer::SCROLL_MODE_AUTO);
properties_scroll->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
properties_scroll->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
object_panel_content->add_child(properties_scroll);
VBoxContainer *properties_container = memnew(VBoxContainer);
properties_container->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
properties_scroll->add_child(properties_container);
properties_container->add_theme_constant_override("separation", 8);
inbound_tree = _make_references_list(properties_container, TTR("Inbound References"), TTR("Source"), TTR("Other object referencing this object"), TTR("Property"), TTR("Property of other object referencing this object"));
inbound_tree->connect(SceneStringName(item_selected), callable_mp(this, &SnapshotObjectView::_reference_selected).bind(inbound_tree));
TreeItem *ib_root = inbound_tree->create_item();
for (const KeyValue<String, ObjectID> &ob : d->inbound_references) {
TreeItem *i = inbound_tree->create_item(ib_root);
SnapshotDataObject *target = d->snapshot->objects[ob.value];
i->set_text(0, target->get_name());
i->set_text(1, ob.key);
reference_item_map[i] = data_item_map[target];
}
outbound_tree = _make_references_list(properties_container, TTR("Outbound References"), TTR("Property"), TTR("Property of this object referencing other object"), TTR("Target"), TTR("Other object being referenced"));
outbound_tree->connect(SceneStringName(item_selected), callable_mp(this, &SnapshotObjectView::_reference_selected).bind(outbound_tree));
TreeItem *ob_root = outbound_tree->create_item();
for (const KeyValue<String, ObjectID> &ob : d->outbound_references) {
TreeItem *i = outbound_tree->create_item(ob_root);
SnapshotDataObject *target = d->snapshot->objects[ob.value];
i->set_text(0, ob.key);
i->set_text(1, target->get_name());
reference_item_map[i] = data_item_map[target];
}
}
void SnapshotObjectView::_reference_selected(Tree *p_source_tree) {
TreeItem *ref_item = p_source_tree->get_selected();
Tree *other_tree = p_source_tree == inbound_tree ? outbound_tree : inbound_tree;
other_tree->deselect_all();
TreeItem *other = reference_item_map[ref_item];
if (other) {
if (!other->is_visible()) {
// Clear the filter if we can't see the node we just chose.
filter_bar->clear_filter();
}
other->get_tree()->deselect_all();
other->get_tree()->set_selected(other);
other->get_tree()->ensure_cursor_is_visible();
}
}
Tree *SnapshotObjectView::_make_references_list(Control *p_container, const String &p_name, const String &p_col_1, const String &p_col_1_tooltip, const String &p_col_2, const String &p_col_2_tooltip) {
VBoxContainer *vbox = memnew(VBoxContainer);
vbox->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
vbox->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
vbox->add_theme_constant_override("separation", 4);
p_container->add_child(vbox);
vbox->set_custom_minimum_size(Vector2(300, 0) * EDSCALE);
RichTextLabel *lbl = memnew(RichTextLabel("[center]" + p_name + "[center]"));
lbl->set_fit_content(true);
lbl->set_use_bbcode(true);
vbox->add_child(lbl);
Tree *tree = memnew(Tree);
tree->set_hide_folding(true);
vbox->add_child(tree);
tree->set_select_mode(Tree::SelectMode::SELECT_ROW);
tree->set_hide_root(true);
tree->set_columns(2);
tree->set_column_titles_visible(true);
tree->set_column_title(0, p_col_1);
tree->set_column_expand(0, true);
tree->set_column_title_tooltip_text(0, p_col_1_tooltip);
tree->set_column_clip_content(0, false);
tree->set_column_title(1, p_col_2);
tree->set_column_expand(1, true);
tree->set_column_clip_content(1, false);
tree->set_column_title_tooltip_text(1, p_col_2_tooltip);
tree->set_v_scroll_enabled(false);
tree->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
return tree;
}
@@ -0,0 +1,63 @@
/**************************************************************************/
/* object_view.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "../snapshot_data.h"
#include "shared_controls.h"
#include "snapshot_view.h"
class Tree;
class HSplitContainer;
class SnapshotObjectView : public SnapshotView {
GDCLASS(SnapshotObjectView, SnapshotView);
protected:
Tree *object_list = nullptr;
Tree *inbound_tree = nullptr;
Tree *outbound_tree = nullptr;
VBoxContainer *object_details = nullptr;
TreeSortAndFilterBar *filter_bar = nullptr;
HSplitContainer *objects_view = nullptr;
HashMap<TreeItem *, SnapshotDataObject *> item_data_map;
HashMap<SnapshotDataObject *, TreeItem *> data_item_map;
HashMap<TreeItem *, TreeItem *> reference_item_map;
void _object_selected();
void _insert_data(GameStateSnapshot *p_snapshot, const String &p_name);
Tree *_make_references_list(Control *p_container, const String &p_name, const String &p_col_1, const String &p_col_1_tooltip, const String &p_col_2, const String &p_col_2_tooltip);
void _reference_selected(Tree *p_source_tree);
public:
SnapshotObjectView();
virtual void show_snapshot(GameStateSnapshot *p_data, GameStateSnapshot *p_diff_data) override;
};
@@ -0,0 +1,310 @@
/**************************************************************************/
/* refcounted_view.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#include "refcounted_view.h"
#include "editor/editor_node.h"
#include "editor/themes/editor_scale.h"
#include "scene/gui/rich_text_label.h"
#include "scene/gui/split_container.h"
SnapshotRefCountedView::SnapshotRefCountedView() {
set_name(TTR("RefCounted"));
}
void SnapshotRefCountedView::show_snapshot(GameStateSnapshot *p_data, GameStateSnapshot *p_diff_data) {
SnapshotView::show_snapshot(p_data, p_diff_data);
item_data_map.clear();
data_item_map.clear();
set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
refs_view = memnew(HSplitContainer);
add_child(refs_view);
refs_view->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);
VBoxContainer *refs_column = memnew(VBoxContainer);
refs_column->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);
refs_view->add_child(refs_column);
// Tree of Refs.
refs_list = memnew(Tree);
filter_bar = memnew(TreeSortAndFilterBar(refs_list, TTR("Filter RefCounteds")));
refs_column->add_child(filter_bar);
int offset = diff_data ? 1 : 0;
if (diff_data) {
filter_bar->add_sort_option(TTR("Snapshot"), TreeSortAndFilterBar::SortType::ALPHA_SORT, 0);
}
filter_bar->add_sort_option(TTR("Class"), TreeSortAndFilterBar::SortType::ALPHA_SORT, offset + 0);
filter_bar->add_sort_option(TTR("Name"), TreeSortAndFilterBar::SortType::ALPHA_SORT, offset + 1);
TreeSortAndFilterBar::SortOptionIndexes default_sort = filter_bar->add_sort_option(
TTR("Native Refs"),
TreeSortAndFilterBar::SortType::NUMERIC_SORT,
offset + 2);
filter_bar->add_sort_option(TTR("ObjectDB Refs"), TreeSortAndFilterBar::SortType::NUMERIC_SORT, offset + 3);
filter_bar->add_sort_option(TTR("Total Refs"), TreeSortAndFilterBar::SortType::NUMERIC_SORT, offset + 4);
filter_bar->add_sort_option(TTR("ObjectDB Cycles"), TreeSortAndFilterBar::SortType::NUMERIC_SORT, offset + 5);
refs_list->set_select_mode(Tree::SelectMode::SELECT_ROW);
refs_list->set_custom_minimum_size(Size2(200, 0) * EDSCALE);
refs_list->set_hide_folding(false);
refs_column->add_child(refs_list);
refs_list->set_hide_root(true);
refs_list->set_columns(diff_data ? 7 : 6);
refs_list->set_column_titles_visible(true);
if (diff_data) {
refs_list->set_column_title(0, TTR("Snapshot"));
refs_list->set_column_expand(0, false);
refs_list->set_column_title_tooltip_text(0, "A: " + snapshot_data->name + ", B: " + diff_data->name);
}
refs_list->set_column_title(offset + 0, TTR("Class"));
refs_list->set_column_expand(offset + 0, true);
refs_list->set_column_title_tooltip_text(offset + 0, TTR("Object's class"));
refs_list->set_column_title(offset + 1, TTR("Name"));
refs_list->set_column_expand(offset + 1, true);
refs_list->set_column_expand_ratio(offset + 1, 2);
refs_list->set_column_title_tooltip_text(offset + 1, TTR("Object's name"));
refs_list->set_column_title(offset + 2, TTR("Native Refs"));
refs_list->set_column_expand(offset + 2, false);
refs_list->set_column_title_tooltip_text(offset + 2, TTR("References not owned by the ObjectDB"));
refs_list->set_column_title(offset + 3, TTR("ObjectDB Refs"));
refs_list->set_column_expand(offset + 3, false);
refs_list->set_column_title_tooltip_text(offset + 3, TTR("References owned by the ObjectDB"));
refs_list->set_column_title(offset + 4, TTR("Total Refs"));
refs_list->set_column_expand(offset + 4, false);
refs_list->set_column_title_tooltip_text(offset + 4, TTR("ObjectDB References + Native References"));
refs_list->set_column_title(offset + 5, TTR("ObjectDB Cycles"));
refs_list->set_column_expand(offset + 5, false);
refs_list->set_column_title_tooltip_text(offset + 5, TTR("Cycles detected in the ObjectDB"));
refs_list->connect("item_selected", callable_mp(this, &SnapshotRefCountedView::_refcounted_selected));
refs_list->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
refs_list->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
// View of the selected refcounted.
ref_details = memnew(VBoxContainer);
ref_details->set_custom_minimum_size(Size2(200, 0) * EDSCALE);
refs_view->add_child(ref_details);
ref_details->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
ref_details->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
refs_list->create_item();
_insert_data(snapshot_data, TTR("A"));
if (diff_data) {
_insert_data(diff_data, TTR("B"));
}
// Push the split as far right as possible.
filter_bar->select_sort(default_sort.descending);
filter_bar->apply();
refs_list->set_selected(refs_list->get_root()->get_first_child());
callable_mp(this, &SnapshotRefCountedView::_set_split_to_center).call_deferred();
}
void SnapshotRefCountedView::_set_split_to_center() {
refs_view->set_split_offset(refs_view->get_size().x * 0.5);
}
void SnapshotRefCountedView::_insert_data(GameStateSnapshot *p_snapshot, const String &p_name) {
for (const KeyValue<ObjectID, SnapshotDataObject *> &pair : p_snapshot->objects) {
if (!pair.value->is_refcounted()) {
continue;
}
TreeItem *item = refs_list->create_item(refs_list->get_root());
item_data_map[item] = pair.value;
data_item_map[pair.value] = item;
int total_refs = pair.value->extra_debug_data.has("ref_count") ? (uint64_t)pair.value->extra_debug_data["ref_count"] : 0;
int objectdb_refs = pair.value->get_unique_inbound_references().size();
int native_refs = total_refs - objectdb_refs;
Array ref_cycles = (Array)pair.value->extra_debug_data["ref_cycles"];
int offset = 0;
if (diff_data) {
item->set_text(0, p_name);
item->set_tooltip_text(0, p_snapshot->name);
offset = 1;
}
item->set_text(offset + 0, pair.value->type_name);
item->set_text(offset + 1, pair.value->get_name());
item->set_text(offset + 2, String::num_uint64(native_refs));
item->set_text(offset + 3, String::num_uint64(objectdb_refs));
item->set_text(offset + 4, String::num_uint64(total_refs));
item->set_text(offset + 5, String::num_uint64(ref_cycles.size())); // Compute cycles and attach it to refcounted object.
if (total_refs == ref_cycles.size()) {
// Often, references are held by the engine so we can't know if we're stuck in a cycle or not
// But if the full cycle is visible in the ObjectDB,
// tell the user by highlighting the cells in red.
item->set_custom_bg_color(offset + 5, Color(1, 0, 0, 0.1));
}
}
}
void SnapshotRefCountedView::_refcounted_selected() {
for (int i = 0; i < ref_details->get_child_count(); i++) {
ref_details->get_child(i)->queue_free();
}
SnapshotDataObject *d = item_data_map[refs_list->get_selected()];
EditorNode::get_singleton()->push_item((Object *)d);
DarkPanelContainer *refcounted_panel = memnew(DarkPanelContainer);
VBoxContainer *refcounted_panel_content = memnew(VBoxContainer);
refcounted_panel_content->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
refcounted_panel_content->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
ref_details->add_child(refcounted_panel);
refcounted_panel->add_child(refcounted_panel_content);
refcounted_panel_content->add_child(memnew(SpanningHeader(d->get_name())));
ScrollContainer *properties_scroll = memnew(ScrollContainer);
properties_scroll->set_horizontal_scroll_mode(ScrollContainer::SCROLL_MODE_DISABLED);
properties_scroll->set_vertical_scroll_mode(ScrollContainer::SCROLL_MODE_AUTO);
properties_scroll->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
properties_scroll->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
refcounted_panel_content->add_child(properties_scroll);
VBoxContainer *properties_container = memnew(VBoxContainer);
properties_container->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
properties_scroll->add_child(properties_container);
properties_container->add_theme_constant_override("separation", 5);
properties_container->add_theme_constant_override("margin_left", 2);
properties_container->add_theme_constant_override("margin_right", 2);
properties_container->add_theme_constant_override("margin_top", 2);
properties_container->add_theme_constant_override("margin_bottom", 2);
int total_refs = d->extra_debug_data.has("ref_count") ? (uint64_t)d->extra_debug_data["ref_count"] : 0;
int objectdb_refs = d->get_unique_inbound_references().size();
int native_refs = total_refs - objectdb_refs;
Array ref_cycles = (Array)d->extra_debug_data["ref_cycles"];
String count_str = "[ul]\n";
count_str += TTR(" Native References: ") + String::num_uint64(native_refs) + "\n";
count_str += TTR(" ObjectDB References: ") + String::num_uint64(objectdb_refs) + "\n";
count_str += TTR(" Total References: ") + String::num_uint64(total_refs) + "\n";
count_str += TTR(" ObjectDB Cycles: ") + String::num_uint64(ref_cycles.size()) + "\n";
count_str += "[/ul]\n";
RichTextLabel *counts = memnew(RichTextLabel(count_str));
counts->set_use_bbcode(true);
counts->set_fit_content(true);
counts->add_theme_constant_override("line_separation", 6);
properties_container->add_child(counts);
if (d->inbound_references.size() > 0) {
RichTextLabel *inbound_lbl = memnew(RichTextLabel(TTR("[center]ObjectDB References[center]")));
inbound_lbl->set_fit_content(true);
inbound_lbl->set_use_bbcode(true);
properties_container->add_child(inbound_lbl);
Tree *inbound_tree = memnew(Tree);
inbound_tree->set_hide_folding(true);
properties_container->add_child(inbound_tree);
inbound_tree->set_select_mode(Tree::SelectMode::SELECT_ROW);
inbound_tree->set_hide_root(true);
inbound_tree->set_columns(3);
inbound_tree->set_column_titles_visible(true);
inbound_tree->set_column_title(0, TTR("Source"));
inbound_tree->set_column_expand(0, true);
inbound_tree->set_column_clip_content(0, false);
inbound_tree->set_column_title_tooltip_text(0, TTR("Other object referencing this object"));
inbound_tree->set_column_title(1, TTR("Property"));
inbound_tree->set_column_expand(1, true);
inbound_tree->set_column_clip_content(1, true);
inbound_tree->set_column_title_tooltip_text(1, TTR("Property of other object referencing this object"));
inbound_tree->set_column_title(2, TTR("Duplicate?"));
inbound_tree->set_column_expand(2, false);
inbound_tree->set_column_title_tooltip_text(2, TTR("Was the same reference returned by multiple getters on the source object?"));
inbound_tree->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
inbound_tree->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
inbound_tree->set_v_scroll_enabled(false);
inbound_tree->connect(SceneStringName(item_selected), callable_mp(this, &SnapshotRefCountedView::_ref_selected).bind(inbound_tree));
// The same reference can exist as multiple properties of an object (for example, gdscript `@export` properties exist twice).
// We flag for the user if a property is exposed multiple times so it's clearer why there are more references in the list
// than the ObjectDB References count would suggest.
HashMap<ObjectID, int> property_repeat_count;
for (const KeyValue<String, ObjectID> &ob : d->inbound_references) {
if (!property_repeat_count.has(ob.value)) {
property_repeat_count.insert(ob.value, 0);
}
property_repeat_count[ob.value]++;
}
TreeItem *root = inbound_tree->create_item();
for (const KeyValue<String, ObjectID> &ob : d->inbound_references) {
TreeItem *i = inbound_tree->create_item(root);
SnapshotDataObject *target = d->snapshot->objects[ob.value];
i->set_text(0, target->get_name());
i->set_text(1, ob.key);
i->set_text(2, property_repeat_count[ob.value] > 1 ? TTR("Yes") : TTR("No"));
reference_item_map[i] = data_item_map[target];
}
}
if (ref_cycles.size() > 0) {
properties_container->add_child(memnew(SpanningHeader(TTR("ObjectDB Cycles"))));
Tree *cycles_tree = memnew(Tree);
cycles_tree->set_hide_folding(true);
properties_container->add_child(cycles_tree);
cycles_tree->set_select_mode(Tree::SelectMode::SELECT_ROW);
cycles_tree->set_hide_root(true);
cycles_tree->set_columns(1);
cycles_tree->set_column_titles_visible(false);
cycles_tree->set_column_expand(0, true);
cycles_tree->set_column_clip_content(0, false);
cycles_tree->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
cycles_tree->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
cycles_tree->set_v_scroll_enabled(false);
TreeItem *root = cycles_tree->create_item();
for (const Variant &cycle : ref_cycles) {
TreeItem *i = cycles_tree->create_item(root);
i->set_text(0, cycle);
i->set_text_overrun_behavior(0, TextServer::OverrunBehavior::OVERRUN_NO_TRIMMING);
}
}
}
void SnapshotRefCountedView::_ref_selected(Tree *p_source_tree) {
TreeItem *target = reference_item_map[p_source_tree->get_selected()];
if (target) {
if (!target->is_visible()) {
// Clear the filter if we can't see the node we just chose.
filter_bar->clear_filter();
}
target->get_tree()->deselect_all();
target->get_tree()->set_selected(target);
target->get_tree()->ensure_cursor_is_visible();
}
}
@@ -0,0 +1,61 @@
/**************************************************************************/
/* refcounted_view.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "../snapshot_data.h"
#include "shared_controls.h"
#include "snapshot_view.h"
class Tree;
class HSplitContainer;
class SnapshotRefCountedView : public SnapshotView {
GDCLASS(SnapshotRefCountedView, SnapshotView);
protected:
Tree *refs_list = nullptr;
VBoxContainer *ref_details = nullptr;
TreeSortAndFilterBar *filter_bar = nullptr;
HSplitContainer *refs_view = nullptr;
HashMap<TreeItem *, SnapshotDataObject *> item_data_map;
HashMap<SnapshotDataObject *, TreeItem *> data_item_map;
HashMap<TreeItem *, TreeItem *> reference_item_map;
void _refcounted_selected();
void _insert_data(GameStateSnapshot *p_snapshot, const String &p_name);
void _ref_selected(Tree *p_source_tree);
void _set_split_to_center();
public:
SnapshotRefCountedView();
virtual void show_snapshot(GameStateSnapshot *p_data, GameStateSnapshot *p_diff_data) override;
};
@@ -0,0 +1,248 @@
/**************************************************************************/
/* shared_controls.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#include "shared_controls.h"
#include "editor/editor_node.h"
#include "editor/editor_string_names.h"
#include "editor/themes/editor_scale.h"
#include "scene/gui/label.h"
#include "scene/gui/menu_button.h"
#include "scene/resources/style_box_flat.h"
SpanningHeader::SpanningHeader(const String &p_text) {
Ref<StyleBoxFlat> title_sbf;
title_sbf.instantiate();
title_sbf->set_bg_color(EditorNode::get_singleton()->get_editor_theme()->get_color("dark_color_3", "Editor"));
add_theme_style_override(SceneStringName(panel), title_sbf);
set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
Label *title = memnew(Label(p_text));
add_child(title);
title->set_horizontal_alignment(HorizontalAlignment::HORIZONTAL_ALIGNMENT_CENTER);
title->set_vertical_alignment(VerticalAlignment::VERTICAL_ALIGNMENT_CENTER);
}
DarkPanelContainer::DarkPanelContainer() {
set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
Ref<StyleBoxFlat> content_wrapper_sbf;
content_wrapper_sbf.instantiate();
content_wrapper_sbf->set_bg_color(EditorNode::get_singleton()->get_editor_theme()->get_color("dark_color_2", "Editor"));
add_theme_style_override("panel", content_wrapper_sbf);
}
void TreeSortAndFilterBar::_apply_filter(TreeItem *p_current_node) {
if (!p_current_node) {
p_current_node = managed_tree->get_root();
}
if (!p_current_node) {
return;
}
// Reset ourself to default state.
p_current_node->set_visible(true);
p_current_node->clear_custom_color(0);
// Go through each child and filter them.
bool any_child_visible = false;
for (TreeItem *child = p_current_node->get_first_child(); child; child = child->get_next()) {
_apply_filter(child);
if (child->is_visible()) {
any_child_visible = true;
}
}
// Check if we match the filter.
String filter_str = filter_edit->get_text().strip_edges(true, true).to_lower();
// We are visible.
bool matches_filter = false;
for (int i = 0; i < managed_tree->get_columns(); i++) {
if (p_current_node->get_text(i).to_lower().contains(filter_str)) {
matches_filter = true;
break;
}
}
if (matches_filter || filter_str.is_empty()) {
p_current_node->set_visible(true);
} else if (any_child_visible) {
// We have a visible child.
p_current_node->set_custom_color(0, get_theme_color(SNAME("font_disabled_color"), EditorStringName(Editor)));
} else {
// We and out children aren't visible.
p_current_node->set_visible(false);
}
}
void TreeSortAndFilterBar::_apply_sort() {
if (!sort_button->is_visible()) {
return;
}
for (int i = 0; i != sort_button->get_popup()->get_item_count(); i++) {
// Update the popup buttons to be checked/unchecked.
sort_button->get_popup()->set_item_checked(i, (i == (int)current_sort));
}
SortItem sort = sort_items[current_sort];
List<TreeItem *> items_to_sort;
items_to_sort.push_back(managed_tree->get_root());
while (items_to_sort.size() > 0) {
TreeItem *to_sort = items_to_sort.front()->get();
items_to_sort.pop_front();
List<TreeItemColumn> items;
for (int i = 0; i < to_sort->get_child_count(); i++) {
items.push_back(TreeItemColumn(to_sort->get_child(i), sort.column));
}
if (sort.type == ALPHA_SORT && sort.ascending == true) {
items.sort_custom<TreeItemAlphaComparator>();
}
if (sort.type == ALPHA_SORT && sort.ascending == false) {
items.sort_custom<TreeItemAlphaComparator>();
items.reverse();
}
if (sort.type == NUMERIC_SORT && sort.ascending == true) {
items.sort_custom<TreeItemNumericComparator>();
}
if (sort.type == NUMERIC_SORT && sort.ascending == false) {
items.sort_custom<TreeItemNumericComparator>();
items.reverse();
}
TreeItem *previous = nullptr;
for (const TreeItemColumn &item : items) {
if (previous != nullptr) {
item.item->move_after(previous);
} else {
item.item->move_before(to_sort->get_first_child());
}
previous = item.item;
items_to_sort.push_back(item.item);
}
}
}
void TreeSortAndFilterBar::_sort_changed(int p_id) {
current_sort = p_id;
_apply_sort();
}
void TreeSortAndFilterBar::_filter_changed(const String &p_filter) {
_apply_filter();
}
TreeSortAndFilterBar::TreeSortAndFilterBar(Tree *p_managed_tree, const String &p_filter_placeholder_text) :
managed_tree(p_managed_tree) {
set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
add_theme_constant_override("h_separation", 10 * EDSCALE);
filter_edit = memnew(LineEdit);
filter_edit->set_clear_button_enabled(true);
filter_edit->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
filter_edit->set_placeholder(p_filter_placeholder_text);
add_child(filter_edit);
filter_edit->connect(SceneStringName(text_changed), callable_mp(this, &TreeSortAndFilterBar::_filter_changed));
sort_button = memnew(MenuButton);
sort_button->set_visible(false);
sort_button->set_flat(false);
sort_button->set_theme_type_variation("FlatMenuButton");
PopupMenu *p = sort_button->get_popup();
p->connect(SceneStringName(id_pressed), callable_mp(this, &TreeSortAndFilterBar::_sort_changed));
add_child(sort_button);
}
void TreeSortAndFilterBar::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_POSTINITIALIZE:
case NOTIFICATION_ENTER_TREE:
case NOTIFICATION_LAYOUT_DIRECTION_CHANGED:
case NOTIFICATION_THEME_CHANGED:
case NOTIFICATION_TRANSLATION_CHANGED: {
filter_edit->set_right_icon(get_editor_theme_icon(SNAME("Search")));
sort_button->set_button_icon(get_editor_theme_icon(SNAME("Sort")));
apply();
} break;
}
}
TreeSortAndFilterBar::SortOptionIndexes TreeSortAndFilterBar::add_sort_option(const String &p_new_option, SortType p_sort_type, int p_sort_column, bool p_is_default) {
sort_button->set_visible(true);
bool is_first_item = sort_items.is_empty();
SortItem item_ascending(sort_items.size(), TTR("Sort By ") + p_new_option + TTR(" (Ascending)"), p_sort_type, true, p_sort_column);
sort_items[item_ascending.id] = item_ascending;
sort_button->get_popup()->add_radio_check_item(item_ascending.label, item_ascending.id);
SortItem item_descending(sort_items.size(), TTR("Sort By ") + p_new_option + TTR(" (Descending)"), p_sort_type, false, p_sort_column);
sort_items[item_descending.id] = item_descending;
sort_button->get_popup()->add_radio_check_item(item_descending.label, item_descending.id);
if (is_first_item) {
sort_button->get_popup()->set_item_checked(0, true);
}
SortOptionIndexes indexes;
indexes.ascending = item_ascending.id;
indexes.descending = item_descending.id;
return indexes;
}
void TreeSortAndFilterBar::clear_filter() {
filter_edit->clear();
}
void TreeSortAndFilterBar::clear() {
sort_button->set_visible(false);
sort_button->get_popup()->clear();
filter_edit->clear();
}
void TreeSortAndFilterBar::select_sort(int p_item_id) {
_sort_changed(p_item_id);
}
void TreeSortAndFilterBar::apply() {
if (!managed_tree || !managed_tree->get_root()) {
return;
}
OS::get_singleton()->benchmark_begin_measure("odb profiler", "TreeSortAndFilterBar::apply _apply_sort");
_apply_sort();
OS::get_singleton()->benchmark_end_measure("odb profiler", "TreeSortAndFilterBar::apply _apply_sort");
OS::get_singleton()->benchmark_begin_measure("odb profiler", "TreeSortAndFilterBar::apply _apply_filter");
_apply_filter();
OS::get_singleton()->benchmark_end_measure("odb profiler", "TreeSortAndFilterBar::apply _apply_filter");
}
@@ -0,0 +1,127 @@
/**************************************************************************/
/* shared_controls.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "scene/gui/box_container.h"
#include "scene/gui/line_edit.h"
#include "scene/gui/panel_container.h"
#include "scene/gui/tree.h"
class MenuButton;
class SpanningHeader : public PanelContainer {
GDCLASS(SpanningHeader, PanelContainer);
public:
SpanningHeader(const String &p_text);
};
class DarkPanelContainer : public PanelContainer {
GDCLASS(DarkPanelContainer, PanelContainer);
public:
DarkPanelContainer();
};
// Utility class that creates a filter text box and a sort menu.
// Takes a reference to a tree and applies the sort and filter to the tree.
class TreeSortAndFilterBar : public HBoxContainer {
GDCLASS(TreeSortAndFilterBar, HBoxContainer);
public:
// The ways a column can be sorted, either alphabetically or numerically.
enum SortType {
NUMERIC_SORT = 0,
ALPHA_SORT,
SORT_TYPE_MAX
};
// Returned when a new sort is added. Each new sort can be either ascending or descending,
// so we return the index of each sort option.
struct SortOptionIndexes {
int ascending;
int descending;
};
protected:
// Context needed to sort the tree in a certain way.
// Combines a sort type, the column to apply it, and if it's ascending or descending.
struct SortItem {
SortItem() {}
SortItem(int p_id, const String &p_label, SortType p_type, bool p_ascending, int p_column) :
id(p_id), label(p_label), type(p_type), ascending(p_ascending), column(p_column) {}
int id = 0;
String label;
SortType type = SortType::NUMERIC_SORT;
bool ascending = false;
int column = 0;
};
struct TreeItemColumn {
TreeItemColumn() {}
TreeItemColumn(TreeItem *p_item, int p_column) :
item(p_item), column(p_column) {}
TreeItem *item = nullptr;
int column;
};
struct TreeItemAlphaComparator {
bool operator()(const TreeItemColumn &p_a, const TreeItemColumn &p_b) const {
return NoCaseComparator()(p_a.item->get_text(p_a.column), p_b.item->get_text(p_b.column));
}
};
struct TreeItemNumericComparator {
bool operator()(const TreeItemColumn &p_a, const TreeItemColumn &p_b) const {
return p_a.item->get_text(p_a.column).to_int() < p_b.item->get_text(p_b.column).to_int();
}
};
LineEdit *filter_edit = nullptr;
MenuButton *sort_button = nullptr;
Tree *managed_tree = nullptr;
HashMap<int, SortItem> sort_items;
int current_sort = 0;
void _apply_filter(TreeItem *p_current_node = nullptr);
void _apply_sort();
void _sort_changed(int p_id);
void _filter_changed(const String &p_filter);
public:
TreeSortAndFilterBar(Tree *p_managed_tree, const String &p_filter_placeholder_text);
void _notification(int p_what);
SortOptionIndexes add_sort_option(const String &p_new_option, SortType p_sort_type, int p_sort_column, bool p_is_default = false);
void clear_filter();
void clear();
void select_sort(int p_item_id);
void apply();
};
@@ -0,0 +1,70 @@
/**************************************************************************/
/* snapshot_view.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#include "snapshot_view.h"
#include "scene/gui/label.h"
#include "scene/gui/rich_text_label.h"
#include "scene/gui/tree.h"
void SnapshotView::clear_snapshot() {
snapshot_data = nullptr;
diff_data = nullptr;
for (int i = 0; i < get_child_count(); i++) {
get_child(i)->queue_free();
}
}
void SnapshotView::show_snapshot(GameStateSnapshot *p_data, GameStateSnapshot *p_diff_data) {
clear_snapshot();
snapshot_data = p_data;
diff_data = p_diff_data;
}
bool SnapshotView::is_showing_snapshot(GameStateSnapshot *p_data, GameStateSnapshot *p_diff_data) {
return p_data == snapshot_data && p_diff_data == diff_data;
}
List<TreeItem *> SnapshotView::_get_children_recursive(Tree *p_tree) {
List<TreeItem *> found_items;
List<TreeItem *> items_to_check;
if (p_tree && p_tree->get_root()) {
items_to_check.push_back(p_tree->get_root());
}
while (items_to_check.size() > 0) {
TreeItem *to_check = items_to_check.front()->get();
items_to_check.pop_front();
found_items.push_back(to_check);
for (int i = 0; i < to_check->get_child_count(); i++) {
items_to_check.push_back(to_check->get_child(i));
}
}
return found_items;
}
@@ -0,0 +1,54 @@
/**************************************************************************/
/* snapshot_view.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "../snapshot_data.h"
#include "scene/gui/control.h"
class Tree;
class TreeItem;
class SnapshotView : public Control {
GDCLASS(SnapshotView, Control);
protected:
GameStateSnapshot *snapshot_data = nullptr;
GameStateSnapshot *diff_data = nullptr;
List<TreeItem *> _get_children_recursive(Tree *p_tree);
public:
String view_name;
virtual void show_snapshot(GameStateSnapshot *p_data, GameStateSnapshot *p_diff_data = nullptr);
virtual void clear_snapshot();
bool is_showing_snapshot(GameStateSnapshot *p_data, GameStateSnapshot *p_diff_data);
};
@@ -0,0 +1,282 @@
/**************************************************************************/
/* summary_view.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#include "summary_view.h"
#include "core/os/time.h"
#include "editor/editor_node.h"
#include "scene/gui/center_container.h"
#include "scene/gui/label.h"
#include "scene/gui/panel_container.h"
#include "scene/gui/rich_text_label.h"
#include "scene/resources/style_box_flat.h"
SnapshotSummaryView::SnapshotSummaryView() {
set_name("Summary");
set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
MarginContainer *mc = memnew(MarginContainer);
mc->add_theme_constant_override("margin_left", 5);
mc->add_theme_constant_override("margin_right", 5);
mc->add_theme_constant_override("margin_top", 5);
mc->add_theme_constant_override("margin_bottom", 5);
mc->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);
PanelContainer *content_wrapper = memnew(PanelContainer);
content_wrapper->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);
Ref<StyleBoxFlat> content_wrapper_sbf;
content_wrapper_sbf.instantiate();
content_wrapper_sbf->set_bg_color(EditorNode::get_singleton()->get_editor_theme()->get_color("dark_color_2", "Editor"));
content_wrapper->add_theme_style_override(SceneStringName(panel), content_wrapper_sbf);
content_wrapper->add_child(mc);
add_child(content_wrapper);
VBoxContainer *content = memnew(VBoxContainer);
mc->add_child(content);
content->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);
PanelContainer *pc = memnew(PanelContainer);
Ref<StyleBoxFlat> sbf;
sbf.instantiate();
sbf->set_bg_color(EditorNode::get_singleton()->get_editor_theme()->get_color("dark_color_3", "Editor"));
pc->add_theme_style_override("panel", sbf);
content->add_child(pc);
pc->set_anchors_preset(LayoutPreset::PRESET_TOP_WIDE);
Label *title = memnew(Label(TTR("ObjectDB Snapshot Summary")));
pc->add_child(title);
title->set_horizontal_alignment(HorizontalAlignment::HORIZONTAL_ALIGNMENT_CENTER);
title->set_vertical_alignment(VerticalAlignment::VERTICAL_ALIGNMENT_CENTER);
explainer_text = memnew(CenterContainer);
explainer_text->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
explainer_text->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
content->add_child(explainer_text);
VBoxContainer *explainer_lines = memnew(VBoxContainer);
explainer_text->add_child(explainer_lines);
Label *l1 = memnew(Label(TTR("Press 'Take ObjectDB Snapshot' to snapshot the ObjectDB.")));
Label *l2 = memnew(Label(TTR("Memory in Godot is either owned natively by the engine or owned by the ObjectDB.")));
Label *l3 = memnew(Label(TTR("ObjectDB Snapshots capture only memory owned by the ObjectDB.")));
l1->set_horizontal_alignment(HorizontalAlignment::HORIZONTAL_ALIGNMENT_CENTER);
l2->set_horizontal_alignment(HorizontalAlignment::HORIZONTAL_ALIGNMENT_CENTER);
l3->set_horizontal_alignment(HorizontalAlignment::HORIZONTAL_ALIGNMENT_CENTER);
explainer_lines->add_child(l1);
explainer_lines->add_child(l2);
explainer_lines->add_child(l3);
ScrollContainer *sc = memnew(ScrollContainer);
sc->set_anchors_preset(LayoutPreset::PRESET_FULL_RECT);
sc->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
sc->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
content->add_child(sc);
blurb_list = memnew(VBoxContainer);
sc->add_child(blurb_list);
blurb_list->set_v_size_flags(SizeFlags::SIZE_EXPAND_FILL);
blurb_list->set_h_size_flags(SizeFlags::SIZE_EXPAND_FILL);
}
void SnapshotSummaryView::show_snapshot(GameStateSnapshot *p_data, GameStateSnapshot *p_diff_data) {
SnapshotView::show_snapshot(p_data, p_diff_data);
explainer_text->set_visible(false);
String snapshot_a_name = diff_data == nullptr ? TTR("Snapshot") : TTR("Snapshot A");
String snapshot_b_name = TTR("Snapshot B");
_push_overview_blurb(snapshot_a_name + TTR(" Overview"), snapshot_data);
if (diff_data) {
_push_overview_blurb(snapshot_b_name + TTR(" Overview"), diff_data);
}
_push_node_blurb(snapshot_a_name + TTR(" Nodes"), snapshot_data);
if (diff_data) {
_push_node_blurb(snapshot_b_name + TTR(" Nodes"), diff_data);
}
_push_refcounted_blurb(snapshot_a_name + TTR(" RefCounteds"), snapshot_data);
if (diff_data) {
_push_refcounted_blurb(snapshot_b_name + TTR(" RefCounteds"), diff_data);
}
_push_object_blurb(snapshot_a_name + TTR(" Objects"), snapshot_data);
if (diff_data) {
_push_object_blurb(snapshot_b_name + TTR(" Objects"), diff_data);
}
}
void SnapshotSummaryView::clear_snapshot() {
// Just clear out the blurbs and leave the explainer.
for (int i = 0; i < blurb_list->get_child_count(); i++) {
blurb_list->get_child(i)->queue_free();
}
snapshot_data = nullptr;
diff_data = nullptr;
explainer_text->set_visible(true);
}
SummaryBlurb::SummaryBlurb(const String &p_title, const String &p_rtl_content) {
add_theme_constant_override("margin_left", 2);
add_theme_constant_override("margin_right", 2);
add_theme_constant_override("margin_top", 2);
add_theme_constant_override("margin_bottom", 2);
label = memnew(RichTextLabel);
label->add_theme_constant_override(SceneStringName(line_separation), 6);
label->set_fit_content(true);
label->set_use_bbcode(true);
label->add_newline();
label->push_bold();
label->add_text(p_title);
label->pop();
label->add_newline();
label->add_newline();
label->append_text(p_rtl_content);
add_child(label);
}
void SnapshotSummaryView::_push_overview_blurb(const String &p_title, GameStateSnapshot *p_snapshot) {
String c = "";
c += "[ul]\n";
c += TTR(" [i]Name:[/i] ") + p_snapshot->name + "\n";
if (p_snapshot->snapshot_context.has("timestamp")) {
c += TTR(" [i]Timestamp:[/i] ") + Time::get_singleton()->get_datetime_string_from_unix_time((double)p_snapshot->snapshot_context["timestamp"]) + "\n";
}
if (p_snapshot->snapshot_context.has("game_version")) {
c += TTR(" [i]Game Version:[/i] ") + (String)p_snapshot->snapshot_context["game_version"] + "\n";
}
if (p_snapshot->snapshot_context.has("editor_version")) {
c += TTR(" [i]Editor Version:[/i] ") + (String)p_snapshot->snapshot_context["editor_version"] + "\n";
}
double bytes_to_mb = 0.000001;
if (p_snapshot->snapshot_context.has("mem_usage")) {
c += TTR(" [i]Memory Used:[/i] ") + String::num((double)((uint64_t)p_snapshot->snapshot_context["mem_usage"]) * bytes_to_mb, 3) + " MB\n";
}
if (p_snapshot->snapshot_context.has("mem_max_usage")) {
c += TTR(" [i]Max Memory Used:[/i] ") + String::num((double)((uint64_t)p_snapshot->snapshot_context["mem_max_usage"]) * bytes_to_mb, 3) + " MB\n";
}
if (p_snapshot->snapshot_context.has("mem_available")) {
// I'm guessing pretty hard about what this is supposed to be. It's hard coded to be -1 cast to a uint64_t in Memory.h,
// so it _could_ be checking if we're on a 64 bit system, I think...
c += TTR(" [i]Max uint64 value:[/i] ") + String::num_uint64((uint64_t)p_snapshot->snapshot_context["mem_available"]) + "\n";
}
c += TTR(" [i]Total Objects:[/i] ") + itos(p_snapshot->objects.size()) + "\n";
int node_count = 0;
for (const KeyValue<ObjectID, SnapshotDataObject *> &pair : p_snapshot->objects) {
if (pair.value->is_node()) {
node_count++;
}
}
c += TTR(" [i]Total Nodes:[/i] ") + itos(node_count) + "\n";
c += "[/ul]\n";
blurb_list->add_child(memnew(SummaryBlurb(p_title, c)));
}
void SnapshotSummaryView::_push_node_blurb(const String &p_title, GameStateSnapshot *p_snapshot) {
List<String> nodes;
for (const KeyValue<ObjectID, SnapshotDataObject *> &pair : p_snapshot->objects) {
// if it's a node AND it doesn't have a parent node
if (pair.value->is_node() && !pair.value->extra_debug_data.has("node_parent") && pair.value->extra_debug_data.has("node_is_scene_root") && !pair.value->extra_debug_data["node_is_scene_root"]) {
const String &node_name = pair.value->extra_debug_data["node_name"];
nodes.push_back(node_name != "" ? node_name : pair.value->get_name());
}
}
if (nodes.size() <= 1) {
return;
}
String c = TTR("Multiple root nodes [i](possible call to 'remove_child' without 'queue_free')[/i]\n");
c += "[ul]\n";
for (const String &node : nodes) {
c += " " + node + "\n";
}
c += "[/ul]\n";
blurb_list->add_child(memnew(SummaryBlurb(p_title, c)));
}
void SnapshotSummaryView::_push_refcounted_blurb(const String &p_title, GameStateSnapshot *p_snapshot) {
List<String> rcs;
for (const KeyValue<ObjectID, SnapshotDataObject *> &pair : p_snapshot->objects) {
if (pair.value->is_refcounted()) {
int ref_count = (uint64_t)pair.value->extra_debug_data["ref_count"];
Array ref_cycles = (Array)pair.value->extra_debug_data["ref_cycles"];
if (ref_count == ref_cycles.size()) {
rcs.push_back(pair.value->get_name());
}
}
}
if (rcs.is_empty()) {
return;
}
String c = TTR("RefCounted objects only referenced in cycles [i](cycles often indicate a memory leaks)[/i]\n");
c += "[ul]\n";
for (const String &rc : rcs) {
c += " " + rc + "\n";
}
c += "[/ul]\n";
blurb_list->add_child(memnew(SummaryBlurb(p_title, c)));
}
void SnapshotSummaryView::_push_object_blurb(const String &p_title, GameStateSnapshot *p_snapshot) {
List<String> objects;
for (const KeyValue<ObjectID, SnapshotDataObject *> &pair : p_snapshot->objects) {
if (pair.value->inbound_references.is_empty() && pair.value->outbound_references.is_empty()) {
if (!pair.value->get_script().is_null()) {
// This blurb will have a lot of false positives, but we can at least suppress false positives
// from unreferenced nodes that are part of the scene tree.
if (pair.value->is_node() && (bool)pair.value->extra_debug_data["node_is_scene_root"]) {
objects.push_back(pair.value->get_name());
}
}
}
}
if (objects.is_empty()) {
return;
}
String c = TTR("Scripted objects not referenced by any other objects [i](unreferenced objects may indicate a memory leak)[/i]\n");
c += "[ul]\n";
for (const String &object : objects) {
c += " " + object + "\n";
}
c += "[/ul]\n";
blurb_list->add_child(memnew(SummaryBlurb(p_title, c)));
}
@@ -0,0 +1,66 @@
/**************************************************************************/
/* summary_view.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "../snapshot_data.h"
#include "scene/gui/margin_container.h"
#include "snapshot_view.h"
class CenterContainer;
class RichTextLabel;
class SummaryBlurb : public MarginContainer {
GDCLASS(SummaryBlurb, MarginContainer);
public:
RichTextLabel *label = nullptr;
SummaryBlurb(const String &p_title, const String &p_rtl_content);
};
class SnapshotSummaryView : public SnapshotView {
GDCLASS(SnapshotSummaryView, SnapshotView);
protected:
VBoxContainer *blurb_list = nullptr;
CenterContainer *explainer_text = nullptr;
void _push_overview_blurb(const String &p_title, GameStateSnapshot *p_snapshot);
void _push_node_blurb(const String &p_title, GameStateSnapshot *p_snapshot);
void _push_refcounted_blurb(const String &p_title, GameStateSnapshot *p_snapshot);
void _push_object_blurb(const String &p_title, GameStateSnapshot *p_snapshot);
public:
SnapshotSummaryView();
virtual void show_snapshot(GameStateSnapshot *p_data, GameStateSnapshot *p_diff_data) override;
virtual void clear_snapshot() override;
};