├── .gitignore ├── LICENSE ├── README.md ├── demo.gif ├── icon.png ├── plugin.cfg └── plugin.gd /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Godot-specific ignores 3 | .import/ 4 | export.cfg 5 | export_presets.cfg 6 | 7 | # Mono-specific ignores 8 | .mono/ 9 | data_*/ 10 | 11 | *.import 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Ryan Badour 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 | # NodeConnectorGodot 2 | Plugin for the Godot editor that provides a convenient way to connect a bunch of UI elements to a script. 3 | 4 | For each of the selected nodes it adds a line to your scene's script like: 5 | 6 | `onready var button = find_node("Button");` 7 | 8 | Will also add a signal handle for Buttons and TextEdit nodes for their "pressed" and "text_changed" events. 9 | 10 | ### Demo: 11 | 12 | ![Gif Demo](demo.gif) 13 | 14 | 15 | Note: There is one annoyance, that when modifying an existing script Godot doesn't pickup the changes until you unfocus Godot and reopen it. 16 | 17 | Icon made by [Freepik](https://www.flaticon.com/authors/freepik) from www.flaticon.com 18 | 19 | 20 | ### How to install: 21 | Once downloaded to your project, create a folder named 'addons' if necessary, then inside 'addons' 22 | create a folder for this plugin ('node-connector' is a good name), move the 'plugin.gd' 23 | script into that directory, under Project Settings > Plugins find the plugin titled 'NodeConnector' 24 | and switch its state to Active. 25 | 26 | Alternatively if you use git you could add this plugin as a submodule in the folder mentioned above. 27 | 28 | Ex. `git submodule add https://github.com/Rybadour/NodeConnectorGodot.git addons/node-connector` 29 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rybadour/NodeConnectorGodot/cef5d829a9fb25e36aa56a28e63ff6915501c0f0/demo.gif -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rybadour/NodeConnectorGodot/cef5d829a9fb25e36aa56a28e63ff6915501c0f0/icon.png -------------------------------------------------------------------------------- /plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="NodeConnector" 4 | description="Provides an option when multi-selecting nodes to 5 | generate code in the current scene's script. For buttons 6 | it generates pressed signal handlers." 7 | author="Ryan Badour" 8 | version="" 9 | script="plugin.gd" 10 | -------------------------------------------------------------------------------- /plugin.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends EditorPlugin 3 | 4 | const PLUGIN_NAME = "NodeConntectorPlugin"; 5 | const MENU_ITEM = "Connect selected Nodes (F9)"; 6 | 7 | func _enter_tree(): 8 | add_tool_menu_item(MENU_ITEM, self, "onUse"); 9 | 10 | 11 | #Adding Support for shortcut instead of going to menu 12 | func _input(event): 13 | if event is InputEventKey: 14 | if event.is_pressed() and event.scancode == KEY_F9: 15 | onUse(self) 16 | 17 | 18 | func onUse(evt): 19 | var editor = get_editor_interface(); 20 | var selectedNodes = editor.get_selection().get_selected_nodes(); 21 | if selectedNodes.size() == 0: 22 | logError("No nodes selected."); 23 | return; 24 | 25 | # Load or create the current scene script 26 | var scene = editor.get_edited_scene_root(); 27 | var sceneScript:Script = scene.get_script(); 28 | var scriptPath = ""; 29 | var scriptSource = ""; 30 | var isBuiltInScript = false; 31 | if sceneScript == null: 32 | scriptPath = getFilePathPrefix(scene.filename) + scene.name + ".gd"; 33 | else: 34 | scriptPath = sceneScript.resource_path; 35 | if scriptPath.match("*.tscn::*"): 36 | isBuiltInScript = true; 37 | 38 | if sceneScript != null && sceneScript.has_source_code(): 39 | scriptSource = sceneScript.source_code; 40 | else: 41 | scriptSource = getDefaultScript(scene); 42 | 43 | # Generate the lines of code to insert 44 | var headerLines = ['']; 45 | var readyLines = []; 46 | var footerLines = []; 47 | var nodeNames = []; 48 | for node in selectedNodes: 49 | var nodeName: String = node.name; 50 | var nodeVariable = getNodeVariableName(nodeName); 51 | nodeNames.append(nodeName); 52 | headerLines.append('onready var %s = find_node("%s");' % [nodeVariable, nodeName]); 53 | 54 | var signalName = ""; 55 | if node is Button: 56 | signalName = "pressed"; 57 | elif node is TextEdit: 58 | signalName = "text_changed"; 59 | 60 | if signalName != "": 61 | var handleName = "_on" + nodeName + getUpperCaseSignal(signalName); 62 | readyLines.append('\t%s.connect("%s", self, "%s");' % [nodeVariable, signalName, handleName]); 63 | footerLines.append('\n'); 64 | footerLines.append('func %s():' % [handleName]); 65 | footerLines.append('\tpass;'); 66 | 67 | # Insert lines of code into sections of the script 68 | var headerPos = findPositionAfterLine(scriptSource, "extends *"); 69 | if headerPos == -1: 70 | logError("Could not find an extends statement in script."); 71 | return; 72 | scriptSource = modifySection(scriptSource, headerPos, headerLines); 73 | 74 | var readyPos = findPositionAfterLine(scriptSource, "func _ready(*"); 75 | if readyPos == -1: 76 | logError("Could not find a _ready function in script."); 77 | return; 78 | scriptSource = modifySection(scriptSource, readyPos, readyLines); 79 | 80 | scriptSource = modifySection(scriptSource, scriptSource.length(), footerLines); 81 | 82 | # Actually write to scene script file 83 | if isBuiltInScript: 84 | if !writeToSceneFile(scene.filename, scriptSource): 85 | return; 86 | else: 87 | if !writeToFile(scriptPath, scriptSource): 88 | return; 89 | 90 | var updatedScript = load(scriptPath) as Script; 91 | scene.set_script(updatedScript); 92 | 93 | var note = ""; 94 | if isBuiltInScript: 95 | note = " (built-in script)"; 96 | 97 | # Success! 98 | print("%s: Added %s to scene script '%s'%s." % [PLUGIN_NAME, str(nodeNames), scriptPath, note]); 99 | if isBuiltInScript: 100 | print( 101 | "%s: Built-in scripts cannot be refreshed without closing the scene and re-opening." + 102 | "Please do that now to see the changes to the script." 103 | ); 104 | else: 105 | print("%s: Please unfocus Godot window and refocus to see changes to script." % PLUGIN_NAME); 106 | 107 | 108 | func writeToSceneFile(filename, scriptSource): 109 | var fullSource = readFile(filename); 110 | 111 | var regex = RegEx.new(); 112 | regex.compile('(?s)(script\/source = ").*(\n"\n)'); 113 | var escaped = scriptSource.replace('"', '\\"'); 114 | var modified = regex.sub(fullSource, "$1" + escaped + "$2"); 115 | 116 | return writeToFile(filename, modified); 117 | 118 | 119 | func readFile(path): 120 | var f = File.new(); 121 | var err = f.open(path, File.READ); 122 | if err > 0: 123 | logError("Cannot open %s for writing. Error code: %d" % [path, err]); 124 | return null; 125 | var source = f.get_as_text(); 126 | f.close(); 127 | return source; 128 | 129 | 130 | func writeToFile(path, source): 131 | var f = File.new(); 132 | var err = f.open(path, File.WRITE); 133 | if err > 0: 134 | logError("Cannot open %s for writing. Error code: %d" % [path, err]); 135 | return false; 136 | f.store_string(source); 137 | f.close(); 138 | return true; 139 | 140 | 141 | func modifySection(source, pos, sourceLines): 142 | if sourceLines.size() == 0: 143 | return source; 144 | 145 | var newSource = ''; 146 | for line in sourceLines: 147 | newSource += line + '\n'; 148 | 149 | return source.insert(pos, newSource); 150 | 151 | 152 | func findPositionAfterLine(source: String, lineMatch: String) -> int: 153 | var lines = source.split("\n"); 154 | var position = 0; 155 | for line in lines: 156 | position += line.length() + 1; 157 | if line.match(lineMatch): 158 | return position; 159 | 160 | return -1; 161 | 162 | 163 | func logError(message): 164 | printerr(PLUGIN_NAME + ": " + message); 165 | 166 | 167 | func getDefaultScript(node): 168 | var lines = PoolStringArray(); 169 | lines.append("extends Control"); 170 | lines.append(""); 171 | lines.append("func _ready():"); 172 | lines.append("\tpass;"); 173 | return lines.join('\n'); 174 | 175 | 176 | func getFilePathPrefix(fileName): 177 | var parts = fileName.split('/'); 178 | parts.remove(parts.size() - 1); 179 | return parts.join('/') + '/'; 180 | 181 | 182 | func getNodeVariableName(name): 183 | name = name.replace(" ", "_"); 184 | var first = name.substr(0, 1); 185 | return first.to_lower() + name.substr(1); 186 | 187 | 188 | func getUpperCaseSignal(name): 189 | var parts = name.split("_"); 190 | var newName = ""; 191 | for part in parts: 192 | var first = part.substr(0, 1); 193 | newName += first.to_upper() + part.substr(1); 194 | return newName; 195 | 196 | 197 | func _exit_tree(): 198 | remove_tool_menu_item(MENU_ITEM); 199 | --------------------------------------------------------------------------------