Updated data manager and fixed some bugs

This commit is contained in:
Matthias Stöckli 2017-08-03 17:09:17 +02:00
parent c9434c9992
commit fc77bab650
9 changed files with 236 additions and 131 deletions

116
README.md Normal file
View File

@ -0,0 +1,116 @@
# Godot Data Editor
This repository hosts a plugin for the [Godot Engine]. It allows users to enter data items based on Godot classes which are then serialized in either as json or binaries. Serialized items may be encrypted if desired.
# Features
* Support for binary or json serialization
* Create data classes with the click of a button
* Use gdscript to add properties and logic to your data classes, all instances will be Nodes
* Make use of the built-in [export property hints]
* Add instance-specific custom properties
* Access data with a simple API using the _data_ singleton
* ALPHA: Use the notification/observer system to react to changes in data items
* ALPHA: Encrypt your data files
# Screenshots
![editor_screenshot]
![class_screenshot]
# Installation
* Open your project folder, e.g. "../MyGame/"
* Create a folder named "addons" (if not already present)
* In addons, create a folder named "DataEditor"
* Copy the content of this repository into it. You may remove the "sceenshots" ;)
* Open your project in the editor and navigate to the plugin (Scene -> Project Settings -> Plugins)
* The plugin "DataEditor" should now appear, change the status from "Inactive" to "Active"
* Restart the editor to make sure that the _data_ singleton is loaded properly
I intend to upload the plugin to the AssetLib, once I feel it is stable enough.
# System Requirements
The plugin was written for version *2.1.3* of the Godot Engine. Upcoming minor versions should be supported as well.
It is very likely that a number of changes will be necessary, once Godot 3 is released.
# API / Tutorial
I created a little video which shows how to use the plugin to create a simple shop system: [[Link to video which does not exist yet :) ]]
Working with data is rather simple, use the provided _data_ class to access the items. The following code snippets demonstrates item retrieval as well as the observation feature:
```gdscript
extends Node
func _ready():
# Get a single item
var herb = data.get_item("shop_item", "herb")
var price = herb.price
# Get all items as dictionary (key: id, value: item)
var shop_items = data.get_items("shop_item")
for shop_item in shop_items.values():
print(shop_item.price)
pass
#######################################
# Observe Properties:
# Please note that you currently have to update properties using the "update_property" to make use of this feature
#######################################
# Be notified when something about this herb changes
data.observe_item(self, herb, "herb_changed")
herb.update_property("name", "better_herb")
# Be notified when the price of this herb changes,
data.observe_item_property(self, herb, "price", "herb_price_changed")
herb.update_property("price", 500)
# Be notified when any item changes
var doge_axe = data.get_item("shop_item", "doge_axe")
data.observe_class(self, "shop_item", "shop_item_changed")
doge_axe.update_property("price", 500)
# Overkill: be notified about everything
data.observe_all_changes(self, "something_changed")
#######################################
# Stop Observing:
# When you are no longer interested in updates, simply unsubscribe/stop observing
#######################################
data.stop_observing_item_property(self, herb, "price")
data.stop_observing_item(self, herb)
data.stop_observing_class(self, "shop_item")
data.stop_observing_changes(self)
func herb_changed(item, property, value):
print("Something about this herb changed!")
func herb_price_changed(item, property, value):
print("Herb price changed!")
func shop_item_changed(item, property, value):
print(item.name + " changed! " + property + " : " + str(value))
func something_changed(item, property, value):
print("I guess something changed.")
```
# Please Contribute!
Please feel free to contribute. Unfortunately, the code base still is not documented that well and there are a number of bugs which will need to be ironed out. I am sure that there are a number of things I have been doing wrong, e.g. performance/memory management issues.
# Known Issues
* The "Rename Class" feature may not properly rename the class file if it still in use
* There are a number of reference issues
* In some cases, the controls are not properly resized - pressing "Reload" should usually do the trick though
* A number of operations will perform a complete refresh of all data, which causes unsaved changes to disappear. This was done to prevent inconsistencies
* There is no support for undo/redo
* Pressing Ctrl+S to save while in the data editor may temporarily hide unsaved items
* The _data_ singleton is only visible in the editor when the project is being restarted. This seems to be a limitation of the engine which does not allow reload the engine.cfg file
* The "class overview" screen is lacking any kind of useful content
# HALP! Something went wrong!
Stay calm, most issues can be resolved by either pressing the "Reload" button or activating and deactivating the plugin. If the problem persists, there is likely an issue with your data. Check if the name of the class (which are stored in the "classes" folder by default) is the same as the folder name of your instances (by default called "data"). If this is the case, there might be a conflict with duplicate IDs or the like. Please post an issue here if this happened without any external influence (e.g. you edited the files manually in another editor).
[Godot Engine]: <https://github.com/godotengine/godot>
[export property hints]: <http://docs.godotengine.org/en/latest/learning/scripting/gdscript/gdscript_basics.html#exports>
[editor_screenshot]: https://github.com/Stoeoeoe/screenshots/editor.png "The Godot Data Editor"
[class_screenshot]: https://github.com/Stoeoeoe/screenshots/class.png "Example Class"

