-
Notifications
You must be signed in to change notification settings - Fork 33
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Added recipe for Inspector Plugin #48
Changes from 11 commits
18c9c79
d39fcac
0bd2254
a2b16ae
ca10439
cbbd82a
97328e5
f064316
3396c79
aca53e8
5643083
b23cb7a
ee67c9d
65be4dd
5fb6cfd
3656f17
4a86681
a387b49
3a15aa7
4b0b4ea
f675b54
d61995b
26e7513
efdc336
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,347 @@ | ||||||
<!-- | ||||||
~ Copyright (c) godot-rust; Bromeon and contributors. | ||||||
~ This Source Code Form is subject to the terms of the Mozilla Public | ||||||
~ License, v. 2.0. If a copy of the MPL was not distributed with this | ||||||
~ file, You can obtain one at https://mozilla.org/MPL/2.0/. | ||||||
--> | ||||||
|
||||||
# Inspector plugins | ||||||
|
||||||
The inspector dock allows you to create custom widgets to edit properties through plugins. | ||||||
This can be beneficial when working with custom datatypes and resources, although you can | ||||||
use the feature to change the inspector widgets for built-in types. You can design custom | ||||||
controls for specific properties, entire objects, and even separate controls associated | ||||||
with particular datatypes. | ||||||
snatvb marked this conversation as resolved.
Show resolved
Hide resolved
snatvb marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
For more info, see | ||||||
[docs.godotengine.org](https://docs.godotengine.org/en/stable/classes/class_editorinspectorplugin.html#class-editorinspectorplugin). | ||||||
|
||||||
The [example](https://docs.godotengine.org/en/stable/tutorials/plugins/editor/inspector_plugins.html) | ||||||
in the `Godot` docs in `Rust`. It will replace Integer input with button that randomize value. | ||||||
snatvb marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
||||||
Before (int input): | ||||||
|
||||||
![Before](./images/before.png) | ||||||
|
||||||
After (button): | ||||||
|
||||||
![After](./images/after.png) | ||||||
|
||||||
Add this dependency to Rust with the shell in the same directory as `Cargo.toml`. | ||||||
|
||||||
```bash | ||||||
cargo add rand | ||||||
``` | ||||||
|
||||||
Add file `addon.rs` and import it in `lib.rs`: | ||||||
|
||||||
```rust | ||||||
// file: lib.rs | ||||||
mod addon; | ||||||
``` | ||||||
|
||||||
Add the following imports at the beginning of the file: | ||||||
|
||||||
```rust | ||||||
use godot::{ | ||||||
classes::{ | ||||||
Button, EditorInspectorPlugin, EditorPlugin, EditorProperty, IEditorInspectorPlugin, | ||||||
IEditorPlugin, IEditorProperty, | ||||||
}, | ||||||
global, | ||||||
prelude::*, | ||||||
}; | ||||||
use rand::Rng; | ||||||
``` | ||||||
|
||||||
Since Rust is a statically typed language, we will proceed in reverse order unlike in Godot documentation, to avoid encountering errors unnecessarily. | ||||||
snatvb marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
||||||
|
||||||
## Add Property Editor | ||||||
|
||||||
To begin with, let's define the editor for properties: | ||||||
|
||||||
```rust | ||||||
#[derive(GodotClass)] | ||||||
#[class(tool, init, base=EditorProperty)] | ||||||
struct RandomIntEditor { | ||||||
base: Base<EditorProperty>, | ||||||
button: Option<Gd<Button>>, | ||||||
} | ||||||
``` | ||||||
|
||||||
After that, we need to add an implementation for the trait `IEditorProperty`: | ||||||
|
||||||
```rust | ||||||
#[godot_api] | ||||||
impl IEditorProperty for RandomIntEditor { | ||||||
fn enter_tree(&mut self) { | ||||||
// Create button element. | ||||||
let mut button = Button::new_alloc(); | ||||||
|
||||||
// Add handler for this button, handle_press will be define in another impl. | ||||||
button.connect("pressed".into(), self.base().callable("handle_press")); | ||||||
button.set_text("Randomize".into()); | ||||||
|
||||||
// Save pointer to the button into struct. | ||||||
self.button = Some(button.clone()); | ||||||
self.base_mut().add_child(button.upcast()); | ||||||
} | ||||||
|
||||||
fn exit_tree(&mut self) { | ||||||
// Remove element from inspector when this plugin unmount: | ||||||
if let Some(button) = self.button.take() { | ||||||
self.base_mut().remove_child(button.upcast()); | ||||||
} else { | ||||||
// Log error if button disappeared before | ||||||
godot_error!("Button wasn't found in exit_tree"); | ||||||
} | ||||||
} | ||||||
} | ||||||
``` | ||||||
|
||||||
Let's add a handler for the button: | ||||||
|
||||||
```rust | ||||||
#[godot_api] | ||||||
impl RandomIntEditor { | ||||||
#[func] | ||||||
fn handle_press(&mut self) { | ||||||
// Update value by button click | ||||||
// - take prop name, randomize number | ||||||
// - send prop name and random number to godot engine for update value | ||||||
// - update button text | ||||||
snatvb marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
let property_name = self.base().get_edited_property(); | ||||||
let num = rand::thread_rng().gen_range(0..100); | ||||||
|
||||||
godot_print!("Randomize! {} for {}", num, property_name); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
self.base_mut() | ||||||
.emit_changed(property_name, num.to_variant().to()); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please fix this everywhere 🙂 |
||||||
|
||||||
if let Some(mut button) = self.button.clone() { | ||||||
let text = format!("Randomize: {}", num); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
button.set_text(text.into()); | ||||||
} else { | ||||||
// Print error of something went wrong | ||||||
godot_error!("Button wasn't found in handle_press"); | ||||||
} | ||||||
} | ||||||
} | ||||||
``` | ||||||
|
||||||
|
||||||
## Add Inspector plugin | ||||||
|
||||||
Now we need to connect this editor to fields with an integer type. | ||||||
To do this, we need to create an EditorInspectorPlugin. | ||||||
snatvb marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
||||||
```rust | ||||||
#[derive(GodotClass)] | ||||||
#[class(tool, init, base=EditorInspectorPlugin)] | ||||||
struct RandomInspectorPlugin { | ||||||
base: Base<EditorInspectorPlugin>, | ||||||
} | ||||||
``` | ||||||
|
||||||
IEditorInspectorPlugin implementation: | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe write a short sentence, introducing the following code block. |
||||||
|
||||||
```rust | ||||||
#[godot_api] | ||||||
impl IEditorInspectorPlugin for RandomInspectorPlugin { | ||||||
fn parse_property( | ||||||
&mut self, | ||||||
_object: Gd<Object>, // object that inspecting | ||||||
snatvb marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
value_type: VariantType, | ||||||
name: GString, | ||||||
_hint_type: global::PropertyHint, | ||||||
_hit_string: GString, | ||||||
_flags: global::PropertyUsageFlags, | ||||||
_wide: bool, | ||||||
) -> bool { | ||||||
if value_type == VariantType::INT { | ||||||
self.base_mut() | ||||||
.add_property_editor(name, RandomIntEditor::new_alloc().upcast()); | ||||||
return true; | ||||||
} | ||||||
|
||||||
false | ||||||
} | ||||||
|
||||||
fn can_handle(&self, _object: Gd<Object>) -> bool { | ||||||
// | ||||||
object.is_class("Node2D".into()) | ||||||
} | ||||||
snatvb marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
} | ||||||
``` | ||||||
|
||||||
If `parse_property` returns `true`, the editor will be created and replace the current | ||||||
representation; if not, it's necessary to return `false`. | ||||||
This allows for specific control over where and how processing occurs. | ||||||
snatvb marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
||||||
|
||||||
## Adding an editor plugin | ||||||
|
||||||
Only one thing left to do: define the editor plugin that will kick off all this magic! | ||||||
This can be a generic `EditorPlugin` or a more specific `InspectorEditorPlugin`, depending | ||||||
on what you want to achieve. | ||||||
|
||||||
|
||||||
```rust | ||||||
#[derive(GodotClass)] | ||||||
#[class(tool, init, editor_plugin, base=EditorPlugin)] | ||||||
struct RustEditorPlugin { | ||||||
base: Base<EditorPlugin>, | ||||||
random_inspector: Gd<RandomInspectorPlugin>, | ||||||
} | ||||||
``` | ||||||
|
||||||
```rust | ||||||
#[godot_api] | ||||||
impl IEditorPlugin for RustEditorPlugin { | ||||||
fn enter_tree(&mut self) { | ||||||
// create our inspector plugin and save it to next remove | ||||||
let plugin = RandomInspectorPlugin::new_gd(); | ||||||
self.random_inspector = plugin.clone(); | ||||||
self.base_mut().add_inspector_plugin(plugin.upcast()); | ||||||
} | ||||||
|
||||||
fn exit_tree(&mut self) { | ||||||
// remove inspector plugin when editor plugin unmount | ||||||
let plugin = self.random_inspector.clone(); | ||||||
self.base_mut().remove_inspector_plugin(plugin.upcast()); | ||||||
} | ||||||
snatvb marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
} | ||||||
``` | ||||||
|
||||||
|
||||||
```admonish note title="Troubleshooting" | ||||||
Sometimes after compilation, you may encounter errors or panic. Most likely, all you need to do is simply **restart** the Godot Editor. | ||||||
``` | ||||||
|
||||||
Example error: | ||||||
|
||||||
```log | ||||||
Initialize godot-rust (API v4.2.stable.official, runtime v4.2.2.stable.official) | ||||||
ERROR: Cannot get class 'RandomInspectorPlugin'. | ||||||
at: (core/object/class_db.cpp:392) | ||||||
ERROR: Cannot get class 'RandomInspectorPlugin'. | ||||||
at: (core/object/class_db.cpp:392) | ||||||
``` | ||||||
|
||||||
|
||||||
# Full code | ||||||
snatvb marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
||||||
|
||||||
```rust | ||||||
// file: addon.rs | ||||||
|
||||||
use godot::{ | ||||||
classes::{ | ||||||
Button, EditorInspectorPlugin, EditorPlugin, EditorProperty, IEditorInspectorPlugin, | ||||||
IEditorPlugin, IEditorProperty, | ||||||
}, | ||||||
global, | ||||||
prelude::*, | ||||||
}; | ||||||
snatvb marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
use rand::Rng; | ||||||
|
||||||
#[derive(GodotClass)] | ||||||
#[class(tool, init, editor_plugin, base=EditorPlugin)] | ||||||
struct RustEditorPlugin { | ||||||
base: Base<EditorPlugin>, | ||||||
random_inspector: Gd<RandomInspectorPlugin>, | ||||||
} | ||||||
|
||||||
#[godot_api] | ||||||
impl IEditorPlugin for RustEditorPlugin { | ||||||
fn enter_tree(&mut self) { | ||||||
let plugin = RandomInspectorPlugin::new_gd(); | ||||||
self.random_inspector = plugin.clone(); | ||||||
self.base_mut().add_inspector_plugin(plugin.upcast()); | ||||||
} | ||||||
|
||||||
fn exit_tree(&mut self) { | ||||||
let plugin = self.random_inspector.clone(); | ||||||
self.base_mut().remove_inspector_plugin(plugin.upcast()); | ||||||
} | ||||||
} | ||||||
|
||||||
#[derive(GodotClass)] | ||||||
#[class(tool, init, base=EditorInspectorPlugin)] | ||||||
struct RandomInspectorPlugin { | ||||||
base: Base<EditorInspectorPlugin>, | ||||||
} | ||||||
|
||||||
#[godot_api] | ||||||
impl IEditorInspectorPlugin for RandomInspectorPlugin { | ||||||
fn parse_property( | ||||||
&mut self, | ||||||
_object: Gd<Object>, | ||||||
value_type: VariantType, | ||||||
name: GString, | ||||||
_hint_type: global::PropertyHint, | ||||||
_hit_name: GString, | ||||||
_flags: global::PropertyUsageFlags, | ||||||
_wide: bool, | ||||||
) -> bool { | ||||||
snatvb marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
if value_type == VariantType::INT { | ||||||
self.base_mut() | ||||||
.add_property_editor(name, RandomIntEditor::new_alloc().upcast()); | ||||||
return true; | ||||||
} | ||||||
|
||||||
false | ||||||
} | ||||||
|
||||||
// This method says Godot that this plugin handle the object if it returns true | ||||||
fn can_handle(&self, object: Gd<Object>) -> bool { | ||||||
// This plugin handle only Node2D and object that extends it | ||||||
object.is_class("Node2D".into()) | ||||||
} | ||||||
} | ||||||
|
||||||
#[derive(GodotClass)] | ||||||
#[class(tool, init, base=EditorProperty)] | ||||||
struct RandomIntEditor { | ||||||
base: Base<EditorProperty>, | ||||||
button: Option<Gd<Button>>, | ||||||
} | ||||||
|
||||||
#[godot_api] | ||||||
impl RandomIntEditor { | ||||||
#[func] | ||||||
fn handle_press(&mut self) { | ||||||
let property_name = self.base().get_edited_property(); | ||||||
let num = rand::thread_rng().gen_range(0..100); | ||||||
godot_print!("Randomize! {} for {}", num, property_name); | ||||||
self.base_mut() | ||||||
.emit_changed(property_name, num.to_variant().to()); | ||||||
if let Some(mut button) = self.button.clone() { | ||||||
let text = format!("Randomize: {}", num); | ||||||
button.set_text(text.into()); | ||||||
} else { | ||||||
godot_error!("Button wasn't found in handle_press"); | ||||||
} | ||||||
} | ||||||
snatvb marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
} | ||||||
|
||||||
#[godot_api] | ||||||
impl IEditorProperty for RandomIntEditor { | ||||||
fn enter_tree(&mut self) { | ||||||
let mut button = Button::new_alloc(); | ||||||
button.connect("pressed".into(), self.base().callable("handle_press")); | ||||||
button.set_text("Randomize".into()); | ||||||
self.button = Some(button.clone()); | ||||||
self.base_mut().add_child(button.upcast()); | ||||||
} | ||||||
|
||||||
fn exit_tree(&mut self) { | ||||||
if let Some(button) = self.button.take() { | ||||||
self.base_mut().remove_child(button.upcast()); | ||||||
} else { | ||||||
godot_error!("Button wasn't found in exit_tree"); | ||||||
} | ||||||
} | ||||||
} | ||||||
|
||||||
``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would probably keep those in one page, as they are closely related and the current page is very short. Just use a h2 for the inspector plugins.
Then, you also wouldn't need a new folder. The images could be named
inspector-before/after.png
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought it could be extended with others exmaples 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah you mean others from this list? Good point, then maybe leave it.
Can you add a HTML comment (which isn't rendered) on the main editor plugin site, like
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yep, have a look