├── .editorconfig
├── .gitignore
├── LICENSE
├── README.md
├── __init__.gd
├── docs
├── .nojekyll
├── README.md
├── _coverpage.md
└── index.html
├── editor
├── __init__.gd
└── tools.gd
├── filesystem
├── __init__.gd
├── path.gd
└── shutil.gd
├── input
├── GestureDetector.gd
└── __init__.gd
├── plugin.cfg
├── plugin.gd
├── resource
├── GridBackgroundTexture.gd
├── __init__.gd
├── image.gd
└── resource.gd
├── scene
├── __init__.gd
└── gui
│ ├── AsyncHttpTextutreRect.gd
│ ├── AsyncTextureRect.gd
│ ├── GridBackground.gd
│ ├── InfinityList.gd
│ ├── ItemListEnhanced.gd
│ ├── Menu.gd
│ └── __init__.gd
├── setup.py
└── utils
├── AsyncTaskQueue.gd
├── InstanceManager.gd
├── ObjectPool.gd
├── RandomPool.gd
├── TexturePacker.gd
├── __init__.gd
├── csv.gd
├── http.gd
├── json.gd
├── utils.gd
└── uuid.gd
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.gd]
2 | indent_style = tab
3 | indent_size = 4
4 | end_of_line = lf
5 | charset = utf-8
6 | trim_trailing_whitespace = false
7 | insert_final_newline = true
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | logs
2 | .vscode
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | ##################################################################################
2 | # Tool generated DO NOT modify #
3 | ##################################################################################
4 | # This file is part of #
5 | # GodotExplorer #
6 | # https://github.com/GodotExplorer #
7 | ##################################################################################
8 | # Copyright (c) 2017-2019 Godot Explorer #
9 | # #
10 | # Permission is hereby granted, free of charge, to any person obtaining a copy #
11 | # of this software and associated documentation files (the "Software"), to deal #
12 | # in the Software without restriction, including without limitation the rights #
13 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #
14 | # copies of the Software, and to permit persons to whom the Software is #
15 | # furnished to do so, subject to the following conditions: #
16 | # #
17 | # The above copyright notice and this permission notice shall be included in all #
18 | # copies or substantial portions of the Software. #
19 | # #
20 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #
21 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #
22 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #
23 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #
24 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #
25 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #
26 | # SOFTWARE. #
27 | ##################################################################################
28 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Godot Utilities
2 | Utilities for godot game engine writing in GDScript
3 |
4 | # Quick Start
5 | 1. Copy the folder `gdutils` into `res://addons`
6 | 2. Activate the plugin `Godot Utilities` in your project manager
--------------------------------------------------------------------------------
/__init__.gd:
--------------------------------------------------------------------------------
1 |
2 | tool
3 | extends Node
4 |
5 | const editor = preload("editor/__init__.gd")
6 | const filesystem = preload("filesystem/__init__.gd")
7 | const input = preload("input/__init__.gd")
8 | const resource = preload("resource/__init__.gd")
9 | const scene = preload("scene/__init__.gd")
10 | const utils = preload("utils/__init__.gd")
11 |
--------------------------------------------------------------------------------
/docs/.nojekyll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GodotExplorer/gdutils/62e96df7b71d8a9f7f6cd5c3de5db19916f56627/docs/.nojekyll
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # Quick Start
2 | 1. Copy the folder `gdutils` into `res://addons`
3 | 2. Activate the plugin `gdutils` in your project manager
--------------------------------------------------------------------------------
/docs/_coverpage.md:
--------------------------------------------------------------------------------
1 | # Godot Utilities
2 |
3 | > Utilities for godot game engine writing in GDScript
4 |
5 | [GitHub](https://github.com/GodotExplorer/gdutils)
6 | [Get Started](/?id=quick-start)
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Document
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
19 | Loading
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/editor/__init__.gd:
--------------------------------------------------------------------------------
1 |
2 | tool
3 |
4 | const tools = preload("tools.gd")
5 |
--------------------------------------------------------------------------------
/editor/tools.gd:
--------------------------------------------------------------------------------
1 | tool
2 | const MENUS = {
3 | "Open user://": 0,
4 | }
5 |
6 | func initialize(plugin):
7 | for key in MENUS:
8 | plugin.add_tool_menu_item(key, self, '_on_menu_pressed', MENUS[key])
9 |
10 | func _on_menu_pressed(action):
11 | match action:
12 | 0:
13 | OS.shell_open(str('file://', OS.get_user_data_dir()))
14 |
--------------------------------------------------------------------------------
/filesystem/__init__.gd:
--------------------------------------------------------------------------------
1 |
2 | tool
3 |
4 | const path = preload("path.gd")
5 | const shutil = preload("shutil.gd")
6 |
--------------------------------------------------------------------------------
/filesystem/path.gd:
--------------------------------------------------------------------------------
1 | # This module implements some useful functions on file path.
2 |
3 | tool
4 |
5 | # Return a list containing the names of the files in the directory.
6 | # It does not include the special entries '.' and '..' even if they are present in the directory.
7 | # - - - - - - - - - -
8 | # *Parameters*
9 | # * [path:String] The directory to search with
10 | # - - - - - - - - - -
11 | # *Returns* Array
12 | # * The list containing the names of the files in the directory
13 | # * Return `[]` if failed to search the target directory
14 | static func list_dir(path):
15 | var pathes = []
16 | var dir = Directory.new()
17 | if dir.open(path) == OK:
18 | dir.list_dir_begin()
19 | var cwd = dir.get_next()
20 | while not cwd.empty():
21 | if not cwd in ['.', '..']:
22 | pathes.append(cwd)
23 | cwd = dir.get_next()
24 | dir.list_dir_end()
25 | return pathes
26 |
27 | # Get file pathes in a list under target folder
28 | # - - - - - - - - - -
29 | # *Parameters*
30 | # * [path:String] The folder to search from
31 | # * [with_dirs:bool = false] Includes directories
32 | # * [recurse:String = false] Search sub-folders recursely
33 | # - - - - - - - - - -
34 | # *Returns* Array
35 | # * File pathes in an array
36 | static func list_files(path, with_dirs=false,recurse=false):
37 | var files = []
38 | var dir = Directory.new()
39 | if dir.open(path) == OK:
40 | dir.list_dir_begin()
41 | var file_name = dir.get_next()
42 | while not file_name.empty():
43 | if dir.current_is_dir() and not (file_name in [".", "..", "./"]):
44 | if recurse:
45 | var childfiles = list_files(str(path, "/", file_name), with_dirs, recurse)
46 | for f in childfiles:
47 | files.append(f)
48 | if not (file_name in [".", ".."]):
49 | var rpath = path
50 | if rpath.ends_with("/"):
51 | pass
52 | elif rpath == ".":
53 | rpath = ""
54 | else:
55 | rpath += "/"
56 | if not with_dirs and dir.current_is_dir():
57 | pass
58 | else:
59 | rpath = str(rpath, file_name).replace("/./", "/")
60 | files.append(rpath)
61 | file_name = dir.get_next()
62 | return files
63 |
64 | # Join two or more path components intelligently.
65 | # The return value is the concatenation of path and any members of rest parameters
66 | # with exactly one directory separator `/`
67 | # *Parameters*
68 | # * [`p0~p9`:String] The paths
69 | # - - - - - - - - - -
70 | # *Returns* String
71 | # * The returen path is `normalized`
72 | static func join(p0, p1, p2='', p3='', p4='', p5='', p6='', p7='', p8='', p9=''):
73 | var args = [p0, p1, p2, p3, p4, p5, p6, p7, p8, p9]
74 | var path = ""
75 | var index = -1
76 | for p in args:
77 | index += 1
78 | p = normalize(p)
79 | if p.empty():
80 | continue
81 | var sep = '/'
82 | if path.ends_with('/') or p.begins_with('/') or index <= 0:
83 | sep = ''
84 | path += str(sep, p)
85 | return path
86 |
87 | # Check is the path is under target directory
88 | # This just simple check with file path strings so it won't check the file or
89 | # directory in your file system
90 | # - - - - - - - - - -
91 | # *Parameters*
92 | # * [path:String] The file path
93 | # * [dir:String] The parent directory path
94 | # - - - - - - - - - -
95 | # *Returns* bool
96 | # * Is the file with `path` is under the directory ` dir`
97 | static func path_under_dir(path, dir):
98 | var d = normalize(dir)
99 | if not d.ends_with('/'):
100 | d += '/'
101 | var p = normalize(path)
102 | return p.begins_with(d) and p != d
103 |
104 | # Get sub-path from the parent directory
105 | # - - - - - - - - - -
106 | # *Parameters*
107 | # * [path:String] The file path
108 | # * [dir:String] The parent directory path
109 | # - - - - - - - - - -
110 | # *Returns* String
111 | # * The sub-path string
112 | static func relative_to_parent_dir(path, dir):
113 | var p = path
114 | if path_under_dir(p, dir):
115 | p = normalize(p)
116 | var d = normalize(dir) + '/'
117 | p = p.substr(d.length(), p.length())
118 | return p
119 |
120 | # Normalize the file path this file replace all `\` and `//` to `/ `
121 | # - - - - - - - - - -
122 | # *Parameters*
123 | # * [p_path:String] The file path to normalize to
124 | # - - - - - - - - - -
125 | # *Returns* String
126 | # * The formated string
127 | static func normalize(p_path):
128 | var replacement = {
129 | '\\': '/',
130 | '//': '/'
131 | }
132 | var path = p_path
133 | for key in replacement:
134 | path = path.replace(key, replacement[key])
135 | return path
136 |
--------------------------------------------------------------------------------
/filesystem/shutil.gd:
--------------------------------------------------------------------------------
1 | # The shutil module offers a number of high-level operations on files and collections of files.
2 | # In particular, functions are provided which support file copying and removal.
3 |
4 | tool
5 |
6 | # Copy the file `src` to the file `dst`.
7 | # Permission bits are ignored. `src` and `dst` are path names given as strings.
8 | # - - -
9 | # **Parameters**
10 | # * src: String The source file to copy from
11 | # * dst: String The destination file to copy
12 | # * buff_size: int The buffer size allocated while coping files
13 | # - - -
14 | # **Return**
15 | # * Error the OK or error code
16 | static func copy(src, dst, buff_size=64*1024):
17 | var fsrc = File.new()
18 | var err = fsrc.open(src, File.READ)
19 | if OK == err:
20 | var dir = Directory.new()
21 | if not dir.dir_exists(dst.get_base_dir()):
22 | err = dir.make_dir_recursive(dst.get_base_dir())
23 | if OK == err:
24 | var fdst = File.new()
25 | err = fdst.open(dst, File.WRITE)
26 | if OK == err:
27 | var page = 0
28 | while fsrc.get_position() < fsrc.get_len():
29 | var sizeleft = fsrc.get_len() - fsrc.get_position()
30 | var lenght = buff_size if sizeleft > buff_size else sizeleft
31 | var buff = fsrc.get_buffer(lenght)
32 | fdst.store_buffer(buff)
33 | page += 1
34 | fdst.close()
35 | fsrc.close()
36 | return err
37 |
--------------------------------------------------------------------------------
/input/GestureDetector.gd:
--------------------------------------------------------------------------------
1 | # This Node is used to detect gestures
2 |
3 | tool
4 | extends Node
5 |
6 | enum SlideDetectMethod { DISTANCE, SPEED }
7 | export(SlideDetectMethod) var silde_detect_method = SlideDetectMethod.SPEED
8 | export var slide_distance = 100
9 | export var slide_speed = 600
10 |
11 | enum SlideGesture {
12 | SLIDE_NONE,
13 | SLIDE_UP,
14 | SLIDE_DOWN,
15 | SLIDE_LEFT,
16 | SLIDE_RIGHT
17 | }
18 |
19 | signal slide(dir)
20 |
21 | var _touch_down = false
22 | var _down_pos = Vector2()
23 | var _accept_slide = false
24 |
25 | func _input(event: InputEvent):
26 | if event is InputEventScreenTouch:
27 | _touch_down = event.pressed
28 | if _touch_down:
29 | _down_pos = event.position
30 | _accept_slide = true
31 | else:
32 | _accept_slide = false
33 | if _touch_down and event is InputEventMouseMotion:
34 | if _accept_slide:
35 | var dir = SlideGesture.SLIDE_NONE
36 | # detect slide by speed
37 | if silde_detect_method == SlideDetectMethod.SPEED:
38 | var speed = event.speed
39 | var speed_abs = speed.abs()
40 | var max_side_speed = max(speed_abs.x, speed_abs.y);
41 | if max_side_speed >= slide_speed:
42 | var side_speed = speed.x
43 | if speed_abs.y == max_side_speed:
44 | side_speed = speed.y
45 | dir = SlideGesture.SLIDE_DOWN if side_speed > 0 else SlideGesture.SLIDE_UP
46 | else:
47 | dir = SlideGesture.SLIDE_RIGHT if side_speed > 0 else SlideGesture.SLIDE_LEFT
48 | # detect slide by move distance
49 | elif silde_detect_method == SlideDetectMethod.DISTANCE:
50 | var delta = event.position - _down_pos
51 | var delta_abs = delta.abs()
52 | var max_side = max(delta_abs.x, delta_abs.y)
53 | if max_side > slide_distance:
54 | if max_side == delta_abs.x:
55 | dir = SlideGesture.SLIDE_RIGHT if delta.x > 0 else SlideGesture.SLIDE_LEFT
56 | else:
57 | dir = SlideGesture.SLIDE_DOWN if delta.y > 0 else SlideGesture.SLIDE_UP
58 | if dir != SlideGesture.SLIDE_NONE:
59 | self._accept_slide = false;
60 | self.emit_signal("slide", dir);
61 |
--------------------------------------------------------------------------------
/input/__init__.gd:
--------------------------------------------------------------------------------
1 |
2 | tool
3 |
4 | const GestureDetector = preload("GestureDetector.gd")
5 |
--------------------------------------------------------------------------------
/plugin.cfg:
--------------------------------------------------------------------------------
1 | [plugin]
2 |
3 | name="gdutils"
4 | description="Adds a new Heart node in 2D"
5 | author="Geequlim"
6 | version="0.0.1"
7 | script="plugin.gd"
--------------------------------------------------------------------------------
/plugin.gd:
--------------------------------------------------------------------------------
1 | ##################################################################################
2 | # Tool generated DO NOT modify #
3 | ##################################################################################
4 | # This file is part of #
5 | # GodotExplorer #
6 | # https://github.com/GodotExplorer #
7 | ##################################################################################
8 | # Copyright (c) 2017-2018 Godot Explorer #
9 | # #
10 | # Permission is hereby granted, free of charge, to any person obtaining a copy #
11 | # of this software and associated documentation files (the "Software"), to deal #
12 | # in the Software without restriction, including without limitation the rights #
13 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #
14 | # copies of the Software, and to permit persons to whom the Software is #
15 | # furnished to do so, subject to the following conditions: #
16 | # #
17 | # The above copyright notice and this permission notice shall be included in all #
18 | # copies or substantial portions of the Software. #
19 | # #
20 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #
21 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #
22 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #
23 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #
24 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #
25 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #
26 | # SOFTWARE. #
27 | ##################################################################################
28 |
29 | tool
30 | extends EditorPlugin
31 | const gdutils = preload("__init__.gd")
32 | var tools = preload("editor/tools.gd").new()
33 |
34 | func _enter_tree():
35 | add_custom_type("AsyncHttpTextureRect", "TextureRect", gdutils.scene.gui.AsyncHttpTextutreRect, null)
36 | add_custom_type("ItemListEnhanced", "ItemList", gdutils.scene.gui.ItemListEnhanced, null)
37 | add_custom_type("InfinityList", "ScrollContainer", gdutils.scene.gui.InfinityList, null)
38 | add_custom_type("GridBackground", "TextureRect", gdutils.scene.gui.GridBackground, null)
39 |
40 | func _ready():
41 | tools.initialize(self)
42 |
43 | func _exit_tree():
44 | remove_custom_type("AsyncHttpTextureRect")
45 | remove_custom_type("AutoLayoutControl")
46 | remove_custom_type("ItemListEnhanced")
47 | remove_custom_type("InfinityList")
48 | remove_custom_type("GridBackground")
49 |
--------------------------------------------------------------------------------
/resource/GridBackgroundTexture.gd:
--------------------------------------------------------------------------------
1 |
2 | tool
3 | extends ImageTexture
4 |
5 | export var color1 = Color("#666666") setget _set_color1
6 | export var color2 = Color("#999999") setget _set_color2
7 |
8 | export(int) var cell_size = 10 setget _set_cell_size
9 |
10 | func _init():
11 | _update_image()
12 |
13 | func _set_cell_size(p_size):
14 | cell_size = p_size
15 | _update_image()
16 |
17 | func _set_color1(p_color):
18 | color1 = p_color
19 | _update_image()
20 |
21 | func _set_color2(p_color):
22 | color2 = p_color
23 | _update_image()
24 |
25 | func _update_image():
26 | var image = Image.new()
27 | image.create(cell_size * 2, cell_size * 2, false, Image.FORMAT_RGBA8)
28 | image.fill(color2)
29 | image.lock()
30 | for x in range(cell_size):
31 | for y in range(cell_size):
32 | image.set_pixel(x, y, color1)
33 | for x in range(cell_size, cell_size * 2):
34 | for y in range(cell_size, cell_size * 2):
35 | image.set_pixel(x, y, color1)
36 | image.unlock()
37 | create_from_image(image)
38 |
--------------------------------------------------------------------------------
/resource/__init__.gd:
--------------------------------------------------------------------------------
1 |
2 | tool
3 |
4 | const GridBackgroundTexture = preload("GridBackgroundTexture.gd")
5 | const image = preload("image.gd")
6 |
7 | # Load text file content as string
8 | # - - -
9 | # **parameter**
10 | # * [path: String] file path to load
11 | # - - -
12 | # **Return**
13 | # * String file content
14 | static func load_text_content(path):
15 | var file = File.new()
16 | if OK == file.open(path, File.READ):
17 | return file.get_as_text()
18 | return ""
19 |
20 | # Save text into file
21 | # - - -
22 | # **parameter**
23 | # * [text: String] The text content to save
24 | # * [path: String] The file path to save with
25 | # - - -
26 | # **Return**
27 | # * in : OK | error code
28 | static func save_text_content(text, path):
29 | var file = File.new()
30 | var err = file.open(path, File.WRITE)
31 | if OK == err:
32 | file.store_string(text)
33 | return err
34 |
35 |
--------------------------------------------------------------------------------
/resource/image.gd:
--------------------------------------------------------------------------------
1 | # some useful functions for image files
2 |
3 | tool
4 |
5 | # Load image texture from external path
6 | # - - -
7 | # **Parameters**
8 | # * [path: String] The absolute image path
9 | # - - -
10 | # **Returns**
11 | # * ImageTexture | null
12 | static func load_image_texture(path):
13 | var img = Image.new()
14 | if OK == img.load(path):
15 | var tex = ImageTexture.new()
16 | tex.create_from_image(img)
17 | return tex
18 | return null
19 |
20 | # Save image into png file
21 | # - - -
22 | # **Parameters**
23 | # * [texture: Image|ImageTexture] The image or texture to save
24 | # * [path: String] The file path to save with
25 | # - - -
26 | # **Returns**
27 | # * [int] OK | error code
28 | static func save_image_texture(texture, path):
29 | if texture == null or typeof(texture) != TYPE_OBJECT or typeof(path) != TYPE_STRING:
30 | return ERR_INVALID_PARAMETER
31 | var img = null
32 | if texture is Image:
33 | img = texture
34 | elif texture is ImageTexture:
35 | img = texture.get_data()
36 | if img != null:
37 | return img.save_png(path)
38 | return ERR_INVALID_DATA
39 |
--------------------------------------------------------------------------------
/resource/resource.gd:
--------------------------------------------------------------------------------
1 | # Load text file content as string
2 | # - - -
3 | # **parameter**
4 | # * [path: String] file path to load
5 | # - - -
6 | # **Return**
7 | # * String file content
8 | static func load_text_content(path):
9 | var file = File.new()
10 | if OK == file.open(path, File.READ):
11 | return file.get_as_text()
12 | return ""
13 |
14 | # Save text into file
15 | # - - -
16 | # **parameter**
17 | # * [text: String] The text content to save
18 | # * [path: String] The file path to save with
19 | # - - -
20 | # **Return**
21 | # * in : OK | error code
22 | static func save_text_content(text, path):
23 | var file = File.new()
24 | var err = file.open(path, File.WRITE)
25 | if OK == err:
26 | file.store_string(text)
27 | return err
28 |
--------------------------------------------------------------------------------
/scene/__init__.gd:
--------------------------------------------------------------------------------
1 |
2 | tool
3 |
4 | const gui = preload("gui/__init__.gd")
5 |
--------------------------------------------------------------------------------
/scene/gui/AsyncHttpTextutreRect.gd:
--------------------------------------------------------------------------------
1 | # The control to display remote picture via http request
2 |
3 | extends "AsyncTextureRect.gd"
4 | var http = preload("../../utils/http.gd").new()
5 |
6 | func _init().():
7 | http.name = "http"
8 | http.connect("request_completed", self, '_on_http_request_done')
9 | http.connect("request_failed", self, '_on_http_request_failed')
10 | http.connect("request_progress", self, '_on_http_request_progress')
11 | add_child(http)
12 |
13 | func _async_load_url():
14 | if OK == http.request(url):
15 | self.state = State.LOADING
16 | else:
17 | self.state = State.FAILED
18 |
19 | func _on_http_request_done(p_url, headers, body):
20 | if p_url == self.url:
21 | var img = http.resolve_respose_body(headers, body)
22 | if typeof(img) == TYPE_OBJECT and img is Image:
23 | var tex = ImageTexture.new()
24 | tex.create_from_image(img)
25 | self.texture = tex
26 | self.state = State.SUCCESS
27 | else:
28 | self.state = State.FAILED
29 |
30 | func _on_http_request_failed(p_url, result, response_code):
31 | if url == p_url:
32 | self.state = State.FAILED
33 |
34 | func _on_http_request_progress(p_url, value, max_size):
35 | if p_url == self.url and progress_node != null:
36 | if progress_node.has_method('set_max'):
37 | progress_node.set_max(max_size)
38 | if progress_node.has_method('set_value'):
39 | progress_node.set_value(value)
40 |
--------------------------------------------------------------------------------
/scene/gui/AsyncTextureRect.gd:
--------------------------------------------------------------------------------
1 | # The control to load texture in background
2 |
3 | tool
4 | extends TextureRect
5 |
6 | enum State {
7 | IDLE,
8 | LOADING,
9 | FAILED,
10 | SUCCESS
11 | }
12 |
13 |
14 | var container = CenterContainer.new()
15 | var state = State.IDLE setget _set_state
16 | export var url = "" setget _set_url
17 | export(PackedScene) var progress_template = null
18 | var progress_node = null
19 | export(PackedScene) var failed_template = null
20 | var failed_node = null
21 | var _pending_load = false
22 |
23 | func _init():
24 | container.name = "PresetControls"
25 | container.set_anchors_and_margins_preset(Control.PRESET_WIDE)
26 | container.mouse_filter = Control.MOUSE_FILTER_IGNORE
27 | add_child(container)
28 |
29 | func _set_url(value):
30 | if url == value and (state in [State.LOADING, State.SUCCESS]):
31 | return
32 | url = value
33 | self.texture = null
34 | if not value.empty():
35 | _pending_load = true
36 | set_process(true)
37 |
38 | # *virtual* Do async load action here don't forget change state
39 | func _async_load_url():
40 | pass
41 |
42 | func _set_state(p_state):
43 | state = p_state
44 | if progress_node: progress_node.hide()
45 | if failed_node: failed_node.hide()
46 | match state:
47 | State.LOADING:
48 | if progress_node: progress_node.show()
49 | State.FAILED:
50 | if failed_node: failed_node.show()
51 |
52 | func _ready():
53 | if progress_template != null:
54 | var node = progress_template.instance()
55 | if node != null:
56 | progress_node = node
57 | progress_node.name = "progress"
58 | container.add_child(progress_node)
59 | if progress_node.has_method('set_min'):
60 | progress_node.set_min(0)
61 | if failed_template != null:
62 | var node = failed_template.instance()
63 | if node != null:
64 | failed_node = node
65 | failed_node.name = "failed"
66 | container.add_child(failed_node)
67 | # initialize properties
68 | self.state = State.IDLE
69 | self.url = url
70 |
71 | func _process(delta):
72 | if _pending_load:
73 | _pending_load = false
74 | set_process(false)
75 | # Don't load the image in the editor as it will save the data to the scene
76 | if Engine.editor_hint:
77 | printt("AsyncTextureRect: ignored loading url ", url)
78 | return
79 | # run async load action
80 | _async_load_url()
81 |
--------------------------------------------------------------------------------
/scene/gui/GridBackground.gd:
--------------------------------------------------------------------------------
1 | tool
2 | extends TextureRect
3 | export var color1 = Color("#666666") setget _set_color1
4 | export var color2 = Color("#999999") setget _set_color2
5 | export(int) var cell_size = 10 setget _set_cell_size
6 |
7 | var grid_texture = preload("../../resource/GridBackgroundTexture.gd").new()
8 |
9 | func _init():
10 | self.texture = grid_texture
11 | self.expand = true
12 | self.stretch_mode = TextureRect.STRETCH_TILE
13 |
14 | func _set_color1(p_color):
15 | color1 = p_color
16 | grid_texture.color1 = p_color
17 |
18 | func _set_color2(p_color):
19 | color2 = p_color
20 | grid_texture.color2 = p_color
21 |
22 | func _set_cell_size(p_size):
23 | cell_size = p_size
24 | var cell_size_dpi_related = int(p_size * OS.get_screen_dpi() / 72.0)
25 | grid_texture.cell_size = cell_size_dpi_related
26 |
27 |
28 |
--------------------------------------------------------------------------------
/scene/gui/InfinityList.gd:
--------------------------------------------------------------------------------
1 | tool
2 | extends ScrollContainer
3 |
4 | signal end_reached()
5 | signal begin_readched()
6 |
7 | export(PackedScene) var item_template
8 | export(PackedScene) var header_template
9 | export(PackedScene) var footer_template
10 | export(int, 0, 1000) var space = 0
11 | export(int, 1, 60) var CACHE_SIZE = 1
12 | export var expand_size_to_parent = false
13 |
14 | export(int, "Horizontal", "Vertical") var direction = VERTICAL setget _set_direction
15 | var data_source = [] setget _set_data_source
16 |
17 | var _container = Control.new()
18 | var _item_size = Vector2()
19 | var _item_node_cache = []
20 | var _queue_updating = true
21 | var _queue_updating_layout = true
22 | var _last_frame_scroll = 0
23 | var _footer = null
24 | var _header = null
25 | var _header_size = Vector2()
26 | var _footer_size = Vector2()
27 | var _last_top_index = -1
28 |
29 | # Queue update the visiable items of the list
30 | # If `item_count_changed` is `true` the content size will be updated
31 | func queue_update(item_count_changed = false):
32 | _queue_updating = true
33 | if item_count_changed:
34 | self.data_source = data_source
35 |
36 | # Force update the list
37 | func queue_update_layout():
38 | _queue_updating_layout = true
39 |
40 | # Show the item
41 | func move_to_item(data):
42 | var index = data_source.find(data)
43 | var pos = _get_node_pos_by_index(index)
44 | if pos.x > 0:
45 | self.scroll_horizontal = pos.x
46 | if pos.y > 0:
47 | self.scroll_vertical = pos.y
48 |
49 | # Move to begin position of the list
50 | func move_to_begin():
51 | self.scroll_vertical = 0
52 | self.scroll_horizontal = 0
53 |
54 | # Get footer node instance
55 | func get_footer():
56 | return _footer
57 |
58 | # Get header node instance
59 | func get_header():
60 | return _header
61 |
62 | var _size_calculate = null# Item to calculate the item size
63 |
64 | func _init():
65 | connect("resized", self, "queue_update_layout")
66 | _container.mouse_filter = Control.MOUSE_FILTER_IGNORE
67 | add_child(_container)
68 | self._queue_updating = true
69 |
70 | func _enter_tree():
71 | # header
72 | if _header == null:
73 | if typeof(header_template) == TYPE_OBJECT and header_template is PackedScene:
74 | _header = header_template.instance()
75 | _container.add_child(_header)
76 | # footer
77 | if _footer == null:
78 | if typeof(footer_template) == TYPE_OBJECT and footer_template is PackedScene:
79 | _footer = footer_template.instance()
80 | _container.add_child(_footer)
81 | # item for item size calculate
82 | if _size_calculate == null and typeof(item_template) == TYPE_OBJECT and item_template is PackedScene:
83 | _size_calculate = item_template.instance()
84 | queue_update_layout()
85 | # queue_update()
86 |
87 | func _exit_tree():
88 | if _size_calculate != null:
89 | _size_calculate.free()
90 | _size_calculate = null
91 |
92 | func _set_data_source(ds):
93 | data_source = ds
94 | _queue_updating_layout = true
95 |
96 | func _set_direction(dir):
97 | direction = dir
98 | queue_update_layout()
99 |
100 | var override_size = Vector2()
101 | func _process(delta):
102 | if override_size == Vector2():
103 | override_size = self.rect_size
104 | if expand_size_to_parent and get_parent() is Control:
105 | override_size = get_parent().rect_size
106 | if self.rect_size != override_size:
107 | _queue_updating_layout = true
108 | # update layout
109 | if _queue_updating_layout:
110 | _update_layout()
111 | _queue_updating = true
112 | _queue_updating_layout = false
113 | # update items
114 | var scroll = self.scroll_horizontal if direction == HORIZONTAL else self.scroll_vertical
115 | if not _queue_updating:
116 | _queue_updating = scroll != _last_frame_scroll
117 | _last_frame_scroll = scroll
118 | if _queue_updating:
119 | _update_items(scroll - _last_frame_scroll)
120 | _queue_updating = false
121 | # footer & header size
122 | if _footer != null and _footer.rect_size != _footer_size:
123 | _footer.rect_size = _footer_size
124 | if _header != null and _header.rect_size != _header_size:
125 | _header.rect_size = _header_size
126 | # item size
127 | for item in _item_node_cache:
128 | if item.is_inside_tree() and item.rect_size != _item_size:
129 | item.rect_size = _item_size
130 |
131 | func _update_items(scroll):
132 | var render_count = _item_node_cache.size()
133 | var top_index = _get_top_line_index()
134 | # replace front and back elements if top item changed
135 | var moved_step = top_index - _last_top_index
136 | _last_top_index = top_index
137 | if moved_step > 0:
138 | for i in range(moved_step):
139 | var front = _item_node_cache[0]
140 | _item_node_cache.pop_front()
141 | _item_node_cache.push_back(front)
142 | elif moved_step < 0:
143 | for i in range(abs(moved_step)):
144 | var back = _item_node_cache[-1]
145 | _item_node_cache.pop_back()
146 | _item_node_cache.push_front(back)
147 | # update item data if necessury
148 | for i in range(render_count):
149 | var index = top_index + i
150 | var node = _item_node_cache[i]
151 | node.rect_position = _get_node_pos_by_index(index)
152 | if index < data_source.size() and index >= 0:
153 | var data = data_source[index]
154 | if node.data != data:
155 | node.data = data
156 | if node.rect_size != _item_size:
157 | node.rect_size = _item_size
158 | node.show()
159 | else:
160 | node.hide()
161 | if _footer:
162 | if direction == VERTICAL:
163 | _footer.rect_position = Vector2(0, _container.rect_min_size.y - _footer_size.y)
164 | elif direction == HORIZONTAL:
165 | _footer.rect_position = Vector2(_container.rect_min_size.x - _footer_size.x, 0)
166 | # emit end/begin reached signal
167 | if moved_step != 0:
168 | if top_index + get_page_size() + moved_step > data_source.size():
169 | emit_signal("end_reached")
170 | elif top_index + moved_step < 0:
171 | emit_signal("begin_readched")
172 |
173 |
174 | func _update_layout():
175 | _update_size()
176 | _alloc_cache_nodes()
177 |
178 | func _update_size():
179 | # update item size and footer/header size
180 | if true:
181 | var size = Vector2()
182 | var target_size = override_size
183 | if _size_calculate != null:
184 | var node = _size_calculate
185 | size = node.rect_min_size
186 | if node.size_flags_horizontal & Control.SIZE_EXPAND:
187 | size.x = max(target_size.x, size.x)
188 | if node.size_flags_vertical & Control.SIZE_EXPAND:
189 | size.y = max(target_size.y, size.y)
190 | # printt(node.size_flags_horizontal, node.size_flags_vertical, node.rect_min_size, target_size, size)
191 | if self._header:
192 | _header_size = _header.rect_min_size
193 | if _header.size_flags_horizontal & Control.SIZE_EXPAND:
194 | _header_size.x = max(target_size.x, _header_size.x)
195 | if _header.size_flags_vertical & Control.SIZE_EXPAND:
196 | _header_size.y = max(target_size.y, _header_size.y)
197 | if _header.rect_size != _header_size:
198 | _header.rect_size = _header_size
199 | if self._footer:
200 | _footer_size = _footer.rect_min_size
201 | if _footer.size_flags_horizontal & Control.SIZE_EXPAND:
202 | _footer_size.x = max(target_size.x, _footer_size.x)
203 | if _footer.size_flags_vertical & Control.SIZE_EXPAND:
204 | _footer_size.y = max(target_size.y, _footer_size.y)
205 | self._item_size = size
206 | # update container size
207 | if typeof(data_source) in [TYPE_ARRAY]:
208 | var min_size = Vector2()
209 | var space_size = (data_source.size() - 1) * space
210 | if space_size < 0:
211 | space_size = 0
212 | if direction == VERTICAL:
213 | min_size = Vector2(_item_size.x, _item_size.y * data_source.size() + space_size)
214 | if _header: min_size.y += _header_size.y
215 | if _footer: min_size.y += _footer_size.y
216 | min_size.x -= get_v_scrollbar().rect_size.x
217 | elif direction == HORIZONTAL:
218 | min_size = Vector2(_item_size.x * data_source.size() + space_size, _item_size.y)
219 | if _header: min_size.x += _header_size.x
220 | if _footer: min_size.x += _footer_size.x
221 | min_size.y -= get_h_scrollbar().rect_size.y
222 | _container.rect_min_size = min_size
223 | # update rect_size
224 | self.rect_size = override_size
225 |
226 | func _alloc_cache_nodes():
227 | var cache_size = 0
228 | if direction == VERTICAL:
229 | cache_size = round(override_size.y / (_item_size.y + space)) + CACHE_SIZE
230 | elif direction == HORIZONTAL:
231 | cache_size = round(override_size.x / (_item_size.x + space)) + CACHE_SIZE
232 | var cur_cache_size = _item_node_cache.size()
233 | if cur_cache_size < cache_size:
234 | _item_node_cache.resize(cache_size)
235 | for i in range(cache_size):
236 | if _item_node_cache[i] == null:
237 | var node = item_template.instance()
238 | _item_node_cache[i] = node
239 | _container.add_child(node)
240 | node.rect_size = _item_size
241 |
242 | func _clear():
243 | for node in _item_node_cache:
244 | node.queue_free()
245 | _item_node_cache.clear()
246 |
247 | func _get_top_line_index():
248 | var index = -1
249 | if data_source.size():
250 | if direction == VERTICAL:
251 | index = floor((self.scroll_vertical - _header_size.y) / (_item_size.y + space))
252 | elif direction == HORIZONTAL:
253 | index = floor((self.scroll_horizontal - _header_size.x) / (_item_size.x + space))
254 | if index >= data_source.size():
255 | index = -1
256 | return index
257 |
258 | func _get_node_pos_by_index(index):
259 | var pos = Vector2()
260 | if index >= 0 and index < data_source.size():
261 | if direction == VERTICAL:
262 | pos.x = 0
263 | pos.y = (_item_size.y + space) * index - space
264 | if _header:
265 | pos.y += _header_size.y
266 | elif direction == HORIZONTAL:
267 | pos.y = 0
268 | pos.x = (_item_size.x + space) * index - space
269 | if _header:
270 | pos.x += _header_size.x
271 | return pos
272 |
273 | func get_page_size():
274 | if direction == VERTICAL:
275 | return ceil(override_size.y / (_item_size.y + space))
276 | elif direction == HORIZONTAL:
277 | return ceil(override_size.x / (_item_size.x + space))
278 |
--------------------------------------------------------------------------------
/scene/gui/ItemListEnhanced.gd:
--------------------------------------------------------------------------------
1 | tool
2 | extends ItemList
3 |
4 | signal double_clicked()
5 | signal mouse_right_clicked()
6 | signal selection_changed(selected_items)
7 |
8 | const ACCEPT_DS_TYPES = [
9 | TYPE_ARRAY,
10 | TYPE_INT_ARRAY,
11 | TYPE_REAL_ARRAY,
12 | TYPE_COLOR_ARRAY,
13 | TYPE_STRING_ARRAY,
14 | TYPE_VECTOR2_ARRAY,
15 | TYPE_VECTOR3_ARRAY
16 | ]
17 |
18 | func _init():
19 | connect("item_selected", self, "__on_select_changed")
20 | connect("multi_selected", self, "__on_select_changed")
21 | connect("nothing_selected", self, "__on_select_changed")
22 |
23 | # ListItemProvider
24 | # The provider of the item list it decides how the data is shown in the list
25 | var provider = ListItemProvider.new() setget _set_provider
26 | func _set_provider(p):
27 | if typeof(p) == TYPE_OBJECT:# and p is ListItemProvider:# FIXME: BUG of GDScript?
28 | provider = p
29 | update_list()
30 |
31 | var menu_handler = MenuActionHandler.new() setget _set_menu_handler
32 | var _popupmenu = null
33 | func _set_menu_handler(h):
34 | menu_handler = h
35 | if _popupmenu != null:
36 | _popupmenu.queue_free()
37 | remove_child(_popupmenu)
38 | _popupmenu = null
39 | if typeof(h) == TYPE_OBJECT:# and h is MenuActionHandler:# FIXME: BUG of GDScript?
40 | _popupmenu = h.create_popupmenu()
41 | if _popupmenu != null:
42 | _popupmenu.connect("id_pressed", self, "_on_menu_id_pressed")
43 | add_child(_popupmenu)
44 |
45 | # Arrary
46 | # The item data source of the list
47 | var data_source = [] setget _set_data_source
48 | func _set_data_source(arr):
49 | data_source = arr
50 | update_list()
51 |
52 | # Variant
53 | # The filter to decide is item in the `data_source` should be shown in the list
54 | var filter = null setget _set_filter
55 | func _set_filter(f):
56 | filter = f
57 | update_list()
58 |
59 |
60 | # Emited while wating action trigged with the list view
61 | signal on_action_pressed(action)
62 |
63 | # Event gui actions to watch with
64 | # Type: Array actions to watch
65 | var watching_actions = []
66 |
67 | # Force update the item list
68 | # This action will clear and re-order the items
69 | func update_list():
70 | clear()
71 | if typeof(data_source) in ACCEPT_DS_TYPES:
72 | var data_for_show = data_source
73 | if provider.sort_required(data_source):
74 | data_for_show = []
75 | for data in data_source:
76 | data_for_show.append(data)
77 | data_for_show.sort_custom(provider, 'sort')
78 |
79 | var index = 0
80 | for data in data_for_show:
81 | if not provider.item_fit_filter(data, filter):
82 | continue
83 | var text = provider.get_item_text(data)
84 | if typeof(text) == TYPE_STRING:
85 | add_item(text)
86 | set_item_metadata(index, data)
87 | set_item_icon(index, provider.get_item_icon(data))
88 | var color = provider.get_item_background_color(data)
89 | if typeof(color) == TYPE_COLOR:
90 | set_item_custom_bg_color(index, color)
91 | index += 1
92 |
93 | # Update target item of in the list
94 | func update_item(p_item):
95 | for id in range(get_item_count()):
96 | if p_item == get_item_metadata(id):
97 | set_item_text(id, provider.get_item_text(p_item))
98 | set_item_custom_bg_color(id, provider.get_item_background_color(p_item))
99 | set_item_icon(id, provider.get_item_icon(p_item))
100 | break
101 |
102 | # Select items, the items selected before will be unselected
103 | # items: Array items to select
104 | func selecte_items(p_items):
105 | if not typeof(p_items) in ACCEPT_DS_TYPES:
106 | p_items = [p_items]
107 | for id in get_selected_items():
108 | unselect(id)
109 | var single = p_items.size() == 1
110 | var selected = false
111 | for item in p_items:
112 | for i in range(get_item_count()):
113 | if get_item_metadata(i) == item:
114 | select(i, single)
115 | selected = true
116 | if single:
117 | break
118 | if single and selected:
119 | break
120 |
121 | # Get selected item data in an array
122 | func get_selected_item_list():
123 | var _items = []
124 | for idx in get_selected_items():
125 | _items.append(get_item_metadata(idx))
126 | return _items
127 |
128 | func _gui_input(event):
129 | # Mouse button actions
130 | if event is InputEventMouseButton:
131 | if event.button_index == BUTTON_RIGHT and not event.pressed:
132 | if _popupmenu != null:
133 | _popupmenu.set_global_position(get_global_mouse_position())
134 | var selectedItems = get_selected_item_list()
135 | for i in range(_popupmenu.get_item_count()):
136 | var id = _popupmenu.get_item_id(i)
137 | _popupmenu.set_item_disabled(i, not menu_handler.item_enabled(id, selectedItems))
138 | _popupmenu.popup()
139 | emit_signal("mouse_right_clicked")
140 | elif event.doubleclick:
141 | emit_signal("double_clicked")
142 | # Watching actions
143 | if event.is_pressed():
144 | for action in watching_actions:
145 | if typeof(action) == TYPE_STRING:
146 | if event.is_action(action):
147 | emit_signal("on_action_pressed", action)
148 | break
149 | elif typeof(action) == TYPE_OBJECT and action is InputEvent:
150 | if event.action_match(action):
151 | emit_signal("on_action_pressed", action)
152 | break
153 | # Popupmenu shortcuts
154 | if event.is_pressed() and _popupmenu != null:
155 | var selectedItems = get_selected_item_list()
156 | for i in range(_popupmenu.get_item_count()):
157 | var id = _popupmenu.get_item_id(i)
158 | var shortcut = _popupmenu.get_item_shortcut(i)
159 | if shortcut != null and shortcut is ShortCut and event.action_match(shortcut.shortcut):
160 | if not _popupmenu.is_item_disabled(i):
161 | menu_handler.id_pressed(id, selectedItems)
162 |
163 | func _on_menu_id_pressed(id):
164 | menu_handler.id_pressed(id, get_selected_item_list())
165 |
166 | func __on_select_changed(useless=null, useless2=null):
167 | emit_signal("selection_changed", get_selected_item_list())
168 |
169 | # The provider class for ItemList
170 | # The provider decide how the data source is shown in the item list
171 | class ListItemProvider:
172 | # If your list need sort items return `true` here then the list wil try to
173 | # sort items with `sort` while update
174 | func sort_required(p_data_source):
175 | return false
176 |
177 | # The sort function to be used for item sorting
178 | #
179 | # -------------
180 | # @parameters
181 | # * p_item1
182 | # * p_item2
183 | # The item data to sort with
184 | #
185 | # -------------
186 | # Return true if `p_item1` needs to be shown before `p_item2`
187 | func sort(p_item1, p_item2):
188 | return true
189 |
190 | # Check if the item should be shown with the given filter from the list
191 | #
192 | # -------------
193 | # @parameters
194 | # * p_item
195 | # * p_filter
196 | #
197 | # -------------
198 | # Return true if `p_item1` needs to be shown before `p_item2`
199 | func item_fit_filter(p_item, p_filter):
200 | return true
201 |
202 | func get_item_text(p_item):
203 | return str(p_item)
204 |
205 | func get_item_icon(p_item):
206 | return null
207 |
208 | func get_item_background_color(p_item):
209 | return Color(0, 0, 0, 0)
210 |
211 | # Right mouse button popup menu handler
212 | class MenuActionHandler:
213 | # Create the popup menu control for this list view
214 | func create_popupmenu():
215 | return null
216 | # Check the item with id is enabled for selected items
217 | func item_enabled(id, selectedItems):
218 | return true
219 | # This method is call when the menu item is pressed or its shortcut is pressed
220 | func id_pressed(id, selectedItems):
221 | pass
222 |
--------------------------------------------------------------------------------
/scene/gui/Menu.gd:
--------------------------------------------------------------------------------
1 | tool
2 | extends Reference
3 |
4 | # Menu type
5 | enum {
6 | NORMAL,
7 | CHECKABLE,
8 | SEPARATOR
9 | }
10 |
11 | var type = NORMAL # Type of the menu item
12 | var icon = null # The icon texture
13 | var title = "" # The title text of the menu item
14 | var disabled = false # The item is disabled or enabled
15 | var checked = false # This item is checked if the type is CHECKABLE
16 | var children = [] # Array