View File

@ -46,6 +46,7 @@ alignment = 0
[node name="ClassPropertiesLabel" type="Label" parent="Panel/Body"]
visibility/visible = false
focus/ignore_mouse = true
focus/stop_mouse = true
size_flags/horizontal = 2
@ -53,7 +54,7 @@ size_flags/vertical = 0
margin/left = 0.0
margin/top = 0.0
margin/right = 1246.0
margin/bottom = 2.0
margin/bottom = 19.0
custom_fonts/font = ExtResource( 3 )
text = "Static Class Properties"
uppercase = true
@ -63,14 +64,15 @@ max_lines_visible = -1
[node name="HSeparator" type="HSeparator" parent="Panel/Body"]
visibility/visible = false
focus/ignore_mouse = false
focus/stop_mouse = true
size_flags/horizontal = 2
size_flags/vertical = 2
margin/left = 0.0
margin/top = 6.0
margin/top = 0.0
margin/right = 1246.0
margin/bottom = 9.0
margin/bottom = 3.0
[node name="Control" type="Control" parent="Panel/Body"]
@ -80,18 +82,19 @@ focus/stop_mouse = true
size_flags/horizontal = 2
size_flags/vertical = 2
margin/left = 0.0
margin/top = 13.0
margin/top = 0.0
margin/right = 1246.0
margin/bottom = 28.0
margin/bottom = 15.0
[node name="Scroll" type="ScrollContainer" parent="Panel/Body"]
visibility/visible = false
focus/ignore_mouse = false
focus/stop_mouse = true
size_flags/horizontal = 3
size_flags/vertical = 3
margin/left = 0.0
margin/top = 32.0
margin/top = 19.0
margin/right = 1246.0
margin/bottom = 566.0
scroll/horizontal = true
@ -99,6 +102,7 @@ scroll/vertical = true
[node name="Statics" type="VBoxContainer" parent="Panel/Body/Scroll"]
visibility/visible = false
focus/ignore_mouse = false
focus/stop_mouse = false
size_flags/horizontal = 3
@ -106,11 +110,12 @@ size_flags/vertical = 2
margin/left = 0.0
margin/top = 0.0
margin/right = 1246.0
margin/bottom = 38.0
margin/bottom = 0.0
alignment = 0
[node name="NoStaticsPropertiesLabel" type="Label" parent="Panel/Body/Scroll/Statics"]
visibility/visible = false
focus/ignore_mouse = true
focus/stop_mouse = true
size_flags/horizontal = 3
@ -126,6 +131,7 @@ max_lines_visible = -1
[node name="AddStaticPropertyButton" type="Button" parent="Panel/Body/Scroll/Statics"]
visibility/visible = false
focus/ignore_mouse = false
focus/stop_mouse = true
size_flags/horizontal = 2

92
data.gd
View File

