├── .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 | ![ReadMe](readme/bottom_buttons.png "Preview") 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 | ![](readme/preview3.png) 33 | 34 | ## Advanced Example 35 | 36 | To specify buttons that show above the inspector, add a `_get_tool_buttons` func. 37 | 38 | ![ReadMe](readme/preview2.png "Preview") 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 --------------------------------------------------------------------------------