level_tree_view.vala 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515
  1. /*
  2. * Copyright (c) 2012-2025 Daniele Bartolini et al.
  3. * SPDX-License-Identifier: GPL-3.0-or-later
  4. */
  5. namespace Crown
  6. {
  7. public class LevelTreeView : Gtk.Box
  8. {
  9. private enum ItemType
  10. {
  11. FOLDER,
  12. CAMERA,
  13. LIGHT,
  14. SOUND,
  15. UNIT
  16. }
  17. private enum Column
  18. {
  19. TYPE,
  20. GUID,
  21. NAME,
  22. COUNT
  23. }
  24. public enum SortMode
  25. {
  26. NAME_AZ,
  27. NAME_ZA,
  28. TYPE_AZ,
  29. TYPE_ZA,
  30. COUNT;
  31. public string to_label()
  32. {
  33. switch (this) {
  34. case NAME_AZ:
  35. return "Name A-Z";
  36. case NAME_ZA:
  37. return "Name Z-A";
  38. case TYPE_AZ:
  39. return "Type A-Z";
  40. case TYPE_ZA:
  41. return "Type Z-A";
  42. default:
  43. return "Unknown";
  44. }
  45. }
  46. }
  47. // Data
  48. private Level _level;
  49. private Database _db;
  50. // Widgets
  51. private EntrySearch _filter_entry;
  52. private Gtk.TreeStore _tree_store;
  53. private Gtk.TreeModelFilter _tree_filter;
  54. private Gtk.TreeModelSort _tree_sort;
  55. private Gtk.TreeView _tree_view;
  56. private Gtk.TreeSelection _tree_selection;
  57. private Gtk.ScrolledWindow _scrolled_window;
  58. private Gtk.Box _sort_items_box;
  59. private Gtk.Popover _sort_items_popover;
  60. private Gtk.MenuButton _sort_items;
  61. private Gtk.GestureMultiPress _gesture_click;
  62. private Gtk.TreeRowReference _units_root;
  63. private Gtk.TreeRowReference _sounds_root;
  64. public LevelTreeView(Database db, Level level)
  65. {
  66. Object(orientation: Gtk.Orientation.VERTICAL, spacing: 0);
  67. // Data
  68. _level = level;
  69. _level.selection_changed.connect(on_level_selection_changed);
  70. _level.object_editor_name_changed.connect(on_object_editor_name_changed);
  71. _db = db;
  72. // Widgets
  73. _filter_entry = new EntrySearch();
  74. _filter_entry.set_placeholder_text("Search...");
  75. _filter_entry.search_changed.connect(on_filter_entry_text_changed);
  76. _tree_store = new Gtk.TreeStore(Column.COUNT
  77. , typeof(int) // Column.TYPE
  78. , typeof(Guid) // Column.GUID
  79. , typeof(string) // Column.NAME
  80. );
  81. _tree_filter = new Gtk.TreeModelFilter(_tree_store, null);
  82. _tree_filter.set_visible_func((model, iter) => {
  83. _tree_view.expand_all();
  84. Value type;
  85. Value name;
  86. model.get_value(iter, Column.TYPE, out type);
  87. model.get_value(iter, Column.NAME, out name);
  88. if ((int)type == ItemType.FOLDER)
  89. return true;
  90. string name_str = (string)name;
  91. string filter_text = _filter_entry.text.down();
  92. return name_str != null
  93. && (filter_text == "" || name_str.down().index_of(filter_text) > -1)
  94. ;
  95. });
  96. _tree_sort = new Gtk.TreeModelSort.with_model(_tree_filter);
  97. _tree_sort.set_sort_column_id(Column.NAME, Gtk.SortType.ASCENDING);
  98. Gtk.TreeViewColumn column = new Gtk.TreeViewColumn();
  99. Gtk.CellRendererPixbuf cell_pixbuf = new Gtk.CellRendererPixbuf();
  100. Gtk.CellRendererText cell_text = new Gtk.CellRendererText();
  101. column.pack_start(cell_pixbuf, false);
  102. column.pack_start(cell_text, true);
  103. column.set_cell_data_func(cell_pixbuf, (cell_layout, cell, model, iter) => {
  104. Value type;
  105. model.get_value(iter, LevelTreeView.Column.TYPE, out type);
  106. if ((int)type == LevelTreeView.ItemType.FOLDER)
  107. cell.set_property("icon-name", "browser-folder-symbolic");
  108. else if ((int)type == LevelTreeView.ItemType.UNIT)
  109. cell.set_property("icon-name", "level-object-unit");
  110. else if ((int)type == LevelTreeView.ItemType.SOUND)
  111. cell.set_property("icon-name", "level-object-sound");
  112. else if ((int)type == LevelTreeView.ItemType.LIGHT)
  113. cell.set_property("icon-name", "level-object-light");
  114. else if ((int)type == LevelTreeView.ItemType.CAMERA)
  115. cell.set_property("icon-name", "level-object-camera");
  116. else
  117. cell.set_property("icon-name", "level-object-unknown");
  118. });
  119. column.set_cell_data_func(cell_text, (cell_layout, cell, model, iter) => {
  120. Value name;
  121. model.get_value(iter, LevelTreeView.Column.NAME, out name);
  122. cell.set_property("text", (string)name);
  123. });
  124. _tree_view = new Gtk.TreeView();
  125. _tree_view.append_column(column);
  126. #if 0
  127. // For debugging.
  128. _tree_view.insert_column_with_attributes(-1
  129. , "Guids"
  130. , new gtk.CellRendererText()
  131. , "text"
  132. , Column.GUID
  133. , null
  134. );
  135. #endif
  136. _tree_view.headers_clickable = false;
  137. _tree_view.headers_visible = false;
  138. _tree_view.model = _tree_sort;
  139. _gesture_click = new Gtk.GestureMultiPress(_tree_view);
  140. _gesture_click.set_propagation_phase(Gtk.PropagationPhase.CAPTURE);
  141. _gesture_click.set_button(0);
  142. _gesture_click.pressed.connect(on_button_pressed);
  143. _tree_selection = _tree_view.get_selection();
  144. _tree_selection.set_mode(Gtk.SelectionMode.MULTIPLE);
  145. _tree_selection.changed.connect(on_tree_selection_changed);
  146. _scrolled_window = new Gtk.ScrolledWindow(null, null);
  147. _scrolled_window.add(_tree_view);
  148. // Setup sort menu button popover.
  149. _sort_items_box = new Gtk.Box(Gtk.Orientation.VERTICAL, 0);
  150. Gtk.RadioButton? button = null;
  151. for (int i = 0; i < SortMode.COUNT; ++i)
  152. button = add_sort_item(button, (SortMode)i);
  153. _sort_items_box.show_all();
  154. _sort_items_popover = new Gtk.Popover(null);
  155. _sort_items_popover.add(_sort_items_box);
  156. _sort_items = new Gtk.MenuButton();
  157. _sort_items.add(new Gtk.Image.from_icon_name("list-sort", Gtk.IconSize.SMALL_TOOLBAR));
  158. _sort_items.get_style_context().add_class("flat");
  159. _sort_items.get_style_context().add_class("image-button");
  160. _sort_items.can_focus = false;
  161. _sort_items.set_popover(_sort_items_popover);
  162. var tree_control = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 0);
  163. tree_control.pack_start(_filter_entry, true, true);
  164. tree_control.pack_end(_sort_items, false, false);
  165. this.pack_start(tree_control, false, true, 0);
  166. this.pack_start(_scrolled_window, true, true, 0);
  167. }
  168. private void on_button_pressed(int n_press, double x, double y)
  169. {
  170. if (_gesture_click.get_current_button() == Gdk.BUTTON_SECONDARY) {
  171. int bx;
  172. int by;
  173. Gtk.TreePath path;
  174. Gtk.TreeViewColumn column;
  175. _tree_view.convert_widget_to_bin_window_coords((int)x, (int)y, out bx, out by);
  176. if (!_tree_view.get_path_at_pos(bx, by, out path, out column, null, null))
  177. return; // Clicked on empty space.
  178. if (!_tree_selection.path_is_selected(path)) {
  179. _tree_selection.unselect_all();
  180. _tree_selection.select_path(path);
  181. }
  182. GLib.Menu menu_model = new GLib.Menu();
  183. GLib.MenuItem mi;
  184. if (_tree_selection.count_selected_rows() == 1) {
  185. GLib.List<Gtk.TreePath> selected_paths = _tree_selection.get_selected_rows(null);
  186. Gtk.TreeIter iter;
  187. if (_tree_view.model.get_iter(out iter, selected_paths.nth(0).data)) {
  188. Value val;
  189. _tree_view.model.get_value(iter, Column.TYPE, out val);
  190. if ((int)val != ItemType.FOLDER) {
  191. _tree_view.model.get_value(iter, Column.GUID, out val);
  192. Guid object_id = (Guid)val;
  193. mi = new GLib.MenuItem("Rename...", null);
  194. mi.set_action_and_target_value("app.rename", new GLib.Variant.tuple({ object_id.to_string(), "" }));
  195. menu_model.append_item(mi);
  196. }
  197. }
  198. }
  199. mi = new GLib.MenuItem("Duplicate", null);
  200. mi.set_action_and_target_value("app.duplicate", null);
  201. menu_model.append_item(mi);
  202. mi = new GLib.MenuItem("Delete", null);
  203. mi.set_action_and_target_value("app.delete", null);
  204. menu_model.append_item(mi);
  205. if (_tree_selection.count_selected_rows() == 1) {
  206. GLib.List<Gtk.TreePath> selected_paths = _tree_selection.get_selected_rows(null);
  207. Gtk.TreeIter iter;
  208. if (_tree_view.model.get_iter(out iter, selected_paths.nth(0).data)) {
  209. Value val;
  210. _tree_view.model.get_value(iter, Column.GUID, out val);
  211. Guid object_id = (Guid)val;
  212. _tree_view.model.get_value(iter, Column.NAME, out val);
  213. string object_name = (string)val;
  214. if (_db.object_type(object_id) == OBJECT_TYPE_UNIT) {
  215. mi = new GLib.MenuItem("Save as Prefab...", null);
  216. mi.set_action_and_target_value("app.unit-save-as-prefab", new GLib.Variant.tuple({ object_id.to_string(), object_name }));
  217. menu_model.append_item(mi);
  218. }
  219. }
  220. }
  221. Gtk.Popover menu = new Gtk.Popover.from_model(null, menu_model);
  222. menu.set_relative_to(_tree_view);
  223. menu.set_pointing_to({ (int)x, (int)y, 1, 1 });
  224. menu.set_position(Gtk.PositionType.BOTTOM);
  225. menu.popup();
  226. _gesture_click.set_state(Gtk.EventSequenceState.CLAIMED);
  227. }
  228. }
  229. private void on_tree_selection_changed()
  230. {
  231. _level.selection_changed.disconnect(on_level_selection_changed);
  232. Gee.ArrayList<Guid?> ids = new Gee.ArrayList<Guid?>();
  233. _tree_selection.selected_foreach((model, path, iter) => {
  234. Value type;
  235. model.get_value(iter, Column.TYPE, out type);
  236. if ((int)type == ItemType.FOLDER)
  237. return;
  238. Value id;
  239. model.get_value(iter, Column.GUID, out id);
  240. ids.add((Guid)id);
  241. });
  242. _level.selection_set(ids.to_array());
  243. _level.selection_changed.connect(on_level_selection_changed);
  244. }
  245. private void on_level_selection_changed(Gee.ArrayList<Guid?> selection)
  246. {
  247. _tree_selection.changed.disconnect(on_tree_selection_changed);
  248. _tree_selection.unselect_all();
  249. Gtk.TreePath? last_selected = null;
  250. _tree_sort.foreach ((model, path, iter) => {
  251. Value type;
  252. model.get_value(iter, Column.TYPE, out type);
  253. if ((int)type == ItemType.FOLDER)
  254. return false;
  255. Value id;
  256. model.get_value(iter, Column.GUID, out id);
  257. foreach (Guid? guid in selection) {
  258. if ((Guid)id == guid) {
  259. _tree_selection.select_iter(iter);
  260. last_selected = path;
  261. return false;
  262. }
  263. }
  264. return false;
  265. });
  266. if (last_selected != null)
  267. _tree_view.scroll_to_cell(last_selected, null, false, 0.0f, 0.0f);
  268. _tree_selection.changed.connect(on_tree_selection_changed);
  269. }
  270. private void on_object_editor_name_changed(Guid object_id, string name)
  271. {
  272. _tree_sort.foreach ((model, path, iter) => {
  273. Value type;
  274. model.get_value(iter, Column.TYPE, out type);
  275. if ((int)type == ItemType.FOLDER)
  276. return false;
  277. Value guid;
  278. model.get_value(iter, Column.GUID, out guid);
  279. Guid guid_model = (Guid)guid;
  280. if (guid_model == object_id) {
  281. Gtk.TreeIter iter_filter;
  282. Gtk.TreeIter iter_model;
  283. _tree_sort.convert_iter_to_child_iter(out iter_filter, iter);
  284. _tree_filter.convert_iter_to_child_iter(out iter_model, iter_filter);
  285. _tree_store.set(iter_model
  286. , Column.NAME
  287. , name
  288. , -1
  289. );
  290. return true;
  291. }
  292. return false;
  293. });
  294. }
  295. private ItemType item_type(Unit u)
  296. {
  297. if (u.is_light())
  298. return ItemType.LIGHT;
  299. else if (u.is_camera())
  300. return ItemType.CAMERA;
  301. else
  302. return ItemType.UNIT;
  303. }
  304. // Sets the level object to show.
  305. public void set_level(Level level)
  306. {
  307. Gtk.TreeIter iter;
  308. _tree_view.model = null;
  309. _tree_store.clear();
  310. _tree_store.insert_with_values(out iter
  311. , null
  312. , -1
  313. , Column.TYPE
  314. , ItemType.FOLDER
  315. , Column.GUID
  316. , GUID_ZERO
  317. , Column.NAME
  318. , "Units"
  319. , -1
  320. );
  321. _units_root = new Gtk.TreeRowReference(_tree_store, _tree_store.get_path(iter));
  322. _tree_store.insert_with_values(out iter
  323. , null
  324. , -1
  325. , Column.TYPE
  326. , ItemType.FOLDER
  327. , Column.GUID
  328. , GUID_ZERO
  329. , Column.NAME
  330. , "Sounds"
  331. , -1
  332. );
  333. _sounds_root = new Gtk.TreeRowReference(_tree_store, _tree_store.get_path(iter));
  334. _tree_view.model = _tree_sort;
  335. _tree_view.expand_all();
  336. _level = level;
  337. on_objects_created(_db.get_property_set(_level._id, "units", new Gee.HashSet<Guid?>()).to_array());
  338. on_objects_created(_db.get_property_set(_level._id, "sounds", new Gee.HashSet<Guid?>()).to_array());
  339. }
  340. public void on_objects_created(Guid?[] object_ids)
  341. {
  342. Gee.HashSet<Guid?> units = _db.get_property_set(_level._id, "units", new Gee.HashSet<Guid?>());
  343. Gee.HashSet<Guid?> sounds = _db.get_property_set(_level._id, "sounds", new Gee.HashSet<Guid?>());
  344. Gtk.TreeIter iter;
  345. foreach (Guid id in object_ids) {
  346. if (_db.object_type(id) == OBJECT_TYPE_UNIT && units.contains(id)) {
  347. Unit u = Unit(_level._db, id);
  348. Gtk.TreeIter units_iter;
  349. _tree_store.get_iter(out units_iter, _units_root.get_path());
  350. _tree_store.insert_with_values(out iter
  351. , units_iter
  352. , -1
  353. , Column.TYPE
  354. , item_type(u)
  355. , Column.GUID
  356. , id
  357. , Column.NAME
  358. , _level.object_editor_name(id)
  359. , -1
  360. );
  361. } else if (_db.object_type(id) == OBJECT_TYPE_SOUND && sounds.contains(id)) {
  362. Gtk.TreeIter sounds_iter;
  363. _tree_store.get_iter(out sounds_iter, _sounds_root.get_path());
  364. _tree_store.insert_with_values(out iter
  365. , sounds_iter
  366. , -1
  367. , Column.TYPE
  368. , ItemType.SOUND
  369. , Column.GUID
  370. , id
  371. , Column.NAME
  372. , _level.object_editor_name(id)
  373. , -1
  374. );
  375. }
  376. }
  377. }
  378. public void on_objects_destroyed(Guid?[] object_ids)
  379. {
  380. foreach (Guid id in object_ids) {
  381. Gtk.TreeIter parent_iter;
  382. if (_db.object_type(id) == OBJECT_TYPE_UNIT)
  383. _tree_store.get_iter(out parent_iter, _units_root.get_path());
  384. else if (_db.object_type(id) == OBJECT_TYPE_SOUND)
  385. _tree_store.get_iter(out parent_iter, _sounds_root.get_path());
  386. else
  387. continue;
  388. remove_item(id, parent_iter);
  389. }
  390. }
  391. private void remove_item(Guid id, Gtk.TreeIter parent_iter)
  392. {
  393. Gtk.TreeIter child;
  394. if (_tree_store.iter_children(out child, parent_iter)) {
  395. Value column_id;
  396. while (true) {
  397. _tree_store.get_value(child, Column.GUID, out column_id);
  398. if (Guid.equal_func((Guid)column_id, id)) {
  399. _tree_store.remove(ref child);
  400. break;
  401. } else {
  402. if (!_tree_store.iter_next(ref child))
  403. break;
  404. }
  405. }
  406. }
  407. }
  408. private void on_filter_entry_text_changed()
  409. {
  410. _tree_selection.changed.disconnect(on_tree_selection_changed);
  411. _tree_filter.refilter();
  412. _tree_selection.changed.connect(on_tree_selection_changed);
  413. }
  414. private Gtk.RadioButton add_sort_item(Gtk.RadioButton? group, SortMode mode)
  415. {
  416. var button = new Gtk.RadioButton.with_label_from_widget(group, mode.to_label());
  417. button.toggled.connect(() => {
  418. if (mode == SortMode.NAME_AZ)
  419. _tree_sort.set_sort_column_id(Column.NAME, Gtk.SortType.ASCENDING);
  420. else if (mode == SortMode.NAME_ZA)
  421. _tree_sort.set_sort_column_id(Column.NAME, Gtk.SortType.DESCENDING);
  422. else if (mode == SortMode.TYPE_AZ)
  423. _tree_sort.set_sort_column_id(Column.TYPE, Gtk.SortType.ASCENDING);
  424. else if (mode == SortMode.TYPE_ZA)
  425. _tree_sort.set_sort_column_id(Column.TYPE, Gtk.SortType.DESCENDING);
  426. _tree_filter.refilter();
  427. _sort_items_popover.popdown();
  428. });
  429. _sort_items_box.pack_start(button, false, false);
  430. return button;
  431. }
  432. }
  433. } /* namespace Crown */