@ -4,40 +4,24 @@ var item_manager = null
var items = {}
var values = {}
signal any_value_changed(item, property, value)
var signals_any_value_changed = []
var signals_item_class_any_value_changed = []
var signals_item_any_values_changed = []
var signals_item_value_changed = []
func _init():
# Caution: This item manager may not be in sync with the one used by the editor
self.item_manager = preload("item_manager.gd").new()
self.items = item_manager.items
# TODO: Allow eager loading
func get_item(item_class, id):
return item_manager.get_item(item_class, id)
func get_items(item_class):
return item_manager.get_items(item_class)
# if items[item_class].has(id):
# return items[item_class][id]
# else:
# _load_item(item_class, id)
# return items[item_class][id]
func _load_item(item_class, id):
items[item_class][id] = item_manager.load_item(item_class, id)
# values[item_class][id] = {}
func load_values_of_all_items():
pass
func load_item_value(item, property):
return get_progress(item._class, item._id, property)
@ -53,52 +37,96 @@ func set_progress(item_class, id, property, value):
var has_value = item.get(property)
if item and has_value:
item.set(property, value)
emit_signal("any_value_changed", item, property, value)
if has_user_signal("@any_value_changed"):
emit_signal("@any_value_changed", item, property, value)
var signal_name = ""
signal_name = item_class
signal_name = "@" + item_class
# Class signal
if has_user_signal(signal_name):
emit_signal(signal_name, item, property, value)
# Item signal
signal_name = item_class + "|" + id
signal_name = "@" + item_class + "|" + id
if has_user_signal(signal_name):
emit_signal(signal_name, item, property, value)
# Property signal
signal_name = item_class + "|" + id + "|" + property
signal_name = "@" + item_class + "|" + id + "|" + property
if has_user_signal(signal_name):
emit_signal(signal_name, item, property, value)
return true
else:
return false
func observe_all_changes(observer, method, binds=[], flags = 0):
self.connect("any_value_changed", observer, method, binds, flags)
var signal_name = "@any_value_changed"
self.add_user_signal(signal_name) # TODO: Args
self.connect(signal_name, observer, method, binds, flags)
func observe_class(observer, item_class, method, binds=[], flags = 0):
self.add_user_signal(item_class) # TODO: Args
self.connect(item_class, observer, method, binds, flags)
var signal_name = "@" + item_class
self.add_user_signal(signal_name) # TODO: Args
self.connect(signal_name, observer, method, binds, flags)
func observe_item(observer, item, method, binds=[], flags = 0):
var signal_name = item._class + "|" + item._id
var signal_name = "@" + item._class + "|" + item._id
if not has_user_signal(signal_name):
self.add_user_signal(signal_name) # TODO: Args
self.connect(signal_name, observer, method, binds, flags)
func observe_item_property(observer, item, property, method, binds=[], flags = 0):
var signal_name = item._class + "|" + item._id + "|" + property
var signal_name = "@" + item._class + "|" + item._id + "|" + property
if not has_user_signal(signal_name):
self.add_user_signal(signal_name) # TODO: Args
self.connect(signal_name, observer, method, binds, flags)
func stop_observing_all_changes(observer):
pass
# observer.disconnect(
func _get_relevant_connections():
var relevant_connections = []
var signals = get_signal_list()
for s in signals:
var name = s["name"]
if name.begins_with("@"):
for c in get_signal_connection_list(name):
relevant_connections.append(c)
return relevant_connections
func stop_observing_class(observer, item_class):
var connection_list = _get_relevant_connections()
for connection in connection_list:
var target = connection["target"]
var signal_info = connection["signal"].replace("@", "").split("|")
if signal_info.size() == 1 and signal_info[0] == item_class and target == observer:
self.disconnect(connection["signal"], target, connection["method"])
func stop_observing_item(observer, item):
var connection_list = _get_relevant_connections()
for connection in connection_list:
var target = connection["target"]
var signal_info = connection["signal"].replace("@", "").split("|")
if signal_info.size() == 2 and signal_info[0] == item._class and signal_info[1] == item._id and target == observer:
self.disconnect(connection["signal"], target, connection["method"])
#TODO: func block_signals()
func stop_observing_item_property(observer, item, property):
var connection_list = _get_relevant_connections()
for connection in connection_list:
var target = connection["target"]
var signal_info = connection["signal"].replace("@", "").split("|")
if signal_info.size() == 3 and signal_info[0] == item._class and signal_info[1] == item._id and signal_info[2] == property and target == observer:
self.disconnect(connection["signal"], target, connection["method"])
func stop_observing_changes(observer):
var connection_list = _get_relevant_connections()
for connection in connection_list:
var target = connection["target"]
if target == observer:
self.disconnect(connection["signal"], target, connection["method"])
# observer.disconnect(
func set_item_progress(item, property, value):

