diff --git a/DungeonShooting_Godot/DungeonShooting.csproj b/DungeonShooting_Godot/DungeonShooting.csproj index 4546f15..8e15ac5 100644 --- a/DungeonShooting_Godot/DungeonShooting.csproj +++ b/DungeonShooting_Godot/DungeonShooting.csproj @@ -1,4 +1,4 @@ - + net6.0 true diff --git a/DungeonShooting_Godot/DungeonShooting.csproj.old.5 b/DungeonShooting_Godot/DungeonShooting.csproj.old.5 new file mode 100644 index 0000000..4546f15 --- /dev/null +++ b/DungeonShooting_Godot/DungeonShooting.csproj.old.5 @@ -0,0 +1,11 @@ + + + net6.0 + true + + + + + + + \ No newline at end of file diff --git a/DungeonShooting_Godot/addons/script_comment_menu/data_util.gd b/DungeonShooting_Godot/addons/script_comment_menu/data_util.gd new file mode 100644 index 0000000..768c46b --- /dev/null +++ b/DungeonShooting_Godot/addons/script_comment_menu/data_util.gd @@ -0,0 +1,310 @@ +#============================================================ +# Data Util +#============================================================ +# - datetime: 2022-12-21 21:19:10 +#============================================================ +## 数据工具 +## +##用作全局获取数据使用 +class_name ScriptCommentMenu_DataUtil + + +## 获取场景树 [SceneTree] 对象的 meta 数据作为单例数据,如果返回的数据为 [code]null[/code] 则会在下次继续调用这个 +##default 回调方法,直到返回的数据不为 [code]null[/code] 为止 +##[br] +##[br][code]meta_key[/code] 数据key +##[br][code]default[/code] 如果没有这个key,则默认返回的数据 +##[br][code]ignore_null[/code] 忽略 null 值。如果为 true,则在默认值为 null 的时候不记录到元数据,直到有数据为止 +static func get_meta_data(meta_key: StringName, default: Callable, ignore_null: bool = true): + if Engine.has_meta(meta_key) and Engine.get_meta(meta_key) != null: + return Engine.get_meta(meta_key) + else: + var value = default.call() + if ignore_null: + if value != null: + set_meta_data(meta_key, value) + else: + set_meta_data(meta_key, value) + + return value + + +## 设置数据 +##[br] +##[br][code]meta_key[/code] 数据key +##[br][code]value[/code] 设置的值 +static func set_meta_data(meta_key: StringName, value): + Engine.set_meta(meta_key, value) + + +## 是否有这个 key 的据 +static func has_meta_data(meta_key: StringName) -> bool: + return Engine.has_meta(meta_key) + + +## 移除数据 +static func remove_meta_data(meta_key: StringName) -> bool: + if Engine.has_meta(meta_key): + Engine.remove_meta(meta_key) + return true + return false + + +## 移除所有meta数据 +static func clear_all_meta() -> void: + for key in Engine.get_meta_list(): + Engine.remove_meta(key) + + +## 获取 Dictionary 数据 +static func get_meta_dict_data(meta_key: StringName, default: Dictionary = {}) -> Dictionary: + if Engine.has_meta(meta_key): + return Engine.get_meta(meta_key) + else: + Engine.set_meta(meta_key, default) + return default + + +## 获取 Array 数据 +static func get_meta_array_data(meta_key: StringName, default: Array = []) -> Array: + if Engine.has_meta(meta_key): + return Engine.get_meta(meta_key) + else: + Engine.set_meta(meta_key, default) + return default + + +## 获取目标的默认数据,以目标对象作为基础存储数据 +static func get_object_data(object: Object, key: StringName, default: Callable ): + if object.has_meta(key): + return object.get_meta(key) + else: + var data = default.call() + object.set_meta(key, data) + return data + + +## 获取标 [Dictionary] 类型数据 +static func get_object_dict_data(object: Object, key: StringName, default: Dictionary = {}) -> Dictionary: + return get_object_data(object, key, func(): return default) + + +class _ClassInfo: + var _type : int = TYPE_NIL + var _class_name : StringName = &"" + var _script : Script = null + + func _to_string(): + return str({ + "_type": _type, + "_class_name": _class_name, + "_script": _script, + }) + + +## 获取类的数据 +##[br] +##[br][code]_class[/code] 类型。这个值可以是类名称,也可以是 [int] 类的数据型枚举的值。最大 +## [constant TYPE_MAX],最小 [constant TYPE_NIL] +##[br][code]return[/code] 返回这个类的信息 +static func get_class_info(_class) -> _ClassInfo: + var map = get_meta_dict_data("DataUtil_get_type_cache_data_for_array", {}) + if map.has(_class): + return map[_class] as _ClassInfo + + else: + var type : int = TYPE_NIL + var _class_name : StringName = &"" + var script = null + if _class is Script: + type = TYPE_OBJECT + _class_name = _class.get_instance_base_type() + script = _class + elif _class is int and _class > 0 and _class < TYPE_MAX: + type = _class + _class = ScriptCommentMenu_ScriptUtil.get_type_name(_class) + elif _class is Object: + var _class_type_ = str(_class) + if _class_type_.contains("GDScriptNativeClass"): + var obj = _class.new() + type = typeof(obj) + _class_name = obj.get_class() + else: + type = TYPE_OBJECT + _class_name = "Object" + elif _class is String: + if ScriptCommentMenu_ScriptUtil.is_base_data_type(_class): + type = ScriptCommentMenu_ScriptUtil.get_type_of(_class) + _class = ScriptCommentMenu_ScriptUtil.get_built_in_class(_class) + else: + type = TYPE_OBJECT + + var data = _ClassInfo.new() + data._type = type + data._class_name = _class_name + data._script = script + map[_class] = data + return data + + +## 获取类型化数组 +##[br] +##[br][code]_class[/code] 数据的类型。比如 [code]"Dictionary", Node, Sprite2D[/code] 等类名(基础数据类型需要加双引号), +##或者自定义类名 Player,或者字符串形式的类名,或者 TYPE_INT, TYPE_DICTIONARY +##[br][code]default[/code] 默认有哪些数据 +static func get_type_array(_class, default : Array = []) -> Array: + var data : _ClassInfo = get_class_info(_class) + # 返回类型化数组 + return Array(default, data._type, data._class_name, data._script ) + + +## 转为类型化数组 +static func to_type_array(_class, array: Array) -> Array: + return get_type_array(_class, array) + + +## 数组转为字典 +## +##[codeblock] +##var dict_data = ScriptCommentMenu_DataUtil.array_to_dictionary( +## node_list, +## func(node): return node.name, # key 键 +## func(node): return {} +##) +##[/codeblock] +static func array_to_dictionary( + list: Array, + get_key: Callable = func(item): return item, + get_value: Callable = func(item): return null +) -> Dictionary: + var data = {} + var key + var value + for i in list: + key = get_key.call(i) + value = get_value.call(i) + data[key] = value + return data + + +## 引用数据 +class RefData: + var value + + func get_value(): + return value + + func _init(value) -> void: + self.value = value + + func _to_string(): + return str(value) + + +## 获取引用数据。 +##[br] +##[br][b]Note:[/b] 主要用在匿名函数里,以处理基本数据类型的值。因为匿名函数之外的基本数据类型的值 +##在匿名函数修改不会发生改变。 +static func get_ref_data(default) -> RefData: + return RefData.new(default) + + +## 获取字典的值,如果没有,则获取并设置默认值 +##[br] +##[br][code]dict[/code] 获取的字典 +##[br][code]key[/code] key 键 +##[br][code]not_exists_set[/code] 没有则返回值设置这个值。这个回调方法返回要设置的数据 +static func get_value_or_set(dict: Dictionary, key, not_exists_set: Callable): + if dict.has(key) and not typeof(dict[key]) == TYPE_NIL: + return dict[key] + else: + dict[key] = not_exists_set.call() + return dict[key] + + +## 生成id +static func generate_id(data_list: Array) -> StringName: + var list = [] + for i in data_list: + list.append(hash(i)) + return ",".join(list).sha1_text() + + +## 如果不为空值结果值 +class NotNullValueChain: + + func _init(value): + set_meta("value", value) + + func get_value(default = null): + return get_meta("value", default) + + func or_else(object, else_object: Callable) -> NotNullValueChain: + return NotNullValueChain.new( object if object else else_object.call() ) + + ## 返回结果不为空时,这个方法需要一个参数接收值 + func if_not_null(else_object: Callable, default = null) -> NotNullValueChain: + var value = get_value() + return NotNullValueChain.new( else_object.call(value) if value else default ) + + +## 如果对象不为 null 则调用。 +## 可以链式调用逐步执行功能 +##[codeblock] +##func get_data(object: Object): +## return ScriptCommentMenu_DataUtil.if_not_null(object, func(): +## return object.get_script() +## ).or_else(func(): +## print("") +## ) +##[/codeblock] +static func if_not_null(object, else_object: Callable) -> NotNullValueChain: + return NotNullValueChain.new(( + else_object.call() if object != null else object + )) + + +## 获取正则 +static func get_regex(pattern: String) -> RegEx: + var re = RegEx.new() + re.compile(pattern) + return re + + +## 合并数据 +##[br] +##[br][code]merge_target[/code] 合并到的目标 +##[br][code]data[/code] 要追加合并的数据 +##[br][return]return[/return] 返回合并后的数据 +static func merge(merge_target, data): + if merge_target is Dictionary: + merge_target.merge(data) + return merge_target + elif merge_target is Array or merge_target is String: + merge_target += merge_target + return merge_target + else: + assert(false, "错误的数据类型!只能合并 [Dictionary, Array, String] 中的一种!") + + +## 获取一个唯一的数字 ID,从 0 始 +static func get_id() -> int: + const KEY = "DataUtil_get_id" + if Engine.has_meta(KEY): + var id = Engine.get_meta(KEY) + id += 1 + Engine.set_meta(KEY, id) + return id + else: + var id = 0 + Engine.set_meta(KEY, id) + return id + + +## 列表转为集合hash值,这样即便列表顺序不一致他的值也是相同的 +static func as_set_hash(list: Array) -> int: + var h : int = 0 + for i in list: + h += hash(i) + return h + diff --git a/DungeonShooting_Godot/addons/script_comment_menu/plugin.cfg b/DungeonShooting_Godot/addons/script_comment_menu/plugin.cfg new file mode 100644 index 0000000..fd4f592 --- /dev/null +++ b/DungeonShooting_Godot/addons/script_comment_menu/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="script_comment_menu" +description="" +author="张学徒" +version="1.2" +script="plugin.gd" diff --git a/DungeonShooting_Godot/addons/script_comment_menu/plugin.gd b/DungeonShooting_Godot/addons/script_comment_menu/plugin.gd new file mode 100644 index 0000000..64f8e79 --- /dev/null +++ b/DungeonShooting_Godot/addons/script_comment_menu/plugin.gd @@ -0,0 +1,47 @@ +#============================================================ +# Plugin +#============================================================ +# - datetime: 2022-06-11 11:26:00 +# - datetime: 2022-07-17 14:54:39 +#============================================================ +@tool +extends EditorPlugin + + + +var menu_button : MenuButton +var util_add_menu := ScriptCommentMenuConstant.AddMenu.new() + +var _sub_menus = [ + _ScriptMenu_Comments.new(), + _ScriptMenu_Overrides.new(), +] + + +func _enter_tree(): + # 编辑器启动不超过 5 秒时 + if Time.get_ticks_msec() < 5000: + await Engine.get_main_loop().create_timer(10).timeout + _init_data.call_deferred() + + +func _exit_tree(): + if menu_button: + menu_button.queue_free() + for sub in _sub_menus: + sub._uninstall() + + +func _init_data(): + # 添加菜单按钮 + menu_button = MenuButton.new() + menu_button.text = "代码工具" + menu_button.switch_on_hover = true + menu_button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN + util_add_menu.add_script_editor_menu(menu_button) + + for sub in _sub_menus: + sub.init_menu(menu_button) + + + diff --git a/DungeonShooting_Godot/addons/script_comment_menu/script_util.gd b/DungeonShooting_Godot/addons/script_comment_menu/script_util.gd new file mode 100644 index 0000000..fbbf5a9 --- /dev/null +++ b/DungeonShooting_Godot/addons/script_comment_menu/script_util.gd @@ -0,0 +1,401 @@ +#============================================================ +# Scirpt Util +#============================================================ +# - datetime: 2022-07-17 17:25:00 +#============================================================ +## 处理脚本的工具 +class_name ScriptCommentMenu_ScriptUtil + + +const DATA_TYPE_TO_NAME = { + TYPE_NIL: &"null", + TYPE_BOOL: &"bool", + TYPE_INT: &"int", + TYPE_FLOAT: &"float", + TYPE_STRING: &"String", + TYPE_RECT2: &"Rect2", + TYPE_VECTOR2: &"Vector2", + TYPE_VECTOR2I: &"Vector2i", + TYPE_VECTOR3: &"Vector3", + TYPE_VECTOR3I: &"Vector3i", + TYPE_TRANSFORM2D: &"Transform2D", + TYPE_VECTOR4: &"Vector4", + TYPE_VECTOR4I: &"Vector4i", + TYPE_PLANE: &"Plane", + TYPE_QUATERNION: &"Quaternion", + TYPE_AABB: &"AABB", + TYPE_BASIS: &"Basis", + TYPE_TRANSFORM3D: &"Transform3D", + TYPE_PROJECTION: &"Projection", + TYPE_COLOR: &"Color", + TYPE_STRING_NAME: &"StringName", + TYPE_NODE_PATH: &"NodePath", + TYPE_RID: &"RID", + TYPE_OBJECT: &"Object", + TYPE_CALLABLE: &"Callable", + TYPE_SIGNAL: &"Signal", + TYPE_DICTIONARY: &"Dictionary", + TYPE_ARRAY: &"Array", + TYPE_PACKED_BYTE_ARRAY: &"PackedByteArray", + TYPE_PACKED_INT32_ARRAY: &"PackedInt32Array", + TYPE_PACKED_INT64_ARRAY: &"PackedInt64Array", + TYPE_PACKED_STRING_ARRAY: &"PackedStringArray", + TYPE_PACKED_VECTOR2_ARRAY: &"PackedVector2Array", + TYPE_PACKED_VECTOR3_ARRAY: &"PackedVector3Array", + TYPE_PACKED_FLOAT32_ARRAY: &"PackedFloat32Array", + TYPE_PACKED_FLOAT64_ARRAY: &"PackedFloat64Array", + TYPE_PACKED_COLOR_ARRAY: &"PackedColorArray", +} + +const NAME_TO_DATA_TYPE = { + &"null": TYPE_NIL, + &"bool": TYPE_BOOL, + &"int": TYPE_INT, + &"float": TYPE_FLOAT, + &"String": TYPE_STRING, + &"Rect2": TYPE_RECT2, + &"Vector2": TYPE_VECTOR2, + &"Vector2i": TYPE_VECTOR2I, + &"Vector3": TYPE_VECTOR3, + &"Vector3i": TYPE_VECTOR3I, + &"Transform2D": TYPE_TRANSFORM2D, + &"Vector4": TYPE_VECTOR4, + &"Vector4i": TYPE_VECTOR4I, + &"Plane": TYPE_PLANE, + &"Quaternion": TYPE_QUATERNION, + &"AABB": TYPE_AABB, + &"Basis": TYPE_BASIS, + &"Transform3D": TYPE_TRANSFORM3D, + &"Projection": TYPE_PROJECTION, + &"Color": TYPE_COLOR, + &"StringName": TYPE_STRING_NAME, + &"NodePath": TYPE_NODE_PATH, + &"RID": TYPE_RID, + &"Object": TYPE_OBJECT, + &"Callable": TYPE_CALLABLE, + &"Signal": TYPE_SIGNAL, + &"Dictionary": TYPE_DICTIONARY, + &"Array": TYPE_ARRAY, + &"PackedByteArray": TYPE_PACKED_BYTE_ARRAY, + &"PackedInt32Array": TYPE_PACKED_INT32_ARRAY, + &"PackedInt64Array": TYPE_PACKED_INT64_ARRAY, + &"PackedStringArray": TYPE_PACKED_STRING_ARRAY, + &"PackedVector2Array": TYPE_PACKED_VECTOR2_ARRAY, + &"PackedVector3Array": TYPE_PACKED_VECTOR3_ARRAY, + &"PackedFloat32Array": TYPE_PACKED_FLOAT32_ARRAY, + &"PackedFloat64Array": TYPE_PACKED_FLOAT64_ARRAY, + &"PackedColorArray": TYPE_PACKED_COLOR_ARRAY, +} + + +static func _get_script_data_cache(script: Script) -> Dictionary: + return ScriptCommentMenu_DataUtil.get_meta_dict_data("ScriptCommentMenu_ScriptUtil__get_script_data_cache") + +## 数据类型名称 +##[br][code]type[/code]: 数据类型枚举值 +##[br][code]return[/code]: 返回数据类型的字符串 +static func get_type_name(type: int) -> StringName: + return DATA_TYPE_TO_NAME.get(type) + +## 获取这个类名的类型 +static func get_type_of(_class_name: StringName) -> int: + return NAME_TO_DATA_TYPE.get(_class_name, -1) + +## 是否有这个类型的枚举 +static func has_type(type: int) -> bool: + return DATA_TYPE_TO_NAME.has(type) + +## 是否是基础数据类型 +static func is_base_data_type(_class_name: StringName) -> bool: + return NAME_TO_DATA_TYPE.has(_class_name) + +## 获取属性列表 +##[br] +##[br]返回类似如下格式的数据 +##[codeblock] +##{ +## "name": "RefCounted", +## "class_name": &"", +## "type": 0, +## "hint": 0, +## "hint_string": "", +## "usage": 128 +##} +##[/codeblock] +static func get_property_data_list(script: Script) -> Array[Dictionary]: + if is_instance_valid(script): + return script.get_script_property_list() + return Array([], TYPE_DICTIONARY, "Dictionary", null) + +## 获取方法列表 +static func get_method_data_list(script: Script) -> Array[Dictionary]: + if is_instance_valid(script): + return script.get_script_method_list() + ScriptCommentMenu_DataUtil.get_type_array("int") + return Array([], TYPE_DICTIONARY, "Dictionary", null) + + +## 获取方法的参数列表数据 +static func get_method_arguments_list(script: Script, method_name: StringName) -> Array[Dictionary]: + var data = get_method_data(script, method_name) + if data: + return data.get("args", ScriptCommentMenu_DataUtil.get_type_array("Dictionary")) + return ScriptCommentMenu_DataUtil.get_type_array("Dictionary") + + +## 获取信号列表 +static func get_signal_data_list(script: Script) -> Array[Dictionary]: + if is_instance_valid(script): + return script.get_script_signal_list() + return Array([], TYPE_DICTIONARY, "Dictionary", null) + +## 获取这个属性名称数据 +static func get_property_data(script: Script, property: StringName) -> Dictionary: + var data = _get_script_data_cache(script) + var p_cache_data : Dictionary = ScriptCommentMenu_DataUtil.get_value_or_set(data, "propery_data_cache", func(): + var property_data : Dictionary = {} + for i in script.get_script_property_list(): + property_data[i['name']] = i + return property_data + ) + return p_cache_data.get(property, {}) + +## 获取这个名称的方法的数据 +static func get_method_data(script: Script, method_name: StringName) -> Dictionary: + var data = _get_script_data_cache(script) + var m_cache_data : Dictionary = ScriptCommentMenu_DataUtil.get_value_or_set(data, "method_data_cache", func(): + var method_data : Dictionary = {} + for i in script.get_script_method_list(): + method_data[i['name']]=i + return method_data + ) + return m_cache_data.get(method_name, {}) + +## 获取这个名称的信号的数据 +static func get_signal_data(script: Script, signal_name: StringName): + var data = _get_script_data_cache(script) + var s_cache_data : Dictionary = ScriptCommentMenu_DataUtil.get_value_or_set(data, "script_data_cache", func(): + var signal_data : Dictionary = {} + for i in script.get_script_signal_list(): + signal_data[i['name']]=i + return signal_data + ) + return s_cache_data.get(signal_name, {}) + + +## 获取方法数据 +## [br] +## [br][code]script[/code]: 脚本 +## [br][code]method[/code]: 要获取的方法数据的方法名 +## [br] +## [br][code]return[/code]: 返回脚本的数据信息。 +## 包括的 key 有 [code]name[/code], [code]args[/code], [code]default_args[/code] +## , [code]flags[/code], [code]return[/code], [code]id[/code] +func find_method_data(script: Script, method: String) -> Dictionary: + var method_data = script.get_script_method_list() + for m in method_data: + if m['name'] == method: + return m + return {} + + +## 获取扩展脚本链(扩展的所有脚本) +##[br] +##[br][code]script[/code] Object 对象或脚本 +##[br][code]return[/code] 返回继承的脚本路径列表 +static func get_extends_link(script: Script) -> PackedStringArray: + var list := PackedStringArray() + while script: + if FileAccess.file_exists(script.resource_path): + list.push_back(script.resource_path) + script = script.get_base_script() + return list + + +## 获取基础类型继承链类列表 +##[br] +##[br][code]_class[/code] 基础类型类名 +##[br][code]return[/code] 返回基础的类名列表 +static func get_extends_link_base(_class) -> PackedStringArray: + if _class is Script: + _class = _class.get_instance_base_type() + elif _class is Object: + _class = _class.get_class() + + var c = _class + var list = [] + while c != "": + list.append(c) + c = ClassDB.get_parent_class(c) + return PackedStringArray(list) + + +## 生成方法代码 +##[br] +##[br][code]method_data[/code] 方法数据 +##[br][code]return[/code] 返回生成的代码 +static func generate_method_code(method_data: Dictionary) -> String: + var temp := method_data.duplicate(true) + var args := "" + for i in temp['args']: + var arg_name = i['name'] + var arg_type = ( get_type_name(i['type']) if i['type'] != TYPE_NIL else "") + if arg_type.strip_edges() == "": + arg_type = str(i['class_name']) + if arg_type.strip_edges() != "": + arg_type = ": " + arg_type + args += "%s%s, " % [arg_name, arg_type] + temp['args'] = args.trim_suffix(", ") + if temp['return']['type'] != TYPE_NIL: + temp['return_type'] = get_type_name(temp['return']['type']) + + if temp.has('return_type') and temp['return_type'] != "": + temp['return_type'] = " -> " + str(temp['return_type']) + temp['return_sentence'] = "pass\n\treturn super." + temp['name'] + "()" + else: + temp['return_type'] = "" + temp['return_sentence'] = "pass" + + return "func {name}({args}){return_type}:\n\t{return_sentence}\n".format(temp) + + +## 获取对象的脚本 +static func get_object_script(object: Object) -> Script: + if object == null: + return null + if object is Script: + return object + return object.get_script() as Script + + +## 对象是否是 tool 状态 +##[br] +##[br][code]object[/code] 返回这个对象的脚本是否是开启 tool 的状态 +static func is_tool(object: Object) -> bool: + var script = get_object_script(object) + return script.is_tool() if script else false + + +## 获取对象的脚本路径,如果不存在脚本,则返回空的字符串 +static func get_object_script_path(object: Object) -> String: + var script = get_object_script(object) + return script.resource_path if script else "" + + +## 获取这个对象的这个方法的信息 +##[br] +##[br][code]object[/code] 对象 +##[br][code]method_name[/code] 方法名 +##[br][code]return[/code] 返回方法的信息 +static func get_object_method_data(object: Object, method_name: StringName) -> Dictionary: + if not is_instance_valid(object): + return {} + var script = get_object_script(object) + if script: + return get_method_data(script, method_name) + return {} + + +## 获取这个信号的数据 +static func get_object_signal_data(object: Object, signal_name: StringName) -> Dictionary: + if not is_instance_valid(object): + return {} + var script = get_object_script(object) + if script: + return get_signal_data(script, signal_name) + return {} + + +## 获取对象的属性数据 +static func get_object_property_data(object: Object, proprety_name: StringName) -> Dictionary: + if not is_instance_valid(object): + return {} + var script = get_object_script(object) + if script: + return get_property_data(script, proprety_name) + return {} + + +## 获取内置类名称转为对象。比如将 "Node" 字符串转为 [Node] 这种 GDScriptNativeClass 类型数据 +##[br] +##[br][code]_class[/code] 类名称 +static func get_built_in_class (_class: StringName): + if not ClassDB.class_exists(_class): + return null + var _class_db = ScriptCommentMenu_DataUtil.get_meta_dict_data("ScriptCommentMenu_ScriptUtil_get_built_in_class") + return ScriptCommentMenu_DataUtil.get_value_or_set(_class_db, _class, func(): + var script = GDScript.new() + script.source_code = "var type = " + _class + if script.reload() == OK: + var obj = script.new() + _class_db[_class] = obj.type + return _class_db[_class] + else: + push_error("错误的类名:", _class) + return null + ) + + +## 根据类名返回类对象 +static func get_script_class(_class: StringName): + if ClassDB.class_exists(_class): + return null + var _class_db = ScriptCommentMenu_DataUtil.get_meta_dict_data("ScriptCommentMenu_ScriptUtil_get_script_class") + return ScriptCommentMenu_DataUtil.get_value_or_set(_class_db, _class, func(): + var script = GDScript.new() + script.source_code = "var type = " + _class + if script.reload() == OK: + var obj = script.new() + _class_db[_class] = obj.type + return _class_db[_class] + else: + push_error("错误的类名:", _class) + return null + ) + + +## 创建脚本 +static func create_script(source_code: String) -> GDScript: + var data = ScriptCommentMenu_DataUtil.get_meta_dict_data("ScriptCommentMenu_ScriptUtil_create_script") + return ScriptCommentMenu_DataUtil.get_value_or_set(data, source_code.sha256_text(), func(): + var script := GDScript.new() + script.source_code = source_code + script.reload() + return script + ) + + +## 获取这个类的场景。这个场景的位置和名称需要和脚本一致,只有后缀名不一样。这个类不能是内部类 +static func get_script_scene(script: GDScript) -> PackedScene: + var data = ScriptCommentMenu_DataUtil.get_meta_dict_data("ScriptCommentMenu_ScriptUtil_get_script_scene") + if data.has(script): + return data[script] + else: + var path := script.resource_path + if path == "": + return null + + var ext := path.get_extension() + var file = path.substr(0, len(path) - len(ext)) + + var scene: PackedScene + if FileAccess.file_exists(file + "tscn"): + scene = ResourceLoader.load(file + "tscn", "PackedScene") as PackedScene + elif FileAccess.file_exists(file + "scn"): + scene = ResourceLoader.load(file + "scn", "PackedScene") as PackedScene + else: + printerr("这个类目录下没有相同名称的场景文件!") + return null + data[script] = scene + return scene + + +## 获取对象的类。如果是自定义类返回 [GDScript] 类;如果是内置类,则返回 [GDScriptNativeClass] 类 +static func get_object_class(object: Object): + if object: + if object is Script: + return object + if object.get_script() != null: + return object.get_script() + return get_built_in_class (object.get_class()) + return &"" diff --git a/DungeonShooting_Godot/addons/script_comment_menu/sub_item/@sub_item.gd b/DungeonShooting_Godot/addons/script_comment_menu/sub_item/@sub_item.gd new file mode 100644 index 0000000..83e9bf5 --- /dev/null +++ b/DungeonShooting_Godot/addons/script_comment_menu/sub_item/@sub_item.gd @@ -0,0 +1,88 @@ +#============================================================ +# @sub Tem +#============================================================ +# - datetime: 2022-07-17 16:32:29 +#============================================================ +class_name _ScriptMenu_SubItem + + +const MenuItemBuilder := ScriptCommentMenuConstant.MenuItemBuilder + + +var _editor_plugin = EditorPlugin.new() +var _util_script : ScriptCommentMenu_ScriptUtil +var _util_script_editor : ScriptCommentMenuConstant.ScriptEditorUtil + + +#============================================================ +# Set/Get +#============================================================ +func get_editor_interface() -> EditorInterface: + return _editor_plugin.get_editor_interface() + +func get_script_editor_util(): + return _util_script_editor + +func get_script_util() -> ScriptCommentMenu_ScriptUtil: + return _util_script + + +#============================================================ +# 自定义 +#============================================================ +## 外部调用初始化菜单 +##[br] +##[br][code]menu_button[/code] 菜单按钮 +func init_menu(menu_button: MenuButton) -> void: + if not menu_button.has_meta("IsInit"): + _util_script_editor = ScriptCommentMenuConstant.ScriptEditorUtil.new() + _util_script = ScriptCommentMenu_ScriptUtil.new() + menu_button.set_meta("IsInit", { + "_util_script_editor": _util_script_editor, + "_util_script": _util_script, + }) + + var data : Dictionary = menu_button.get_meta("IsInit") + for property in data: + var value = data[property] + set(property, value) + + _init_menu(menu_button) + + +## 添加分隔符 +func add_separator(menu_button: MenuButton): + (MenuItemBuilder.instance() + .set_menu_by_menu_button(menu_button) + .add_separator() + .build() + ) + +## 添加菜单 +func add_menu_item(menu_button: MenuButton, name: String, key_map: Dictionary, callable: Callable): + # 添加菜单 + (MenuItemBuilder.instance() + .set_menu(menu_button.get_popup()) + .set_item_name(name) + .set_connect(callable) + .set_key(key_map.get("key", false)) + .set_ctrl(key_map.get("ctrl", false)) + .set_shift(key_map.get("shift", false)) + .set_alt(key_map.get("alt", false)) + .build() + ) + + + +## 重写方法,初始化菜单 +##[br] +##[br][code]menu_button[/code] 菜单按钮 +func _init_menu(menu_button: MenuButton) -> void: + pass + + +## 卸载子项 +func _uninstall(): + pass + + diff --git a/DungeonShooting_Godot/addons/script_comment_menu/sub_item/comment/comment.gd b/DungeonShooting_Godot/addons/script_comment_menu/sub_item/comment/comment.gd new file mode 100644 index 0000000..1243aa7 --- /dev/null +++ b/DungeonShooting_Godot/addons/script_comment_menu/sub_item/comment/comment.gd @@ -0,0 +1,134 @@ +#============================================================ +# Comment +#============================================================ +# - datetime: 2022-07-17 14:49:15 +#============================================================ +## 脚本注释 +class_name _ScriptMenu_Comments +extends _ScriptMenu_SubItem + + +const SEPARATE_LENGTH = 60 + + +var util_script_editor := ScriptCommentMenuConstant.ScriptEditorUtil.new() +var util_script := ScriptCommentMenu_ScriptUtil.new() + +var regex = RegEx.new() + + +#============================================================ +# 内置 +#============================================================ +func _init(): + var pattern = "(?\\s*)(static\\s+)?func\\s+(?[^\\(]+)" + regex.compile(pattern) + + +#============================================================ +# 自定义 +#============================================================ +#(override) +func _init_menu(menu_button: MenuButton): + # 设置添加菜单项 + var menu : PopupMenu = menu_button.get_popup() + add_menu_item(menu_button, "脚本注释", {}, _script_comment) + add_separator(menu_button) + add_menu_item(menu_button, "方法注释", { + "key": KEY_C, + "ctrl": true, + "shift": true, + }, _func_comment) + add_menu_item(menu_button, "类别分隔", { + "key": KEY_SLASH, + "ctrl": true, + "shift": true, + }, _category_comment) + + +#============================================================ +# 功能 +#============================================================ +## 方法注释 +func _func_comment(): + var text_edit : TextEdit = util_script_editor.get_current_code_editor() + var line : int = text_edit.get_caret_line() + + for i in range(line, text_edit.get_line_count()): + var line_code : String = text_edit.get_line(i) + var result = regex.search(line_code) + if result: + var method = result.get_string("method").strip_edges() + printt(method + , util_script_editor.get_current_script() + , util_script_editor.get_current_script().resource_path + ) + + var indent = result.get_string("indent") + var data = util_script.find_method_data(util_script_editor.get_current_script(), method) + if data.size() == 0: + printerr('没有找到', method,'方法的数据,脚本是否还未保存?') + return + + var code : String = "## %s\n" % data['name'] + if data['args'].size() > 0: + code += (indent + "##[br]\n") + for arg in data['args']: + code += (indent + "##[br][code]%s[/code] \n" % arg['name']) + if data['return']['type'] != TYPE_NIL: + code += (indent + "##[br][code]return[/code] ") + code = code.trim_suffix("\n") + util_script_editor.insert_code_current_pos(code, true) + break + +## 脚本注释 +func _script_comment(): + var script = util_script_editor.get_current_script() + if script == null: + return + + var separa = "=".repeat(SEPARATE_LENGTH) + + # 脚本名 + var script_name = script.resource_path.get_file().get_basename().capitalize() + # 时间 + var datetime = Time.get_datetime_dict_from_system() + var datetime_str = "%02d-%02d-%02d %02d:%02d:%02d" % [ + datetime['year'], datetime['month'], datetime['day'], + datetime['hour'], datetime['minute'], datetime['second'], + ] + + var code = """#{sep} +# {name} +#{sep} +# - author: zhangxuetu +# - datetime: {datetime} +# - version: 4.0 +#{sep} +""".format({ + "sep": separa, + "name": script_name, + "datetime": datetime_str, +}) + # 插入到顶部 + var textedit = util_script_editor.get_current_code_editor() + textedit.set_caret_line(0) + textedit.set_caret_column(0) + textedit.insert_text_at_caret(code) + + +## 类别分隔 +func _category_comment(): + var separa = "=".repeat(SEPARATE_LENGTH) + var code = """#{sep} +# +#{sep}""".format({ + "sep": separa, + }) + + var textedit = util_script_editor.get_current_code_editor() + textedit.set_caret_column(0) + textedit.insert_text_at_caret(code) + textedit.set_caret_line(textedit.get_caret_line() - 1) + + diff --git a/DungeonShooting_Godot/addons/script_comment_menu/sub_item/override/dialog.gd b/DungeonShooting_Godot/addons/script_comment_menu/sub_item/override/dialog.gd new file mode 100644 index 0000000..f38412f --- /dev/null +++ b/DungeonShooting_Godot/addons/script_comment_menu/sub_item/override/dialog.gd @@ -0,0 +1,53 @@ +#============================================================ +# Dialog +#============================================================ +# - datetime: 2022-07-17 15:49:30 +#============================================================ +@tool +extends Control + + +signal selected_method(method_names : Array) + + +const SCRIPT_METHOD_LIST_SCRIPT = preload("res://addons/script_comment_menu/sub_item/override/scene/script_method_list.gd") +const CHECK_LABEL = preload("res://addons/script_comment_menu/sub_item/override/scene/check_label.gd") + + +@onready var confirmation_dialog = find_child("ConfirmationDialog") +@onready var script_method_list = find_child("ScriptMethodList") as SCRIPT_METHOD_LIST_SCRIPT + + +#============================================================ +# 自定义 +#============================================================ +## 显示弹窗 +func show_popup(script: Script): + confirmation_dialog.popup_centered_ratio(0.6) + script_method_list.update_data(script) + + +#============================================================ +# 自定义 +#============================================================ +func _ready(): + confirmation_dialog.confirmed.connect(_confirmed) + if not Engine.is_editor_hint(): + confirmation_dialog.popup_centered() + + +#============================================================ +# 连接信号 +#============================================================ +func _confirmed(): + var selected_items = script_method_list.get_selected_items() + + var method_list = {} + for item in selected_items: + item = item as CHECK_LABEL + var method_name : String = item.text + method_list[method_name] = null + item.selected = false + selected_method.emit(method_list.keys()) + + diff --git a/DungeonShooting_Godot/addons/script_comment_menu/sub_item/override/dialog.tscn b/DungeonShooting_Godot/addons/script_comment_menu/sub_item/override/dialog.tscn new file mode 100644 index 0000000..d8fefe8 --- /dev/null +++ b/DungeonShooting_Godot/addons/script_comment_menu/sub_item/override/dialog.tscn @@ -0,0 +1,22 @@ +[gd_scene load_steps=3 format=3 uid="uid://cnx2ri54w1dpr"] + +[ext_resource type="Script" path="res://addons/script_comment_menu/sub_item/override/dialog.gd" id="1_isfs8"] +[ext_resource type="PackedScene" uid="uid://btcf0syabq2et" path="res://addons/script_comment_menu/sub_item/override/scene/script_method_list.tscn" id="2_lxp0b"] + +[node name="Dialog" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +mouse_filter = 2 +script = ExtResource("1_isfs8") +metadata/_edit_lock_ = true + +[node name="ConfirmationDialog" type="ConfirmationDialog" parent="."] +dialog_close_on_escape = false + +[node name="ScriptMethodList" parent="ConfirmationDialog" instance=ExtResource("2_lxp0b")] +anchor_right = 0.0 +anchor_bottom = 0.0 +offset_right = 636.0 +offset_bottom = 342.0 diff --git a/DungeonShooting_Godot/addons/script_comment_menu/sub_item/override/override.gd b/DungeonShooting_Godot/addons/script_comment_menu/sub_item/override/override.gd new file mode 100644 index 0000000..2e10602 --- /dev/null +++ b/DungeonShooting_Godot/addons/script_comment_menu/sub_item/override/override.gd @@ -0,0 +1,106 @@ +#============================================================ +# Override +#============================================================ +# - datetime: 2022-07-17 15:53:56 +#============================================================ + +## 重写 +class_name _ScriptMenu_Overrides +extends _ScriptMenu_SubItem + + +const DIALOG_SCRIPT = preload("dialog.gd") +const DIALOG_SCENE = preload("dialog.tscn") + + +var dialog = DIALOG_SCENE.instantiate() as DIALOG_SCRIPT + + +#============================================================ +# 自定义 +#============================================================ +#(override) +func _init_menu(menu_button: MenuButton): + # 添加弹窗 + dialog.selected_method.connect(_selected_method) + get_editor_interface().get_base_control().add_child(dialog) + dialog.theme = get_editor_interface().get_base_control().theme + # 添加菜单 + add_separator(menu_button) + add_menu_item(menu_button, "重写方法", { + "key": KEY_O, + "ctrl": true, + "shift": true, + }, _show_popup) + + +#(override) +func _uninstall(): + super._uninstall() + dialog.queue_free() + + +#============================================================ +# 连接信号 +#============================================================ +## 显示弹窗 +func _show_popup(): + var script = get_script_editor_util().get_current_script() as Script + dialog.show_popup(script) + + +const FORMAT = """ +#(override) +func {method_name}({arguments}){return_type}: + {return_value}super.{method_name}({parameters}) + +""" + +func _selected_method(method_names : Array): + + var text_edit = get_script_editor_util().get_current_code_editor() as TextEdit + var script = get_script_editor_util().get_current_script() as Script + + var code : String = "" + + var added = {} + for method_data in script.get_script_method_list(): + if added.has(method_data['name']) or not method_data['name'] in method_names: + continue + + added[method_data['name']] = null + + var method_name = method_data['name'] + var method_type = method_data.get("type", 0) + var method_args = method_data['args'] as Array + var method_return = method_data['return'] + + # 参数列表 + var arguments : String = ", ".join(method_args.map(func(arg): return arg['name'])) + arguments = arguments.strip_edges().trim_suffix(",") + + # 类型 + var return_type : String = ScriptCommentMenu_ScriptUtil.get_type_name(method_type) + var return_value : String = "" + if return_type == "null": + return_type = "" + if return_type != "": + return_type = " -> " + return_type + return_value = "return " + + code += FORMAT.format({ + "method_name": method_name, + "method_type": ScriptCommentMenu_ScriptUtil.get_type_name(method_type), + "return_type": return_type, + "return_value": "", + "arguments": arguments, + "parameters": arguments, + }) + + text_edit.set_caret_column(0) + text_edit.insert_text_at_caret(code) + + + + + diff --git a/DungeonShooting_Godot/addons/script_comment_menu/sub_item/override/scene/check_label.gd b/DungeonShooting_Godot/addons/script_comment_menu/sub_item/override/scene/check_label.gd new file mode 100644 index 0000000..be8c877 --- /dev/null +++ b/DungeonShooting_Godot/addons/script_comment_menu/sub_item/override/scene/check_label.gd @@ -0,0 +1,51 @@ +#============================================================ +# Check check_box +#============================================================ +# - datetime: 2022-07-16 22:30:22 +#============================================================ +@tool +extends HBoxContainer + + +signal pressed + + +@export +var text : String = "": + set(value): + text = value + if check_box == null: + await ready + check_box.text = value + get: + return check_box.text + +@export +var selected : bool = false : + set(value): + selected = value + if check_box == null: + await ready + check_box.button_pressed = value + get: + return check_box.button_pressed + +@export +var color : Color = Color.WHITE : + set(value): + color = value + if check_box == null: + await ready + check_box.modulate = color + + +@onready var check_box = $CheckBox as CheckBox + + +func _ready(): + check_box.pressed.connect(_pressed) + + +func _pressed(): + pressed.emit() + diff --git a/DungeonShooting_Godot/addons/script_comment_menu/sub_item/override/scene/check_label.tscn b/DungeonShooting_Godot/addons/script_comment_menu/sub_item/override/scene/check_label.tscn new file mode 100644 index 0000000..e034436 --- /dev/null +++ b/DungeonShooting_Godot/addons/script_comment_menu/sub_item/override/scene/check_label.tscn @@ -0,0 +1,15 @@ +[gd_scene load_steps=2 format=3 uid="uid://bvoxy56c0um5v"] + +[ext_resource type="Script" path="res://addons/script_comment_menu/sub_item/override/scene/check_label.gd" id="1_7qjn7"] + +[node name="CheckLabel" type="HBoxContainer"] +offset_right = 484.0 +offset_bottom = 24.0 +size_flags_horizontal = 3 +size_flags_vertical = 0 +script = ExtResource("1_7qjn7") + +[node name="CheckBox" type="CheckBox" parent="."] +offset_right = 484.0 +offset_bottom = 24.0 +size_flags_horizontal = 3 diff --git a/DungeonShooting_Godot/addons/script_comment_menu/sub_item/override/scene/item_list.gd b/DungeonShooting_Godot/addons/script_comment_menu/sub_item/override/scene/item_list.gd new file mode 100644 index 0000000..baf1991 --- /dev/null +++ b/DungeonShooting_Godot/addons/script_comment_menu/sub_item/override/scene/item_list.gd @@ -0,0 +1,88 @@ +#============================================================ +# Item List +#============================================================ +# - datetime: 2022-07-17 15:02:44 +#============================================================ +@tool +extends VBoxContainer + + +const CHECK_LABEL_SCENE = preload("check_label.tscn") +const CHECK_LABEL_SCRIPT = preload("check_label.gd") + + +var last_press_check : CHECK_LABEL_SCRIPT + + +## 添加 Item +##[br] +##[br][code]label[/code] +##[br][code]color[/code] +##[br] +##[br][code]return[/code] +func add_item(label: String, color : Color = Color.WHITE) -> CHECK_LABEL_SCRIPT: + var check_label = CHECK_LABEL_SCENE.instantiate() + add_child(check_label) + check_label.text = label + check_label.color = color + # 上一次点击的对象 + check_label.pressed.connect(_pressed.bind(check_label)) + return check_label + + +## 添加组别标签 +##[br] +##[br][code]text[/code] 标签名 +func add_label(text: String): + var space = Control.new() + space.custom_minimum_size.y = 4 + add_child(space) + var panel = Panel.new() + panel.custom_minimum_size.y = 1 + add_child(panel) + var label = Label.new() + label.text = text + add_child(label) + + +## 获取所有项 +func get_all_item() -> Array: + var list = [] + for child in get_children(): + if child is CHECK_LABEL_SCRIPT: + list.append(child) + return list + + +## 获取选中的项 +func get_selected_items() -> Array: + var list = [] + for item in get_all_item(): + item = item as CHECK_LABEL_SCRIPT + if item.selected: + list.append(item) + return list + +## 清除所有 +func clear(): + for child in get_children(): + child.queue_free() + + + +#============================================================ +# 连接信号 +#============================================================ +func _pressed(check: CHECK_LABEL_SCRIPT): + # 如果是按着 Shift 键 + var all_item = get_all_item() + if Input.is_key_pressed(KEY_SHIFT): + if last_press_check != check: + var last_index = all_item.find(last_press_check) + var curr_index = all_item.find(check) + var selected = check.selected + for i in range(last_index, curr_index, sign(curr_index - last_index)): + all_item[i].selected = selected + + last_press_check = check + diff --git a/DungeonShooting_Godot/addons/script_comment_menu/sub_item/override/scene/item_list.tscn b/DungeonShooting_Godot/addons/script_comment_menu/sub_item/override/scene/item_list.tscn new file mode 100644 index 0000000..9177745 --- /dev/null +++ b/DungeonShooting_Godot/addons/script_comment_menu/sub_item/override/scene/item_list.tscn @@ -0,0 +1,10 @@ +[gd_scene load_steps=2 format=3 uid="uid://dmiw7e671j5ox"] + +[ext_resource type="Script" path="res://addons/script_comment_menu/sub_item/override/scene/item_list.gd" id="1_of1bk"] + +[node name="ItemList" type="VBoxContainer"] +offset_right = 1024.0 +offset_bottom = 600.0 +size_flags_horizontal = 3 +size_flags_vertical = 3 +script = ExtResource("1_of1bk") diff --git a/DungeonShooting_Godot/addons/script_comment_menu/sub_item/override/scene/script_method_list.gd b/DungeonShooting_Godot/addons/script_comment_menu/sub_item/override/scene/script_method_list.gd new file mode 100644 index 0000000..240df10 --- /dev/null +++ b/DungeonShooting_Godot/addons/script_comment_menu/sub_item/override/scene/script_method_list.gd @@ -0,0 +1,69 @@ +#============================================================ +# Script Method List +#============================================================ +# - datetime: 2022-07-17 15:22:58 +#============================================================ +@tool +extends MarginContainer + + +const ITEM_LIST_SCRIPT = preload("item_list.gd") + + +@onready var item_list = $ScrollContainer/ItemList as ITEM_LIST_SCRIPT +@onready var scroll_container = $ScrollContainer + + +## 获取所有选中的项 +func get_selected_items() -> Array: + return item_list.get_selected_items() + + +## 更新数据 +##[br] +##[br][code]script[/code] 脚本 +func update_data(script: Script): + # 获取脚本的继承的所有脚本类 + var scripts = [] + script = script.get_base_script() + while script != null: + scripts.append(script) + script = script.get_base_script() + # 显示脚本的数据 + show_script_list_data(scripts) + + # 滚动到下面 +# await get_tree().create_timer(0.1).timeout +# scroll_container.scroll_vertical = 2000 + + +## 展示脚本列表数据 +##[br] +##[br][code]scripts[/code] +func show_script_list_data(scripts: Array): + item_list.clear() + # 已添加过的(防止重复获取) + var added : Dictionary = {} + # 开始遍历 + scripts.reverse() + for script in scripts: + var path = script.resource_path.get_file() + item_list.add_label(path) + + # 获取数据 + var data = {} + for method_data in script.get_script_method_list(): + var method_name : String = method_data['name'] + if not added.has(method_name): + data[method_name] = method_data + added[method_name] = null + + # 排序 + var list = data.keys() + list.sort() + for key in list: + var method_data : Dictionary = data[key] + var method : String = method_data['name'] + item_list.add_item(method) + + diff --git a/DungeonShooting_Godot/addons/script_comment_menu/sub_item/override/scene/script_method_list.tscn b/DungeonShooting_Godot/addons/script_comment_menu/sub_item/override/scene/script_method_list.tscn new file mode 100644 index 0000000..8f47fd9 --- /dev/null +++ b/DungeonShooting_Godot/addons/script_comment_menu/sub_item/override/scene/script_method_list.tscn @@ -0,0 +1,17 @@ +[gd_scene load_steps=3 format=3 uid="uid://btcf0syabq2et"] + +[ext_resource type="Script" path="res://addons/script_comment_menu/sub_item/override/scene/script_method_list.gd" id="1_ao6ev"] +[ext_resource type="PackedScene" uid="uid://dmiw7e671j5ox" path="res://addons/script_comment_menu/sub_item/override/scene/item_list.tscn" id="1_rynrx"] + +[node name="ScriptMethodList" type="MarginContainer"] +anchor_right = 1.0 +anchor_bottom = 1.0 +size_flags_horizontal = 3 +size_flags_vertical = 3 +script = ExtResource("1_ao6ev") + +[node name="ScrollContainer" type="ScrollContainer" parent="."] +offset_right = 1024.0 +offset_bottom = 600.0 + +[node name="ItemList" parent="ScrollContainer" instance=ExtResource("1_rynrx")] diff --git a/DungeonShooting_Godot/addons/script_comment_menu/util/@constant.gd b/DungeonShooting_Godot/addons/script_comment_menu/util/@constant.gd new file mode 100644 index 0000000..29fed0c --- /dev/null +++ b/DungeonShooting_Godot/addons/script_comment_menu/util/@constant.gd @@ -0,0 +1,7 @@ +class_name ScriptCommentMenuConstant + + +const AddMenu = preload("res://addons/script_comment_menu/util/add_menu.gd") +const MenuItemBuilder = preload("res://addons/script_comment_menu/util/menu_item_builder.gd") +const PopupMenuUtil = preload("res://addons/script_comment_menu/util/popup_menu_util.gd") +const ScriptEditorUtil = preload("res://addons/script_comment_menu/util/script_editor_util.gd") diff --git a/DungeonShooting_Godot/addons/script_comment_menu/util/add_menu.gd b/DungeonShooting_Godot/addons/script_comment_menu/util/add_menu.gd new file mode 100644 index 0000000..0415ba8 --- /dev/null +++ b/DungeonShooting_Godot/addons/script_comment_menu/util/add_menu.gd @@ -0,0 +1,106 @@ +extends EditorScript + + +const EditorUtil_PopupMenu = preload("popup_menu_util.gd") + + +var json = JSON.new() +var util_popup_menu = EditorUtil_PopupMenu.new() + + +func _run(): + pass + + var menu = MenuButton.new() + menu.text = "测试菜单" + add_editor_menu(menu) + await get_tree().create_timer(2).timeout + menu.queue_free() + + + +var _top_container: HBoxContainer +func _get_top_container() -> HBoxContainer: + if _top_container == null: + for child in get_editor_interface().get_base_control().get_children(): + if child is VBoxContainer: + _top_container = child.get_child(0) + break + return _top_container + +var _editor_menu_container : HBoxContainer +func get_editor_menu_container() -> HBoxContainer: + if _editor_menu_container == null: + _editor_menu_container = _get_top_container().get_child(0) + return _editor_menu_container + + +func add_editor_menu(menu_button: MenuButton): + get_editor_menu_container().add_child(menu_button) + + +func get_tree(): + return get_editor_interface().get_tree() + + +## 添加脚本菜单按钮 +func add_script_editor_menu(menu_button: MenuButton, items: Array = []): + var popup = menu_button.get_popup() + for item in items: + if item.begins_with("-"): + popup.add_separator() + else: + while item.begins_with("-"): + item = item.trim_prefix("-") + popup.add_item(item) + + var menu_container : Control + while true: + var tmp = get_editor_interface() \ + .get_script_editor() \ + .get_current_editor() + if tmp == null: + await Engine.get_main_loop().create_timer(1).timeout + continue + for i in 4: + tmp = tmp.get_parent_control() + if tmp == null: + break + if tmp == null: + await Engine.get_main_loop().create_timer(1).timeout + continue + menu_container = tmp.get_child(0) as Control + break + + var node_index : int = 0 + for i in range(menu_container.get_child_count() - 1, -1, -1): + if menu_container.get_child(i) is MenuButton: + node_index = i + 1 + break + menu_container.add_child(menu_button) + menu_container.move_child(menu_button, node_index) + + +func connect_menu(menu, item_name: String, callable, method: String = ""): + var popup : PopupMenu + if menu is MenuButton: + popup = menu.get_popup() + elif menu is PopupMenu: + popup = menu + if method: + util_popup_menu.connect_popup_item(menu.get_popup(), item_name, callable, method) + else: + util_popup_menu.connect_popup_item(menu.get_popup(), item_name, callable.get_object(), callable.get_method()) + + +static func add_menu_item_shortcut( + menu: MenuButton + , item_name: String + , keycode: int + , ctrl : bool + , alt : bool + , shift : bool +): + EditorUtil_PopupMenu.add_popup_shortcut( + menu.get_popup(), item_name, keycode, ctrl, alt, shift + ) diff --git a/DungeonShooting_Godot/addons/script_comment_menu/util/menu_item_builder.gd b/DungeonShooting_Godot/addons/script_comment_menu/util/menu_item_builder.gd new file mode 100644 index 0000000..50640f6 --- /dev/null +++ b/DungeonShooting_Godot/addons/script_comment_menu/util/menu_item_builder.gd @@ -0,0 +1,139 @@ +#============================================================ +# Menu Item Builder +#============================================================ +# - datetime: 2022-07-17 14:14:42 +#============================================================ +# 菜单项建造器 + +var _menu : PopupMenu +var _name : String +var _method : Callable +var _short : Dictionary = { + key = KEY_NONE, + ctrl = false, + alt = false, + shift = false, +} +var _as_separator : bool = false + +var _id : int = -1 + + +#============================================================ +# Set/Get +#============================================================ +## 设置添加的菜单 +##[br] +##[br][code]menu[/code] 菜单 +func set_menu(menu: PopupMenu): + self._menu = menu + return self + +## 设置菜单按钮对象 +func set_menu_by_menu_button(menu_button: MenuButton): + set_menu( menu_button.get_popup() ) + return self + + +## 设置菜单名 +##[br] +##[br][code]name[/code] +func set_item_name(name: String): + self._name = name + return self + +## 设置连接的方法 +##[br] +##[br][code]method[/code] 连接的方法 +func set_connect(method: Callable): + self._method = method + return self + +## 设置快捷键 +##[br] +##[br][code]key[/code] 按键 Key 值 +##[br][code]ctrl[/code] Ctrl +##[br][code]alt[/code] Alt +##[br][code]shift[/code] Shift +func set_shortcut( + key: int + , ctrl: bool = false + , alt: bool = false + , shift: bool = false +): + self._short.key = key + self._short.ctrl = ctrl + self._short.alt = alt + self._short.shift = shift + return self + +func set_key(key : int): + self._short.key = key + return self + +func set_ctrl(value : bool = true): + self._short.ctrl = value + return self + +func set_shift(value : bool = true): + self._short.shift = value + return self + +func set_alt(value : bool = true): + self._short.alt = value + return self + + +#============================================================ +# 自定义 +#============================================================ +## 实例化一个 Builder +##[br] +##[br][code]return[/code] 返回实例化对象 +static func instance(): + var builder = load("res://addons/script_comment_menu/util/menu_item_builder.gd").new() + return builder + + +## 添加分隔符 +func add_separator(): + _as_separator = true + return self + + +## 构建添加 +##[br] +##[br][code]return[/code] 返回菜单的 id 值 +func build() -> int: + _id = _menu.item_count + + # 引用这个 builder,不这样则会因为引用消失而造成下面的 _id_pressed 失效 + _menu.set_meta("MenuItemMenu_%d" % _id, self) + + if _menu.id_pressed.connect( _id_pressed ) != OK: + printerr(" > id_pressed 信号连接方法失败") + + if not _as_separator: + _menu.add_item(_name) + else: + _menu.add_separator(_name) + + if _short.key != KEY_NONE: + var input = InputEventKey.new() + input.keycode = _short.key + input.ctrl_pressed = _short.ctrl + input.alt_pressed = _short.alt + input.shift_pressed = _short.shift + var shortcut = Shortcut.new() + shortcut.events.append(input) + _menu.set_item_shortcut(_id, shortcut) + + return _id + + +#============================================================ +# 连接信号 +#============================================================ +func _id_pressed(id): + if id == _id: + _method.call() diff --git a/DungeonShooting_Godot/addons/script_comment_menu/util/popup_menu_util.gd b/DungeonShooting_Godot/addons/script_comment_menu/util/popup_menu_util.gd new file mode 100644 index 0000000..e000a24 --- /dev/null +++ b/DungeonShooting_Godot/addons/script_comment_menu/util/popup_menu_util.gd @@ -0,0 +1,62 @@ +extends RefCounted + + +static func find_popup_menu_id(popup: PopupMenu, item_name: String) -> int: + for idx in popup.get_item_count(): + # 找到这个菜单 + if popup.get_item_text(idx) == item_name: + return idx + return -1 + + +static func add_popup_shortcut( + popup: PopupMenu + , item_name: String + , keycode: int + , ctrl : bool + , alt : bool + , shift : bool +): + var idx = find_popup_menu_id(popup, item_name) + if idx > -1: + var shortcut = Shortcut.new() + var input = InputEventKey.new() + input.keycode = keycode + input.ctrl_pressed = ctrl + input.alt_pressed = alt + input.shift_pressed = shift + shortcut.events.append(input) + popup.set_item_shortcut(idx, shortcut) + else: + printerr("没有这个名称 ", item_name, " 的菜单项") + + + +var _popup_data := {} +func connect_popup_item(popup: PopupMenu, item_name: String, target: Object, method: String) -> int: + var idx = find_popup_menu_id(popup, item_name) + if idx > -1: + if not popup.id_pressed.is_connected(self._popup_id_pressed): + popup.id_pressed.connect(self._popup_id_pressed.bind(popup)) + if not _popup_data.has(popup): + _popup_data[popup] = {} + if not _popup_data[popup].has(idx): + _popup_data[popup][idx] = [] + # 记录这个菜单的 id 的点击数据 + _popup_data[popup][idx].append({ + "target": target, + "method": method, + }) + else: + printerr("这个菜单 popup 没有 ", item_name, " 的菜单项") + + return idx + + +func _popup_id_pressed(idx: int, popup: PopupMenu): + if _popup_data.has(popup): + var connected_data_list : Array = _popup_data[popup][idx] + for data in connected_data_list: + var target : Object = data['target'] + var method : String = data['method'] + target.call(method) diff --git a/DungeonShooting_Godot/addons/script_comment_menu/util/script_editor_util.gd b/DungeonShooting_Godot/addons/script_comment_menu/util/script_editor_util.gd new file mode 100644 index 0000000..853dcaf --- /dev/null +++ b/DungeonShooting_Godot/addons/script_comment_menu/util/script_editor_util.gd @@ -0,0 +1,65 @@ +extends EditorScript + + +## 当前代码编辑器 +func get_current_code_editor() -> TextEdit: + return (get_editor_interface() + .get_script_editor() + .get_current_editor() + .get_base_editor()) as TextEdit + + +## 当前脚本的代码 +func get_current_script_code() -> String: + return get_current_code_editor().text + + +var _script_popup_id : int = -1 +var _script_popup := {} +## 添加脚本弹窗 +## @return 返回添加的弹窗菜单的[code]id[/code] +func add_script_popup(popup: PopupMenu) -> int: + _script_popup_id += 1 + _script_popup[_script_popup_id] = popup + get_editor_interface().get_script_editor().add_child(popup) + return _script_popup_id + + +## 弹出菜单 +## @id 菜单 [code]id[/code] 为 [method add_script_popup]add_script_popup[/method] 后返回的值 +func popup_menu(id: int): + if _script_popup.has(id): + var editor = get_current_code_editor() as TextEdit + var popup : PopupMenu = _script_popup[id] + popup.position = (get_current_code_editor().global_position + + get_current_code_editor().get_caret_draw_pos() + + Vector2(0, 50) + ) + popup.popup() + +func get_current_script() -> Script: + return get_editor_interface() \ + .get_script_editor() \ + .get_current_script() + + +func insert_code_current_pos(code: String, insert_first: bool = false): + var textedit = get_current_code_editor() + if insert_first: + textedit.set_caret_column(0) + textedit.insert_text_at_caret(code) + + +func _run(): + pass + + var popup = PopupMenu.new() + popup.add_item("test_01") + popup.add_item("test_02") + popup.add_item("test_03") + var id = add_script_popup(popup) + popup_menu(id) + + await get_editor_interface().get_tree().create_timer(1).timeout + popup.queue_free() + diff --git a/DungeonShooting_Godot/addons/table_data_editor/column_width_test.gdata b/DungeonShooting_Godot/addons/table_data_editor/column_width_test.gdata new file mode 100644 index 0000000..71ec83c --- /dev/null +++ b/DungeonShooting_Godot/addons/table_data_editor/column_width_test.gdata Binary files differ diff --git a/DungeonShooting_Godot/addons/table_data_editor/plugin.cfg b/DungeonShooting_Godot/addons/table_data_editor/plugin.cfg new file mode 100644 index 0000000..26cdea3 --- /dev/null +++ b/DungeonShooting_Godot/addons/table_data_editor/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="table-data-editor" +description="" +author="张学徒" +version="1.3" +script="plugin.gd" diff --git a/DungeonShooting_Godot/addons/table_data_editor/plugin.gd b/DungeonShooting_Godot/addons/table_data_editor/plugin.gd new file mode 100644 index 0000000..610cba5 --- /dev/null +++ b/DungeonShooting_Godot/addons/table_data_editor/plugin.gd @@ -0,0 +1,64 @@ +#============================================================ +# Plugin +#============================================================ +# - datetime: 2022-11-27 22:27:12 +#============================================================ +@tool +extends EditorPlugin + + +const MAIN = preload("src/table_data_editor/table_data_editor.tscn") + +var main := MAIN.instantiate() as TableDataEditor +# 第一次显示出来 +var first_show := false + + +func _ready(): + if Time.get_ticks_msec() < 5000: + await Engine.get_main_loop().create_timer(5).timeout + + main.visible = false + get_editor_interface().get_editor_main_screen().add_child(main) + main.call_deferred("set_anchors_preset", Control.PRESET_FULL_RECT) + main.set_deferred("size", main.get_parent().size) + main.get_child(0).set_deferred("size", main.size) + + # 创建新文件时进行扫描 + main.created_file.connect(func(path): + await Engine.get_main_loop().create_timer(0.1).timeout + get_editor_interface() \ + .get_resource_filesystem() \ + .scan.call_deferred() + ) + + +func _exit_tree() -> void: + main.queue_free() + +func _has_main_screen(): + return true + +func _make_visible(visible): + main.visible = visible + +func _get_plugin_name(): + return "TableDataEditor" + +func _get_plugin_icon(): + var icon = get_editor_interface() \ + .get_base_control() \ + .get_theme_icon("GridContainer", "EditorIcons") as Texture2D + + icon = icon.duplicate(true) + var image = icon.get_image() as Image + var image_size = image.get_size() + var color : Color + for x in image_size.x: + for y in image_size.y: + color = image.get_pixel(x, y) + if color.a != 0: + color = get_editor_interface().get_editor_settings().get_setting("text_editor/theme/highlighting/text_color") + image.set_pixel(x, y, color) + var texture = ImageTexture.create_from_image(image) + return texture diff --git a/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/cache_data.gd b/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/cache_data.gd new file mode 100644 index 0000000..a6b2eae --- /dev/null +++ b/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/cache_data.gd @@ -0,0 +1,86 @@ +#============================================================ +# Project Data +#============================================================ +# - author: zhangxuetu +# - datetime: 2023-05-17 17:37:56 +# - version: 4.0 +#============================================================ +## 项目数据 +## +##整个表格项目中的之前的缓存数据,也可视作整个项目数据 +##[br]启动这个节点后 +class_name TableDataEditor_CacheData + + +const CACHE_DATA_PATH = "res://.godot/table_data_editor/~json_edit_grid_cache_data.gdata" + + +## 最后一次操作的文件的路径 +var last_operation_path : String = "" +# 最近打开过的文件 +var recently_opend_paths = {}: + set(v): + if v is Array: + recently_opend_paths = {} + for item in v: + recently_opend_paths[item] = null + elif v is Dictionary: + recently_opend_paths = v + else: + assert(false, "错误的数据类型") +# 弹窗路径 +var dialog_path : String = "" + + +#============================================================ +# SetGet +#============================================================ +func get_recently_opend_paths() -> Array: + if recently_opend_paths is Array: + return recently_opend_paths + return recently_opend_paths.keys() + + + +#============================================================ +# 自定义 +#============================================================ +static func instance() -> TableDataEditor_CacheData: + var script = TableDataEditor_CacheData as GDScript + const KEY = "instance" + if script.has_meta(KEY): + return script.get_meta(KEY) + + else: + var object = TableDataEditor_CacheData.new() + var data + if FileAccess.file_exists(CACHE_DATA_PATH): + var bytes = FileAccess.get_file_as_bytes(CACHE_DATA_PATH) + data = bytes_to_var(bytes) + if data is Dictionary: + TableDataUtil.Classes.set_property_by_dict(object, data) + + script.set_meta(KEY, object) + return object + + +## 保存数据 +static func save_data() -> void: + var data = TableDataUtil.Classes.get_dict_by_property(instance()) + TableDataUtil.Files.save_data(CACHE_DATA_PATH, data) + + +## 存在打开的文件 +func exists_opened_path() -> bool: + return last_operation_path != "" and FileAccess.file_exists(last_operation_path) + + +func update_last_operation_path(path: String): + path = path.strip_edges() + last_operation_path = path + if path != "": + if not recently_opend_paths is Dictionary: + recently_opend_paths = {} + if FileAccess.file_exists(path): + recently_opend_paths[path] = null + diff --git a/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/export_preview/export_preview_window.gd b/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/export_preview/export_preview_window.gd new file mode 100644 index 0000000..6077a84 --- /dev/null +++ b/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/export_preview/export_preview_window.gd @@ -0,0 +1,284 @@ +#============================================================ +# Export Preview +#============================================================ +# - author: zhangxuetu +# - datetime: 2022-11-27 17:45:09 +# - version: 4.0 +#============================================================ +# 预览导出 +@tool +class_name ExportPreviewWindow +extends Window + + +## 最大示例数量 +const EXAMPLE_COUNT : int = 5 + + +## 导出 json 数据 +signal exported(path: String, type, data) + + +@export_node_path("TableDataEditor") var _table_data_editor : NodePath +@onready var table_data_editor : TableDataEditor = get_node(_table_data_editor) + +var __init_node = InjectUtil.auto_inject(self, "_") +var _text_box : TextEdit +var _head_as_key_panel : Control +var _head_line_box : SpinBox +var _save_dialog : FileDialog +var _compact : CheckBox +var _select_items : Control +var _selected_item_param : Control +var _resource_class_name : LineEdit + + +## 指定的 head 列数对应的值内容。_head_map[列数] = 值内容 +var _head_map : Dictionary = {} +## 类型选项按钮组 +var _button_group : ButtonGroup + + +#============================================================ +# 内置 +#============================================================ +func _ready() -> void: + _button_group = _select_items.get_child(0).button_group as ButtonGroup + _button_group.pressed.connect(func(button): + for child in _selected_item_param.get_children(): + if child is Control: + child.visible = false + + var item_node : Control = _selected_item_param.get_node_or_null(str(button.name)) + if item_node: + item_node.visible = true + + update_text_box_content() + ) + + +#============================================================ +# SetGet +#============================================================ +## 获取头部字段行。列值对应字段值 +func get_head_map() -> Dictionary: + var head_row_number : int = _head_line_box.value + var data_set = table_data_editor.get_table_edit().get_data_set() + var head_row_data : Dictionary = data_set.grid_data.get(head_row_number, {}) + return head_row_data + + +## 将有值的行的数据进行保存 +func get_data_by_head_row() -> Array: + var result : Array = [] + var head_row_number : int = _head_line_box.value + var data_set = table_data_editor.get_table_edit().get_data_set() + var head_row_data : Dictionary = data_set.grid_data.get(head_row_number, {}) + head_row_number += 1 + for row in range(head_row_number, data_set.grid_data.size() + 1): + var data = {} + var row_data = data_set.grid_data[row] + for column in head_row_data: + data[head_row_data[column]] = row_data.get(column, "") + result.append(data) + return result + + +## 获取 CSV 格式数据 +func get_csv_data() -> Array[String]: + var data_set = table_data_editor.get_table_edit().data_set as TableDataEditor_TableDataSet + var max_column : int = data_set.get_max_column() + if max_column == 0: + return [] + + var csv_list : Array[String] = [] + for row in data_set.get_row_list(): + var line : Array = [] + for column in range(1, max_column + 1): + line.append( + JSON.stringify(data_set.get_value(Vector2i(column, row))) + ) + csv_list.append(",".join(line)) + + return csv_list + + +## 获取转为资源的数据 +func get_resources_by_path(path: String) -> Array[Resource]: + var head_row_data : Dictionary = get_head_map() + var head_row_number : int = _head_line_box.value + var data_set = table_data_editor.get_table_edit().get_data_set() + var row_list = data_set.get_row_list() + for idx in row_list.size(): + var row = row_list[idx] + if row > head_row_number: + row_list = row_list.slice(idx, row_list.size()) + break + + # 类名 + var r_class_name : String = _resource_class_name.text.strip_edges() + + # 属性列表 + var propertys : Array = [] + for column in head_row_data: + + # 寻找这个字段有数据的行,判断数据类型 + var value = "" + for row in row_list: + value = data_set.grid_data[row].get(column) + if value != "": + break + + # 判断数据类型 + var type = "String" + if value != "": + var json = JSON.parse_string(value) + match typeof(json): + TYPE_STRING, TYPE_NIL: type = "String" + TYPE_INT: type = "int" + TYPE_FLOAT: type = "float" + TYPE_BOOL: type = "bool" + TYPE_ARRAY: type = "Array" + TYPE_DICTIONARY: type = "Dictionary" + TYPE_COLOR: type = "Color" + TYPE_VECTOR2: type = "Vector2" + TYPE_VECTOR2I: type = "Vector2i" + TYPE_VECTOR3: type = "Vector3" + TYPE_VECTOR3I: type = "Vector3i" + TYPE_VECTOR4: type = "Vector4" + TYPE_VECTOR4I: type = "Vector4i" + _: "String" + + # 生成 @export s属性 + var property = head_row_data[column] + printt(column, property, value) + propertys.append("@export var %s : %s " % [property, type]) + + # 生成脚本 + var script = GDScript.new() + script.source_code = """# {ScriptName} +{ClassName}extends Resource + +{Propertys} +""".format({ + "ScriptName": path.get_file(), + "ClassName": ("class_name %s\n" % [r_class_name]) if r_class_name else "", + "Propertys": "\n".join(propertys), + }) + ResourceSaver.save(script, path) + + # 生成资源 + var resources : Array[Resource] = [] + var new_script = load(path) as GDScript + for row in row_list: + # 生成数据 + var data = {} + var row_data = data_set.grid_data[row] + for column in head_row_data: + data[head_row_data[column]] = row_data.get(column, "") + + # 生成资源。避免 new 时跟已有的类冲突造成的报错 + var resource = Resource.new() + resource.set_script(new_script) + for property in data: + resource[property] = data[property] + resources.append(resource) + + return resources + + + +#============================================================ +# 自定义 +#============================================================ +func _data_format(data) -> String: + return JSON.stringify(data, "" if _compact.button_pressed else "\t") + + +func _update_by_head_row(): + var data_list = get_data_by_head_row() + var examples = [] + for i in range(min(data_list.size(), EXAMPLE_COUNT)): + examples.append(data_list[i]) + _text_box.text = JSON.stringify(examples, "\t") + + +func _update_by_csv(): + var data_list = get_csv_data() + var examples = [] + for i in range(min(data_list.size(), EXAMPLE_COUNT)): + examples.append(data_list[i]) + _text_box.text = "\n".join(examples) + + +# 更新文本框的内容 +func update_text_box_content(): + _text_box.text = "" + match _button_group.get_pressed_button().name: + "json", "resource": + _update_by_head_row() + "csv": + _update_by_csv() + + + +#============================================================ +# 连接信号 +#============================================================ +func _on_head_line_box_value_changed(value: float) -> void: + update_text_box_content() + + +func _on_export_pressed() -> void: + var extension = str(_button_group.get_pressed_button().name) + if extension == "resource": + var r_class_name = _resource_class_name.text.strip_edges() + if r_class_name == "": + r_class_name = "res_0" + _save_dialog.current_file = r_class_name.to_snake_case() + ".gd" + + else: + _save_dialog.current_file = "new_file." + extension + + _save_dialog.popup_centered_ratio(0.5) + + +func _on_save_dialog_file_selected(path: String) -> void: + print(path) + var data + var type = str(_button_group.get_pressed_button().name) + match type: + "csv": + data = "\n".join(get_csv_data()) + TableDataUtil.Files.save_as_string( path, data ) + + # 导出的文件保持默认文件,不作为翻译文件 + var keep_import_path = path + ".import" + TableDataUtil.Files.save_as_string(keep_import_path, '[remap]\n\nimporter="keep"\n\n') + + "json": + data = get_data_by_head_row() + TableDataUtil.Files.save_as_string( path, _data_format(data) ) + + "resource": + data = get_resources_by_path(path) + var idx : int = 0 + var dir = path.get_base_dir() + var filename = path.get_file().get_basename() + for resource in data: + var file_path = dir.path_join("%s_%002d.tres" % [filename, idx]) + while FileAccess.file_exists(file_path): + idx += 1 + file_path = dir.path_join("%s_%002d.tres" % [filename, idx]) + ResourceSaver.save(resource, file_path) + idx += 1 + + _save_dialog.current_path = path + + self.hide() + print("[ ExportPreview ] 保存数据:", path) + self.exported.emit(path, type, data ) + + +func _on_cancel_pressed(): + self.hide() diff --git a/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/export_preview/export_preview_window.tscn b/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/export_preview/export_preview_window.tscn new file mode 100644 index 0000000..3d5ee97 --- /dev/null +++ b/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/export_preview/export_preview_window.tscn @@ -0,0 +1,181 @@ +[gd_scene load_steps=6 format=3 uid="uid://cqpmbxqgny2kq"] + +[ext_resource type="Script" path="res://addons/table_data_editor/src/table_data_editor/export_preview/export_preview_window.gd" id="1_h8l6h"] + +[sub_resource type="ButtonGroup" id="ButtonGroup_pxfjs"] + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_2ocp7"] + +[sub_resource type="InputEventKey" id="InputEventKey_s3ym3"] +pressed = true +keycode = 4194305 + +[sub_resource type="Shortcut" id="Shortcut_5m7cm"] +events = [SubResource("InputEventKey_s3ym3")] + +[node name="export_preview_window" type="Window"] +title = "Export" +size = Vector2i(800, 400) +wrap_controls = true +exclusive = true +script = ExtResource("1_h8l6h") + +[node name="export_preview" type="MarginContainer" parent="."] +editor_description = "导出预览" +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="Panel" type="Panel" parent="export_preview"] +layout_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="export_preview"] +layout_mode = 2 +theme_override_constants/margin_left = 4 +theme_override_constants/margin_top = 4 +theme_override_constants/margin_right = 4 +theme_override_constants/margin_bottom = 8 + +[node name="VBoxContainer" type="VBoxContainer" parent="export_preview/MarginContainer"] +layout_mode = 2 + +[node name="HSplitContainer" type="HSplitContainer" parent="export_preview/MarginContainer/VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 +split_offset = 200 + +[node name="VBoxContainer" type="VBoxContainer" parent="export_preview/MarginContainer/VBoxContainer/HSplitContainer"] +layout_mode = 2 + +[node name="HBoxContainer" type="HBoxContainer" parent="export_preview/MarginContainer/VBoxContainer/HSplitContainer/VBoxContainer"] +layout_mode = 2 + +[node name="select_items" type="HBoxContainer" parent="export_preview/MarginContainer/VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 + +[node name="csv" type="CheckBox" parent="export_preview/MarginContainer/VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/select_items"] +layout_mode = 2 +button_pressed = true +button_group = SubResource("ButtonGroup_pxfjs") +text = "CSV" + +[node name="json" type="CheckBox" parent="export_preview/MarginContainer/VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/select_items"] +layout_mode = 2 +button_group = SubResource("ButtonGroup_pxfjs") +text = "JSON" + +[node name="resource" type="CheckBox" parent="export_preview/MarginContainer/VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/select_items"] +layout_mode = 2 +button_group = SubResource("ButtonGroup_pxfjs") +text = "Resource" + +[node name="VSeparator" type="VSeparator" parent="export_preview/MarginContainer/VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer"] +layout_mode = 2 +theme_override_constants/separation = 16 + +[node name="selected_item_param" type="HBoxContainer" parent="export_preview/MarginContainer/VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 + +[node name="csv" type="HBoxContainer" parent="export_preview/MarginContainer/VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/selected_item_param"] +layout_mode = 2 +theme_override_constants/separation = 8 + +[node name="Label" type="Label" parent="export_preview/MarginContainer/VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/selected_item_param/csv"] +layout_mode = 2 +text = "delim" + +[node name="LineEdit" type="LineEdit" parent="export_preview/MarginContainer/VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/selected_item_param/csv"] +layout_mode = 2 +text = "," +placeholder_text = "表格间的分隔符,默认为 ," + +[node name="json" type="HBoxContainer" parent="export_preview/MarginContainer/VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/selected_item_param"] +visible = false +layout_mode = 2 + +[node name="Label" type="Label" parent="export_preview/MarginContainer/VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/selected_item_param/json"] +layout_mode = 2 +tooltip_text = "作为 key 的行" +text = "Key Row: " + +[node name="head_line_box" type="SpinBox" parent="export_preview/MarginContainer/VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/selected_item_param/json"] +unique_name_in_owner = true +layout_mode = 2 +tooltip_text = "作为 key 的行" +min_value = 1.0 +max_value = 10000.0 +value = 1.0 + +[node name="compact" type="CheckBox" parent="export_preview/MarginContainer/VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/selected_item_param/json"] +unique_name_in_owner = true +visible = false +layout_mode = 2 +tooltip_text = "紧凑的格式导出,这样JSON会占用空间会最小。数据量不是非常大可以不用勾选" +text = "compact" + +[node name="resource" type="HBoxContainer" parent="export_preview/MarginContainer/VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/selected_item_param"] +visible = false +layout_mode = 2 + +[node name="Label" type="Label" parent="export_preview/MarginContainer/VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/selected_item_param/resource"] +layout_mode = 2 +text = "ClassName" + +[node name="resource_class_name" type="LineEdit" parent="export_preview/MarginContainer/VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/selected_item_param/resource"] +unique_name_in_owner = true +custom_minimum_size = Vector2(100, 0) +layout_mode = 2 +placeholder_text = "Class Name" + +[node name="Label" type="Label" parent="export_preview/MarginContainer/VBoxContainer/HSplitContainer/VBoxContainer"] +modulate = Color(1, 1, 1, 0.462745) +layout_mode = 2 +text = "Sample data in the first few lines..." + +[node name="text_box" type="TextEdit" parent="export_preview/MarginContainer/VBoxContainer/HSplitContainer/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_vertical = 3 +editable = false + +[node name="HBoxContainer" type="HBoxContainer" parent="export_preview/MarginContainer/VBoxContainer"] +layout_mode = 2 + +[node name="Control" type="Control" parent="export_preview/MarginContainer/VBoxContainer/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="export" type="Button" parent="export_preview/MarginContainer/VBoxContainer/HBoxContainer"] +custom_minimum_size = Vector2(100, 32) +layout_mode = 2 +text = "Export" + +[node name="VSeparator" type="VSeparator" parent="export_preview/MarginContainer/VBoxContainer/HBoxContainer"] +layout_mode = 2 +theme_override_constants/separation = 32 +theme_override_styles/separator = SubResource("StyleBoxEmpty_2ocp7") + +[node name="cancel" type="Button" parent="export_preview/MarginContainer/VBoxContainer/HBoxContainer"] +custom_minimum_size = Vector2(80, 0) +layout_mode = 2 +shortcut = SubResource("Shortcut_5m7cm") +text = "Cancel" + +[node name="Control3" type="Control" parent="export_preview/MarginContainer/VBoxContainer/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="save_dialog" type="FileDialog" parent="."] +unique_name_in_owner = true +size = Vector2i(375, 161) +access = 2 +filters = PackedStringArray("*.json; JSON", "*.csv; CSV", "*.tres; TRES", "*.res; RES", "*.gd; GDScript") + +[connection signal="value_changed" from="export_preview/MarginContainer/VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/selected_item_param/json/head_line_box" to="." method="_on_head_line_box_value_changed"] +[connection signal="pressed" from="export_preview/MarginContainer/VBoxContainer/HBoxContainer/export" to="." method="_on_export_pressed"] +[connection signal="pressed" from="export_preview/MarginContainer/VBoxContainer/HBoxContainer/cancel" to="." method="_on_cancel_pressed"] +[connection signal="file_selected" from="save_dialog" to="." method="_on_save_dialog_file_selected"] diff --git a/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/file_data.gd b/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/file_data.gd new file mode 100644 index 0000000..d282f53 --- /dev/null +++ b/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/file_data.gd @@ -0,0 +1,106 @@ +#============================================================ +# File Data +#============================================================ +# - author: zhangxuetu +# - datetime: 2023-03-23 23:30:46 +# - version: 4.0 +#============================================================ +## 当前文件数据 +## +##新建/打开/存储 处理其中的数据。 +##[br]文件相关的数据都在这里保存,加载存储获取都在这里,这样保证数据不会乱 +class_name TableDataEditor_FileData + + +## 原始数据 +var origin_data : Dictionary +# 数据格式版本 +var version: + get: return 1_3_0 +# 数据集 +var data_set = TableDataEditor_TableDataSet.new(): + set(v): + data_set = TableDataEditor_TableDataSet.new(v) \ + if v is Dictionary \ + else v +## 列宽数据 +var column_width : Dictionary = {} +## 行高数据 +var row_height : Dictionary = {} +## 编辑框大小 +var edit_dialog_size : Vector2 = Vector2(100, 50) +## 文件路径 +var path : String = "" + + +#============================================================ +# SetGet +#============================================================ +func is_empty() -> bool: + return origin_data.is_empty() + +func is_new_file() -> bool: + return path.is_empty() + + +#============================================================ +# 内置 +#============================================================ +func _init(origin_data: Dictionary): + self.origin_data = origin_data + + # 加载旧版本数据 + var version = origin_data.get("version") + if not ((version is float or version is int) and version >= 1.3): + var grid_data : Dictionary = origin_data.get("grid_data", {}) + for coords in grid_data: + self.data_set.set_value(coords, grid_data[coords]) + + # 根据字典数据设置当前对象属性 + TableDataUtil.Classes.set_property_by_dict(self, origin_data) + + +#============================================================ +# 自定义 +#============================================================ +## 加载文件。 +static func load_file(path: String) -> TableDataEditor_FileData: + if FileAccess.file_exists(path): + var data = TableDataUtil.Files.load_file(path) + if not data is Dictionary: + var reader = FileAccess.open(path, FileAccess.READ) + if reader: + data = reader.get_var() + + if not data is Dictionary: + data = {} + + var file_data = TableDataEditor_FileData.new(data) + file_data.path = path + return file_data + else: + return TableDataEditor_FileData.new({}) + + +## 保存当前文件的数据 +##[br] +##[br][code]saved_path[/code] 保存到的路径,默认不传参数保存为当前路径, +##如果当前没有设置路径则会报错,所以第一次要传入路径进行保存 +func save_data(saved_path: String = "") -> bool: + if saved_path == "": + saved_path = self.path + self.path = saved_path + + assert(self.path.strip_edges() != "", "当前没有设置文件路径") + + # 获取数据 + var data : Dictionary = {} + var propertys = TableDataUtil.Classes.get_propertys(self.get_script()) + for property in propertys: + if property in self: + data[property] = self[property] + + data["data_set"] = data_set.get_config_data() + + return TableDataUtil.Files.save_data(self.path, data) + diff --git a/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/menu_list.gd b/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/menu_list.gd new file mode 100644 index 0000000..1378434 --- /dev/null +++ b/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/menu_list.gd @@ -0,0 +1,331 @@ +#============================================================ +# Menu List +#============================================================ +# - author: zhangxuetu +# - datetime: 2022-11-27 01:01:10 +# - version: 4.0 +#============================================================ +## 菜单列表 +@tool +class_name MenuList +extends MenuBar + + +## 菜单被点击 +##[br] +##[br][code]idx[/code] 菜单的索引值 +##[br][code]menu_path[/code] 菜单路径 +signal menu_pressed(idx: int, menu_path: StringName) +## 复选框状态发生切换 +signal menu_check_toggled(idx: int, menu_path: StringName) + + +# 自动增长的菜单 idx。用以下面添加菜单项时记录添加的菜单的 idx +var _auto_increment_menu_idx := -1 +# 菜单路径对应 PopupMenu +var _menu_path_to_popup_menu_map := {} +# 菜单的 idx 对应的菜单路径 +var _idx_to_menu_path_map := {} +# 菜单路径对应的菜单 idx +var _menu_path_to_idx_map := {} +# 子节点路径 +var _child_menu_path_idx_list := {} + + +#===================================================== +# Set/Get +#===================================================== +## 获取弹窗菜单 +##[br] +##[br][code]menu_path[/code] 这个菜单路径的父弹窗菜单节点 +func get_menu(menu_path: StringName) -> PopupMenu: + return _menu_path_to_popup_menu_map.get(menu_path) as PopupMenu + + +## 添加快捷键 +##[br] +##[br][code]menu_path[/code] 菜单路径 +##[br][code]data[/code] 快捷键数据,示例数据:ctrl + shift + C 快捷键 +##[codeblock] +##{ +## "ctrl": true, +## "shift": true, +## "keycode": KEY_C, +##} +##[/codeblock] +func set_menu_shortcut(menu_path: StringName, data: Dictionary): + var shortcut = Shortcut.new() + var input = InputEventKey.new() + shortcut.events.append(input) + input.keycode = data.get("keycode", 0) + input.ctrl_pressed = data.get("ctrl", false) + input.alt_pressed = data.get("alt", false) + input.shift_pressed = data.get("shift", false) + + var popup_menu := _menu_path_to_popup_menu_map.get(menu_path) as PopupMenu + var menu_name = menu_path.get_file() + if popup_menu: + for i in popup_menu.item_count: + if popup_menu.get_item_text(i) == menu_name: + popup_menu.set_item_shortcut(i, shortcut) + break + + + +# 获取菜单的 ID +func _get_menu_id(menu_path: String) -> int: + return _menu_path_to_idx_map.get(menu_path, -1) + +func _execute_menu_by_path(menu_path: StringName, method_name: String, params: Array = []): + var menu = get_menu(menu_path) + var idx = get_menu_idx(menu_path) + if menu and idx > 0: + return menu.call(method_name, params) + return null + +## 设置菜单的可用性 +func set_menu_disabled_by_path(menu_path: StringName, value: bool): + var menu = get_menu(menu_path) + var idx = get_menu_idx(menu_path) + if menu and idx > -1: + menu.set_item_disabled(idx, value) + +func set_menu_as_checkable(menu_path: StringName, value: bool): + var menu = get_menu(menu_path) + var idx = get_menu_idx(menu_path) + if menu and idx > 0: + menu.set_item_as_checkable(idx, value) + +func set_menu_check_by_path(menu_path: StringName, value: bool): + var menu = get_menu(menu_path) + var idx = get_menu_idx(menu_path) + if menu and idx > 0 and menu.is_item_checked(idx) != value: + menu.set_item_checked(idx, value) + self.menu_check_toggled.emit(idx, menu_path) + +func get_menu_check_by_path(menu_path: StringName) -> bool: + var menu = get_menu(menu_path) + var idx = get_menu_idx(menu_path) + if menu and idx > 0: + return menu.is_item_checked(idx) + return false + +func toggle_menu_check_by_path(menu_path: StringName) -> bool: + return _execute_menu_by_path(menu_path, "is_item_checked", []) + +## 获取这个菜单的索引,如果不存在这个菜单,则返回 [code]-1[/code] +func get_menu_idx(menu_path: StringName) -> int: + var id = _menu_path_to_idx_map.get(menu_path, -1) + var menu = get_menu(menu_path) + if menu == null: + var parent_path = get_parent_menu_path(menu_path) + menu = get_menu(parent_path) + return menu.get_item_index(id) + + +## 获取这个索引的菜单路径 +func get_menu_path(menu_path: StringName) -> StringName: + var idx = get_menu_idx(menu_path) + return _idx_to_menu_path_map.get(idx, "") + + +## 是否有这个菜单路径 +func has_menu_path(menu_path: StringName) -> bool: + return _menu_path_to_popup_menu_map.has(menu_path) + + +## 获取父菜单路径 +func get_parent_menu_path(menu_path: StringName) -> StringName: + var idx : int = _menu_path_to_idx_map.get(menu_path, -1) + if idx == -1: + return "" + for parent_path in _child_menu_path_idx_list: + var list = _child_menu_path_idx_list[parent_path] as Array + if list.has(idx): + return parent_path + return "" + + +#===================================================== +# 自定义方法 +#===================================================== +## 初始化菜单。示例: +## [codeblock] +## init_menu({ +## "File": ["Open", "Save", "Save As", { +## "Export As...": [ "Export PNG", "Export JPG" ] +## } ], +## "item": { +## "letter": ["a", "b", "c"], +## "number": [ "1", "2"], +## }, +## }) +## [/codeblock] +func init_menu(data: Dictionary): + add_menu(data, "/") + + +## 初始化快捷键,需要添加对应菜单。示例: +##[codeblock] +##{ +## "/File/Open": {"keycode": KEY_O, "ctrl": true}, +## "/File/Save": {"keycode": KEY_S, "ctrl": true}, +##} +##[/codeblock] +func init_shortcut(data_list: Dictionary): + var data : Dictionary + for menu_path in data_list: + data = data_list[menu_path] + set_menu_shortcut(menu_path, data) + + +## 添加菜单项 +##[br] +##[br][code]menu_data[/code] 这个菜单项包含的数据 +##[br][code]parent_menu_path[/code] 父级菜单路径 +func add_menu(menu_data, parent_menu_path: StringName): + var parent_popup_menu : PopupMenu = get_menu(parent_menu_path) + + _auto_increment_menu_idx += 1 + + # 不是根路径时 + if parent_menu_path != "/": + # Dictionary + if menu_data is Dictionary: + for menu_name in menu_data: + add_menu( menu_data[menu_name], parent_menu_path.path_join(menu_name)) + + # Array + elif menu_data is Array: + for data in menu_data: + add_menu(data, parent_menu_path) + + # String + elif menu_data is String or menu_data is StringName: + # 添加菜单 + if not _menu_path_to_popup_menu_map.has(parent_menu_path): + create_menu(parent_menu_path, null) + parent_popup_menu = get_menu(parent_menu_path) + # 不是 Array 和 Dictionary 类型时,只能是 String 类型了 + var menu_name := StringName(menu_data) + if not menu_name.begins_with("-"): + var menu_path := "%s/%s" % [parent_menu_path, menu_name] + # 添加菜单项 + parent_popup_menu.add_item(menu_name, _auto_increment_menu_idx) + _idx_to_menu_path_map[_auto_increment_menu_idx] = menu_path + _menu_path_to_idx_map[menu_path] = _auto_increment_menu_idx + _menu_path_to_popup_menu_map[menu_path] = parent_popup_menu + if not _child_menu_path_idx_list.has(parent_menu_path): + _child_menu_path_idx_list[parent_menu_path] = [] + _child_menu_path_idx_list[parent_menu_path].append(_auto_increment_menu_idx) + + else: + parent_popup_menu.add_separator() + + else: + assert(false, "错误的数据类型:" + str(typeof(menu_data)) ) + + else: + + # 根菜单按钮 + for menu_name in menu_data: + # 添加菜单按钮 + + var menu := PopupMenu.new() + menu.name = menu_name + add_child(menu) + + # 设置属性 + var menu_path = parent_menu_path.path_join(menu_name) + _set_popup_menu(menu_path, menu) + + # 添加这个按钮菜单的子菜单 + add_menu(menu_data[menu_name], menu_path) + + +## 移除菜单 +func remove_menu(menu_path: StringName) -> bool: + if has_menu_path(menu_path): + # 移除子菜单及其数据 + var child_menu_idx_list = _child_menu_path_idx_list.get(menu_path, []) + for child_menu_idx in child_menu_idx_list: + var child_menu_path = get_menu_idx(child_menu_idx) + if has_menu_path(child_menu_path): + remove_menu(child_menu_path) + + # 移除自身数据 + var idx = get_menu_idx(menu_path) + + # 移除菜单节点 + var menu = get_menu(menu_path) as PopupMenu + if menu != null: + menu.queue_free() + else: + var parent_menu_path = get_parent_menu_path(menu_path) + var parent_menu = get_menu(parent_menu_path) + parent_menu.remove_item(idx) + + _menu_path_to_popup_menu_map.erase(menu_path) + _menu_path_to_idx_map.erase(menu_path) + _idx_to_menu_path_map.erase(idx) + return true + return false + + +## 清空菜单 +func clear_menu(menu_path: StringName) -> bool: + if has_menu_path(menu_path): + var menu = get_menu(menu_path) + if menu != null: + menu.clear() + return true + return false + + +## 创建菜单 +##[br] +##[br][code]menu_path[/code] 菜单路径 +##[br][code]parent_menu[/code] 父级菜单 +func create_menu(menu_path: StringName, parent_menu: PopupMenu): + # 切分菜单名 + var parent_menu_names := menu_path.split("/") + # 因为切分后 0 索引都是空字符串,所以移除 + parent_menu_names.remove_at(0) + + # 逐个添加菜单 + parent_menu = get_menu("/" + "/".join(parent_menu_names.slice(0, 1))) + for i in parent_menu_names.size(): + var sub_menu_path = "/" + "/".join(parent_menu_names.slice(0, i + 1)) + # 没有这个菜单则添加 + if not _menu_path_to_popup_menu_map.has(sub_menu_path): + var menu_name = parent_menu_names[i] + var menu_popup = _create_popup_menu(sub_menu_path) + _menu_path_to_popup_menu_map[sub_menu_path] = menu_popup + parent_menu.add_child(menu_popup) + parent_menu.add_submenu_item( menu_name, menu_name ) + # 开始记录这个菜单,用以这个菜单的下一级别的菜单 + parent_menu = get_menu(sub_menu_path) + + + +#===================================================== +# 连接信号 +#===================================================== +# 创建这个路径的菜单 +func _create_popup_menu(path: StringName) -> PopupMenu: + var menu_popup = PopupMenu.new() + menu_popup.name = path.get_file() + _set_popup_menu(path, menu_popup) + return menu_popup + + +# 设置菜单属性 +func _set_popup_menu(menu_path: StringName, menu_popup: PopupMenu): + self._menu_path_to_popup_menu_map[menu_path] = menu_popup + # 点击菜单时 + menu_popup.id_pressed.connect(func(id): + self.menu_pressed.emit(id, _idx_to_menu_path_map[id]) + ) + + + diff --git a/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_data_editor.gd b/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_data_editor.gd new file mode 100644 index 0000000..e533e12 --- /dev/null +++ b/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_data_editor.gd @@ -0,0 +1,456 @@ +#============================================================ +# Json Editor +#============================================================ +# - datetime: 2022-11-27 01:31:14 +#============================================================ +## JSON 数据编辑器 +## +##这里将各独立的组件组合起来,功能整合 +@tool +class_name TableDataEditor +extends MarginContainer + + +## 没有保存文件时的颜色 +const NOT_SAVED_COLOR = Color(1, 0.65, 0.275, 1) +## 保存文件后的颜色 +const SAVED_COLOR = Color(1, 1, 1, 0.625) +## 最大显示的最近打开的文件数量 +const RECENTLY_OPEND_MAX_COUNT = 10 + + +## 文件弹窗文件过滤 +const FILTERS = ["*.gdata; GData"] +## 菜单项数据 +const MENU_ITEM : Dictionary = { + "File": [ + "New", "Open", {"Recently Opened": ["/"]}, "-", + "Save", "Save As...", "-", + "Export...", + "Import...", + ], + "Edit": [ + "Undo", "Redo", "-", + "Double click edit" + ], + "Help": ["Help"], +} +## 菜单快捷键 +const MENU_SHORTCUT : Dictionary = { + "/File/New": { "keycode": KEY_N, "ctrl": true }, + "/File/Open": { "keycode": KEY_O, "ctrl": true }, + "/File/Save": { "keycode": KEY_S, "ctrl": true }, + "/File/Save As...": { "keycode": KEY_S, "ctrl": true, "shift": true }, + "/File/Export...": { "keycode": KEY_E, "ctrl": true }, + "/File/Import...": { "keycode": KEY_I, "ctrl": true }, + "/Edit/Undo": {"keycode": KEY_Z, "ctrl": true}, + "/Edit/Redo": {"keycode": KEY_Z, "ctrl": true, "shift": true}, +} +const MENU_CHECKABLE : Array = [ + "/Edit/Double click edit", +] + + +## 创建新的文件 +signal created_file(path: String) + + +# 保存到的文件路径 +var _saved_path : String = "" : + set(v): + _saved_path = v + _file_path_label.text = _saved_path +# 是否已保存 +var _saved: bool = true: + set(v): + _saved = v + if _saved_status_label == null: + await ready + if _saved: + _saved_status_label.text = "(saved)" + _saved_status_label.self_modulate = SAVED_COLOR + else: + _saved_status_label.text = "(unsaved)" + _saved_status_label.self_modulate = NOT_SAVED_COLOR + +# 行映射。记录哪些行有数据 +var _has_value_row_map := {} +# 列映射。记录哪些列有数据 +var _has_value_column_map := {} +# 撤销重做 +var _undo_redo : UndoRedo = UndoRedo.new() + +# 上次打开的文件路径 +var _dialog_path : String = "" +# 是否已加载完成 +var _is_reloaded := false : + set(v): + if _is_reloaded == false: + _is_reloaded = v + + +var __init_node = InjectUtil.auto_inject(self, "_") + + +var _table_edit : TableEdit # 编辑表格节点 +var _menu_list : MenuList # 菜单列表 +var _scroll_pos : LineEdit # 滚动条位置输入框 +var _pages : ItemList # 切换页面(暂未开始实现功能) +var _export_preview_window : ExportPreviewWindow +var _confirm_dialog : ConfirmationDialog +var _tooltip_dialog : AcceptDialog +var _save_as_dialog : FileDialog +var _open_file_dialog : FileDialog +var _import_dialog : FileDialog +var _saved_status_label : Label +var _file_path_label : Label +var _prompt_message : Label +var _prompt_message_player : AnimationPlayer + + +var file_data := TableDataEditor_FileData.new({}) +var cache_data := TableDataEditor_CacheData.instance() + + + +#============================================================ +# SetGet +#============================================================ +## 获取编辑表格对象 +func get_table_edit() -> TableEdit: + return _table_edit + + +#============================================================ +# 内置 +#============================================================ +func _ready() -> void: + file_data = TableDataEditor_FileData.new({}) + cache_data = TableDataEditor_CacheData.instance() + + (func(): + _saved_path = "" + + _init_dialog() + _init_menu() + + _load_last_cache_data() + + _is_reloaded = true + + ).call_deferred() + + +func _exit_tree(): + if not Engine.is_editor_hint() or TableDataUtil.Editor.is_enabled(): + cache_data.save_data() + + +#============================================================ +# 私有方法 +#============================================================ +# 新建文件 +func _new_file() -> void: + load_file_path("") + + +# 初始化菜单列表 +func _init_menu(): + # TODO: 最近打开的文件替换增加数据 + _menu_list.init_menu(MENU_ITEM) + # 设置快捷键 + _menu_list.init_shortcut(MENU_SHORTCUT) + + _menu_list.set_menu_disabled_by_path("/Edit/Undo", true) + _menu_list.set_menu_disabled_by_path("/Edit/Redo", true) + + for menu_path in MENU_CHECKABLE: + _menu_list.set_menu_as_checkable(menu_path, true) + _menu_list.set_menu_check_by_path("/Edit/Double click edit", true) + + +# 初始化弹窗 +func _init_dialog(): + + # 数据导出预览 + _export_preview_window.close_requested.connect( func(): _export_preview_window.visible = false ) + + # 添加文件类型var FILTERS = ["*.gdata; GData"] + _open_file_dialog.filters = FILTERS + _save_as_dialog.filters = FILTERS + _import_dialog.filters = ["*.csv; CSV"] + + # 打开窗口的路径位置 + var callable = func(dialog: FileDialog): + if dialog.current_dir != _dialog_path: + _dialog_path = dialog.current_dir + _open_file_dialog.visibility_changed.connect(callable.bind(_open_file_dialog)) + _save_as_dialog.visibility_changed.connect(callable.bind(_save_as_dialog)) + + +# 加载上次缓存的数据 +func _load_last_cache_data(): + for dialog in [_open_file_dialog, _save_as_dialog, _import_dialog]: + dialog.current_dir = cache_data.dialog_path + dialog.visibility_changed.connect(func(): + if not dialog.visible: + cache_data.dialog_path = dialog.current_dir + ) + + if cache_data.exists_opened_path(): + load_file_path(cache_data.last_operation_path) + + # 添加打开过的路径 + const RECENTLY_OPEND_MENU = "/File/Recently Opened" + var list : Array = cache_data.get_recently_opend_paths() + list.reverse() + for idx in range(min(list.size(), RECENTLY_OPEND_MAX_COUNT)): + var path = list[idx] + if FileAccess.file_exists(path): + _menu_list.add_menu(path, RECENTLY_OPEND_MENU) + + +#============================================================ +# 自定义 +#============================================================ +## 显示提示信息 +func display_prompt_message(message: String, color: Color = Color.WHITE) -> void: + _prompt_message.text = message + _prompt_message.modulate = color + _prompt_message_player.stop() + _prompt_message_player.play("flicker") + + +## 加载路径的数据 +##[br] +##[br][code]path[/code] 加载这个路径的数据,如果为空字符串,则是为临时数据,保存时会弹窗保存位置 +func load_file_path(path: String): + # 这个文件的数据 + load_file_data(TableDataEditor_FileData.load_file(path)) + + _saved_path = path + + cache_data.update_last_operation_path(_saved_path) + cache_data.save_data() + + +## 加载文件数据 +##[br] +##[br][code]file_data[/code] 加载文件数据 +func load_file_data(file_data: TableDataEditor_FileData): + self.file_data = file_data + + # 加载到表格中 + (func(): + _table_edit.row_to_height_map = file_data.row_height + _table_edit.column_to_width_map = file_data.column_width + _table_edit.data_set = file_data.data_set + + _table_edit.get_edit_dialog().box_size = file_data.edit_dialog_size + _table_edit.update_cell_list() + ).call_deferred() + + # 其他 + _saved = true + _saved_path = "" + + _undo_redo.clear_history() + _menu_list.set_menu_disabled_by_path("/Edit/Undo", true) + _menu_list.set_menu_disabled_by_path("/Edit/Redo", true) + + _table_edit.get_edit_dialog().showed = false + + +## 保存数据到这个路径中 +func save_data_to(path: String): + if file_data.save_data(path): + # 保存成功,则进行处理 + self._saved = true + self._saved_path = path + cache_data.update_last_operation_path(path) + cache_data.save_data() + + self.created_file.emit(path) + + display_prompt_message("保存成功. %s" % [Time.get_datetime_string_from_system().replace("T", " ")]) + print("[ TableDataEditor ] 保存成功 ", Time.get_datetime_string_from_system()) + + else: + display_prompt_message("保存失败") + printerr("[ TableDataEditor ] 保存失败") + + +## 保存为 JSON +func save_as_json(path: String): + var data = _table_edit.data_set.get_origin_data() + TableDataUtil.Files.save_as_string(path, data) + self.created_file.emit(path) + + +## 显示保存 Dialog +func show_save_dialog(default_file_name: String = ""): + if default_file_name != "": + _save_as_dialog.current_file = default_file_name + _save_as_dialog.popup_centered_ratio(0.5) + + +## 导入文件 +func import_file(path: String): + const FILE_TYPE = ["csv"] + if not path.get_extension() in FILE_TYPE: + display_prompt_message("错误的文件类型:%s。暂不支持 %s 以外的文件类型" % [ + path.get_extension(), FILE_TYPE + ]) + assert(false, "错误的文件类型") + + # 加载 csv数据 到 数据集 中 + var data_set = TableDataEditor_TableDataSet.new() + var csv_lines = TableDataUtil.Files.read_csv_file(path) + + var line : PackedStringArray + for row in csv_lines.size(): + line = csv_lines[row] + for column in line.size(): + data_set.set_value(Vector2i(column, row) + Vector2i.ONE, line[column]) + + # 加载数据 + var tmp_file_data = file_data.load_file("") + tmp_file_data.data_set = data_set + load_file_data(tmp_file_data) + + cache_data.dialog_path = path + cache_data.save_data() + + + +#============================================================ +# 连接信号 +#============================================================ +func _on_table_edit_cell_value_changed(cell: InputCell, coords: Vector2i, previous: String, value: String): + _saved = false + +# print("[ TableDataEditor ] 单元格发生改变") + + # 记录存在有数据的行列 + _undo_redo.create_action("修改单元格的值") + _undo_redo.add_do_method( _table_edit.alter_value.bind(coords, value, false) ) + _undo_redo.add_do_method( _table_edit.update_cell_list ) + _undo_redo.add_undo_method( _table_edit.alter_value.bind(coords, previous, false) ) + _undo_redo.add_undo_method( _table_edit.update_cell_list ) + _undo_redo.commit_action() + + # 撤销可用性 + _menu_list.set_menu_disabled_by_path("/Edit/Undo", false) + + +func _on_table_edit_scroll_changed(coords: Vector2i): + _scroll_pos.text = str(coords) + + +func _on_scroll_pos_text_submitted(new_text): + var re = RegEx.new() + re.compile("(\\d+)\\s*,\\s*(\\d+)") + var result = re.search(new_text) + if result == null: + return + + var pos = str_to_var("Vector2i(%s, %s)" % [result.get_string(1), result.get_string(2)]) + if pos is Vector2i: + _table_edit.scroll_to(pos) + print("[ TableDataEditor ] 跳转到位置:", pos) + + +func _on_menu_list_menu_pressed(idx, menu_path: StringName): +# print_debug("[ TableDataEditor ] 点击菜单 ", menu_path) + + match menu_path: + "/File/New": + if not _saved: + _confirm_dialog.dialog_text = "当前还没有保存,是否要继续创建?" + _confirm_dialog.popup_centered() + else: + _new_file() + + "/File/Open": + _open_file_dialog.popup_centered_ratio(0.5) + + "/File/Save": + if _saved_path == "": + show_save_dialog() + else: + save_data_to(_saved_path) + + "/File/Save As...": + show_save_dialog("new_file.gdata") + + "/File/Export...": + _export_preview_window.popup_centered_ratio(0.5) + _export_preview_window.update_text_box_content() + + "/File/Import...": + _import_dialog.popup_centered_ratio(0.5) + + "/Edit/Undo": + _undo_redo.undo() + _menu_list.set_menu_disabled_by_path("/Edit/Undo", not _undo_redo.has_undo()) + _menu_list.set_menu_disabled_by_path("/Edit/Redo", false) + + "/Edit/Redo": + _undo_redo.redo() + _menu_list.set_menu_disabled_by_path("/Edit/Redo", not _undo_redo.has_redo()) + _menu_list.set_menu_disabled_by_path("/Edit/Undo", false) + + "/Edit/Double click edit": + var status = _menu_list.get_menu_check_by_path("/Edit/Double click edit") + _menu_list.set_menu_check_by_path("/Edit/Double click edit", not status) + _table_edit.double_click_edit = not status + + "/Help/Help": + _tooltip_dialog.popup_centered() + + _: + # 最近打开的文件 + if menu_path.contains("/File/Recently Opened"): + var file_path = menu_path.trim_prefix("/File/Recently Opened/") + if FileAccess.file_exists(file_path): + load_file_path(file_path) + + else: + printerr("文件不存在") + _menu_list.remove_menu(menu_path) + + +func _on_save_as_dialog_file_selected(path): + _saved_path = path + match _saved_path.get_extension(): + "gdata": + save_data_to( _saved_path ) + "json": + save_as_json( _saved_path ) + _: + display_prompt_message("错误的文件类型:%s" % [ _saved_path.get_extension() ]) + printerr("[ TableDataEditor ] ", _saved_path.get_extension()) + + +func _on_export_preview_window_exported(path, type, data): + _export_preview_window.visible = false + display_prompt_message("已导出 %s 资源" % [type]) + self.created_file.emit(path) + + +func _on_open_file_dialog_file_selected(path: String): + cache_data.dialog_path = path.get_base_dir() + load_file_path(path) + + +func _on_file_path_label_gui_input(event): + if event is InputEventMouseButton: + if event.button_index == MOUSE_BUTTON_LEFT and event.double_click: + if _saved_path != "" and DirAccess.dir_exists_absolute(_saved_path.get_base_dir()): + var path = TableDataUtil.Files.get_absolute_path(_saved_path) + OS.shell_open(path.get_base_dir()) + + +func _on_table_edit_popup_edit_box_size_changed(box_size): + file_data.edit_dialog_size = box_size + diff --git a/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_data_editor.tscn b/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_data_editor.tscn new file mode 100644 index 0000000..120a8dd --- /dev/null +++ b/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_data_editor.tscn @@ -0,0 +1,233 @@ +[gd_scene load_steps=8 format=3 uid="uid://68vjwfxquvlf"] + +[ext_resource type="Script" path="res://addons/table_data_editor/src/table_data_editor/table_data_editor.gd" id="1_j3oce"] +[ext_resource type="Script" path="res://addons/table_data_editor/src/table_data_editor/menu_list.gd" id="2_o7gjv"] +[ext_resource type="PackedScene" uid="uid://ctppgkl2dpksd" path="res://addons/table_data_editor/src/table_data_editor/table_edit/table_edit.tscn" id="3_87xax"] +[ext_resource type="PackedScene" uid="uid://cqpmbxqgny2kq" path="res://addons/table_data_editor/src/table_data_editor/export_preview/export_preview_window.tscn" id="4_8p413"] + +[sub_resource type="Animation" id="Animation_jmtv6"] +length = 0.001 + +[sub_resource type="Animation" id="Animation_lfmqt"] +resource_name = "flicker" +length = 8.0 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath(".:modulate:a") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 5, 8), +"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1), +"update": 0, +"values": [0.0, 0.55, 1.0, 0.55, 1.0, 0.55, 1.0, 0.55, 1.0, 1.0, 0.0] +} + +[sub_resource type="AnimationLibrary" id="AnimationLibrary_xid6c"] +_data = { +"RESET": SubResource("Animation_jmtv6"), +"flicker": SubResource("Animation_lfmqt") +} + +[node name="table_data_editor" type="MarginContainer"] +clip_contents = true +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_override_constants/margin_left = 2 +theme_override_constants/margin_top = 2 +theme_override_constants/margin_right = 2 +theme_override_constants/margin_bottom = 2 +script = ExtResource("1_j3oce") + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +layout_mode = 2 + +[node name="menu_list" type="MenuBar" parent="VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +focus_mode = 1 +flat = true +script = ExtResource("2_o7gjv") + +[node name="GridContainer" type="HSplitContainer" parent="VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 +split_offset = 120 + +[node name="pages" type="ItemList" parent="VBoxContainer/GridContainer"] +unique_name_in_owner = true +visible = false +layout_mode = 2 + +[node name="table_edit" parent="VBoxContainer/GridContainer" instance=ExtResource("3_87xax")] +unique_name_in_owner = true +layout_mode = 2 +size_flags_vertical = 3 + +[node name="PanelContainer" type="PanelContainer" parent="VBoxContainer"] +layout_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="VBoxContainer/PanelContainer"] +layout_mode = 2 +theme_override_constants/margin_left = 4 +theme_override_constants/margin_top = 2 +theme_override_constants/margin_right = 4 +theme_override_constants/margin_bottom = 2 + +[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/PanelContainer/MarginContainer"] +layout_mode = 2 + +[node name="scroll_pos" type="LineEdit" parent="VBoxContainer/PanelContainer/MarginContainer/HBoxContainer"] +unique_name_in_owner = true +custom_minimum_size = Vector2(100, 0) +layout_mode = 2 +focus_mode = 1 +text = "(1, 1)" +alignment = 1 +select_all_on_focus = true + +[node name="MarginContainer3" type="MarginContainer" parent="VBoxContainer/PanelContainer/MarginContainer/HBoxContainer"] +layout_mode = 2 +theme_override_constants/margin_left = 2 +theme_override_constants/margin_top = 4 +theme_override_constants/margin_right = 2 +theme_override_constants/margin_bottom = 4 + +[node name="Panel" type="Panel" parent="VBoxContainer/PanelContainer/MarginContainer/HBoxContainer/MarginContainer3"] +custom_minimum_size = Vector2(1, 0) +layout_mode = 2 + +[node name="Control" type="HBoxContainer" parent="VBoxContainer/PanelContainer/MarginContainer/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +focus_mode = 1 + +[node name="prompt_message" type="Label" parent="VBoxContainer/PanelContainer/MarginContainer/HBoxContainer/Control"] +unique_name_in_owner = true +modulate = Color(1, 1, 1, 0) +layout_mode = 2 +text = "(Prompt Message)" + +[node name="prompt_message_player" type="AnimationPlayer" parent="VBoxContainer/PanelContainer/MarginContainer/HBoxContainer/Control/prompt_message"] +unique_name_in_owner = true +libraries = { +"": SubResource("AnimationLibrary_xid6c") +} + +[node name="MarginContainer2" type="MarginContainer" parent="VBoxContainer/PanelContainer/MarginContainer/HBoxContainer"] +layout_mode = 2 +theme_override_constants/margin_left = 2 +theme_override_constants/margin_top = 4 +theme_override_constants/margin_right = 2 +theme_override_constants/margin_bottom = 4 + +[node name="Panel" type="Panel" parent="VBoxContainer/PanelContainer/MarginContainer/HBoxContainer/MarginContainer2"] +custom_minimum_size = Vector2(1, 0) +layout_mode = 2 + +[node name="file_path_label" type="Label" parent="VBoxContainer/PanelContainer/MarginContainer/HBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +tooltip_text = "双击打开所在文件目录" +focus_mode = 1 +mouse_filter = 0 +text = "res://addons/table_data_editor/column_width_test.gdata" + +[node name="MarginContainer" type="MarginContainer" parent="VBoxContainer/PanelContainer/MarginContainer/HBoxContainer"] +layout_mode = 2 +theme_override_constants/margin_left = 2 +theme_override_constants/margin_top = 4 +theme_override_constants/margin_right = 2 +theme_override_constants/margin_bottom = 4 + +[node name="Panel" type="Panel" parent="VBoxContainer/PanelContainer/MarginContainer/HBoxContainer/MarginContainer"] +custom_minimum_size = Vector2(1, 0) +layout_mode = 2 + +[node name="saved_status_label" type="Label" parent="VBoxContainer/PanelContainer/MarginContainer/HBoxContainer"] +unique_name_in_owner = true +self_modulate = Color(1, 1, 1, 0.625) +layout_mode = 2 +size_flags_horizontal = 8 +focus_mode = 1 +text = "(saved)" + +[node name="export_preview_window" parent="." instance=ExtResource("4_8p413")] +unique_name_in_owner = true +position = Vector2i(0, -500) +visible = false +_table_data_editor = NodePath("..") + +[node name="confirm_dialog" type="ConfirmationDialog" parent="."] +unique_name_in_owner = true + +[node name="tooltip_dialog" type="AcceptDialog" parent="."] +unique_name_in_owner = true +gui_embed_subwindows = true +title = "Help" +size = Vector2i(500, 317) + +[node name="MarginContainer" type="MarginContainer" parent="tooltip_dialog"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 8.0 +offset_top = 8.0 +offset_right = -8.0 +offset_bottom = -49.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="Label" type="Label" parent="tooltip_dialog/MarginContainer"] +layout_mode = 2 +size_flags_vertical = 0 +text = "双击单元格进行编辑 + +按住 Alt 键进行左右滚动,松开即可上下滚动 + +按住 Tab 键或 Enter 键进行切换到下一个编辑的单元格,如果同时按下 Shift 则是切换到上一个单元格 + + +" + +[node name="save_as_dialog" type="FileDialog" parent="."] +unique_name_in_owner = true +filters = PackedStringArray("*.gdata; GData") + +[node name="open_file_dialog" type="FileDialog" parent="."] +unique_name_in_owner = true +title = "Open a File" +size = Vector2i(312, 157) +ok_button_text = "打开" +file_mode = 0 +filters = PackedStringArray("*.gdata; GData") + +[node name="import_dialog" type="FileDialog" parent="."] +unique_name_in_owner = true +title = "Open a File" +size = Vector2i(295, 161) +ok_button_text = "打开" +file_mode = 0 +access = 2 +filters = PackedStringArray("*.csv; CSV") + +[connection signal="menu_pressed" from="VBoxContainer/menu_list" to="." method="_on_menu_list_menu_pressed"] +[connection signal="cell_value_changed" from="VBoxContainer/GridContainer/table_edit" to="." method="_on_table_edit_cell_value_changed"] +[connection signal="popup_edit_box_size_changed" from="VBoxContainer/GridContainer/table_edit" to="." method="_on_table_edit_popup_edit_box_size_changed"] +[connection signal="scroll_changed" from="VBoxContainer/GridContainer/table_edit" to="." method="_on_table_edit_scroll_changed"] +[connection signal="text_submitted" from="VBoxContainer/PanelContainer/MarginContainer/HBoxContainer/scroll_pos" to="." method="_on_scroll_pos_text_submitted"] +[connection signal="gui_input" from="VBoxContainer/PanelContainer/MarginContainer/HBoxContainer/file_path_label" to="." method="_on_file_path_label_gui_input"] +[connection signal="exported" from="export_preview_window" to="." method="_on_export_preview_window_exported"] +[connection signal="confirmed" from="confirm_dialog" to="." method="_new_file"] +[connection signal="file_selected" from="save_as_dialog" to="." method="_on_save_as_dialog_file_selected"] +[connection signal="file_selected" from="open_file_dialog" to="." method="_on_open_file_dialog_file_selected"] +[connection signal="file_selected" from="import_dialog" to="." method="import_file"] diff --git a/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/cell/base_cell_element.gd b/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/cell/base_cell_element.gd new file mode 100644 index 0000000..74ee159 --- /dev/null +++ b/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/cell/base_cell_element.gd @@ -0,0 +1,13 @@ +#============================================================ +# Base Cell Element +#============================================================ +# - datetime: 2022-11-26 22:25:48 +#============================================================ +## 基础的单元格元素 +@tool +class_name BaseCellElement +extends MarginContainer + + + + diff --git a/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/cell/base_cell_element.tscn b/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/cell/base_cell_element.tscn new file mode 100644 index 0000000..5f02d1f --- /dev/null +++ b/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/cell/base_cell_element.tscn @@ -0,0 +1,11 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://addons/table_data_editor/src/table_data_editor/table_edit/cell/base_cell_element.gd" id="1_wjxc7"] + +[node name="base_cell_element" type="MarginContainer"] +offset_right = 80.0 +offset_bottom = 32.0 +focus_mode = 2 +theme_override_constants/margin_right = 4 +theme_override_constants/margin_bottom = 4 +script = ExtResource("1_wjxc7") diff --git a/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/cell/input_cell/input_cell.gd b/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/cell/input_cell/input_cell.gd new file mode 100644 index 0000000..c5af1c7 --- /dev/null +++ b/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/cell/input_cell/input_cell.gd @@ -0,0 +1,145 @@ +#============================================================ +# Input Cell +#============================================================ +# - datetime: 2022-11-26 18:12:34 +#============================================================ +##输入单元格 +@tool +class_name InputCell +extends MarginContainer + + +##单击单元格 +signal single_clicked +##双击单元格 +signal double_clicked +##水平方向拖拽 +##[br] +##[br][code]distance[/code] 为当前鼠标位置与第一次按下时鼠标位置的距离 +##[br][code]pressed_size[/code] 为点击时的节点的大小 +signal h_dragged(distance: float, pressed_node_size: Vector2i) +##垂直方向拖拽 +##[br] +##[br][code]distance[/code] 为当前鼠标位置与第一次按下时鼠标位置的距离 +##[br][code]pressed_size[/code] 为点击时的节点的大小 +signal v_dragged(distance: float, pressed_node_size: Vector2i) + + +var _data : String +var _latest_v_dragged := false +var _latest_h_dragged := false +var _latest_dragged := false + +# 点击时的鼠标位置 +var _pressed_mouse_pos: Vector2 +# 点击时节点的大小 +var _pressed_node_size: Vector2i +# 是否可以拖拽,防止 _event 发送速度过快 +var _enabled_dragged := true + + +@onready +var _text_edit := %text_edit as TextEdit +@onready +var _process_timer := %process_timer as Timer + + +#============================================================ +# SetGet +#============================================================ +func set_value(v: String): + _data = v + show_value(v) + +func get_value() -> String: + return _data + +func show_value(v): + _text_edit.text = v if v else "" + +## 值发生了改变 +func is_changed(): + return _text_edit.text != _data + + +#============================================================ +# 内置 +#============================================================ +func _ready(): +# _text_edit.gui_input.connect(self._gui_input) + + _text_edit.focus_entered.connect( func(): self.focus_entered.emit() ) + _text_edit.focus_exited.connect( func(): + self.focus_exited.emit() + if is_changed(): + set_value(_text_edit.text) + ) + set_process(false) + _process_timer.timeout.connect( set_process.bind(false) ) + self.mouse_entered.connect(func(): + set_process(true) + _process_timer.stop() + ) + self.mouse_exited.connect(func(): + _process_timer.start() + ) + + + +func _process(delta): + # 更新显示的鼠标图像 + var margin = custom_minimum_size - get_local_mouse_position() + if margin.x < self["theme_override_constants/margin_right"] and margin.y < self["theme_override_constants/margin_bottom"]: + self.mouse_default_cursor_shape = Control.CURSOR_MOVE + elif margin.x <= self["theme_override_constants/margin_right"]: + self.mouse_default_cursor_shape = Control.CURSOR_HSPLIT + elif margin.y <= self["theme_override_constants/margin_bottom"]: + self.mouse_default_cursor_shape = Control.CURSOR_VSPLIT + else: + if not(_latest_h_dragged or _latest_v_dragged): + self.mouse_default_cursor_shape = Control.CURSOR_ARROW + + _enabled_dragged = true + + +func _gui_input(event): + if event is InputEventMouseMotion: + if _enabled_dragged: + var diff = get_local_mouse_position() - _pressed_mouse_pos + if _latest_h_dragged: + self.custom_minimum_size.x = _pressed_node_size.x + diff.x + h_dragged.emit(self.custom_minimum_size.x - _pressed_node_size.x, _pressed_node_size) + _process_timer.stop() + if _latest_v_dragged: + self.custom_minimum_size.y = _pressed_node_size.y + diff.y + v_dragged.emit(self.custom_minimum_size.y - _pressed_node_size.y, _pressed_node_size) + _process_timer.stop() + _enabled_dragged = false + + elif event is InputEventMouseButton: + if event.pressed and event.button_index == MOUSE_BUTTON_LEFT: + var margin = custom_minimum_size - get_local_mouse_position() + if event.double_click: + self.double_clicked.emit() + else: + + _pressed_node_size = self.size + _pressed_mouse_pos = get_local_mouse_position() + + if margin.x <= self["theme_override_constants/margin_right"]: + _latest_h_dragged = true + if margin.y <= self["theme_override_constants/margin_bottom"]: + _latest_v_dragged = true + + self.single_clicked.emit() + + else: + self.mouse_default_cursor_shape = Control.CURSOR_ARROW + _latest_h_dragged = false + _latest_v_dragged = false + + var diff = self.custom_minimum_size - get_local_mouse_position() + if diff.x < 0 or diff.y < 0: + _process_timer.start() + + diff --git a/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/cell/input_cell/input_cell.tscn b/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/cell/input_cell/input_cell.tscn new file mode 100644 index 0000000..d03cf28 --- /dev/null +++ b/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/cell/input_cell/input_cell.tscn @@ -0,0 +1,124 @@ +[gd_scene load_steps=16 format=3] + +[ext_resource type="PackedScene" path="res://addons/table_data_editor/src/table_data_editor/table_edit/cell/base_cell_element.tscn" id="1_ney3p"] +[ext_resource type="Script" path="res://addons/table_data_editor/src/table_data_editor/table_edit/cell/input_cell/input_cell.gd" id="2_y6m17"] + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_54uk8"] + +[sub_resource type="Image" id="Image_pbgwo"] +data = { +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 3, 255, 255, 255, 41, 255, 255, 255, 67, 255, 255, 255, 67, 255, 255, 255, 40, 255, 255, 255, 3, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 41, 255, 255, 255, 74, 255, 255, 255, 74, 255, 255, 255, 74, 255, 255, 255, 74, 255, 255, 255, 40, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 67, 255, 255, 255, 74, 255, 255, 255, 74, 255, 255, 255, 74, 255, 255, 255, 74, 255, 255, 255, 67, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 67, 255, 255, 255, 74, 255, 255, 255, 74, 255, 255, 255, 74, 255, 255, 255, 74, 255, 255, 255, 67, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 40, 255, 255, 255, 74, 255, 255, 255, 74, 255, 255, 255, 74, 255, 255, 255, 74, 255, 255, 255, 40, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 3, 255, 255, 255, 40, 255, 255, 255, 67, 255, 255, 255, 67, 255, 255, 255, 40, 255, 255, 255, 3, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"format": "RGBA8", +"height": 12, +"mipmaps": false, +"width": 12 +} + +[sub_resource type="ImageTexture" id="ImageTexture_o2enw"] +image = SubResource("Image_pbgwo") + +[sub_resource type="StyleBoxTexture" id="StyleBoxTexture_hq7i2"] +content_margin_left = 2.0 +content_margin_top = 2.0 +content_margin_right = 2.0 +content_margin_bottom = 2.0 +texture = SubResource("ImageTexture_o2enw") +margin_left = 6.0 +margin_top = 6.0 +margin_right = 6.0 +margin_bottom = 6.0 +region_rect = Rect2(0, 0, 12, 12) + +[sub_resource type="Image" id="Image_s7tle"] +data = { +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 6, 248, 248, 248, 102, 249, 249, 249, 168, 249, 249, 249, 168, 248, 248, 248, 101, 213, 213, 213, 6, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 248, 248, 248, 102, 249, 249, 249, 186, 249, 249, 249, 186, 249, 249, 249, 186, 249, 249, 249, 186, 248, 248, 248, 101, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 249, 249, 249, 168, 249, 249, 249, 186, 249, 249, 249, 186, 249, 249, 249, 186, 249, 249, 249, 186, 249, 249, 249, 168, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 249, 249, 249, 168, 249, 249, 249, 186, 249, 249, 249, 186, 249, 249, 249, 186, 249, 249, 249, 186, 248, 248, 248, 168, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 248, 248, 248, 101, 249, 249, 249, 186, 249, 249, 249, 186, 249, 249, 249, 186, 249, 249, 249, 186, 250, 250, 250, 99, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 213, 213, 213, 6, 248, 248, 248, 101, 249, 249, 249, 168, 248, 248, 248, 168, 250, 250, 250, 99, 213, 213, 213, 6, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"format": "RGBA8", +"height": 12, +"mipmaps": false, +"width": 12 +} + +[sub_resource type="ImageTexture" id="ImageTexture_pi2w1"] +image = SubResource("Image_s7tle") + +[sub_resource type="StyleBoxTexture" id="StyleBoxTexture_daefy"] +content_margin_left = 2.0 +content_margin_top = 2.0 +content_margin_right = 2.0 +content_margin_bottom = 2.0 +texture = SubResource("ImageTexture_pi2w1") +margin_left = 5.0 +margin_top = 5.0 +margin_right = 5.0 +margin_bottom = 5.0 +region_rect = Rect2(0, 0, 12, 12) + +[sub_resource type="Image" id="Image_4ojul"] +data = { +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 213, 213, 213, 6, 180, 180, 180, 102, 181, 181, 181, 168, 181, 181, 181, 168, 179, 179, 179, 101, 170, 170, 170, 6, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 180, 180, 180, 102, 180, 180, 180, 186, 180, 180, 180, 186, 180, 180, 180, 186, 180, 180, 180, 186, 179, 179, 179, 101, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 181, 181, 181, 168, 180, 180, 180, 186, 180, 180, 180, 186, 180, 180, 180, 186, 180, 180, 180, 186, 181, 181, 181, 168, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 181, 181, 181, 168, 180, 180, 180, 186, 180, 180, 180, 186, 180, 180, 180, 186, 180, 180, 180, 186, 179, 179, 179, 168, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 179, 179, 179, 101, 180, 180, 180, 186, 180, 180, 180, 186, 180, 180, 180, 186, 180, 180, 180, 186, 181, 181, 181, 99, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 170, 170, 170, 6, 179, 179, 179, 101, 181, 181, 181, 168, 179, 179, 179, 168, 181, 181, 181, 99, 170, 170, 170, 6, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"format": "RGBA8", +"height": 12, +"mipmaps": false, +"width": 12 +} + +[sub_resource type="ImageTexture" id="ImageTexture_tvj35"] +image = SubResource("Image_4ojul") + +[sub_resource type="StyleBoxTexture" id="StyleBoxTexture_x7jas"] +content_margin_left = 2.0 +content_margin_top = 2.0 +content_margin_right = 2.0 +content_margin_bottom = 2.0 +texture = SubResource("ImageTexture_tvj35") +margin_left = 6.0 +margin_top = 6.0 +margin_right = 6.0 +margin_bottom = 6.0 +region_rect = Rect2(0, 0, 12, 12) + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_eycgy"] + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_64aem"] + +[sub_resource type="Theme" id="Theme_cy8yd"] +HScrollBar/icons/decrement = null +HScrollBar/icons/decrement_highlight = null +HScrollBar/icons/decrement_pressed = null +HScrollBar/icons/increment = null +HScrollBar/icons/increment_highlight = null +HScrollBar/icons/increment_pressed = null +HScrollBar/styles/grabber = null +HScrollBar/styles/grabber_highlight = null +HScrollBar/styles/grabber_pressed = null +HScrollBar/styles/scroll = SubResource("StyleBoxEmpty_54uk8") +HScrollBar/styles/scroll_focus = null +VScrollBar/icons/decrement = null +VScrollBar/icons/decrement_highlight = null +VScrollBar/icons/decrement_pressed = null +VScrollBar/icons/increment = null +VScrollBar/icons/increment_highlight = null +VScrollBar/icons/increment_pressed = null +VScrollBar/styles/grabber = SubResource("StyleBoxTexture_hq7i2") +VScrollBar/styles/grabber_highlight = SubResource("StyleBoxTexture_daefy") +VScrollBar/styles/grabber_pressed = SubResource("StyleBoxTexture_x7jas") +VScrollBar/styles/scroll = SubResource("StyleBoxEmpty_eycgy") +VScrollBar/styles/scroll_focus = SubResource("StyleBoxEmpty_64aem") + +[node name="input_cell" instance=ExtResource("1_ney3p")] +custom_minimum_size = Vector2(80, 35) +offset_bottom = 35.0 +mouse_filter = 0 +script = ExtResource("2_y6m17") + +[node name="text_edit" type="TextEdit" parent="." index="0"] +unique_name_in_owner = true +offset_right = 76.0 +offset_bottom = 31.0 +mouse_filter = 2 +theme = SubResource("Theme_cy8yd") + +[node name="process_timer" type="Timer" parent="." index="1"] +unique_name_in_owner = true +wait_time = 2.0 +one_shot = true diff --git a/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/cell/serial_number_cell/serial_number_cell.gd b/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/cell/serial_number_cell/serial_number_cell.gd new file mode 100644 index 0000000..9cf22d6 --- /dev/null +++ b/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/cell/serial_number_cell/serial_number_cell.gd @@ -0,0 +1,19 @@ +#============================================================ +# Serial Number Cell +#============================================================ +# - datetime: 2022-11-26 22:33:13 +#============================================================ +# 序列号表格 +@tool +class_name SerialNumberCell +extends BaseCellElement + + +@onready +var label := $label as Label + + +func show_number(v: int): + if label == null: await ready + label.text = str(v) + diff --git a/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/cell/serial_number_cell/serial_number_cell.tscn b/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/cell/serial_number_cell/serial_number_cell.tscn new file mode 100644 index 0000000..c262db6 --- /dev/null +++ b/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/cell/serial_number_cell/serial_number_cell.tscn @@ -0,0 +1,17 @@ +[gd_scene load_steps=3 format=3] + +[ext_resource type="PackedScene" path="res://addons/table_data_editor/src/table_data_editor/table_edit/cell/base_cell_element.tscn" id="1_5yxaj"] +[ext_resource type="Script" path="res://addons/table_data_editor/src/table_data_editor/table_edit/cell/serial_number_cell/serial_number_cell.gd" id="2_bf00s"] + +[node name="serial_number_cell" instance=ExtResource("1_5yxaj")] +offset_right = 50.0 +offset_bottom = 30.0 +theme_override_constants/margin_right = 8 +script = ExtResource("2_bf00s") + +[node name="label" type="Label" parent="." index="0"] +offset_right = 42.0 +offset_bottom = 26.0 +size_flags_vertical = 1 +text = "0" +vertical_alignment = 1 diff --git a/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/edit_box_window/edit_box_window.gd b/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/edit_box_window/edit_box_window.gd new file mode 100644 index 0000000..d356a90 --- /dev/null +++ b/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/edit_box_window/edit_box_window.gd @@ -0,0 +1,136 @@ +#============================================================ +# Edit Box Window +#============================================================ +# - author: zhangxuetu +# - datetime: 2023-03-19 11:48:30 +# - version: 4.0 +#============================================================ +@tool +class_name PopupEditBox +extends Control + + +signal popup_hide(text: String) +signal box_size_changed(box_size: Vector2) +signal input_switch_char(character: int) + + +@export +var text : String = "" : + set(v): + text = v + if not is_inside_tree(): await ready + if _edit_box.text != text: + _edit_box.text = text +@export +var showed : bool = true: + set(v): + if v != self.visible: + showed = v + self.visible = v +@export +var box_size: Vector2 : + set(v): + box_size = v + if not is_inside_tree(): await ready + if _edit_box.size != box_size: + _edit_box.size = box_size + get: + if _edit_box: + return _edit_box.size + return Vector2(0, 0) + + +@onready var _edit_box := %edit_box as TextEdit +@onready var _scale_rect := %scale_rect as Control + + +var _resize_pressed : bool = false +var _pressed_size : Vector2 = Vector2(0,0) +var _pressed_pos : Vector2 = Vector2(0,0) + + +#============================================================ +# SetGet +#============================================================ +func get_edit_box() -> TextEdit: + return _edit_box + +func get_text() -> String: + return _edit_box.text + + +#============================================================ +# 内置 +#============================================================ +func _ready(): + _edit_box.position = Vector2(0,0) + + _scale_rect.gui_input.connect(func(event): + if event is InputEventMouseMotion: + if _resize_pressed: + var diff_v = get_global_mouse_position() - _pressed_pos + _edit_box.size = _pressed_size + diff_v + + elif event is InputEventMouseButton: + if event.button_index == MOUSE_BUTTON_LEFT: + _resize_pressed = event.pressed + if _resize_pressed: + _pressed_size = _edit_box.size + _pressed_pos = get_global_mouse_position() + ) + + +#============================================================ +# 自定义 +#============================================================ +func popup(rect: Rect2 = Rect2()): + if _edit_box == null: await ready + + if rect.position != Vector2(): + _edit_box.global_position = rect.position + if rect.size != Vector2(): + _edit_box.size = rect.size + + # 聚焦编辑 + _edit_box.visible = true + _edit_box.set_caret_line( _edit_box.get_line_count() ) + _edit_box.set_caret_column( _edit_box.text.length() ) + self.showed = true + +# print("[ PopupEditBox ] 弹出窗口") + + # 取消焦点时隐藏 + var t = _edit_box.text + _edit_box.grab_focus() + _edit_box.focus_exited.connect(func(): + if t != _edit_box.text: + self.popup_hide.emit(_edit_box.text) + _edit_box.visible = false +# print("[ PopupEditBox ] 弹窗隐藏") + , Object.CONNECT_ONE_SHOT) + + +func _on_edit_box_resized(): + if _edit_box == null: await ready + self.box_size_changed.emit(_edit_box.size) + + +func _on_edit_box_gui_input(event): + if event is InputEventKey: + if event.pressed: + if not event.alt_pressed: + # Enter/Tab 切换单元格 + if event.keycode in [KEY_ENTER, KEY_KP_ENTER]: + self.input_switch_char.emit(KEY_ENTER) + get_tree().root.set_input_as_handled() + + elif event.keycode in [KEY_TAB]: + self.input_switch_char.emit(KEY_TAB) + get_tree().root.set_input_as_handled() + + else: + # Alt+Enter换行 + if event.keycode in [KEY_ENTER, KEY_KP_ENTER]: + _edit_box.insert_text_at_caret("\n") + diff --git a/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/edit_box_window/edit_box_window.tscn b/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/edit_box_window/edit_box_window.tscn new file mode 100644 index 0000000..ed890f1 --- /dev/null +++ b/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/edit_box_window/edit_box_window.tscn @@ -0,0 +1,42 @@ +[gd_scene load_steps=2 format=3 uid="uid://4xts0ha85fja"] + +[ext_resource type="Script" path="res://addons/table_data_editor/src/table_data_editor/table_edit/edit_box_window/edit_box_window.gd" id="1_qj0ea"] + +[node name="popup_edit_box" type="Control"] +visible = false +layout_mode = 3 +anchors_preset = 0 +size_flags_horizontal = 0 +size_flags_vertical = 0 +mouse_filter = 2 +script = ExtResource("1_qj0ea") +box_size = Vector2(200, 120) + +[node name="edit_box" type="TextEdit" parent="."] +unique_name_in_owner = true +custom_minimum_size = Vector2(100, 30) +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_right = 200.0 +offset_bottom = 120.0 +grow_horizontal = 2 +grow_vertical = 2 +wrap_mode = 1 + +[node name="scale_rect" type="Control" parent="edit_box"] +unique_name_in_owner = true +layout_mode = 1 +anchor_left = 0.996 +anchor_top = 0.987 +anchor_right = 0.996 +anchor_bottom = 0.987 +offset_left = -0.200012 +offset_top = -0.440002 +offset_right = 7.79999 +offset_bottom = 7.56 +mouse_default_cursor_shape = 12 + +[connection signal="gui_input" from="edit_box" to="." method="_on_edit_box_gui_input"] +[connection signal="resized" from="edit_box" to="." method="_on_edit_box_resized"] diff --git a/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/serial_number_container.gd b/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/serial_number_container.gd new file mode 100644 index 0000000..c5baa17 --- /dev/null +++ b/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/serial_number_container.gd @@ -0,0 +1,88 @@ +#============================================================ +# Serial Number Container +#============================================================ +# - datetime: 2022-11-26 23:09:40 +#============================================================ +# 显示左上的数字序号的容器 +@tool +class_name SerialNumberContainer +extends GridContainer + + +## 显示序号的单元格场景 +@export var serial_number_cell : PackedScene + + +var __init_node = InjectUtil.auto_inject(self, "_", true) + +var _table_container : TableContainer +var _h_serial_number_container : HBoxContainer +var _v_serial_number_container : VBoxContainer +var _space : Control + + +var _last_top_left : Vector2i + + + +#============================================================ +# 自定义 +#============================================================ +## 更新横竖列数字 +##[br] +##[br][code]top_left[/code] 以 top_left 值开始向下更新 +func update_serial_number(top_left: Vector2i): + var serial_number : SerialNumberCell + for i in _h_serial_number_container.get_child_count(): + serial_number = _h_serial_number_container.get_child(i) as SerialNumberCell + serial_number.show_number(top_left.x + i) + for i in _v_serial_number_container.get_child_count(): + serial_number = _v_serial_number_container.get_child(i) as SerialNumberCell + serial_number.show_number(top_left.y + i) + _last_top_left = top_left + + _space.custom_minimum_size = Vector2(_v_serial_number_container.size.x, _h_serial_number_container.size.y) + if _space.custom_minimum_size == Vector2(0,0): + _space.custom_minimum_size = Vector2(27, 35) + +## 更新这个行高 +func update_row_height(origin_row: int, height: int): + if _v_serial_number_container.get_child_count() > 0: + var node = _v_serial_number_container.get_child(origin_row) as Control + node.custom_minimum_size.y = height + +## 更新这个列宽 +func update_column_width(origin_column: int, width: int): + if _h_serial_number_container.get_child_count() > 0: + var node = _h_serial_number_container.get_child(origin_column) as Control + node.custom_minimum_size.x = width + + +## 更新行列数字标题节点的数量 +func update_grid_cell_count(grid_size: Vector2i): + if _table_container == null: + while _table_container == null: + await get_tree().process_frame + + # 表格数量发生改变时添加序号节点单元格 + var tile_size = _table_container.get_tile_size() +# print_debug("单元格大小:", tile_size) + + # 水平单元格 + if grid_size.x > _h_serial_number_container.get_child_count(): + var diff_count = grid_size.x - _h_serial_number_container.get_child_count() + for i in diff_count: + var node = serial_number_cell.instantiate() as Control + node.custom_minimum_size = tile_size + _h_serial_number_container.add_child(node) + + # 垂直单元格 + if grid_size.y > _v_serial_number_container.get_child_count(): + var diff_count = grid_size.y - _v_serial_number_container.get_child_count() + for i in diff_count: + var node = serial_number_cell.instantiate() as Control + node.custom_minimum_size.y = tile_size.y + _v_serial_number_container.add_child(node) + + update_serial_number(_last_top_left) + diff --git a/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/table_container/column_container/column_container.gd b/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/table_container/column_container/column_container.gd new file mode 100644 index 0000000..518641f --- /dev/null +++ b/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/table_container/column_container/column_container.gd @@ -0,0 +1,39 @@ +#============================================================ +# Line +#============================================================ +# - datetime: 2022-11-26 16:57:14 +#============================================================ +@tool +class_name ColumnContainer +extends MarginContainer + + +signal newly_added_cell(cell: Node) + + +@export +var item : PackedScene + + +@onready +var container := %container as HBoxContainer + + +func update_cell_amount(count: int): + if container == null: + await self.ready + if item: + var node + for i in count - container.get_child_count(): + node = item.instantiate() + container.add_child(node) + newly_added_cell.emit(node) + + +func get_cells(): + return container.get_children() + + +func get_cell(idx: int) -> Node: + return container.get_child(idx) + diff --git a/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/table_container/column_container/column_container.tscn b/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/table_container/column_container/column_container.tscn new file mode 100644 index 0000000..71f2879 --- /dev/null +++ b/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/table_container/column_container/column_container.tscn @@ -0,0 +1,18 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://addons/table_data_editor/src/table_data_editor/table_edit/table_container/column_container/column_container.gd" id="1_1xecd"] + +[node name="column_container" type="MarginContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_1xecd") +metadata/_edit_lock_ = true + +[node name="container" type="HBoxContainer" parent="."] +unique_name_in_owner = true +offset_right = 1152.0 +offset_bottom = 648.0 +theme_override_constants/separation = 0 diff --git a/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/table_container/row_container/row_container.gd b/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/table_container/row_container/row_container.gd new file mode 100644 index 0000000..6b30ef6 --- /dev/null +++ b/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/table_container/row_container/row_container.gd @@ -0,0 +1,36 @@ +#============================================================ +# List +#============================================================ +# - datetime: 2022-11-26 16:57:09 +#============================================================ +## 数据行的容器 + +@tool +class_name RowContainer +extends MarginContainer + + +signal newly_added_line(line: ColumnContainer) + + +@export var item : PackedScene + + +@onready var container = %container as VBoxContainer + + +func get_columns_containers() -> Array: + return container.get_children() + + +## 更新行数量 +func update_row_amount(count: int): + if container == null: + await self.ready + var node + for i in count - container.get_child_count(): + node = item.instantiate() + container.add_child(node) + newly_added_line.emit(node) + + diff --git a/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/table_container/row_container/row_container.tscn b/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/table_container/row_container/row_container.tscn new file mode 100644 index 0000000..3e8685d --- /dev/null +++ b/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/table_container/row_container/row_container.tscn @@ -0,0 +1,20 @@ +[gd_scene load_steps=3 format=3 uid="uid://dyhbjld6bhk1p"] + +[ext_resource type="Script" path="res://addons/table_data_editor/src/table_data_editor/table_edit/table_container/row_container/row_container.gd" id="1_hd2dr"] +[ext_resource type="PackedScene" path="res://addons/table_data_editor/src/table_data_editor/table_edit/table_container/column_container/column_container.tscn" id="2_cbcfu"] + +[node name="row_container" type="MarginContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 1.0 +offset_right = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_hd2dr") +item = ExtResource("2_cbcfu") + +[node name="container" type="VBoxContainer" parent="."] +unique_name_in_owner = true +layout_mode = 2 +theme_override_constants/separation = 0 diff --git a/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/table_container/table_container.gd b/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/table_container/table_container.gd new file mode 100644 index 0000000..eb1764e --- /dev/null +++ b/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/table_container/table_container.gd @@ -0,0 +1,147 @@ +#============================================================ +# Table Container +#============================================================ +# - datetime: 2022-11-26 17:01:54 +#============================================================ +## 表格 +## +##这里只进行表格单元格相关管理,不进行数据的处理,仅作为表格样式显示 +@tool +class_name TableContainer +extends MarginContainer + + +## 新增单元格 +signal newly_added_cell(coords: Vector2i, new_cell: Control) +## 网格中的单元格大小发生改变 +signal grid_cell_size_changed(grid_size: Vector2i) + + +@export var cell : PackedScene + + +var __init_node = InjectUtil.auto_inject(self, "_", true) + +var _row_container : RowContainer +var _update_row_column_amount_timer : Timer + + +# 表格单元格数量大小 +var _grid_cell_count_size := Vector2i() +# 单个单元格大小 +var _tile_size := Vector2i() +# 单元格列表 +var _cell_list : Array[Control] = [] +# 列对应的单元格 +var _column_to_cells_map := {} +# 行对应的单元格 +var _row_to_cells_map := {} + + +#============================================================ +# SetGet +#============================================================ +## 获取单元格行列数量大小 +func get_grid_row_column_count_size() -> Vector2i: + return _grid_cell_count_size + +## 获取表格的行数量 +func get_grid_row_count() -> int: + return _grid_cell_count_size.y + +## 获取表格的列数量 +func get_grid_column_count() -> int: + return _grid_cell_count_size.x + +## 获取单元格大小 +func get_tile_size() -> Vector2i: + return _tile_size + +## 获取行容器 +func get_row_container() -> RowContainer: + return _row_container + +## 获取所有行的单元格列容器 +func get_column_container() -> Array[ColumnContainer]: + return _row_container.get_columns_containers() + +## 获取所有单元格 +func get_all_cell() -> Array[Control]: + return _cell_list + +## 获取一列的单元格。column 从 0 开始,最大为 [method get_grid_row_column_count_siz] 的 x 值 +func get_column_cells(column: int) -> Array: + return _column_to_cells_map[column] + +## 获取这一行的单元格。row 从 0 开始,最大为 [method get_grid_row_column_count_siz] 的 y 值 +func get_row_cells(row: int) -> Array: + return _row_to_cells_map[row] + + + +#============================================================ +# 内置 +#============================================================ +func _ready() -> void: + assert(cell != null, "还没有设置 cell 属性!") + + # cell 的更新与大小 + _row_container.newly_added_line.connect(func(new_line: ColumnContainer): + new_line.newly_added_cell.connect( func(new_cell: Control): + self._cell_list.append(new_cell) + + var column : int = new_cell.get_index() + var row : int = new_line.get_index() + self.newly_added_cell.emit( Vector2i(column, row), new_cell ) + + if _column_to_cells_map.has(column): + _column_to_cells_map[column].append(new_cell) + else: + _column_to_cells_map[column] = [] + _column_to_cells_map[column].append(new_cell) + if _row_to_cells_map.has(row): + _row_to_cells_map[row].append(new_cell) + else: + _row_to_cells_map[row] = [] + _row_to_cells_map[row].append(new_cell) + ) + new_line.item = self.cell + new_line.update_cell_amount(_grid_cell_count_size.x) + ) + self.resized.connect(_update_row_column_amount) + + # 先创建出来第一个,用以获取最小 cell 大小 + _row_container.update_row_amount(1) + (_row_container.get_columns_containers()[0] as ColumnContainer).update_cell_amount(1) + var first_cell = _row_container.get_columns_containers()[0].get_cells()[0] as Control + _tile_size = first_cell.size + + # 更新单元格数量 + _update_row_column_amount_timer.timeout.connect(func(): + var tmp_cell_amount = _grid_cell_count_size + _grid_cell_count_size.x = int(self.size.x / _tile_size.x) + 1 + _grid_cell_count_size.y = int(self.size.y / _tile_size.y) + 1 + + if tmp_cell_amount != _grid_cell_count_size: + print("[ TableContainer ] 单元格小:", _grid_cell_count_size) + self.grid_cell_size_changed.emit(_grid_cell_count_size) + + for line in _row_container.get_columns_containers(): + line = line as ColumnContainer + line.update_cell_amount(_grid_cell_count_size.x) + _row_container.update_row_amount(_grid_cell_count_size.y) + ) + _update_row_column_amount_timer.start() + _update_row_column_amount_timer.autostart = true + _update_row_column_amount_timer.one_shot = true + _update_row_column_amount_timer.timeout.emit() + + + +#============================================================ +# 自定义 +#============================================================ +func _update_row_column_amount(): + _update_row_column_amount_timer.stop() + _update_row_column_amount_timer.start() + diff --git a/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/table_container/table_container.tscn b/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/table_container/table_container.tscn new file mode 100644 index 0000000..de628d8 --- /dev/null +++ b/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/table_container/table_container.tscn @@ -0,0 +1,42 @@ +[gd_scene load_steps=4 format=3 uid="uid://d02f0rqt6qnpv"] + +[ext_resource type="Script" path="res://addons/table_data_editor/src/table_data_editor/table_edit/table_container/table_container.gd" id="1_v7wlj"] +[ext_resource type="PackedScene" path="res://addons/table_data_editor/src/table_data_editor/table_edit/cell/input_cell/input_cell.tscn" id="2_t8gur"] +[ext_resource type="PackedScene" uid="uid://dyhbjld6bhk1p" path="res://addons/table_data_editor/src/table_data_editor/table_edit/table_container/row_container/row_container.tscn" id="3_auuku"] + +[node name="table_container" type="MarginContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_v7wlj") +cell = ExtResource("2_t8gur") +metadata/_edit_lock_ = true + +[node name="control" type="Control" parent="."] +unique_name_in_owner = true +clip_contents = true +layout_mode = 2 +mouse_filter = 2 +metadata/_edit_lock_ = true + +[node name="row_container" parent="control" instance=ExtResource("3_auuku")] +unique_name_in_owner = true +layout_mode = 1 +anchors_preset = 0 +anchor_right = 0.0 +anchor_bottom = 0.0 +offset_left = 0.0 +offset_right = 0.0 +grow_horizontal = 1 +grow_vertical = 1 +size_flags_horizontal = 3 +size_flags_vertical = 3 +metadata/_edit_lock_ = true + +[node name="update_row_column_amount_timer" type="Timer" parent="."] +unique_name_in_owner = true +wait_time = 0.05 +one_shot = true +autostart = true diff --git a/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/table_data_set.gd b/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/table_data_set.gd new file mode 100644 index 0000000..287ba13 --- /dev/null +++ b/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/table_data_set.gd @@ -0,0 +1,105 @@ +#============================================================ +# Table Data Set +#============================================================ +# - author: zhangxuetu +# - datetime: 2023-05-16 23:22:41 +# - version: 4.0 +#============================================================ +## 表格数据集 +## +##专门存储管理表单的数据。处理每个单元格中的数据 +class_name TableDataEditor_TableDataSet + + +enum { + COLUMN, + ROW, +} + +# 表格数据。以 [code]grid_data[行][列] = 值[/code] 的格式存储数据。 +var grid_data : Dictionary = {} +# 列集合。所有行哪些列有值 +var column_set : Dictionary = {} + + +#============================================================ +# SetGet +#============================================================ +func get_config_data(): + return { + "grid_data": grid_data, + "column_set": column_set, + } + +func set_config_data(data: Dictionary): + self.grid_data = data.get("grid_data", {}) + self.column_set = data.get("column_set", {}) + +func get_origin_data() -> Dictionary: + return grid_data + +func is_empty() -> bool: + return grid_data.is_empty() + +func has_value(coords: Vector2i) -> bool: + return (grid_data.has(coords[ROW]) + and grid_data[coords[ROW]].has(coords[COLUMN]) + ) + +func get_value(coords: Vector2i): + if has_value(coords): + var row : int = coords[ROW] + var column : int = coords[COLUMN] + return grid_data[row][column] + return "" + +func set_value(coords: Vector2i, value) -> void: + var row : int = coords[ROW] + var column : int = coords[COLUMN] + + if not grid_data.has(row): + grid_data[row] = {} + grid_data[row][column] = value + + if not column_set.has(column): + column_set[column] = 0 + column_set[column] += 1 + + +func get_max_column() -> int: + if column_set.is_empty(): + return 0 + return column_set.keys().max() + +func remove_value(coords: Vector2i) -> bool: + if has_value(coords): + var row : int = coords[ROW] + var column : int = coords[COLUMN] + grid_data[row].erase(column) + column_set[column] -= 1 + + # 没有数据时,进行移除 + if Dictionary(grid_data[row]).is_empty(): + grid_data.erase(row) + if column_set[column] == 0: + column_set.erase(column) + return true + return false + +func get_row_list() -> Array[int]: + return Array(grid_data.keys(), TYPE_INT, "", null) + + + +#============================================================ +# 内置 +#============================================================ +func _to_string(): + return var_to_str( TableDataUtil.Classes.get_dict_by_property(self) ) + + +func _init(data: Dictionary = {}): + self.grid_data = data.get("grid_data", {}) + self.column_set = data.get("column_set", {}) + + diff --git a/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/table_edit.gd b/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/table_edit.gd new file mode 100644 index 0000000..0cb00a7 --- /dev/null +++ b/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/table_edit.gd @@ -0,0 +1,498 @@ +#============================================================ +# Table Edit +#============================================================ +# - author: zhangxuetu +# - datetime: 2022-11-26 17:53:52 +# - version: 4.0 +#============================================================ +## 编辑网格 +## +##这里管理 Cell 单元格的数据内容,真正开始对表格数据进行处理。 +@tool +class_name TableEdit +extends MarginContainer + + +## 数据发生改变 +signal data_set_changed +## 选中单元格 +signal selected_cell(cell: InputCell) +## 取消选中单元格 +signal deselected_cell(cell: InputCell) +## 单击单元格 +signal single_clicked_cell(cell: InputCell) +## 双击单元格 +signal double_clicked_cell(cell: InputCell) +## 准备编辑单元格 +signal ready_edit_cell(cell: InputCell) +## 已编辑单元格 +signal edited_cell(cell: InputCell) +## 单元格数据发生改变 +signal cell_value_changed(cell: InputCell, coords: Vector2i, previous: String, value: String) +## 滚动条滚动 +signal scroll_changed(coords: Vector2i) +## 行高发生改变 +signal row_height_changed(value: int) +## 列宽发生改变 +signal column_width_changed(value: int) +## 弹窗编辑器表格大小发生改变 +signal popup_edit_box_size_changed(box_size: Vector2) + + +## 双击单元格进行编辑 +@export var double_click_edit : bool = true + + +## 表格中的数据。格式:[code]data[row][column] = data[/code] +var grid_data := {}: + set(v): + grid_data = v + + update_cell_list() + scroll_to(Vector2i(0,0)) + self.data_set_changed.emit() +## 默认单元格大小 +var default_tile_size : Vector2i +## 数据集,管理获取数据 +var data_set : TableDataEditor_TableDataSet = TableDataEditor_TableDataSet.new(): + set(v): + data_set = v + + # 当前线程其他代码调用完成后调用这个 + (func(): + update_cell_list() + scroll_to(Vector2i(0,0)) + ).call_deferred() +## 行对应的行高 +var row_to_height_map := {} +## 列对应的列宽 +var column_to_width_map := {} + + +var __init_node = InjectUtil.auto_inject(self, "_", true) + +var _table_container : TableContainer +var _popup_edit_box : PopupEditBox +var _v_scroll_bar : VScrollBar +var _h_scroll_bar : HScrollBar +var _update_grid_data_timer : Timer +var _serial_number_container : SerialNumberContainer + + +# 是否允许发出取消选中 cell 的信号,用于编辑表格数据,编辑的时候代表这个单元格还是被选中的 +var _enabled_emit_deselected_signal := true + +# 原始坐标位置对应的单元格 +var _origin_coords_to_cell_map := {} +# 单元格对应的原点坐标位置 +var _cell_to_origin_coords_map := {} +# 当前选中的单元格 +var _selected_cell : InputCell: + set(v): + _selected_cell = v + if v != null: + _last_cell = v +# 最后一次选中的单元格 +var _last_cell : InputCell +# 上一次左上角的坐标位置 +var _latest_top_left := Vector2i() + +# 正在按着 alt 键 +var _pressing_alt := false +# 是否已经开始更新 +var _updated : bool = false + + +#============================================================ +# SetGet +#============================================================ +## 获取编辑弹窗 +func get_edit_dialog() -> PopupEditBox: + return _popup_edit_box + +func get_grid_data() -> Dictionary: + return grid_data + +func get_data_set() -> TableDataEditor_TableDataSet: + return data_set + +## 获取当前滚动到的左上角位置 +func get_scroll_top_left() -> Vector2i: + return Vector2i( _h_scroll_bar.value, _v_scroll_bar.value ) + +## 获取滚动条最顶部 Y 的值 +func get_scroll_top() -> int: + return int(_v_scroll_bar.value) + +## 获取滚动条最左边 X 的值 +func get_scroll_left() -> int: + return int(_h_scroll_bar.value) + +## 获取这个 cell 的当前坐标位置 +func get_cell_coords(cell: InputCell) -> Vector2i: + return get_scroll_top_left() + _cell_to_origin_coords_map.get(cell, Vector2i(-1, -1)) + +## 获取这个坐标上的 cell +func get_cell_node(coords: Vector2i) -> InputCell: + var origin_coords = coords - get_scroll_top_left() + return _origin_coords_to_cell_map.get(origin_coords) as InputCell + +## 获取列宽 +func get_column_width(column: int, default_width: int = 0) -> int: + if default_width <= 0: + default_width = _table_container.get_tile_size().x + return column_to_width_map.get(column, default_width) + +## 获取行高 +func get_row_height(row: int, default_heigt : int = 0): + if default_heigt <= 0: + default_heigt = _table_container.get_tile_size().y + return row_to_height_map.get(row, default_heigt) + +## 获取列宽数据,数据中的 key 为列值,对应列宽 +func get_column_width_data() -> Dictionary: + return column_to_width_map + +## 获取行高数据,数据中的 key 为行值,对应行宽 +func get_row_height_data() -> Dictionary: + return row_to_height_map + +## 获取单元格行列大小数量 +func get_column_row_size() -> Vector2i: + return _table_container.get_grid_row_column_count_size() + +## 获取单元格当前整个矩形数据的位置 +func get_current_rect() -> Rect2i: + return Rect2i(get_scroll_top_left(), get_column_row_size()) + + +#============================================================ +# 内置 +#============================================================ +func _ready() -> void: + + # 滚动条 + _h_scroll_bar.scrolling.connect(func(): + _h_scroll_bar.max_value = _h_scroll_bar.value + 100 + update_cell_list() + ) + _v_scroll_bar.scrolling.connect(func(): + _v_scroll_bar.max_value = _v_scroll_bar.value + 100 + update_cell_list() + ) + + # 编辑表格窗。隐藏就更新对应的单元格的数据 + _popup_edit_box.popup_hide.connect(func(value): + # 弹窗消失后才允许发送取消选中的信号 + _enabled_emit_deselected_signal = true + # 更新选中的 cell + if _last_cell: + _last_cell.set_value( value ) + var coords = get_cell_coords(_last_cell) + alter_value(coords, value) + self.edited_cell.emit(_last_cell) + + ) + + # 必须要等空闲时间时调用,否则 _table_container 中的节点没有加载完成,则看不到节点的大小 + update_serial_num.call_deferred(Vector2i(0, 0)) + self.default_tile_size = _table_container.get_tile_size() + + # 切换窗口时取消 alt + while get_window() == null: + await Engine.get_main_loop().process_frame + _pressing_alt = false + + # 更新表格数据 + update_cell_list() + + _serial_number_container.update_serial_number(Vector2i(0,0)) + + +func _notification(what): + if what == NOTIFICATION_WM_WINDOW_FOCUS_OUT: + _pressing_alt = false + + +func _unhandled_input(event): + if event is InputEventKey: + # 按下 alt 键 + if event.keycode == KEY_ALT: +# if not _pressing_alt and event.is_pressed(): +# print("[ TableEdit ] 按下了 Alt 键") + _pressing_alt = event.is_pressed() + + +#============================================================ +# 自定义 +#============================================================ +# 真正进行修改行高,但数据不会缓存到数据中 +func _alter_row_height(row: int, height: int): + height = max(height, default_tile_size.y) + for cell in _table_container.get_row_cells(row - get_scroll_top()): + cell.custom_minimum_size.y = height + _serial_number_container.update_row_height(row - get_scroll_top(), height) + +# 真正进行修改单元格宽度,但数据不会缓存到数据中 +func _alter_column_width(column: int, width: int): + width = max(width, default_tile_size.x) + for cell in _table_container.get_column_cells( column - get_scroll_left() ): + cell.custom_minimum_size.x = width + _serial_number_container.update_column_width(column - get_scroll_left(), width) + + +## 修改单元格数据 +##[br] +##[br][code]coords[/code] 修改的坐标位置 +##[br][code]value[/code] 修改的值,如果为需改为 [code]""[/code],则会删除掉这个数据 +##[br][code]emit_signal_state[/code] 是否发送信号 +func alter_value( + coords: Vector2i, + value: String, + emit_signal_state: bool = true +) -> void: + var previous = data_set.get_value(coords) + if value: + if previous != value: + data_set.set_value(coords, value) + if emit_signal_state: + var cell = get_cell_node(coords) + self.cell_value_changed.emit(cell, coords, previous, value ) + else: + if data_set.remove_value(coords): + if emit_signal_state: + var cell = get_cell_node(coords) + self.cell_value_changed.emit(cell, coords, previous, "") + + +## 修改行高 +##[br] +##[br][code]row[/code] 所在的行,从 1 开始 +##[br][code]height[/code] 设置的行高 +func alter_row_height(row: int, height: int): + row_to_height_map[row] = height + _alter_row_height(row, height) + + +## 修改列宽 +##[br] +##[br][code]column[/code] 所在的列,从 1 开始 +##[br][code]width[/code] 设置的列宽 +func alter_column_width(column: int, width: int): + column_to_width_map[column] = width + _alter_column_width(column, width) + + +## 更新单元格信息(使用计时器缓冲更新,防止同一时间多次重复调用) +func update_cell_list(): + if _updated: + return + _updated = true + await Engine.get_main_loop().process_frame + _updated = false + force_update_cell_list() + + +# 真正实际执行的更新 +func force_update_cell_list(): + # 更新数据 + var top_left = get_scroll_top_left() + var coords : Vector2i + for cell in _cell_to_origin_coords_map: + coords = get_cell_coords(cell) + cell.show_value(data_set.get_value(coords)) + + # 更新单元格的宽高 + var grid_row_column_size = _table_container.get_grid_row_column_count_size() + for column in range(top_left.x, top_left.x + grid_row_column_size.x): + _alter_column_width(column, get_column_width(column, 0) ) + for row in range(top_left.y, top_left.y + grid_row_column_size.y): + _alter_row_height(row, get_row_height(row, 0) ) + + + if _latest_top_left != top_left: + _latest_top_left = top_left + self.scroll_changed.emit( _latest_top_left ) + _popup_edit_box.showed = false + _popup_edit_box.get_edit_box().visible = false + _serial_number_container.update_serial_number(top_left) + + +## 更新行列序号的值 +##[br] +##[br][code]left_top[/code] 左上角行列值 +func update_serial_num(left_top: Vector2i): + _serial_number_container.update_column_width(left_top.x, default_tile_size.x) + _serial_number_container.update_row_height(left_top.y, default_tile_size.y) + + +## 滚动到指定位置 +func scroll_to(left_top: Vector2i): + _h_scroll_bar.value = left_top.x + _v_scroll_bar.value = left_top.y + _h_scroll_bar.scrolling.emit() + _v_scroll_bar.scrolling.emit() + + +## 编辑单元格 +func edit_cell(cell_coords: Vector2i): + _enabled_emit_deselected_signal = false + + # 设置选中的 cell + var cell = get_cell_node(cell_coords) + _selected_cell = cell + + # 弹窗 + _popup_edit_box.text = data_set.get_value(cell_coords) + _popup_edit_box.popup(Rect2(cell.global_position, Vector2(0,0))) + self.ready_edit_cell.emit(cell) + + +## 切换到下一个位置进行编辑。表格坐标是从 Vector2i(1, 1) 开始的,不是 Vector2i(0, 0) +func edit_to_next_cell(coords: Vector2i, direction : Vector2i): + if _selected_cell: + var next_coords = coords + direction + next_coords.x = max(1, next_coords.x) + next_coords.y = max(1, next_coords.y) + + _popup_edit_box.showed = false + _popup_edit_box.showed = true + + # 如果所在的单元格超出当前视图内的单元格,则进行滚动 + if direction.x > 0 or direction.y > 0: + var get_width_height_callback : Callable = self.get_column_width \ + if direction.x != 0 \ + else self.get_row_height + var dir_idx = 0 \ + if direction.x != 0 \ + else 1 + var total = 0 + var top_left = get_scroll_top_left() + for i in range(next_coords[dir_idx] - 1, top_left[dir_idx] - 1, -1): + total += abs(get_width_height_callback.call(i)) + 8 + if total > self.size[dir_idx]: + # 超出屏幕则换行 + scroll_to(top_left + direction * (i - top_left[dir_idx] + 1)) + break + + else: + var rect = get_current_rect() + rect.size -= Vector2i.ONE + if not rect.has_point(next_coords): + var top_left = get_scroll_top_left() + scroll_to(top_left + direction) + + # 切换到下一个网格的位置编辑网格 + await Engine.get_main_loop().create_timer(0.1).timeout + edit_cell.call_deferred(next_coords) + + + +#============================================================ +# 连接信号 +#============================================================ +# 添加新的单元格时 +func _newly_added_cell(coords: Vector2i, new_cell: InputCell): + # 滑轮滚动 + new_cell.gui_input.connect(func(event): + # 单元格 Input + if event is InputEventMouseButton and event.is_pressed(): + var scroll_bar : ScrollBar + if _pressing_alt: + scroll_bar = _h_scroll_bar + else: + scroll_bar = _v_scroll_bar + if event.button_index == MOUSE_BUTTON_WHEEL_DOWN: + scroll_bar.value += scroll_bar.step + scroll_bar.scrolling.emit() + elif event.button_index == MOUSE_BUTTON_WHEEL_UP: + scroll_bar.value -= scroll_bar.step + scroll_bar.scrolling.emit() + ) + + # 单元格行列坐标映射 + _cell_to_origin_coords_map[new_cell] = coords + _origin_coords_to_cell_map[coords] = new_cell + + update_cell_list() + + # 选中单元格 + new_cell.focus_entered.connect(func(): + _selected_cell = new_cell + if _popup_edit_box.showed: + _popup_edit_box.showed = false + if _selected_cell: + # 取消上次选中的单元格 + self.deselected_cell.emit(_selected_cell) + _selected_cell = null + + # 当前 cell + self.selected_cell.emit(new_cell) + _popup_edit_box.showed = false + ) + + # 取消选中单元格 + new_cell.focus_exited.connect(func(): + if _enabled_emit_deselected_signal: + _selected_cell = null + self.deselected_cell.emit(new_cell) + ) + + # 单击 + new_cell.single_clicked.connect(func(): + if not double_click_edit: + edit_cell(get_cell_coords(new_cell)) + self.single_clicked_cell.emit(new_cell) + ) + + # 双击 + new_cell.double_clicked.connect(func(): + if double_click_edit: + edit_cell(get_cell_coords(new_cell)) + self.double_clicked_cell.emit(new_cell) + ) + + # 水平拖拽移动 + new_cell.h_dragged.connect(func(distance: float, pressed_node_size: Vector2i): + # 表格当前坐标位置 + var current_coords = get_cell_coords(new_cell) + var width = pressed_node_size.x + int(distance) + alter_column_width(current_coords.x, width) + + # 记录改变的列宽 + column_to_width_map[current_coords.x] = width + self.column_width_changed.emit(width) + ) + + # 垂直拖拽移动 + new_cell.v_dragged.connect(func(distance: float, pressed_node_size: Vector2i): + + # 表格当前坐标位置 + var current_coords = get_cell_coords(new_cell) + var height = pressed_node_size.y + int(distance) + alter_row_height(current_coords.y, height) + + # 记录改变的行高 + row_to_height_map[current_coords.y] = height + self.row_height_changed.emit(height) + + ) + +func _on_table_container_grid_cell_size_changed(grid_size): + # 更新序号 + _serial_number_container.update_grid_cell_count(grid_size) + +func _on_popup_edit_box_box_size_changed(box_size): + self.popup_edit_box_size_changed.emit(box_size) + + +func _on_popup_edit_box_input_switch_char(character): + match character: + KEY_TAB: + var coords = get_cell_coords(_selected_cell) + edit_to_next_cell(coords, Vector2i.LEFT if Input.is_key_pressed(KEY_SHIFT) else Vector2i.RIGHT) + + KEY_ENTER: + var coords = get_cell_coords(_selected_cell) + edit_to_next_cell(coords, Vector2i.UP if Input.is_key_pressed(KEY_SHIFT) else Vector2i.DOWN) + diff --git a/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/table_edit.tscn b/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/table_edit.tscn new file mode 100644 index 0000000..213cd2f --- /dev/null +++ b/DungeonShooting_Godot/addons/table_data_editor/src/table_data_editor/table_edit/table_edit.tscn @@ -0,0 +1,118 @@ +[gd_scene load_steps=6 format=3 uid="uid://ctppgkl2dpksd"] + +[ext_resource type="Script" path="res://addons/table_data_editor/src/table_data_editor/table_edit/table_edit.gd" id="1_b56jx"] +[ext_resource type="Script" path="res://addons/table_data_editor/src/table_data_editor/table_edit/serial_number_container.gd" id="2_irx05"] +[ext_resource type="PackedScene" path="res://addons/table_data_editor/src/table_data_editor/table_edit/cell/serial_number_cell/serial_number_cell.tscn" id="3_xe81h"] +[ext_resource type="PackedScene" uid="uid://d02f0rqt6qnpv" path="res://addons/table_data_editor/src/table_data_editor/table_edit/table_container/table_container.tscn" id="4_sm235"] +[ext_resource type="PackedScene" uid="uid://4xts0ha85fja" path="res://addons/table_data_editor/src/table_data_editor/table_edit/edit_box_window/edit_box_window.tscn" id="6_mt1mb"] + +[node name="table_edit" type="MarginContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_constants/margin_left = 2 +theme_override_constants/margin_top = 2 +theme_override_constants/margin_right = 2 +theme_override_constants/margin_bottom = 2 +script = ExtResource("1_b56jx") + +[node name="serial_number_container" type="GridContainer" parent="."] +unique_name_in_owner = true +layout_mode = 2 +theme_override_constants/h_separation = 0 +theme_override_constants/v_separation = 0 +columns = 2 +script = ExtResource("2_irx05") +serial_number_cell = ExtResource("3_xe81h") + +[node name="space" type="Control" parent="serial_number_container"] +unique_name_in_owner = true +custom_minimum_size = Vector2(27, 35) +layout_mode = 2 + +[node name="Control" type="Control" parent="serial_number_container"] +clip_contents = true +layout_mode = 2 +mouse_filter = 2 + +[node name="h_serial_number_container" type="HBoxContainer" parent="serial_number_container/Control"] +unique_name_in_owner = true +layout_mode = 0 +theme_override_constants/separation = 0 + +[node name="Control2" type="Control" parent="serial_number_container"] +clip_contents = true +custom_minimum_size = Vector2(32, 0) +layout_mode = 2 +mouse_filter = 2 + +[node name="v_serial_number_container" type="VBoxContainer" parent="serial_number_container/Control2"] +unique_name_in_owner = true +layout_mode = 0 +theme_override_constants/separation = 0 + +[node name="Control3" type="Control" parent="serial_number_container"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +mouse_filter = 2 + +[node name="grid_container" type="GridContainer" parent="serial_number_container/Control3"] +unique_name_in_owner = true +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_override_constants/h_separation = 0 +theme_override_constants/v_separation = 0 +columns = 2 + +[node name="table_container" parent="serial_number_container/Control3/grid_container" instance=ExtResource("4_sm235")] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="v_scroll_bar" type="VScrollBar" parent="serial_number_container/Control3/grid_container"] +unique_name_in_owner = true +layout_mode = 2 +focus_mode = 1 +min_value = 1.0 +step = 1.0 +page = 10.0 +value = 1.0 +exp_edit = true + +[node name="h_scroll_bar" type="HScrollBar" parent="serial_number_container/Control3/grid_container"] +unique_name_in_owner = true +layout_mode = 2 +focus_mode = 1 +min_value = 1.0 +step = 1.0 +page = 10.0 +value = 1.0 +exp_edit = true + +[node name="Control" type="Control" parent="serial_number_container/Control3/grid_container"] +layout_mode = 2 + +[node name="update_grid_data_timer" type="Timer" parent="serial_number_container/Control3/grid_container"] +unique_name_in_owner = true +wait_time = 0.02 +one_shot = true +autostart = true + +[node name="popup_edit_box" parent="." instance=ExtResource("6_mt1mb")] +unique_name_in_owner = true +layout_mode = 2 + +[connection signal="grid_cell_size_changed" from="serial_number_container/Control3/grid_container/table_container" to="." method="_on_table_container_grid_cell_size_changed"] +[connection signal="newly_added_cell" from="serial_number_container/Control3/grid_container/table_container" to="." method="_newly_added_cell"] +[connection signal="box_size_changed" from="popup_edit_box" to="." method="_on_popup_edit_box_box_size_changed"] +[connection signal="input_switch_char" from="popup_edit_box" to="." method="_on_popup_edit_box_input_switch_char"] diff --git a/DungeonShooting_Godot/addons/table_data_editor/src/util/inject_util.gd b/DungeonShooting_Godot/addons/table_data_editor/src/util/inject_util.gd new file mode 100644 index 0000000..d85f0e2 --- /dev/null +++ b/DungeonShooting_Godot/addons/table_data_editor/src/util/inject_util.gd @@ -0,0 +1,45 @@ +#============================================================ +# Inject Util +#============================================================ +# - author: zhangxuetu +# - datetime: 2023-05-14 23:53:46 +# - version: 4.0 +#============================================================ +## 注入节点工具 +class_name InjectUtil + + +## 自动注入 unique (唯一名称)节点属性 +##[br] +##[br][code]parent[/code] 目标节点,对这个节点的属性进行自动注入节点属性 +##[br][code]prefix[/code] 注入的属性的前缀值 +##[br]示例: +##[codeblock] +##extends Node +## +##var __init_node__ = InjectUtil.auto_inject(self, "_") +### 当前场景中有 %sprite 、%collision 节点则会自动获取并自动设置下面两个属性 +##var _sprite : Sprite2D +##var _collision: Collision +## +##[/codeblock] +static func auto_inject(parent: Node, prefix: String = "", open_err: bool = false): + var method : Callable = func(): + for data in (parent.get_script() as GDScript).get_script_property_list(): + if data['type'] == TYPE_OBJECT and parent[data['name']] == null: + var prop = str(data['name']).trim_prefix(prefix) + if parent.has_node("%" + prop): + # 注入属性 + var node = parent.get_node_or_null("%" + prop) + if node: + parent[data['name']] = node + else: + if open_err: + printerr("没有 ", prop, " 属性相关节点") + + if parent.is_inside_tree(): + method.call() + else: + parent.tree_entered.connect(method, Object.CONNECT_ONE_SHOT) + return true + diff --git a/DungeonShooting_Godot/addons/table_data_editor/src/util/table_data_util.gd b/DungeonShooting_Godot/addons/table_data_editor/src/util/table_data_util.gd new file mode 100644 index 0000000..f3542b2 --- /dev/null +++ b/DungeonShooting_Godot/addons/table_data_editor/src/util/table_data_util.gd @@ -0,0 +1,151 @@ +#============================================================ +# Table Data Util +#============================================================ +# - author: zhangxuetu +# - datetime: 2023-05-17 19:14:50 +# - version: 4.0 +#============================================================ +class_name TableDataUtil + + +class SingletonData: + + static func get_value(key, default: Callable): + if not Engine.has_meta(key): + Engine.set_meta(key, default.call()) + return Engine.get_meta(key) + + static func get_child_value(key, child_key, child_default: Callable): + var data = get_value(key, func(): return {}) + if data.has(child_key): + return data[child_key] + else: + data[child_key] = child_default.call() + return data[child_key] + + + +class Files: + + static func load_file(path: String): + if FileAccess.file_exists(path): + var bytes = FileAccess.get_file_as_bytes(path) + return bytes_to_var(bytes) + + static func make_dir(dir: String) -> bool: + if not DirAccess.dir_exists_absolute(dir): + DirAccess.make_dir_recursive_absolute(dir) + return true + return false + + static func save_data(path: String, data) -> bool: + make_dir(path.get_base_dir()) + if not path.is_empty(): + var bytes : PackedByteArray = var_to_bytes(data) + var writer : FileAccess = FileAccess.open(path, FileAccess.WRITE) + if writer.get_open_error() != OK: + printerr("打开文件失败!", writer.get_open_error()) + return false + + writer.store_buffer(bytes) + if writer.get_error() != OK: + printerr("写入文件失败:", writer.get_error()) + return false + + writer = null + return true + return false + + static func save_as_string(path:String, data): + make_dir(path.get_base_dir()) + var writer = FileAccess.open(path, FileAccess.WRITE) + writer.store_string( + JSON.stringify(data) + if not data is String + else data + ) + + static func read_as_string(path: String) -> String: + if FileAccess.file_exists(path): + var reader = FileAccess.open(path, FileAccess.READ) + return reader.get_as_text() + return "" + + static func read_csv_file(path: String, delim: String = ",") -> Array[PackedStringArray]: + if FileAccess.file_exists(path): + var reader = FileAccess.open(path, FileAccess.READ) + var lines : Array[PackedStringArray]= [] + var line = reader.get_csv_line(delim) + while line != PackedStringArray([""]): + lines.append(line) + line = reader.get_csv_line(delim) + return lines + return [] + + static func get_absolute_path(path: String) -> String: + var reader = FileAccess.open(path, FileAccess.READ) + if reader: + return reader.get_path_absolute() + return "" + + + +class Classes: + + static func get_propertys(script: Script) -> Array[String]: + return SingletonData.get_child_value("TableDataUtil_Classes_propertys", script, func(): + var list : Array[String] = [] + list.append_array(script \ + .get_script_property_list() \ + .map(func(data): return data['name']) + .filter(func(name: String): return name.find(".") == -1) + ) + return list + ) + + static func set_property_by_dict(object: Object, dict: Dictionary): + for property in dict: + if property in object: + object[property] = dict[property] + + static func get_dict_by_property(object: Object) -> Dictionary: + var dict : Dictionary = {} + var list = get_propertys(object.get_script()) + for property in get_propertys(object.get_script()): + dict[property] = object[property] + return dict + + + +class Editor: + + ## 获取编辑器接口 + static func get_editor_interface() -> EditorInterface: + if not Engine.is_editor_hint(): + return null + const KEY = "TableDataUtil_get_editor_interface" + if Engine.has_meta(KEY): + return Engine.get_meta(KEY) + else: + var plugin = ClassDB.instantiate("EditorPlugin") + Engine.set_meta(KEY, plugin.get_editor_interface()) + return plugin.get_editor_interface() + + ## 是否开启了插件 + static func is_enabled() -> bool: + if not Engine.is_editor_hint(): + return false + if get_editor_interface() == null: + return false + return get_editor_interface().is_plugin_enabled("table_data_editor") + + ## 当前插件是否是打开的 + static func is_main_node() -> bool: + var node = get_editor_interface().get_editor_main_screen() + for child in node.get_children(): + if child is Control and child.visible: + # 显示的是当前插件的名称 + return child.name == 'table_data_editor' + return false + + diff --git a/DungeonShooting_Godot/addons/table_data_editor/test.gdata b/DungeonShooting_Godot/addons/table_data_editor/test.gdata new file mode 100644 index 0000000..2047ed3 --- /dev/null +++ b/DungeonShooting_Godot/addons/table_data_editor/test.gdata Binary files differ diff --git a/DungeonShooting_Godot/project.godot b/DungeonShooting_Godot/project.godot index 601f8cb..9961bcf 100644 --- a/DungeonShooting_Godot/project.godot +++ b/DungeonShooting_Godot/project.godot @@ -32,7 +32,7 @@ [editor_plugins] -enabled=PackedStringArray("res://addons/dungeonShooting_plugin/plugin.cfg") +enabled=PackedStringArray("res://addons/dungeonShooting_plugin/plugin.cfg", "res://addons/script_comment_menu/plugin.cfg", "res://addons/table_data_editor/plugin.cfg") [gui] diff --git a/DungeonShooting_Godot/src/framework/activity/ActivityObject_Register.cs b/DungeonShooting_Godot/src/framework/activity/ActivityObject_Register.cs index 85f2521..da315be 100644 --- a/DungeonShooting_Godot/src/framework/activity/ActivityObject_Register.cs +++ b/DungeonShooting_Godot/src/framework/activity/ActivityObject_Register.cs @@ -8,22 +8,6 @@ { private static bool _initState = false; - //物体注册数据 - private class RegisterActivityData - { - public RegisterActivityData(RegisterActivity registerActivity, Func callBack) - { - RegisterActivity = registerActivity; - CallBack = callBack; - } - - public RegisterActivity RegisterActivity; - public Func CallBack; - } - - //所有注册物体集合 - private static readonly Dictionary _activityRegisterMap = new(); - /// /// 初始化调用, 开始扫描当前程序集, 并自动注册 ActivityObject 物体 /// @@ -35,47 +19,7 @@ } _initState = true; - //扫描当前程序集 - ScannerFromAssembly(typeof(ActivityObject).Assembly); - } - - /// - /// 扫描指定程序集, 自动注册带有 RegisterActivity 特性的类 - /// - public static void ScannerFromAssembly(Assembly assembly) - { - var types = assembly.GetTypes(); - foreach (var type in types) - { - //注册类 - var attribute = Attribute.GetCustomAttributes(type, typeof(RegisterActivity), false); - if (attribute.Length > 0) - { - if (!typeof(ActivityObject).IsAssignableFrom(type)) - { - //不是继承自 ActivityObject - throw new Exception($"The registered object '{type.FullName}' does not inherit the class ActivityObject."); - } - else if (type.IsAbstract) - { - //不能加到抽象类上 - throw new Exception($"'RegisterActivity' cannot be used on abstract class '{type.FullName}'."); - } - var attrs = (RegisterActivity[])attribute; - foreach (var att in attrs) - { - //注册操作 - if (_activityRegisterMap.ContainsKey(att.ItemId)) - { - throw new Exception($"Object ID: '{att.ItemId}' is already registered"); - } - _activityRegisterMap.Add(att.ItemId, new RegisterActivityData(att, () => - { - return (ActivityObject)Activator.CreateInstance(type); - })); - } - } - } + } /// @@ -83,18 +27,18 @@ /// public static ActivityObject Create(string itemId) { - var world = GameApplication.Instance.World; - if (world == null) - { - throw new Exception("实例化 ActivityObject 前请先调用 'GameApplication.Instance.CreateNewWorld()' 初始化 World 对象"); - } - if (_activityRegisterMap.TryGetValue(itemId, out var item)) - { - var instance = item.CallBack(); - instance._InitNode(item.RegisterActivity.ItemId, item.RegisterActivity.PrefabPath, world); - item.RegisterActivity.CustomHandler(instance); - return instance; - } + // var world = GameApplication.Instance.World; + // if (world == null) + // { + // throw new Exception("实例化 ActivityObject 前请先调用 'GameApplication.Instance.CreateNewWorld()' 初始化 World 对象"); + // } + // if (_activityRegisterMap.TryGetValue(itemId, out var item)) + // { + // var instance = item.CallBack(); + // instance._InitNode(item.RegisterActivity.ItemId, item.RegisterActivity.PrefabPath, world); + // item.RegisterActivity.CustomHandler(instance); + // return instance; + // } return null; } diff --git a/DungeonShooting_Godot/src/framework/activity/RegisterActivity.cs b/DungeonShooting_Godot/src/framework/activity/RegisterActivity.cs deleted file mode 100644 index 4782dd1..0000000 --- a/DungeonShooting_Godot/src/framework/activity/RegisterActivity.cs +++ /dev/null @@ -1,32 +0,0 @@ - -using System; - -/// -/// 用在 ActivityObject 子类上, 用于注册游戏中的物体, 一个类可以添加多个 [RegisterActivity] 特性, ActivityObject 会自动扫描并注册物体 -/// -[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] -public class RegisterActivity : Attribute -{ - /// - /// 注册物体唯一ID, 该ID不能有重复 - /// - public string ItemId { get; protected set; } - - /// - /// 模板 Prefab 的路径 - /// - public string PrefabPath { get; protected set; } - - public RegisterActivity(string itemId, string prefabPath) - { - ItemId = itemId; - PrefabPath = prefabPath; - } - - /// - /// 该函数在物体实例化后调用, 可用于一些自定义操作, 参数为实例对象 - /// - public virtual void CustomHandler(ActivityObject instance) - { - } -} \ No newline at end of file diff --git a/DungeonShooting_Godot/src/game/effects/EnemyDebris.cs b/DungeonShooting_Godot/src/game/effects/EnemyDebris.cs index 0017f0f..04a85f4 100644 --- a/DungeonShooting_Godot/src/game/effects/EnemyDebris.cs +++ b/DungeonShooting_Godot/src/game/effects/EnemyDebris.cs @@ -2,7 +2,6 @@ using System.Collections; using Godot; -[RegisterActivity(ActivityIdPrefix.Effect + "0001", ResourcePath.prefab_effect_activityObject_EnemyDebris_tscn)] public partial class EnemyDebris : ActivityObject { diff --git a/DungeonShooting_Godot/src/game/item/bullet/Bullet.cs b/DungeonShooting_Godot/src/game/item/bullet/Bullet.cs index f015411..4ae5688 100644 --- a/DungeonShooting_Godot/src/game/item/bullet/Bullet.cs +++ b/DungeonShooting_Godot/src/game/item/bullet/Bullet.cs @@ -3,8 +3,6 @@ /// /// 子弹类 /// -[RegisterActivity(ActivityIdPrefix.Bullet + "0001", ResourcePath.prefab_weapon_bullet_Bullet1_tscn)] -[RegisterActivity(ActivityIdPrefix.Bullet + "0002", ResourcePath.prefab_weapon_bullet_Bullet2_tscn)] public partial class Bullet : ActivityObject { /// diff --git a/DungeonShooting_Godot/src/game/item/bullet/RegisterBullet.cs b/DungeonShooting_Godot/src/game/item/bullet/RegisterBullet.cs deleted file mode 100644 index 7357cad..0000000 --- a/DungeonShooting_Godot/src/game/item/bullet/RegisterBullet.cs +++ /dev/null @@ -1,7 +0,0 @@ - -public class RegisterBullet : RegisterActivity -{ - public RegisterBullet(string itemId, string prefabPath) : base(itemId, prefabPath) - { - } -} \ No newline at end of file diff --git a/DungeonShooting_Godot/src/game/item/shell/ShellCase.cs b/DungeonShooting_Godot/src/game/item/shell/ShellCase.cs index 1966222..18e64a2 100644 --- a/DungeonShooting_Godot/src/game/item/shell/ShellCase.cs +++ b/DungeonShooting_Godot/src/game/item/shell/ShellCase.cs @@ -4,7 +4,6 @@ /// /// 弹壳类 /// -[RegisterActivity(ActivityIdPrefix.Shell + "0001", ResourcePath.prefab_weapon_shell_ShellCase_tscn)] public partial class ShellCase : ActivityObject { public override void OnInit() diff --git a/DungeonShooting_Godot/src/game/item/weapon/RegisterWeapon.cs b/DungeonShooting_Godot/src/game/item/weapon/RegisterWeapon.cs deleted file mode 100644 index 0a1ca23..0000000 --- a/DungeonShooting_Godot/src/game/item/weapon/RegisterWeapon.cs +++ /dev/null @@ -1,27 +0,0 @@ - -using System; - -/// -/// 注册武器 -/// -public class RegisterWeapon : RegisterActivity -{ - /// - /// 武器属性 - /// - private readonly WeaponAttribute _weaponAttribute; - - public RegisterWeapon(string itemId, Type attribute) : base(itemId, null) - { - _weaponAttribute = (WeaponAttribute)Activator.CreateInstance(attribute); - if (_weaponAttribute != null) PrefabPath = _weaponAttribute.WeaponPrefab; - } - - public override void CustomHandler(ActivityObject instance) - { - if (instance is Weapon weapon) - { - weapon.InitWeapon(_weaponAttribute.Clone()); - } - } -} \ No newline at end of file diff --git a/DungeonShooting_Godot/src/game/item/weapon/gun/Gun.cs b/DungeonShooting_Godot/src/game/item/weapon/gun/Gun.cs index 3204ed6..2d9839d 100644 --- a/DungeonShooting_Godot/src/game/item/weapon/gun/Gun.cs +++ b/DungeonShooting_Godot/src/game/item/weapon/gun/Gun.cs @@ -3,9 +3,6 @@ /// /// 普通的枪 /// -[RegisterWeapon(ActivityIdPrefix.Weapon + "0001", typeof(RifleAttribute))] -[RegisterWeapon(ActivityIdPrefix.Weapon + "0003", typeof(PistolAttribute))] -[RegisterWeapon(ActivityIdPrefix.Weapon + "0005", typeof(SniperRifleAttribute))] public partial class Gun : Weapon { //步枪属性数据 diff --git a/DungeonShooting_Godot/src/game/item/weapon/gun/Shotgun.cs b/DungeonShooting_Godot/src/game/item/weapon/gun/Shotgun.cs index ca471e9..43cde9b 100644 --- a/DungeonShooting_Godot/src/game/item/weapon/gun/Shotgun.cs +++ b/DungeonShooting_Godot/src/game/item/weapon/gun/Shotgun.cs @@ -1,6 +1,5 @@ using Godot; -[RegisterWeapon(ActivityIdPrefix.Weapon + "0002", typeof(ShotgunAttribute))] public partial class Shotgun : Weapon { diff --git a/DungeonShooting_Godot/src/game/item/weapon/knife/Knife.cs b/DungeonShooting_Godot/src/game/item/weapon/knife/Knife.cs index 8171d86..99f4f09 100644 --- a/DungeonShooting_Godot/src/game/item/weapon/knife/Knife.cs +++ b/DungeonShooting_Godot/src/game/item/weapon/knife/Knife.cs @@ -1,7 +1,6 @@ using Godot; -[RegisterWeapon(ActivityIdPrefix.Weapon + "0004", typeof(KnifeAttribute))] public partial class Knife : Weapon { private class KnifeAttribute : WeaponAttribute diff --git a/DungeonShooting_Godot/src/game/role/Player.cs b/DungeonShooting_Godot/src/game/role/Player.cs index 8b288cf..180b033 100644 --- a/DungeonShooting_Godot/src/game/role/Player.cs +++ b/DungeonShooting_Godot/src/game/role/Player.cs @@ -4,7 +4,6 @@ /// /// 玩家角色基类, 所有角色都必须继承该类 /// -[RegisterActivity(ActivityIdPrefix.Role + "0001", ResourcePath.prefab_role_Player_tscn)] public partial class Player : Role { /// diff --git a/DungeonShooting_Godot/src/game/role/enemy/Enemy.cs b/DungeonShooting_Godot/src/game/role/enemy/Enemy.cs index 2177bda..85b45af 100644 --- a/DungeonShooting_Godot/src/game/role/enemy/Enemy.cs +++ b/DungeonShooting_Godot/src/game/role/enemy/Enemy.cs @@ -16,7 +16,6 @@ /// /// 基础敌人 /// -[RegisterActivity(ActivityIdPrefix.Enemy + "0001", ResourcePath.prefab_role_Enemy_tscn)] public partial class Enemy : Role { /// diff --git a/DungeonShooting_Godot/src/game/room/RoomDoor.cs b/DungeonShooting_Godot/src/game/room/RoomDoor.cs index 89c73b8..b1f4055 100644 --- a/DungeonShooting_Godot/src/game/room/RoomDoor.cs +++ b/DungeonShooting_Godot/src/game/room/RoomDoor.cs @@ -4,11 +4,6 @@ /// /// 房间的门, 门有两种状态, 打开和关闭 /// -// [RegisterActivity(ActivityIdPrefix.Other + "0001", ResourcePath.prefab_map_RoomDoor_tscn)] -[RegisterActivity(ActivityIdPrefix.Other + "door_n", ResourcePath.prefab_map_RoomDoor_N_tscn)] -[RegisterActivity(ActivityIdPrefix.Other + "door_s", ResourcePath.prefab_map_RoomDoor_S_tscn)] -[RegisterActivity(ActivityIdPrefix.Other + "door_w", ResourcePath.prefab_map_RoomDoor_W_tscn)] -[RegisterActivity(ActivityIdPrefix.Other + "door_e", ResourcePath.prefab_map_RoomDoor_E_tscn)] public partial class RoomDoor : ActivityObject { /// diff --git a/DungeonShooting_Godot/src/test/TestActivity.cs b/DungeonShooting_Godot/src/test/TestActivity.cs index ef47a9e..f541b73 100644 --- a/DungeonShooting_Godot/src/test/TestActivity.cs +++ b/DungeonShooting_Godot/src/test/TestActivity.cs @@ -1,6 +1,5 @@ using Godot; -[RegisterActivity(ActivityIdPrefix.Test + "0001", ResourcePath.prefab_test_TestActivity_tscn)] public partial class TestActivity : ActivityObject { public override void OnInit()