project_browser.vala 50 KB

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