Skip to content

Commit

Permalink
Rework the way instance library items are exposed in the editor.
Browse files Browse the repository at this point in the history
This is mainly to workaround issues with the old approach that was
based on "emulated properties". Now it's a Blender-style custom list
where the selected item appears below.
An array was not possible because items need stable IDs.
  • Loading branch information
Zylann committed Aug 21, 2024
1 parent f610a08 commit e30c3be
Show file tree
Hide file tree
Showing 14 changed files with 684 additions and 225 deletions.
6 changes: 6 additions & 0 deletions doc/source/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ Semver is not yet in place, so each version can have breaking changes, although

Primarily developped with Godot 4.3.

- Fixes
- `VoxelInstanceLibrary`: Editor: reworked the way items are exposed as a Blender-style list. Now removing an item while the library is open as a sub-inspector is no longer problematic

- Breaking changes
- `VoxelInstanceLibrary`: Items should no longer be accessed using generated properties (`item1`, `item2` etc). Use `get_item` instead.


1.3 - 17/08/2024 - branch `1.3` - tag `v1.3.0`
----------------------------------------------
Expand Down
93 changes: 93 additions & 0 deletions editor/instance_library/control_sizer.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
#include "control_sizer.h"
#include "../../util/errors.h"
#include "../../util/godot/classes/input_event_mouse_button.h"
#include "../../util/godot/classes/input_event_mouse_motion.h"
#include "../../util/godot/core/input_enums.h"
#include "../../util/godot/editor_scale.h"
#include "../../util/math/funcs.h"

namespace zylann {

ZN_ControlSizer::ZN_ControlSizer() {
set_default_cursor_shape(Control::CURSOR_VSIZE);
const real_t editor_scale = EDSCALE;
set_custom_minimum_size(Vector2(0, editor_scale * 5));
}

void ZN_ControlSizer::set_target_control(Control *control) {
_target_control.set(control);
}

#ifdef ZN_GODOT
void ZN_ControlSizer::gui_input(const Ref<InputEvent> &p_event) {
#elif defined(ZN_GODOT_EXTENSION)
void ZN_ControlSizer::_gui_input(const Ref<InputEvent> &p_event) {
#endif

Ref<InputEventMouseButton> mb = p_event;
if (mb.is_valid()) {
Control *target_control = _target_control.get();
ZN_ASSERT_RETURN(target_control != nullptr);

if (mb->is_pressed()) {
if (mb->get_button_index() == ::godot::MOUSE_BUTTON_LEFT) {
_dragging = true;
}
} else {
_dragging = false;
}
}

Ref<InputEventMouseMotion> mm = p_event;
if (mm.is_valid()) {
if (_dragging) {
Control *target_control = _target_control.get();
ZN_ASSERT_RETURN(target_control != nullptr);

const Vector2 ms = target_control->get_custom_minimum_size();
// Assuming the UI is not scaled
const Vector2 rel = mm->get_relative();
// Assuming vertical for now
// TODO Clamp min_size to `target.get_minimum_size()`?
target_control->set_custom_minimum_size(Vector2(ms.x, math::clamp(ms.y + rel.y, _min_size, _max_size)));
}
}
}

void ZN_ControlSizer::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_MOUSE_ENTER: {
_mouse_inside = true;
queue_redraw();
} break;

case NOTIFICATION_MOUSE_EXIT: {
_mouse_inside = false;
queue_redraw();
} break;

case NOTIFICATION_ENTER_TREE:
cache_theme();
break;

case NOTIFICATION_THEME_CHANGED:
cache_theme();
break;

case NOTIFICATION_DRAW: {
if (_dragging || _mouse_inside) {
draw_texture(_hover_icon, (get_size() - _hover_icon->get_size()) / 2);
}
} break;
}
}

void ZN_ControlSizer::cache_theme() {
// TODO I'd like to cache this, but `BIND_THEME_ITEM_CUSTOM` is not exposed to GDExtension...
// TODO Have a framework-level StringName cache singleton
_hover_icon = get_theme_icon("v_grabber", "SplitContainer");
}

void ZN_ControlSizer::_bind_methods() {}

} // namespace zylann
40 changes: 40 additions & 0 deletions editor/instance_library/control_sizer.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#ifndef ZN_GODOT_CONTROL_SIZER_H
#define ZN_GODOT_CONTROL_SIZER_H

#include "../../util/godot/classes/control.h"
#include "../../util/godot/object_weak_ref.h"

namespace zylann {

// Implements similar logic as the middle resizing handle of SplitContainer, but works on a target control instead
class ZN_ControlSizer : public Control {
GDCLASS(ZN_ControlSizer, Control)
public:
ZN_ControlSizer();

void set_target_control(Control *control);

#ifdef ZN_GODOT
void gui_input(const Ref<InputEvent> &p_event) override;
#elif defined(ZN_GODOT_EXTENSION)
void _gui_input(const Ref<InputEvent> &p_event) override;
#endif

private:
static void _bind_methods();

void _notification(int p_what);

void cache_theme();

zylann::godot::ObjectWeakRef<Control> _target_control;
bool _dragging = false;
bool _mouse_inside = false;
float _min_size = 10.0;
float _max_size = 1000.0;
Ref<Texture2D> _hover_icon;
};

} // namespace zylann

