project_browser.vala 52 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707
  1. /*
  2. * Copyright (c) 2012-2025 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. private string project_path(string type, string name)
  14. {
  15. if (type == "<folder>")
  16. return name;
  17. return ResourceId.path(type, name);
  18. }
  19. // Menu to open when clicking on project's files and folders.
  20. private Gtk.Menu? project_entry_menu_create(string type, string name)
  21. {
  22. Gtk.Menu? menu;
  23. if (type == "<folder>") {
  24. if (name == "..")
  25. return null;
  26. menu = new Gtk.Menu();
  27. Gtk.MenuItem mi;
  28. mi = new Gtk.MenuItem.with_label("Import...");
  29. mi.activate.connect(() => {
  30. GLib.Application.get_default().activate_action("import", new GLib.Variant.string((string)name));
  31. });
  32. menu.add(mi);
  33. mi = new Gtk.SeparatorMenuItem();
  34. menu.add(mi);
  35. mi = new Gtk.MenuItem.with_label("New Script...");
  36. mi.activate.connect(() => {
  37. Gtk.Dialog dg = new Gtk.Dialog.with_buttons("Script Name"
  38. , ((Gtk.Application)GLib.Application.get_default()).active_window
  39. , DialogFlags.MODAL
  40. , "Cancel"
  41. , ResponseType.CANCEL
  42. , "Ok"
  43. , ResponseType.OK
  44. , null
  45. );
  46. EntryText sb = new EntryText();
  47. sb.activate.connect(() => { dg.response(ResponseType.OK); });
  48. dg.get_content_area().add(sb);
  49. dg.skip_taskbar_hint = true;
  50. dg.show_all();
  51. if (dg.run() == (int)ResponseType.OK) {
  52. if (sb.text.strip() == "") {
  53. dg.destroy();
  54. return;
  55. }
  56. var tuple = new GLib.Variant.tuple({(string)name, sb.text, true});
  57. GLib.Application.get_default().activate_action("create-script", tuple);
  58. }
  59. dg.destroy();
  60. });
  61. menu.add(mi);
  62. mi = new Gtk.MenuItem.with_label("New Script (Unit)...");
  63. mi.activate.connect(() => {
  64. Gtk.Dialog dg = new Gtk.Dialog.with_buttons("Script Name"
  65. , ((Gtk.Application)GLib.Application.get_default()).active_window
  66. , DialogFlags.MODAL
  67. , "Cancel"
  68. , ResponseType.CANCEL
  69. , "Ok"
  70. , ResponseType.OK
  71. , null
  72. );
  73. EntryText sb = new EntryText();
  74. sb.activate.connect(() => { dg.response(ResponseType.OK); });
  75. dg.get_content_area().add(sb);
  76. dg.skip_taskbar_hint = true;
  77. dg.show_all();
  78. if (dg.run() == (int)ResponseType.OK) {
  79. if (sb.text.strip() == "") {
  80. dg.destroy();
  81. return;
  82. }
  83. var tuple = new GLib.Variant.tuple({(string)name, sb.text, false});
  84. GLib.Application.get_default().activate_action("create-script", tuple);
  85. }
  86. dg.destroy();
  87. });
  88. menu.add(mi);
  89. mi = new Gtk.SeparatorMenuItem();
  90. menu.add(mi);
  91. mi = new Gtk.MenuItem.with_label("New Unit...");
  92. mi.activate.connect(() => {
  93. Gtk.Dialog dg = new Gtk.Dialog.with_buttons("Unit Name"
  94. , ((Gtk.Application)GLib.Application.get_default()).active_window
  95. , DialogFlags.MODAL
  96. , "Cancel"
  97. , ResponseType.CANCEL
  98. , "Ok"
  99. , ResponseType.OK
  100. , null
  101. );
  102. EntryText sb = new EntryText();
  103. sb.activate.connect(() => { dg.response(ResponseType.OK); });
  104. dg.get_content_area().add(sb);
  105. dg.skip_taskbar_hint = true;
  106. dg.show_all();
  107. if (dg.run() == (int)ResponseType.OK) {
  108. if (sb.text.strip() == "") {
  109. dg.destroy();
  110. return;
  111. }
  112. var tuple = new GLib.Variant.tuple({(string)name, sb.text});
  113. GLib.Application.get_default().activate_action("create-unit", tuple);
  114. }
  115. dg.destroy();
  116. });
  117. menu.add(mi);
  118. mi = new Gtk.MenuItem.with_label("New Material...");
  119. mi.activate.connect(() => {
  120. Gtk.Dialog dg = new Gtk.Dialog.with_buttons("Material Name"
  121. , ((Gtk.Application)GLib.Application.get_default()).active_window
  122. , DialogFlags.MODAL
  123. , "Cancel"
  124. , ResponseType.CANCEL
  125. , "Ok"
  126. , ResponseType.OK
  127. , null
  128. );
  129. EntryText sb = new EntryText();
  130. sb.activate.connect(() => { dg.response(ResponseType.OK); });
  131. dg.get_content_area().add(sb);
  132. dg.skip_taskbar_hint = true;
  133. dg.show_all();
  134. if (dg.run() == (int)ResponseType.OK) {
  135. if (sb.text.strip() == "") {
  136. dg.destroy();
  137. return;
  138. }
  139. var tuple = new GLib.Variant.tuple({(string)name, sb.text});
  140. GLib.Application.get_default().activate_action("create-material", tuple);
  141. }
  142. dg.destroy();
  143. });
  144. menu.add(mi);
  145. mi = new Gtk.SeparatorMenuItem();
  146. menu.add(mi);
  147. mi = new Gtk.MenuItem.with_label("New Folder...");
  148. mi.activate.connect(() => {
  149. Gtk.Dialog dg = new Gtk.Dialog.with_buttons("Folder Name"
  150. , ((Gtk.Application)GLib.Application.get_default()).active_window
  151. , DialogFlags.MODAL
  152. , "Cancel"
  153. , ResponseType.CANCEL
  154. , "Ok"
  155. , ResponseType.OK
  156. , null
  157. );
  158. EntryText sb = new EntryText();
  159. sb.activate.connect(() => { dg.response(ResponseType.OK); });
  160. dg.get_content_area().add(sb);
  161. dg.skip_taskbar_hint = true;
  162. dg.show_all();
  163. if (dg.run() == (int)ResponseType.OK) {
  164. if (sb.text.strip() == "") {
  165. dg.destroy();
  166. return;
  167. }
  168. var tuple = new GLib.Variant.tuple({(string)name, sb.text});
  169. GLib.Application.get_default().activate_action("create-directory", tuple);
  170. }
  171. dg.destroy();
  172. });
  173. menu.add(mi);
  174. if ((string)name != ProjectStore.ROOT_FOLDER) {
  175. mi = new Gtk.MenuItem.with_label("Delete Folder");
  176. mi.activate.connect(() => {
  177. Gtk.MessageDialog md = new Gtk.MessageDialog(((Gtk.Application)GLib.Application.get_default()).active_window
  178. , Gtk.DialogFlags.MODAL
  179. , Gtk.MessageType.WARNING
  180. , Gtk.ButtonsType.NONE
  181. , "Delete Folder " + (string)name + "?"
  182. );
  183. Gtk.Widget btn;
  184. md.add_button("_Cancel", ResponseType.CANCEL);
  185. btn = md.add_button("_Delete", ResponseType.YES);
  186. btn.get_style_context().add_class(Gtk.STYLE_CLASS_DESTRUCTIVE_ACTION);
  187. md.set_default_response(ResponseType.CANCEL);
  188. int rt = md.run();
  189. md.destroy();
  190. if (rt != (int)ResponseType.YES)
  191. return;
  192. GLib.Application.get_default().activate_action("delete-directory", new GLib.Variant.string((string)name));
  193. });
  194. menu.add(mi);
  195. }
  196. } else { // If file
  197. menu = new Gtk.Menu();
  198. Gtk.MenuItem mi;
  199. mi = new Gtk.MenuItem.with_label("Delete File");
  200. mi.activate.connect(() => {
  201. string path = project_path(type, name);
  202. GLib.Application.get_default().activate_action("delete-file", new GLib.Variant.string(path));
  203. });
  204. menu.add(mi);
  205. if (type == OBJECT_TYPE_MESH_SKELETON || type == OBJECT_TYPE_SPRITE) {
  206. mi = new Gtk.MenuItem.with_label("New State Machine...");
  207. mi.activate.connect(() => {
  208. Gtk.Dialog dg = new Gtk.Dialog.with_buttons("State Machine Name"
  209. , ((Gtk.Application)GLib.Application.get_default()).active_window
  210. , DialogFlags.MODAL
  211. , "Cancel"
  212. , ResponseType.CANCEL
  213. , "Ok"
  214. , ResponseType.OK
  215. , null
  216. );
  217. EntryText sb = new EntryText();
  218. sb.value = GLib.Path.get_basename((string)name);
  219. sb.activate.connect(() => { dg.response(ResponseType.OK); });
  220. dg.get_content_area().add(sb);
  221. dg.skip_taskbar_hint = true;
  222. dg.show_all();
  223. if (dg.run() == (int)ResponseType.OK) {
  224. if (sb.text.strip() == "") {
  225. dg.destroy();
  226. return;
  227. }
  228. string skeleton_name;
  229. if (type == OBJECT_TYPE_SPRITE)
  230. skeleton_name = "";
  231. else
  232. skeleton_name = name;
  233. var tuple = new GLib.Variant.tuple({ResourceId.parent_folder(name), sb.text, skeleton_name});
  234. GLib.Application.get_default().activate_action("create-state-machine", tuple);
  235. }
  236. dg.destroy();
  237. });
  238. menu.add(mi);
  239. }
  240. }
  241. // Add shared menu items.
  242. Gtk.MenuItem mi;
  243. mi = new Gtk.SeparatorMenuItem();
  244. menu.add(mi);
  245. mi = new Gtk.MenuItem.with_label("Copy Path");
  246. mi.activate.connect(() => {
  247. string path = project_path(type, name);
  248. GLib.Application.get_default().activate_action("copy-path", new GLib.Variant.string(path));
  249. });
  250. menu.add(mi);
  251. mi = new Gtk.MenuItem.with_label("Copy Name");
  252. mi.activate.connect(() => {
  253. GLib.Application.get_default().activate_action("copy-name", new GLib.Variant.string(name));
  254. });
  255. menu.add(mi);
  256. mi = new Gtk.MenuItem.with_label("Open Containing Folder...");
  257. mi.activate.connect(() => {
  258. GLib.Application.get_default().activate_action("open-containing", new GLib.Variant.string(name));
  259. });
  260. menu.add(mi);
  261. if (type != "<folder>" || name != "") {
  262. mi = new Gtk.MenuItem.with_label("Add to Favorites");
  263. mi.activate.connect(() => {
  264. var tuple = new GLib.Variant.tuple({type, name});
  265. GLib.Application.get_default().activate_action("favorite-resource", tuple);
  266. });
  267. menu.add(mi);
  268. }
  269. return menu;
  270. }
  271. // Menu to open when clicking on favorites' entries.
  272. private Gtk.Menu? favorites_entry_menu_create(string type, string name)
  273. {
  274. Gtk.Menu? menu;
  275. menu = new Gtk.Menu();
  276. Gtk.MenuItem mi;
  277. mi = new Gtk.MenuItem.with_label("Open Containing Folder...");
  278. mi.activate.connect(() => {
  279. GLib.Application.get_default().activate_action("open-containing", new GLib.Variant.string(name));
  280. });
  281. menu.add(mi);
  282. mi = new Gtk.SeparatorMenuItem();
  283. menu.add(mi);
  284. mi = new Gtk.MenuItem.with_label("Copy Path");
  285. mi.activate.connect(() => {
  286. string path = project_path(type, name);
  287. GLib.Application.get_default().activate_action("copy-path", new GLib.Variant.string(path));
  288. });
  289. menu.add(mi);
  290. mi = new Gtk.MenuItem.with_label("Copy Name");
  291. mi.activate.connect(() => {
  292. GLib.Application.get_default().activate_action("copy-name", new GLib.Variant.string(name));
  293. });
  294. menu.add(mi);
  295. mi = new Gtk.MenuItem.with_label("Remove from Favorites");
  296. mi.activate.connect(() => {
  297. var tuple = new GLib.Variant.tuple({type, name});
  298. GLib.Application.get_default().activate_action("unfavorite-resource", tuple);
  299. });
  300. menu.add(mi);
  301. mi = new Gtk.MenuItem.with_label("Reveal");
  302. mi.activate.connect(() => {
  303. var tuple = new GLib.Variant.tuple({type, name});
  304. GLib.Application.get_default().activate_action("reveal-resource", tuple);
  305. });
  306. menu.add(mi);
  307. return menu;
  308. }
  309. private void set_thumbnail(Gtk.CellRenderer cell, string type, string name, int icon_size, ThumbnailCache thumbnail_cache)
  310. {
  311. // https://specifications.freedesktop.org/icon-naming-spec/icon-naming-spec-latest.html
  312. if (type == "<folder>")
  313. cell.set_property("icon-name", "browser-folder-symbolic");
  314. else if ((string)type == "<favorites>")
  315. cell.set_property("icon-name", "browser-favorites");
  316. else if ((string)type == "state_machine")
  317. cell.set_property("icon-name", "text-x-generic-symbolic");
  318. else if ((string)type == "config")
  319. cell.set_property("icon-name", "text-x-generic-symbolic");
  320. else if ((string)type == "font")
  321. cell.set_property("icon-name", "font-x-generic-symbolic");
  322. else if ((string)type == "level")
  323. cell.set_property("icon-name", "text-x-generic-symbolic");
  324. else if ((string)type == "material")
  325. cell.set_property("pixbuf", thumbnail_cache.get(type, name, icon_size));
  326. else if ((string)type == "mesh")
  327. cell.set_property("icon-name", "text-x-generic-symbolic");
  328. else if ((string)type == "package")
  329. cell.set_property("icon-name", "package-x-generic-symbolic");
  330. else if ((string)type == "physics_config")
  331. cell.set_property("icon-name", "text-x-generic-symbolic");
  332. else if ((string)type == "lua")
  333. cell.set_property("icon-name", "x-office-document-symbolic");
  334. else if ((string)type == "unit")
  335. cell.set_property("pixbuf", thumbnail_cache.get(type, name, icon_size));
  336. else if ((string)type == "shader")
  337. cell.set_property("icon-name", "text-x-generic-symbolic");
  338. else if ((string)type == "sound")
  339. cell.set_property("pixbuf", thumbnail_cache.get(type, name, icon_size));
  340. else if ((string)type == "sprite_animation")
  341. cell.set_property("icon-name", "text-x-generic-symbolic");
  342. else if ((string)type == "sprite")
  343. cell.set_property("icon-name", "text-x-generic-symbolic");
  344. else if ((string)type == "texture")
  345. cell.set_property("pixbuf", thumbnail_cache.get(type, name, icon_size));
  346. else
  347. cell.set_property("icon-name", "text-x-generic-symbolic");
  348. }
  349. public class ProjectFolderView : Gtk.Bin
  350. {
  351. private static string prettify_type(string type)
  352. {
  353. if (type == "<folder>")
  354. return "Folder";
  355. else
  356. return type;
  357. }
  358. private static string prettify_size(uint64 size)
  359. {
  360. uint64 si_size;
  361. string si_unit;
  362. if (size >= 1024*1024*1024) {
  363. si_size = size / (1024*1024*1024);
  364. si_unit = "GiB";
  365. } else if (size >= 1024*1024) {
  366. si_size = size / (1024*1024);
  367. si_unit = "MiB";
  368. } else if (size >= 1024) {
  369. si_size = size / 1024;
  370. si_unit = "KiB";
  371. } else {
  372. si_size = size;
  373. si_unit = size > 1 ? "bytes" : "byte";
  374. }
  375. return "%d %s".printf((int)si_size, si_unit);
  376. }
  377. private static string prettify_time(uint64 time)
  378. {
  379. int64 mtime_secs = (int64)(time / (1000*1000*1000));
  380. GLib.DateTime date_time = new GLib.DateTime.from_unix_local(mtime_secs);
  381. return date_time.format("%d %b %Y; %H:%M:%S");
  382. }
  383. public enum Column
  384. {
  385. TYPE,
  386. NAME,
  387. PIXBUF,
  388. SIZE,
  389. MTIME,
  390. COUNT
  391. }
  392. public string _selected_type;
  393. public string _selected_name;
  394. public ProjectStore _project_store;
  395. public ThumbnailCache _thumbnail_cache;
  396. public Gtk.ListStore _list_store;
  397. public Gtk.IconView _icon_view;
  398. public Gtk.TreeView _list_view;
  399. public Gtk.CellRendererPixbuf _cell_renderer_pixbuf;
  400. public Gtk.CellRendererText _cell_renderer_text;
  401. public Gdk.Pixbuf _empty_pixbuf;
  402. public bool _showing_project_folder;
  403. public Gtk.ScrolledWindow _icon_view_window;
  404. public Gtk.ScrolledWindow _list_view_window;
  405. public Gtk.Stack _stack;
  406. public ProjectFolderView(ProjectStore project_store, ThumbnailCache thumbnail_cache)
  407. {
  408. _project_store = project_store;
  409. _thumbnail_cache = thumbnail_cache;
  410. _list_store = new Gtk.ListStore(Column.COUNT
  411. , typeof(string) // Column.TYPE
  412. , typeof(string) // Column.NAME
  413. , typeof(Gdk.Pixbuf) // Column.PIXBUF
  414. , typeof(uint64) // Column.SIZE
  415. , typeof(uint64) // Column.MTIME
  416. );
  417. _icon_view = new Gtk.IconView();
  418. _icon_view.set_model(_list_store);
  419. _icon_view.set_item_width(80);
  420. _icon_view.button_press_event.connect(on_button_pressed);
  421. _icon_view.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, dnd_targets, Gdk.DragAction.COPY);
  422. _icon_view.drag_data_get.connect(on_drag_data_get);
  423. _icon_view.drag_begin.connect_after(on_drag_begin);
  424. _icon_view.drag_end.connect(on_drag_end);
  425. _icon_view.has_tooltip = true;
  426. _icon_view.query_tooltip.connect(on_icon_view_query_tooltip);
  427. // https://gitlab.gnome.org/GNOME/gtk/-/blob/3.24.43/gtk/gtkiconview.c#L5147
  428. _cell_renderer_text = new Gtk.CellRendererText();
  429. _cell_renderer_text.set("wrap-mode", Pango.WrapMode.WORD_CHAR);
  430. _cell_renderer_text.set("alignment", Pango.Alignment.CENTER);
  431. _cell_renderer_text.set("xalign", 0.5);
  432. _cell_renderer_text.set("yalign", 0.0);
  433. int wrap_width = _icon_view.item_width;
  434. wrap_width -= 2 * _icon_view.item_padding * 2;
  435. _cell_renderer_text.set("wrap-width", wrap_width);
  436. _cell_renderer_text.set("width", wrap_width);
  437. _icon_view.pack_end(_cell_renderer_text, false);
  438. _icon_view.set_cell_data_func(_cell_renderer_text, icon_view_text_func);
  439. _cell_renderer_pixbuf = new Gtk.CellRendererPixbuf();
  440. _cell_renderer_pixbuf.stock_size = Gtk.IconSize.DIALOG;
  441. _icon_view.pack_start(_cell_renderer_pixbuf, false);
  442. _icon_view.set_cell_data_func(_cell_renderer_pixbuf, icon_view_pixbuf_func);
  443. _list_view = new Gtk.TreeView();
  444. _list_view.set_model(_list_store);
  445. _list_view.button_press_event.connect(on_button_pressed);
  446. _list_view.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, dnd_targets, Gdk.DragAction.COPY);
  447. _list_view.drag_data_get.connect(on_drag_data_get);
  448. _list_view.drag_begin.connect_after(on_drag_begin);
  449. _list_view.drag_end.connect(on_drag_end);
  450. var cell_pixbuf = new Gtk.CellRendererPixbuf();
  451. cell_pixbuf.stock_size = Gtk.IconSize.DND;
  452. var cell_text = new Gtk.CellRendererText();
  453. Gtk.TreeViewColumn column = null;
  454. column = new Gtk.TreeViewColumn();
  455. // column.title = "Thumbnail";
  456. column.pack_start(cell_pixbuf, false);
  457. column.set_cell_data_func(cell_pixbuf, list_view_pixbuf_func);
  458. _list_view.append_column(column);
  459. column = new Gtk.TreeViewColumn();
  460. column.title = "Basename";
  461. column.pack_start(cell_text, true);
  462. column.set_cell_data_func(cell_text, list_view_basename_text_func);
  463. _list_view.append_column(column);
  464. column = new Gtk.TreeViewColumn();
  465. column.title = "Type";
  466. column.pack_start(cell_text, true);
  467. column.set_cell_data_func(cell_text, list_view_type_text_func);
  468. _list_view.append_column(column);
  469. column = new Gtk.TreeViewColumn();
  470. column.title = "Size";
  471. column.pack_start(cell_text, true);
  472. column.set_cell_data_func(cell_text, list_view_size_text_func);
  473. _list_view.append_column(column);
  474. column = new Gtk.TreeViewColumn();
  475. column.title = "Modified";
  476. column.pack_start(cell_text, true);
  477. column.set_cell_data_func(cell_text, list_view_mtime_text_func);
  478. _list_view.append_column(column);
  479. column = new Gtk.TreeViewColumn();
  480. column.title = "Name";
  481. column.pack_start(cell_text, true);
  482. column.set_cell_data_func(cell_text, list_view_name_text_func);
  483. _list_view.append_column(column);
  484. _empty_pixbuf = new Gdk.Pixbuf.from_data({ 0x00, 0x00, 0x00, 0x00 }, Gdk.Colorspace.RGB, true, 8, 1, 1, 4);
  485. _showing_project_folder = true;
  486. _icon_view_window = new Gtk.ScrolledWindow(null, null);
  487. _icon_view_window.add(_icon_view);
  488. _list_view_window = new Gtk.ScrolledWindow(null, null);
  489. _list_view_window.add(_list_view);
  490. _stack = new Gtk.Stack();
  491. _stack.add_named(_icon_view_window, "icon-view");
  492. _stack.add_named(_list_view_window, "list-view");
  493. _stack.set_visible_child_full("icon-view", Gtk.StackTransitionType.NONE);
  494. this.add(_stack);
  495. }
  496. private void on_drag_data_get(Gdk.DragContext context, Gtk.SelectionData data, uint info, uint time_)
  497. {
  498. // https://valadoc.org/gtk+-3.0/Gtk.Widget.drag_data_get.html
  499. Gtk.TreePath path;
  500. if (!selected_path(out path))
  501. return;
  502. Gtk.TreeIter iter;
  503. _list_store.get_iter(out iter, path);
  504. Value val;
  505. string type;
  506. string name;
  507. _list_store.get_value(iter, Column.TYPE, out val);
  508. type = (string)val;
  509. _list_store.get_value(iter, Column.NAME, out val);
  510. name = (string)val;
  511. string resource_path = ResourceId.path(type, name);
  512. data.set(Gdk.Atom.intern_static_string("RESOURCE_PATH"), 8, resource_path.data);
  513. }
  514. private void on_drag_begin(Gdk.DragContext context)
  515. {
  516. // https://valadoc.org/gtk+-3.0/Gtk.Widget.drag_begin.html
  517. Gtk.drag_set_icon_pixbuf(context, _empty_pixbuf, 0, 0);
  518. }
  519. private void on_drag_end(Gdk.DragContext context)
  520. {
  521. // https://valadoc.org/gtk+-3.0/Gtk.Widget.drag_end.html
  522. GLib.Application.get_default().activate_action("cancel-place", null);
  523. }
  524. private bool on_button_pressed(Gdk.EventButton ev)
  525. {
  526. Gtk.TreePath? path;
  527. if (_stack.get_visible_child() == _icon_view_window) {
  528. path = _icon_view.get_path_at_pos((int)ev.x, (int)ev.y);
  529. } else if (_stack.get_visible_child() == _list_view_window) {
  530. if (!_list_view.get_path_at_pos((int)ev.x, (int)ev.y, out path, null, null, null))
  531. path = null;
  532. } else {
  533. assert(false);
  534. return Gdk.EVENT_PROPAGATE;
  535. }
  536. if (ev.button == Gdk.BUTTON_SECONDARY) {
  537. string type;
  538. string name;
  539. if (path != null) {
  540. _icon_view.select_path(path);
  541. _icon_view.scroll_to_path(path, false, 0.0f, 0.0f);
  542. Gtk.TreeIter iter;
  543. _list_store.get_iter(out iter, path);
  544. Value val;
  545. _list_store.get_value(iter, Column.TYPE, out val);
  546. type = (string)val;
  547. _list_store.get_value(iter, Column.NAME, out val);
  548. name = (string)val;
  549. } else {
  550. type = _selected_type;
  551. name = _selected_name;
  552. }
  553. Gtk.Menu? menu;
  554. menu = project_entry_menu_create(type, name);
  555. if (_showing_project_folder)
  556. menu = project_entry_menu_create(type, name);
  557. else
  558. menu = favorites_entry_menu_create(type, name);
  559. if (menu != null) {
  560. menu.show_all();
  561. menu.popup_at_pointer(ev);
  562. }
  563. } else if (ev.button == Gdk.BUTTON_PRIMARY && ev.type == Gdk.EventType.@2BUTTON_PRESS) {
  564. if (path != null) {
  565. Gtk.TreeIter iter;
  566. _list_store.get_iter(out iter, path);
  567. Value type;
  568. Value name;
  569. _list_store.get_value(iter, Column.TYPE, out type);
  570. _list_store.get_value(iter, Column.NAME, out name);
  571. if ((string)type == "<folder>") {
  572. string dir_name;
  573. if ((string)name == "..")
  574. dir_name = ResourceId.parent_folder((string)_selected_name);
  575. else
  576. dir_name = (string)name;
  577. GLib.Application.get_default().activate_action("open-directory", new GLib.Variant.string(dir_name));
  578. } else {
  579. GLib.Application.get_default().activate_action("open-resource", ResourceId.path((string)type, (string)name));
  580. }
  581. }
  582. }
  583. return Gdk.EVENT_PROPAGATE;
  584. }
  585. private void icon_view_pixbuf_func(Gtk.CellLayout cell_layout, Gtk.CellRenderer cell, Gtk.TreeModel model, Gtk.TreeIter iter)
  586. {
  587. Value val;
  588. string type;
  589. string name;
  590. model.get_value(iter, Column.TYPE, out val);
  591. type = (string)val;
  592. model.get_value(iter, Column.NAME, out val);
  593. name = (string)val;
  594. set_thumbnail(cell, type, name, 64, _thumbnail_cache);
  595. }
  596. private void icon_view_text_func(Gtk.CellLayout cell_layout, Gtk.CellRenderer cell, Gtk.TreeModel model, Gtk.TreeIter iter)
  597. {
  598. Value type;
  599. Value name;
  600. model.get_value(iter, Column.TYPE, out type);
  601. model.get_value(iter, Column.NAME, out name);
  602. if (name == "..")
  603. cell.set_property("text", name);
  604. else
  605. cell.set_property("text", GLib.Path.get_basename((string)name));
  606. }
  607. private void list_view_pixbuf_func(Gtk.CellLayout cell_layout, Gtk.CellRenderer cell, Gtk.TreeModel model, Gtk.TreeIter iter)
  608. {
  609. Value val;
  610. string type;
  611. string name;
  612. model.get_value(iter, Column.TYPE, out val);
  613. type = (string)val;
  614. model.get_value(iter, Column.NAME, out val);
  615. name = (string)val;
  616. set_thumbnail(cell, type, name, 32, _thumbnail_cache);
  617. }
  618. private void list_view_basename_text_func(Gtk.CellLayout cell_layout, Gtk.CellRenderer cell, Gtk.TreeModel model, Gtk.TreeIter iter)
  619. {
  620. Value name;
  621. model.get_value(iter, Column.NAME, out name);
  622. if (name == "..")
  623. cell.set_property("text", name);
  624. else
  625. cell.set_property("text", GLib.Path.get_basename((string)name));
  626. }
  627. private void list_view_type_text_func(Gtk.CellLayout cell_layout, Gtk.CellRenderer cell, Gtk.TreeModel model, Gtk.TreeIter iter)
  628. {
  629. Value type;
  630. model.get_value(iter, Column.TYPE, out type);
  631. cell.set_property("text", prettify_type((string)type));
  632. }
  633. private void list_view_size_text_func(Gtk.CellLayout cell_layout, Gtk.CellRenderer cell, Gtk.TreeModel model, Gtk.TreeIter iter)
  634. {
  635. Value val;
  636. model.get_value(iter, Column.SIZE, out val);
  637. uint64 size = (uint64)val;
  638. if (size != 0)
  639. cell.set_property("text", prettify_size(size));
  640. else
  641. cell.set_property("text", "n/a");
  642. }
  643. private void list_view_mtime_text_func(Gtk.CellLayout cell_layout, Gtk.CellRenderer cell, Gtk.TreeModel model, Gtk.TreeIter iter)
  644. {
  645. Value type;
  646. model.get_value(iter, Column.MTIME, out type);
  647. uint64 mtime = (uint64)type;
  648. if (mtime != 0)
  649. cell.set_property("text", prettify_time(mtime));
  650. else
  651. cell.set_property("text", "n/a");
  652. }
  653. private void list_view_name_text_func(Gtk.CellLayout cell_layout, Gtk.CellRenderer cell, Gtk.TreeModel model, Gtk.TreeIter iter)
  654. {
  655. Value name;
  656. model.get_value(iter, Column.NAME, out name);
  657. if (name == "..")
  658. cell.set_property("text", "n/a");
  659. else
  660. cell.set_property("text", (string)name);
  661. }
  662. public void reveal(string type, string name)
  663. {
  664. _list_store.foreach((model, path, iter) => {
  665. GLib.Value val;
  666. string store_type;
  667. string store_name;
  668. model.get_value(iter, Column.TYPE, out val);
  669. store_type = (string)val;
  670. model.get_value(iter, Column.NAME, out val);
  671. store_name = (string)val;
  672. if (store_name == name && store_type == type) {
  673. _icon_view.select_path(path);
  674. _icon_view.scroll_to_path(path, false, 0.0f, 0.0f);
  675. _list_view.get_selection().select_path(path);
  676. _list_view.scroll_to_cell(path, null, false, 0.0f, 0.0f);
  677. return true;
  678. }
  679. return false;
  680. });
  681. }
  682. public bool selected_path(out Gtk.TreePath? path)
  683. {
  684. if (_stack.get_visible_child() == _icon_view_window) {
  685. GLib.List<Gtk.TreePath> selected_paths = _icon_view.get_selected_items();
  686. if (selected_paths.length() == 0u) {
  687. path = null;
  688. return false;
  689. }
  690. path = selected_paths.nth(0).data;
  691. return true;
  692. } else if (_stack.get_visible_child() == _list_view_window) {
  693. Gtk.TreeModel selected_model;
  694. Gtk.TreeIter iter;
  695. if (!_list_view.get_selection().get_selected(out selected_model, out iter)) {
  696. path = null;
  697. return false;
  698. }
  699. path = selected_model.get_path(iter);
  700. return true;
  701. } else {
  702. path = null;
  703. return false;
  704. }
  705. }
  706. private bool on_icon_view_query_tooltip(int x, int y, bool keyboard_tooltip, Gtk.Tooltip tooltip)
  707. {
  708. Gtk.TreePath? path = _icon_view.get_path_at_pos(x, y);
  709. if (path == null)
  710. return false;
  711. Gtk.TreeIter iter;
  712. _list_store.get_iter(out iter, path);
  713. Value val;
  714. _list_store.get_value(iter, Column.TYPE, out val);
  715. string type = (string)val;
  716. _list_store.get_value(iter, Column.NAME, out val);
  717. string name = (string)val;
  718. _list_store.get_value(iter, Column.SIZE, out val);
  719. uint64 size = (uint64)val;
  720. _list_store.get_value(iter, Column.MTIME, out val);
  721. uint64 mtime = (uint64)val;
  722. string text = "<b>%s</b>\nType: %s\nSize: %s\nModified: %s".printf(GLib.Markup.escape_text(name)
  723. , GLib.Markup.escape_text(prettify_type(type))
  724. , size == 0 ? "n/a" : prettify_size(size)
  725. , mtime == 0 ? "n/a" : prettify_time(mtime)
  726. );
  727. tooltip.set_markup(text);
  728. return true;
  729. }
  730. }
  731. public class ProjectBrowser : Gtk.Bin
  732. {
  733. public enum SortMode
  734. {
  735. NAME_AZ,
  736. NAME_ZA,
  737. TYPE_AZ,
  738. TYPE_ZA,
  739. SIZE_MIN_MAX,
  740. SIZE_MAX_MIN,
  741. LAST_MTIME,
  742. FIRST_MTIME,
  743. COUNT;
  744. public string to_label()
  745. {
  746. switch (this) {
  747. case NAME_AZ:
  748. return "Name A-Z";
  749. case NAME_ZA:
  750. return "Name Z-A";
  751. case TYPE_AZ:
  752. return "Type A-Z";
  753. case TYPE_ZA:
  754. return "Type Z-A";
  755. case SIZE_MIN_MAX:
  756. return "Size min-Max";
  757. case SIZE_MAX_MIN:
  758. return "Size Max-min";
  759. case LAST_MTIME:
  760. return "Last Modified";
  761. case FIRST_MTIME:
  762. return "First Modified";
  763. default:
  764. return "Unknown";
  765. }
  766. }
  767. }
  768. // Data
  769. public ProjectStore _project_store;
  770. public ThumbnailCache _thumbnail_cache;
  771. // Widgets
  772. public Gtk.TreeModelFilter _tree_filter;
  773. public Gtk.TreeModelSort _tree_sort;
  774. public Gtk.TreeView _tree_view;
  775. public Gtk.TreeSelection _tree_selection;
  776. public Gdk.Pixbuf _empty_pixbuf;
  777. public ProjectFolderView _folder_view;
  778. public bool _show_folder_view;
  779. public Gtk.Button _toggle_folder_view;
  780. public Gtk.Box _tree_view_content;
  781. public Gtk.Button _toggle_icon_view;
  782. public Gtk.ListStore _folder_list_store;
  783. public Gtk.TreeModelSort _folder_list_sort;
  784. public SortMode _sort_mode;
  785. public Gtk.Box _sort_items_box;
  786. public Gtk.Popover _sort_items_popover;
  787. public Gtk.MenuButton _sort_items;
  788. public Gtk.Box _empty_favorites_box;
  789. public Gtk.Stack _folder_stack;
  790. public Gtk.Box _folder_view_content;
  791. public Gtk.ScrolledWindow _scrolled_window;
  792. public Gtk.Paned _paned;
  793. public bool _hide_core_resources;
  794. public ProjectBrowser(ProjectStore project_store, ThumbnailCache thumbnail_cache)
  795. {
  796. // Data
  797. _project_store = project_store;
  798. _thumbnail_cache = thumbnail_cache;
  799. _thumbnail_cache.changed.connect(() => {
  800. _tree_view.queue_draw();
  801. _folder_view.queue_draw();
  802. });
  803. // Widgets
  804. _tree_filter = new Gtk.TreeModelFilter(_project_store._tree_store, null);
  805. _tree_filter.set_visible_func((model, iter) => {
  806. if (_project_store.project_root_path() != null)
  807. _tree_view.expand_row(_project_store.project_root_path(), false);
  808. Value type;
  809. Value name;
  810. model.get_value(iter, ProjectStore.Column.TYPE, out type);
  811. model.get_value(iter, ProjectStore.Column.NAME, out name);
  812. bool should_show = (string)type != null
  813. && (string)name != null
  814. && !row_should_be_hidden((string)type, (string)name)
  815. ;
  816. if (_show_folder_view) {
  817. // Hide all descendants of the favorites root.
  818. Gtk.TreePath? path = model.get_path(iter);
  819. if (path != null && _project_store.favorites_root_path() != null && path.is_descendant(_project_store.favorites_root_path()))
  820. return false;
  821. return should_show && (type == "<folder>" || type == "<favorites>");
  822. } else {
  823. return should_show;
  824. }
  825. });
  826. _tree_sort = new Gtk.TreeModelSort.with_model(_tree_filter);
  827. _tree_sort.set_default_sort_func((model, iter_a, iter_b) => {
  828. Value type_a;
  829. Value type_b;
  830. model.get_value(iter_a, ProjectStore.Column.TYPE, out type_a);
  831. model.get_value(iter_b, ProjectStore.Column.TYPE, out type_b);
  832. // Favorites is always on top.
  833. if ((string)type_a == "<favorites>")
  834. return -1;
  835. if ((string)type_b == "<favorites>")
  836. return 1;
  837. // Then folders.
  838. if ((string)type_a == "<folder>") {
  839. if ((string)type_b != "<folder>")
  840. return -1;
  841. } else if ((string)type_b == "<folder>") {
  842. if ((string)type_a != "<folder>")
  843. return 1;
  844. }
  845. // And finally, regular files.
  846. Value id_a;
  847. Value id_b;
  848. model.get_value(iter_a, ProjectStore.Column.NAME, out id_a);
  849. model.get_value(iter_b, ProjectStore.Column.NAME, out id_b);
  850. return strcmp(GLib.Path.get_basename((string)id_a), GLib.Path.get_basename((string)id_b));
  851. });
  852. Gtk.CellRendererPixbuf cell_pixbuf = new Gtk.CellRendererPixbuf();
  853. cell_pixbuf.stock_size = Gtk.IconSize.SMALL_TOOLBAR;
  854. Gtk.CellRendererText cell_text = new Gtk.CellRendererText();
  855. Gtk.TreeViewColumn column = new Gtk.TreeViewColumn();
  856. column.pack_start(cell_pixbuf, false);
  857. column.pack_start(cell_text, true);
  858. column.set_cell_data_func(cell_pixbuf, pixbuf_func);
  859. column.set_cell_data_func(cell_text, text_func);
  860. _tree_view = new Gtk.TreeView();
  861. _tree_view.append_column(column);
  862. #if 0
  863. // For debugging.
  864. _tree_view.insert_column_with_attributes(-1
  865. , "Segment"
  866. , new Gtk.CellRendererText()
  867. , "text"
  868. , ProjectStore.Column.SEGMENT
  869. , null
  870. );
  871. _tree_view.insert_column_with_attributes(-1
  872. , "Name"
  873. , new Gtk.CellRendererText()
  874. , "text"
  875. , ProjectStore.Column.NAME
  876. , null
  877. );
  878. _tree_view.insert_column_with_attributes(-1
  879. , "Type"
  880. , new Gtk.CellRendererText()
  881. , "text"
  882. , ProjectStore.Column.TYPE
  883. , null
  884. );
  885. #endif /* if 0 */
  886. _tree_view.model = _tree_sort;
  887. _tree_view.headers_visible = false;
  888. _tree_view.button_press_event.connect(on_button_pressed);
  889. _tree_view.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, dnd_targets, Gdk.DragAction.COPY);
  890. _tree_view.drag_data_get.connect(on_drag_data_get);
  891. _tree_view.drag_begin.connect_after(on_drag_begin);
  892. _tree_view.drag_end.connect(on_drag_end);
  893. _tree_selection = _tree_view.get_selection();
  894. _tree_selection.set_mode(Gtk.SelectionMode.BROWSE);
  895. _tree_selection.changed.connect(() => { update_folder_view(); });
  896. _empty_pixbuf = new Gdk.Pixbuf.from_data({ 0x00, 0x00, 0x00, 0x00 }, Gdk.Colorspace.RGB, true, 8, 1, 1, 4);
  897. _project_store._tree_store.row_inserted.connect((path, iter) => { update_folder_view(); });
  898. _project_store._tree_store.row_changed.connect((path, iter) => { update_folder_view(); });
  899. _project_store._tree_store.row_deleted.connect((path) => { update_folder_view(); });
  900. // Create icon view.
  901. _folder_view = new ProjectFolderView(_project_store, thumbnail_cache);
  902. // Create switch button.
  903. _show_folder_view = true;
  904. _toggle_folder_view = new Gtk.Button.from_icon_name("level-tree-symbolic", Gtk.IconSize.SMALL_TOOLBAR);
  905. _toggle_folder_view.get_style_context().add_class("flat");
  906. _toggle_folder_view.can_focus = false;
  907. _toggle_folder_view.clicked.connect(() => {
  908. _show_folder_view = !_show_folder_view;
  909. if (_show_folder_view) {
  910. // Save the currently selected resource and a path to its parent. Those will be
  911. // used later, after the tree has been refiltered, to show the correct folder
  912. // and reveal the selected resource in the icon view.
  913. string? selected_type = null;
  914. string? selected_name = null;
  915. Gtk.TreePath? parent_path = null;
  916. Gtk.TreeModel selected_model;
  917. Gtk.TreeIter selected_iter;
  918. if (_tree_selection.get_selected(out selected_model, out selected_iter)) {
  919. Value val;
  920. selected_model.get_value(selected_iter, ProjectStore.Column.TYPE, out val);
  921. selected_type = (string)val;
  922. selected_model.get_value(selected_iter, ProjectStore.Column.NAME, out val);
  923. selected_name = (string)val;
  924. if (selected_type != "<folder>") {
  925. Gtk.TreeIter parent_iter;
  926. if (selected_model.iter_parent(out parent_iter, selected_iter))
  927. parent_path = _tree_view.model.get_path(parent_iter);
  928. }
  929. }
  930. _tree_filter.refilter();
  931. if (parent_path != null) {
  932. _tree_selection.select_path(parent_path);
  933. _folder_view.reveal(selected_type, selected_name);
  934. }
  935. _folder_view_content.show_all();
  936. _toggle_folder_view.set_image(new Gtk.Image.from_icon_name("level-tree-symbolic", Gtk.IconSize.SMALL_TOOLBAR));
  937. } else {
  938. // Save the currently selected resource. This will be used later, after the tree
  939. // has been refiltered, to reveal the selected resource in the tree view.
  940. string? selected_type = null;
  941. string? selected_name = null;
  942. Gtk.TreePath selected_path;
  943. if (_folder_view.selected_path(out selected_path)) {
  944. Gtk.TreeIter iter;
  945. _folder_view._list_store.get_iter(out iter, selected_path);
  946. GLib.Value val;
  947. _folder_view._list_store.get_value(iter, ProjectFolderView.Column.TYPE, out val);
  948. selected_type = (string)val;
  949. _folder_view._list_store.get_value(iter, ProjectFolderView.Column.NAME, out val);
  950. selected_name = (string)val;
  951. }
  952. _tree_filter.refilter();
  953. if (selected_type != null && selected_type != "<folder>") {
  954. reveal(selected_type, selected_name);
  955. }
  956. _folder_view_content.hide();
  957. _toggle_folder_view.set_image(new Gtk.Image.from_icon_name("browser-icon-view", Gtk.IconSize.SMALL_TOOLBAR));
  958. _tree_view.queue_draw(); // It doesn't draw by itself sometimes...
  959. }
  960. });
  961. // Create paned split-view.
  962. _scrolled_window = new Gtk.ScrolledWindow(null, null);
  963. _scrolled_window.add(_tree_view);
  964. var _tree_view_control = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 0);
  965. _tree_view_control.pack_end(_toggle_folder_view, false, false);
  966. _tree_view_content = new Gtk.Box(Gtk.Orientation.VERTICAL, 0);
  967. _tree_view_content.pack_start(_tree_view_control, false);
  968. _tree_view_content.pack_start(_scrolled_window, true, true);
  969. // Setup sort menu button popover.
  970. _sort_items_box = new Gtk.Box(Gtk.Orientation.VERTICAL, 0);
  971. Gtk.RadioButton? button = null;
  972. for (int i = 0; i < SortMode.COUNT; ++i)
  973. button = add_sort_item(button, (SortMode)i);
  974. _sort_items_box.show_all();
  975. _sort_items_popover = new Gtk.Popover(null);
  976. _sort_items_popover.add(_sort_items_box);
  977. _sort_items = new Gtk.MenuButton();
  978. _sort_items.add(new Gtk.Image.from_icon_name("list-sort", Gtk.IconSize.SMALL_TOOLBAR));
  979. _sort_items.get_style_context().add_class("flat");
  980. _sort_items.can_focus = false;
  981. _sort_items.set_popover(_sort_items_popover);
  982. bool _show_icon_view = true;
  983. _toggle_icon_view = new Gtk.Button.from_icon_name("browser-list-view", Gtk.IconSize.SMALL_TOOLBAR);
  984. _toggle_icon_view.get_style_context().add_class("flat");
  985. _toggle_icon_view.can_focus = false;
  986. _toggle_icon_view.clicked.connect(() => {
  987. Gtk.TreePath path;
  988. bool any_selected = _folder_view.selected_path(out path);
  989. if (_show_icon_view) {
  990. if (any_selected) {
  991. Gtk.TreeIter iter;
  992. _folder_view._list_store.get_iter(out iter, path);
  993. _folder_view._list_view.get_selection().select_iter(iter);
  994. }
  995. _folder_view._stack.set_visible_child_full("list-view", Gtk.StackTransitionType.NONE);
  996. _toggle_icon_view.set_image(new Gtk.Image.from_icon_name("browser-icon-view", Gtk.IconSize.SMALL_TOOLBAR));
  997. } else {
  998. if (any_selected)
  999. _folder_view._icon_view.select_path(path);
  1000. _folder_view._stack.set_visible_child_full("icon-view", Gtk.StackTransitionType.NONE);
  1001. _toggle_icon_view.set_image(new Gtk.Image.from_icon_name("browser-list-view", Gtk.IconSize.SMALL_TOOLBAR));
  1002. }
  1003. _show_icon_view = !_show_icon_view;
  1004. });
  1005. var _folder_view_control = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 0);
  1006. _folder_view_control.pack_end(_toggle_icon_view, false, false);
  1007. _folder_view_control.pack_end(_sort_items, false, false);
  1008. _empty_favorites_box = new Gtk.Box(Gtk.Orientation.VERTICAL, 0);
  1009. _empty_favorites_box.valign = Gtk.Align.CENTER;
  1010. _empty_favorites_box.pack_start(new Gtk.Image.from_icon_name("browser-favorites", Gtk.IconSize.DIALOG), false, false);
  1011. _empty_favorites_box.pack_start(new Gtk.Label("Favorites is empty"), false, false);
  1012. _folder_stack = new Gtk.Stack();
  1013. _folder_stack.add_named(_folder_view, "folder-view");
  1014. _folder_stack.add_named(_empty_favorites_box, "empty-favorites");
  1015. _folder_stack.set_visible_child_full("folder-view", Gtk.StackTransitionType.NONE);
  1016. _folder_view_content = new Gtk.Box(Gtk.Orientation.VERTICAL, 0);
  1017. _folder_view_content.pack_start(_folder_view_control, false);
  1018. _folder_view_content.pack_start(_folder_stack, true, true);
  1019. _paned = new Gtk.Paned(Gtk.Orientation.VERTICAL);
  1020. _paned.pack1(_tree_view_content, true, false);
  1021. _paned.pack2(_folder_view_content, true, false);
  1022. _paned.set_position(400);
  1023. this.add(_paned);
  1024. _hide_core_resources = true;
  1025. _folder_list_store = new Gtk.ListStore(ProjectStore.Column.COUNT
  1026. , typeof(string) // ProjectStore.Column.NAME
  1027. , typeof(string) // ProjectStore.Column.TYPE
  1028. , typeof(uint64) // ProjectStore.Column.SIZE
  1029. , typeof(uint64) // ProjectStore.Column.MTIME
  1030. );
  1031. _folder_list_sort = new Gtk.TreeModelSort.with_model(_folder_list_store);
  1032. _folder_list_sort.set_default_sort_func((model, iter_a, iter_b) => {
  1033. Value type_a;
  1034. Value type_b;
  1035. model.get_value(iter_a, ProjectStore.Column.TYPE, out type_a);
  1036. model.get_value(iter_b, ProjectStore.Column.TYPE, out type_b);
  1037. Value name_a;
  1038. Value name_b;
  1039. model.get_value(iter_a, ProjectStore.Column.NAME, out name_a);
  1040. model.get_value(iter_b, ProjectStore.Column.NAME, out name_b);
  1041. // Folders are always on top.
  1042. if ((string)type_a == "<folder>" && (string)type_b != "<folder>") {
  1043. return -1;
  1044. } else if ((string)type_a != "<folder>" && (string)type_b == "<folder>") {
  1045. return 1;
  1046. } else if ((string)type_a == "<folder>" && (string)type_b == "<folder>") {
  1047. // Special folders always first.
  1048. if ((string)name_a == "..")
  1049. return -1;
  1050. else if ((string)name_b == "..")
  1051. return 1;
  1052. }
  1053. switch (_sort_mode) {
  1054. case SortMode.NAME_AZ:
  1055. case SortMode.NAME_ZA: {
  1056. int cmp = strcmp((string)name_a, (string)name_b);
  1057. return _sort_mode == SortMode.NAME_AZ ? cmp : -cmp;
  1058. }
  1059. case SortMode.TYPE_AZ:
  1060. case SortMode.TYPE_ZA: {
  1061. int cmp = strcmp((string)type_a, (string)type_b);
  1062. return _sort_mode == SortMode.TYPE_AZ ? cmp : -cmp;
  1063. }
  1064. case SortMode.SIZE_MIN_MAX:
  1065. case SortMode.SIZE_MAX_MIN: {
  1066. Value size_a;
  1067. Value size_b;
  1068. model.get_value(iter_a, ProjectStore.Column.SIZE, out size_a);
  1069. model.get_value(iter_b, ProjectStore.Column.SIZE, out size_b);
  1070. int cmp = (uint64)size_a <= (uint64)size_b ? -1 : 1;
  1071. return _sort_mode == SortMode.SIZE_MIN_MAX ? cmp : -cmp;
  1072. }
  1073. case SortMode.LAST_MTIME:
  1074. case SortMode.FIRST_MTIME: {
  1075. Value mtime_a;
  1076. Value mtime_b;
  1077. model.get_value(iter_a, ProjectStore.Column.MTIME, out mtime_a);
  1078. model.get_value(iter_b, ProjectStore.Column.MTIME, out mtime_b);
  1079. int cmp = (uint64)mtime_a >= (uint64)mtime_b ? -1 : 1;
  1080. return _sort_mode == SortMode.LAST_MTIME ? cmp : -cmp;
  1081. }
  1082. default:
  1083. return 0;
  1084. }
  1085. });
  1086. // Actions.
  1087. GLib.ActionEntry[] action_entries =
  1088. {
  1089. { "open-directory", on_open_directory, "s", null },
  1090. { "favorite-resource", on_favorite_resource, "(ss)", null },
  1091. { "unfavorite-resource", on_unfavorite_resource, "(ss)", null }
  1092. };
  1093. GLib.Application.get_default().add_action_entries(action_entries, this);
  1094. }
  1095. private void on_drag_data_get(Gdk.DragContext context, Gtk.SelectionData data, uint info, uint time_)
  1096. {
  1097. // https://valadoc.org/gtk+-3.0/Gtk.Widget.drag_data_get.html
  1098. Gtk.TreeModel selected_model;
  1099. Gtk.TreeIter selected_iter;
  1100. if (!_tree_selection.get_selected(out selected_model, out selected_iter))
  1101. return;
  1102. Value val;
  1103. string type;
  1104. string name;
  1105. selected_model.get_value(selected_iter, ProjectStore.Column.TYPE, out val);
  1106. type = (string)val;
  1107. selected_model.get_value(selected_iter, ProjectStore.Column.NAME, out val);
  1108. name = (string)val;
  1109. string resource_path = ResourceId.path(type, name);
  1110. data.set(Gdk.Atom.intern_static_string("RESOURCE_PATH"), 8, resource_path.data);
  1111. }
  1112. private void on_drag_begin(Gdk.DragContext context)
  1113. {
  1114. // https://valadoc.org/gtk+-3.0/Gtk.Widget.drag_begin.html
  1115. Gtk.drag_set_icon_pixbuf(context, _empty_pixbuf, 0, 0);
  1116. }
  1117. private void on_drag_end(Gdk.DragContext context)
  1118. {
  1119. // https://valadoc.org/gtk+-3.0/Gtk.Widget.drag_end.html
  1120. GLib.Application.get_default().activate_action("cancel-place", null);
  1121. }
  1122. // Returns true if the row should be hidden.
  1123. private bool row_should_be_hidden(string type, string name)
  1124. {
  1125. return type == "<folder>" && name == "core" && _hide_core_resources
  1126. || type == "importer_settings"
  1127. || name == Project.LEVEL_EDITOR_TEST_NAME
  1128. || _project_store._project.is_type_importable(type)
  1129. ;
  1130. }
  1131. public void reveal(string type, string name)
  1132. {
  1133. if (name.has_prefix("core/")) {
  1134. _hide_core_resources = false;
  1135. _tree_filter.refilter();
  1136. }
  1137. string parent_type = type;
  1138. string parent_name = name;
  1139. Gtk.TreePath filter_path = null;
  1140. do {
  1141. Gtk.TreePath store_path;
  1142. if (!_project_store.path_for_resource_type_name(out store_path, parent_type, parent_name)) {
  1143. break;
  1144. }
  1145. filter_path = _tree_filter.convert_child_path_to_path(store_path);
  1146. if (filter_path == null) {
  1147. // Either the path is not valid or points to a non-visible row in the model.
  1148. parent_type = "<folder>";
  1149. parent_name = ResourceId.parent_folder(parent_name);
  1150. continue;
  1151. }
  1152. Gtk.TreePath sort_path = _tree_sort.convert_child_path_to_path(filter_path);
  1153. if (sort_path == null) {
  1154. // The path is not valid.
  1155. break;
  1156. }
  1157. _tree_view.expand_to_path(sort_path);
  1158. _tree_view.get_selection().select_path(sort_path);
  1159. _tree_view.scroll_to_cell(sort_path, null, false, 0.0f, 0.0f);
  1160. _folder_view.reveal(type, name);
  1161. } while (filter_path == null);
  1162. }
  1163. private void on_open_directory(GLib.SimpleAction action, GLib.Variant? param)
  1164. {
  1165. string dir_name = param.get_string();
  1166. if (dir_name.has_prefix("core/") || dir_name == "core") {
  1167. _hide_core_resources = false;
  1168. _tree_filter.refilter();
  1169. }
  1170. Gtk.TreePath store_path;
  1171. if (_project_store.path_for_resource_type_name(out store_path, "<folder>", dir_name)) {
  1172. Gtk.TreePath filter_path = _tree_filter.convert_child_path_to_path(store_path);
  1173. if (filter_path == null) // Either the path is not valid or points to a non-visible row in the model.
  1174. return;
  1175. Gtk.TreePath sort_path = _tree_sort.convert_child_path_to_path(filter_path);
  1176. if (sort_path == null) // The path is not valid.
  1177. return;
  1178. _tree_view.expand_to_path(sort_path);
  1179. _tree_view.get_selection().select_path(sort_path);
  1180. }
  1181. }
  1182. private void on_favorite_resource(GLib.SimpleAction action, GLib.Variant? param)
  1183. {
  1184. string type = (string)param.get_child_value(0);
  1185. string name = (string)param.get_child_value(1);
  1186. _project_store.add_to_favorites(type, name);
  1187. }
  1188. private void on_unfavorite_resource(GLib.SimpleAction action, GLib.Variant? param)
  1189. {
  1190. string type = (string)param.get_child_value(0);
  1191. string name = (string)param.get_child_value(1);
  1192. _project_store.remove_from_favorites(type, name);
  1193. }
  1194. private bool on_button_pressed(Gdk.EventButton ev)
  1195. {
  1196. if (ev.button == Gdk.BUTTON_SECONDARY) {
  1197. Gtk.TreePath path;
  1198. if (_tree_view.get_path_at_pos((int)ev.x, (int)ev.y, out path, null, null, null)) {
  1199. Gtk.TreeIter iter;
  1200. _tree_view.model.get_iter(out iter, path);
  1201. Value type;
  1202. Value name;
  1203. _tree_view.model.get_value(iter, ProjectStore.Column.TYPE, out type);
  1204. _tree_view.model.get_value(iter, ProjectStore.Column.NAME, out name);
  1205. Gtk.TreePath? filter_path = _tree_sort.convert_path_to_child_path(path);
  1206. Gtk.TreePath? store_path = _tree_filter.convert_path_to_child_path(filter_path);
  1207. Gtk.Menu? menu;
  1208. if (store_path.is_descendant(_project_store.project_root_path()) || store_path.compare(_project_store.project_root_path()) == 0)
  1209. menu = project_entry_menu_create((string)type, (string)name);
  1210. else if (store_path.is_descendant(_project_store.favorites_root_path()))
  1211. menu = favorites_entry_menu_create((string)type, (string)name);
  1212. else
  1213. menu = null;
  1214. if (menu != null) {
  1215. menu.show_all();
  1216. menu.popup_at_pointer(ev);
  1217. }
  1218. }
  1219. } else if (ev.button == Gdk.BUTTON_PRIMARY && ev.type == Gdk.EventType.@2BUTTON_PRESS) {
  1220. Gtk.TreePath path;
  1221. if (_tree_view.get_path_at_pos((int)ev.x, (int)ev.y, out path, null, null, null)) {
  1222. Gtk.TreeIter iter;
  1223. _tree_view.model.get_iter(out iter, path);
  1224. Value type;
  1225. _tree_view.model.get_value(iter, ProjectStore.Column.TYPE, out type);
  1226. if ((string)type == "<folder>")
  1227. return Gdk.EVENT_PROPAGATE;
  1228. Value name;
  1229. _tree_view.model.get_value(iter, ProjectStore.Column.NAME, out name);
  1230. GLib.Application.get_default().activate_action("open-resource", ResourceId.path((string)type, (string)name));
  1231. }
  1232. }
  1233. return Gdk.EVENT_PROPAGATE;
  1234. }
  1235. private void update_folder_view()
  1236. {
  1237. _folder_list_store.clear();
  1238. _folder_view._list_store.clear();
  1239. // Get the selected node's type and name.
  1240. Gtk.TreeModel selected_model;
  1241. Gtk.TreeIter selected_iter;
  1242. if (!_tree_selection.get_selected(out selected_model, out selected_iter))
  1243. return;
  1244. string selected_type;
  1245. string selected_name;
  1246. Value val;
  1247. selected_model.get_value(selected_iter, ProjectStore.Column.TYPE, out val);
  1248. selected_type = (string)val;
  1249. selected_model.get_value(selected_iter, ProjectStore.Column.NAME, out val);
  1250. selected_name = (string)val;
  1251. if (selected_type == "<folder>") {
  1252. _folder_view._showing_project_folder = true;
  1253. // Add parent folder.
  1254. if (selected_name != "") {
  1255. Gtk.TreeIter dummy;
  1256. _folder_list_store.insert_with_values(out dummy
  1257. , -1
  1258. , ProjectStore.Column.TYPE
  1259. , "<folder>"
  1260. , ProjectStore.Column.NAME
  1261. , ".."
  1262. , ProjectStore.Column.SIZE
  1263. , 0u
  1264. , ProjectStore.Column.MTIME
  1265. , 0u
  1266. , -1
  1267. );
  1268. }
  1269. // Fill the intermediate icon view list with paths matching the selected node's name.
  1270. _project_store._list_store.foreach((model, path, iter) => {
  1271. string type;
  1272. string name;
  1273. model.get_value(iter, ProjectStore.Column.TYPE, out val);
  1274. type = (string)val;
  1275. model.get_value(iter, ProjectStore.Column.NAME, out val);
  1276. name = (string)val;
  1277. if (row_should_be_hidden(type, name))
  1278. return false;
  1279. // Skip paths without common ancestor.
  1280. if (ResourceId.parent_folder(name) != selected_name)
  1281. return false;
  1282. // Skip paths that are too deep in the hierarchy:
  1283. // selected_name: foo
  1284. // hierarchy:
  1285. // foo/bar OK
  1286. // foo/baz OK
  1287. // foo/bar/baz NOPE
  1288. string name_suffix;
  1289. if (selected_name == "") // Project folder.
  1290. name_suffix = name.substring((selected_name).length);
  1291. else if (selected_name != name) // Folder itself.
  1292. name_suffix = name.substring((selected_name).length + 1);
  1293. else
  1294. return false;
  1295. if (name_suffix.index_of_char('/') != -1)
  1296. return false;
  1297. uint64 size;
  1298. uint64 mtime;
  1299. model.get_value(iter, ProjectStore.Column.SIZE, out val);
  1300. size = (uint64)val;
  1301. model.get_value(iter, ProjectStore.Column.MTIME, out val);
  1302. mtime = (uint64)val;
  1303. // Add the path to the list.
  1304. Gtk.TreeIter dummy;
  1305. _folder_list_store.insert_with_values(out dummy
  1306. , -1
  1307. , ProjectStore.Column.TYPE
  1308. , type
  1309. , ProjectStore.Column.NAME
  1310. , name
  1311. , ProjectStore.Column.SIZE
  1312. , size
  1313. , ProjectStore.Column.MTIME
  1314. , mtime
  1315. , -1
  1316. );
  1317. return false;
  1318. });
  1319. _folder_view._selected_type = selected_type;
  1320. _folder_view._selected_name = selected_name;
  1321. _folder_stack.set_visible_child_full("folder-view", Gtk.StackTransitionType.NONE);
  1322. } else if (selected_type == "<favorites>") {
  1323. _folder_view._showing_project_folder = false;
  1324. int num_items = 0;
  1325. // Fill the icon view list with paths whose ancestor is the favorites root.
  1326. _project_store._tree_store.foreach((model, path, iter) => {
  1327. string type;
  1328. string name;
  1329. model.get_value(iter, ProjectStore.Column.TYPE, out val);
  1330. type = (string)val;
  1331. model.get_value(iter, ProjectStore.Column.NAME, out val);
  1332. name = (string)val;
  1333. if (!path.is_descendant(_project_store.favorites_root_path()))
  1334. return false;
  1335. uint64 size;
  1336. uint64 mtime;
  1337. model.get_value(iter, ProjectStore.Column.SIZE, out val);
  1338. size = (uint64)val;
  1339. model.get_value(iter, ProjectStore.Column.MTIME, out val);
  1340. mtime = (uint64)val;
  1341. // Add the path to the list.
  1342. Gtk.TreeIter dummy;
  1343. _folder_list_store.insert_with_values(out dummy
  1344. , -1
  1345. , ProjectStore.Column.TYPE
  1346. , type
  1347. , ProjectStore.Column.NAME
  1348. , name
  1349. , ProjectStore.Column.SIZE
  1350. , size
  1351. , ProjectStore.Column.MTIME
  1352. , mtime
  1353. , -1
  1354. );
  1355. ++num_items;
  1356. return false;
  1357. });
  1358. if (num_items == 0)
  1359. _folder_stack.set_visible_child_full("empty-favorites", Gtk.StackTransitionType.NONE);
  1360. else
  1361. _folder_stack.set_visible_child_full("folder-view", Gtk.StackTransitionType.NONE);
  1362. }
  1363. // Now, fill the actual icon view list with correctly sorted paths.
  1364. _folder_list_sort.foreach((model, path, iter) => {
  1365. string type;
  1366. string name;
  1367. uint64 size;
  1368. uint64 mtime;
  1369. model.get_value(iter, ProjectStore.Column.TYPE, out val);
  1370. type = (string)val;
  1371. model.get_value(iter, ProjectStore.Column.NAME, out val);
  1372. name = (string)val;
  1373. model.get_value(iter, ProjectStore.Column.SIZE, out val);
  1374. size = (uint64)val;
  1375. model.get_value(iter, ProjectStore.Column.MTIME, out val);
  1376. mtime = (uint64)val;
  1377. // Add the path to the list.
  1378. Gtk.TreeIter dummy;
  1379. _folder_view._list_store.insert_with_values(out dummy
  1380. , -1
  1381. , ProjectFolderView.Column.TYPE
  1382. , type
  1383. , ProjectFolderView.Column.NAME
  1384. , name
  1385. , ProjectFolderView.Column.SIZE
  1386. , size
  1387. , ProjectFolderView.Column.MTIME
  1388. , mtime
  1389. , -1
  1390. );
  1391. return false;
  1392. });
  1393. }
  1394. public void select_project_root()
  1395. {
  1396. Gtk.TreePath? filter_path = _tree_filter.convert_child_path_to_path(_project_store.project_root_path());
  1397. if (filter_path == null)
  1398. return;
  1399. Gtk.TreePath? sort_path = _tree_sort.convert_child_path_to_path(filter_path);
  1400. if (sort_path == null)
  1401. return;
  1402. _tree_selection.select_path(sort_path);
  1403. }
  1404. private void pixbuf_func(Gtk.CellLayout cell_layout, Gtk.CellRenderer cell, Gtk.TreeModel model, Gtk.TreeIter iter)
  1405. {
  1406. Value val;
  1407. string type;
  1408. string name;
  1409. model.get_value(iter, ProjectStore.Column.TYPE, out val);
  1410. type = (string)val;
  1411. model.get_value(iter, ProjectStore.Column.NAME, out val);
  1412. name = (string)val;
  1413. set_thumbnail(cell, type, name, 16, _thumbnail_cache);
  1414. }
  1415. private void text_func(Gtk.CellLayout cell_layout, Gtk.CellRenderer cell, Gtk.TreeModel model, Gtk.TreeIter iter)
  1416. {
  1417. Value name;
  1418. Value type;
  1419. model.get_value(iter, ProjectStore.Column.NAME, out name);
  1420. model.get_value(iter, ProjectStore.Column.TYPE, out type);
  1421. string basename = GLib.Path.get_basename((string)name);
  1422. if ((string)type == "<folder>") {
  1423. if ((string)name == "")
  1424. cell.set_property("text", _project_store._project.name());
  1425. else
  1426. cell.set_property("text", basename);
  1427. } else if ((string)type == "<favorites>") {
  1428. cell.set_property("text", "Favorites");
  1429. } else {
  1430. cell.set_property("text", ResourceId.path((string)type, basename));
  1431. }
  1432. }
  1433. private Gtk.RadioButton add_sort_item(Gtk.RadioButton? group, SortMode mode)
  1434. {
  1435. var button = new Gtk.RadioButton.with_label_from_widget(group, mode.to_label());
  1436. button.toggled.connect(() => {
  1437. _sort_mode = mode;
  1438. update_folder_view();
  1439. _sort_items_popover.popdown();
  1440. });
  1441. _sort_items_box.pack_start(button, false, false);
  1442. return button;
  1443. }
  1444. }
  1445. } /* namespace Crown */