├── addons
└── .gitkeep
├── project-manager
├── .gitkeep
└── inspect
│ ├── helpers.gd
│ └── inspect_plugin.gd
├── .gitattributes
├── .gitignore
├── _internal
├── injector
│ ├── plugin.cfg
│ └── plugin.gd
├── folder_colors
│ ├── plugin.cfg
│ └── plugin.gd
├── path_processor
│ ├── plugin.cfg
│ └── plugin.gd
├── runner.gd
├── plugins_enabler.gd
├── testing
│ └── node.tscn
└── loader.gd
├── editor-only
└── included
│ ├── paths.gd
│ ├── gdx.gd
│ └── addon_import_plugin.gd
├── .editorconfig
├── project.godot
├── icon.svg
├── icon.svg.import
├── GDX.md
└── README.md
/addons/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/project-manager/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Normalize EOL for all files that Git considers text files.
2 | * text=auto eol=lf
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Godot 4+ specific ignores
2 | .godot/
3 | .processed/
4 | android/
5 | addons/*
6 | !.gitkeep
7 | editor-only/testing
8 |
--------------------------------------------------------------------------------
/_internal/injector/plugin.cfg:
--------------------------------------------------------------------------------
1 | [plugin]
2 |
3 | name="global-project-plugin"
4 | description=""
5 | author="dugramen"
6 | version=""
7 | script="plugin.gd"
8 |
--------------------------------------------------------------------------------
/_internal/folder_colors/plugin.cfg:
--------------------------------------------------------------------------------
1 | [plugin]
2 |
3 | name="global-project-plugin"
4 | description=""
5 | author="dugramen"
6 | version=""
7 | script="plugin.gd"
8 |
--------------------------------------------------------------------------------
/_internal/path_processor/plugin.cfg:
--------------------------------------------------------------------------------
1 | [plugin]
2 |
3 | name="global-project-plugin"
4 | description=""
5 | author="dugramen"
6 | version=""
7 | script="plugin.gd"
8 |
--------------------------------------------------------------------------------
/_internal/runner.gd:
--------------------------------------------------------------------------------
1 | #@tool
2 | static var path := "_PATH_TO_REPLACE_"
3 | static func _static_init() -> void:
4 | var file = load(path)
5 | if file is GDScript and "init_extensions" in file:
6 | file.init_extensions(path, file)
7 |
--------------------------------------------------------------------------------
/editor-only/included/paths.gd:
--------------------------------------------------------------------------------
1 | @tool
2 |
3 | static var global := path_preload("res://").trim_suffix(".processed/")
4 | static var processed := path_preload("res://")
5 | static var local := ProjectSettings.globalize_path("res://")
6 |
7 | static func path_preload(s: String) -> String:
8 | return s
9 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | insert_final_newline = true
6 |
7 | # Matches multiple files with brace expansion notation
8 | [*.gd]
9 | charset = utf-8
10 | indent_style = tab
11 | indent_size = 4
12 | trim_trailing_whitespace = true
13 |
14 | [*.md]
15 | trim_trailing_whitespace = false
16 |
--------------------------------------------------------------------------------
/_internal/plugins_enabler.gd:
--------------------------------------------------------------------------------
1 | @tool
2 |
3 | static func _static_init():
4 | print("enabling internal plugin")
5 | for plugin in [
6 | "folder_colors",
7 | "injector",
8 | "path_processor"
9 | ]:
10 | var path := plugin_path(plugin)
11 | if EditorInterface.is_plugin_enabled(path):
12 | EditorInterface.set_plugin_enabled(path, false)
13 | EditorInterface.set_plugin_enabled(path, true)
14 |
15 | static func plugin_path(s: String) -> String:
16 | return "res://_internal".path_join(s).path_join("plugin.cfg")
17 |
--------------------------------------------------------------------------------
/_internal/injector/plugin.gd:
--------------------------------------------------------------------------------
1 | @tool
2 |
3 | extends EditorPlugin
4 |
5 | var scr: GDScript = preload("res://_internal/runner.gd")
6 |
7 | func _enter_tree() -> void:
8 | print("static init")
9 | var settings := EditorInterface.get_editor_settings()
10 | var new_scr := GDScript.new()
11 | #print('gp path ', ProjectSettings.globalize_path("res://loader.gd"))
12 | new_scr.source_code = scr.source_code\
13 | .trim_prefix("#")\
14 | .replace("_PATH_TO_REPLACE_", ProjectSettings.globalize_path("res://_internal/loader.gd"))
15 | settings.set_setting("portable_plugins/injected_script", new_scr)
16 | new_scr.reload()
17 |
--------------------------------------------------------------------------------
/project.godot:
--------------------------------------------------------------------------------
1 | ; Engine configuration file.
2 | ; It's best edited using the editor UI and not directly,
3 | ; since the parameters that go here are not all obvious.
4 | ;
5 | ; Format:
6 | ; [section] ; section goes between []
7 | ; param=value ; assign values to parameters
8 |
9 | config_version=5
10 |
11 | [application]
12 |
13 | config/name="global-project"
14 | config/version="1.1"
15 | run/main_scene="res://_internal/testing/node.tscn"
16 | config/features=PackedStringArray("4.3", "GL Compatibility")
17 | config/icon="res://icon.svg"
18 |
19 | [editor_plugins]
20 |
21 | enabled=PackedStringArray("res://_internal/folder_colors/plugin.cfg", "res://_internal/injector/plugin.cfg", "res://_internal/path_processor/plugin.cfg")
22 |
23 | [rendering]
24 |
25 | renderer/rendering_method="gl_compatibility"
26 | renderer/rendering_method.mobile="gl_compatibility"
27 |
--------------------------------------------------------------------------------
/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/icon.svg.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="texture"
4 | type="CompressedTexture2D"
5 | uid="uid://b868eeotkql50"
6 | path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
7 | metadata={
8 | "vram_texture": false
9 | }
10 |
11 | [deps]
12 |
13 | source_file="res://icon.svg"
14 | dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
15 |
16 | [params]
17 |
18 | compress/mode=0
19 | compress/high_quality=false
20 | compress/lossy_quality=0.7
21 | compress/hdr_compression=1
22 | compress/normal_map=0
23 | compress/channel_pack=0
24 | mipmaps/generate=false
25 | mipmaps/limit=-1
26 | roughness/mode=0
27 | roughness/src_normal=""
28 | process/fix_alpha_border=true
29 | process/premult_alpha=false
30 | process/normal_map_invert_y=false
31 | process/hdr_as_srgb=false
32 | process/hdr_clamp_exposure=false
33 | process/size_limit=0
34 | detect_3d/compress_to=1
35 | svg/scale=1.0
36 | editor/scale_with_editor_scale=false
37 | editor/convert_colors_with_editor_theme=false
38 |
--------------------------------------------------------------------------------
/_internal/testing/node.tscn:
--------------------------------------------------------------------------------
1 | [gd_scene load_steps=2 format=3 uid="uid://bbr6u2kefuco4"]
2 |
3 | [sub_resource type="GDScript" id="GDScript_l8f1h"]
4 | script/source = "extends Control
5 |
6 | #var farm := preload(\"res://editor-only/testing/farm.tscn\")
7 |
8 | func _ready() -> void:
9 | pass
10 | #print(ResourceLoader.get_dependencies(\"res://node.tscn\"))
11 | #var deps := ResourceLoader.get_dependencies(\"res://editor-only/testing/farm.tscn\")
12 | #var content := FileAccess.get_file_as_string(\"res://editor-only/testing/farm.tscn\")
13 | #var process_path := ProjectSettings.globalize_path(\"res://.processed\")
14 | #
15 | #var i := 0
16 | #while i >= 0:
17 | #i = content.find(\"\\n[ext_resource \", i + 1)
18 | #if i < 0: break
19 | #
20 | #var path_substr := ' path=\"'
21 | #var path_start := content.find(path_substr, i) + path_substr.length()
22 | #var path_end := content.find('\"', path_start)
23 | #
24 | #var end = content.find(\"]\\n\", i + 1)
25 | #var slice := content.substr(path_start, path_end - path_start)
26 | #if slice.begins_with(\"res://\"):
27 | #slice = slice.trim_prefix(\"res://\")
28 | #slice = process_path.path_join(slice)
29 | #content = content.erase(path_start, path_end - path_start)
30 | #content = content.insert(path_start, slice)
31 | #i = path_start + slice.length()
32 | #print(content)
33 | "
34 |
35 | [node name="Node" type="Control"]
36 | layout_mode = 3
37 | anchors_preset = 0
38 | script = SubResource("GDScript_l8f1h")
39 |
--------------------------------------------------------------------------------
/project-manager/inspect/helpers.gd:
--------------------------------------------------------------------------------
1 | @tool
2 |
3 | static var engine_root: Node = Engine.get_main_loop().root
4 | static var path_cache := {}
5 |
6 | static func extract_node(arr: Array, root := engine_root, include_internal := true, use_cache := true) -> Node:
7 | var current_node := root
8 | var next_node: Node = null
9 |
10 | if use_cache and arr in path_cache:
11 | return path_cache[arr]
12 |
13 | for item in arr:
14 | next_node = null
15 | if item is int:
16 | next_node = current_node.get_child(item, include_internal)
17 | elif item is String:
18 | for child in current_node.get_children(include_internal):
19 | if child.name.match(item):
20 | next_node = child
21 | break
22 | elif item is Array:
23 | var string := ""
24 | var index := 0
25 | for a in item:
26 | if a is String:
27 | string = a
28 | elif a is int:
29 | index = a
30 | if string.is_empty():
31 | return null
32 | var count := 0
33 | for child in current_node.get_children(include_internal):
34 | if child.name.match(string):
35 | if count == index:
36 | next_node = child
37 | break
38 | count += 1
39 | if next_node == null:
40 | return null
41 | else:
42 | current_node = next_node
43 |
44 | if use_cache:
45 | path_cache[arr] = current_node
46 |
47 | return current_node
48 |
49 | static func create_project(path: String) -> String:
50 | if !path.ends_with("project.godot"):
51 | path = path.path_join("project.godot")
52 | if FileAccess.file_exists(path):
53 | return path
54 | DirAccess.make_dir_recursive_absolute(path.get_base_dir())
55 | var cfg := ConfigFile.new()
56 | cfg.save(path)
57 | return path
58 |
59 | #static func cli():
60 | #OS.create_instance(["C:/poo", "-e", "--editor"])
61 |
--------------------------------------------------------------------------------
/_internal/folder_colors/plugin.gd:
--------------------------------------------------------------------------------
1 | @tool
2 | extends EditorPlugin
3 |
4 | var menu: PopupMenu
5 |
6 | func _enter_tree() -> void:
7 | #print("folder plugin entered")
8 | modify_folder_color_text()
9 |
10 | func _exit_tree() -> void:
11 | #print("folder plugin exited")
12 | if menu:
13 | if menu.about_to_popup.is_connected(handle_menu_popup):
14 | #print('disconnecting menu')
15 | menu.about_to_popup.disconnect(handle_menu_popup)
16 |
17 | func modify_folder_color_text():
18 | var dock := EditorInterface.get_file_system_dock()
19 | menu = dock.get_child(2, true)
20 | if menu:
21 | #print(menu)
22 | #print(menu.item_count)
23 | #print(menu.get_children(true))
24 | #for i in menu.item_count:
25 | #print(menu.get_item_text(i))
26 | menu.about_to_popup.connect(handle_menu_popup)
27 |
28 | func handle_menu_popup():
29 | var color_menu = menu.get_child(-1, true)
30 | if color_menu is PopupMenu:
31 | if color_menu.item_count > 2:
32 | if color_menu.get_item_text(2).begins_with("Red"):
33 | var renames := {
34 | "Default": "
35 | No automatic behavior.
36 | Import these via the Addon Importer popup.
37 |
38 | It will popup automatically once per project.
39 | After that it's available at:
40 | Project > Tools > Addon Importer
41 | ",
42 | "": "",
43 | "Red --- Refresh": "
44 | Red addons are synced exactly as they appear in the global project.
45 |
46 | On load, they are deleted, and then copied over again.
47 | This means if addons are no longer Red in the global project,
48 | they will no longer exist in your other projects.
49 |
50 | This is ideal if you're developing & testing your own addons locally.
51 | ",
52 | "Orange --- Refresh Versioned": "
53 | This is recommended for asset store addons.
54 |
55 | Only when the version in plugin.cfg has changed,
56 | the addons are deleted, then copied over.
57 | Addons that are no longer Orange will also be deleted.
58 |
59 | This method makes it so files aren't copied everytime.
60 | ",
61 | "Yellow --- Update": "
62 | This is the same behavior as my old 'globalize-plugins' addon.
63 |
64 | On load, all yellow plugins are copied over. Nothing is deleted.
65 | So folders that are no longer yellow will still remain.
66 | If the addon's file structure / naming change, the old files will remain.
67 |
68 | Some addons store user data / preferences within its directory.
69 | Red and Orange addons will overwrite those, but Yellow addons won't.
70 | ",
71 | "Green --- Update Versioned": "
72 | This is the same as Yellow, but only copying when the version has changed.
73 |
74 | No files or directories will be removed.
75 | Addons whose colors have changed or were deleted will remain in projects.
76 | ",
77 | }
78 | var i := 0
79 | for key in renames:
80 | color_menu.set_item_text(i, key)
81 | color_menu.set_item_tooltip(i, renames[key])
82 | i += 1
83 |
84 | #for i in color_menu.item_count:
85 | #var text = color_menu.get_item_text(i)
86 | #print(text)
87 | #color_menu.set_item_text(i, text + " - " + str(i))
88 |
--------------------------------------------------------------------------------
/_internal/loader.gd:
--------------------------------------------------------------------------------
1 | @tool
2 |
3 | #class_name Loader
4 |
5 | static var global_path := ""
6 | static var local_path := ""
7 | static var editor_plugin_holder: Node = null
8 |
9 | static func init_extensions(loader_path: String, this_file: GDScript) -> void:
10 | #load(loader_path).take_over_path("res://loader.gd")
11 | #return
12 | global_path = loader_path.trim_suffix("_internal/loader.gd")
13 | print('running loader ', global_path)
14 | var main_loop := Engine.get_main_loop()
15 | if main_loop is SceneTree:
16 | main_loop.process_frame.connect(func():
17 | if Engine.is_editor_hint():
18 | #var path := loader_path.trim_suffix("loader.gd")
19 | var base_control := EditorInterface.get_base_control()
20 | var holder_name := "PortablePluginsHolder"
21 | editor_plugin_holder = base_control.get_node_or_null(holder_name)
22 | if editor_plugin_holder:
23 | for child in editor_plugin_holder.get_children(true):
24 | child.queue_free()
25 | else:
26 | editor_plugin_holder = Node.new()
27 | base_control.add_child(editor_plugin_holder)
28 | editor_plugin_holder.name = holder_name
29 |
30 | local_path = ProjectSettings.globalize_path("res://")
31 |
32 | if global_path == local_path: return
33 |
34 | var scripts: Array[Script] = []
35 | var paths := [".processed/editor-only"]
36 | while !paths.is_empty():
37 | var path: String = paths.pop_back()
38 | #print(path)
39 | for dir_name in DirAccess.get_directories_at(global_path.path_join(path)):
40 | paths.push_back(path.path_join(dir_name))
41 | for file_name in DirAccess.get_files_at(global_path.path_join(path)):
42 | var file_path := global_path.path_join(path).path_join(file_name)
43 | #print(file_path)
44 | if file_name.ends_with("plugin.gd"):
45 | var file = load(file_path)
46 | if file is GDScript:
47 | instantiate_plugin(file)
48 | else:
49 | #print("project manager?")
50 | var root: Node = main_loop.root
51 | var paths := [global_path.path_join(".processed/project-manager")]
52 | while !paths.is_empty():
53 | var path: String = paths.pop_back() as String
54 | for dir_name in DirAccess.get_directories_at(path):
55 | paths.push_back(path.path_join(dir_name))
56 | for file_name in DirAccess.get_files_at(path):
57 | var file_path := path.path_join(file_name)
58 | if file_name.ends_with("plugin.gd"):
59 | var file = load(file_path)
60 | if file is GDScript:
61 | var instance: Object = file.new()
62 | #prints('project plugin ', file, instance)
63 | if instance is Node:
64 | root.add_child(instance)
65 | , CONNECT_ONE_SHOT)
66 |
67 |
68 | static func instantiate_plugin(file: GDScript):
69 | #file.reload(true)
70 | if file.get_instance_base_type() == "EditorPlugin":
71 | var plugin: EditorPlugin = file.new()
72 | if editor_plugin_holder:
73 | editor_plugin_holder.add_child(plugin)
74 |
75 |
76 | static func process_extension(file: GDScript, global_path := ProjectSettings.globalize_path("res://")):
77 | var new_source_code: String = file.source_code
78 | var index := 0
79 | var file_path := file.resource_path
80 | var folder_path := file_path.get_base_dir()
81 |
82 | while index > -1:
83 | index = new_source_code.find("preload(", index)
84 | if index == -1:
85 | break
86 |
87 | index += 8
88 | var string_char: String = new_source_code[index]
89 | if string_char != '"' and string_char != "'":
90 | continue
91 |
92 | var end: int = new_source_code.find(string_char, index + 1)
93 | var preload_path = new_source_code.substr(index + 1, end - index - 1)
94 |
95 | var splits: Array = preload_path.split("//", true, 1)
96 | var new_path := ""
97 | if splits.size() <= 1:
98 | new_path = folder_path + "/" + preload_path
99 | else:
100 | new_path = global_path + "/" + splits[1]
101 | new_source_code = new_source_code.erase(index + 1, end - index - 1)
102 | new_source_code = new_source_code.insert(index + 1, new_path)
103 |
104 | index += 1
105 |
106 | file.source_code = new_source_code
107 |
--------------------------------------------------------------------------------
/editor-only/included/gdx.gd:
--------------------------------------------------------------------------------
1 | @tool
2 |
3 | var _render_map := {}
4 | var _deletion_map := {}
5 | var _new_deletion_map := {}
6 | var _current_callable
7 | #static var hello := "World!"
8 | var confirmed_callable: Callable
9 |
10 | func get_this_render() -> Callable:
11 | return render.bind(_current_callable)
12 |
13 | func render(callable = null):
14 | if callable == null:
15 | callable = confirmed_callable
16 | else:
17 | confirmed_callable = callable
18 | var pc = _current_callable
19 | _current_callable = callable
20 | var tree: Array = _current_callable.call()
21 | var dm: Dictionary = _deletion_map.get_or_add(_current_callable, {})
22 | var ndm: Dictionary = _new_deletion_map.get_or_add(_current_callable, {})
23 | var node_from_previous_render = _render_map.get(_current_callable, null)
24 | var index := 0
25 | if node_from_previous_render is Node:
26 | node_from_previous_render = node_from_previous_render.get_parent()
27 | index = node_from_previous_render.get_index()
28 | var result = _build_element(_current_callable, tree, {index = index, node = node_from_previous_render})
29 | if result is Array:
30 | if result.size() > 0:
31 | result = result[0]
32 | _render_map[_current_callable] = result
33 | for node in dm:
34 | if is_instance_valid(node):
35 | node.queue_free()
36 | _deletion_map[_current_callable] = ndm
37 | _new_deletion_map.erase(_current_callable)
38 | _current_callable = pc
39 | #if pc != null:
40 | return result
41 |
42 | func _build_element(callable: Callable, tree: Array, context := {node = null, index = 0}):
43 | if tree.is_empty(): return
44 | if tree[0] is Node or "new" in tree[0]:
45 | var props := {}
46 | var children := []
47 | var calls: Array[Callable] = []
48 | var parent: Node = context.node
49 | # Gather props and children
50 | for item in tree:
51 | if item is Dictionary:
52 | props.merge(item, true)
53 | elif item is Array:
54 | children.append(item)
55 | elif item is Callable:
56 | calls.append(item)
57 |
58 | # Create or reuse node
59 | var node: Node = null
60 | #prints('index ', context.index, node)
61 | if tree[0] is Node:
62 | node = tree[0]
63 | if parent:
64 | var node_parent = node.get_parent()
65 | var i = context.index
66 | if node_parent == null:
67 | parent.add_child(node)
68 | elif node_parent != parent:
69 | node.reparent(parent)
70 | parent.move_child(node, i)
71 | else:
72 | var should_create := true
73 | if parent:
74 | var key: String = str(props.get('name', "")).validate_node_name()
75 | if parent.has_node(key):
76 | node = parent.get_node(key)
77 | var i = context.index
78 | parent.move_child(node, i)
79 | should_create = false
80 | elif parent.get_child_count() > context.index:
81 | node = parent.get_child(context.index)
82 | var existing_class = node.get_meta("node_class", null)
83 | if existing_class == tree[0]:
84 | should_create = false
85 | if should_create:
86 | node = tree[0].new()
87 | node.set_meta("node_class", tree[0])
88 | if parent:
89 | parent.add_child(node)
90 | _deletion_map[callable].erase(node)
91 | #_new_deletion_map.get_or_add(callable, {})[node] = true
92 | if callable in _new_deletion_map:
93 | _new_deletion_map[callable][node] = true
94 |
95 | # Disconnect signals from previous render
96 | var connections: Dictionary = node.get_meta("_gdx_connections", {})
97 | for signal_name in connections:
98 | var c = connections[signal_name]
99 | node.disconnect(signal_name, c)
100 | connections.clear()
101 | node.set_meta("_gdx_connections", connections)
102 |
103 | # Handle props
104 | for key in props:
105 | var value = props[key]
106 | #prints(key, node, value)
107 | if key is String:
108 | if key.begins_with("on_"):
109 | var signal_name: String = key.trim_prefix("on_")
110 | if node.has_signal(signal_name):
111 | if value is Callable:
112 | #var sig: Signal = node.get(signal_name) as Signal
113 | var connection_call := func(
114 | a0 = null,
115 | a1 = null,
116 | a2 = null,
117 | a3 = null,
118 | a4 = null,
119 | a5 = null,
120 | a6 = null,
121 | a7 = null,
122 | a8 = null,
123 | a9 = null
124 | ):
125 | var args := [a0, a1, a2, a3, a4, a5, a6, a7, a8, a9]
126 | var current_c = _current_callable
127 | _current_callable = callable
128 | #value.call()
129 | value.callv(args.slice(0, value.get_argument_count()))
130 | _current_callable = current_c
131 | node.connect(signal_name, connection_call)
132 | connections[signal_name] = connection_call
133 | continue
134 | if node is Control and key.begins_with("theme_") and value is Dictionary:
135 | for p in value:
136 | node.call("add_" + key + "_override", p, value[p])
137 | continue
138 | if key in node:
139 | var curr = node.get(key)
140 | if typeof(curr) != typeof(value) or curr != value:
141 | node.set(key, value)
142 | continue
143 | continue
144 |
145 | #print('getting indexed')
146 | #node.has_node_and_resource()
147 | var indexed_val = node.get_indexed(key)
148 | if typeof(indexed_val) == typeof(value):
149 | #print('proped')
150 | if indexed_val != value:
151 | node.set_indexed(key, value)
152 | continue
153 | continue
154 | else:
155 | push_error(key, " not found on ", node)
156 | #prints('key ', key)
157 | #print('indexed ', indexed_val)
158 | #print('value ', value)
159 |
160 | for c in calls:
161 | c.call(node)
162 |
163 | var result := []
164 | var new_context := {
165 | node = node,
166 | index = 0
167 | }
168 | for child in children:
169 | _build_element(callable, child, new_context)
170 | context.index += 1
171 | return node
172 | elif tree[0] is Array:
173 | var result := []
174 | for branch in tree:
175 | result.append(_build_element(callable, branch, context))
176 | return result
177 |
178 | static func map_i(arr: Array, callable: Callable):
179 | var result := []
180 | for i in arr.size():
181 | var args := [arr[i], i, arr, callable]
182 | result.append(callable.callv(args.slice(0, callable.get_argument_count())))
183 | return result
184 |
185 | static func map_key(dict: Dictionary, callable: Callable):
186 | var result := []
187 | var i := -1
188 | for key in dict:
189 | i += 1
190 | var args := [key, dict[key], i, dict, callable]
191 | result.append(callable.callv(args.slice(0, callable.get_argument_count())))
192 | return result
193 |
194 |
195 |
--------------------------------------------------------------------------------
/_internal/path_processor/plugin.gd:
--------------------------------------------------------------------------------
1 | @tool
2 | extends EditorPlugin
3 |
4 | static var global_path := ProjectSettings.globalize_path("res://")
5 | var rfs := EditorInterface.get_resource_filesystem()
6 | var reprocess_button := Button.new()
7 |
8 | func _enter_tree() -> void:
9 | process_all_resources()
10 | #resource_saved.connect(on_resource_saved)
11 | #scene_saved.connect(on_scene_saved)
12 | #rfs.resources_reimported.connect(rfs_connection)
13 |
14 | #add_control_to_container(EditorPlugin.CONTAINER_TOOLBAR, reprocess_button)
15 | #reprocess_button.text = "Process Plugins"
16 | #reprocess_button.icon = reprocess_button.get_theme_icon("Save", "EditorIcons")
17 | #reprocess_button
18 | #reprocess_button.get_parent().move_child(reprocess_button, 0)
19 |
20 | #func _exit_tree():
21 | #remove_control_from_docks(reprocess_button)
22 | #resource_saved.disconnect(on_resource_saved)
23 | #scene_saved.disconnect(on_scene_saved)
24 | #rfs.resources_reimported.disconnect(rfs_connection)
25 |
26 |
27 | func _save_external_data():
28 | process_all_resources()
29 |
30 |
31 | func rfs_connection(resources):
32 | for res in resources:
33 | process_file_path(res)
34 |
35 |
36 | func on_scene_saved(file_path: String):
37 | process_file_path(file_path)
38 |
39 |
40 | func on_resource_saved(resource: Resource):
41 | print('saving resource ', resource.resource_path)
42 | process_file_path(resource.resource_path)
43 |
44 |
45 | func process_all_resources() -> void:
46 | print("processing plugins")
47 | delete_processed_folder()
48 | var paths := ["res://editor-only", "res://project-manager"]
49 | while paths.size() > 0:
50 | var path: String = paths.pop_back()
51 | for dir_name in DirAccess.get_directories_at(path):
52 | paths.push_back(path + "/" + dir_name)
53 | for file_name in DirAccess.get_files_at(path):
54 | var file_path := path + "/" + file_name
55 | process_file_path(file_path)
56 |
57 |
58 | func delete_processed_folder(path := global_path.path_join(".processed")):
59 | if DirAccess.dir_exists_absolute(path):
60 | for file_name in DirAccess.get_files_at(path):
61 | DirAccess.remove_absolute(path.path_join(file_name))
62 | for dir_name in DirAccess.get_directories_at(path):
63 | delete_processed_folder(path.path_join(dir_name))
64 | DirAccess.remove_absolute(path)
65 |
66 |
67 | func process_file_path(file_path: String):
68 | if file_path.begins_with("res://editor-only/") or file_path.begins_with("res://project-manager/"):
69 | # Process .import files
70 | if file_path.ends_with(".import"):
71 | process_import(file_path)
72 | return
73 |
74 | # Skip raw assets
75 | if FileAccess.file_exists(file_path + ".import"):
76 | return
77 |
78 | # Scenes and normal resources
79 | if file_path.get_extension() in ["tscn", "tres"]:
80 | process_resource(file_path)
81 | return
82 |
83 | # Scripts
84 | if file_path.ends_with(".gd"):
85 | var file := load(file_path)
86 | if file is GDScript:
87 | process_script(file)
88 | return
89 |
90 | else:
91 | process_raw_file(file_path)
92 |
93 |
94 | func should_process_path(file_path: String) -> bool:
95 | if file_path.begins_with("res://editor-only") or file_path.begins_with("res://project-manager"):
96 | if file_path.get_extension() in [
97 | "gd", "tres", "tscn", "import"
98 | ]:
99 | return true
100 | if FileAccess.file_exists(file_path + ".import"):
101 | return true
102 | return false
103 |
104 |
105 |
106 | func globalize_path(file_path: String, processed := should_process_path(file_path)) -> String:
107 | if processed:
108 | return ProjectSettings.globalize_path(file_path).replace(global_path, global_path.path_join(".processed/"))
109 | return ProjectSettings.globalize_path(file_path)
110 |
111 |
112 |
113 | func process_raw_file(file_path: String):
114 | return
115 | #if should_process_path(file_path):
116 | #var new_path := globalize_path(file_path)
117 | #DirAccess.make_dir_recursive_absolute(new_path.get_base_dir())
118 | #DirAccess.copy_absolute(file_path, new_path)
119 |
120 |
121 | func process_resource(file_path: String):
122 | var content := FileAccess.get_file_as_string(file_path)
123 |
124 | var i := 0
125 | while i >= 0:
126 | i = content.find("\n[ext_resource ", i + 1)
127 | if i < 0: break
128 |
129 | var path_substr := ' path="'
130 | var path_start := content.find(path_substr, i) + path_substr.length()
131 | var path_end := content.find('"', path_start)
132 |
133 | var end = content.find("]\n", i + 1)
134 | var slice := content.substr(path_start, path_end - path_start)
135 |
136 | slice = globalize_path(slice)
137 | content = content.erase(path_start, path_end - path_start)
138 | content = content.insert(path_start, slice)
139 | i = path_start + slice.length()
140 |
141 | var new_path := globalize_path(file_path, true)
142 | DirAccess.make_dir_recursive_absolute(new_path.get_base_dir())
143 | var fs := FileAccess.open(new_path, FileAccess.WRITE)
144 | fs.store_string(content)
145 | fs.close()
146 |
147 |
148 | func process_import(file_path: String):
149 | if !file_path.ends_with(".import"): return
150 |
151 | var cfg := ConfigFile.new()
152 | cfg.load(file_path)
153 |
154 | var remap_path = cfg.get_value("remap", "path")
155 | if remap_path is String:
156 | var new_path := globalize_path(remap_path, false)
157 | cfg.set_value("remap", "path", new_path)
158 |
159 | var source_file = cfg.get_value("deps", "source_file")
160 | if source_file is String:
161 | var new_path := globalize_path(source_file, false)
162 | cfg.set_value("deps", "source_file", new_path)
163 |
164 | var dest_files = cfg.get_value("deps", "dest_files", [])
165 | if dest_files is Array:
166 | for i in dest_files.size():
167 | dest_files[i] = globalize_path(dest_files[i], false)
168 | cfg.set_value("deps", "dest_files", dest_files)
169 |
170 | var new_path := globalize_path(file_path, true)
171 | DirAccess.make_dir_recursive_absolute(new_path.get_base_dir())
172 | cfg.save(new_path)
173 |
174 |
175 | func process_script(file: GDScript):
176 | var new_source_code: String = file.source_code
177 | var index := 0
178 | var file_path := file.resource_path
179 | var folder_path := file_path.get_base_dir()
180 |
181 | while index > -1:
182 | index = new_source_code.find("preload(", index)
183 | if index == -1:
184 | break
185 |
186 | index += 8
187 | var string_char: String = new_source_code[index]
188 | if string_char != '"' and string_char != "'":
189 | continue
190 |
191 | var end: int = new_source_code.find(string_char, index + 1)
192 | var preload_path = new_source_code.substr(index + 1, end - index - 1)
193 |
194 | var new_path := globalize_path(preload_path)
195 | new_source_code = new_source_code.erase(index + 1, end - index - 1)
196 | new_source_code = new_source_code.insert(index + 1, new_path)
197 |
198 | index += 1
199 |
200 | var new_file := GDScript.new()
201 | new_file.source_code = new_source_code
202 | var new_path := globalize_path(file_path, true)
203 | DirAccess.make_dir_recursive_absolute(new_path.get_base_dir())
204 | ResourceSaver.save(new_file, new_path)
205 |
--------------------------------------------------------------------------------
/GDX.md:
--------------------------------------------------------------------------------
1 |
2 | # GDX
3 | This is the UI framework. This exists specifically because for most of development, you couldn't load PackedScenes due to subresource paths. So the UI had to be done in code. It's not needed anymore, but its still a nice way to use UI and is what the addon_importer and tree_inspector use.
4 |
5 | This is a lightweight framework to make UI in code a lot easier to do. It works similar to ReactJS (gdx is a reference to jsx files)
6 |
7 | ## Render
8 | Preload the `res://editor-only/included/gdx.gd` class and instantiate it. This gives you access to a `gdx.render()` function.
9 | - Call `gdx.render()` with a callable that returns your UI tree, to render your UI for the first time.
10 | - Call `gdx.render()` with no arguments to rerender that UI tree, updating it to reflect some state change.
11 | ```gdscript
12 | var gdx := preload('res://editor-only/included/gdx.gd').new()
13 | var ui = gdx.render(func(): return (
14 | [VBoxContainer, [
15 | [Button],
16 | [Label],
17 | [TextEdit],
18 | ]
19 | ))
20 | add_child(ui)
21 | ```
22 |
23 | ## Structure
24 | An element structure is an array that looks like this `[NodeType or Node, { Properties }, [ Children ], Callable]`.
25 | - The first item should be:
26 | - A node type like `Button`, which will be instantiated
27 | - A raw node like `my_button` or even `self`
28 | - The order of all other items do not matter, and there can be however many of each.
29 | - A `Dictionary` item will set properties on the node.
30 | - Keys being the property names
31 | - Values being property values
32 | - Special names will be detailed in later sections, for things like signal connections and theme properties
33 | - An `Array` should be a list of other elements, which will be added as children. Arrays can be nested at any depth, until a node type / raw node is found as the first item.
34 | - So `[Container, [Button]]` is the same as `[Container, [[[[[Button]]]]]]`
35 | - Keep in mind that an element itself is an array, so you'll have quite a few nested arrays for children.
36 | - This is incorrect
37 | ```gdscript
38 | # Incorrect
39 | [VBoxContainer, [ # This array is used to denote children, so it cannot also denote the Button's element
40 | Button
41 | ]]
42 |
43 | # Correct
44 | [VBoxContainer, [
45 | [Button] # The button gets its own array
46 | ]]
47 | ```
48 | - You can also put a callable, which will be passed the node as the paramater. This is for when the first item is a node type, and you need direct access to it.
49 | Example:
50 | ```gdscript
51 | [Button, {
52 | text = "Click me!"
53 | },
54 | func(it: Button):
55 | it.grab_focus()
56 | print(it.position)
57 | , # It's annoying but gdscript syntax requires a trailing comma at a certain indentation
58 | ]
59 | ```
60 |
61 | ## Nested Properties
62 | Some property values have nested properties, like Color or Vector2. You can set those too like you would with a NodePath. And they can edit previously set props.
63 | ```gdscript
64 | [Label, {
65 | text = "My Text!",
66 | custom_minimum_size = Vector2(0, 0),
67 | 'custom_minimum_size:y' = 100,
68 | 'modulate:a' = 0.5,
69 | }]
70 | ```
71 | > [!WARNING]
72 | > This appears to have broken in godot 4.3 stable. Avoid for now
73 |
74 | ## Signals
75 | A signal connection is just a prop with "on_" followed by the signal name.
76 | Example:
77 | ```gdscript
78 | [Button, {
79 | text = "Click me!",
80 | on_pressed = func():
81 | print("You clicked me!")
82 | pass,
83 | }]
84 | ```
85 | > Godot's function syntax is pretty annoying here. For some reason it complains unless that last comma is there
86 |
87 | ## Theme Properties
88 | Control nodes have various theme properties that are only editable using methods like `add_theme_color_override`.
89 | To customize them more easily in gdx, you can use special `theme_` props instead.
90 | ```gdscript
91 | [Button, {
92 | theme_constant = {
93 | outline_size = 1,
94 | icon_max_width = 24,
95 | },
96 | theme_color = {
97 | font_color = Color.RED
98 | },
99 | theme_font = {
100 | font = Font.new()
101 | },
102 | theme_font_size = {
103 | font_size = 20
104 | },
105 | theme_icon = {
106 | icon = Icon.new()
107 | },
108 | theme_stylebox = {
109 | normal = StyleBoxEmpty.new()
110 | }
111 | }]
112 | ```
113 |
114 | ## Rerender
115 | When you want to update the ui, you call `GDX.render()` again, without any arguments. This will rerender the the current tree of elements. GDX will do its best to reuse nodes from the previous render, rather than create new nodes every time.
116 |
117 | Rerender Example:
118 | ```gdscript
119 | var gdx := preload('res://editor-only/included/gdx.gd').new()
120 | gdx.render(func(): return (
121 | [Button, {
122 | on_pressed = func():
123 | gdx.render(), # rerenders the UI
124 | }]
125 | ))
126 |
127 | func some_time_later():
128 | gdx.render() # You can rerender from outside as well
129 | ```
130 |
131 | ## State
132 | State is the data that your UI reads. In gdx, state is external to the render function, meaning you store it in variables outside of `GDX.render()`. Due to gdscript limitations, you should store in reference types like `Array`, `Dictionary`, or `Object`. You can store in variables of a class, just not in local variables of a function.
133 |
134 | To update your UI along with your state change, you just set your state variable then call `gdx.render()` to rerender the ui.
135 | Here's a simple counter:
136 | ```gdscript
137 | var gdx := preload('res://editor-only/included/gdx.gd').new()
138 | var st := { counter = 0 }
139 | var my_ui = gdx.render(func(): return (
140 | [Button, {
141 | text = "Count: " + str(st.counter)
142 | on_pressed = func():
143 | st.counter += 1
144 | gdx.render(),
145 | }]
146 | ))
147 | ```
148 |
149 | ## List Rendering / Dynamic Rendering
150 | Just map an array into an array of elements. You can also use an extra method `gdx.map_i(array, func(element, index, array, callable): return)`, if you want access to the index, array, or function itself while mapping (useful for recursion / tree rendering).
151 | ```gdscript
152 | var my_list := ["Hello", "There", "World"]
153 | var ui = gdx.render(func(): return (
154 | [VBoxContainer, [
155 | my_list.map(func(item): return (
156 | [Label, { text = item }]
157 | )),
158 | [LineEdit, {
159 | on_text_submitted = func(text):
160 | my_list.append(text)
161 | gdx.render()
162 | pass,
163 | }]
164 | ]]
165 | ))
166 | ```
167 | Dynamic rendering can cause some nodes to be needlessly recreated. This is because nodes are tracked by their index in their parent. Rendering a dynamic list makes that index unreliable, so instead you can provide a name. Elements with a name provided can always be tracked and reused.
168 |
169 | If you ran the example above, you may have noticed that the LineEdit keeps unfocusing after submitting. This is because the node was being recreated. If you give it a name, it will be reused instead
170 | ```gdscript
171 | var my_list := ["Hello", "There", "World"]
172 | var ui = GDX.render(func(): return (
173 | [VBoxContainer, [
174 | my_list.map(func(item): return (
175 | [Label, { text = item }]
176 | )),
177 | [LineEdit, {
178 | name = "Text Input",
179 | on_text_submitted = func(text):
180 | my_list.append(text)
181 | GDX.render()
182 | pass,
183 | }]
184 | ]]
185 | ))
186 | ```
187 |
188 | ## Callables in element
189 | Sometimes you just need access to the node, and do some direct calls on it. For these cases, you can also put a callable in an element's array
190 | ```gdscript
191 | [OptionButton, func(it: OptionButton):
192 | it.add_item("First")
193 | it.add_item("Second")
194 | it.add_item("Third")
195 | ]
196 | ```
197 |
198 | ## Access element outside of render
199 | Sometimes you need access to an element outside of the render function. There are 2 ways of doing this.
200 | The best way is to create the node beforehand, and just include it as an element (Recommended)
201 | ```gdscript
202 | var my_button := Button.new()
203 | var ui = GDX.render(func(): return (
204 | [MarginContainer, [
205 | [my_button]
206 | ]]
207 | ))
208 | my_button.text = "Some text"
209 | ```
210 |
211 | Another way is using a "state" and a callable, more akin to reactjs (Not recommended)
212 | ```gdscript
213 | var st := {
214 | my_button = null
215 | }
216 | var ui = GDX.render(func(): return (
217 | [MarginContainer, [
218 | [Button, func(it: Button):
219 | st.my_button = it
220 | ]
221 | ]]
222 | ))
223 | my_button.text = "Some text"
224 | ```
225 |
226 |
227 | ## Streamlining `add_child`
228 | The above method can even be used to skip the `add_child(my_ui)`.
229 | All you have to do is include the parent node in the element tree.
230 | You can do all the same things to a raw node too, like setting props, callables, and children.
231 | ```gdscript
232 | GDX.render(func(): return (
233 | [self, { "self_modulate:a" = 0.8 }, [
234 | [VBoxContainer, [
235 | [HBoxContainer, [
236 | [Button]
237 | ]]
238 | ]]
239 | ]]
240 | ))
241 | ```
242 |
243 | ## Reusable Components
244 | There's no special syntax for reusable components. You can use gdscript's existing features to achieve this.
245 | Using a class, you'll see that you need a separate instance of gdx for each component.
246 | ```gdscript
247 | class TaskItem extends HBoxContainer:
248 | var GDX := preload('res://editor-only/included/gdx.gd').new()
249 | signal deleted
250 | var text := ""
251 | var enabled := false
252 | func _ready():
253 | GDX.render(func(): return (
254 | [self, [
255 | [CheckBox, {
256 | text = text
257 | }],
258 | [Button, {
259 | text = "Delete",
260 | on_pressed = deleted.emit
261 | }]
262 | ]]
263 | ))
264 |
265 | class TaskList extends VBoxContainer:
266 | var GDX := preload('res://editor-only/included/gdx.gd').new()
267 | var items := []
268 | func _ready():
269 | GDX.render(func(): return (
270 | [self, [
271 | GDX.map_i(items, func(item, i): return (
272 | [TaskItem, {
273 | text = item.text,
274 | enabled = item.enabled,
275 | on_deleted = func():
276 | items.remove_at(i)
277 | GDX.render(),
278 | }]
279 | ))
280 | ]]
281 | ))
282 | ```
283 |
--------------------------------------------------------------------------------
/editor-only/included/addon_import_plugin.gd:
--------------------------------------------------------------------------------
1 | @tool
2 | extends EditorPlugin
3 |
4 | var gdx := preload('res://editor-only/included/gdx.gd').new()
5 | var Paths := preload("res://editor-only/included/paths.gd")
6 |
7 | func delete_directory(path: String):
8 | print('deleting ', path)
9 | for dir_name in DirAccess.get_directories_at(path):
10 | delete_directory(path.path_join(dir_name))
11 |
12 | for file_name in DirAccess.get_files_at(path):
13 | DirAccess.remove_absolute(path.path_join(file_name))
14 |
15 | DirAccess.remove_absolute(path)
16 |
17 | func import_by_colors():
18 | var project_settings := ConfigFile.new()
19 | project_settings.load(Paths.global.path_join("project.godot"))
20 | var folder_colors: Dictionary = project_settings.get_value("file_customization", "folder_colors", {})
21 |
22 | var rfs := EditorInterface.get_resource_filesystem()
23 | while rfs.is_scanning():
24 | await get_tree().process_frame
25 |
26 | var plugins_to_enable := []
27 | var plugin_exist_map := {}
28 |
29 | # Delete reds and oranges
30 | DirAccess.make_dir_absolute(Paths.local.path_join("addons"))
31 | for dir_name in DirAccess.get_directories_at("res://addons"):
32 | var path := "res://addons".path_join(dir_name) + "/"
33 | var plugin_path := path.path_join("plugin.cfg")
34 |
35 | if FileAccess.file_exists(plugin_path):
36 | plugin_exist_map[plugin_path] = true
37 | EditorInterface.set_plugin_enabled(plugin_path, false)
38 |
39 | if FileAccess.file_exists(path.path_join(".global-red")):
40 | delete_directory(Paths.local.path_join("addons").path_join(dir_name))
41 |
42 | elif FileAccess.file_exists(path.path_join(".global-orange")):
43 | if folder_colors.get(path, "") != "orange":
44 | delete_directory(Paths.local.path_join("addons").path_join(dir_name))
45 |
46 | for addon_path in DirAccess.get_directories_at(Paths.global.path_join("addons")):
47 | var res_path := "res://addons".path_join(addon_path)
48 | var color = folder_colors.get(res_path + '/')
49 |
50 | # Default no color
51 | if color == null:
52 | continue
53 |
54 | # Versioned
55 | if color in ["orange", "green"]:
56 | var res_cfg_path := res_path.path_join("plugin.cfg")
57 | var glb_cfg_path := Paths.global.path_join("addons").path_join(addon_path).path_join("plugin.cfg")
58 | if FileAccess.file_exists(res_cfg_path) and FileAccess.file_exists(glb_cfg_path):
59 | var cfg := ConfigFile.new()
60 | cfg.load(res_path.path_join("plugin.cfg"))
61 | var old_version = cfg.get_value("plugin", "version", "")
62 | var nfg := ConfigFile.new()
63 | nfg.load(glb_cfg_path)
64 | var new_version = nfg.get_value("plugin", "version", "")
65 |
66 | if old_version == new_version:
67 | print("Versions match. Skipping ", addon_path)
68 | continue
69 |
70 | # Make dummy .global-color file for tracking
71 | DirAccess.make_dir_recursive_absolute(res_path)
72 | FileAccess.open(res_path.path_join(".global-" + color), FileAccess.WRITE)
73 |
74 | var plugin_path := res_path.path_join("plugin.cfg")
75 | if FileAccess.file_exists(plugin_path):
76 | plugin_exist_map[res_path.path_join("plugin.cfg")] = true
77 |
78 | var paths := ["addons".path_join(addon_path)]
79 | while !paths.is_empty():
80 | var path: String = paths.pop_back()
81 | DirAccess.make_dir_absolute(Paths.local.path_join(path))
82 | for dir_name in DirAccess.get_directories_at(Paths.global.path_join(path)):
83 | paths.push_back(path.path_join(dir_name))
84 | for file_name in DirAccess.get_files_at(Paths.global.path_join(path)):
85 | DirAccess.copy_absolute(
86 | Paths.global.path_join(path) + "/" + file_name,
87 | Paths.local.path_join(path) + "/" + file_name
88 | )
89 |
90 | var popup := PopupPanel.new()
91 | gdx.render(func(): return (
92 | [self, [
93 | [popup, {
94 | popup_window = false,
95 | title = "Please wait",
96 | exclusive = true,
97 | borderless = false,
98 | keep_title_visible = true,
99 | }, [
100 | [Label, {
101 | text = "Enabling Imported Addons"
102 | }]
103 | ]]
104 | ]]
105 | ))
106 | popup.popup_centered()
107 |
108 | rfs.scan_sources()
109 | while rfs.is_scanning():
110 | await get_tree().process_frame
111 |
112 | for dir_name in DirAccess.get_directories_at("res://addons"):
113 | var cfg_path := "res://addons".path_join(dir_name).path_join("plugin.cfg")
114 | if FileAccess.file_exists(cfg_path):
115 | EditorInterface.set_plugin_enabled(cfg_path, true)
116 | popup.hide()
117 |
118 |
119 | func copy_addons(addons: Array):
120 | var plugin_folders_to_enable := []
121 | var files_to_reimport := []
122 | var rfs := EditorInterface.get_resource_filesystem()
123 |
124 | #var Paths.local := ProjectSettings.globalize_path("res://")
125 |
126 | for addon in addons:
127 |
128 | if addon is not String:
129 | continue
130 |
131 | var path: String = Paths.global + "/addons/" + addon + "/"
132 |
133 | # Check if plugin.cfg file exists
134 | if !FileAccess.file_exists(path + "/plugin.cfg"):
135 | push_warning("A plugin.cfg file wasn't found at path ", path, "")
136 |
137 | # Check if the plugin already exists in the project
138 | var already_exists := FileAccess.file_exists("res://addons/" + addon + "/" + "plugin.cfg")
139 | if EditorInterface.is_plugin_enabled(addon):
140 | EditorInterface.set_plugin_enabled(addon, false)
141 |
142 | # Recursively copy the folder contents into this project
143 | var dirs := [addon]
144 | var i := 0
145 | while i < dirs.size():
146 | var dir: String = dirs[i]
147 | var global_path: String = Paths.global + '/addons/' + dir
148 | var local_path: String = Paths.local + "/addons/" + dir
149 | DirAccess.make_dir_recursive_absolute(local_path)
150 |
151 | for file in DirAccess.get_files_at(global_path):
152 | DirAccess.copy_absolute(global_path + '/' + file, local_path + '/' + file)
153 | #files_to_reimport.push_back(local_path + "/" + file)
154 |
155 | for d in DirAccess.get_directories_at(global_path):
156 | dirs.append(dir + '/' + d)
157 | i += 1
158 |
159 | plugin_folders_to_enable.push_back(addon)
160 | #if !already_exists:
161 | #plugin_folders_to_enable.push_back(addon)
162 |
163 | # The FileSystem dock doesn't properly scan new files if scanned immediately
164 | #rfs.scan()
165 | var pop := PopupPanel.new()
166 | gdx.render(func(): return (
167 | [self, [
168 | [pop, { popup_window = false }, [
169 | [Label, {
170 | text = "Enabling addons..."
171 | }]
172 | ]]
173 | ]]
174 | ))
175 |
176 | pop.popup_centered()
177 |
178 | rfs.scan_sources()
179 | print("begin scan")
180 | while rfs.is_scanning():
181 | #print('is scanning')
182 | await get_tree().process_frame
183 | await get_tree().process_frame
184 | print("end scan")
185 |
186 | for folder in plugin_folders_to_enable:
187 | EditorInterface.set_plugin_enabled(folder, true)
188 | pop.hide()
189 | pop.queue_free()
190 |
191 | func _enter_tree() -> void:
192 |
193 | if Paths.global == Paths.local:
194 | return
195 |
196 | #print(gdx.map_i([1, 2, 3], func(a): return "num-" + str(a)))
197 |
198 | await import_by_colors()
199 |
200 | var path: String = Paths.global
201 | var popup := PopupPanel.new()
202 |
203 | var current_dir_map := {}
204 | var update_dir_map := func():
205 | current_dir_map.clear()
206 | if DirAccess.dir_exists_absolute("res://addons/"):
207 | for dir in DirAccess.get_directories_at("res://addons/"):
208 | current_dir_map[dir] = true
209 | update_dir_map.call()
210 |
211 | var addons: Array = Array(DirAccess.get_directories_at(path + "/addons")).map(func(a):
212 | return {
213 | text = a,
214 | checked = !current_dir_map.has(a),
215 | }
216 | )
217 | await get_tree().process_frame
218 | gdx.render(func(): return (
219 | [popup, {
220 | keep_title_visible = true,
221 | borderless = false,
222 | transient = true,
223 | popup_window = false,
224 | title = "Choose addons to copy over"
225 | }, [
226 | [VBoxContainer, [
227 | [CheckBox, {
228 | text = "(All)",
229 | on_toggled = func(v):
230 | for a in addons:
231 | a.checked = v
232 | gdx.render()
233 | pass,
234 | }],
235 | [MarginContainer, {
236 | size_flags_horizontal = Control.SIZE_EXPAND_FILL,
237 | size_flags_vertical = Control.SIZE_EXPAND_FILL,
238 | }, [
239 | [Panel],
240 | [ScrollContainer, {
241 | custom_minimum_size = Vector2(200, 200),
242 | horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
243 | }, [
244 | [MarginContainer, {
245 | size_flags_horizontal = Control.SIZE_EXPAND_FILL,
246 | size_flags_vertical = Control.SIZE_EXPAND_FILL,
247 | theme_constant = {
248 | margin_left = 8,
249 | margin_right = 8,
250 | margin_top = 8,
251 | margin_bottom = 8
252 | }
253 | }, [
254 | [VBoxContainer, [
255 | gdx.map_i(addons, func(a): return (
256 |
257 | [HBoxContainer, {
258 | size_flags_horizontal = Control.SIZE_EXPAND_FILL
259 | }, [
260 | [CheckBox, {
261 | text = a.text,
262 | button_pressed = a.checked,
263 | size_flags_horizontal = Control.SIZE_EXPAND_FILL,
264 | on_toggled = func(v):
265 | a.checked = v
266 | gdx.render()
267 | pass,
268 | }],
269 | [Label, { text = "New!" }] if !current_dir_map.has(a.text) else []
270 | ]]
271 | ))
272 | #addons.map(func(a): return (
273 | #))
274 | ]]
275 | ]]
276 | ]]
277 | ]],
278 | [HBoxContainer, {
279 | alignment = HBoxContainer.ALIGNMENT_CENTER,
280 | theme_constant = {
281 | separation = 10
282 | }
283 | }, [
284 | [Button, {
285 | text = "Cancel",
286 | on_pressed = func():
287 | popup.hide()
288 | pass,
289 | }],
290 | [Button, {
291 | text = "(Re)import",
292 | on_pressed = func():
293 | popup.hide()
294 | copy_addons(addons.map(func(a):
295 | if a.checked:
296 | return a.text
297 | return null
298 | ))
299 | pass,
300 | }]
301 | ]]
302 | ]]
303 | ]]
304 | ))
305 |
306 | #print(Loader)
307 | EditorInterface.get_base_control().add_child(popup)
308 |
309 | var show_on_startup_setting := "addon_importer/show_on_startup"
310 | if ProjectSettings.get_setting(show_on_startup_setting, true):
311 | update_dir_map.call()
312 | popup.popup_centered()
313 | ProjectSettings.set_setting(show_on_startup_setting, false)
314 | ProjectSettings.save()
315 | add_tool_menu_item("Addon Importer", popup.popup_centered)
316 |
317 | func _exit_tree() -> void:
318 | #print('exiting')
319 | remove_tool_menu_item("Addon Importer")
320 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Godot Global Project
2 | A framework for making true global plugins for the Godot Game Engine.
3 |
4 | ## Installation
5 | This is not an addon, but a project for you to keep on your computer. All you need to do is open this project in godot, and a script will be injected into EditorSettings. The project has 3 directories, one for each type of plugin:
6 |
7 | - [`editor-only`](#editor-only) plugins are global, running in the Editor every time you open a project.
8 | - [`addons`](#addons) will be imported & enabled when projects load, depending on what color you assign their folders.
9 | - [`project-manager`](#project-manager) plugins will run alongside the project manager.
10 |
11 | There's a 4th `_internal` folder, where all the code that makes the project work is stored. You can ignore this unless you're curious of how things work under the hood.
12 |
13 | ## `editor-only`
14 | In this directory you store subfolders that contain your plugin. Scripts that end with `plugin.gd` and extend `EditorPlugin` will be loaded, instantiated, and ran everytime the editor loads.
15 | > As always, EditorPlugins should have `@tool` at the top of the file.
16 | ```
17 | editor-only
18 | └── my-subfolder
19 | ├── my-plugin.gd # ✓ Automatically instantiated
20 | └── my-script.gd # x Must load manually from "my-plugin.gd"
21 | ```
22 |
23 | They work mostly the same as normal EditorPlugins, but with a few differences. For starters, they will not be available to projects, only the editor, hence `editor-only`.
24 |
25 | These plugins are not copied into any project, rather they are loaded directly from the global-project folder. They're not like the plugins you enable in ProjectSettings, so they don't have access to:
26 | - `EditorPlugin` virtual methods
27 | - `EditorPlugin` signals
28 |
29 | Every other function should still be available though.
30 |
31 | ### Dependencies
32 |
33 | They're also handled in a special way when it comes to dependencies. Normally when loading resources, `res://` paths can only point to the current project's directory. To load global-project files, they must use absolute paths. The editor makes that very hard to do, so I've come up with a way to automatically convert them.
34 |
35 | When the global-project loads or saves, any resource in the `editor-only` or `project-manager` directory will be copied & processed into the `.processed` folder (hidden from the editor).
36 |
37 | The processed files have their paths converted to absolute, pointing to other files in the `.processed` directory. It works slightly different depending on whether its a script, a normal resource, or an imported resource:
38 | - Normal resources ending in `.tres` or `.tscn`, are copied into the `.processed` folder, and their `ext_resource` paths are converted.
39 | - For imported resources, the raw resource will not be touched. However, the `.import` file will be copied into the `.processed` folder, and its paths will be converted.
40 | - For scripts ending in `.gd`, their `preload` paths are converted.
41 | ```gdscript
42 | # This
43 | var my_res_file := preload("res://editor-only/my-plugin/my-file.gd")
44 | # Becomes something like this
45 | var my_abs_file := preload("D:/Godot/global-project//editor-only/my-plugin/my-file.gd")
46 | ```
47 | - If you want to load manually, you can get direct access to the global-project path by preloading the included `paths.gd` script like so
48 | ```gdscript
49 | var Paths := preload("res://editor-only/included/paths.gd")
50 |
51 | # Paths.global is the global-project path
52 | var my_file_path := Paths.global + "/my_file.txt"
53 | var some_file = FileAccess.open(my_file_path, FileAccess.READ)
54 |
55 | # Paths.processed is where the processed files are stored
56 | var my_resource_path := Paths.processed + "/my_resource.tres"
57 | var some_res = load(my_resource_path)
58 | ```
59 |
60 | > [!TIP]
61 | > The entire addon import functionality (next section) is implemented as an `editor-only` plugin in `/editor-only/included/addon-importer-plugin.gd`. You can view that code as an example.
62 | >
63 | > You'll notice that it uses a code based UI framework, [GDX](GDX.md). I made it because for most of the development, PackedScenes, or any resource with external dependencies, could not be loaded.
64 | > That's not the case anymore, but it's still included for convenience, and its how the included plugins render UI.
65 |
66 | > [!WARNING]
67 | > Binary resources, files ending in `.res` and `.scn`, won't be processed. Since they're not stored as text they can't be easily converted.
68 | >
69 | > Imported resources, like `icon.svg`, have an accompanying `.import` file. Only the `.import` will be converted and saved into the `.processed` folder. So when manually loading an asset, if you want to load it as a resource, use `Paths.processed`. But if you want the raw asset, use `Paths.global`.
70 |
71 | > [!CAUTION]
72 | > `class_name` should not be declared for `editor-only` and `project-manager` scripts. Since these files won't be in a project's directory, the editor won't load the class_names into the global namespace. Use preloads instead, which have similar intellisense. The only difference is they cannot be used as types directly.
73 | >
74 | > Built-in scripts with dependencies are also not supported, since currently it relies on the file extension to decide how to process the file. Built-in scripts are embedded in `.tres` or `.tscn` files, so they won't get processed like `.gd` scripts. Simple isolated scripts should still work though.
75 |
76 | ## `addons`
77 | Store your normal / typical addons in this directory, even ones from the AssetLib. There are various options for how these addons should be (automatically) imported, depending on folder colors.
78 | Set the folder color by `right click > Set Folder Color...`
79 |
80 | 
81 |
82 |
83 |
84 | | Folder_Color | Behavior |
85 | | --------------- | --- |
86 | | 🔵 Default | By default, addons won't be automatically imported.
Instead, a popup will appear once per project, prompting you to select which addons to import. The list will show all the addons in `addons` directory of the global project.
 If you need to see the popup again, for example to import new addons or update existing ones, go to **`Project > Tools > Adddon Importer`**.
All the below colors will import addons automatically, without this popup.
|
87 | | 🔴 Red | This is ideal if you're developing & testing your own addons locally. Red addons are synced exactly as they appear in the global project.
On load, they are deleted, and then copied over again. This means if addons are no longer Red in the global project, they will no longer exist in your other projects.
|
88 | | 🟠 Orange | This is recommended for asset store addons.
Only when the version in plugin.cfg has changed, the addons are deleted, then copied over. Addons that are no longer Orange will also be deleted.
This method makes it so files aren't copied over every time.
|
89 | | 🟡 Yellow | This is for compatability with certain addons. This is also the same behavior as my old 'globalize-plugins' addon.
On load, all yellow plugins are copied over, but nothing is deleted. Folders that are no longer yellow will still remain. Even if the addon's file structure / naming changes, the outdated files and folders will remain.
Some addons store user data / preferences within its directory. Red and Orange addons would keep overwriting those preferences, but Yellows won't.
|
90 | | 🟢 Green | This is the same as Yellow addons, but with version comparison. Nothing will happen if the plugin's version has not changed.
|
91 |
92 | ## `project-manager`
93 | - This folder works the same as the `editor-only` folder, except the scripts run in the project manager instead of the editor.
94 | - The scripts should not extend `EditorPlugin` and should not use any function from `EditorInterface`, since those do not exist in the project manager. But they should still end with `plugin.gd`
95 | - There is no simple api for accessing parts of the UI. You'll have to access nodes manually, but you shouldn't rely on `NodePaths`, as node names have random generated numbers in them. Use index based paths instead, like `Engine.get_main_loop().get_child(0).get_child(0)`, or recursively search the tree and use `String.match()`
96 | - I have created and included a `project-manager` plugin that adds an `inspect` button to the top right, to help you find the node in the SceneTree structure.
97 |
98 | 
99 |
100 | - This pops up a window that lets you view the project manager's scene tree. On the right is the selected node's property list.
101 |
102 | 
103 |
104 | - If you click `Pick from UI`, you can click directly in the UI to pick a node. The hovered node will be highlighted in red.
105 |
106 | 
107 |
108 |
109 |
110 | ## Troubleshooting
111 | Since editor-only and project-manager plugins load alongside the editor, if one of them is bugged, the editor may crash. To fix this:
112 | - Open the global-project's `project.godot` file directly, since global plugins are disabled for that project
113 | - Fix the bugged plugin or remove it altogether
114 | - It helps to open the console version of the editor, so you can view the debug logs
115 |
116 | If for some reason the global-project also crashes:
117 | - Find `_internal/loader.gd`. This is the script that loads plugins when it itself is loaded
118 | - Temporarily rename or move the file, so that it doesn't get loaded
119 | - If you suspect that the `loader.gd` file had a bug, please report it
120 |
121 | If that still doesn't work, there might be a bug in the injected EditorSettings script. This is very unlikely due to how simple it is, but just in case you'll want to remove that script. This requires you to locate the EditorSettings file.
122 |
123 | Check [here](https://docs.godotengine.org/en/stable/tutorials/io/data_paths.html#editor-data-paths) for its location. Open the `editor_settings-4.3` (or whichever verion) file in a text editor and erase the script. It'll look something like this, just erase this whole chunk of text:
124 |
125 | 
126 |
127 | If you don't know how to open it in a text editor, or are too scared to make changes, just delete the whole `editor_settings-4.x` file, and godot will recreate it when it next loads. Again, its very unlikely you'll need to do this.
128 |
--------------------------------------------------------------------------------
/project-manager/inspect/inspect_plugin.gd:
--------------------------------------------------------------------------------
1 | extends Control
2 |
3 | var gdx := preload("res://editor-only/included/gdx.gd").new()
4 | var helpers := preload("res://project-manager/inspect/helpers.gd")
5 |
6 | var topmost_node: Control
7 | var hovered_nodes := {}
8 | var all_controls := {}
9 |
10 | var grouped_props := {}
11 | var inspected_node: Node:
12 | set(v):
13 | inspected_node = v
14 | grouped_props.clear()
15 | var scripts := [v.get_script()]
16 | while !scripts.is_empty():
17 | var scr = scripts.pop_back()
18 | if scr is Script:
19 | scripts.push_back(scr.get_base_script())
20 | var sname := scr.get_global_name() as StringName
21 | if sname.is_empty():
22 | sname = scr.resource_path()
23 | grouped_props[sname] = scr.get_script_property_list()
24 | break
25 | var classes := [v.get_class()]
26 | while !classes.is_empty():
27 | var c = classes.pop_back() as String
28 | classes.push_back(ClassDB.get_parent_class(c))
29 | grouped_props[c] = ClassDB.class_get_property_list(c, true)
30 | if c == "Object":
31 | break
32 |
33 | var popup := PopupPanel.new()
34 |
35 | var inspect_button := Button.new()
36 | var inspecting := false:
37 | set(v):
38 | if v:
39 | #if !popup.visible:
40 | #popup.popup_centered(Vector2(500, 400))
41 | mouse_filter = MouseFilter.MOUSE_FILTER_STOP
42 | get_window().grab_focus()
43 | else:
44 | mouse_filter = MouseFilter.MOUSE_FILTER_IGNORE
45 | topmost_node.queue_redraw()
46 | inspecting = v
47 |
48 | func _enter_tree() -> void:
49 | print("I was spawned")
50 | set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
51 | print("extracting")
52 | print(helpers.extract_node([0, 1, 0, 0]))
53 | print(helpers.extract_node([0, "*EditorAbout*"]))
54 | #mouse_filter = MOUSE_FILTER_PASS
55 |
56 | var root: Node = get_parent()
57 | var manager: Node = root.get_child(0, true)
58 | #print(manager.get_children(true))
59 |
60 | var nodes := [root]
61 | while !nodes.is_empty():
62 | var node: Node = nodes.pop_back()
63 | nodes.append_array(node.get_children(true))
64 | connect_node(node)
65 | get_tree().node_added.connect(connect_node)
66 |
67 | mouse_filter = MouseFilter.MOUSE_FILTER_IGNORE
68 | inspect_button.icon = get_theme_icon("FileTree", "EditorIcons")
69 | inspect_button.text = "Inspect"
70 | var button_parent: Node = root.get_child(0, true).get_child(1, true).get_child(0, true).get_child(0, true).get_child(2, true)
71 | #print(button_parent)
72 | button_parent.add_child(inspect_button)
73 | button_parent.move_child(inspect_button, 0)
74 | inspect_button.pressed.connect(
75 | func():
76 | popup.popup_centered(Vector2(600, 400))
77 | #inspecting = true
78 | )
79 |
80 | var scroll_container := ScrollContainer.new()
81 | var button_group := ButtonGroup.new()
82 | button_group.allow_unpress = true
83 | gdx.render(func(): return ([
84 | [self, [
85 | [popup, {
86 | popup_window = false,
87 | title = "Inspect Nodes",
88 | borderless = false,
89 | keep_title_visible = true,
90 | unresizable = false,
91 | #mouse_passthrough = true
92 | }, [
93 | [HBoxContainer, {
94 | size_flags_horizontal = Control.SIZE_EXPAND_FILL,
95 | size_flags_vertical = Control.SIZE_EXPAND_FILL
96 | #collapsed = inspected_node == null,
97 | }, [
98 | # Left Half
99 | [VBoxContainer, {
100 | size_flags_horizontal = Control.SIZE_EXPAND_FILL,
101 | size_flags_vertical = Control.SIZE_EXPAND_FILL
102 | }, [
103 | [Button, {
104 | text = "Pick from UI",
105 | icon = get_theme_icon("ColorPick", "EditorIcons"),
106 | size_flags_horizontal = SIZE_SHRINK_BEGIN,
107 | on_pressed = func():
108 | inspecting = true,
109 | }],
110 | [scroll_container, {
111 | #"custom_minimum_size" = Vector2(100, 200),
112 | #follow_focus = true,
113 | size_flags_vertical = Control.SIZE_EXPAND_FILL,
114 | size_flags_horizontal = Control.SIZE_EXPAND_FILL,
115 | }, [
116 | [GridContainer, {
117 | columns = 2
118 | }, [
119 | [Control],
120 | [Label, {text = "Inspector"}],
121 | gdx.map_i([root], func(item: Node, index, array, callable):
122 | if item == self or self.is_ancestor_of(item):
123 | return []
124 | return ([
125 | [MarginContainer, [
126 | [Button, {
127 | disabled = true if item.get_child_count(true) == 0 else false,
128 | #alignment = HORIZONTAL_ALIGNMENT_LEFT,
129 | toggle_mode = true,
130 | on_pressed = func(val = false):
131 | item.set_meta("expanded", !item.get_meta("expanded", false))
132 | #item.set_meta('expanded', val)
133 | #item.set_meta('expanded', !item.get_meta('expanded', false))
134 | gdx.render()
135 | pass,
136 | #theme_stylebox = {
137 | #disabled = {
138 | #
139 | #},
140 | #}
141 | }],
142 | [Label, {
143 | text = (
144 | "" if item.get_child_count(true) == 0 else
145 | "v" if item.get_meta("expanded", false) else
146 | ">"
147 | ),
148 | mouse_filter = MOUSE_FILTER_IGNORE,
149 | size_flags_vertical = SIZE_SHRINK_BEGIN,
150 | }]
151 | ]],
152 | [VBoxContainer, {
153 | size_flags_horizontal = Control.SIZE_EXPAND_FILL,
154 | }, [
155 | [Button, {
156 | size_flags_horizontal = Control.SIZE_EXPAND_FILL,
157 | alignment = HORIZONTAL_ALIGNMENT_LEFT,
158 | text = item.name,
159 | toggle_mode = true,
160 | button_group = button_group,
161 | #on_gui_input = func(event = null):
162 | #print(event)
163 | #pass,
164 | #button_pressed = inspected_node == item,
165 | on_toggled = func(val):
166 | #if v:
167 | if val:
168 | inspected_node = item
169 | #elif inspected_node == item:
170 | #inspected_node = null
171 | gdx.render()
172 | pass,
173 | on_mouse_entered = func():
174 | if item is Control:
175 | if topmost_node:
176 | topmost_node.queue_redraw()
177 | topmost_node = item
178 | hovered_nodes[item] = true
179 | item.queue_redraw()
180 | ,
181 | on_mouse_exited = func():
182 | if item is Control:
183 | if topmost_node:
184 | topmost_node.queue_redraw()
185 | if topmost_node == item:
186 | topmost_node = null
187 | hovered_nodes.erase(item)
188 | item.queue_redraw()
189 | ,
190 | icon = get_theme_icon(item.get_class(), "EditorIcons")
191 | },
192 | func(it: Button):
193 | if item.get_meta("just_grabbed_focus", false):
194 | it.grab_focus()
195 | it.set_pressed_no_signal(true)
196 | item.remove_meta("just_grabbed_focus")
197 | await get_tree().process_frame
198 | var offset := it.global_position - scroll_container.global_position
199 | var scroll := Vector2(scroll_container.scroll_horizontal, scroll_container.scroll_vertical)
200 | offset += scroll
201 | scroll_container.scroll_vertical = offset.y - scroll_container.size.y / 2.0
202 | scroll_container.scroll_horizontal = offset.x + it.size.x
203 | prints("focused pos ", it.global_position, '-', scroll, offset)
204 | elif item.has_meta("just_lost_focus"):
205 | it.release_focus()
206 | it.set_pressed_no_signal(false)
207 | it.remove_meta("just_lost_focus")
208 | get_script()
209 | ,
210 | ],
211 | [GridContainer, {
212 | columns = 2,
213 | }, [
214 | gdx.map_i(item.get_children(true), callable)
215 | ]] if item.get_meta("expanded", false) else [],
216 | ]],
217 | ]
218 | )),
219 | ]]
220 | ]],
221 | ]],
222 |
223 | # Right Half
224 | [VBoxContainer, {
225 | size_flags_horizontal = Control.SIZE_EXPAND_FILL,
226 | size_flags_vertical = Control.SIZE_EXPAND_FILL,
227 | #size_flags_stretch_ratio = .65,
228 | }, [
229 | [
230 | [Label, {
231 | text = inspected_node.name
232 | }],
233 | [Button, {
234 | text = "Copy Path as Index Array",
235 | icon = get_theme_icon("ActionCopy", "EditorIcons"),
236 | on_pressed = func():
237 | #var list := str(inspected_node.get_path()).split("/", false)
238 | #var index_list := []
239 | var list := []
240 | var node := inspected_node
241 | while node != root:
242 | list.push_back(node.get_index())
243 | node = node.get_parent()
244 | list.reverse()
245 | DisplayServer.clipboard_set(str(list))
246 | print(list)
247 | pass,
248 | }],
249 | [Label, {
250 | text = str(inspected_node.get_path()),
251 | clip_text = true,
252 | }],
253 | #[Label, {
254 | #text = "Hello end"
255 | #}],
256 | [ScrollContainer, {
257 | size_flags_vertical = Control.SIZE_EXPAND_FILL,
258 | size_flags_horizontal = Control.SIZE_EXPAND_FILL,
259 | }, [
260 | [VBoxContainer, {
261 | size_flags_horizontal = Control.SIZE_EXPAND_FILL,
262 | size_flags_vertical = Control.SIZE_EXPAND_FILL,
263 | }, [
264 | gdx.map_key(
265 | grouped_props,
266 | func(key, value, i): return ([
267 | [Button, {
268 | text = key
269 | }],
270 | [GridContainer, {
271 | columns = 2,
272 | size_flags_horizontal = Control.SIZE_EXPAND_FILL,
273 | size_flags_vertical = Control.SIZE_EXPAND_FILL,
274 | }, func(it: Container): it.queue_sort(), [
275 | gdx.map_i(value,
276 | func(prop): return ([
277 | [Button, {
278 | text = prop.name,
279 | #clip_text = true,
280 | alignment = HORIZONTAL_ALIGNMENT_LEFT,
281 | autowrap_mode = TextServer.AUTOWRAP_ARBITRARY,
282 | size_flags_horizontal = Control.SIZE_EXPAND_FILL,
283 | size_flags_vertical = Control.SIZE_EXPAND_FILL,
284 | }],
285 | [Label, {
286 | text = inspected_node.get(prop.name),
287 | #clip_text = true,
288 | autowrap_mode = TextServer.AUTOWRAP_ARBITRARY,
289 | size_flags_horizontal = Control.SIZE_EXPAND_FILL,
290 | size_flags_vertical = Control.SIZE_EXPAND_FILL,
291 | }]
292 | ])
293 | )
294 | ]]
295 | ]),
296 | )
297 | ]],
298 | #[GridContainer, {
299 | #columns = 2,
300 | #size_flags_horizontal = Control.SIZE_EXPAND_FILL,
301 | #size_flags_vertical = Control.SIZE_EXPAND_FILL
302 | #}, [
303 | #gdx.map_i(
304 | #inspected_node.get_property_list(),
305 | #func(prop, index, array, callable): return (
306 | #[
307 | #[Button, {
308 | #text = prop.name,
309 | #size_flags_horizontal = Control.SIZE_EXPAND_FILL,
310 | #size_flags_vertical = Control.SIZE_EXPAND_FILL,
311 | #alignment = HORIZONTAL_ALIGNMENT_LEFT,
312 | #clip_text = true,
313 | #}],
314 | #[Label, {
315 | #text = str(inspected_node.get_indexed(prop.name)),
316 | #size_flags_horizontal = Control.SIZE_EXPAND_FILL,
317 | #clip_text = true,
318 | #}]
319 | #]
320 | #),
321 | #)
322 | #]]
323 | ]]
324 | ],
325 | ]] if inspected_node else [],
326 | ]],
327 | ]]
328 | ]]
329 | ]))
330 | popup.popup_hide.connect(
331 | func():
332 | inspecting = false
333 | #inspect_button.button_pressed = false
334 | #inspect_button.set_pressed_no_signal(false)
335 | )
336 | popup.focus_entered.connect(
337 | func():
338 | topmost_node.queue_redraw()
339 | topmost_node = null
340 | )
341 | #popup.popup_centered(Vector2(500, 400))
342 |
343 | func connect_node(node: Node):
344 | if node is Control and node != self and node != popup and !is_ancestor_of(node):
345 | all_controls[node] = true
346 | node.draw.connect(node_draw.bind(node))
347 | node.tree_exiting.connect(disconnect_node.bind(node))
348 | #node.mouse_entered.connect(node_mouse_entered.bind(node))
349 | #node.mouse_exited.connect(node_mouse_exited.bind(node))
350 | #node.gui_input.connect(node_gui_input.bind(node))
351 |
352 | func disconnect_node(node: Node):
353 | all_controls.erase(node)
354 |
355 | func _gui_input(event: InputEvent) -> void:
356 | if event is InputEventMouseButton and event.pressed:
357 | var old_inspected = inspected_node
358 | inspected_node = topmost_node
359 | if inspected_node:
360 | var nodes: Array[Node] = [inspected_node.get_parent()]
361 | while !nodes.is_empty():
362 | var node := nodes.pop_back() as Node
363 | node.set_meta("expanded", true)
364 | if node == get_tree().root:
365 | break
366 | nodes.push_back(node.get_parent())
367 | popup.grab_focus()
368 | inspected_node.set_meta("just_grabbed_focus", true)
369 | if old_inspected is Node:
370 | old_inspected.set_meta("just_lost_focus", true)
371 | inspecting = false
372 | gdx.render()
373 | elif event is InputEventMouseMotion:
374 | var recheck_top := false
375 | for node: Control in all_controls:
376 | if !is_instance_valid(node):
377 | continue
378 | if !node.is_visible_in_tree():
379 | hovered_nodes.erase(node)
380 | continue
381 | if !node.get_window().has_focus():
382 | hovered_nodes.erase(node)
383 | continue
384 |
385 | pass
386 |
387 |
388 | var new_val := node.get_global_rect().has_point(event.position)
389 | var old_val = hovered_nodes.get(node, false)
390 | if new_val != old_val:
391 | recheck_top = true
392 | node.queue_redraw()
393 | if new_val:
394 | hovered_nodes[node] = true
395 | else:
396 | hovered_nodes.erase(node)
397 |
398 | if recheck_top:
399 | if topmost_node:
400 | topmost_node.queue_redraw()
401 | topmost_node = null
402 | for node: Control in hovered_nodes:
403 | if !topmost_node or node.is_greater_than(topmost_node):
404 | topmost_node = node
405 | if topmost_node:
406 | topmost_node.queue_redraw()
407 | #print(topmost_node.get_path())
408 |
409 |
410 | func node_gui_input(event: InputEvent, node: Control):
411 | pass
412 | #if event is InputEventMouseButton:
413 | #if event.pressed:
414 | #node.accept_event()
415 |
416 | func node_mouse_entered(node: Control):
417 | print("entered ", node)
418 | hovered_nodes[node] = true
419 | #node.set_meta("hovered", true)
420 | node.queue_redraw()
421 |
422 | func node_mouse_exited(node: Control):
423 | print('exited ', node)
424 | hovered_nodes[node] = false
425 | #node.set_meta("hovered", false)
426 | node.queue_redraw()
427 |
428 | func node_draw(node: Control):
429 | #if inspecting:
430 | if hovered_nodes.get(node):
431 | if node == topmost_node:
432 | node.draw_rect(Rect2(Vector2(), node.size), Color(Color.RED, .5))
433 | #else:
434 | #node.draw_rect(Rect2(Vector2(), node.size), Color(Color.RED, .0))
435 | #if node.get_meta("hovered", false):
436 |
--------------------------------------------------------------------------------