├── LICENSE.md ├── README.md ├── icons ├── haxe.svg └── haxe.svg.import ├── plugin.cfg ├── scenes ├── about.tscn ├── building.tscn ├── new_script.tscn └── tab.tscn └── scripts ├── Setup.hx ├── building.gd ├── constants.gd ├── editor_property.gd ├── haxe.gd ├── inspector_plugin.gd ├── new_script.gd └── tab.gd /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Valentin Lemière 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://raw.github.com/HaxeGodot/godot/main/.github/logo.png) 2 | 3 | [haxe externs](https://github.com/HaxeGodot/godot) | [editor plugin](https://github.com/HaxeGodot/editor-plugin) | [demo](https://github.com/HaxeGodot/squash-the-creeps-3d) | [api doc](https://haxegodot.github.io/godot/) | [discussions](https://github.com/HaxeGodot/godot/discussions) 4 | 5 | # Godot Editor Haxe Support Plugin 6 | 7 | Godot 3.3 engine editor plugin to help with Haxe development. 8 | 9 | The plugin is still in alpha, open an [issue](https://github.com/HaxeGodot/editor-plugin/issues) for bug reports or feature requests. 10 | 11 | ## Installation 12 | 13 | The plugin isn't yet available on the godot asset library, to install it you can either: 14 | 15 | * [download this repository](https://github.com/HaxeGodot/editor-plugin/archive/refs/heads/main.zip) and extract it in the `addons/haxe` folder of your project 16 | 17 | You need to remove the `editor-plugin-main` folder added by github: have `addons/haxe/plugin.cfg` not `addons/haxe/editor-plugin-main/plugin.cfg` 18 | * add it as a submodule `git submodule add https://github.com/HaxeGodot/editor-plugin.git addons/haxe` 19 | 20 | You need to enable the plugin by going in the Project -> Project Settings menu, Plugins tab, and checking the Enabled box for the Haxe plugin. 21 | 22 | ## Setup 23 | 24 | Haxe support requires Godot C#, if it hasn't been setup click on Project -> Tools -> C# -> Create C# solution. 25 | 26 | The plugin can setup by clicking on the Project -> Tools -> Haxe -> Setup menu. 27 | 28 | This will check for the presence of the godot haxelib, update the C# solution and add a hxml. 29 | If the project already contains some of these files the setup will be stopped. 30 | 31 | You can also do a [manual setup](#manual-setup) if you want more control. 32 | 33 | ## Haxe scripts 34 | 35 | You can add/load/remove a Haxe script on a node by clicking on it, and in the inspector in Node -> Script -> Haxe Script click on the resource box. 36 | 37 | When creating or clicking Edit on an script it'll open in your editor. By default it is configured for VSCode, and can be changed in Project -> Project Settings -> Haxe -> External Editor. For now only `None` and `VSCode` are supported. 38 | 39 | ### Building 40 | 41 | You need to build the Haxe code before launching your game, you can do that: 42 | 43 | * by manually using the hxml `haxe build.hxml` 44 | * through your editor 45 | * directly in the Godot editor in the bottom tab Haxe -> Build Haxe Project 46 | 47 | Note: The files in `scripts/` must all define their main type, for a file `Foo.hx` you must have the type `Foo`, otherwise compilation will fail. 48 | 49 | ## Manual setup 50 | 51 | Example hxml: 52 | ```hxml 53 | --cs build 54 | --define net-ver=50 55 | --define no-compilation 56 | --define analyzer-optimize 57 | --class-path scripts 58 | --library godot 59 | --macro godot.Godot.buildProject() 60 | --dce full 61 | ``` 62 | 63 | Modify the `` of the `csproj` file: 64 | ```xml 65 | true 66 | netstandard2.1 67 | ``` 68 | 69 | ## License 70 | 71 | The plugin is MIT licensed. 72 | -------------------------------------------------------------------------------- /icons/haxe.svg: -------------------------------------------------------------------------------- 1 | 2 | 18 | 20 | 21 | 23 | image/svg+xml 24 | 26 | 27 | 28 | 29 | 30 | 32 | 57 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /icons/haxe.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="StreamTexture" 5 | path="res://.import/haxe.svg-b666ced8be37e93da1cbc844142b1cd5.stex" 6 | metadata={ 7 | "vram_texture": false 8 | } 9 | 10 | [deps] 11 | 12 | source_file="res://addons/haxe/icons/haxe.svg" 13 | dest_files=[ "res://.import/haxe.svg-b666ced8be37e93da1cbc844142b1cd5.stex" ] 14 | 15 | [params] 16 | 17 | compress/mode=0 18 | compress/lossy_quality=0.7 19 | compress/hdr_mode=0 20 | compress/bptc_ldr=0 21 | compress/normal_map=0 22 | flags/repeat=0 23 | flags/filter=false 24 | flags/mipmaps=false 25 | flags/anisotropic=false 26 | flags/srgb=2 27 | process/fix_alpha_border=true 28 | process/premult_alpha=false 29 | process/HDR_as_SRGB=false 30 | process/invert_color=false 31 | process/normal_map_invert_y=false 32 | stream=false 33 | size_limit=0 34 | detect_3d=true 35 | svg/scale=1.0 36 | -------------------------------------------------------------------------------- /plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="Haxe" 4 | description="Haxe support for the Godot engine." 5 | author="ibilon" 6 | version="0.1.0" 7 | script="scripts/haxe.gd" 8 | -------------------------------------------------------------------------------- /scenes/about.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene format=2] 2 | 3 | [node name="About" type="AcceptDialog"] 4 | margin_right = 83.0 5 | margin_bottom = 58.0 6 | window_title = "About Haxe support" 7 | dialog_text = "This plugin adds support for the Haxe programming language. 8 | Haxe support is in alpha. 9 | 10 | Discuss usage at: https://github.com/HaxeGodot/godot/discussions 11 | 12 | Report bugs about the editor plugin at: https://github.com/HaxeGodot/editor-plugin/issues/ 13 | Report bugs about the code support at: https://github.com/HaxeGodot/godot/issues/ 14 | " 15 | -------------------------------------------------------------------------------- /scenes/building.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=2] 2 | 3 | [ext_resource path="res://addons/haxe/scripts/building.gd" type="Script" id=1] 4 | 5 | [sub_resource type="StyleBoxFlat" id=1] 6 | bg_color = Color( 0.219608, 0.211765, 0.227451, 1 ) 7 | border_width_left = 3 8 | border_width_top = 3 9 | border_width_right = 3 10 | border_width_bottom = 3 11 | border_color = Color( 0.270588, 0.270588, 0.270588, 1 ) 12 | corner_radius_top_left = 4 13 | 14 | [node name="Control" type="Control"] 15 | anchor_left = 0.5 16 | anchor_top = 0.5 17 | anchor_right = 0.5 18 | anchor_bottom = 0.5 19 | margin_left = -152.0 20 | margin_top = -38.5 21 | margin_right = 152.0 22 | margin_bottom = 38.5 23 | script = ExtResource( 1 ) 24 | 25 | [node name="Background" type="Panel" parent="."] 26 | anchor_right = 1.0 27 | anchor_bottom = 1.0 28 | custom_styles/panel = SubResource( 1 ) 29 | 30 | [node name="Label" type="Label" parent="."] 31 | anchor_right = 1.0 32 | margin_bottom = 26.0 33 | text = "Building Haxe project..." 34 | align = 1 35 | valign = 1 36 | autowrap = true 37 | clip_text = true 38 | 39 | [node name="ProgressBar" type="ProgressBar" parent="."] 40 | anchor_top = 0.5 41 | anchor_right = 1.0 42 | anchor_bottom = 0.5 43 | margin_left = 11.0 44 | margin_top = -6.5 45 | margin_right = -11.0 46 | margin_bottom = 7.5 47 | max_value = 1.0 48 | -------------------------------------------------------------------------------- /scenes/new_script.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=2] 2 | 3 | [ext_resource path="res://addons/haxe/scripts/new_script.gd" type="Script" id=1] 4 | 5 | [node name="NewScript" type="WindowDialog"] 6 | margin_right = 350.0 7 | margin_bottom = 270.0 8 | size_flags_horizontal = 3 9 | size_flags_vertical = 3 10 | window_title = "Attach Node Haxe Script" 11 | script = ExtResource( 1 ) 12 | __meta__ = { 13 | "_edit_use_anchors_": false 14 | } 15 | 16 | [node name="MarginContainer" type="MarginContainer" parent="."] 17 | anchor_right = 1.0 18 | anchor_bottom = 1.0 19 | margin_left = 10.0 20 | margin_top = 10.0 21 | margin_right = -10.0 22 | margin_bottom = -10.0 23 | __meta__ = { 24 | "_edit_use_anchors_": false 25 | } 26 | 27 | [node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"] 28 | margin_right = 330.0 29 | margin_bottom = 250.0 30 | __meta__ = { 31 | "_edit_use_anchors_": false 32 | } 33 | 34 | [node name="GridContainer" type="GridContainer" parent="MarginContainer/VBoxContainer"] 35 | margin_right = 330.0 36 | margin_bottom = 52.0 37 | columns = 2 38 | __meta__ = { 39 | "_edit_use_anchors_": false 40 | } 41 | 42 | [node name="ClassLabel" type="Label" parent="MarginContainer/VBoxContainer/GridContainer"] 43 | margin_top = 5.0 44 | margin_right = 54.0 45 | margin_bottom = 19.0 46 | text = "Inherits:" 47 | 48 | [node name="ClassValue" type="LineEdit" parent="MarginContainer/VBoxContainer/GridContainer"] 49 | margin_left = 58.0 50 | margin_right = 330.0 51 | margin_bottom = 24.0 52 | size_flags_horizontal = 3 53 | text = "VBoxContainer" 54 | 55 | [node name="PathLabel" type="Label" parent="MarginContainer/VBoxContainer/GridContainer"] 56 | margin_top = 33.0 57 | margin_right = 54.0 58 | margin_bottom = 47.0 59 | text = "Path:" 60 | __meta__ = { 61 | "_edit_use_anchors_": false 62 | } 63 | 64 | [node name="Path" type="HBoxContainer" parent="MarginContainer/VBoxContainer/GridContainer"] 65 | margin_left = 58.0 66 | margin_top = 28.0 67 | margin_right = 330.0 68 | margin_bottom = 52.0 69 | 70 | [node name="PathValue" type="LineEdit" parent="MarginContainer/VBoxContainer/GridContainer/Path"] 71 | margin_right = 256.0 72 | margin_bottom = 24.0 73 | size_flags_horizontal = 3 74 | text = "res://scripts/VBoxContainer.hx" 75 | 76 | [node name="Load" type="Button" parent="MarginContainer/VBoxContainer/GridContainer/Path"] 77 | margin_left = 260.0 78 | margin_right = 272.0 79 | margin_bottom = 24.0 80 | 81 | [node name="PaddingTop" type="Control" parent="MarginContainer/VBoxContainer"] 82 | margin_top = 56.0 83 | margin_right = 330.0 84 | margin_bottom = 66.0 85 | rect_min_size = Vector2( 0, 10 ) 86 | 87 | [node name="TextEdit" type="RichTextLabel" parent="MarginContainer/VBoxContainer"] 88 | margin_top = 70.0 89 | margin_right = 330.0 90 | margin_bottom = 212.0 91 | rect_min_size = Vector2( 0, 110 ) 92 | size_flags_vertical = 3 93 | bbcode_enabled = true 94 | scroll_active = false 95 | 96 | [node name="PaddingBottom" type="Control" parent="MarginContainer/VBoxContainer"] 97 | margin_top = 216.0 98 | margin_right = 330.0 99 | margin_bottom = 226.0 100 | rect_min_size = Vector2( 0, 10 ) 101 | 102 | [node name="Buttons" type="HBoxContainer" parent="MarginContainer/VBoxContainer"] 103 | margin_top = 230.0 104 | margin_right = 330.0 105 | margin_bottom = 250.0 106 | alignment = 1 107 | __meta__ = { 108 | "_edit_use_anchors_": false 109 | } 110 | 111 | [node name="Left" type="Button" parent="MarginContainer/VBoxContainer/Buttons"] 112 | margin_left = 92.0 113 | margin_right = 146.0 114 | margin_bottom = 20.0 115 | text = "Cancel" 116 | 117 | [node name="Padding" type="Control" parent="MarginContainer/VBoxContainer/Buttons"] 118 | margin_left = 150.0 119 | margin_right = 180.0 120 | margin_bottom = 20.0 121 | rect_min_size = Vector2( 30, 0 ) 122 | __meta__ = { 123 | "_edit_use_anchors_": false 124 | } 125 | 126 | [node name="Right" type="Button" parent="MarginContainer/VBoxContainer/Buttons"] 127 | margin_left = 184.0 128 | margin_right = 237.0 129 | margin_bottom = 20.0 130 | text = "Create" 131 | -------------------------------------------------------------------------------- /scenes/tab.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=2] 2 | 3 | [ext_resource path="res://addons/haxe/scripts/tab.gd" type="Script" id=1] 4 | 5 | [node name="Tab" type="Control"] 6 | margin_right = 40.0 7 | margin_bottom = 40.0 8 | rect_min_size = Vector2( 0, 200 ) 9 | script = ExtResource( 1 ) 10 | __meta__ = { 11 | "_edit_use_anchors_": false 12 | } 13 | 14 | [node name="VBoxContainer" type="VBoxContainer" parent="."] 15 | anchor_right = 1.0 16 | anchor_bottom = 1.0 17 | rect_min_size = Vector2( 0, 200 ) 18 | __meta__ = { 19 | "_edit_use_anchors_": false 20 | } 21 | 22 | [node name="Button" type="Button" parent="VBoxContainer"] 23 | margin_right = 130.0 24 | margin_bottom = 20.0 25 | size_flags_horizontal = 0 26 | text = "Build Haxe Project" 27 | 28 | [node name="TextLog" type="TextEdit" parent="VBoxContainer"] 29 | margin_top = 24.0 30 | margin_right = 130.0 31 | margin_bottom = 200.0 32 | size_flags_horizontal = 3 33 | size_flags_vertical = 3 34 | readonly = true 35 | context_menu_enabled = false 36 | -------------------------------------------------------------------------------- /scripts/Setup.hx: -------------------------------------------------------------------------------- 1 | import sys.io.Process; 2 | import haxe.xml.Access; 3 | import sys.FileSystem; 4 | import sys.io.File; 5 | 6 | using StringTools; 7 | 8 | class Setup { 9 | public static function main() { 10 | // Checking haxelib for godot externs. 11 | final haxelibCheck = new Process("haxelib", ["path", "godot"]); 12 | if (haxelibCheck.exitCode() != 0) { 13 | Sys.print("haxelib"); 14 | return; 15 | } 16 | 17 | // Find unique csproj file. 18 | var csproj = null; 19 | 20 | for (entry in FileSystem.readDirectory(".")) { 21 | if (FileSystem.isDirectory(entry)) { 22 | continue; 23 | } 24 | 25 | if (entry.endsWith(".csproj")) { 26 | if (csproj != null) { 27 | Sys.print("multiple_csproj"); 28 | return; 29 | } 30 | 31 | csproj = entry; 32 | } 33 | } 34 | 35 | if (csproj == null) { 36 | Sys.print("csproj"); 37 | return; 38 | } 39 | 40 | // Dirty check. 41 | final dirty = ["build.hxml", "build/", "scripts/"].filter(entry -> FileSystem.exists(entry)); 42 | 43 | if (dirty.length != 0) { 44 | Sys.print("dirty:" + dirty.join(" ")); 45 | return; 46 | } 47 | 48 | // Update csproj file. 49 | final csprojData = new Access(Xml.parse(File.getContent(csproj))); 50 | final propertyGroup = csprojData.node.Project.node.PropertyGroup; 51 | 52 | for (property in propertyGroup.elements) { 53 | switch (property.name) { 54 | case "AllowUnsafeBlocks", "TargetFramework": 55 | propertyGroup.x.removeChild(property.x); 56 | 57 | default: 58 | } 59 | } 60 | 61 | propertyGroup.x.addChild(Xml.parse("true")); 62 | propertyGroup.x.addChild(Xml.parse("netstandard2.1")); 63 | 64 | File.saveContent(csproj, XmlPrinter.print(csprojData.x)); 65 | 66 | // Create project. 67 | FileSystem.createDirectory("scripts"); 68 | File.saveContent("scripts/import.hx", "import godot.*;\nimport godot.GD.*;\n\nusing godot.Utils;\n"); 69 | File.saveContent("build.hxml", "--cs build\n--define net-ver=50\n--define no-compilation\n--define analyzer-optimize\n--class-path scripts\n--library godot\n--macro godot.Godot.buildProject()\n--dce full\n"); 70 | 71 | final ret = Sys.command("haxe", ["build.hxml"]); 72 | if (ret == 0) { 73 | Sys.print("ok"); 74 | } 75 | } 76 | } 77 | 78 | // Modified version of haxe.xml.Printer 79 | class XmlPrinter { 80 | static public function print(xml:Xml) { 81 | final printer = new XmlPrinter(); 82 | printer.writeNode(xml, ""); 83 | return printer.output.toString(); 84 | } 85 | 86 | var output:StringBuf; 87 | 88 | function new() { 89 | output = new StringBuf(); 90 | } 91 | 92 | function writeNode(value:Xml, indent:String) { 93 | switch (value.nodeType) { 94 | case CData: 95 | write(indent + ""); 98 | newline(); 99 | case Comment: 100 | var commentContent = value.nodeValue; 101 | commentContent = ~/[\n\r\t]+/g.replace(commentContent, ""); 102 | commentContent = ""; 103 | write(indent); 104 | write(StringTools.trim(commentContent)); 105 | newline(); 106 | case Document: 107 | for (child in value) { 108 | writeNode(child, indent); 109 | } 110 | case Element: 111 | write(indent + "<"); 112 | write(value.nodeName); 113 | for (attribute in value.attributes()) { 114 | write(" " + attribute + "=\""); 115 | write(StringTools.htmlEscape(value.get(attribute), true)); 116 | write("\""); 117 | } 118 | if (hasChildren(value)) { 119 | final textOnly = hasTextOnly(value); 120 | write(">"); 121 | if (!textOnly) { 122 | newline(); 123 | } 124 | for (child in value) { 125 | writeNode(child, textOnly ? "" : (indent + " ")); 126 | } 127 | write((textOnly ? "" : indent) + ""); 130 | newline(); 131 | } else { 132 | write("/>"); 133 | newline(); 134 | } 135 | case PCData: 136 | final nodeValue = value.nodeValue.trim(); 137 | if (nodeValue.length != 0) { 138 | write(indent + StringTools.htmlEscape(nodeValue)); 139 | } 140 | case ProcessingInstruction: 141 | write(""); 142 | newline(); 143 | case DocType: 144 | write(""); 145 | newline(); 146 | } 147 | } 148 | 149 | inline function write(input:String) { 150 | output.add(input); 151 | } 152 | 153 | inline function newline() { 154 | output.add("\n"); 155 | } 156 | 157 | function hasTextOnly(value:Xml):Bool { 158 | for (child in value) { 159 | switch (child.nodeType) { 160 | case PCData: 161 | default: 162 | return false; 163 | } 164 | } 165 | return true; 166 | } 167 | 168 | function hasChildren(value:Xml):Bool { 169 | for (child in value) { 170 | switch (child.nodeType) { 171 | case Element, PCData: 172 | return true; 173 | case CData, Comment: 174 | if (StringTools.ltrim(child.nodeValue).length != 0) { 175 | return true; 176 | } 177 | case _: 178 | } 179 | } 180 | return false; 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /scripts/building.gd: -------------------------------------------------------------------------------- 1 | tool 2 | class_name Building 3 | 4 | extends Control 5 | 6 | func build_haxe_project(): 7 | print("Building haxe project..."); 8 | 9 | var res = OS.execute("haxe", ["build.hxml"], true); 10 | 11 | $ProgressBar.value = 1 12 | yield(VisualServer, 'frame_post_draw') 13 | 14 | print("Project builded with code: ", res) 15 | 16 | queue_free() 17 | -------------------------------------------------------------------------------- /scripts/constants.gd: -------------------------------------------------------------------------------- 1 | tool 2 | class_name HaxePluginConstants 3 | 4 | const SETTING_HIDE_NATIVE_SCRIPT_FIELD := "haxe/hide_native_script_field" 5 | const SETTING_EXTERNAL_EDITOR := "haxe/external_editor" 6 | const BUILD_ON_PLAY := "haxe/build_on_play" 7 | -------------------------------------------------------------------------------- /scripts/editor_property.gd: -------------------------------------------------------------------------------- 1 | tool 2 | class_name HaxePluginEditorProperty 3 | extends EditorProperty 4 | 5 | var haxe_icon := preload("res://addons/haxe/icons/haxe.svg") 6 | var new_script_dialog := preload("res://addons/haxe/scenes/new_script.tscn") 7 | 8 | var base:Control 9 | var object:Node 10 | var script_name := "" 11 | var script_path := "" 12 | var b:MenuButton 13 | var b2:MenuButton 14 | 15 | func setup(base:Control, object:Node) -> void: 16 | self.base = base 17 | self.object = object 18 | label = "Haxe Script" 19 | 20 | var h := HBoxContainer.new() 21 | 22 | # TODO revert icon 23 | b = MenuButton.new() 24 | b.flat = true 25 | h.add_child(b) 26 | 27 | b2 = MenuButton.new() 28 | b2.flat = true 29 | b2.icon = base.get_icon("GuiDropdown", "EditorIcons") 30 | h.add_child(b2) 31 | 32 | add_child(h) 33 | 34 | update_property() 35 | 36 | func setup_menu(base:Control, button:MenuButton, has_script:bool) -> void: 37 | if not button.is_connected("gui_input", self, "on_menu_gui"): 38 | button.connect("gui_input", self, "on_menu_gui") 39 | 40 | var menu := button.get_popup() 41 | 42 | for i in range(menu.get_item_count()): 43 | menu.remove_item(0) 44 | 45 | if not has_script: 46 | menu.add_icon_item(base.get_icon("ScriptCreate", "EditorIcons"), "New Haxe Script") 47 | else: 48 | menu.add_icon_item(base.get_icon("ScriptRemove", "EditorIcons"), "Remove Haxe Script") 49 | 50 | menu.add_icon_item(base.get_icon("Load", "EditorIcons"), "Load Haxe Script") 51 | 52 | if has_script: 53 | menu.add_icon_item(base.get_icon("Edit", "EditorIcons"), "Edit") 54 | 55 | if not menu.is_connected("index_pressed", self, "on_popup_select"): 56 | menu.connect("index_pressed", self, "on_popup_select", [has_script]) 57 | 58 | func on_menu_gui(event:InputEvent) -> void: 59 | # If is right click then pretend it's a left click 60 | if event is InputEventMouseButton and event.pressed and event.button_index == 2: 61 | event.button_index = 1 62 | 63 | func on_popup_select(id:int, has_script:bool) -> void: 64 | if id == 0: # New/Remove 65 | if not has_script: # New 66 | var dialog := new_script_dialog.instance() 67 | dialog.setup(base, object.get_class(), object.get_path().get_name(object.get_path().get_name_count() - 1)) 68 | dialog.theme = base.theme 69 | dialog.connect("create", self, "on_create") 70 | base.add_child(dialog) 71 | dialog.popup_centered() 72 | else: # Remove 73 | object.remove_meta("haxe_script") 74 | object.set_script(null) 75 | elif id == 1: # Load 76 | var dialog := EditorFileDialog.new() 77 | base.add_child(dialog) 78 | dialog.access = EditorFileDialog.ACCESS_RESOURCES 79 | dialog.current_dir = "res://scripts/" 80 | dialog.mode = EditorFileDialog.MODE_OPEN_FILE 81 | dialog.theme = base.theme 82 | dialog.add_filter("*.hx ; Haxe script") 83 | dialog.connect("file_selected", self, "on_load_file") 84 | dialog.popup_centered_ratio() 85 | elif id == 2: # Edit 86 | open_file(script_path) 87 | else: 88 | print("Unknown entry: ", id) 89 | 90 | func on_create(is_load:bool, class_value:String, path_value:String) -> void: 91 | if not is_load: 92 | var f := path_value.find_last("/") 93 | var name := path_value.substr(f + 1, path_value.find_last(".hx") - f - 1) 94 | 95 | var d := path_value.substr(14).split("/") 96 | d.remove(d.size() - 1) 97 | 98 | var pack := d.join(".") 99 | if not pack.empty(): 100 | pack = " " + pack; 101 | 102 | if class_value == name: 103 | class_value = "godot." + class_value 104 | 105 | var file := File.new() 106 | file.open(path_value, File.WRITE) 107 | file.store_string("package" + pack + ";\n\nclass " + name + " extends " + class_value + " {\n}\n") 108 | file.close() 109 | 110 | open_file(path_value) 111 | 112 | on_load_file(path_value) 113 | 114 | func open_file(path:String) -> void: 115 | var editor:String = ProjectSettings.get(HaxePluginConstants.SETTING_EXTERNAL_EDITOR) 116 | if editor == "None": 117 | pass 118 | elif editor == "VSCode": 119 | OS.execute("code", [ProjectSettings.globalize_path(path)], false) 120 | else: 121 | print("Unknown external editor: " + editor) 122 | 123 | func on_load_file(path:String) -> void: 124 | object.set_meta("haxe_script", path) 125 | var cs_path := path.replace("res://scripts", "") 126 | var p := cs_path.find_last("/") 127 | var name := cs_path.substr(p, cs_path.length() - 2 - p) + "cs" 128 | cs_path = "build/src" + cs_path.substr(0, p) 129 | 130 | var d := Directory.new() 131 | d.make_dir_recursive(cs_path) 132 | 133 | var file_path := "res://" + cs_path + name 134 | var cs_file := File.new() 135 | if not cs_file.file_exists(file_path): 136 | cs_file.open(file_path, File.WRITE) 137 | cs_file.store_string("\n") 138 | cs_file.close() 139 | object.set_script(load(file_path)) 140 | 141 | func update_property() -> void: 142 | var script_name := "[empty]" 143 | 144 | if object.has_meta("haxe_script"): 145 | if not object.get_script(): 146 | object.remove_meta("haxe_script") 147 | else: 148 | script_path = object.get_meta("haxe_script") 149 | var p := script_path.find_last("/") 150 | script_name = script_path.substr(p + 1) 151 | 152 | var has_script := script_path != "" 153 | 154 | b.size_flags_horizontal = MenuButton.SIZE_EXPAND_FILL 155 | if has_script: 156 | b.icon = haxe_icon 157 | b.text = script_name 158 | b.hint_tooltip = script_path 159 | setup_menu(base, b, has_script) 160 | 161 | setup_menu(base, b2, has_script) 162 | 163 | func get_tooltip_text() -> String: 164 | return "Haxe Script" 165 | -------------------------------------------------------------------------------- /scripts/haxe.gd: -------------------------------------------------------------------------------- 1 | tool 2 | class_name HaxePlugin 3 | extends EditorPlugin 4 | 5 | var about_dialog := preload("res://addons/haxe/scenes/about.tscn") 6 | var tab := preload("res://addons/haxe/scenes/tab.tscn").instance() 7 | var build_dialog := preload("res://addons/haxe/scenes/building.tscn") 8 | 9 | var inspector_plugin:HaxePluginInspectorPlugin 10 | 11 | func _enter_tree() -> void: 12 | var base := get_editor_interface().get_base_control() 13 | 14 | # Init 15 | setup_settings() 16 | 17 | # Inspector plugin 18 | inspector_plugin = HaxePluginInspectorPlugin.new() 19 | inspector_plugin.setup(base) 20 | add_inspector_plugin(inspector_plugin) 21 | 22 | # Tool menu entry 23 | var menu := PopupMenu.new() 24 | menu.add_item("About") 25 | menu.add_item("Setup") 26 | menu.connect("index_pressed", self, "on_menu") 27 | add_tool_submenu_item("Haxe", menu) 28 | 29 | # Bottom dock tab 30 | tab.setup(base) 31 | add_control_to_bottom_panel(tab, "Haxe") 32 | 33 | func _exit_tree() -> void: 34 | # TODO tab.gd still leaks? 35 | remove_control_from_bottom_panel(tab) 36 | tab.queue_free() 37 | remove_tool_menu_item("Haxe") 38 | remove_inspector_plugin(inspector_plugin) 39 | 40 | func setup_settings() -> void: 41 | if not ProjectSettings.has_setting(HaxePluginConstants.SETTING_HIDE_NATIVE_SCRIPT_FIELD): 42 | ProjectSettings.set_setting(HaxePluginConstants.SETTING_HIDE_NATIVE_SCRIPT_FIELD, true) 43 | 44 | if not ProjectSettings.has_setting(HaxePluginConstants.SETTING_EXTERNAL_EDITOR): 45 | ProjectSettings.set_setting(HaxePluginConstants.SETTING_EXTERNAL_EDITOR, "VSCode") 46 | ProjectSettings.add_property_info({ 47 | "name": HaxePluginConstants.SETTING_EXTERNAL_EDITOR, 48 | "type": TYPE_STRING, 49 | "hint": PROPERTY_HINT_ENUM, 50 | "hint_string": "None,VSCode" 51 | }); 52 | 53 | if not ProjectSettings.has_setting(HaxePluginConstants.BUILD_ON_PLAY): 54 | ProjectSettings.set_setting(HaxePluginConstants.BUILD_ON_PLAY, false) 55 | 56 | func on_menu(id:int) -> void: 57 | var theme := get_editor_interface().get_base_control().theme 58 | 59 | if id == 0: # About 60 | var dialog := about_dialog.instance() 61 | add_child(dialog) 62 | dialog.theme = theme 63 | dialog.popup_centered() 64 | elif id == 1: # Setup 65 | var output := [] 66 | OS.execute("haxe", ["--class-path", "addons/haxe/scripts", "--run", "Setup"], true, output, true) 67 | 68 | var dialog := AcceptDialog.new() 69 | add_child(dialog) 70 | 71 | if output.size() != 1: 72 | dialog.dialog_text = "Unknown error:\n" + PoolStringArray(output).join("\n") 73 | elif "command not found" in output[0].to_lower(): 74 | dialog.dialog_text = "Haxe command not found." 75 | elif output[0] == "haxelib": 76 | dialog.dialog_text = "Godot externs not found.\nRun 'haxelib install godot' first." 77 | elif output[0] == "multiple_csproj": 78 | dialog.dialog_text = "Multiple C# solutions found.\nCannot setup." 79 | elif output[0] == "csproj": 80 | dialog.dialog_text = "C# solution not found (.csproj file).\nYou need to setup Godot Mono first:\nProject -> Tools -> Mono -> Create C# solution." 81 | elif output[0].begins_with("dirty:"): 82 | dialog.dialog_text = "Project already contains: " + output[0].substr(6) + "\nTo avoid data loss the setup wasn't run." 83 | elif output[0] == "ok": 84 | dialog.dialog_text = "Setup successful." 85 | else: 86 | dialog.dialog_text = "Unknown error: " + output[0] 87 | 88 | dialog.theme = theme 89 | dialog.window_title = "Haxe Setup" 90 | dialog.popup_centered() 91 | else: 92 | print("Unknown menu: ", id) 93 | 94 | func _input(event): 95 | if event is InputEventKey and ProjectSettings.get_setting(HaxePluginConstants.BUILD_ON_PLAY): 96 | if event.scancode == KEY_F5 or event.scancode == KEY_F6 and event.echo: 97 | var dialog = build_dialog.instance() 98 | 99 | add_child(dialog) 100 | 101 | yield(VisualServer, 'frame_post_draw') 102 | 103 | dialog.call("build_haxe_project") 104 | -------------------------------------------------------------------------------- /scripts/inspector_plugin.gd: -------------------------------------------------------------------------------- 1 | tool 2 | class_name HaxePluginInspectorPlugin 3 | extends EditorInspectorPlugin 4 | 5 | var base:Control 6 | 7 | func setup(base:Control) -> void: 8 | self.base = base 9 | 10 | #warning-ignore:unused_argument 11 | func can_handle(object:Object) -> bool: 12 | return true 13 | 14 | #warning-ignore:unused_argument 15 | func parse_property(object:Object, type:int, path:String, hint:int, hint_text:String, usage:int) -> bool: 16 | if object is Node and type == TYPE_OBJECT and path == "script": 17 | var e := HaxePluginEditorProperty.new() 18 | e.setup(base, object) 19 | add_custom_control(e) 20 | return ProjectSettings.get_setting(HaxePluginConstants.SETTING_HIDE_NATIVE_SCRIPT_FIELD) 21 | 22 | return false 23 | -------------------------------------------------------------------------------- /scripts/new_script.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends WindowDialog 3 | 4 | signal create(is_load, class_value, path_value) 5 | 6 | var base:Control 7 | 8 | var cancel_button:Button 9 | var create_button:Button 10 | 11 | var class_valid := true 12 | var path_valid := true 13 | var name_valid := true 14 | var name_warning := false 15 | var extension_valid := true 16 | 17 | var is_load := false 18 | var class_value := "" 19 | var path_value := "" 20 | 21 | func setup(base:Control, class_value:String, name:String) -> void: 22 | self.base = base 23 | 24 | var left := $MarginContainer/VBoxContainer/Buttons/Left 25 | var right := $MarginContainer/VBoxContainer/Buttons/Right 26 | 27 | if OS.get_name() == "Windows" or OS.get_name() == "UWP": 28 | setup_buttons(right, left) 29 | else: 30 | setup_buttons(left, right) 31 | 32 | $MarginContainer/VBoxContainer/GridContainer/ClassValue.connect("text_changed", self, "on_class") 33 | $MarginContainer/VBoxContainer/GridContainer/Path/PathValue.connect("text_changed", self, "on_path") 34 | 35 | var path_button := $MarginContainer/VBoxContainer/GridContainer/Path/Load 36 | path_button.icon = base.get_icon("Folder", "EditorIcons") 37 | path_button.connect("button_down", self, "on_folder") 38 | 39 | on_class(class_value) 40 | on_path("res://scripts/" + name.substr(0, 1).to_upper() + name.substr(1) + ".hx") 41 | 42 | func setup_buttons(cancel:Button, create:Button) -> void: 43 | cancel.text = "Cancel" 44 | cancel.connect("button_down", self, "on_cancel") 45 | cancel_button = cancel 46 | 47 | create.text = "Create" 48 | create.connect("button_down", self, "on_create") 49 | create_button = create 50 | 51 | func on_cancel() -> void: 52 | hide() 53 | 54 | func on_create() -> void: 55 | hide() 56 | emit_signal("create", is_load, class_value, path_value) 57 | 58 | func on_class(value:String) -> void: 59 | class_value = value 60 | class_valid = ClassDB.class_exists(value) and ClassDB.can_instance(value) 61 | revalidate() 62 | 63 | func on_folder() -> void: 64 | var file := path_value 65 | file = file.substr(file.find_last("/") + 1) 66 | 67 | var dialog := EditorFileDialog.new() 68 | base.add_child(dialog) 69 | dialog.access = EditorFileDialog.ACCESS_RESOURCES 70 | dialog.current_dir = "res://scripts/" 71 | dialog.current_file = file 72 | dialog.disable_overwrite_warning = true 73 | dialog.theme = base.theme 74 | dialog.window_title = "Open Haxe Script / Choose Location" 75 | dialog.add_filter("*.hx ; Haxe script") 76 | dialog.connect("file_selected", self, "on_path") 77 | dialog.get_ok().text = "Open" 78 | dialog.popup_centered_ratio() 79 | 80 | func on_path(fullpath:String) -> void: 81 | path_value = fullpath 82 | 83 | var dir_p := fullpath.find_last("/") 84 | var ext_p := fullpath.find_last(".") 85 | 86 | var path := "" 87 | var file := "" 88 | 89 | if dir_p < ext_p: 90 | path = fullpath.substr(0, dir_p) 91 | file = fullpath.substr(dir_p + 1) 92 | else: 93 | path = fullpath 94 | 95 | var d := Directory.new() 96 | var f := File.new() 97 | 98 | is_load = f.file_exists(path_value) 99 | extension_valid = file.ends_with(".hx") 100 | name_valid = ext_p < fullpath.length() - 1 and ext_p > dir_p + 1 101 | name_warning = name_valid && extension_valid && isBuiltin(file.substr(0, file.length() - 3)) 102 | path_valid = path.begins_with("res://") and d.dir_exists(path) 103 | revalidate() 104 | 105 | func isBuiltin(name:String) -> bool: 106 | var haxeGodotBuiltins = ["Action", "CustomSignal", "CustomSignalUsings", "Godot", "Nullable1", "Signal", "SignalUsings", "Utils"] 107 | return ClassDB.class_exists(name) or haxeGodotBuiltins.has(name) 108 | 109 | func revalidate() -> void: 110 | var text_edit := $MarginContainer/VBoxContainer/TextEdit 111 | text_edit.bbcode_text = "" 112 | 113 | var valid_color := Color(0.062775, 0.730469, 0.062775) 114 | var error_color := Color(0.820312, 0.028839, 0.028839) 115 | var warning_color := Color(0.9375, 0.537443, 0.06958) 116 | 117 | if not class_valid: 118 | text_edit.push_color(error_color) 119 | text_edit.append_bbcode("- Invalid inherited parent name.\n\n") 120 | text_edit.pop() 121 | elif not extension_valid: 122 | text_edit.push_color(error_color) 123 | text_edit.append_bbcode("- Invalid extension.\n\n") 124 | text_edit.pop() 125 | elif not path_valid: 126 | text_edit.push_color(error_color) 127 | text_edit.append_bbcode("- Invalid path.\n\n") 128 | text_edit.pop() 129 | elif not name_valid: 130 | text_edit.push_color(error_color) 131 | text_edit.append_bbcode("- Invalid filename.\n\n") 132 | text_edit.pop() 133 | else: 134 | text_edit.push_color(valid_color) 135 | text_edit.append_bbcode("- Haxe script path is valid.\n\n") 136 | 137 | if is_load: 138 | text_edit.append_bbcode("- Will load an existing Haxe script.\n\n") 139 | else: 140 | text_edit.append_bbcode("- Will create a new Haxe script.\n\n") 141 | 142 | text_edit.pop() 143 | 144 | if name_warning: 145 | text_edit.push_color(warning_color) 146 | text_edit.append_bbcode("Warning: Having the script name be the same as a built-in type is usually not desired.\n\n") 147 | text_edit.pop() 148 | 149 | var class_edit:LineEdit = $MarginContainer/VBoxContainer/GridContainer/ClassValue 150 | var class_edit_column := class_edit.caret_position 151 | class_edit.text = class_value 152 | class_edit.caret_position = class_edit_column if class_edit_column <= class_value.length() else class_value.length() 153 | 154 | var path_edit:LineEdit = $MarginContainer/VBoxContainer/GridContainer/Path/PathValue 155 | var path_edit_column := path_edit.caret_position 156 | path_edit.text = path_value 157 | path_edit.caret_position = path_edit_column if path_edit_column <= path_value.length() else path_value.length() 158 | -------------------------------------------------------------------------------- /scripts/tab.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends Control 3 | 4 | onready var button := $VBoxContainer/Button 5 | onready var text_log := $VBoxContainer/TextLog 6 | 7 | var base:Control 8 | var icon := 0 9 | var icons := [] 10 | var mutex := Mutex.new() 11 | var output := [] 12 | var time := 0.0 13 | var thread:Thread = null 14 | 15 | func setup(base:Control) -> void: 16 | self.base = base 17 | 18 | for i in range(8): 19 | icons.append(base.get_icon("Progress%s"%(i + 1), "EditorIcons")) 20 | 21 | func _ready() -> void: 22 | button.connect("button_down", self, "build_haxe_project") 23 | 24 | func build_haxe_project() -> void: 25 | if thread != null: 26 | return 27 | 28 | thread = Thread.new() 29 | 30 | button.icon = icons[0] 31 | button.text = "Building Haxe Project ..." 32 | icon = 0 33 | text_log.text = "" 34 | time = 0.0 35 | output = [] 36 | 37 | thread.start(self, "run_thread") 38 | 39 | func _process(delta:float) -> void: 40 | if thread != null: 41 | update_log() 42 | time += delta 43 | if time > 0.1: 44 | time = 0 45 | icon = (icon + 1) % 8 46 | button.icon = icons[icon] 47 | 48 | func run_thread(userdata) -> void: 49 | var ret := OS.execute("haxe", ["build.hxml"], true, output, true) 50 | update_log() 51 | button.icon = base.get_icon("StatusSuccess" if ret == 0 else "StatusError", "EditorIcons") 52 | button.text = "Build Haxe Project" 53 | call_deferred("end_thread") 54 | 55 | func end_thread() -> void: 56 | thread.wait_to_finish() 57 | thread = null 58 | 59 | func update_log() -> void: 60 | mutex.lock() 61 | text_log.text = PoolStringArray(output).join("\n") 62 | mutex.unlock() 63 | 64 | func _exit_tree(): 65 | if thread != null: 66 | thread.wait_to_finish() 67 | thread = null 68 | --------------------------------------------------------------------------------