View File

@ -43,19 +43,6 @@ onready var new_item_class_icon = get_node("NewClassDialog/ClassIconPath")
onready var new_item_class_icon_dialog = get_node("NewClassDialog/ClassIconFileDialog")
#onready var delete_item_dialog = get_node("DeleteItemDialog")
#onready var rename_item_dialog = get_node("RenameItemDialog")
#onready var rename_item_id = get_node("RenameItemDialog/Id")
#onready var duplicate_item_dialog = get_node("DuplicateItemDialog")
#onready var duplicate_item_id = get_node("DuplicateItemDialog/Id")
#onready var delete_class_dialog = get_node("DeleteClassDialog")
#onready var display_name_dialog = get_node("DisplayNameDialog")
#onready var display_name_dialog_name = get_node("DisplayNameDialog/Name")
onready var warn_dialog = get_node("WarnDialog")
onready var options_screen = get_node("OptionsDialog")
@ -68,21 +55,9 @@ signal input_dialog_confirmed(text1, text2)
# First initialize the item manager which is used for loading, saving and configs
func _init():
item_manager = preload("item_manager.gd").new()
#item_manager.set_name("ItemManager")
#Globals.set("item_manager", item_manager)
# self.add_child(item_manager)
# item_manager.raise()
item_manager = preload("item_manager.gd").new() # This item_manager will add itself to the globals
func _ready():
#add_button.set_shortcut(create_shortcut(KEY_MASK_CMD|KEY_N))
#delete_button.set_shortcut(create_shortcut(KEY_DELETE))
#save_button.set_shortcut(create_shortcut(KEY_MASK_SHIFT|KEY_MASK_CMD|KEY_S))
#save_all_button.set_shortcut(create_shortcut(KEY_MASK_SHIFT|KEY_MASK_CMD|KEY_MASK_ALT|KEY_S))
func _ready():
Globals.set("debug_is_editor", false)
# Tree signals
item_tree.connect("on_new_item_pressed", self, "handle_actions", ["add"])
@ -136,11 +111,20 @@ func _ready():
# Select the first item in the tree when loading the GUI
change_item_context(selected_item, selected_class)
# TODO: Other OS...
# TODO: Implement
func open_item():
var item_path = item_manager.get_full_path(selected_item)
print(item_path)
OS.execute("explorer", [item_path], false)
var program = ""
var os_name = OS.get_name()
if os_name == "Windows":
program = "explorer"
item_path = item_path.replace("/", "\\") # ~_~...
# TODO: Not sure if these work... Probably add the possibility to add a custom editor
elif os_name == "OSX":
program = "open"
else:
program = "nautilus"
OS.execute(program, [item_path], false)
func change_item_context(selected_item, selected_class):
@ -170,13 +154,11 @@ func change_item_context(selected_item, selected_class):
# An item was selected
if selected_item:
# Context was lost, e.g. because of changes to the classes. Get a new copy from the item_manager
# Context was lost, e.g. because of changes to the classes. Reload.
if selected_item and not selected_item.get("_id"):
self.item_manager.load_manager()
self.item_tree.load_tree(true)
selected_item = item_tree.select_first_element()
change_display_name_button.set_disabled(false)
duplicate_button.set_disabled(false)
@ -214,7 +196,7 @@ func change_item_context(selected_item, selected_class):
self.selected_item = null
self.selected_id = null
id_label.set_text(selected_class.capitalize() + " " + (selected_class))
id_label.set_text(selected_class.capitalize())
class_overview.show()
instance_details.hide()
no_classes.hide()
@ -232,10 +214,12 @@ func create_shortcut(keys):
input_event.ID = keys
short_cut.set_shortcut(input_event)
# TODO: Implement
func warn_about_reload():
if item_manager.has_unsaved_items():
input_dialog.popup(self, "reload_confirmed", tr("Confirm reload"), tr("Some changes have not been saved. \nThey will be discarded if you proceed. Are you sure you want to perform this action?"))
func reload():
item_manager.load_manager()
item_tree.load_tree(true)
@ -243,20 +227,10 @@ func reload():
item_tree.select_item(item_manager.get_item(selected_class, selected_id))
#var last_selected_class = selected_class
#var last_selected_id = selected_id
#selected_item = item_manager.get_item(last_selected_class, last_selected_id)
#if selected_item:
#selected_id = selected_item._id
#change_item_context(selected_item, selected_class)
func toggle_item_dirty_state(item):
item._dirty = true
item_tree.set_tree_item_label_text(item)
#func show_delete_custom_property_dialog(property_name):
# input_dialog.popup(self, "delete_custom_property", tr("Delete Custom Property"), tr("Are you sure you want to delete this property?"))
# Validation takes place in the item manager
func _on_NewCustomPropertyDialog_confirmed():
@ -269,10 +243,8 @@ func _on_NewCustomPropertyDialog_confirmed():
custom_properties.build_properties(selected_item)
# TODO: Show confirmation dialog
func delete_custom_property(property_name):
#input_dialog.popup(self, "_on_delete__confirmed", tr("New Item"), tr("Please enter an ID and optionally a display name the new item"), tr("ID"), "", tr("Display Name (optional)"), "")
item_manager.delete_custom_property(selected_item, property_name)
toggle_item_dirty_state(selected_item)
custom_properties.build_properties(selected_item)
@ -301,10 +273,11 @@ func _on_NewClassDialog_confirmed():
func _on_NewClassIconSearchButton_button_down():
new_item_class_icon_dialog.popup_centered()
# Icon for new class was selected
func _on_NewClassIconFileDialog_file_selected(path):
new_item_class_icon.set_text(path)
# General handler for a lot of actions to centralize the GUI logic a bit
func handle_actions(action, argument = ""):
if action == "add":
input_dialog.popup(self, "_on_add_item_confirmed", tr("New Item"), tr("Please enter an ID for and optionally a display name the new item"), tr("ID"), "", tr("Display Name (optional)"), "")