#endif // ZN_GODOT_CONTROL_SIZER_H
168 changes: 8 additions & 160 deletions editor/instance_library/voxel_instance_library_editor_plugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,28 +19,12 @@ VoxelInstanceLibraryEditorPlugin::VoxelInstanceLibraryEditorPlugin() {}

// TODO GDX: Can't initialize EditorPlugins in their constructor when they access EditorNode.
// See https://github.com/godotengine/godot-cpp/issues/1179
void VoxelInstanceLibraryEditorPlugin::init() {
Control *base_control = get_editor_interface()->get_base_control();
void VoxelInstanceLibraryEditorPlugin::init() {}

_confirmation_dialog = memnew(ConfirmationDialog);
_confirmation_dialog->connect(
"confirmed", callable_mp(this, &VoxelInstanceLibraryEditorPlugin::_on_remove_item_confirmed)
);
base_control->add_child(_confirmation_dialog);

_info_dialog = memnew(AcceptDialog);
base_control->add_child(_info_dialog);

_open_scene_dialog = memnew(EditorFileDialog);
PackedStringArray extensions = godot::get_recognized_extensions_for_type(PackedScene::get_class_static());
for (int i = 0; i < extensions.size(); ++i) {
_open_scene_dialog->add_filter("*." + extensions[i]);
}
_open_scene_dialog->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_FILE);
base_control->add_child(_open_scene_dialog);
_open_scene_dialog->connect(
"file_selected", callable_mp(this, &VoxelInstanceLibraryEditorPlugin::_on_open_scene_dialog_file_selected)
);
EditorUndoRedoManager &VoxelInstanceLibraryEditorPlugin::get_undo_redo2() {
EditorUndoRedoManager *ur = get_undo_redo();
ZN_ASSERT(ur != nullptr);
return *ur;
}

bool VoxelInstanceLibraryEditorPlugin::_zn_handles(const Object *p_object) const {
Expand All @@ -49,8 +33,8 @@ bool VoxelInstanceLibraryEditorPlugin::_zn_handles(const Object *p_object) const
}

void VoxelInstanceLibraryEditorPlugin::_zn_edit(Object *p_object) {
VoxelInstanceLibrary *lib = Object::cast_to<VoxelInstanceLibrary>(p_object);
_library.reference_ptr(lib);
// VoxelInstanceLibrary *lib = Object::cast_to<VoxelInstanceLibrary>(p_object);
// _library.reference_ptr(lib);
}

