project_browser.vala 36 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154
  1. /*
  2. * Copyright (c) 2012-2024 Daniele Bartolini et al.
  3. * SPDX-License-Identifier: GPL-3.0-or-later
  4. */
  5. using Gtk;
  6. using Gee;
  7. namespace Crown
  8. {
  9. public const Gtk.TargetEntry[] dnd_targets =
  10. {
  11. { "RESOURCE_PATH", Gtk.TargetFlags.SAME_APP, 0 },
  12. };
  13. // Menu to open when clicking on project's files and folders.
  14. private Gtk.Menu? project_entry_menu_create(string type, string name)
  15. {
  16. Gtk.Menu? menu;
  17. if (type == "<folder>") {
  18. if (name == "..")
  19. return null;
  20. menu = new Gtk.Menu();
  21. Gtk.MenuItem mi;
  22. mi = new Gtk.MenuItem.with_label("Import...");
  23. mi.activate.connect(() => {
  24. GLib.Application.get_default().activate_action("import", new GLib.Variant.string((string)name));
  25. });
  26. menu.add(mi);
  27. mi = new Gtk.SeparatorMenuItem();
  28. menu.add(mi);
  29. mi = new Gtk.MenuItem.with_label("New Script...");
  30. mi.activate.connect(() => {
  31. Gtk.Dialog dg = new Gtk.Dialog.with_buttons("Script Name"
  32. , ((Gtk.Application)GLib.Application.get_default()).active_window
  33. , DialogFlags.MODAL
  34. , "Cancel"
  35. , ResponseType.CANCEL
  36. , "Ok"
  37. , ResponseType.OK
  38. , null
  39. );
  40. EntryText sb = new EntryText();
  41. sb.activate.connect(() => { dg.response(ResponseType.OK); });
  42. dg.get_content_area().add(sb);
  43. dg.skip_taskbar_hint = true;
  44. dg.show_all();
  45. if (dg.run() == (int)ResponseType.OK) {
  46. if (sb.text.strip() == "") {
  47. dg.destroy();
  48. return;
  49. }
  50. var tuple = new GLib.Variant.tuple({(string)name, sb.text, true});
  51. GLib.Application.get_default().activate_action("create-script", tuple);
  52. }
  53. dg.destroy();
  54. });
  55. menu.add(mi);
  56. mi = new Gtk.MenuItem.with_label("New Script (Unit)...");
  57. mi.activate.connect(() => {
  58. Gtk.Dialog dg = new Gtk.Dialog.with_buttons("Script Name"
  59. , ((Gtk.Application)GLib.Application.get_default()).active_window
  60. , DialogFlags.MODAL
  61. , "Cancel"
  62. , ResponseType.CANCEL
  63. , "Ok"
  64. , ResponseType.OK
  65. , null
  66. );
  67. EntryText sb = new EntryText();
  68. sb.activate.connect(() => { dg.response(ResponseType.OK); });
  69. dg.get_content_area().add(sb);
  70. dg.skip_taskbar_hint = true;
  71. dg.show_all();
  72. if (dg.run() == (int)ResponseType.OK) {
  73. if (sb.text.strip() == "") {
  74. dg.destroy();
  75. return;
  76. }
  77. var tuple = new GLib.Variant.tuple({(string)name, sb.text, false});
  78. GLib.Application.get_default().activate_action("create-script", tuple);
  79. }
  80. dg.destroy();
  81. });
  82. menu.add(mi);
  83. mi = new Gtk.SeparatorMenuItem();
  84. menu.add(mi);
  85. mi = new Gtk.MenuItem.with_label("New Unit...");
  86. mi.activate.connect(() => {
  87. Gtk.Dialog dg = new Gtk.Dialog.with_buttons("Unit Name"
  88. , ((Gtk.Application)GLib.Application.get_default()).active_window
  89. , DialogFlags.MODAL
  90. , "Cancel"
  91. , ResponseType.CANCEL
  92. , "Ok"
  93. , ResponseType.OK
  94. , null
  95. );
  96. EntryText sb = new EntryText();
  97. sb.activate.connect(() => { dg.response(ResponseType.OK); });
  98. dg.get_content_area().add(sb);
  99. dg.skip_taskbar_hint = true;
  100. dg.show_all();
  101. if (dg.run() == (int)ResponseType.OK) {
  102. if (sb.text.strip() == "") {
  103. dg.destroy();
  104. return;
  105. }
  106. }
  107. var tuple = new GLib.Variant.tuple({(string)name, sb.text});
  108. GLib.Application.get_default().activate_action("create-unit", tuple);
  109. dg.destroy();
  110. });
  111. menu.add(mi);
  112. mi = new Gtk.SeparatorMenuItem();
  113. menu.add(mi);
  114. mi = new Gtk.MenuItem.with_label("New Folder...");
  115. mi.activate.connect(() => {
  116. Gtk.Dialog dg = new Gtk.Dialog.with_buttons("Folder Name"
  117. , ((Gtk.Application)GLib.Application.get_default()).active_window
  118. , DialogFlags.MODAL
  119. , "Cancel"
  120. , ResponseType.CANCEL
  121. , "Ok"
  122. , ResponseType.OK
  123. , null
  124. );
  125. EntryText sb = new EntryText();
  126. sb.activate.connect(() => { dg.response(ResponseType.OK); });
  127. dg.get_content_area().add(sb);
  128. dg.skip_taskbar_hint = true;
  129. dg.show_all();
  130. if (dg.run() == (int)ResponseType.OK) {
  131. if (sb.text.strip() == "") {
  132. dg.destroy();
  133. return;
  134. }
  135. var tuple = new GLib.Variant.tuple({(string)name, sb.text});
  136. GLib.Application.get_default().activate_action("create-directory", tuple);
  137. }
  138. dg.destroy();
  139. });
  140. menu.add(mi);
  141. if ((string)name != ProjectStore.ROOT_FOLDER) {
  142. mi = new Gtk.MenuItem.with_label("Delete Folder");
  143. mi.activate.connect(() => {
  144. Gtk.MessageDialog md = new Gtk.MessageDialog(((Gtk.Application)GLib.Application.get_default()).active_window
  145. , Gtk.DialogFlags.MODAL
  146. , Gtk.MessageType.WARNING
  147. , Gtk.ButtonsType.NONE
  148. , "Delete Folder " + (string)name + "?"
  149. );
  150. Gtk.Widget btn;
  151. md.add_button("_Cancel", ResponseType.CANCEL);
  152. btn = md.add_button("_Delete", ResponseType.YES);
  153. btn.get_style_context().add_class(Gtk.STYLE_CLASS_DESTRUCTIVE_ACTION);
  154. md.set_default_response(ResponseType.CANCEL);
  155. int rt = md.run();
  156. md.destroy();
  157. if (rt != (int)ResponseType.YES)
  158. return;
  159. GLib.Application.get_default().activate_action("delete-directory", new GLib.Variant.string((string)name));
  160. });
  161. menu.add(mi);
  162. }
  163. } else { // If file
  164. menu = new Gtk.Menu();
  165. Gtk.MenuItem mi;
  166. mi = new Gtk.MenuItem.with_label("Delete File");
  167. mi.activate.connect(() => {
  168. GLib.Application.get_default().activate_action("delete-file", new GLib.Variant.string(ResourceId.path((string)type, (string)name)));
  169. });
  170. menu.add(mi);
  171. mi = new Gtk.MenuItem.with_label("Open Containing Folder...");
  172. mi.activate.connect(() => {
  173. GLib.Application.get_default().activate_action("open-containing", new GLib.Variant.string(name));
  174. });
  175. menu.add(mi);
  176. }
  177. // Add shared menu items.
  178. Gtk.MenuItem mi;
  179. mi = new Gtk.SeparatorMenuItem();
  180. menu.add(mi);
  181. mi = new Gtk.MenuItem.with_label("Copy Path");
  182. mi.activate.connect(() => {
  183. string path;
  184. if (type == "<folder>")
  185. path = name;
  186. else
  187. path = ResourceId.path(type, name);
  188. GLib.Application.get_default().activate_action("copy-path", new GLib.Variant.string(path));
  189. });
  190. menu.add(mi);
  191. if (type != "<folder>" || name != "") {
  192. mi = new Gtk.MenuItem.with_label("Add to Favorites");
  193. mi.activate.connect(() => {
  194. var tuple = new GLib.Variant.tuple({type, name});
  195. GLib.Application.get_default().activate_action("favorite-resource", tuple);
  196. });
  197. menu.add(mi);
  198. }
  199. return menu;
  200. }
  201. // Menu to open when clicking on favorites' entries.
  202. private Gtk.Menu? favorites_entry_menu_create(string type, string name)
  203. {
  204. Gtk.Menu? menu;
  205. menu = new Gtk.Menu();
  206. Gtk.MenuItem mi;
  207. mi = new Gtk.MenuItem.with_label("Open Containing Folder...");
  208. mi.activate.connect(() => {
  209. GLib.Application.get_default().activate_action("open-containing", new GLib.Variant.string(name));
  210. });
  211. menu.add(mi);
  212. mi = new Gtk.SeparatorMenuItem();
  213. menu.add(mi);
  214. mi = new Gtk.MenuItem.with_label("Copy Path");
  215. mi.activate.connect(() => {
  216. string path;
  217. if (type == "<folder>")
  218. path = name;
  219. else
  220. path = ResourceId.path(type, name);
  221. GLib.Application.get_default().activate_action("copy-path", new GLib.Variant.string(path));
  222. });
  223. menu.add(mi);
  224. mi = new Gtk.MenuItem.with_label("Remove from Favorites");
  225. mi.activate.connect(() => {
  226. var tuple = new GLib.Variant.tuple({type, name});
  227. GLib.Application.get_default().activate_action("unfavorite-resource", tuple);
  228. });
  229. menu.add(mi);
  230. return menu;
  231. }
  232. public class ProjectIconView : Gtk.IconView
  233. {
  234. const int ICON_SIZE = 48;
  235. public enum Column
  236. {
  237. TYPE,
  238. NAME,
  239. PIXBUF,
  240. COUNT
  241. }
  242. public string _selected_type;
  243. public string _selected_name;
  244. public ProjectStore _project_store;
  245. public ThumbnailCache _thumbnail_cache;
  246. public Gtk.ListStore _list_store;
  247. public Gtk.CellRendererPixbuf _cell_renderer_pixbuf;
  248. public Gtk.CellRendererText _cell_renderer_text;
  249. public Gdk.Pixbuf _empty_pixbuf;
  250. public bool _showing_project_folder;
  251. public ProjectIconView(ProjectStore project_store, ThumbnailCache thumbnail_cache)
  252. {
  253. _project_store = project_store;
  254. _thumbnail_cache = thumbnail_cache;
  255. _thumbnail_cache.changed.connect(() => {
  256. this.queue_draw();
  257. });
  258. _list_store = new Gtk.ListStore(Column.COUNT
  259. , typeof(string) // Column.TYPE
  260. , typeof(string) // Column.NAME
  261. , typeof(Gdk.Pixbuf) // Column.PIXBUF
  262. );
  263. _list_store.set_sort_column_id(Column.TYPE, Gtk.SortType.ASCENDING);
  264. this.button_press_event.connect(on_button_pressed);
  265. this.set_item_width(80);
  266. _cell_renderer_pixbuf = new Gtk.CellRendererPixbuf();
  267. _cell_renderer_pixbuf.stock_size = Gtk.IconSize.DIALOG;
  268. // https://gitlab.gnome.org/GNOME/gtk/-/blob/3.24.43/gtk/gtkiconview.c#L5147
  269. _cell_renderer_text = new Gtk.CellRendererText();
  270. _cell_renderer_text.set("wrap-mode", Pango.WrapMode.WORD_CHAR);
  271. _cell_renderer_text.set("alignment", Pango.Alignment.CENTER);
  272. _cell_renderer_text.set("xalign", 0.5);
  273. _cell_renderer_text.set("yalign", 0.0);
  274. int wrap_width = this.item_width;
  275. wrap_width -= 2 * this.item_padding * 2;
  276. _cell_renderer_text.set("wrap-width", wrap_width);
  277. _cell_renderer_text.set("width", wrap_width);
  278. _empty_pixbuf = new Gdk.Pixbuf.from_data({ 0x00, 0x00, 0x00, 0x00 }, Gdk.Colorspace.RGB, true, 8, 1, 1, 4);
  279. _showing_project_folder = true;
  280. this.pack_start(_cell_renderer_pixbuf, false);
  281. this.pack_start(_cell_renderer_text, false);
  282. this.set_cell_data_func(_cell_renderer_pixbuf, pixbuf_func);
  283. this.set_cell_data_func(_cell_renderer_text, text_func);
  284. this.set_model(_list_store);
  285. this.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, dnd_targets, Gdk.DragAction.COPY);
  286. this.drag_data_get.connect(on_drag_data_get);
  287. this.drag_begin.connect_after(on_drag_begin);
  288. this.drag_end.connect(on_drag_end);
  289. }
  290. private void on_drag_data_get(Gdk.DragContext context, Gtk.SelectionData data, uint info, uint time_)
  291. {
  292. // https://valadoc.org/gtk+-3.0/Gtk.Widget.drag_data_get.html
  293. GLib.List<Gtk.TreePath> selected_paths = this.get_selected_items();
  294. if (selected_paths.length() == 0u)
  295. return;
  296. Gtk.TreeIter selected_iter;
  297. this.model.get_iter(out selected_iter, selected_paths.nth(0).data);
  298. Value val;
  299. string type;
  300. string name;
  301. this.model.get_value(selected_iter, Column.TYPE, out val);
  302. type = (string)val;
  303. this.model.get_value(selected_iter, Column.NAME, out val);
  304. name = (string)val;
  305. string resource_path = ResourceId.path(type, name);
  306. data.set(Gdk.Atom.intern_static_string("RESOURCE_PATH"), 8, resource_path.data);
  307. }
  308. private void on_drag_begin(Gdk.DragContext context)
  309. {
  310. // https://valadoc.org/gtk+-3.0/Gtk.Widget.drag_begin.html
  311. Gtk.drag_set_icon_pixbuf(context, _empty_pixbuf, 0, 0);
  312. }
  313. private void on_drag_end(Gdk.DragContext context)
  314. {
  315. // https://valadoc.org/gtk+-3.0/Gtk.Widget.drag_end.html
  316. GLib.Application.get_default().activate_action("cancel-place", null);
  317. }
  318. private bool on_button_pressed(Gdk.EventButton ev)
  319. {
  320. if (ev.button == Gdk.BUTTON_SECONDARY) {
  321. string type;
  322. string name;
  323. Gtk.TreePath path;
  324. if ((path = this.get_path_at_pos((int)ev.x, (int)ev.y)) != null) {
  325. select_path(path);
  326. scroll_to_path(path, false, 0.0f, 0.0f);
  327. Gtk.TreeIter iter;
  328. this.model.get_iter(out iter, path);
  329. Value val;
  330. this.model.get_value(iter, Column.TYPE, out val);
  331. type = (string)val;
  332. this.model.get_value(iter, Column.NAME, out val);
  333. name = (string)val;
  334. } else {
  335. type = _selected_type;
  336. name = _selected_name;
  337. }
  338. Gtk.Menu? menu;
  339. menu = project_entry_menu_create(type, name);
  340. if (_showing_project_folder)
  341. menu = project_entry_menu_create(type, name);
  342. else
  343. menu = favorites_entry_menu_create(type, name);
  344. if (menu != null) {
  345. menu.show_all();
  346. menu.popup_at_pointer(ev);
  347. }
  348. } else if (ev.button == Gdk.BUTTON_PRIMARY && ev.type == Gdk.EventType.@2BUTTON_PRESS) {
  349. Gtk.TreePath path;
  350. if ((path = this.get_path_at_pos((int)ev.x, (int)ev.y)) != null) {
  351. Gtk.TreeIter iter;
  352. this.model.get_iter(out iter, path);
  353. Value type;
  354. Value name;
  355. this.model.get_value(iter, Column.TYPE, out type);
  356. this.model.get_value(iter, Column.NAME, out name);
  357. if ((string)type == "<folder>") {
  358. string dir_name;
  359. if ((string)name == "..")
  360. dir_name = ResourceId.parent_folder((string)_selected_name);
  361. else
  362. dir_name = (string)name;
  363. GLib.Application.get_default().activate_action("open-directory", new GLib.Variant.string(dir_name));
  364. } else {
  365. GLib.Application.get_default().activate_action("open-resource", ResourceId.path((string)type, (string)name));
  366. }
  367. }
  368. }
  369. return Gdk.EVENT_PROPAGATE;
  370. }
  371. private void pixbuf_func(Gtk.CellLayout cell_layout, Gtk.CellRenderer cell, Gtk.TreeModel model, Gtk.TreeIter iter)
  372. {
  373. Value val;
  374. string type;
  375. string name;
  376. model.get_value(iter, Column.TYPE, out val);
  377. type = (string)val;
  378. model.get_value(iter, Column.NAME, out val);
  379. name = (string)val;
  380. // https://specifications.freedesktop.org/icon-naming-spec/icon-naming-spec-latest.html
  381. if (type == "<folder>")
  382. cell.set_property("icon-name", "browser-folder-symbolic");
  383. else if ((string)type == "state_machine")
  384. cell.set_property("icon-name", "text-x-generic-symbolic");
  385. else if ((string)type == "config")
  386. cell.set_property("icon-name", "text-x-generic-symbolic");
  387. else if ((string)type == "font")
  388. cell.set_property("icon-name", "font-x-generic-symbolic");
  389. else if ((string)type == "level")
  390. cell.set_property("icon-name", "text-x-generic-symbolic");
  391. else if ((string)type == "material")
  392. cell.set_property("pixbuf", _thumbnail_cache.get(type, name));
  393. else if ((string)type == "mesh")
  394. cell.set_property("icon-name", "text-x-generic-symbolic");
  395. else if ((string)type == "package")
  396. cell.set_property("icon-name", "package-x-generic-symbolic");
  397. else if ((string)type == "physics_config")
  398. cell.set_property("icon-name", "text-x-generic-symbolic");
  399. else if ((string)type == "lua")
  400. cell.set_property("icon-name", "x-office-document-symbolic");
  401. else if ((string)type == "unit")
  402. cell.set_property("pixbuf", _thumbnail_cache.get(type, name));
  403. else if ((string)type == "shader")
  404. cell.set_property("icon-name", "text-x-generic-symbolic");
  405. else if ((string)type == "sound")
  406. cell.set_property("pixbuf", _thumbnail_cache.get(type, name));
  407. else if ((string)type == "sprite_animation")
  408. cell.set_property("icon-name", "text-x-generic-symbolic");
  409. else if ((string)type == "sprite")
  410. cell.set_property("icon-name", "text-x-generic-symbolic");
  411. else if ((string)type == "texture")
  412. cell.set_property("pixbuf", _thumbnail_cache.get(type, name));
  413. else
  414. cell.set_property("icon-name", "text-x-generic-symbolic");
  415. }
  416. private void text_func(Gtk.CellLayout cell_layout, Gtk.CellRenderer cell, Gtk.TreeModel model, Gtk.TreeIter iter)
  417. {
  418. Value type;
  419. Value name;
  420. model.get_value(iter, Column.TYPE, out type);
  421. model.get_value(iter, Column.NAME, out name);
  422. if (name == "..")
  423. cell.set_property("text", name);
  424. else
  425. cell.set_property("text", GLib.Path.get_basename((string)name));
  426. }
  427. public void reveal(string type, string name)
  428. {
  429. _list_store.foreach((model, path, iter) => {
  430. GLib.Value val;
  431. string store_type;
  432. string store_name;
  433. model.get_value(iter, Column.TYPE, out val);
  434. store_type = (string)val;
  435. model.get_value(iter, Column.NAME, out val);
  436. store_name = (string)val;
  437. if (store_name == name && store_type == type) {
  438. select_path(path);
  439. scroll_to_path(path, false, 0.0f, 0.0f);
  440. return true;
  441. }
  442. return false;
  443. });
  444. }
  445. }
  446. public class ProjectBrowser : Gtk.Bin
  447. {
  448. // Data
  449. public ProjectStore _project_store;
  450. // Widgets
  451. public Gtk.TreeModelFilter _tree_filter;
  452. public Gtk.TreeModelSort _tree_sort;
  453. public Gtk.TreeView _tree_view;
  454. public Gtk.TreeSelection _tree_selection;
  455. public Gdk.Pixbuf _empty_pixbuf;
  456. public ProjectIconView _icon_view;
  457. public bool _show_icon_view;
  458. public Gtk.Button _toggle_icon_view;
  459. public Gtk.Box _tree_view_content;
  460. public Gtk.Box _icon_view_content;
  461. public Gtk.ScrolledWindow _scrolled_window_a;
  462. public Gtk.ScrolledWindow _scrolled_window_b;
  463. public Gtk.Paned _paned;
  464. public bool _hide_core_resources;
  465. public ProjectBrowser(ProjectStore project_store, ThumbnailCache thumbnail_cache)
  466. {
  467. // Data
  468. _project_store = project_store;
  469. // Widgets
  470. _tree_filter = new Gtk.TreeModelFilter(_project_store._tree_store, null);
  471. _tree_filter.set_visible_func((model, iter) => {
  472. if (_project_store.project_root_path() != null)
  473. _tree_view.expand_row(_project_store.project_root_path(), false);
  474. Value type;
  475. Value name;
  476. model.get_value(iter, ProjectStore.Column.TYPE, out type);
  477. model.get_value(iter, ProjectStore.Column.NAME, out name);
  478. bool should_show = (string)type != null
  479. && (string)name != null
  480. && !row_should_be_hidden((string)type, (string)name)
  481. ;
  482. if (_show_icon_view) {
  483. // Hide all descendants of the favorites root.
  484. Gtk.TreePath? path = model.get_path(iter);
  485. if (path != null && _project_store.favorites_root_path() != null && path.is_descendant(_project_store.favorites_root_path()))
  486. return false;
  487. return should_show && (type == "<folder>" || type == "<favorites>");
  488. } else {
  489. return should_show;
  490. }
  491. });
  492. _tree_sort = new Gtk.TreeModelSort.with_model(_tree_filter);
  493. _tree_sort.set_default_sort_func((model, iter_a, iter_b) => {
  494. Value type_a;
  495. Value type_b;
  496. model.get_value(iter_a, ProjectStore.Column.TYPE, out type_a);
  497. model.get_value(iter_b, ProjectStore.Column.TYPE, out type_b);
  498. // Favorites is always on top.
  499. if ((string)type_a == "<favorites>")
  500. return -1;
  501. if ((string)type_b == "<favorites>")
  502. return 1;
  503. // Then folders.
  504. if ((string)type_a == "<folder>") {
  505. if ((string)type_b != "<folder>")
  506. return -1;
  507. } else if ((string)type_b == "<folder>") {
  508. if ((string)type_a != "<folder>")
  509. return 1;
  510. }
  511. // And finally, regular files.
  512. Value id_a;
  513. Value id_b;
  514. model.get_value(iter_a, ProjectStore.Column.NAME, out id_a);
  515. model.get_value(iter_b, ProjectStore.Column.NAME, out id_b);
  516. return strcmp(GLib.Path.get_basename((string)id_a), GLib.Path.get_basename((string)id_b));
  517. });
  518. Gtk.CellRendererPixbuf cell_pixbuf = new Gtk.CellRendererPixbuf();
  519. Gtk.CellRendererText cell_text = new Gtk.CellRendererText();
  520. Gtk.TreeViewColumn column = new Gtk.TreeViewColumn();
  521. column.pack_start(cell_pixbuf, false);
  522. column.pack_start(cell_text, true);
  523. column.set_cell_data_func(cell_pixbuf, pixbuf_func);
  524. column.set_cell_data_func(cell_text, text_func);
  525. _tree_view = new Gtk.TreeView();
  526. _tree_view.append_column(column);
  527. #if 0
  528. // For debugging.
  529. _tree_view.insert_column_with_attributes(-1
  530. , "Segment"
  531. , new Gtk.CellRendererText()
  532. , "text"
  533. , ProjectStore.Column.SEGMENT
  534. , null
  535. );
  536. _tree_view.insert_column_with_attributes(-1
  537. , "Name"
  538. , new Gtk.CellRendererText()
  539. , "text"
  540. , ProjectStore.Column.NAME
  541. , null
  542. );
  543. _tree_view.insert_column_with_attributes(-1
  544. , "Type"
  545. , new Gtk.CellRendererText()
  546. , "text"
  547. , ProjectStore.Column.TYPE
  548. , null
  549. );
  550. #endif /* if 0 */
  551. _tree_view.model = _tree_sort;
  552. _tree_view.headers_visible = false;
  553. _tree_view.button_press_event.connect(on_button_pressed);
  554. _tree_view.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, dnd_targets, Gdk.DragAction.COPY);
  555. _tree_view.drag_data_get.connect(on_drag_data_get);
  556. _tree_view.drag_begin.connect_after(on_drag_begin);
  557. _tree_view.drag_end.connect(on_drag_end);
  558. _tree_selection = _tree_view.get_selection();
  559. _tree_selection.set_mode(Gtk.SelectionMode.BROWSE);
  560. _tree_selection.changed.connect(() => { update_icon_view(); });
  561. _empty_pixbuf = new Gdk.Pixbuf.from_data({ 0x00, 0x00, 0x00, 0x00 }, Gdk.Colorspace.RGB, true, 8, 1, 1, 4);
  562. _project_store._tree_store.row_inserted.connect((path, iter) => { update_icon_view(); });
  563. _project_store._tree_store.row_deleted.connect((path) => { update_icon_view(); });
  564. // Create icon view.
  565. _icon_view = new ProjectIconView(_project_store, thumbnail_cache);
  566. // Create switch button.
  567. _show_icon_view = true;
  568. _toggle_icon_view = new Gtk.Button.from_icon_name("level-tree-symbolic", Gtk.IconSize.SMALL_TOOLBAR);
  569. _toggle_icon_view.get_style_context().add_class("flat");
  570. _toggle_icon_view.can_focus = false;
  571. _toggle_icon_view.clicked.connect(() => {
  572. _show_icon_view = !_show_icon_view;
  573. if (_show_icon_view) {
  574. // Save the currently selected resource and a path to its parent. Those will be
  575. // used later, after the tree has been refiltered, to show the correct folder
  576. // and reveal the selected resource in the icon view.
  577. string? selected_type = null;
  578. string? selected_name = null;
  579. Gtk.TreePath? parent_path = null;
  580. Gtk.TreeModel selected_model;
  581. Gtk.TreeIter selected_iter;
  582. if (_tree_selection.get_selected(out selected_model, out selected_iter)) {
  583. Value val;
  584. selected_model.get_value(selected_iter, ProjectStore.Column.TYPE, out val);
  585. selected_type = (string)val;
  586. selected_model.get_value(selected_iter, ProjectStore.Column.NAME, out val);
  587. selected_name = (string)val;
  588. if (selected_type != "<folder>") {
  589. Gtk.TreeIter parent_iter;
  590. if (selected_model.iter_parent(out parent_iter, selected_iter))
  591. parent_path = _tree_view.model.get_path(parent_iter);
  592. }
  593. }
  594. _tree_filter.refilter();
  595. if (parent_path != null) {
  596. _tree_selection.select_path(parent_path);
  597. _icon_view.reveal(selected_type, selected_name);
  598. }
  599. _icon_view_content.show_all();
  600. _toggle_icon_view.set_image(new Gtk.Image.from_icon_name("level-tree-symbolic", Gtk.IconSize.SMALL_TOOLBAR));
  601. } else {
  602. // Save the currently selected resource. This will be used later, after the tree
  603. // has been refiltered, to reveal the selected resource in the tree view.
  604. string? selected_type = null;
  605. string? selected_name = null;
  606. GLib.List<Gtk.TreePath> selected_paths = _icon_view.get_selected_items();
  607. if (selected_paths.length() == 1u) {
  608. Gtk.TreeIter selected_iter;
  609. if (_icon_view._list_store.get_iter(out selected_iter, selected_paths.nth(0).data)) {
  610. GLib.Value val;
  611. _icon_view._list_store.get_value(selected_iter, ProjectIconView.Column.TYPE, out val);
  612. selected_type = (string)val;
  613. _icon_view._list_store.get_value(selected_iter, ProjectIconView.Column.NAME, out val);
  614. selected_name = (string)val;
  615. }
  616. }
  617. _tree_filter.refilter();
  618. if (selected_type != null && selected_type != "<folder>") {
  619. reveal(selected_type, selected_name);
  620. }
  621. _icon_view_content.hide();
  622. _toggle_icon_view.set_image(new Gtk.Image.from_icon_name("browser-icon-view", Gtk.IconSize.SMALL_TOOLBAR));
  623. _tree_view.queue_draw(); // It doesn't draw by itself sometimes...
  624. }
  625. });
  626. // Create paned split-view.
  627. _scrolled_window_a = new Gtk.ScrolledWindow(null, null);
  628. _scrolled_window_a.add(_tree_view);
  629. _scrolled_window_b = new Gtk.ScrolledWindow(null, null);
  630. _scrolled_window_b.add(_icon_view);
  631. var _tree_view_control = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 0);
  632. _tree_view_control.pack_end(_toggle_icon_view, false, false);
  633. _tree_view_content = new Gtk.Box(Gtk.Orientation.VERTICAL, 0);
  634. _tree_view_content.pack_start(_tree_view_control, false);
  635. _tree_view_content.pack_start(_scrolled_window_a, true, true);
  636. var _icon_view_control = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 0);
  637. // _icon_view_control.pack_end(..., false, false);
  638. _icon_view_content = new Gtk.Box(Gtk.Orientation.VERTICAL, 0);
  639. _icon_view_content.pack_start(_icon_view_control, false);
  640. _icon_view_content.pack_start(_scrolled_window_b, true, true);
  641. _paned = new Gtk.Paned(Gtk.Orientation.VERTICAL);
  642. _paned.pack1(_tree_view_content, true, false);
  643. _paned.pack2(_icon_view_content, true, false);
  644. _paned.set_position(400);
  645. this.add(_paned);
  646. _hide_core_resources = true;
  647. // Actions.
  648. GLib.ActionEntry[] action_entries =
  649. {
  650. { "reveal-resource", on_reveal, "(ss)", null },
  651. { "open-directory", on_open_directory, "s", null },
  652. { "favorite-resource", on_favorite_resource, "(ss)", null },
  653. { "unfavorite-resource", on_unfavorite_resource, "(ss)", null }
  654. };
  655. GLib.Application.get_default().add_action_entries(action_entries, this);
  656. }
  657. private void on_drag_data_get(Gdk.DragContext context, Gtk.SelectionData data, uint info, uint time_)
  658. {
  659. // https://valadoc.org/gtk+-3.0/Gtk.Widget.drag_data_get.html
  660. Gtk.TreeModel selected_model;
  661. Gtk.TreeIter selected_iter;
  662. if (!_tree_selection.get_selected(out selected_model, out selected_iter))
  663. return;
  664. Value val;
  665. string type;
  666. string name;
  667. selected_model.get_value(selected_iter, ProjectStore.Column.TYPE, out val);
  668. type = (string)val;
  669. selected_model.get_value(selected_iter, ProjectStore.Column.NAME, out val);
  670. name = (string)val;
  671. string resource_path = ResourceId.path(type, name);
  672. data.set(Gdk.Atom.intern_static_string("RESOURCE_PATH"), 8, resource_path.data);
  673. }
  674. private void on_drag_begin(Gdk.DragContext context)
  675. {
  676. // https://valadoc.org/gtk+-3.0/Gtk.Widget.drag_begin.html
  677. Gtk.drag_set_icon_pixbuf(context, _empty_pixbuf, 0, 0);
  678. }
  679. private void on_drag_end(Gdk.DragContext context)
  680. {
  681. // https://valadoc.org/gtk+-3.0/Gtk.Widget.drag_end.html
  682. GLib.Application.get_default().activate_action("cancel-place", null);
  683. }
  684. // Returns true if the row should be hidden.
  685. private bool row_should_be_hidden(string type, string name)
  686. {
  687. return type == "<folder>" && name == "core" && _hide_core_resources
  688. || type == "importer_settings"
  689. || name == Project.LEVEL_EDITOR_TEST_NAME
  690. ;
  691. }
  692. private void reveal(string type, string name)
  693. {
  694. string parent_type = type;
  695. string parent_name = name;
  696. Gtk.TreePath filter_path = null;
  697. do {
  698. Gtk.TreePath store_path;
  699. if (!_project_store.path_for_resource_type_name(out store_path, parent_type, parent_name)) {
  700. break;
  701. }
  702. filter_path = _tree_filter.convert_child_path_to_path(store_path);
  703. if (filter_path == null) {
  704. // Either the path is not valid or points to a non-visible row in the model.
  705. parent_type = "<folder>";
  706. parent_name = ResourceId.parent_folder(parent_name);
  707. continue;
  708. }
  709. Gtk.TreePath sort_path = _tree_sort.convert_child_path_to_path(filter_path);
  710. if (sort_path == null) {
  711. // The path is not valid.
  712. break;
  713. }
  714. _tree_view.expand_to_path(sort_path);
  715. _tree_view.get_selection().select_path(sort_path);
  716. _tree_view.scroll_to_cell(sort_path, null, false, 0.0f, 0.0f);
  717. _icon_view.reveal(type, name);
  718. } while (filter_path == null);
  719. }
  720. private void on_reveal(GLib.SimpleAction action, GLib.Variant? param)
  721. {
  722. string type = (string)param.get_child_value(0);
  723. string name = (string)param.get_child_value(1);
  724. if (name.has_prefix("core/")) {
  725. _hide_core_resources = false;
  726. _tree_filter.refilter();
  727. }
  728. reveal(type, name);
  729. }
  730. private void on_open_directory(GLib.SimpleAction action, GLib.Variant? param)
  731. {
  732. string dir_name = param.get_string();
  733. Gtk.TreePath store_path;
  734. if (_project_store.path_for_resource_type_name(out store_path, "<folder>", dir_name)) {
  735. Gtk.TreePath filter_path = _tree_filter.convert_child_path_to_path(store_path);
  736. if (filter_path == null) // Either the path is not valid or points to a non-visible row in the model.
  737. return;
  738. Gtk.TreePath sort_path = _tree_sort.convert_child_path_to_path(filter_path);
  739. if (sort_path == null) // The path is not valid.
  740. return;
  741. _tree_view.expand_to_path(sort_path);
  742. _tree_view.get_selection().select_path(sort_path);
  743. }
  744. }
  745. private void on_favorite_resource(GLib.SimpleAction action, GLib.Variant? param)
  746. {
  747. string type = (string)param.get_child_value(0);
  748. string name = (string)param.get_child_value(1);
  749. _project_store.add_to_favorites(type, name);
  750. }
  751. private void on_unfavorite_resource(GLib.SimpleAction action, GLib.Variant? param)
  752. {
  753. string type = (string)param.get_child_value(0);
  754. string name = (string)param.get_child_value(1);
  755. _project_store.remove_from_favorites(type, name);
  756. }
  757. private bool on_button_pressed(Gdk.EventButton ev)
  758. {
  759. if (ev.button == Gdk.BUTTON_SECONDARY) {
  760. Gtk.TreePath path;
  761. if (_tree_view.get_path_at_pos((int)ev.x, (int)ev.y, out path, null, null, null)) {
  762. Gtk.TreeIter iter;
  763. _tree_view.model.get_iter(out iter, path);
  764. Value type;
  765. Value name;
  766. _tree_view.model.get_value(iter, ProjectStore.Column.TYPE, out type);
  767. _tree_view.model.get_value(iter, ProjectStore.Column.NAME, out name);
  768. Gtk.TreePath? filter_path = _tree_sort.convert_path_to_child_path(path);
  769. Gtk.TreePath? store_path = _tree_filter.convert_path_to_child_path(filter_path);
  770. Gtk.Menu? menu;
  771. if (store_path.is_descendant(_project_store.project_root_path()) || store_path.compare(_project_store.project_root_path()) == 0)
  772. menu = project_entry_menu_create((string)type, (string)name);
  773. else if (store_path.is_descendant(_project_store.favorites_root_path()))
  774. menu = favorites_entry_menu_create((string)type, (string)name);
  775. else
  776. menu = null;
  777. if (menu != null) {
  778. menu.show_all();
  779. menu.popup_at_pointer(ev);
  780. }
  781. }
  782. } else if (ev.button == Gdk.BUTTON_PRIMARY && ev.type == Gdk.EventType.@2BUTTON_PRESS) {
  783. Gtk.TreePath path;
  784. if (_tree_view.get_path_at_pos((int)ev.x, (int)ev.y, out path, null, null, null)) {
  785. Gtk.TreeIter iter;
  786. _tree_view.model.get_iter(out iter, path);
  787. Value type;
  788. _tree_view.model.get_value(iter, ProjectStore.Column.TYPE, out type);
  789. if ((string)type == "<folder>")
  790. return Gdk.EVENT_PROPAGATE;
  791. Value name;
  792. _tree_view.model.get_value(iter, ProjectStore.Column.NAME, out name);
  793. GLib.Application.get_default().activate_action("open-resource", ResourceId.path((string)type, (string)name));
  794. }
  795. }
  796. return Gdk.EVENT_PROPAGATE;
  797. }
  798. private void update_icon_view()
  799. {
  800. Gtk.TreeModel selected_model;
  801. Gtk.TreeIter selected_iter;
  802. if (!_tree_selection.get_selected(out selected_model, out selected_iter))
  803. return;
  804. // If there is a selected node.
  805. _icon_view._list_store.clear();
  806. // Get the selected node's type and name.
  807. string selected_type;
  808. string selected_name;
  809. Value val;
  810. selected_model.get_value(selected_iter, ProjectStore.Column.TYPE, out val);
  811. selected_type = (string)val;
  812. selected_model.get_value(selected_iter, ProjectStore.Column.NAME, out val);
  813. selected_name = (string)val;
  814. if (selected_type == "<folder>") {
  815. _icon_view._showing_project_folder = true;
  816. // Add parent folder.
  817. if (selected_name != "") {
  818. Gtk.TreeIter dummy;
  819. _icon_view._list_store.insert_with_values(out dummy
  820. , -1
  821. , ProjectIconView.Column.TYPE
  822. , "<folder>"
  823. , ProjectIconView.Column.NAME
  824. , ".."
  825. , -1
  826. );
  827. }
  828. // Fill the icon view list with paths matching the selected node's name.
  829. _project_store._list_store.foreach((model, path, iter) => {
  830. string type;
  831. string name;
  832. model.get_value(iter, ProjectStore.Column.TYPE, out val);
  833. type = (string)val;
  834. model.get_value(iter, ProjectStore.Column.NAME, out val);
  835. name = (string)val;
  836. if (row_should_be_hidden(type, name))
  837. return false;
  838. // Skip paths without common ancestor.
  839. if (!name.has_prefix(selected_name))
  840. return false;
  841. // Skip paths that are too deep in the hierarchy:
  842. // selected_name: foo
  843. // hierarchy:
  844. // foo/bar OK
  845. // foo/baz OK
  846. // foo/bar/baz NOPE
  847. string name_suffix;
  848. if (selected_name == "") // Project folder.
  849. name_suffix = name.substring((selected_name).length);
  850. else if (selected_name != name) // Folder itself.
  851. name_suffix = name.substring((selected_name).length + 1);
  852. else
  853. return false;
  854. if (name_suffix.index_of_char('/') != -1)
  855. return false;
  856. // Add the path to the list.
  857. Gtk.TreeIter dummy;
  858. _icon_view._list_store.insert_with_values(out dummy
  859. , -1
  860. , ProjectIconView.Column.TYPE
  861. , type
  862. , ProjectIconView.Column.NAME
  863. , name
  864. , -1
  865. );
  866. return false;
  867. });
  868. _icon_view._selected_type = selected_type;
  869. _icon_view._selected_name = selected_name;
  870. } else if (selected_type == "<favorites>") {
  871. _icon_view._showing_project_folder = false;
  872. // Fill the icon view list with paths whose ancestor is the favorites root.
  873. _project_store._tree_store.foreach((model, path, iter) => {
  874. string type;
  875. string name;
  876. model.get_value(iter, ProjectStore.Column.TYPE, out val);
  877. type = (string)val;
  878. model.get_value(iter, ProjectStore.Column.NAME, out val);
  879. name = (string)val;
  880. if (!path.is_descendant(_project_store.favorites_root_path()))
  881. return false;
  882. // Add the path to the list.
  883. Gtk.TreeIter dummy;
  884. _icon_view._list_store.insert_with_values(out dummy
  885. , -1
  886. , ProjectIconView.Column.TYPE
  887. , type
  888. , ProjectIconView.Column.NAME
  889. , name
  890. , -1
  891. );
  892. return false;
  893. });
  894. }
  895. }
  896. public void select_project_root()
  897. {
  898. Gtk.TreePath? filter_path = _tree_filter.convert_child_path_to_path(_project_store.project_root_path());
  899. if (filter_path == null)
  900. return;
  901. Gtk.TreePath? sort_path = _tree_sort.convert_child_path_to_path(filter_path);
  902. if (sort_path == null)
  903. return;
  904. _tree_selection.select_path(sort_path);
  905. }
  906. private void pixbuf_func(Gtk.CellLayout cell_layout, Gtk.CellRenderer cell, Gtk.TreeModel model, Gtk.TreeIter iter)
  907. {
  908. Value val;
  909. string type;
  910. string name;
  911. model.get_value(iter, ProjectStore.Column.TYPE, out val);
  912. type = (string)val;
  913. model.get_value(iter, ProjectStore.Column.NAME, out val);
  914. name = (string)val;
  915. // https://specifications.freedesktop.org/icon-naming-spec/icon-naming-spec-latest.html
  916. if ((string)type == "<folder>")
  917. cell.set_property("icon-name", "browser-folder-symbolic");
  918. else if ((string)type == "<favorites>")
  919. cell.set_property("icon-name", "browser-favorites");
  920. else if ((string)type == "state_machine")
  921. cell.set_property("icon-name", "text-x-generic-symbolic");
  922. else if ((string)type == "config")
  923. cell.set_property("icon-name", "text-x-generic-symbolic");
  924. else if ((string)type == "font")
  925. cell.set_property("icon-name", "font-x-generic-symbolic");
  926. else if ((string)type == "unit")
  927. cell.set_property("icon-name", "level-object-unit");
  928. else if ((string)type == "level")
  929. cell.set_property("icon-name", "text-x-generic-symbolic");
  930. else if ((string)type == "material")
  931. cell.set_property("icon-name", "text-x-generic-symbolic");
  932. else if ((string)type == "mesh")
  933. cell.set_property("icon-name", "text-x-generic-symbolic");
  934. else if ((string)type == "package")
  935. cell.set_property("icon-name", "package-x-generic-symbolic");
  936. else if ((string)type == "physics_config")
  937. cell.set_property("icon-name", "text-x-generic-symbolic");
  938. else if ((string)type == "lua")
  939. cell.set_property("icon-name", "x-office-document-symbolic");
  940. else if ((string)type == "shader")
  941. cell.set_property("icon-name", "text-x-generic-symbolic");
  942. else if ((string)type == "sound")
  943. cell.set_property("icon-name", "audio-x-generic-symbolic");
  944. else if ((string)type == "sprite_animation")
  945. cell.set_property("icon-name", "text-x-generic-symbolic");
  946. else if ((string)type == "sprite")
  947. cell.set_property("icon-name", "text-x-generic-symbolic");
  948. else if ((string)type == "texture")
  949. cell.set_property("icon-name", "image-x-generic-symbolic");
  950. else
  951. cell.set_property("icon-name", "text-x-generic-symbolic");
  952. }
  953. private void text_func(Gtk.CellLayout cell_layout, Gtk.CellRenderer cell, Gtk.TreeModel model, Gtk.TreeIter iter)
  954. {
  955. Value name;
  956. Value type;
  957. model.get_value(iter, ProjectStore.Column.NAME, out name);
  958. model.get_value(iter, ProjectStore.Column.TYPE, out type);
  959. string basename = GLib.Path.get_basename((string)name);
  960. if ((string)type == "<folder>") {
  961. if ((string)name == "")
  962. cell.set_property("text", _project_store._project.name());
  963. else
  964. cell.set_property("text", basename);
  965. } else if ((string)type == "<favorites>") {
  966. cell.set_property("text", "Favorites");
  967. } else {
  968. cell.set_property("text", ResourceId.path((string)type, basename));
  969. }
  970. }
  971. }
  972. } /* namespace Crown */