View File

@ -1,7 +1,5 @@
extends Node
var _items_path = ""
# Class information
# No setter, that's why there is a comma
var _class setget ,get_class
@ -10,7 +8,6 @@ var _class_name setget ,get_class_name
var _dirty = false # TODO: Exclude
var _persistent = false # TODO: Exclude
var _id = ""
var _display_name setget set_display_name,get_display_name
var _created = 0
@ -21,13 +18,13 @@ var _last_modified = 0
var _custom_properties = {}
func _ready():
var config = ConfigFile.new()
config.load("res://addons/DataEditor/plugin.cfg")
_items_path = config.has_section_key("plugin", "output_directory")
pass
func get_class():
return self.get_script().get_path().get_file().basename()
func get_class_name():
return self.get_class().capitalize()
@ -38,21 +35,16 @@ func get_display_name():
else:
return _display_name
func set_display_name(name):
_display_name = name
func sanitize_value(property, type, value):
if property["type"] == TYPE_COLOR:
value = value.to_html()
elif property["type"] == TYPE_STRING:
value = value.json_escape()
return value
func update_property(property, value):
var data_singleton = Globals.get_singleton("data")
if data_singleton:
data_singleton.set_progress(_class, _id, property, value)
func _init(id):
self._id = id
func _notification(what):
print(what)
self._id = id

View File

