level_tree_view.vala 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  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 SortMode _sort_mode;
  59. private Gtk.Box _sort_items_box;
  60. private Gtk.Popover _sort_items_popover;
  61. private Gtk.MenuButton _sort_items;
  62. private Gtk.GestureMultiPress _gesture_click;
  63. public LevelTreeView(Database db, Level level)
  64. {
  65. Object(orientation: Gtk.Orientation.VERTICAL, spacing: 0);
  66. // Data
  67. _level = level;
  68. _level.selection_changed.connect(on_level_selection_changed);
  69. _level.object_editor_name_changed.connect(on_object_editor_name_changed);
  70. _db = db;
  71. _db.key_changed.connect(on_database_key_changed);
  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_default_sort_func((model, iter_a, iter_b) => {
  98. Value type_a;
  99. Value type_b;
  100. model.get_value(iter_a, Column.TYPE, out type_a);
  101. model.get_value(iter_b, Column.TYPE, out type_b);
  102. if ((int)type_a == ItemType.FOLDER || (int)type_b == ItemType.FOLDER)
  103. return -1;
  104. switch (_sort_mode) {
  105. case SortMode.NAME_AZ:
  106. case SortMode.NAME_ZA: {
  107. Value name_a;
  108. Value name_b;
  109. model.get_value(iter_a, Column.NAME, out name_a);
  110. model.get_value(iter_b, Column.NAME, out name_b);
  111. int cmp = strcmp((string)name_a, (string)name_b);
  112. return _sort_mode == SortMode.NAME_AZ ? cmp : -cmp;
  113. }
  114. case SortMode.TYPE_AZ:
  115. case SortMode.TYPE_ZA: {
  116. int cmp = (int)type_a - (int)type_b;
  117. return _sort_mode == SortMode.TYPE_AZ ? cmp : -cmp;
  118. }
  119. default:
  120. return 0;
  121. }
  122. });
  123. Gtk.TreeViewColumn column = new Gtk.TreeViewColumn();
  124. Gtk.CellRendererPixbuf cell_pixbuf = new Gtk.CellRendererPixbuf();
  125. Gtk.CellRendererText cell_text = new Gtk.CellRendererText();
  126. column.pack_start(cell_pixbuf, false);
  127. column.pack_start(cell_text, true);
  128. column.set_cell_data_func(cell_pixbuf, (cell_layout, cell, model, iter) => {
  129. Value type;
  130. model.get_value(iter, LevelTreeView.Column.TYPE, out type);
  131. if ((int)type == LevelTreeView.ItemType.FOLDER)
  132. cell.set_property("icon-name", "browser-folder-symbolic");
  133. else if ((int)type == LevelTreeView.ItemType.UNIT)
  134. cell.set_property("icon-name", "level-object-unit");
  135. else if ((int)type == LevelTreeView.ItemType.SOUND)
  136. cell.set_property("icon-name", "level-object-sound");
  137. else if ((int)type == LevelTreeView.ItemType.LIGHT)
  138. cell.set_property("icon-name", "level-object-light");
  139. else if ((int)type == LevelTreeView.ItemType.CAMERA)
  140. cell.set_property("icon-name", "level-object-camera");
  141. else
  142. cell.set_property("icon-name", "level-object-unknown");
  143. });
  144. column.set_cell_data_func(cell_text, (cell_layout, cell, model, iter) => {
  145. Value name;
  146. model.get_value(iter, LevelTreeView.Column.NAME, out name);
  147. cell.set_property("text", (string)name);
  148. });
  149. _tree_view = new Gtk.TreeView();
  150. _tree_view.append_column(column);
  151. #if 0
  152. // For debugging.
  153. _tree_view.insert_column_with_attributes(-1
  154. , "Guids"
  155. , new gtk.CellRendererText()
  156. , "text"
  157. , Column.GUID
  158. , null
  159. );
  160. #endif
  161. _tree_view.headers_clickable = false;
  162. _tree_view.headers_visible = false;
  163. _tree_view.model = _tree_sort;
  164. _gesture_click = new Gtk.GestureMultiPress(_tree_view);
  165. _gesture_click.set_propagation_phase(Gtk.PropagationPhase.CAPTURE);
  166. _gesture_click.set_button(0);
  167. _gesture_click.pressed.connect(on_button_pressed);
  168. _tree_selection = _tree_view.get_selection();
  169. _tree_selection.set_mode(Gtk.SelectionMode.MULTIPLE);
  170. _tree_selection.changed.connect(on_tree_selection_changed);
  171. _scrolled_window = new Gtk.ScrolledWindow(null, null);
  172. _scrolled_window.add(_tree_view);
  173. // Setup sort menu button popover.
  174. _sort_items_box = new Gtk.Box(Gtk.Orientation.VERTICAL, 0);
  175. Gtk.RadioButton? button = null;
  176. for (int i = 0; i < SortMode.COUNT; ++i)
  177. button = add_sort_item(button, (SortMode)i);
  178. _sort_items_box.show_all();
  179. _sort_items_popover = new Gtk.Popover(null);
  180. _sort_items_popover.add(_sort_items_box);
  181. _sort_items = new Gtk.MenuButton();
  182. _sort_items.add(new Gtk.Image.from_icon_name("list-sort", Gtk.IconSize.SMALL_TOOLBAR));
  183. _sort_items.get_style_context().add_class("flat");
  184. _sort_items.get_style_context().add_class("image-button");
  185. _sort_items.can_focus = false;
  186. _sort_items.set_popover(_sort_items_popover);
  187. var tree_control = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 0);
  188. tree_control.pack_start(_filter_entry, true, true);
  189. tree_control.pack_end(_sort_items, false, false);
  190. this.pack_start(tree_control, false, true, 0);
  191. this.pack_start(_scrolled_window, true, true, 0);
  192. }
  193. private void on_button_pressed(int n_press, double x, double y)
  194. {
  195. if (_gesture_click.get_current_button() == Gdk.BUTTON_SECONDARY) {
  196. Gtk.TreePath path;
  197. Gtk.TreeViewColumn column;
  198. if (!_tree_view.get_path_at_pos((int)x, (int)y, out path, out column, null, null))
  199. return; // Clicked on empty space.
  200. if (!_tree_selection.path_is_selected(path)) {
  201. _tree_selection.unselect_all();
  202. _tree_selection.select_path(path);
  203. }
  204. GLib.Menu menu_model = new GLib.Menu();
  205. GLib.MenuItem mi;
  206. if (_tree_selection.count_selected_rows() == 1) {
  207. GLib.List<Gtk.TreePath> selected_paths = _tree_selection.get_selected_rows(null);
  208. Gtk.TreeIter iter;
  209. if (_tree_view.model.get_iter(out iter, selected_paths.nth(0).data)) {
  210. Value val;
  211. _tree_view.model.get_value(iter, Column.TYPE, out val);
  212. if ((int)val != ItemType.FOLDER) {
  213. _tree_view.model.get_value(iter, Column.GUID, out val);
  214. Guid object_id = (Guid)val;
  215. mi = new GLib.MenuItem("Rename...", null);
  216. mi.set_action_and_target_value("app.rename", new GLib.Variant.tuple({ object_id.to_string(), "" }));
  217. menu_model.append_item(mi);
  218. }
  219. }
  220. }
  221. mi = new GLib.MenuItem("Duplicate", null);
  222. mi.set_action_and_target_value("app.duplicate", null);
  223. menu_model.append_item(mi);
  224. mi = new GLib.MenuItem("Delete", null);
  225. mi.set_action_and_target_value("app.delete", null);
  226. menu_model.append_item(mi);
  227. if (_tree_selection.count_selected_rows() == 1) {
  228. GLib.List<Gtk.TreePath> selected_paths = _tree_selection.get_selected_rows(null);
  229. Gtk.TreeIter iter;
  230. if (_tree_view.model.get_iter(out iter, selected_paths.nth(0).data)) {
  231. Value val;
  232. _tree_view.model.get_value(iter, Column.GUID, out val);
  233. Guid object_id = (Guid)val;
  234. _tree_view.model.get_value(iter, Column.NAME, out val);
  235. string object_name = (string)val;
  236. if (_db.object_type(object_id) == OBJECT_TYPE_UNIT) {
  237. mi = new GLib.MenuItem("Save as Prefab...", null);
  238. mi.set_action_and_target_value("app.unit-save-as-prefab", new GLib.Variant.tuple({ object_id.to_string(), object_name }));
  239. menu_model.append_item(mi);
  240. }
  241. }
  242. }
  243. Gtk.Popover menu = new Gtk.Popover.from_model(null, menu_model);
  244. menu.set_relative_to(_tree_view);
  245. menu.set_pointing_to({ (int)x, (int)y, 1, 1 });
  246. menu.set_position(Gtk.PositionType.BOTTOM);
  247. menu.popup();
  248. _gesture_click.set_state(Gtk.EventSequenceState.CLAIMED);
  249. }
  250. }
  251. private void on_tree_selection_changed()
  252. {
  253. _level.selection_changed.disconnect(on_level_selection_changed);
  254. Gee.ArrayList<Guid?> ids = new Gee.ArrayList<Guid?>();
  255. _tree_selection.selected_foreach((model, path, iter) => {
  256. Value type;
  257. model.get_value(iter, Column.TYPE, out type);
  258. if ((int)type == ItemType.FOLDER)
  259. return;
  260. Value id;
  261. model.get_value(iter, Column.GUID, out id);
  262. ids.add((Guid)id);
  263. });
  264. _level.selection_set(ids.to_array());
  265. _level.selection_changed.connect(on_level_selection_changed);
  266. }
  267. private void on_level_selection_changed(Gee.ArrayList<Guid?> selection)
  268. {
  269. _tree_selection.changed.disconnect(on_tree_selection_changed);
  270. _tree_selection.unselect_all();
  271. Gtk.TreePath? last_selected = null;
  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 id;
  278. model.get_value(iter, Column.GUID, out id);
  279. foreach (Guid? guid in selection) {
  280. if ((Guid)id == guid) {
  281. _tree_selection.select_iter(iter);
  282. last_selected = path;
  283. return false;
  284. }
  285. }
  286. return false;
  287. });
  288. if (last_selected != null)
  289. _tree_view.scroll_to_cell(last_selected, null, false, 0.0f, 0.0f);
  290. _tree_selection.changed.connect(on_tree_selection_changed);
  291. }
  292. private void on_object_editor_name_changed(Guid object_id, string name)
  293. {
  294. _tree_sort.foreach ((model, path, iter) => {
  295. Value type;
  296. model.get_value(iter, Column.TYPE, out type);
  297. if ((int)type == ItemType.FOLDER)
  298. return false;
  299. Value guid;
  300. model.get_value(iter, Column.GUID, out guid);
  301. Guid guid_model = (Guid)guid;
  302. if (guid_model == object_id) {
  303. Gtk.TreeIter iter_filter;
  304. Gtk.TreeIter iter_model;
  305. _tree_sort.convert_iter_to_child_iter(out iter_filter, iter);
  306. _tree_filter.convert_iter_to_child_iter(out iter_model, iter_filter);
  307. _tree_store.set(iter_model
  308. , Column.NAME
  309. , name
  310. , -1
  311. );
  312. return true;
  313. }
  314. return false;
  315. });
  316. }
  317. private ItemType item_type(Unit u)
  318. {
  319. if (u.is_light())
  320. return ItemType.LIGHT;
  321. else if (u.is_camera())
  322. return ItemType.CAMERA;
  323. else
  324. return ItemType.UNIT;
  325. }
  326. private void on_database_key_changed(Guid id, string key)
  327. {
  328. if (id != _level._id)
  329. return;
  330. if (key != "units" && key != "sounds")
  331. return;
  332. _tree_selection.changed.disconnect(on_tree_selection_changed);
  333. _tree_view.model = null;
  334. _tree_store.clear();
  335. Gtk.TreeIter units_iter;
  336. _tree_store.insert_with_values(out units_iter
  337. , null
  338. , -1
  339. , Column.TYPE
  340. , ItemType.FOLDER
  341. , Column.GUID
  342. , GUID_ZERO
  343. , Column.NAME
  344. , "Units"
  345. , -1
  346. );
  347. Gtk.TreeIter sounds_iter;
  348. _tree_store.insert_with_values(out sounds_iter
  349. , null
  350. , -1
  351. , Column.TYPE
  352. , ItemType.FOLDER
  353. , Column.GUID
  354. , GUID_ZERO
  355. , Column.NAME
  356. , "Sounds"
  357. , -1
  358. );
  359. Gee.HashSet<Guid?> units = _db.get_property_set(_level._id, "units", new Gee.HashSet<Guid?>());
  360. Gee.HashSet<Guid?> sounds = _db.get_property_set(_level._id, "sounds", new Gee.HashSet<Guid?>());
  361. foreach (Guid unit_id in units) {
  362. Unit u = Unit(_level._db, unit_id);
  363. Gtk.TreeIter iter;
  364. _tree_store.insert_with_values(out iter
  365. , units_iter
  366. , -1
  367. , Column.TYPE
  368. , item_type(u)
  369. , Column.GUID
  370. , unit_id
  371. , Column.NAME
  372. , _level.object_editor_name(unit_id)
  373. , -1
  374. );
  375. }
  376. foreach (Guid sound in sounds) {
  377. Gtk.TreeIter iter;
  378. _tree_store.insert_with_values(out iter
  379. , sounds_iter
  380. , -1
  381. , Column.TYPE
  382. , ItemType.SOUND
  383. , Column.GUID
  384. , sound
  385. , Column.NAME
  386. , _level.object_editor_name(sound)
  387. , -1
  388. );
  389. }
  390. _tree_view.model = _tree_sort;
  391. _tree_view.expand_all();
  392. _tree_selection.changed.connect(on_tree_selection_changed);
  393. }
  394. private void on_filter_entry_text_changed()
  395. {
  396. _tree_selection.changed.disconnect(on_tree_selection_changed);
  397. _tree_filter.refilter();
  398. _tree_selection.changed.connect(on_tree_selection_changed);
  399. }
  400. private Gtk.RadioButton add_sort_item(Gtk.RadioButton? group, SortMode mode)
  401. {
  402. var button = new Gtk.RadioButton.with_label_from_widget(group, mode.to_label());
  403. button.toggled.connect(() => {
  404. _sort_mode = mode;
  405. _tree_filter.refilter();
  406. _sort_items_popover.popdown();
  407. });
  408. _sort_items_box.pack_start(button, false, false);
  409. return button;
  410. }
  411. }
  412. } /* namespace Crown */