diff options
Diffstat (limited to 'logic')
| -rw-r--r-- | logic/database.gd | 288 | ||||
| -rw-r--r-- | logic/database_entry.gd | 74 | ||||
| -rw-r--r-- | logic/file_picker.gd | 18 | ||||
| -rw-r--r-- | logic/menu.gd | 152 | ||||
| -rw-r--r-- | logic/stage.gd | 294 |
5 files changed, 826 insertions, 0 deletions
diff --git a/logic/database.gd b/logic/database.gd new file mode 100644 index 0000000..ed80ae7 --- /dev/null +++ b/logic/database.gd @@ -0,0 +1,288 @@ +extends TouchItemList +class_name Database + +const DATABASE_FILE_PATH: String = "user://database.json" +const DATABASE_FILE_VERSION: String = "SL_DB_V1" + +var db: Array +var selected_idx: int +var staged_idx: int + +onready var stage := get_node("/root/main/stage") # as Stage # Commented to avoid cyclic dependencies. +onready var delete_button := get_node("actions/delete") as Button +onready var edit_button := get_node("actions/edit") as Button +onready var add_button := get_node("actions/add") as Button +onready var popup := get_node("/root/main/popup") as ModalPopup +onready var dialog := get_node("/root/main/dialog") as Dialog + +func _init(): + selected_idx = -1 + staged_idx = -1 + + +func _ready(): + self.connect("item_selected", self, "item_selected") + self.connect("nothing_selected", self, "clear_selection") + + delete_button.connect("pressed", self, "delete_action") + edit_button.connect("pressed", self, "edit_action") + add_button.connect("pressed", self, "add_action") + + stage.connect("save", self, "save_stage") + stage.connect("discard", self, "discard_stage") + + load_database() + + +func get_entry_view(database_entry: Dictionary) -> String: + return "%9s | %4s | %04d-%02d" % [database_entry.process_id, database_entry.surgery_id, database_entry.date_year, database_entry.date_month] + + +func item_selected(index: int): + selected_idx = index + set_buttons_active(true) + + +func clear_selection(): + selected_idx = -1 + unselect_all() + set_buttons_active(false) + + +func set_buttons_active(active: bool): + (get_node("actions/delete") as Button).disabled = !active + (get_node("actions/edit") as Button).disabled = !active + + +func delete_action(): + if selected_idx < 0: + return + + dialog.setup("The entry #%d with process ID '%s' will be deleted from the database." % [selected_idx+1, db[selected_idx].process_id], "Delete", "No") + dialog.connect("accepted", self, "delete_action_confirmed") + popup.open_popup("Delete entry?", dialog) + + +func delete_action_confirmed(): + db.remove(selected_idx) + self.remove_item(selected_idx) + selected_idx = -1 + save_database() + clear_selection() + + +func edit_action(): + if selected_idx < 0: + return + + staged_idx = selected_idx + self.visible = false + stage.visible = true + var staged := (db[staged_idx] as Dictionary).duplicate(true) + stage.set_stage(staged, "Entry #%d" % (staged_idx+1)) + + +func add_action(): + self.visible = false + stage.visible = true + var staged := DatabaseEntry.instance_entry() + stage.set_stage(staged, "New entry") + + +func save_stage(entry: Dictionary): + if DatabaseEntry.is_valid_entry(entry) == false: + printerr("Invalid entry detected.") + return + + var next_selected_idx: int + if staged_idx >= 0: + db[staged_idx] = entry + next_selected_idx = staged_idx + else: + db.append(entry) + self.add_item(get_entry_view(entry)) + next_selected_idx = db.size() - 1 + + select(next_selected_idx) + emit_signal("item_selected", next_selected_idx) # Calling "select" does not trigger the "item_selected" signal. + set_item_text(selected_idx, get_entry_view(db[selected_idx])) + ensure_current_is_visible() + + save_database() + + staged_idx = -1 + self.visible = true + grab_focus() + + +func discard_stage(): + staged_idx = -1 + self.visible = true + grab_focus() + + +func save_database(file_path: String = DATABASE_FILE_PATH): + match file_path.get_extension(): + "json": + export_json(file_path, db) + + "csv": + export_csv(file_path, db) + + _: + printerr("Invalid database file extension '%s', expected 'json' or 'csv'." % file_path.get_file()) + return + + +static func export_json(file_path: String, data: Array): + var database_file := { + "version": DATABASE_FILE_VERSION, + "database": data, + } + var indentation_char := "" if file_path == DATABASE_FILE_PATH else "\t" + var file_content := JSON.print(database_file, indentation_char) + + var file := File.new() + file.open(file_path, File.WRITE) + file.store_string(file_content) + file.close() + + +static func export_csv(file_path: String, data: Array): + var file := File.new() + file.open(file_path, File.WRITE) + var header := PoolStringArray(DatabaseEntry.ENTRY_PROTOTYPE.keys()) + file.store_csv_line(header) + for it in data: + file.store_csv_line(it.values()) + file.close() + + +func load_database(file_path: String = DATABASE_FILE_PATH): + match file_path.get_extension(): + "json": + clear_database() + db = import_json(file_path) + if file_path != DATABASE_FILE_PATH: + sanitize_database(db) + + "csv": + clear_database() + db = import_csv(file_path) + + _: + printerr("Invalid database file extension '%s', expected 'json' or 'csv'." % file_path.get_file()) + return + + for it in db: + # The JSON specification does not define integer or float types, but only a number type. + # Therefore, converting a Variant to JSON text will convert all numerical values to float types. + # Thus, we cast all integer values once we load them. + it["date_year"] = int(it["date_year"]) + it["date_month"] = int(it["date_month"]) + it["date_day"] = int(it["date_day"]) + + self.add_item(get_entry_view(it)) + + +static func sanitize_database(database: Array): + var blueprint := DatabaseEntry.ENTRY_PROTOTYPE + + for entry in database: + # Delete extra keys. + var keys_to_delete: Array + for key in entry: + if blueprint.has(key) == false: + keys_to_delete.append(key) + for key in keys_to_delete: + entry.erase(key) + + # Fix wrong value types. + for key in blueprint: + if entry.has(key) == false || typeof(entry[key]) != typeof(blueprint[key]): + entry[key] = blueprint[key] + + +static func import_json(file_path: String) -> Array: + var result := [] + + var file := File.new() + var error := file.open(file_path, File.READ_WRITE) + if error != OK: + printerr("Failed to open database file '%s' (error %d)." % [file_path, error]) + return result + var file_content = file.get_as_text() + file.close() + var parse_result = JSON.parse(file_content) + + if parse_result.error != OK: + printerr("Failed to parse database file '%s' (error %d)." % [file_path, parse_result.error]) + return result + + if typeof(parse_result.result) != TYPE_DICTIONARY: + printerr("Invalid database file type '%s', expected '%s'." % [typeof(parse_result.result), TYPE_DICTIONARY]) + return result + + if parse_result.result["version"] != DATABASE_FILE_VERSION: + printerr("Invalid database file version '%s', expected '%s'." % [parse_result.result["version"], DATABASE_FILE_VERSION]) + return result + + if typeof(parse_result.result["database"]) != TYPE_ARRAY: + printerr("Invalid database content type '%s', expected '%s'." % [typeof(parse_result.result["database"]), TYPE_ARRAY]) + return result + + result = parse_result.result["database"] + return result + + +static func import_csv(file_path: String) -> Array: + var result: Array + var file := File.new() + file.open(file_path, File.READ_WRITE) + var headers := file.get_csv_line() + while file.get_position() < file.get_len(): + var csv_entry := file.get_csv_line() + var entry = DatabaseEntry.instance_entry() + for idx in headers.size(): + var field_name := headers[idx] + var field_value := csv_entry[idx] + match field_name: + "date": + DatabaseEntry.set_entry_date(entry, field_value) + "date_year", "date_month", "date_day": + entry[field_name] = int(field_value) + "is_urgency": + entry[field_name] = true if field_value.strip_edges().to_lower() == "true" else false + _: + if DatabaseEntry.ENTRY_PROTOTYPE.has(field_name): + entry[field_name] = field_value + if DatabaseEntry.is_valid_entry(entry): + result.append(entry) + file.close() + return result + + +func clear_database(): + clear_selection() + self.clear() + db.resize(0) + + +#func DEBUG_create_fake_database(): +# clear_database() +# for idx in range(100): +# var today := OS.get_date(true) +# var date_year = today.year + int(float(idx) / 30.0 / 12) +# var date_month = 1 + int(float(idx) / 30.0) % 12 +# var date_day = 1 + (idx % 30) +# var fake_entry = DatabaseEntry.instance_entry({ +# "process_id": "%06d" % idx, +# "surgery_id": "s%05d" % idx, +# "date_year": date_year, +# "date_month": date_month, +# "date_day": date_day, +# }) +# db.append(fake_entry) +# self.add_item(get_entry_view(fake_entry)) + + diff --git a/logic/database_entry.gd b/logic/database_entry.gd new file mode 100644 index 0000000..8b0c51f --- /dev/null +++ b/logic/database_entry.gd @@ -0,0 +1,74 @@ +extends Reference +class_name DatabaseEntry + +const DATE_SEPARATOR: String = "-" +const DATE_FORMAT: String = "%04d-%02d-%02d" +const ENTRY_PROTOTYPE: Dictionary = { + "process_id": "", + "surgery_id": "", + "date_year": 1, + "date_month": 1, + "date_day": 1, + "place": "", + "anesthesia": "", + "first_assistant": "", + "type": "", + "sub_type": "", + "sub_sub_type": "", + "pathology": "", + "intervention": "", + "is_urgency": false, + "notes": "", +} + + +static func instance_entry(params: Dictionary = {}) -> Dictionary: + var new_entry := ENTRY_PROTOTYPE.duplicate(true) + new_entry.process_id = params.get("process_id", "") + new_entry.surgery_id = params.get("surgery_id", "") + + var today = OS.get_date() + new_entry.date_year = params.get("date_year", today.year) + new_entry.date_month = params.get("date_month", today.month) + new_entry.date_day = params.get("date_day", today.day) + + new_entry.place = params.get("place", "") + new_entry.anesthesia = params.get("anesthesia", "") + new_entry.first_assistant = params.get("first_assistant", "") + new_entry.type = params.get("type", "") + new_entry.sub_type = params.get("sub_type", "") + new_entry.sub_sub_type = params.get("sub_sub_type", "") + new_entry.pathology = params.get("pathology", "") + new_entry.intervention = params.get("intervention", "") + new_entry.is_urgency = params.get("is_urgency", false) + new_entry.notes = params.get("notes", "") + + return new_entry + + +static func is_valid_entry(entry: Dictionary) -> bool: + var is_valid: bool + + is_valid = entry.has_all(ENTRY_PROTOTYPE.keys()) && ENTRY_PROTOTYPE.keys().size() == entry.keys().size() + + for it in ENTRY_PROTOTYPE.keys(): + if typeof(ENTRY_PROTOTYPE[it]) != typeof(entry[it]): + is_valid = false + break + + return is_valid + + +static func get_entry_date(entry: Dictionary) -> String: + return DATE_FORMAT % [entry.date_year, entry.date_month, entry.date_day] + + +static func set_entry_date(entry: Dictionary, date: String): + date = date.strip_edges().replace(" ", DATE_SEPARATOR).replace("/", DATE_SEPARATOR).replace("\\", DATE_SEPARATOR) + var year_month_idx := date.find(DATE_SEPARATOR) + var month_day_idx := date.find(DATE_SEPARATOR, year_month_idx + 1) + entry.date_year = int(date.substr(0, year_month_idx)) + entry.date_month = int(date.substr(year_month_idx + 1, month_day_idx - year_month_idx - 1)) + entry.date_day = int(date.substr(month_day_idx + 1)) + + diff --git a/logic/file_picker.gd b/logic/file_picker.gd new file mode 100644 index 0000000..9715d0d --- /dev/null +++ b/logic/file_picker.gd @@ -0,0 +1,18 @@ +extends FileDialog + +export var clear_signals_on_hide := true + + +func _init(): + self.connect("hide", self, "_clear_signals") + + +func _clear_signals(): + if clear_signals_on_hide == false: + return + + for signal_name in ["file_selected"]: + for it in get_signal_connection_list(signal_name): + disconnect(it.signal, it.target, it.method) + + diff --git a/logic/menu.gd b/logic/menu.gd new file mode 100644 index 0000000..4b0b7b3 --- /dev/null +++ b/logic/menu.gd @@ -0,0 +1,152 @@ +extends MenuButton + +const LOGS_FILE_PATH: String = "user://logs/godot.log" +const menu_items: Array = [ + { label = "Import Option Sets", action = "import_option_sets_action" }, + { label = "Export Option Sets", action = "export_option_sets_action" }, + { label = "Clear Option Sets", action = "clear_option_sets_action" }, + { label = "Import Database", action = "import_database_action" }, + { label = "Export Database", action = "export_database_action" }, + { label = "Clear Database", action = "clear_database_action" }, + { label = "Export App Log", action = "export_app_log_action" }, + { label = "About", action = "about_action" }, +] +const license_font_b612: String = "res://licenses/font_b612.txt" +const license_godot: String = "res://licenses/godot.txt" + +onready var menu := get_popup() as PopupMenu +onready var popup := get_node("/root/main/popup") as ModalPopup +onready var dialog := get_node("/root/main/dialog") as Dialog +onready var file_picker := get_node("/root/main/file_picker") as FileDialog +onready var database := get_node("/root/main/database") as Database +onready var stage := get_node("/root/main/stage") as Stage + + +func _ready(): + for idx in range(menu_items.size()): + menu.add_item(menu_items[idx].label, idx) + menu.connect("id_pressed", self, "id_pressed") + + +func id_pressed(id: int): + self.call_deferred(menu_items[id].action) + + +func import_option_sets_action(): + dialog.setup("All option sets from the dropdown menus will be replaced.", "Continue", "No") + dialog.connect("accepted", self, "import_option_sets_action_accepted") + popup.open_popup("Replace option sets?", dialog) + + +func import_option_sets_action_accepted(): + file_picker.mode = FileDialog.MODE_OPEN_FILE + file_picker.current_dir = OS.get_system_dir(OS.SYSTEM_DIR_DOWNLOADS) + file_picker.filters = ["*.json", "*.csv"] + file_picker.current_file = "" + file_picker.connect("file_selected", self, "import_option_sets") + file_picker.show_modal(true) + file_picker.invalidate() + + +func import_option_sets(file_path: String): + stage.load_option_sets(file_path) + stage.save_option_sets() + + +func export_option_sets_action(): + file_picker.mode = FileDialog.MODE_SAVE_FILE + file_picker.current_dir = OS.get_system_dir(OS.SYSTEM_DIR_DOWNLOADS) + file_picker.filters = ["*.json"] + file_picker.current_file = "" + file_picker.connect("file_selected", stage, "save_option_sets") + file_picker.show_modal(true) + file_picker.invalidate() + + +func clear_option_sets_action(): + dialog.setup("All option sets from the dropdown menus will be deleted.", "Delete all", "No") + dialog.connect("accepted", self, "clear_option_sets") + popup.open_popup("Clear option sets?", dialog) + + +func clear_option_sets(): + stage.clear_option_sets() + stage.save_option_sets() + + +func import_database_action(): + dialog.setup("All entries from the database will be replaced.", "Continue", "No") + dialog.connect("accepted", self, "import_database_action_accepted") + popup.open_popup("Replace database?", dialog) + + +func import_database_action_accepted(): + file_picker.mode = FileDialog.MODE_OPEN_FILE + file_picker.current_dir = OS.get_system_dir(OS.SYSTEM_DIR_DOWNLOADS) + file_picker.filters = ["*.json", "*.csv"] + file_picker.current_file = "" + file_picker.connect("file_selected", self, "import_database") + file_picker.show_modal(true) + file_picker.invalidate() + + +func import_database(file_path: String): + database.load_database(file_path) + database.save_database() + + +func export_database_action(): + file_picker.mode = FileDialog.MODE_SAVE_FILE + file_picker.current_dir = OS.get_system_dir(OS.SYSTEM_DIR_DOWNLOADS) + file_picker.filters = ["*.csv"] + file_picker.current_file = "" + file_picker.connect("file_selected", database, "save_database") + file_picker.show_modal(true) + file_picker.invalidate() + + +func clear_database_action(): + dialog.setup("All entries from the database will be deleted.", "Delete all", "No") + dialog.connect("accepted", self, "clear_database") + popup.open_popup("Clear database?", dialog) + + +func clear_database(): + database.clear_database() + database.save_database() + + +func export_app_log_action(): + file_picker.mode = FileDialog.MODE_SAVE_FILE + file_picker.current_dir = OS.get_system_dir(OS.SYSTEM_DIR_DOWNLOADS) + file_picker.filters = ["*.log"] + file_picker.current_file = "" + file_picker.connect("file_selected", self, "export_app_log") + file_picker.show_modal(true) + file_picker.invalidate() + + +func export_app_log(file_path: String): + var error : int + var file := File.new() + + error = file.open(LOGS_FILE_PATH, File.READ) + if error != OK: + printerr("Failed to open log file '%s' (error %d)." % [LOGS_FILE_PATH, error]) + return + var file_content = file.get_as_text() + file.close() + + error = file.open(file_path, File.WRITE) + if error != OK: + printerr("Failed to open file '%s' to write log (error %d)." % [file_path, error]) + return + file.store_string(file_content) + file.close() + + +func about_action(): + dialog.setup("Surgery Log\nversion %s" % ProjectSettings.get_setting("global/version"), "", "") + popup.open_popup("About", dialog) + + diff --git a/logic/stage.gd b/logic/stage.gd new file mode 100644 index 0000000..e6d9474 --- /dev/null +++ b/logic/stage.gd @@ -0,0 +1,294 @@ +extends TouchVerticalContainer +class_name Stage + +signal save # (database_entry: Dictionary) +signal discard # () + +const OPTION_SETS_FILE_PATH: String = "user://option_sets.json" +const OPTION_SETS_FILE_VERSION: String = "SL_OS_V1" +const OPTION_SETS_NOT_AVAILABLE: String = "--" +const OPTION_SETS_TREE_STRUCTURE := { + "place": null, + "anesthesia": null, + "first_assistant": null, + "type": { + "sub_type": { + "sub_sub_type": null, + "pathology": null, + "intervention": null, + } + } +} + +var staged_entry_hash: int +var option_sets: Dictionary + +onready var title := get_node("controls/title") as Label +onready var process_id := get_node("controls/process_id") as LineEdit +onready var surgery_id := get_node("controls/surgery_id") as LineEdit +onready var date := get_node("controls/date_picker") as DatePicker +onready var place := get_node("controls/place") as OptionSet +onready var anesthesia := get_node("controls/anesthesia") as OptionSet +onready var first_assistant := get_node("controls/first_assistant") as OptionSet +onready var type := get_node("controls/type") as OptionSet +onready var sub_type := get_node("controls/sub_type") as OptionSet +onready var sub_sub_type := get_node("controls/sub_sub_type") as OptionSet +onready var pathology := get_node("controls/pathology") as OptionSet +onready var intervention := get_node("controls/intervention") as OptionSet +onready var is_urgency := get_node("controls/is_urgency") as Button +onready var notes := get_node("controls/notes") as LineEdit +onready var save_button := get_node("controls/buttons/save") as Button +onready var discard_button := get_node("controls/buttons/discard") as Button +onready var scrollbar := get_v_scrollbar() +onready var popup := get_node("/root/main/popup") as ModalPopup +onready var dialog := get_node("/root/main/dialog") as Dialog + + +func _init(): + exclude_controls = ["date_picker", "save", "discard"] + load_option_sets() + + +func _ready(): + # Fix height of buttons container. + (get_node("controls/buttons") as Control).rect_min_size.y = save_button.rect_size.y + + scrollbar.connect("visibility_changed", self, "adjust_layout") + save_button.connect("pressed", self, "save_action") + discard_button.connect("pressed", self, "discard_action") + + for it in controls.get_children(): + if it is LineEdit: + it.connect("focus_entered", it, "set", ["caret_position", it.max_length]) + it.connect("focus_exited", it, "deselect") + + # Map option sets buttons. + var option_sets_map := { + "place": place, + "anesthesia": anesthesia, + "first_assistant": first_assistant, + "type": type, + "sub_type": sub_type, + "sub_sub_type": sub_sub_type, + "pathology": pathology, + "intervention": intervention + } + for key in option_sets_map: + var button := option_sets_map[key].get_node("button") as Button + button.connect("pressed", self, "show_options", [key]) + + +func adjust_layout(): + var margin_size := scrollbar.rect_size.x + self.margin_left = margin_size + self.margin_top = margin_size + self.margin_bottom = -margin_size + self.margin_right = 0.0 if scrollbar.visible else -margin_size + + +func show_options(field: String): + var option_set_field := self[field] as OptionSet + var options: Array + + match field: + "sub_type": + options = option_sets.get("type", {}).get(type.text, {}).get(field, {}).keys() + "sub_sub_type", "pathology", "intervention": + options = option_sets.get("type", {}).get(type.text, {}).get("sub_type", {}).get(sub_type.text, {}).get(field, {}).keys() + _: + options = option_sets.get(field, {}).keys() + + options.sort() + option_set_field.show_options(options) + + +func save_action(): + self.visible = false + var staged_entry := get_stage() + gather_option_sets(staged_entry, option_sets) + save_option_sets() + emit_signal("save", staged_entry) + + +func discard_action(): + if get_stage().hash() != staged_entry_hash: + dialog.setup("Changes made to this entry will be discarded.", "Discard", "No") + dialog.connect("accepted", self, "discard_action_confirmed") + popup.open_popup("Discard changes?", dialog) + else: + discard_action_confirmed() + + +func discard_action_confirmed(): + self.visible = false + emit_signal("discard") + + +func set_stage(entry: Dictionary, title: String): + self.title.text = title + staged_entry_hash = entry.hash() + process_id.text = entry.process_id + surgery_id.text = entry.surgery_id + date.set_date(entry.date_year, entry.date_month, entry.date_day) + place.text = entry.place + anesthesia.text = entry.anesthesia + first_assistant.text = entry.first_assistant + type.text = entry.type + sub_type.text = entry.sub_type + sub_sub_type.text = entry.sub_sub_type + pathology.text = entry.pathology + intervention.text = entry.intervention + is_urgency.pressed = entry.is_urgency + notes.text = entry.notes + self.scroll_vertical = 0 + + +func get_stage() -> Dictionary: + var entry := { + "process_id": process_id.text, + "surgery_id": surgery_id.text, + "date_year": date.get_year(), + "date_month": date.get_month(), + "date_day": date.get_day(), + "place": place.text, + "anesthesia": anesthesia.text, + "first_assistant": first_assistant.text, + "type": type.text, + "sub_type": sub_type.text, + "sub_sub_type": sub_sub_type.text, + "pathology": pathology.text, + "intervention": intervention.text, + "is_urgency": is_urgency.pressed, + "notes": notes.text, + } + return entry + + +static func sanitize_option_sets(entry: Dictionary, blueprint: Dictionary = OPTION_SETS_TREE_STRUCTURE): + # Delete extra keys. + var keys_to_delete: Array + for key in entry: + if blueprint.has(key) == false: + keys_to_delete.append(key) + for key in keys_to_delete: + entry.erase(key) + + for key in blueprint: + # Add missing keys. + if typeof(entry.get(key)) != TYPE_DICTIONARY: + entry[key] = {} + # Process sub-keys + if blueprint[key] != null: + for sub_key in entry[key]: + if typeof(entry[key][sub_key]) != TYPE_DICTIONARY: + entry[key][sub_key] = {} + sanitize_option_sets(entry[key][sub_key], blueprint[key]) + + +static func gather_option_sets(entry: Dictionary, target: Dictionary, blueprint: Dictionary = OPTION_SETS_TREE_STRUCTURE): + for key in blueprint: + if target.get(key) == null: + target[key] = {} + + var value := (entry[key] as String).strip_edges() + if value == "" || value == OPTION_SETS_NOT_AVAILABLE: + continue + + if target[key].has(value) == false: + target[key][value] = null if blueprint[key] == null else {} + + if blueprint[key] != null: + gather_option_sets(entry, target[key][value], blueprint[key]) + + +func save_option_sets(file_path: String = OPTION_SETS_FILE_PATH): + match file_path.get_extension(): + "json": + export_json(file_path, option_sets) + +# "csv": +# export_csv(file_path, option_sets) + + _: + printerr("Invalid option sets file extension '%s', expected 'json'." % file_path.get_file()) + return + + +static func export_json(file_path: String, data: Dictionary): + var option_sets_file := { + "version": OPTION_SETS_FILE_VERSION, + "option_sets": data, + } + var indentation_char := "" if file_path == OPTION_SETS_FILE_PATH else "\t" + var file_content := JSON.print(option_sets_file, indentation_char) + + var file := File.new() + file.open(file_path, File.WRITE) + file.store_string(file_content) + file.close() + + +#static func export_csv(file_path: String, data: Dictionary): +# pass + + +func load_option_sets(file_path: String = OPTION_SETS_FILE_PATH): + match file_path.get_extension(): + "json": + clear_option_sets() + option_sets = import_json(file_path) + if file_path != OPTION_SETS_FILE_PATH: + sanitize_option_sets(option_sets) + + "csv": + clear_option_sets() + option_sets = import_csv(file_path) + + _: + printerr("Invalid option sets file extension '%s', expected 'json' or 'csv'." % file_path.get_file()) + return + + +static func import_json(file_path: String) -> Dictionary: + var result := {} + + var file := File.new() + var error := file.open(file_path, File.READ_WRITE) + if error != OK: + printerr("Failed to open option sets file '%s' (error %d)." % [file_path, error]) + return result + var file_content = file.get_as_text() + file.close() + var parse_result = JSON.parse(file_content) + + if parse_result.error != OK: + printerr("Failed to parse option sets file '%s' (error %d)." % [file_path, parse_result.error]) + return result + + if typeof(parse_result.result) != TYPE_DICTIONARY: + printerr("Invalid option sets file type '%s', expected '%s'." % [typeof(parse_result.result), TYPE_DICTIONARY]) + return result + + if parse_result.result["version"] != OPTION_SETS_FILE_VERSION: + printerr("Invalid option sets file version '%s', expected '%s'." % [parse_result.result["version"], OPTION_SETS_FILE_VERSION]) + return result + + if typeof(parse_result.result["option_sets"]) != TYPE_DICTIONARY: + printerr("Invalid option sets content type '%s', expected '%s'." % [typeof(parse_result.result["option_sets"]), TYPE_DICTIONARY]) + return result + + result = parse_result.result["option_sets"] + return result + + +static func import_csv(file_path: String) -> Dictionary: + var result := {} + for it in Database.import_csv(file_path): + gather_option_sets(it, result) + return result + + +func clear_option_sets(): + option_sets = {} + + |
