Skip to content
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

Merged
merged 24 commits into from
Jun 19, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
18c9c79
Added recipe for Inspector Plugin
snatvb Jun 9, 2024
d39fcac
Fixed linting issue
snatvb Jun 9, 2024
0bd2254
Removed old file that has been moved before
snatvb Jun 9, 2024
a2b16ae
Fixed review issues
snatvb Jun 9, 2024
ca10439
Update src/recipes/editor-plugin/inspector-plugins.md
snatvb Jun 10, 2024
cbbd82a
Update src/recipes/editor-plugin/inspector-plugins.md
snatvb Jun 10, 2024
97328e5
Update src/recipes/editor-plugin/inspector-plugins.md
snatvb Jun 10, 2024
f064316
Update src/recipes/editor-plugin/inspector-plugins.md
snatvb Jun 10, 2024
3396c79
Update src/recipes/editor-plugin/inspector-plugins.md
snatvb Jun 10, 2024
aca53e8
Update src/recipes/editor-plugin/inspector-plugins.md
snatvb Jun 10, 2024
5643083
Fixed comment in review: rid of incorrect casts, added more explainings
snatvb Jun 10, 2024
b23cb7a
Fixed review comments
snatvb Jun 10, 2024
ee67c9d
Update src/recipes/editor-plugin/inspector-plugins.md
snatvb Jun 10, 2024
65be4dd
Merge branch 'inspector-plugin-recipe' of github.com:snatvb/godot-rus…
snatvb Jun 10, 2024
5fb6cfd
Added comment TODO for note that necessary to add more plugin examples
snatvb Jun 10, 2024
3656f17
Fixed imports - converted into flat structure
snatvb Jun 11, 2024
4a86681
Fixed formatting strings in exmaples
snatvb Jun 11, 2024
a387b49
Update src/recipes/editor-plugin/inspector-plugins.md
snatvb Jun 11, 2024
3a15aa7
Added more information for implementation of inspector editor line
snatvb Jun 11, 2024
4b0b4ea
Added breckets for codeline
snatvb Jun 11, 2024
f675b54
Merge branch 'inspector-plugin-recipe' of github.com:snatvb/godot-rus…
snatvb Jun 11, 2024
d61995b
Removed unnecessary cast number after to_variant
snatvb Jun 11, 2024
26e7513
Reduce PNG size with oxipng
Bromeon Jun 11, 2024
efdc336
Update src/recipes/editor-plugin/inspector-plugins.md
snatvb Jun 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
- [Export to Web](toolchain/export-web.md)
- [Recipes](recipes/index.md)
- [Custom resources](recipes/custom-resources.md)
- [Editor plugins](recipes/editor-plugin.md)
- [Editor plugins](recipes/editor-plugin/index.md)
- [Inspector plugins](recipes/editor-plugin/inspector-plugins.md)
Comment on lines +33 to +34
Copy link
Member

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.

Copy link
Contributor Author

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 🤔

Copy link
Member

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

<!-- TODO: more plugins from https://docs.godotengine.org/en/stable/tutorials/plugins/editor/index.html -->

?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep, have a look

- [Engine singletons](recipes/engine-singleton.md)
- [Custom node icons](recipes/custom-icons.md)
- [Contributing to gdext](contribute/index.md)
Expand Down
Binary file added src/recipes/editor-plugin/images/after.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/recipes/editor-plugin/images/before.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,4 @@ Use an [`is_editor_hint` guard][api-engine-iseditorhint] if you don't want some

[api-engine-iseditorhint]: https://godot-rust.github.io/docs/gdext/master/godot/engine/struct.Engine.html#method.is_editor_hint
[wiki-guard-csci]: https://en.wikipedia.org/wiki/Guard_(computer_science)
```
347 changes: 347 additions & 0 deletions src/recipes/editor-plugin/inspector-plugins.md
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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
godot_print!("Randomize! {} for {}", num, property_name);
godot_print!("Randomize: {num} for {property_name}");


self.base_mut()
.emit_changed(property_name, num.to_variant().to());
Copy link
Member

Choose a reason for hiding this comment

The 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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let text = format!("Randomize: {}", num);
let text = format!("Randomize: {num}");

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:
Copy link
Member

Choose a reason for hiding this comment

The 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");
}
}
}

```
Loading