Skip to content

Commit e30c3be

Browse files
committed
Rework the way instance library items are exposed in the editor.
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.
1 parent f610a08 commit e30c3be

14 files changed

+684
-225
lines changed

doc/source/changelog.md

+6
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ Semver is not yet in place, so each version can have breaking changes, although
1212

1313
Primarily developped with Godot 4.3.
1414

15+
- Fixes
16+
- `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
17+
18+
- Breaking changes
19+
- `VoxelInstanceLibrary`: Items should no longer be accessed using generated properties (`item1`, `item2` etc). Use `get_item` instead.
20+
1521

1622
1.3 - 17/08/2024 - branch `1.3` - tag `v1.3.0`
1723
----------------------------------------------
+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
#include "control_sizer.h"
2+
#include "../../util/errors.h"
3+
#include "../../util/godot/classes/input_event_mouse_button.h"
4+
#include "../../util/godot/classes/input_event_mouse_motion.h"
5+
#include "../../util/godot/core/input_enums.h"
6+
#include "../../util/godot/editor_scale.h"
7+
#include "../../util/math/funcs.h"
8+
9+
namespace zylann {
10+
11+
ZN_ControlSizer::ZN_ControlSizer() {
12+
set_default_cursor_shape(Control::CURSOR_VSIZE);
13+
const real_t editor_scale = EDSCALE;
14+
set_custom_minimum_size(Vector2(0, editor_scale * 5));
15+
}
16+
17+
void ZN_ControlSizer::set_target_control(Control *control) {
18+
_target_control.set(control);
19+
}
20+
21+
#ifdef ZN_GODOT
22+
void ZN_ControlSizer::gui_input(const Ref<InputEvent> &p_event) {
23+
#elif defined(ZN_GODOT_EXTENSION)
24+
void ZN_ControlSizer::_gui_input(const Ref<InputEvent> &p_event) {
25+
#endif
26+
27+
Ref<InputEventMouseButton> mb = p_event;
28+
if (mb.is_valid()) {
29+
Control *target_control = _target_control.get();
30+
ZN_ASSERT_RETURN(target_control != nullptr);
31+
32+
if (mb->is_pressed()) {
33+
if (mb->get_button_index() == ::godot::MOUSE_BUTTON_LEFT) {
34+
_dragging = true;
35+
}
36+
} else {
37+
_dragging = false;
38+
}
39+
}
40+
41+
Ref<InputEventMouseMotion> mm = p_event;
42+
if (mm.is_valid()) {
43+
if (_dragging) {
44+
Control *target_control = _target_control.get();
45+
ZN_ASSERT_RETURN(target_control != nullptr);
46+
47+
const Vector2 ms = target_control->get_custom_minimum_size();
48+
// Assuming the UI is not scaled
49+
const Vector2 rel = mm->get_relative();
50+
// Assuming vertical for now
51+
// TODO Clamp min_size to `target.get_minimum_size()`?
52+
target_control->set_custom_minimum_size(Vector2(ms.x, math::clamp(ms.y + rel.y, _min_size, _max_size)));
53+
}
54+
}
55+
}
56+
57+
void ZN_ControlSizer::_notification(int p_what) {
58+
switch (p_what) {
59+
case NOTIFICATION_MOUSE_ENTER: {
60+
_mouse_inside = true;
61+
queue_redraw();
62+
} break;
63+
64+
case NOTIFICATION_MOUSE_EXIT: {
65+
_mouse_inside = false;
66+
queue_redraw();
67+
} break;
68+
69+
case NOTIFICATION_ENTER_TREE:
70+
cache_theme();
71+
break;
72+
73+
case NOTIFICATION_THEME_CHANGED:
74+
cache_theme();
75+
break;
76+
77+
case NOTIFICATION_DRAW: {
78+
if (_dragging || _mouse_inside) {
79+
draw_texture(_hover_icon, (get_size() - _hover_icon->get_size()) / 2);
80+
}
81+
} break;
82+
}
83+
}
84+
85+
void ZN_ControlSizer::cache_theme() {
86+
// TODO I'd like to cache this, but `BIND_THEME_ITEM_CUSTOM` is not exposed to GDExtension...
87+
// TODO Have a framework-level StringName cache singleton
88+
_hover_icon = get_theme_icon("v_grabber", "SplitContainer");
89+
}
90+
91+
void ZN_ControlSizer::_bind_methods() {}
92+
93+
} // namespace zylann
+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#ifndef ZN_GODOT_CONTROL_SIZER_H
2+
#define ZN_GODOT_CONTROL_SIZER_H
3+
4+
#include "../../util/godot/classes/control.h"
5+
#include "../../util/godot/object_weak_ref.h"
6+
7+
namespace zylann {
8+
9+
// Implements similar logic as the middle resizing handle of SplitContainer, but works on a target control instead
10+
class ZN_ControlSizer : public Control {
11+
GDCLASS(ZN_ControlSizer, Control)
12+
public:
13+
ZN_ControlSizer();
14+
15+
void set_target_control(Control *control);
16+
17+
#ifdef ZN_GODOT
18+
void gui_input(const Ref<InputEvent> &p_event) override;
19+
#elif defined(ZN_GODOT_EXTENSION)
20+
void _gui_input(const Ref<InputEvent> &p_event) override;
21+
#endif
22+
23+
private:
24+
static void _bind_methods();
25+
26+
void _notification(int p_what);
27+
28+
void cache_theme();
29+
30+
zylann::godot::ObjectWeakRef<Control> _target_control;
31+
bool _dragging = false;
32+
bool _mouse_inside = false;
33+
float _min_size = 10.0;
34+
float _max_size = 1000.0;
35+
Ref<Texture2D> _hover_icon;
36+
};
37+
38+
} // namespace zylann
39+
40+
#endif // ZN_GODOT_CONTROL_SIZER_H

editor/instance_library/voxel_instance_library_editor_plugin.cpp

+8-160
Original file line numberDiff line numberDiff line change
@@ -19,28 +19,12 @@ VoxelInstanceLibraryEditorPlugin::VoxelInstanceLibraryEditorPlugin() {}
1919

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

25-
_confirmation_dialog = memnew(ConfirmationDialog);
26-
_confirmation_dialog->connect(
27-
"confirmed", callable_mp(this, &VoxelInstanceLibraryEditorPlugin::_on_remove_item_confirmed)
28-
);
29-
base_control->add_child(_confirmation_dialog);
30-
31-
_info_dialog = memnew(AcceptDialog);
32-
base_control->add_child(_info_dialog);
33-
34-
_open_scene_dialog = memnew(EditorFileDialog);
35-
PackedStringArray extensions = godot::get_recognized_extensions_for_type(PackedScene::get_class_static());
36-
for (int i = 0; i < extensions.size(); ++i) {
37-
_open_scene_dialog->add_filter("*." + extensions[i]);
38-
}
39-
_open_scene_dialog->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_FILE);
40-
base_control->add_child(_open_scene_dialog);
41-
_open_scene_dialog->connect(
42-
"file_selected", callable_mp(this, &VoxelInstanceLibraryEditorPlugin::_on_open_scene_dialog_file_selected)
43-
);
24+
EditorUndoRedoManager &VoxelInstanceLibraryEditorPlugin::get_undo_redo2() {
25+
EditorUndoRedoManager *ur = get_undo_redo();
26+
ZN_ASSERT(ur != nullptr);
27+
return *ur;
4428
}
4529

4630
bool VoxelInstanceLibraryEditorPlugin::_zn_handles(const Object *p_object) const {
@@ -49,8 +33,8 @@ bool VoxelInstanceLibraryEditorPlugin::_zn_handles(const Object *p_object) const
4933
}
5034

5135
void VoxelInstanceLibraryEditorPlugin::_zn_edit(Object *p_object) {
52-
VoxelInstanceLibrary *lib = Object::cast_to<VoxelInstanceLibrary>(p_object);
53-
_library.reference_ptr(lib);
36+
// VoxelInstanceLibrary *lib = Object::cast_to<VoxelInstanceLibrary>(p_object);
37+
// _library.reference_ptr(lib);
5438
}
5539

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

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

74-
void VoxelInstanceLibraryEditorPlugin::_on_add_item_button_pressed(int id) {
75-
_on_button_pressed(id);
76-
}
77-
78-
void VoxelInstanceLibraryEditorPlugin::_on_remove_item_button_pressed() {
79-
_on_button_pressed(VoxelInstanceLibraryInspectorPlugin::BUTTON_REMOVE_ITEM);
80-
}
81-
82-
void VoxelInstanceLibraryEditorPlugin::_on_button_pressed(int id) {
83-
_last_used_button = id;
84-
85-
switch (id) {
86-
case VoxelInstanceLibraryInspectorPlugin::BUTTON_ADD_MULTIMESH_ITEM: {
87-
ERR_FAIL_COND(_library.is_null());
88-
89-
Ref<VoxelInstanceLibraryMultiMeshItem> item;
90-
item.instantiate();
91-
// Setup some defaults
92-
Ref<BoxMesh> mesh;
93-
mesh.instantiate();
94-
item->set_mesh(mesh, 0);
95-
96-
// We could decide to use a different default here if we can detect that the instancer the library is used
97-
// 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
98-
// support, otherwise things look broken. 0 is the default.
99-
// item->set_lod_index(2);
100-
101-
Ref<VoxelInstanceGenerator> generator;
102-
generator.instantiate();
103-
item->set_generator(generator);
104-
105-
const int item_id = _library->get_next_available_id();
106-
107-
EditorUndoRedoManager &ur = *get_undo_redo();
108-
ur.create_action("Add multimesh item");
109-
ur.add_do_method(*_library, "add_item", item_id, item);
110-
ur.add_undo_method(*_library, "remove_item", item_id);
111-
ur.commit_action();
112-
} break;
113-
114-
case VoxelInstanceLibraryInspectorPlugin::BUTTON_ADD_SCENE_ITEM: {
115-
_open_scene_dialog->popup_centered_ratio();
116-
} break;
117-
118-
case VoxelInstanceLibraryInspectorPlugin::BUTTON_REMOVE_ITEM: {
119-
ERR_FAIL_COND(_library.is_null());
120-
const int item_id = try_get_selected_item_id();
121-
if (item_id != -1) {
122-
_item_id_to_remove = item_id;
123-
_confirmation_dialog->set_text(ZN_TTR("Remove item {0}?").format(varray(_item_id_to_remove)));
124-
_confirmation_dialog->popup_centered();
125-
}
126-
} break;
127-
128-
default:
129-
ERR_PRINT("Unknown menu item");
130-
break;
131-
}
132-
}
133-
134-
// TODO This function does not modify anything, but cannot be `const` because get_editor_interface() is not...
135-
int VoxelInstanceLibraryEditorPlugin::try_get_selected_item_id() {
136-
String path = get_editor_interface()->get_inspector()->get_selected_path();
137-
String prefix = "item_";
138-
139-
if (path.begins_with(prefix)) {
140-
const int id = path.substr(prefix.length()).to_int();
141-
return id;
142-
143-
} else {
144-
// The inspector won't let us know which item the current resource is stored into...
145-
// Gridmap works because it simulates every item as properties of the library,
146-
// but our current resource does not do that,
147-
// and I don't want to modify the API just because the built-in inspector is bad.
148-
_info_dialog->set_text(
149-
ZN_TTR(String("Could not determine selected item from property path: `{0}`.\n"
150-
"You must select the `item_X` property label of the item you want to remove."))
151-
.format(varray(path))
152-
);
153-
_info_dialog->popup_centered();
154-
return -1;
155-
}
156-
}
157-
158-
void VoxelInstanceLibraryEditorPlugin::_on_remove_item_confirmed() {
159-
ERR_FAIL_COND(_library.is_null());
160-
ERR_FAIL_COND(_item_id_to_remove == -1);
161-
162-
Ref<VoxelInstanceLibraryItem> item = _library->get_item(_item_id_to_remove);
163-
164-
EditorUndoRedoManager &ur = *get_undo_redo();
165-
ur.create_action("Remove item");
166-
ur.add_do_method(*_library, "remove_item", _item_id_to_remove);
167-
ur.add_undo_method(*_library, "add_item", _item_id_to_remove, item);
168-
ur.commit_action();
169-
170-
_item_id_to_remove = -1;
171-
}
172-
173-
void VoxelInstanceLibraryEditorPlugin::_on_open_scene_dialog_file_selected(String fpath) {
174-
switch (_last_used_button) {
175-
case VoxelInstanceLibraryInspectorPlugin::BUTTON_ADD_SCENE_ITEM:
176-
add_scene_item(fpath);
177-
break;
178-
179-
default:
180-
ERR_PRINT("Invalid menu option");
181-
break;
182-
}
183-
}
184-
185-
void VoxelInstanceLibraryEditorPlugin::add_scene_item(String fpath) {
186-
ERR_FAIL_COND(_library.is_null());
187-
188-
Ref<PackedScene> scene = godot::load_resource(fpath);
189-
ERR_FAIL_COND(scene.is_null());
190-
191-
Ref<VoxelInstanceLibrarySceneItem> item;
192-
item.instantiate();
193-
// Setup some defaults
194-
item->set_lod_index(2);
195-
item->set_scene(scene);
196-
Ref<VoxelInstanceGenerator> generator;
197-
generator.instantiate();
198-
generator->set_density(0.01f); // Low density for scenes because that's heavier
199-
item->set_generator(generator);
200-
201-
const int item_id = _library->get_next_available_id();
202-
203-
EditorUndoRedoManager &ur = *get_undo_redo();
204-
ur.create_action("Add scene item");
205-
ur.add_do_method(_library.ptr(), "add_item", item_id, item);
206-
ur.add_undo_method(_library.ptr(), "remove_item", item_id);
207-
ur.commit_action();
208-
}
209-
21058
void VoxelInstanceLibraryEditorPlugin::_bind_methods() {}
21159

21260
} // namespace zylann::voxel

editor/instance_library/voxel_instance_library_editor_plugin.h

+2-18
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ class VoxelInstanceLibraryEditorPlugin : public zylann::godot::ZN_EditorPlugin {
2424

2525
VoxelInstanceLibraryEditorPlugin();
2626

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

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

38-
int try_get_selected_item_id();
39-
void add_scene_item(String fpath);
40-
41-
void _on_remove_item_confirmed();
42-
void _on_open_scene_dialog_file_selected(String fpath);
43-
44-
void _on_button_pressed(int id);
45-
4638
static void _bind_methods();
4739

48-
ConfirmationDialog *_confirmation_dialog = nullptr;
49-
AcceptDialog *_info_dialog = nullptr;
50-
int _item_id_to_remove = -1;
51-
int _item_id_to_update = -1;
52-
EditorFileDialog *_open_scene_dialog;
53-
int _last_used_button;
54-
55-
Ref<VoxelInstanceLibrary> _library;
5640
Ref<VoxelInstanceLibraryInspectorPlugin> _inspector_plugin;
5741
};
5842

0 commit comments

Comments
 (0)