void VoxelInstanceLibraryEditorPlugin::_notification(int p_what) {
Expand All @@ -59,7 +43,7 @@ void VoxelInstanceLibraryEditorPlugin::_notification(int p_what) {

Control *base_control = get_editor_interface()->get_base_control();
_inspector_plugin.instantiate();
_inspector_plugin->button_listener = this;
_inspector_plugin->plugin = this;
_inspector_plugin->icon_provider = base_control;
// TODO Why can other Godot plugins do this in the constructor??
// I found I could not put this in the constructor,
Expand All @@ -71,142 +55,6 @@ void VoxelInstanceLibraryEditorPlugin::_notification(int p_what) {
}
}

void VoxelInstanceLibraryEditorPlugin::_on_add_item_button_pressed(int id) {
_on_button_pressed(id);
}

void VoxelInstanceLibraryEditorPlugin::_on_remove_item_button_pressed() {
_on_button_pressed(VoxelInstanceLibraryInspectorPlugin::BUTTON_REMOVE_ITEM);
}

void VoxelInstanceLibraryEditorPlugin::_on_button_pressed(int id) {
_last_used_button = id;

switch (id) {
case VoxelInstanceLibraryInspectorPlugin::BUTTON_ADD_MULTIMESH_ITEM: {
ERR_FAIL_COND(_library.is_null());

Ref<VoxelInstanceLibraryMultiMeshItem> item;
item.instantiate();
// Setup some defaults
Ref<BoxMesh> mesh;
mesh.instantiate();
item->set_mesh(mesh, 0);

// We could decide to use a different default here if we can detect that the instancer the library is used
// into is child of a terrain with LOD or no LOD. At the very least it should always be 0 if there is no LOD
// support, otherwise things look broken. 0 is the default.
// item->set_lod_index(2);

Ref<VoxelInstanceGenerator> generator;
generator.instantiate();
item->set_generator(generator);

const int item_id = _library->get_next_available_id();

EditorUndoRedoManager &ur = *get_undo_redo();
ur.create_action("Add multimesh item");
ur.add_do_method(*_library, "add_item", item_id, item);
ur.add_undo_method(*_library, "remove_item", item_id);
ur.commit_action();
} break;

case VoxelInstanceLibraryInspectorPlugin::BUTTON_ADD_SCENE_ITEM: {
_open_scene_dialog->popup_centered_ratio();
} break;

case VoxelInstanceLibraryInspectorPlugin::BUTTON_REMOVE_ITEM: {
ERR_FAIL_COND(_library.is_null());
const int item_id = try_get_selected_item_id();
if (item_id != -1) {
_item_id_to_remove = item_id;
_confirmation_dialog->set_text(ZN_TTR("Remove item {0}?").format(varray(_item_id_to_remove)));
_confirmation_dialog->popup_centered();
}
} break;

default:
ERR_PRINT("Unknown menu item");
break;
}
}

// TODO This function does not modify anything, but cannot be `const` because get_editor_interface() is not...
int VoxelInstanceLibraryEditorPlugin::try_get_selected_item_id() {
String path = get_editor_interface()->get_inspector()->get_selected_path();
String prefix = "item_";

if (path.begins_with(prefix)) {
const int id = path.substr(prefix.length()).to_int();
return id;

} else {
// The inspector won't let us know which item the current resource is stored into...
// Gridmap works because it simulates every item as properties of the library,
// but our current resource does not do that,
// and I don't want to modify the API just because the built-in inspector is bad.
_info_dialog->set_text(
ZN_TTR(String("Could not determine selected item from property path: `{0}`.\n"
"You must select the `item_X` property label of the item you want to remove."))
.format(varray(path))
);
_info_dialog->popup_centered();
return -1;
}
}

void VoxelInstanceLibraryEditorPlugin::_on_remove_item_confirmed() {
ERR_FAIL_COND(_library.is_null());
ERR_FAIL_COND(_item_id_to_remove == -1);

Ref<VoxelInstanceLibraryItem> item = _library->get_item(_item_id_to_remove);

EditorUndoRedoManager &ur = *get_undo_redo();
ur.create_action("Remove item");
ur.add_do_method(*_library, "remove_item", _item_id_to_remove);
ur.add_undo_method(*_library, "add_item", _item_id_to_remove, item);
ur.commit_action();

_item_id_to_remove = -1;
}

void VoxelInstanceLibraryEditorPlugin::_on_open_scene_dialog_file_selected(String fpath) {
switch (_last_used_button) {
case VoxelInstanceLibraryInspectorPlugin::BUTTON_ADD_SCENE_ITEM:
add_scene_item(fpath);
break;

default:
ERR_PRINT("Invalid menu option");
break;
}
}

void VoxelInstanceLibraryEditorPlugin::add_scene_item(String fpath) {
ERR_FAIL_COND(_library.is_null());

Ref<PackedScene> scene = godot::load_resource(fpath);
ERR_FAIL_COND(scene.is_null());

Ref<VoxelInstanceLibrarySceneItem> item;
item.instantiate();
// Setup some defaults
item->set_lod_index(2);
item->set_scene(scene);
Ref<VoxelInstanceGenerator> generator;
generator.instantiate();
generator->set_density(0.01f); // Low density for scenes because that's heavier
item->set_generator(generator);

const int item_id = _library->get_next_available_id();

EditorUndoRedoManager &ur = *get_undo_redo();
ur.create_action("Add scene item");
ur.add_do_method(_library.ptr(), "add_item", item_id, item);
ur.add_undo_method(_library.ptr(), "remove_item", item_id);
ur.commit_action();
}

void VoxelInstanceLibraryEditorPlugin::_bind_methods() {}

} // namespace zylann::voxel
20 changes: 2 additions & 18 deletions editor/instance_library/voxel_instance_library_editor_plugin.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ class VoxelInstanceLibraryEditorPlugin : public zylann::godot::ZN_EditorPlugin {

VoxelInstanceLibraryEditorPlugin();

void _on_add_item_button_pressed(int id);
void _on_remove_item_button_pressed();
// Because this is protected in the base class when compiling as a module
EditorUndoRedoManager &get_undo_redo2();

protected:
bool _zn_handles(const Object *p_object) const override;
Expand All @@ -35,24 +35,8 @@ class VoxelInstanceLibraryEditorPlugin : public zylann::godot::ZN_EditorPlugin {
void init();
void _notification(int p_what);

int try_get_selected_item_id();
void add_scene_item(String fpath);

void _on_remove_item_confirmed();
void _on_open_scene_dialog_file_selected(String fpath);

void _on_button_pressed(int id);

static void _bind_methods();

ConfirmationDialog *_confirmation_dialog = nullptr;
AcceptDialog *_info_dialog = nullptr;
int _item_id_to_remove = -1;
int _item_id_to_update = -1;
EditorFileDialog *_open_scene_dialog;
int _last_used_button;

Ref<VoxelInstanceLibrary> _library;
Ref<VoxelInstanceLibraryInspectorPlugin> _inspector_plugin;
};

Expand Down
Loading

0 comments on commit e30c3be

Please sign in to comment.