level_tree_view.vala 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480
  1. /*
  2. * Copyright (c) 2012-2024 Daniele Bartolini et al.
  3. * SPDX-License-Identifier: GPL-3.0-or-later
  4. */
  5. using Gee;
  6. using Gtk;
  7. namespace Crown
  8. {
  9. public class LevelTreeView : Gtk.Box
  10. {
  11. private enum ItemType
  12. {
  13. FOLDER,
  14. UNIT,
  15. SOUND,
  16. LIGHT,
  17. CAMERA
  18. }
  19. private enum Column
  20. {
  21. TYPE,
  22. GUID,
  23. NAME,
  24. COUNT
  25. }
  26. // Data
  27. private Level _level;
  28. private Database _db;
  29. // Widgets
  30. private EntrySearch _filter_entry;
  31. private Gtk.TreeStore _tree_store;
  32. private Gtk.TreeModelFilter _tree_filter;
  33. private Gtk.TreeModelSort _tree_sort;
  34. private Gtk.TreeView _tree_view;
  35. private Gtk.TreeSelection _tree_selection;
  36. private Gtk.ScrolledWindow _scrolled_window;
  37. public LevelTreeView(Database db, Level level)
  38. {
  39. Object(orientation: Gtk.Orientation.VERTICAL, spacing: 0);
  40. // Data
  41. _level = level;
  42. _level.selection_changed.connect(on_level_selection_changed);
  43. _level.object_editor_name_changed.connect(on_object_editor_name_changed);
  44. _db = db;
  45. _db.key_changed.connect(on_database_key_changed);
  46. // Widgets
  47. _filter_entry = new EntrySearch();
  48. _filter_entry.set_placeholder_text("Search...");
  49. _filter_entry.search_changed.connect(on_filter_entry_text_changed);
  50. _tree_store = new Gtk.TreeStore(Column.COUNT
  51. , typeof(int) // Column.TYPE
  52. , typeof(Guid) // Column.GUID
  53. , typeof(string) // Column.NAME
  54. );
  55. _tree_filter = new Gtk.TreeModelFilter(_tree_store, null);
  56. _tree_filter.set_visible_func((model, iter) => {
  57. _tree_view.expand_all();
  58. Value type;
  59. Value name;
  60. model.get_value(iter, Column.TYPE, out type);
  61. model.get_value(iter, Column.NAME, out name);
  62. if ((int)type == ItemType.FOLDER)
  63. return true;
  64. string name_str = (string)name;
  65. string filter_text = _filter_entry.text.down();
  66. return name_str != null
  67. && (filter_text == "" || name_str.down().index_of(filter_text) > -1)
  68. ;
  69. });
  70. _tree_sort = new Gtk.TreeModelSort.with_model(_tree_filter);
  71. _tree_sort.set_default_sort_func((model, iter_a, iter_b) => {
  72. Value type_a;
  73. Value type_b;
  74. model.get_value(iter_a, Column.TYPE, out type_a);
  75. model.get_value(iter_b, Column.TYPE, out type_b);
  76. if ((int)type_a == ItemType.FOLDER || (int)type_b == ItemType.FOLDER)
  77. return -1;
  78. Value id_a;
  79. Value id_b;
  80. model.get_value(iter_a, Column.NAME, out id_a);
  81. model.get_value(iter_b, Column.NAME, out id_b);
  82. return strcmp((string)id_a, (string)id_b);
  83. });
  84. Gtk.TreeViewColumn column = new Gtk.TreeViewColumn();
  85. Gtk.CellRendererPixbuf cell_pixbuf = new Gtk.CellRendererPixbuf();
  86. Gtk.CellRendererText cell_text = new Gtk.CellRendererText();
  87. column.pack_start(cell_pixbuf, false);
  88. column.pack_start(cell_text, true);
  89. column.set_cell_data_func(cell_pixbuf, (cell_layout, cell, model, iter) => {
  90. Value type;
  91. model.get_value(iter, LevelTreeView.Column.TYPE, out type);
  92. if ((int)type == LevelTreeView.ItemType.FOLDER)
  93. cell.set_property("icon-name", "browser-folder-symbolic");
  94. else if ((int)type == LevelTreeView.ItemType.UNIT)
  95. cell.set_property("icon-name", "level-object-unit");
  96. else if ((int)type == LevelTreeView.ItemType.SOUND)
  97. cell.set_property("icon-name", "level-object-sound");
  98. else if ((int)type == LevelTreeView.ItemType.LIGHT)
  99. cell.set_property("icon-name", "level-object-light");
  100. else if ((int)type == LevelTreeView.ItemType.CAMERA)
  101. cell.set_property("icon-name", "level-object-camera");
  102. else
  103. cell.set_property("icon-name", "level-object-unknown");
  104. });
  105. column.set_cell_data_func(cell_text, (cell_layout, cell, model, iter) => {
  106. Value name;
  107. model.get_value(iter, LevelTreeView.Column.NAME, out name);
  108. cell.set_property("text", (string)name);
  109. });
  110. _tree_view = new Gtk.TreeView();
  111. _tree_view.append_column(column);
  112. #if 0
  113. // For debugging.
  114. _tree_view.insert_column_with_attributes(-1
  115. , "Guids"
  116. , new gtk.CellRendererText()
  117. , "text"
  118. , Column.GUID
  119. , null
  120. );
  121. #endif
  122. _tree_view.headers_clickable = false;
  123. _tree_view.headers_visible = false;
  124. _tree_view.model = _tree_sort;
  125. _tree_view.button_press_event.connect(on_button_pressed);
  126. _tree_view.button_release_event.connect(on_button_released);
  127. _tree_selection = _tree_view.get_selection();
  128. _tree_selection.set_mode(Gtk.SelectionMode.MULTIPLE);
  129. _tree_selection.changed.connect(on_tree_selection_changed);
  130. _scrolled_window = new Gtk.ScrolledWindow(null, null);
  131. _scrolled_window.add(_tree_view);
  132. this.pack_start(_filter_entry, false, true, 0);
  133. this.pack_start(_scrolled_window, true, true, 0);
  134. }
  135. private bool on_button_pressed(Gdk.EventButton ev)
  136. {
  137. if (ev.button == Gdk.BUTTON_SECONDARY) {
  138. Gtk.TreePath path;
  139. Gtk.TreeViewColumn column;
  140. if (_tree_view.get_path_at_pos((int)ev.x, (int)ev.y, out path, out column, null, null)) {
  141. if (!_tree_selection.path_is_selected(path)) {
  142. _tree_selection.unselect_all();
  143. _tree_selection.select_path(path);
  144. }
  145. } else { // Clicked on empty space.
  146. return Gdk.EVENT_PROPAGATE;
  147. }
  148. Gtk.Menu menu = new Gtk.Menu();
  149. Gtk.MenuItem mi;
  150. mi = new Gtk.MenuItem.with_label("Rename...");
  151. mi.activate.connect(() => {
  152. Gtk.Dialog dg = new Gtk.Dialog.with_buttons("New Name"
  153. , (Gtk.Window)this.get_toplevel()
  154. , DialogFlags.MODAL
  155. , "Cancel"
  156. , ResponseType.CANCEL
  157. , "Ok"
  158. , ResponseType.OK
  159. , null
  160. );
  161. EntryText sb = new EntryText();
  162. _tree_selection.selected_foreach((model, path, iter) => {
  163. Value name;
  164. model.get_value(iter, Column.NAME, out name);
  165. sb.text = (string)name;
  166. return;
  167. });
  168. sb.activate.connect(() => { dg.response(ResponseType.OK); });
  169. dg.get_content_area().add(sb);
  170. dg.skip_taskbar_hint = true;
  171. dg.show_all();
  172. if (dg.run() == (int)ResponseType.OK) {
  173. string cur_name = "";
  174. string new_name = "";
  175. Guid object_id = GUID_ZERO;
  176. _tree_selection.selected_foreach((model, path, iter) => {
  177. Value type;
  178. model.get_value(iter, Column.TYPE, out type);
  179. if ((int)type == ItemType.FOLDER)
  180. return;
  181. Value name;
  182. model.get_value(iter, Column.NAME, out name);
  183. cur_name = (string)name;
  184. Value guid;
  185. model.get_value(iter, Column.GUID, out guid);
  186. object_id = (Guid)guid;
  187. new_name = sb.text.strip();
  188. });
  189. if (new_name != "" && new_name != cur_name)
  190. _level.object_set_editor_name(object_id, new_name);
  191. }
  192. dg.destroy();
  193. });
  194. if (_tree_selection.count_selected_rows() == 1)
  195. menu.add(mi);
  196. mi = new Gtk.MenuItem.with_label("Duplicate");
  197. mi.activate.connect(() => {
  198. GLib.Application.get_default().activate_action("duplicate", null);
  199. });
  200. menu.add(mi);
  201. mi = new Gtk.MenuItem.with_label("Delete");
  202. mi.activate.connect(() => {
  203. GLib.Application.get_default().activate_action("delete", null);
  204. });
  205. menu.add(mi);
  206. menu.show_all();
  207. menu.popup_at_pointer(ev);
  208. return Gdk.EVENT_STOP;
  209. }
  210. return Gdk.EVENT_PROPAGATE;
  211. }
  212. private bool on_button_released(Gdk.EventButton ev)
  213. {
  214. if (ev.button == Gdk.BUTTON_PRIMARY) {
  215. Gtk.TreePath path;
  216. if (_tree_view.get_path_at_pos((int)ev.x, (int)ev.y, out path, null, null, null)) {
  217. Gtk.TreeIter iter;
  218. _tree_view.model.get_iter(out iter, path);
  219. Value type;
  220. _tree_view.model.get_value(iter, Column.TYPE, out type);
  221. if ((int)type != ItemType.FOLDER)
  222. return Gdk.EVENT_PROPAGATE;
  223. if (_tree_view.is_row_expanded(path))
  224. _tree_view.collapse_row(path);
  225. else
  226. _tree_view.expand_row(path, /*open_all = */ false);
  227. return Gdk.EVENT_STOP;
  228. }
  229. }
  230. return Gdk.EVENT_PROPAGATE;
  231. }
  232. private void on_tree_selection_changed()
  233. {
  234. _level.selection_changed.disconnect(on_level_selection_changed);
  235. Guid?[] ids = {};
  236. _tree_selection.selected_foreach((model, path, iter) => {
  237. Value type;
  238. model.get_value(iter, Column.TYPE, out type);
  239. if ((int)type == ItemType.FOLDER)
  240. return;
  241. Value id;
  242. model.get_value(iter, Column.GUID, out id);
  243. ids += (Guid)id;
  244. });
  245. _level.selection_set(ids);
  246. _level.selection_changed.connect(on_level_selection_changed);
  247. }
  248. private void on_level_selection_changed(Gee.ArrayList<Guid?> selection)
  249. {
  250. _tree_selection.changed.disconnect(on_tree_selection_changed);
  251. _tree_selection.unselect_all();
  252. Gtk.TreePath? last_selected = null;
  253. _tree_sort.foreach ((model, path, iter) => {
  254. Value type;
  255. model.get_value(iter, Column.TYPE, out type);
  256. if ((int)type == ItemType.FOLDER)
  257. return false;
  258. Value id;
  259. model.get_value(iter, Column.GUID, out id);
  260. foreach (Guid? guid in selection) {
  261. if ((Guid)id == guid) {
  262. _tree_selection.select_iter(iter);
  263. last_selected = path;
  264. return false;
  265. }
  266. }
  267. return false;
  268. });
  269. if (last_selected != null)
  270. _tree_view.scroll_to_cell(last_selected, null, false, 0.0f, 0.0f);
  271. _tree_selection.changed.connect(on_tree_selection_changed);
  272. }
  273. private void on_object_editor_name_changed(Guid object_id, string name)
  274. {
  275. _tree_sort.foreach ((model, path, iter) => {
  276. Value type;
  277. model.get_value(iter, Column.TYPE, out type);
  278. if ((int)type == ItemType.FOLDER)
  279. return false;
  280. Value guid;
  281. model.get_value(iter, Column.GUID, out guid);
  282. Guid guid_model = (Guid)guid;
  283. if (guid_model == object_id) {
  284. Gtk.TreeIter iter_filter;
  285. Gtk.TreeIter iter_model;
  286. _tree_sort.convert_iter_to_child_iter(out iter_filter, iter);
  287. _tree_filter.convert_iter_to_child_iter(out iter_model, iter_filter);
  288. _tree_store.set(iter_model
  289. , Column.NAME
  290. , name
  291. , -1
  292. );
  293. return true;
  294. }
  295. return false;
  296. });
  297. }
  298. private void on_database_key_changed(Guid id, string key)
  299. {
  300. if (id != _level._id)
  301. return;
  302. if (key != "units" && key != "sounds")
  303. return;
  304. _tree_selection.changed.disconnect(on_tree_selection_changed);
  305. _tree_view.model = null;
  306. _tree_store.clear();
  307. Gtk.TreeIter units_iter;
  308. _tree_store.insert_with_values(out units_iter
  309. , null
  310. , -1
  311. , Column.TYPE
  312. , ItemType.FOLDER
  313. , Column.GUID
  314. , GUID_ZERO
  315. , Column.NAME
  316. , "Units"
  317. , -1
  318. );
  319. Gtk.TreeIter lights_iter;
  320. _tree_store.insert_with_values(out lights_iter
  321. , null
  322. , -1
  323. , Column.TYPE
  324. , ItemType.FOLDER
  325. , Column.GUID
  326. , GUID_ZERO
  327. , Column.NAME
  328. , "Lights"
  329. , -1
  330. );
  331. Gtk.TreeIter sounds_iter;
  332. _tree_store.insert_with_values(out sounds_iter
  333. , null
  334. , -1
  335. , Column.TYPE
  336. , ItemType.FOLDER
  337. , Column.GUID
  338. , GUID_ZERO
  339. , Column.NAME
  340. , "Sounds"
  341. , -1
  342. );
  343. Gtk.TreeIter cameras_iter;
  344. _tree_store.insert_with_values(out cameras_iter
  345. , null
  346. , -1
  347. , Column.TYPE
  348. , ItemType.FOLDER
  349. , Column.GUID
  350. , GUID_ZERO
  351. , Column.NAME
  352. , "Cameras"
  353. , -1
  354. );
  355. HashSet<Guid?> units = _db.get_property_set(_level._id, "units", new HashSet<Guid?>());
  356. HashSet<Guid?> sounds = _db.get_property_set(_level._id, "sounds", new HashSet<Guid?>());
  357. foreach (Guid unit in units) {
  358. Unit u = new Unit(_level._db, unit);
  359. int item_type = LevelTreeView.ItemType.UNIT;
  360. Gtk.TreeIter tree_iter = units_iter;
  361. if (u.is_light()) {
  362. item_type = LevelTreeView.ItemType.LIGHT;
  363. tree_iter = lights_iter;
  364. } else if (u.is_camera()) {
  365. item_type = LevelTreeView.ItemType.CAMERA;
  366. tree_iter = cameras_iter;
  367. }
  368. Gtk.TreeIter iter;
  369. _tree_store.insert_with_values(out iter
  370. , tree_iter
  371. , -1
  372. , Column.TYPE
  373. , item_type
  374. , Column.GUID
  375. , unit
  376. , Column.NAME
  377. , _level.object_editor_name(unit)
  378. , -1
  379. );
  380. }
  381. foreach (Guid sound in sounds) {
  382. Gtk.TreeIter iter;
  383. _tree_store.insert_with_values(out iter
  384. , sounds_iter
  385. , -1
  386. , Column.TYPE
  387. , ItemType.SOUND
  388. , Column.GUID
  389. , sound
  390. , Column.NAME
  391. , _level.object_editor_name(sound)
  392. , -1
  393. );
  394. }
  395. _tree_view.model = _tree_sort;
  396. _tree_view.expand_all();
  397. _tree_selection.changed.connect(on_tree_selection_changed);
  398. }
  399. private void on_filter_entry_text_changed()
  400. {
  401. _tree_selection.changed.disconnect(on_tree_selection_changed);
  402. _tree_filter.refilter();
  403. _tree_selection.changed.connect(on_tree_selection_changed);
  404. }
  405. }
  406. } /* namespace Crown */