@ -44,6 +44,7 @@ var type_names = {"STRING":TYPE_STRING, "BOOL":TYPE_BOOL, "COLOR":TYPE_COLOR, "O
func _init():
load_manager()
func load_manager():
initialize_variables()
load_config()
@ -111,7 +112,8 @@ func get_item_path(item):
return config_output_directory + "/" + item._class + "/" + item._id + "." + config_extension
func get_full_path(item):
return config_output_directory.replace("res://", "") + "/" + item._class + "/" + item._id + "." + config_extension
return Globals.globalize_path(config_output_directory + "/" + item._class + "/" + item._id + "." + config_extension)
# return config_output_directory.replace("res://", "") + "/" + item._class + "/" + item._id + "." + config_extension
func load_items():
items.clear()
@ -373,23 +375,18 @@ func duplicate_item(item, id, display_name):
items[new_item._class][new_item._id] = new_item
return new_item
# Rename the item, delete the old entrym overwrite the id and save anew
# TODO: Consider rename, could it still be referenced/locked somewhere?
# Rename the item, delete the old entry, overwrite the id and save anew
# TODO: Could it still be referenced/locked somewhere?
# TODO: Check for duplicate ids?
func rename_item(item, new_id):
new_id = sanitize_string(new_id)
if is_id_available(new_id):
var directory = Directory.new()
directory.remove(get_item_path(item))
if item._id == item._display_name:
item._display_name = new_id
save_item(item)
load_manager()
else:
pass # TODO: Issue warning
func is_id_available(id):
return true
var directory = Directory.new()
directory.remove(get_item_path(item))
if item._id == item._display_name:
item._display_name = new_id
item._id = new_id
save_item(item)
load_manager()
# Adds a custom property to an item.
# Returns true if it succeeded, false if it failed
@ -502,12 +499,9 @@ func rename_id_if_exists(item_class, id):
var number = 0
var current_name = id
while(true):
regex.find(current_name)
var id_without_number = regex.get_capture(1)
var number_at_end_string = regex.get_capture(2)
# var id_without_number = regex.search(current_name).get_string(1)
# var number_at_end_string = regex.search(current_name).get_string(2)
var number_at_end = int(number_at_end_string)
number = number + number_at_end + 1
var new_id = id_without_number + str(number)
@ -542,13 +536,11 @@ func rename_extension_of_all_items(new_extension, serializer):
directory.rename(original_item_path, new_item_path)
directory.remove(original_item_path)
load_config()
# load_manager()
save_all_items()
else:
directory.remove(original_item_path)
load_config()
save_all_items()
# load_manager()
pass
@ -575,6 +567,4 @@ func has_unsaved_items():
return false
# TODO: Lazy loading
# TODO: Proper path handling
# TODO: Arrays
# TODO: Proper renaming
# TODO: Arrays

View File

@ -175,7 +175,7 @@ margin/top = 0.0
margin/right = 20.0
margin/bottom = 20.0
popup/exclusive = false
items = [ "Add", ExtResource( 2 ), false, false, false, 0, 0, null, "", false, "Rename", ExtResource( 4 ), false, false, false, 1, 0, null, "", false, "Delete", ExtResource( 3 ), false, false, false, 2, 0, null, "", false, "Duplicate", ExtResource( 5 ), false, false, false, 3, 0, null, "", false, "Open File", ExtResource( 6 ), false, false, true, 4, 0, null, "", false ]
items = [ "Add", ExtResource( 2 ), false, false, false, 0, 0, null, "", false, "Rename", ExtResource( 4 ), false, false, false, 1, 0, null, "", false, "Delete", ExtResource( 3 ), false, false, false, 2, 0, null, "", false, "Duplicate", ExtResource( 5 ), false, false, false, 3, 0, null, "", false, "Open File", ExtResource( 6 ), false, false, false, 4, 0, null, "", false ]
[connection signal="button_down" from="Panel/VBox/Margin/HBoxContainer/AddButton" to="." method="_on_AddButton_button_down"]

BIN
screenshots/class.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

BIN
screenshots/editor.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB