diff --git a/samples/core/editors/level_editor/level_editor.lua b/samples/core/editors/level_editor/level_editor.lua index 82afc3b6c8..b88e98b0a1 100644 --- a/samples/core/editors/level_editor/level_editor.lua +++ b/samples/core/editors/level_editor/level_editor.lua @@ -244,7 +244,9 @@ end function Selection:clear() for k, v in pairs(self._ids) do - LevelEditor._objects[v]:on_selected(false) + if LevelEditor._objects[v] ~= nil then + LevelEditor._objects[v]:on_selected(false) + end end self._ids = {} end @@ -285,7 +287,9 @@ end function Selection:set(ids) self:clear() for k, v in pairs(ids) do - LevelEditor._objects[v]:on_selected(true) + if LevelEditor._objects[v] ~= nil then + LevelEditor._objects[v]:on_selected(true) + end end self._ids = ids end diff --git a/tools/core/guid.vala b/tools/core/guid.vala index 50f00ec9eb..8383c5af56 100644 --- a/tools/core/guid.vala +++ b/tools/core/guid.vala @@ -13,23 +13,34 @@ public struct Guid public static Guid new_guid() { - // FIXME: Replace Rand with something better. - Rand rnd = new Rand(); - uint64 a = rnd.next_int(); - uint64 b = rnd.next_int(); - uint64 c = rnd.next_int(); - uint64 d = rnd.next_int(); + Guid new_guid = { 0, 0 }; - uint64 d1; - d1 = a << 32; - d1 |= b << 0; - uint64 d2; - d2 = c << 32; - d2 |= d << 0; + do + { + // FIXME: Replace Rand with something better. + Rand rnd = new Rand(); + uint64 a = rnd.next_int(); + uint64 b = rnd.next_int(); + uint64 c = rnd.next_int(); + uint64 d = rnd.next_int(); + + uint64 d1; + d1 = a << 32; + d1 |= b << 0; + uint64 d2; + d2 = c << 32; + d2 |= d << 0; + + d1 = (d1 & 0xffffffffffff4fffu) | 0x4000u; + d2 = (d2 & 0x3fffffffffffffffu) | 0x8000000000000000u; + + new_guid = { d1, d2 }; + } + while (new_guid == GUID_NONE_FOLDER + || new_guid == GUID_UNIT_FOLDER + || new_guid == GUID_SOUND_FOLDER); - d1 = (d1 & 0xffffffffffff4fffu) | 0x4000u; - d2 = (d2 & 0x3fffffffffffffffu) | 0x8000000000000000u; - return { d1, d2 }; + return new_guid; } public static Guid parse(string guid) diff --git a/tools/level_editor/level.vala b/tools/level_editor/level.vala index 85a57c7200..6de6cf2419 100644 --- a/tools/level_editor/level.vala +++ b/tools/level_editor/level.vala @@ -2,7 +2,6 @@ * Copyright (c) 2012-2025 Daniele Bartolini et al. * SPDX-License-Identifier: GPL-3.0-or-later */ - using Gee; namespace Crown @@ -10,6 +9,21 @@ namespace Crown /// Manages objects in a level. public class Level { + public class Folder + { + public Guid id { get; set; } + public string name { get; set; } + public Guid parent_id { get; set; } + + // Constructor + public Folder(Guid id, string name, Guid parent_id = GUID_ZERO) + { + this.id = id; + this.name = name; + this.parent_id = parent_id; + } + } + public Project _project; // Engine connections @@ -31,13 +45,17 @@ public class Level public double _camera_orthographic_size; public double _camera_target_distance; public CameraViewType _camera_view_type; + public Gee.ArrayList _folders; // Signals public signal void selection_changed(Gee.ArrayList selection); public signal void object_editor_name_changed(Guid object_id, string name); + public signal void level_loaded(); public Level(Database db, RuntimeInstance runtime, Project project) { + _folders = new Gee.ArrayList(); + _project = project; // Engine connections @@ -89,6 +107,7 @@ public class Level camera_decode(); GLib.Application.get_default().activate_action("camera-view", new GLib.Variant.int32(_camera_view_type)); + _name = name; _path = path; @@ -103,19 +122,94 @@ public class Level // FIXME: hack to keep the LevelTreeView working. _db.key_changed(_id, "units"); + load_folders(); + + level_loaded(); + return 0; } + public void load_folders() { + _folders.clear(); + + HashSet folder_ids = _db.get_property_set(_id, "folders", new HashSet()); + bool needToSave = false; + + foreach (var root_info in get_root_folder_info()) { + needToSave |= ensure_root_folder(folder_ids, root_info.guid, root_info.name, root_info.object_type); + } + if (_path != null && needToSave) { + _db.save(_path, _id); + } + + foreach (Guid folder_id in folder_ids) { + _folders.add(load_or_create_folder(folder_id)); + } + } + + private bool ensure_root_folder(HashSet folder_ids, Guid folder_id, string name, string type) { + if (!folder_ids.contains(folder_id)) { + if (_path != null) { + add_folder(folder_id, name, type, GUID_ZERO); + } + folder_ids.add(folder_id); + return true; + } + return false; + } + + private Folder load_or_create_folder(Guid folder_id) { + if (_db.has_object(folder_id)) { + return new Folder( + folder_id, + _db.get_property_string(folder_id, "editor.name"), + _db.get_property_guid(folder_id, "parent_folder") + ); + } else { + foreach (var root_info in get_root_folder_info()) { + if (folder_id == root_info.guid) { + // TODO AVOID THIS TO PREVENT "NEED TO SAVE TRIGGER" , but for now its needed to fix crash at Value? item_type = view_i.db.get_property(guid, "_type"); from on_drag_data_get_internal + _db.create(folder_id,root_info.object_type); + _db.set_property(folder_id, "editor.name", root_info.name); + _db.set_property(folder_id, "parent_folder", GUID_ZERO); + _db.set_property(folder_id, "_type", root_info.object_type); + return new Folder(folder_id, root_info.name, GUID_ZERO); + } + } + print("ERROR: Unknown Folder ID - %s", folder_id.to_string()); + return null; + } + } + public void save(string name) { string path = Path.build_filename(_project.source_dir(), name + ".level"); + save_folders(); + camera_encode(); _db.save(path, _id); _path = path; _name = name; } + public void save_folders() + { + foreach (Folder folder in _folders) { + _db.set_property_string(folder.id, "editor.name", folder.name); + _db.set_property_guid(folder.id, "parent_folder", folder.parent_id); + _db.add_to_set(_id, "folders", folder.id); + } + } + public void add_folder(Guid id, string name, string type, Guid parent_id = GUID_ZERO) + { + _db.create(id, type); + _db.set_property_string(id, "editor.name", name); + _db.set_property_guid(id, "parent_folder", parent_id); + _db.add_to_set(_id, "folders", id); + _folders.add(new Folder(id, name, parent_id)); + } + public void spawn_empty_unit() { Guid id = Guid.new_guid(); @@ -176,14 +270,16 @@ public class Level selection_set(ids); } - public void on_unit_spawned(Guid id, string? name, Vector3 pos, Quaternion rot, Vector3 scl) - { - Unit unit = Unit(_db, id); - unit.create(name, pos, rot, scl); - - _db.set_property_string(id, "editor.name", "unit_%04u".printf(_num_units++)); - _db.add_to_set(_id, "units", id); - } + public void on_unit_spawned(Guid id, string? name, Vector3 pos, Quaternion rot, Vector3 scl) + { + Unit unit = Unit(_db, id); + unit.create(name, pos, rot, scl); + + _db.set_property_string(id, "editor.name", "unit_%04u".printf(_num_units++)); + _db.add_to_set(_id, "units", id); + + _db.set_property_guid(id, "parent_folder", GUID_UNIT_FOLDER); + } public void on_sound_spawned(Guid id, string name, Vector3 pos, Quaternion rot, Vector3 scl, double range, double volume, bool loop) { @@ -192,6 +288,8 @@ public class Level _db.set_property_string (id, "editor.name", "sound_%04u".printf(_num_sounds++)); _db.add_to_set(_id, "sounds", id); + + _db.set_property_guid(id, "parent_folder", GUID_SOUND_FOLDER); } public void on_move_objects(Guid?[] ids, Vector3[] positions, Quaternion[] rotations, Vector3[] scales) diff --git a/tools/level_editor/level_tree_organization.vala b/tools/level_editor/level_tree_organization.vala new file mode 100644 index 0000000000..33448ee64d --- /dev/null +++ b/tools/level_editor/level_tree_organization.vala @@ -0,0 +1,290 @@ +/*TODOS +fix ctrl + z and probably ctrl + y causing crash if i move an object into a subfolder and click on ctrl + z +support can drag and drop multiple elements at once easily + +made can dupplicate folder (like unit) +made can remove folder (like unit) +support undo/redo feature +support folder drag and drop +/* +* Copyright (c) 2012-2025 Daniele Bartolini et al. +* SPDX-License-Identifier: GPL-3.0-or-later +*/ +using Gee; +using Gtk; + +namespace Crown +{ + // Manage drag and drop and folder organization for the LevelTreeView + public class LevelTreeOrganization + { + public static bool on_drag_drop_internal(LevelTreeView view_i, string d_target, Gtk.Widget widget, Gdk.DragContext context, int x, int y, uint time) { + Gtk.drag_get_data(widget, context, Gdk.Atom.intern(d_target, false), time); + Signal.stop_emission_by_name(view_i.tree_view, "drag-drop"); + return true; + } + + public static bool on_drag_motion_internal(LevelTreeView view_i, Gtk.Widget widget, Gdk.DragContext context, int x, int y, uint time) { + Gtk.TreePath path; + Gtk.TreeViewDropPosition pos; + if (view_i.tree_view.get_dest_row_at_pos(x, y, out path, out pos)) { + view_i.tree_view.set_drag_dest_row(path, pos); + Gdk.drag_status(context, Gdk.DragAction.MOVE, time); + return true; + } + return false; + } + + public static void on_drag_data_get_internal(LevelTreeView view_i, string d_target, Gtk.Widget widget, Gdk.DragContext context, Gtk.SelectionData selection_data, uint info, uint time) { + Gtk.TreeModel model; + GLib.List paths = view_i.tree_selection.get_selected_rows(out model); + StringBuilder data_builder = new StringBuilder(); + string source_set = null; + + foreach (Gtk.TreePath path in paths) { + Gtk.TreeIter iter; + model.get_iter(out iter, path); + + // Retrieve the GUID + Value guid_val; + model.get_value(iter, LevelTreeView.Column.GUID, out guid_val); + Guid guid = (Guid)guid_val; + + Value? item_type = view_i.db.get_property(guid, "_type"); + + string type_str = (string)item_type; + + if (type_str.has_suffix("_folder")) { + print("Folder drag ignored: %s\n", type_str); + Gtk.drag_cancel(context); + return; + } + + // Find the corresponding source_set + bool type_found = false; + foreach (var root_info in get_root_folder_info()) { + if (type_str == root_info.contains_item_type_str) { + source_set = root_info.contains_source_set_str; + type_found = true; + break; + } + } + + if (!type_found) { + print("Unhandled type: %s\n", type_str); + continue; + } + + data_builder.append(guid.to_string() + ","); + } + + if (data_builder.len > 0) { + data_builder.truncate(data_builder.len - 1); // Remove trailing comma + string data_str = @"$source_set;$(data_builder.str)"; + var target = Gdk.Atom.intern(d_target, false); + selection_data.set(target, 8, data_str.data); + } else { + print("No draggable items - cancelling drag operation."); + Gtk.drag_finish(context, false, false, time); + } + } + + // TODO SUPPORT FOLDER DRAG AND DROPPING LATER + public static void on_drag_data_received_internal(LevelTreeView view_i, string d_target, Gtk.Widget widget, Gdk.DragContext context, int x, int y, Gtk.SelectionData selection_data, uint info, uint time) { + view_i.tree_selection.changed.disconnect(view_i.on_tree_selection_changed); + Signal.stop_emission_by_name(view_i.tree_view, "drag-data-received"); + + var expected_target = Gdk.Atom.intern(d_target, false); + if (selection_data.get_target() != expected_target) { + Gtk.drag_finish(context, false, false, time); + view_i.tree_selection.changed.connect(view_i.on_tree_selection_changed); + return; + } + + string raw_data = (string)selection_data.get_data(); + if (raw_data == null || raw_data.strip() == null) { + print("ERROR: Received empty or null drag data (if you tried to drag and dropped a folder its normal that you had this error ,for now its not supported).\n"); + Gtk.drag_finish(context, false, false, time); + view_i.tree_selection.changed.connect(view_i.on_tree_selection_changed); + return; + } + + raw_data = raw_data.strip(); + + string[] data = raw_data.split(";", 2); + + string source_set = data[0].strip(); + + string[] guids = data[1].split(","); + + Gtk.TreePath path; + Gtk.TreeViewDropPosition pos; + if (!view_i.tree_view.get_dest_row_at_pos(x, y, out path, out pos)) { + Gtk.drag_finish(context, false, false, time); + view_i.tree_selection.changed.connect(view_i.on_tree_selection_changed); + return; + } + Gtk.TreeIter target_iter; + view_i.tree_sort.get_iter(out target_iter, path); + string target_parent_guid = ""; + Value target_guid_val; + Value target_type_val; + view_i.tree_sort.get_value(target_iter, LevelTreeView.Column.TYPE, out target_type_val); + if (target_type_val == LevelTreeView.ItemType.FOLDER) { + view_i.tree_sort.get_value(target_iter, LevelTreeView.Column.GUID, out target_guid_val); + target_parent_guid = ((Guid)target_guid_val).to_string(); + } else { + view_i.tree_selection.changed.connect(view_i.on_tree_selection_changed); + return; + } + + string? moving_type = null; + foreach (string guid_str in guids) { + if (guid_str.strip().length == 0) continue; + Guid guid = Guid.parse(guid_str); + + Value? parent_guid_val = view_i.db.get_property(guid, "parent_folder"); + Guid parent_guid; + + if (parent_guid_val == null) { + string item_type = (string)view_i.db.get_property(guid, "_type"); + parent_guid = GUID_ZERO; + foreach (var root_info in get_root_folder_info()) { + if (item_type == root_info.contains_item_type_str) { + parent_guid = root_info.guid; + break; + } + } + + if (parent_guid == GUID_ZERO) { + print("Error: Unable to determine the root parent of element %s", guid.to_string()); + continue; + } + } else { + parent_guid = (Guid)parent_guid_val; + } + + Value? parent_type_val = view_i.db.get_property(parent_guid, "_type"); + string parent_type = (string)parent_type_val; + moving_type = parent_type; + + Guid target_guid; + if (!Guid.try_parse(target_parent_guid, out target_guid)) { + print("Error: Invalid target GUID for parent %s", target_parent_guid); + view_i.tree_selection.changed.connect(view_i.on_tree_selection_changed); + return; + } + string target_type = null; + foreach (var root_info in get_root_folder_info()) { + if (Guid.equal_func(target_guid, root_info.guid)) { + target_type = root_info.object_type; + break; + } + } + if (target_type == null || target_type.strip().length == 0) { + Value? target_type_val_db = view_i.db.get_property(target_guid, "_type"); + if (target_type_val_db != null) { + target_type = (string)target_type_val_db; + } else { + print("Error: Unable to determine target type for GUID %s", target_guid.to_string()); + view_i.tree_selection.changed.connect(view_i.on_tree_selection_changed); + return; + } + } + string current_item_type = (string)view_i.db.get_property(guid, "_type"); + + bool valid_move = false; + foreach (var root_info in get_root_folder_info()) { + if (target_type == root_info.object_type && current_item_type == root_info.contains_item_type_str) { + valid_move = true; + break; + } + } + + if (!valid_move) { + print("Error: Cannot move %s into %s", current_item_type, target_type); + view_i.tree_selection.changed.connect(view_i.on_tree_selection_changed); + return; + } + if (parent_guid != target_guid) { + view_i.db.set_property(guid, "parent_folder", target_guid); + } + TreeIter? parent_iter = find_parent_iter(view_i, target_guid); + if (parent_iter != null) { + Gtk.TreeIter iter; + bool type_managed = false; + foreach (var root_info in get_root_folder_info()) { + if (root_info.object_type == target_type) { + LevelTreeSynching.append_to_tree_store(view_i, parent_iter, root_info.contains_item_type, guid, view_i.level.object_editor_name(guid)); + type_managed = true; + break; + } + } + if (!type_managed) { + print("target_type folder : " + target_type + " not managed"); + } + } + view_i.tree_view.expand_all(); + } + Gtk.drag_finish(context, true, false, time); + view_i.tree_selection.changed.connect(view_i.on_tree_selection_changed); + } + + private static void add_folders_recursively(LevelTreeView view_i, string parent_guid, HashTable folder_map) { + foreach (var folder in view_i.level._folders) { + if (folder.parent_id.to_string() == parent_guid) { + Gtk.TreeIter? parent_iter = folder_map[parent_guid]; + Gtk.TreeIter iter = LevelTreeSynching.append_to_tree_store(view_i, parent_iter, LevelTreeView.ItemType.FOLDER, folder.id, folder.name); + folder_map[folder.id.to_string()] = iter; + add_folders_recursively(view_i, folder.id.to_string(), folder_map); + } + } + } + + public static void add_folder_to_tree(LevelTreeView view_i, bool save_to_file, Gtk.TreeIter? parent_iter, string name,string type, Guid guid) + { + if (save_to_file) { + Guid parent_guid = get_parent_guid(view_i, parent_iter); + view_i.level.add_folder(guid, name,type, parent_guid); + } + + Gtk.TreeIter iter; + view_i.tree_store.insert_with_values(out iter, parent_iter, -1, + LevelTreeView.Column.TYPE, LevelTreeView.ItemType.FOLDER, + LevelTreeView.Column.GUID, guid, + LevelTreeView.Column.NAME, name + ); + + if (parent_iter != null) { + Gtk.TreePath path = view_i.tree_store.get_path(parent_iter); + view_i.tree_view.expand_row(path, false); + } + } + public static Guid get_parent_guid(LevelTreeView view_i, Gtk.TreeIter? parent_iter) + { + if (parent_iter == null) + return GUID_NONE_FOLDER; + + Value val; + view_i.tree_store.get_value(parent_iter, LevelTreeView.Column.GUID, out val); + return (Guid)val; + } + public static Gtk.TreeIter? find_parent_iter(LevelTreeView view_i, Guid parent_id) + { + Gtk.TreeIter? found = null; + view_i.tree_store.foreach((model, path, iter) => { + Value val; + model.get_value(iter, LevelTreeView.Column.GUID, out val); + if (val.holds(typeof(Guid))) { + Guid guid = (Guid)val; + if (guid == parent_id) { + found = iter; + return true; + } + } + return false; + }); + return found; + } + } +} \ No newline at end of file diff --git a/tools/level_editor/level_tree_synching.vala b/tools/level_editor/level_tree_synching.vala new file mode 100644 index 0000000000..0a302a7f94 --- /dev/null +++ b/tools/level_editor/level_tree_synching.vala @@ -0,0 +1,219 @@ +/* +* Copyright (c) 2012-2025 Daniele Bartolini et al. +* SPDX-License-Identifier: GPL-3.0-or-later +*/ +using Gee; +using Gtk; + +namespace Crown +{ + // Manage synching for the LevelTreeView + public class LevelTreeSynching + { + public static void on_database_key_changed_internal(LevelTreeView view_i, Guid id, string key) { + if (id != view_i.level._id || (key != "units" && key != "sounds")) return; + view_i.tree_selection.changed.disconnect(view_i.on_tree_selection_changed); + + // Define target folder and item type based on the key + Guid target_folder = (key == "units") ? GUID_UNIT_FOLDER : GUID_SOUND_FOLDER; + LevelTreeView.ItemType item_type = (key == "units") ? LevelTreeView.ItemType.UNIT : LevelTreeView.ItemType.SOUND; + + // Track existing GUIDs within the folder + var existing_guids = new HashTable(Guid.hash_func, Guid.equal_func); + Gtk.TreeIter target_iter; + bool folder_exists = false; + + // Check if the target folder already exists + if (view_i.tree_store.get_iter_first(out target_iter)) { + do { + Value guid_val; + view_i.tree_store.get_value(target_iter, LevelTreeView.Column.GUID, out guid_val); + + // Ensure the value holds a Guid + if (guid_val.holds(typeof(Guid))) { + Guid guid = (Guid)guid_val; + + // Check if the extracted Guid matches the target folder + if (guid == target_folder) { + folder_exists = true; + break; + } + } + } while (view_i.tree_store.iter_next(ref target_iter)); + } + + // If the folder doesn't exist, create it + if (!folder_exists) { + append_to_tree_store(view_i, null, LevelTreeView.ItemType.FOLDER, target_folder, (key == "units") ? "Units" : "Sounds"); + } + + // Populate existing GUIDs in the folder + Gtk.TreeIter child; + if (view_i.tree_store.iter_children(out child, target_iter)) { + do { + Value guid_val; + view_i.tree_store.get_value(child, LevelTreeView.Column.GUID, out guid_val); + if (guid_val.holds(typeof(Guid))) { + existing_guids[(Guid)guid_val] = child; + } + } while (view_i.tree_store.iter_next(ref child)); + } + + // Retrieve updated GUIDs from the database + var current_guids = view_i.db.get_property_set(id, key, new HashSet()); + + // Remove outdated items but avoid removing subfolders + // TODO SUPPORT FOLDER REMOVING LATER + existing_guids.foreach((guid, iter) => { + Value item_type_val; + view_i.tree_store.get_value(iter, LevelTreeView.Column.TYPE, out item_type_val); + if (item_type_val.holds(typeof(LevelTreeView.ItemType)) && (LevelTreeView.ItemType)item_type_val != LevelTreeView.ItemType.FOLDER) { + if (!current_guids.contains(guid)) { + view_i.tree_store.remove(ref iter); + } + } + remove_outdated_items_in_subfolders(view_i, iter, current_guids); + }); + + // Synchronize parent folders and add missing items + foreach (Guid guid in current_guids) { + // Check if the GUID is already present in the folder + if (!existing_guids.contains(guid)) { + string item_name = view_i.level.object_editor_name(guid); + + Value? parent_value = view_i.db.get_property(guid, "parent_folder"); + Guid parent_id = (parent_value != null) ? (Guid)parent_value : target_folder; + + // Check if we have the correct parent iter for the item + Gtk.TreeIter? parent_iter = LevelTreeOrganization.find_parent_iter(view_i, parent_id); + + if (parent_iter != null) { + // Ensure we don't duplicate this item under the same parent + bool is_duplicate = false; + + // Check if the GUID is already under the parent folder + Gtk.TreeIter sibling_iter; + if (view_i.tree_store.iter_children(out sibling_iter, parent_iter)) { + do { + Value sibling_guid_val; + view_i.tree_store.get_value(sibling_iter, LevelTreeView.Column.GUID, out sibling_guid_val); + + Guid sibling_guid; + if (sibling_guid_val.holds(typeof(Guid))) { + sibling_guid = (Guid)sibling_guid_val; // Cast the Value directly to Guid + if (sibling_guid == guid) { + is_duplicate = true; + break; // If found, don't add it again + } + } + + } while (view_i.tree_store.iter_next(ref sibling_iter)); + } + // Add the item if it is not a duplicate + if (!is_duplicate) { + append_to_tree_store(view_i, parent_iter, item_type, guid, item_name); + } + } else { + print("ERROR: Parent iter for GUID %s not found in the tree!", guid.to_string()); + } + } + } + + view_i.tree_view.expand_all(); + view_i.tree_selection.changed.connect(view_i.on_tree_selection_changed); + } + private static void remove_outdated_items_in_subfolders(LevelTreeView view_i, Gtk.TreeIter iter, HashSet current_guids) { + // Iterate over all children (subfolders and items) under the current iter + Gtk.TreeIter child_iter; + if (view_i.tree_store.iter_children(out child_iter, iter)) { + do { + // Get the GUID and item type of the current child + Value guid_val; + view_i.tree_store.get_value(child_iter, LevelTreeView.Column.GUID, out guid_val); + Value item_type_val; + view_i.tree_store.get_value(child_iter, LevelTreeView.Column.TYPE, out item_type_val); + + if (guid_val.holds(typeof(Guid)) && item_type_val.holds(typeof(LevelTreeView.ItemType))) { + Guid guid = (Guid)guid_val; + LevelTreeView.ItemType item_type = (LevelTreeView.ItemType)item_type_val; + + // If this item is not a folder, check if it's outdated and remove it + if (item_type != LevelTreeView.ItemType.FOLDER) { + if (!current_guids.contains(guid)) { + view_i.tree_store.remove(ref child_iter); + } + } else { + // If it's a folder, recursively check for outdated items inside it + remove_outdated_items_in_subfolders(view_i, child_iter, current_guids); + } + } + } while (view_i.tree_store.iter_next(ref child_iter)); + } + } + public static void rebuild_tree(LevelTreeView view_i) { + view_i.tree_store.clear(); + var folder_map = new HashTable(str_hash, str_equal); + + foreach (var root_info in get_root_folder_info()) { + Gtk.TreeIter iter = append_to_tree_store(view_i, null, root_info.item_type, root_info.guid, root_info.name); + folder_map[root_info.guid.to_string()] = iter; + } + + foreach (var folder in view_i.level._folders) { + if (folder.id in get_root_folder_guids()) { + continue; + } + Gtk.TreeIter? parent_iter = folder_map[folder.parent_id.to_string()]; + Gtk.TreeIter iter = append_to_tree_store(view_i, parent_iter, LevelTreeView.ItemType.FOLDER, folder.id, folder.name); + folder_map[folder.id.to_string()] = iter; + } + + sync_existing_units_and_sounds(view_i); + } + public static void sync_existing_units_and_sounds(LevelTreeView view_i) + { + foreach (var folder in get_root_folder_info()) + { + sync_items(view_i, folder.contains_source_set_str, folder.guid, folder.contains_item_type); + } + } + + public static void sync_items(LevelTreeView view_i, string property_name, Guid default_folder, LevelTreeView.ItemType item_type) + { + var items = view_i.db.get_property_set(view_i.level._id, property_name, new HashSet()); + if (items.size == 0) return; + + // Cache parent GUID -> TreeIter mappings + var parent_iter_cache = new Gee.HashMap(Guid.hash_func, Guid.equal_func); + + foreach (Guid guid in items) { + // Get parent GUID (with default fallback) + var parent_value = view_i.db.get_property(guid, "parent_folder"); + Guid parent_id = (parent_value != null) ? (Guid)parent_value : default_folder; + if (parent_id == GUID_ZERO) parent_id = default_folder; + + // Cache lookup/creation + Gtk.TreeIter? parent_iter = null; + if (!parent_iter_cache.has_key(parent_id)) { + parent_iter = LevelTreeOrganization.find_parent_iter(view_i, parent_id); + parent_iter_cache[parent_id] = parent_iter; + } else { + parent_iter = parent_iter_cache[parent_id]; + } + + if (parent_iter != null) { + append_to_tree_store(view_i, parent_iter, item_type, guid, view_i.level.object_editor_name(guid)); + } else { + print("ERROR: Parent iter for " + item_type.to_string() + " GUID " + guid.to_string() + " not found!"); + } + } + } + + public static Gtk.TreeIter append_to_tree_store(LevelTreeView view_i, Gtk.TreeIter? parent_iter, LevelTreeView.ItemType type, Guid guid, string name) { + Gtk.TreeIter iter; + view_i.tree_store.append(out iter, parent_iter); + view_i.tree_store.set(iter, LevelTreeView.Column.TYPE, type, LevelTreeView.Column.GUID, guid, LevelTreeView.Column.NAME, name, -1); + return iter; + } + } +} \ No newline at end of file diff --git a/tools/level_editor/level_tree_view.vala b/tools/level_editor/level_tree_view.vala index ff4a475803..307b6bad8c 100644 --- a/tools/level_editor/level_tree_view.vala +++ b/tools/level_editor/level_tree_view.vala @@ -10,7 +10,12 @@ namespace Crown { public class LevelTreeView : Gtk.Box { - private enum ItemType + private const Gtk.TargetEntry[] TARGET_ENTRIES = { + { CROWN_DND_TARGET, Gtk.TargetFlags.SAME_APP, 0 } + }; + private const string CROWN_DND_TARGET = "application/x-crown-set"; + + public enum ItemType { FOLDER, CAMERA, @@ -19,7 +24,7 @@ public class LevelTreeView : Gtk.Box UNIT } - private enum Column + public enum Column { TYPE, GUID, @@ -71,6 +76,21 @@ public class LevelTreeView : Gtk.Box private Gtk.Popover _sort_items_popover; private Gtk.MenuButton _sort_items; + // Getters + public Level level { get { return _level; } } + public Database db { get { return _db; } } + public EntrySearch filter_entry { get { return _filter_entry; } } + public Gtk.TreeStore tree_store { get { return _tree_store; } } + public Gtk.TreeModelFilter tree_filter { get { return _tree_filter; } } + public Gtk.TreeModelSort tree_sort { get { return _tree_sort; } } + public Gtk.TreeView tree_view { get { return _tree_view; } } + public Gtk.TreeSelection tree_selection { get { return _tree_selection; } } + public Gtk.ScrolledWindow scrolled_window { get { return _scrolled_window; } } + public SortMode sort_mode { get { return _sort_mode; } } + public Gtk.Box sort_items_box { get { return _sort_items_box; } } + public Gtk.Popover sort_items_popover { get { return _sort_items_popover; } } + public Gtk.MenuButton sort_items { get { return _sort_items; } } + public LevelTreeView(Database db, Level level) { Object(orientation: Gtk.Orientation.VERTICAL, spacing: 0); @@ -89,9 +109,9 @@ public class LevelTreeView : Gtk.Box _filter_entry.search_changed.connect(on_filter_entry_text_changed); _tree_store = new Gtk.TreeStore(Column.COUNT - , typeof(int) // Column.TYPE - , typeof(Guid) // Column.GUID - , typeof(string) // Column.NAME + , typeof(ItemType)// Column.TYPE + , typeof(Guid) // Column.GUID + , typeof(string) // Column.NAME ); _tree_filter = new Gtk.TreeModelFilter(_tree_store, null); @@ -103,7 +123,7 @@ public class LevelTreeView : Gtk.Box model.get_value(iter, Column.TYPE, out type); model.get_value(iter, Column.NAME, out name); - if ((int)type == ItemType.FOLDER) + if (type == ItemType.FOLDER) return true; string name_str = (string)name; @@ -120,9 +140,14 @@ public class LevelTreeView : Gtk.Box Value type_b; model.get_value(iter_a, Column.TYPE, out type_a); model.get_value(iter_b, Column.TYPE, out type_b); - if ((int)type_a == ItemType.FOLDER || (int)type_b == ItemType.FOLDER) - return -1; - + + // Ensure folders come first (prioritize folders over units/sounds) + if ((ItemType)type_a == ItemType.FOLDER && (ItemType)type_b != ItemType.FOLDER) + return -1; // folder should be before non-folder + if ((ItemType)type_b == ItemType.FOLDER && (ItemType)type_a != ItemType.FOLDER) + return 1; // folder should be after non-folder + + // If both are folders, or both are non-folders, sort by the usual criteria switch (_sort_mode) { case SortMode.NAME_AZ: case SortMode.NAME_ZA: { @@ -130,22 +155,20 @@ public class LevelTreeView : Gtk.Box Value name_b; model.get_value(iter_a, Column.NAME, out name_a); model.get_value(iter_b, Column.NAME, out name_b); - + int cmp = strcmp((string)name_a, (string)name_b); return _sort_mode == SortMode.NAME_AZ ? cmp : -cmp; } - case SortMode.TYPE_AZ: case SortMode.TYPE_ZA: { int cmp = (int)type_a - (int)type_b; return _sort_mode == SortMode.TYPE_AZ ? cmp : -cmp; } - default: return 0; } }); - + Gtk.TreeViewColumn column = new Gtk.TreeViewColumn(); Gtk.CellRendererPixbuf cell_pixbuf = new Gtk.CellRendererPixbuf(); Gtk.CellRendererText cell_text = new Gtk.CellRendererText(); @@ -153,24 +176,24 @@ public class LevelTreeView : Gtk.Box column.pack_start(cell_text, true); column.set_cell_data_func(cell_pixbuf, (cell_layout, cell, model, iter) => { Value type; - model.get_value(iter, LevelTreeView.Column.TYPE, out type); + model.get_value(iter, Column.TYPE, out type); - if ((int)type == LevelTreeView.ItemType.FOLDER) + if (type == ItemType.FOLDER) cell.set_property("icon-name", "browser-folder-symbolic"); - else if ((int)type == LevelTreeView.ItemType.UNIT) + else if (type == ItemType.UNIT) cell.set_property("icon-name", "level-object-unit"); - else if ((int)type == LevelTreeView.ItemType.SOUND) + else if (type == ItemType.SOUND) cell.set_property("icon-name", "level-object-sound"); - else if ((int)type == LevelTreeView.ItemType.LIGHT) + else if (type == ItemType.LIGHT) cell.set_property("icon-name", "level-object-light"); - else if ((int)type == LevelTreeView.ItemType.CAMERA) + else if (type == ItemType.CAMERA) cell.set_property("icon-name", "level-object-camera"); else cell.set_property("icon-name", "level-object-unknown"); }); column.set_cell_data_func(cell_text, (cell_layout, cell, model, iter) => { Value name; - model.get_value(iter, LevelTreeView.Column.NAME, out name); + model.get_value(iter, Column.NAME, out name); cell.set_property("text", (string)name); }); @@ -191,6 +214,14 @@ public class LevelTreeView : Gtk.Box _tree_view.model = _tree_sort; _tree_view.button_press_event.connect(on_button_pressed); + // Enable drag and drop + _tree_view.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, TARGET_ENTRIES, Gdk.DragAction.MOVE); + _tree_view.enable_model_drag_dest(TARGET_ENTRIES, Gdk.DragAction.MOVE); + _tree_view.drag_data_get.connect(on_drag_data_get); + _tree_view.drag_data_received.connect(on_drag_data_received); + _tree_view.drag_drop.connect(on_drag_drop); + _tree_view.drag_motion.connect(on_drag_motion); + _tree_selection = _tree_view.get_selection(); _tree_selection.set_mode(Gtk.SelectionMode.MULTIPLE); _tree_selection.changed.connect(on_tree_selection_changed); @@ -220,6 +251,11 @@ public class LevelTreeView : Gtk.Box this.pack_start(tree_control, false, true, 0); this.pack_start(_scrolled_window, true, true, 0); + + _level.level_loaded.connect(() => { + LevelTreeSynching.rebuild_tree(this); + }); + LevelTreeSynching.rebuild_tree(this); } private bool on_button_pressed(Gdk.EventButton ev) @@ -233,64 +269,163 @@ public class LevelTreeView : Gtk.Box _tree_selection.select_path(path); } } else { // Clicked on empty space. - return Gdk.EVENT_PROPAGATE; + return Gdk.EVENT_STOP; } - Gtk.Menu menu = new Gtk.Menu(); - Gtk.MenuItem mi; - + Gtk.MenuItem mi = null; + _tree_selection.selected_foreach((model, path, iter) => { + Value type; + model.get_value(iter, Column.TYPE, out type); + + if (type == ItemType.FOLDER) { + Gtk.TreeIter parent_iter = Gtk.TreeIter(); + Gtk.TreeStore base_store = null; + bool valid_conversion = true; + + if (model is Gtk.TreeModelSort) { + Gtk.TreeModelSort sort_model = (Gtk.TreeModelSort)model; + Gtk.TreeModel base_model = sort_model.model; + + if (base_model is Gtk.TreeModelFilter) { + Gtk.TreeModelFilter filter_model = (Gtk.TreeModelFilter)base_model; + Gtk.TreeModel inner_model = filter_model.get_model(); + if (inner_model is Gtk.TreeStore) { + base_store = (Gtk.TreeStore)inner_model; + } else { + print("The internal model of the filter is not a TreeStore."); + return; + } + + Gtk.TreePath child_path = sort_model.convert_path_to_child_path(path); + if (child_path != null) { + valid_conversion = base_store.get_iter(out parent_iter, child_path); + } else { + valid_conversion = false; + } + } else { + valid_conversion = false; + } + } else { + valid_conversion = false; + } + if (valid_conversion && base_store.iter_is_valid(parent_iter)) { + var menu_item = new Gtk.MenuItem.with_label("Create Subfolder"); + menu_item.activate.connect(() => { + Guid parent_guid = LevelTreeOrganization.get_parent_guid(this, parent_iter); + + string? parent_type_str = null; + foreach (var root_info in get_root_folder_info()) { + if (parent_guid == root_info.guid) { + parent_type_str = root_info.item_type_str; + break; + } + } + if (parent_type_str == null) { + Value parent_type_val = _db.get_property(parent_guid, "_type"); + parent_type_str = parent_type_val.holds(typeof(string)) ? (string)parent_type_val : null; + } + if (parent_type_str != null) { + foreach (var root_info in get_root_folder_info()) { + if (parent_type_str == root_info.item_type_str) { + create_new_folder(parent_iter, root_info.object_type); + return; + } + } + print("Unmanaged or invalid type: %s", parent_type_str); + } + else { + print("Error: Parent type is not a string. Type found: %s", parent_guid.to_string()); + } + }); + menu.add(menu_item); + } + else { + print("Error: Invalid conversion or iterator"); + } + } + }); + mi = new Gtk.MenuItem.with_label("Rename..."); mi.activate.connect(() => { - Gtk.Dialog dg = new Gtk.Dialog.with_buttons("New Name" - , (Gtk.Window)this.get_toplevel() - , DialogFlags.MODAL - , "Cancel" - , ResponseType.CANCEL - , "Ok" - , ResponseType.OK - , null - ); - - EntryText sb = new EntryText(); - _tree_selection.selected_foreach((model, path, iter) => { - Value name; - model.get_value(iter, Column.NAME, out name); - sb.value = (string)name; - return; - }); - sb.activate.connect(() => { dg.response(ResponseType.OK); }); - dg.get_content_area().add(sb); - dg.skip_taskbar_hint = true; - dg.show_all(); - - if (dg.run() == (int)ResponseType.OK) { - string cur_name = ""; - string new_name = ""; - Guid object_id = GUID_ZERO; - - _tree_selection.selected_foreach((model, path, iter) => { - Value type; - model.get_value(iter, Column.TYPE, out type); - if ((int)type == ItemType.FOLDER) - return; - - Value name; - model.get_value(iter, Column.NAME, out name); - cur_name = (string)name; - + GLib.List selectedPaths = null; + Gtk.TreeModel model = null; + selectedPaths = _tree_selection.get_selected_rows(out model); + + if (selectedPaths != null) { + foreach (Gtk.TreePath selectedPath in selectedPaths) { + Gtk.TreeIter iter; + + if (model.get_iter(out iter, selectedPath)) { + Value type; + model.get_value(iter, Column.TYPE, out type); + + if ((ItemType)type == ItemType.FOLDER) { Value guid; model.get_value(iter, Column.GUID, out guid); - object_id = (Guid)guid; - - new_name = sb.text.strip(); - }); - - if (new_name != "" && new_name != cur_name) - _level.object_set_editor_name(object_id, new_name); + Guid object_id = (Guid)guid; + + bool can_rename = true; + foreach (var root_folder in Crown.get_root_folder_info()) { + if (root_folder.guid == object_id) { + can_rename = false; + break; + } + } + + if (can_rename) { + rename_folder(model, iter); + } else { + Gtk.MessageDialog dialog = new Gtk.MessageDialog( + (Gtk.Window)this.get_toplevel(), + Gtk.DialogFlags.MODAL, + Gtk.MessageType.WARNING, + Gtk.ButtonsType.OK, + "You cannot rename this folder, it's a predefined root folder." + ); + dialog.run(); + dialog.destroy(); + } + } else { + Gtk.Dialog dg = new Gtk.Dialog.with_buttons("New Name" + , (Gtk.Window)this.get_toplevel() + , DialogFlags.MODAL + , "Cancel" + , ResponseType.CANCEL + , "Ok" + , ResponseType.OK + , null + ); + + EntryText sb = new EntryText(); + + Value name; + model.get_value(iter, Column.NAME, out name); + sb.value = (string)name; + + sb.activate.connect(() => { dg.response(ResponseType.OK); }); + dg.get_content_area().add(sb); + dg.skip_taskbar_hint = true; + dg.show_all(); + + if (dg.run() == (int)ResponseType.OK) { + string cur_name = (string)name; + string new_name = sb.text.strip(); + Guid object_id = GUID_ZERO; + + Value guid; + model.get_value(iter, Column.GUID, out guid); + object_id = (Guid)guid; + + if (new_name != "" && new_name != cur_name) + _level.object_set_editor_name(object_id, new_name); + } + dg.destroy(); + } + } } - - dg.destroy(); - }); + } + }); + if (_tree_selection.count_selected_rows() == 1) menu.add(mi); @@ -313,8 +448,127 @@ public class LevelTreeView : Gtk.Box return Gdk.EVENT_PROPAGATE; } + private void rename_folder(Gtk.TreeModel model, Gtk.TreeIter iter) + { + Gtk.TreeStore base_store = null; + Gtk.TreeIter base_iter = iter; + bool conversion_valide = true; + + if (model is Gtk.TreeModelSort) + { + Gtk.TreeModelSort sort_model = (Gtk.TreeModelSort)model; + Gtk.TreeModelFilter filter_model = sort_model.model as Gtk.TreeModelFilter; + + if (filter_model != null) + { + base_store = filter_model.get_model() as Gtk.TreeStore; + Gtk.TreeIter filter_iter; + sort_model.convert_iter_to_child_iter(out filter_iter, iter); + filter_model.convert_iter_to_child_iter(out base_iter, filter_iter); + } + else + { + conversion_valide = false; + } + } + else if (model is Gtk.TreeStore) + { + base_store = model as Gtk.TreeStore; + } + else + { + conversion_valide = false; + } + + Value name_value; + base_store.get_value(base_iter, Column.NAME, out name_value); + string current_name = (string)name_value; + + Gtk.Dialog dialog = new Gtk.Dialog.with_buttons( + "New Name", + (Gtk.Window)get_toplevel(), + Gtk.DialogFlags.MODAL, + "Cancel", Gtk.ResponseType.CANCEL, + "Ok", Gtk.ResponseType.OK, + null + ); + + Gtk.Entry entry = new Gtk.Entry(); + entry.text = current_name; + dialog.get_content_area().add(entry); + dialog.show_all(); + + if (dialog.run() == Gtk.ResponseType.OK) + { + string new_name = entry.text.strip(); + + if (new_name != "" && new_name != current_name) + { + base_store.set_value(base_iter, Column.NAME, new_name); + + Value guid_value; + base_store.get_value(base_iter, Column.GUID, out guid_value); + Guid folder_guid = (Guid)guid_value; + + foreach (var folder in _level._folders) + { + if (folder.id == folder_guid) + { + folder.name = new_name; + break; + } + } + _level._db.set_property_string(folder_guid, "editor.name", new_name); + } + } + dialog.destroy(); + } + + private void create_new_folder(Gtk.TreeIter? parent_iter = null,string type) + { + Gtk.Dialog dialog = new Gtk.Dialog.with_buttons( + "New Folder Name", + ((Gtk.Application)GLib.Application.get_default()).active_window, + Gtk.DialogFlags.MODAL, + "Cancel", Gtk.ResponseType.CANCEL, + "OK", Gtk.ResponseType.OK + ); + + Gtk.Entry entry = new Gtk.Entry(); + entry.set_placeholder_text("Enter folder name"); + dialog.get_content_area().pack_start(entry, true, true, 0); + dialog.show_all(); + + if (dialog.run() == Gtk.ResponseType.OK) { + string folder_name = entry.get_text().strip(); + if (folder_name == "") { + folder_name = "Folder Name"; + } + dialog.destroy(); + Guid folder_guid = Guid.new_guid(); + LevelTreeOrganization.add_folder_to_tree(this, true, parent_iter, folder_name,type, folder_guid); + } else { + dialog.destroy(); + } + } + + private bool on_drag_drop(Gtk.Widget widget, Gdk.DragContext context, int x, int y, uint time) { + return LevelTreeOrganization.on_drag_drop_internal(this, CROWN_DND_TARGET, widget, context, x, y, time); + } + + private bool on_drag_motion(Gtk.Widget widget, Gdk.DragContext context, int x, int y, uint time) { + return LevelTreeOrganization.on_drag_motion_internal(this, widget, context, x, y, time); + } - private void on_tree_selection_changed() + private void on_drag_data_get(Gtk.Widget widget, Gdk.DragContext context, Gtk.SelectionData selection_data, uint info, uint time) { + LevelTreeOrganization.on_drag_data_get_internal(this, CROWN_DND_TARGET,widget, context, selection_data, info, time); + } + + private void on_drag_data_received(Gtk.Widget widget, Gdk.DragContext context, int x, int y, Gtk.SelectionData selection_data, uint info, uint time) { + LevelTreeOrganization.on_drag_data_received_internal(this, CROWN_DND_TARGET, widget, context, x, y, selection_data, info, time); + } + + public void on_tree_selection_changed() { _level.selection_changed.disconnect(on_level_selection_changed); @@ -322,7 +576,7 @@ public class LevelTreeView : Gtk.Box _tree_selection.selected_foreach((model, path, iter) => { Value type; model.get_value(iter, Column.TYPE, out type); - if ((int)type == ItemType.FOLDER) + if (type == ItemType.FOLDER) return; Value id; @@ -344,7 +598,7 @@ public class LevelTreeView : Gtk.Box _tree_sort.foreach ((model, path, iter) => { Value type; model.get_value(iter, Column.TYPE, out type); - if ((int)type == ItemType.FOLDER) + if (type == ItemType.FOLDER) return false; Value id; @@ -372,7 +626,7 @@ public class LevelTreeView : Gtk.Box _tree_sort.foreach ((model, path, iter) => { Value type; model.get_value(iter, Column.TYPE, out type); - if ((int)type == ItemType.FOLDER) + if (type == ItemType.FOLDER) return false; Value guid; @@ -407,84 +661,11 @@ public class LevelTreeView : Gtk.Box else return ItemType.UNIT; } - - private void on_database_key_changed(Guid id, string key) - { - if (id != _level._id) - return; - - if (key != "units" && key != "sounds") - return; - - _tree_selection.changed.disconnect(on_tree_selection_changed); - _tree_view.model = null; - _tree_store.clear(); - - Gtk.TreeIter units_iter; - _tree_store.insert_with_values(out units_iter - , null - , -1 - , Column.TYPE - , ItemType.FOLDER - , Column.GUID - , GUID_ZERO - , Column.NAME - , "Units" - , -1 - ); - Gtk.TreeIter sounds_iter; - _tree_store.insert_with_values(out sounds_iter - , null - , -1 - , Column.TYPE - , ItemType.FOLDER - , Column.GUID - , GUID_ZERO - , Column.NAME - , "Sounds" - , -1 - ); - - HashSet units = _db.get_property_set(_level._id, "units", new HashSet()); - HashSet sounds = _db.get_property_set(_level._id, "sounds", new HashSet()); - - foreach (Guid unit_id in units) { - Unit u = Unit(_level._db, unit_id); - - Gtk.TreeIter iter; - _tree_store.insert_with_values(out iter - , units_iter - , -1 - , Column.TYPE - , item_type(u) - , Column.GUID - , unit_id - , Column.NAME - , _level.object_editor_name(unit_id) - , -1 - ); - } - foreach (Guid sound in sounds) { - Gtk.TreeIter iter; - _tree_store.insert_with_values(out iter - , sounds_iter - , -1 - , Column.TYPE - , ItemType.SOUND - , Column.GUID - , sound - , Column.NAME - , _level.object_editor_name(sound) - , -1 - ); - } - - _tree_view.model = _tree_sort; - _tree_view.expand_all(); - - _tree_selection.changed.connect(on_tree_selection_changed); + + private void on_database_key_changed(Guid id, string key) { + LevelTreeSynching.on_database_key_changed_internal(this, id, key); } - + private void on_filter_entry_text_changed() { _tree_selection.changed.disconnect(on_tree_selection_changed); diff --git a/tools/resource/global_guids.vala b/tools/resource/global_guids.vala new file mode 100644 index 0000000000..12143d63f6 --- /dev/null +++ b/tools/resource/global_guids.vala @@ -0,0 +1,31 @@ +/* +* Copyright (c) 2012-2025 Daniele Bartolini et al. +* SPDX-License-Identifier: GPL-3.0-or-later +*/ +namespace Crown { + static Guid GUID_NONE_FOLDER = { 0x0000000000000000u, 0x0000000000000000u }; + static Guid GUID_UNIT_FOLDER = { 0x0000000000000000u, 0x0000000000000001u }; + static Guid GUID_SOUND_FOLDER = { 0x0000000000000000u, 0x0000000000000002u }; + + struct RootFolderInfo { + public Guid guid; + public string name; + public string object_type; + public LevelTreeView.ItemType item_type; + public string item_type_str; + public LevelTreeView.ItemType contains_item_type; + public string contains_item_type_str; + public string contains_source_set_str; + } + + static RootFolderInfo[] get_root_folder_info() { + return new RootFolderInfo[] { + { GUID_UNIT_FOLDER, "Units", OBJECT_TYPE_FOLDER_UNIT, LevelTreeView.ItemType.FOLDER, "unit_folder", LevelTreeView.ItemType.UNIT, "unit", "units"}, + { GUID_SOUND_FOLDER, "Sounds", OBJECT_TYPE_FOLDER_SOUND, LevelTreeView.ItemType.FOLDER, "sound_folder", LevelTreeView.ItemType.SOUND, "sound", "sounds"} + }; + } + + static Guid[] get_root_folder_guids() { + return new Guid[] { GUID_UNIT_FOLDER, GUID_SOUND_FOLDER }; + } +} /* namespace Crown */ diff --git a/tools/resource/types.vala b/tools/resource/types.vala index 6c92c6fbe2..1b12ad1e62 100644 --- a/tools/resource/types.vala +++ b/tools/resource/types.vala @@ -29,4 +29,6 @@ const string OBJECT_TYPE_SPRITE_ANIMATION = "sprite_animation"; const string OBJECT_TYPE_FONT = "font"; const string OBJECT_TYPE_MESH = "mesh"; +const string OBJECT_TYPE_FOLDER_UNIT = "unit_folder"; +const string OBJECT_TYPE_FOLDER_SOUND = "sound_folder"; } /* namespace Crown */