├── .gitignore
├── LICENSE
├── README.md
├── addons
└── tool_button
│ ├── TB_Button.gd
│ ├── TB_InspectorPlugin.gd
│ ├── TB_Plugin.gd
│ └── plugin.cfg
└── readme
├── .gdignore
├── bottom_buttons.png
├── logo.png
├── preview2.png
└── preview3.png
/.gitignore:
--------------------------------------------------------------------------------
1 | # Godot-specific ignores
2 | .import/
3 | .godot/
4 | export.cfg
5 | export_presets.cfg
6 |
7 | # Imported translations (automatically generated from CSV files)
8 | *.translation
9 |
10 | # Mono-specific ignores
11 | .mono/
12 | data_*/
13 |
14 | default_env.tres
15 | icon.png
16 | icon.png.import
17 | Test.gd
18 | ParentClass.gd
19 | Test.tscn
20 | project.godot
21 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 teebarjunk
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | ## ToolButtonPlugin for Godot 4.0 - v1.4
3 |
4 | Editor buttons with ~~one line of code: `@tool`~~ zero lines of code.
5 |
6 | Simply install, and select any node:
7 |
8 | - Public methods will be listed at the bottom of editor panel.
9 | - Signals with 0 arguments will also be shown.
10 |
11 | For more advanced features, read below.
12 |
13 | 
14 |
15 | ## Getting Started
16 |
17 | - Enable plugin.
18 | - Add `@tool` to top of your script.
19 | - Add a `_get_tool_buttons() -> Array` method.
20 | - The array items can be:
21 | - Names of methods `"my_method"`
22 | - Method itself `my_method`
23 | - Bound method `method.bind(true, "ok" 1.0)`
24 | - Anonymous methods `func(): print("Hey!")`
25 | - Anonymous methods with name `func press_me(): print("You pressed me!")`
26 | - Names of signals `"my_signal"`
27 | - Signal itself `my_signal`
28 | - Array with signal and arguments `["my_signal", [true, "ok"]]`
29 | - Dictionarys with fine tuned control: `{call=method.bind(true), text="My Method", tint=Color.RED}`
30 | - Arrays with any of the above `["my_method", method2.bind(1.0), "a_signal", {text="X", call=reset}]`
31 |
32 | 
33 |
34 | ## Advanced Example
35 |
36 | To specify buttons that show above the inspector, add a `_get_tool_buttons` func.
37 |
38 | 
39 |
40 | With `Strings`:
41 | ```gd
42 | @tool
43 | extends Node
44 |
45 | func _get_tool_buttons():
46 | return [boost_score, remove_player]
47 |
48 | func boost_score():
49 | Player.score += 100
50 |
51 | func remove_player():
52 | Player.queue_free()
53 | ```
54 | and/or `Callable`s
55 |
56 | ```gd
57 | # WARNING, some stuff won't work: If you get *"Cannot access member without instance"*: https://github.com/godotengine/godot/issues/56780
58 | @tool
59 | extends Node
60 |
61 | func _get_tool_buttons():
62 | return [
63 | func add_score(): score += 10,
64 | func reset_health(): health = 0
65 | ]
66 | ```
67 | and/or `Dictionarys`
68 | ```gd
69 | @tool
70 | extends Node
71 |
72 | signal reset()
73 |
74 | func _get_tool_buttons(): return [
75 | "boost_score",
76 | {call="boost_score", args=[100], tint=Color.DEEP_SKY_BLUE},
77 | {call="emit_signal", args=["reset"], text="Reset", tint=Color.TOMATO},
78 | {
79 | call=func(): print("My Health: ", health),
80 | text="Print Health",
81 | tint=func(): return Color(health * .1, 0.0, 0.0)
82 | lock=func(): return health == 100
83 | }
84 | ]
85 |
86 | func boost_score(x=10):
87 | Player.score += x
88 | ```
89 |
90 | `call` is mandatory. Other's are optional.
91 |
92 | |key |desc |default |
93 | |:------|:------------------------------|:--------------------|
94 | |call | Method to call. | - |
95 | |args | Array of arguments to pass.
*(Mouse over button to see args.)* | - |
96 | |text | Button label. | - |
97 | |tint | Button color. | Color.WHITE |
98 | |icon | Button icon. | -
99 | |flat | Button is flat style. | false |
100 | |hint | Hint text for mouse over. | - |
101 | |print | Print output of method call? | true |
102 | |align | Button alignment. | BoxContainer.ALIGNMENT_CENTER |
103 | |lock | Disable button? | false |
104 | |update_filesystem| Tells Godot editor to rescan file system. | false |
105 |
106 |
107 | ## Resource Example
108 |
109 | For `_get_tool_buttons` to work on a `Resource` it needs to be static.
110 |
111 | ```gd
112 | @tool
113 | extends Resource
114 | class_name MyResource
115 |
116 | # STATIC
117 | static func _get_tool_buttons():
118 | return ["my_button"]
119 |
120 | # LOCAL
121 | export(String) var my_name:String = ""
122 |
123 | func my_button():
124 | print(my_name)
125 | ```
126 |
127 | ## Signals
128 | ```gd
129 | signal my_signal()
130 | signal my_arg_signal(x, y)
131 |
132 | func _get_tool_buttons():
133 | return [
134 | my_signal,
135 | [my_arg_signal, [true, "okay"]]
136 | ]
137 | ```
138 |
139 | ## Multi button support
140 | You can have multiple buttons on the same line.
141 | They can all be called at once (default), or seperate, based on a toggle.
142 | ```
143 | func _get_tool_buttons():
144 | return [
145 | [msg, msg.bind("yes"), msg.bind("no")],
146 | ]
147 |
148 | func msg(msg := "Default Message"):
149 | print("Got: ", msg)
150 |
151 | func doit():
152 | pass
153 | ```
154 |
155 | # Changes
156 | ## 1.4
157 | - Added Signal support.
158 | - Added support for multiple buttons in the same row.
159 | - Added automatically generated buttons for testing public methods.
160 | - Added automatically generated buttons for testing argumentless signals.
161 | - Auto tint method buttons blue and signal buttons yellow.
162 | - Lot's of little fixes.
163 |
164 | ## 1.3
165 | - Added `@SELECT_AND_EDIT::file_path`, `@EDIT_RESOURCE::file_path`, `@SELECT_FILE::file_path`.
166 |
167 | ## 1.2
168 | - Added 'Print Meta' button, which will print all meta data in an object.
169 |
170 | ## 1.1
171 | - Updated for Godot `4.0.alpha2`
172 | - Added `Callable` support `call` `text` `icon` `tint` `lock`: `{ text="Callable", call=func(): print("From Callable with love") }`
173 | - Changed `disabled` to `lock`
174 | - Bottom buttons are alphabetically sorted.
175 |
176 |
177 |
--------------------------------------------------------------------------------
/addons/tool_button/TB_Button.gd:
--------------------------------------------------------------------------------
1 | extends HBoxContainer
2 |
3 | var all_info := []
4 | var check: CheckBox
5 | var object: Object
6 | var pluginref: EditorPlugin
7 | var hash_id: int
8 |
9 | # @SCAN
10 | # @CREATE_AND_EDIT;file_path;text
11 | # @SELECT_AND_EDIT;file_path
12 | # @SELECT_FILE;file_path
13 | # @EDIT_RESOURCE;file_path
14 |
15 | const TINT_METHOD := Color.PALE_TURQUOISE
16 | const TINT_SIGNAL := Color.PALE_GOLDENROD
17 |
18 | func _init(obj: Object, d, p):
19 | object = obj
20 | pluginref = p
21 | hash_id = hash(d)
22 |
23 | alignment = BoxContainer.ALIGNMENT_CENTER
24 | size_flags_horizontal = SIZE_EXPAND_FILL
25 |
26 | var got = _get_info(d)
27 | all_info = [got] if got is Dictionary else got
28 |
29 | for index in len(all_info):
30 | var info: Dictionary = all_info[index]
31 |
32 | var button := Button.new()
33 | add_child(button)
34 | button.size_flags_horizontal = SIZE_EXPAND_FILL
35 | button.text = info.text
36 | button.modulate = _get_key_or_call(info, "tint", TYPE_COLOR, Color.WHITE)
37 | button.disabled = _get_key_or_call(info, "lock", TYPE_BOOL, false)
38 |
39 | button.button_down.connect(self._on_button_pressed.bind(index))
40 |
41 | if "hint" in info:
42 | button.tooltip_text = _get_key_or_call(info, "hint", TYPE_STRING, "")
43 | else:
44 | button.tooltip_text = "%s(%s)" % [info.call, _get_args_string(info)]
45 |
46 | button.flat = info.get("flat", false)
47 | button.alignment = info.get("align", HORIZONTAL_ALIGNMENT_CENTER)
48 |
49 | if "icon" in info:
50 | button.expand_icon = false
51 | button.set_button_icon(load(_get_key_or_call(info, "icon", TYPE_STRING, "")))
52 |
53 | # add a check box if there are multiple buttons
54 | if len(all_info) > 1:
55 | check = CheckBox.new()
56 | check.button_pressed = _get_tool_button_state().get(hash_id, false)
57 | check.tooltip_text = "All at once"
58 | check.pressed.connect(_check_pressed)
59 | add_child(check)
60 |
61 | func _get_tool_button_state() -> Dictionary:
62 | if not object.has_meta("tool_button_state"):
63 | object.set_meta("tool_button_state", {})
64 | return object.get_meta("tool_button_state")
65 |
66 | func _check_pressed():
67 | var d := _get_tool_button_state()
68 | d[hash_id] = check.button_pressed
69 |
70 | func _get_info(d: Variant) -> Variant:
71 | var out := {}
72 |
73 | if d is String:
74 | out.call = d
75 | out.text = d
76 |
77 | elif d is Callable:
78 | if d.is_custom():
79 | out.call = d
80 | out.text = str(d)
81 | if "::" in out.text:
82 | out.text = out.text.split("::")[-1]
83 | if "(lambda)" in out.text:
84 | out.text = out.text.replace("(lambda)", "")
85 |
86 | if d.get_object() != null and d.get_object().has_method(out.text):
87 | out.tint = TINT_METHOD
88 |
89 | out.text = out.text.capitalize()
90 |
91 | else:
92 | out.call = d
93 | out.text = str(d.get_method()).capitalize()
94 | out.tint = TINT_METHOD
95 |
96 | elif d is Signal:
97 | out.call = _do_signal.bind([d, []])
98 | out.text = str(d.get_name()).capitalize()
99 | out.tint = TINT_SIGNAL
100 | out.hint = str(d)
101 |
102 | elif d is Array:
103 | if d[0] is Signal:
104 | var sig: Signal = d[0]
105 | var args = " ".join(d[1].map(func(x): return str(x)))
106 | out.call=_do_signal.bind(d)
107 | out.text="%s\n%s" % [str(sig.get_name()).capitalize(), args]
108 | out.tint=TINT_SIGNAL
109 | out.hint=str(sig)
110 |
111 | else:
112 | var out_list := []
113 | for item in d:
114 | out_list.append(_get_info(item))
115 | return out_list
116 |
117 | elif d is Dictionary:
118 | out = d
119 |
120 | if not "text" in out:
121 | out.text = str(out.call)
122 |
123 | else:
124 | print("Hmm 0?", d)
125 |
126 | _process_info(out)
127 | return out
128 |
129 | func _process_info(out: Dictionary):
130 | if not "call" in out:
131 | var s = str(out)
132 | out.call = func(): print("No call defined for %s." % s)
133 |
134 | # special tags of extra actions
135 | var parts := Array(_get_label(out).split(";"))
136 | if out.call is String:
137 | out.call = parts[0]
138 | out.text = parts.pop_front().capitalize()
139 | else:
140 | out.text = parts.pop_front()
141 |
142 | # was just a string passed?
143 | if out.call is String:
144 | # was it a method?
145 | if object.has_method(out.call):
146 | if not "tint" in out:
147 | out.tint = TINT_METHOD
148 |
149 | # was it a signal?
150 | elif object.has_signal(out.call):
151 | if not "tint" in out:
152 | out.tint = TINT_SIGNAL
153 |
154 | for i in len(parts):
155 | if parts[i].begins_with("!"):
156 | var tag: String = parts[i].substr(1)
157 | var clr := Color()
158 | out.tint = clr.from_string(tag, Color.SLATE_GRAY)
159 |
160 | return out
161 |
162 | func _do_signal(sig_args: Array):
163 | var sig: Signal = sig_args[0]
164 | var args: Array = sig_args[1]
165 | match len(args):
166 | 0: sig.emit()
167 | 1: sig.emit(args[0])
168 | 2: sig.emit(args[0], args[1])
169 | 3: sig.emit(args[0], args[1], args[2])
170 | 4: sig.emit(args[0], args[1], args[2], args[3])
171 | 5: sig.emit(args[0], args[1], args[2], args[3], args[4])
172 | 6: sig.emit(args[0], args[1], args[2], args[3], args[4], args[5])
173 | _: push_error("Not implemented.")
174 |
175 | func _get_label(x: Variant) -> String:
176 | if x is String:
177 | return x.capitalize()
178 | elif x is Callable:
179 | return str(x.get_method()).capitalize()
180 | elif x is Dictionary:
181 | if "text" in x:
182 | if x.text is String:
183 | return x.text
184 | elif x.text is Callable:
185 | return x.text.call()
186 | else:
187 | return str(x.text)
188 | else:
189 | return _get_label(x.call)
190 | else:
191 | return "???"
192 |
193 | func _get_key_or_call(info: Dictionary, k: String, t: int, default):
194 | if k in info:
195 | if typeof(info[k]) == t:
196 | return info[k]
197 | elif info[k] is Callable:
198 | return info[k].call()
199 | else:
200 | print("TB_BUTTON: Shouldn't happen.")
201 | else:
202 | return default
203 |
204 | func _get_args_string(info: Dictionary):
205 | if not "args" in info:
206 | return ""
207 | var args = ""
208 | for a in info.args:
209 | if not args == "":
210 | args += ", "
211 | if a is String:
212 | args += '"%s"' % [a]
213 | else:
214 | args += str(a)
215 | return args
216 |
217 | func _call(x: Variant) -> Variant:
218 | if x is Dictionary:
219 | return _call(x.call)
220 |
221 | elif x is Callable:
222 | # prints(x.get_object(), x.get_object_id(), x.is_custom(), x.is_null(), x.is_standard(), x.is_valid())
223 | var got = x.call()
224 | # if x.is_custom():
225 | # return get_error_name(got)
226 | return got
227 |
228 | elif x is Array:
229 | var out := []
230 | for item in x:
231 | out.append(_call(item))
232 | return out
233 |
234 | elif x is String:
235 | # special internal editor actions.
236 | if x.begins_with("@"):
237 | var p = x.substr(1).split(";")
238 | match p[0]:
239 | "SCAN":
240 | pluginref.get_editor_interface().get_resource_filesystem().scan()
241 |
242 | "CREATE_AND_EDIT":
243 | var f = FileAccess.open(p[1], FileAccess.WRITE)
244 | f.store_string(p[2])
245 | f.close()
246 | var rf: EditorFileSystem = pluginref.get_editor_interface().get_resource_filesystem()
247 | rf.update_file(p[1])
248 | rf.scan()
249 | rf.scan_sources()
250 |
251 | pluginref.get_editor_interface().select_file(p[1])
252 | pluginref.get_editor_interface().edit_resource.call_deferred(load(p[1]))
253 |
254 | "SELECT_AND_EDIT":
255 | if FileAccess.file_exists(p[1]):
256 | pluginref.get_editor_interface().select_file(p[1])
257 | pluginref.get_editor_interface().edit_resource.call_deferred(load(p[1]))
258 | else:
259 | push_error("Nothing to select and edit at %s." % p[1])
260 |
261 | "SELECT_FILE":
262 | if FileAccess.file_exists(p[1]):
263 | pluginref.get_editor_interface().select_file(p[1])
264 | else:
265 | push_error("No file to select at %s." % p[1])
266 |
267 | "EDIT_RESOURCE":
268 | if FileAccess.file_exists(p[1]):
269 | pluginref.get_editor_interface().edit_resource.call_deferred(load(p[1]))
270 | else:
271 | push_error("No resource to edit at %s." % p[1])
272 |
273 | return null
274 |
275 | else:
276 | # as method
277 | if object.has_method(x):
278 | return object.call(x)
279 |
280 | # as signal
281 | elif object.has_signal(x):
282 | var error := object.emit_signal(x)
283 | var err_name := get_error_name(error)
284 | return "%s: %s (signal)" % [err_name, x]
285 |
286 | else:
287 | push_error("Hmm 1?")
288 | return null
289 | else:
290 | push_error("Hmm 2?")
291 | return null
292 |
293 | func _edit(file: String):
294 | pluginref.get_editor_interface().select_file(file)
295 | pluginref.get_editor_interface().edit_resource.call_deferred(ResourceLoader.load(file, "TextFile", 0))
296 |
297 | func _on_button_pressed(index: int):
298 | var got
299 | if check and check.button_pressed:
300 | got = []
301 | for info in all_info:
302 | got.append(_call(info))
303 | else:
304 | got = _call(all_info[index])
305 | if got != null:
306 | print("[tool_button]: ", got)
307 |
308 | static func get_error_name(error: int) -> String:
309 | match error:
310 | OK: return "Okay"
311 | FAILED: return "Generic"
312 | ERR_UNAVAILABLE: return "Unavailable"
313 | ERR_UNCONFIGURED: return "Unconfigured"
314 | ERR_UNAUTHORIZED: return "Unauthorized"
315 | ERR_PARAMETER_RANGE_ERROR: return "Parameter range"
316 | ERR_OUT_OF_MEMORY: return "Out of memory (OOM)"
317 | ERR_FILE_NOT_FOUND: return "File: Not found"
318 | ERR_FILE_BAD_DRIVE: return "File: Bad drive"
319 | ERR_FILE_BAD_PATH: return "File: Bad path"
320 | ERR_FILE_NO_PERMISSION: return "File: No permission"
321 | ERR_FILE_ALREADY_IN_USE: return "File: Already in use"
322 | ERR_FILE_CANT_OPEN: return "File: Can't open"
323 | ERR_FILE_CANT_WRITE: return "File: Can't write"
324 | ERR_FILE_CANT_READ: return "File: Can't read"
325 | ERR_FILE_UNRECOGNIZED: return "File: Unrecognized"
326 | ERR_FILE_CORRUPT: return "File: Corrupt"
327 | ERR_FILE_MISSING_DEPENDENCIES: return "File: Missing dependencies"
328 | ERR_FILE_EOF: return "File: End of file (EOF)"
329 | ERR_CANT_OPEN: return "Can't open"
330 | ERR_CANT_CREATE: return "Can't create"
331 | ERR_QUERY_FAILED: return "Query failed"
332 | ERR_ALREADY_IN_USE: return "Already in use"
333 | ERR_LOCKED: return "Locked"
334 | ERR_TIMEOUT: return "Timeout"
335 | ERR_CANT_CONNECT: return "Can't connect"
336 | ERR_CANT_RESOLVE: return "Can't resolve"
337 | ERR_CONNECTION_ERROR: return "Connection"
338 | ERR_CANT_ACQUIRE_RESOURCE: return "Can't acquire resource"
339 | ERR_CANT_FORK: return "Can't fork process"
340 | ERR_INVALID_DATA: return "Invalid data"
341 | ERR_INVALID_PARAMETER: return "Invalid parameter"
342 | ERR_ALREADY_EXISTS: return "Already exists"
343 | ERR_DOES_NOT_EXIST: return "Does not exist"
344 | ERR_DATABASE_CANT_READ: return "Database: Read"
345 | ERR_DATABASE_CANT_WRITE: return "Database: Write"
346 | ERR_COMPILATION_FAILED: return "Compilation failed"
347 | ERR_METHOD_NOT_FOUND: return "Method not found"
348 | ERR_LINK_FAILED: return "Linking failed"
349 | ERR_SCRIPT_FAILED: return "Script failed"
350 | ERR_CYCLIC_LINK: return "Cycling link (import cycle)"
351 | ERR_INVALID_DECLARATION: return "Invalid declaration"
352 | ERR_DUPLICATE_SYMBOL: return "Duplicate symbol"
353 | ERR_PARSE_ERROR: return "Parse"
354 | ERR_BUSY: return "Busy"
355 | ERR_SKIP: return "Skip"
356 | ERR_HELP: return "Help"
357 | ERR_BUG: return "Bug"
358 | ERR_PRINTER_ON_FIRE: return "Printer on fire. (This is an easter egg, no engine methods return this error code.)"
359 | _: return "ERROR %s???" % error
360 |
--------------------------------------------------------------------------------
/addons/tool_button/TB_InspectorPlugin.gd:
--------------------------------------------------------------------------------
1 | extends EditorInspectorPlugin
2 |
3 | var InspectorToolButton = preload("res://addons/tool_button/TB_Button.gd")
4 | var pluginref
5 | var default_node_signals := []
6 | var default_resource_signals := []
7 |
8 | const ALLOW_NODE_METHODS := [
9 | "get_class", "get_path", "raise", "get_groups",
10 | "get_owner", "get_process_priority", "get_scene_file_path"
11 | ]
12 | const ALLOW_RESOURCE_METHODS := [
13 | "get_class", "get_path"
14 | ]
15 | const BLOCK_RESOURCE_SIGNALS := [
16 | "setup_local_to_scene_requested"
17 | ]
18 |
19 | var inherited_method_list = []
20 | func _init(p):
21 | pluginref = p
22 | default_node_signals = Node.new().get_signal_list()\
23 | .filter(func(m): return len(m.args) == 0)\
24 | .map(func(m): return m.name)
25 | default_resource_signals = Resource.new().get_signal_list()\
26 | .filter(func(m): return len(m.args) == 0)\
27 | .map(func(m): return m.name)
28 |
29 | inherited_method_list += Node.new().get_method_list()
30 | inherited_method_list += Node2D.new().get_method_list()
31 | inherited_method_list += Control.new().get_method_list()
32 |
33 | func _can_handle(object) -> bool:
34 | return true
35 |
36 | # buttons defined in _get_tool_buttons show at the top
37 | func _parse_begin(object: Object) -> void:
38 | if object.has_method("_get_tool_buttons"):
39 | var methods
40 | if object is Resource:
41 | methods = object.get_script()._get_tool_buttons()
42 | else:
43 | methods = object._get_tool_buttons()
44 |
45 | if methods:
46 | for method in methods:
47 | add_custom_control(InspectorToolButton.new(object, method, pluginref))
48 |
49 | var object_category_cache = []
50 | func _parse_category(object: Object, category: String) -> void:
51 | var allowed_categories = []
52 | if ProjectSettings.get_setting("show_default_buttons"):
53 | allowed_categories = ["Node", "Resource"]
54 |
55 | # var obj_script = object.get_script()
56 | # if obj_script:
57 | # var has_exports = "@export" in obj_script.source_code
58 | # var attached_script_category = ""
59 | # if has_exports:
60 | # attached_script_category = obj_script.resource_path.get_file()
61 | # object_category_cache.append(attached_script_category)
62 | # allowed_categories.append(attached_script_category)
63 |
64 | if not category in allowed_categories:
65 | return
66 |
67 | var flags := {}
68 | if object.has_method("_get_tool_button_flags"):
69 | flags = object._get_tool_button_flags()
70 |
71 | var methods := object.get_method_list().filter(
72 | func(m: Dictionary):
73 | if m in inherited_method_list:
74 | return false
75 | if m.name[0] == "@":
76 | return false
77 | if not flags.get("private", false) and m.name[0] in "_":
78 | return false
79 | if not flags.get("getters", false) and m.name.begins_with("get_"):
80 | return false
81 | if not flags.get("setters", false) and m.name.begins_with("set_"):
82 | return false
83 | return true
84 | )
85 | # sort on name
86 | methods.sort_custom(func(a, b): return a.name < b.name)
87 | for method in methods:
88 | add_custom_control(InspectorToolButton.new(object, {
89 | tint=Color.PALE_TURQUOISE,
90 | call=method.name,
91 | }, pluginref))
92 |
93 | if category == "Node":
94 | for method in ALLOW_NODE_METHODS:
95 | add_custom_control(InspectorToolButton.new(object, {
96 | tint=Color.PALE_TURQUOISE.lerp(Color.DARK_GRAY, .5),
97 | call=method,
98 | }, pluginref))
99 | elif category == "Resource":
100 | for method in ALLOW_RESOURCE_METHODS:
101 | add_custom_control(InspectorToolButton.new(object, {
102 | tint=Color.PALE_TURQUOISE.lerp(Color.DARK_GRAY, .5),
103 | call=method,
104 | }, pluginref))
105 |
106 | var parent_signals = ClassDB.class_get_signal_list(ClassDB.get_parent_class(object.get_class()))\
107 | .filter(func(s): return len(s.args) == 0)\
108 | .map(func(x): return x.name)
109 | parent_signals.sort_custom(func(a, b): return a < b)
110 |
111 | var signals = object.get_signal_list()\
112 | .filter(func(s): return len(s.args) == 0 and not s.name in parent_signals)\
113 | .map(func(x): return x.name)
114 | if category == "Node":
115 | signals = signals.filter(func(x): return not x in default_node_signals)
116 | elif category == "Resource":
117 | signals = signals.filter(func(x): return not x in default_resource_signals)
118 |
119 | signals.sort_custom(func(a, b): return a < b)
120 |
121 | for sig in signals:
122 | add_custom_control(InspectorToolButton.new(object, {
123 | tint=Color.PALE_GOLDENROD,
124 | call=sig
125 | }, pluginref))
126 |
127 | if category == "Node":
128 | parent_signals = parent_signals.filter(func(x): return not x in default_node_signals)
129 | elif category == "Resource":
130 | parent_signals = parent_signals.filter(func(x): return not x in default_resource_signals)
131 |
132 | for sig in parent_signals:
133 | add_custom_control(InspectorToolButton.new(object, {
134 | tint=Color.PALE_GOLDENROD.lerp(Color.DARK_GRAY, .5),
135 | call=sig
136 | }, pluginref))
137 |
--------------------------------------------------------------------------------
/addons/tool_button/TB_Plugin.gd:
--------------------------------------------------------------------------------
1 | @tool
2 | extends EditorPlugin
3 |
4 | var plugin
5 |
6 | func _enter_tree():
7 | plugin = preload("res://addons/tool_button/TB_InspectorPlugin.gd").new(self)
8 | ProjectSettings.set_setting("show_default_buttons", false)
9 | add_inspector_plugin(plugin)
10 |
11 | func _exit_tree():
12 | remove_inspector_plugin(plugin)
13 |
14 | func rescan_filesystem():
15 | var fs = get_editor_interface().get_resource_filesystem()
16 | fs.update_script_classes()
17 | fs.scan_sources()
18 | fs.scan()
19 |
--------------------------------------------------------------------------------
/addons/tool_button/plugin.cfg:
--------------------------------------------------------------------------------
1 | [plugin]
2 |
3 | name="ToolButtonPlugin"
4 | description="Add tool buttons wherever you need them."
5 | author="teebar"
6 | version="1.4"
7 | script="TB_Plugin.gd"
8 |
--------------------------------------------------------------------------------
/readme/.gdignore:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/readme/bottom_buttons.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/teebarjunk/godot-4.0-tool_button/4d6e539c558a76fcca11568f338f8ea0744239dd/readme/bottom_buttons.png
--------------------------------------------------------------------------------
/readme/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/teebarjunk/godot-4.0-tool_button/4d6e539c558a76fcca11568f338f8ea0744239dd/readme/logo.png
--------------------------------------------------------------------------------
/readme/preview2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/teebarjunk/godot-4.0-tool_button/4d6e539c558a76fcca11568f338f8ea0744239dd/readme/preview2.png
--------------------------------------------------------------------------------
/readme/preview3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/teebarjunk/godot-4.0-tool_button/4d6e539c558a76fcca11568f338f8ea0744239dd/readme/preview3.png
--------------------------------------------------------------------------------