level_editor.vala 83 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814
  1. /*
  2. * Copyright (c) 2012-2022 Daniele Bartolini et al.
  3. * License: https://github.com/crownengine/crown/blob/master/LICENSE
  4. */
  5. using Gdk; // Pixbuf
  6. using Gee;
  7. using Gtk;
  8. namespace Crown
  9. {
  10. const int WINDOW_DEFAULT_WIDTH = 1280;
  11. const int WINDOW_DEFAULT_HEIGHT = 720;
  12. const string LEVEL_EDITOR_WINDOW_TITLE = "Crown Editor";
  13. const string CROWN_ICON_NAME = "crown";
  14. public enum Theme
  15. {
  16. DARK,
  17. LIGHT,
  18. COUNT
  19. }
  20. public class LevelEditorWindow : Gtk.ApplicationWindow
  21. {
  22. private const GLib.ActionEntry[] action_entries =
  23. {
  24. { "fullscreen", on_fullscreen, null, null }
  25. };
  26. public bool _fullscreen;
  27. public LevelEditorWindow(Gtk.Application app)
  28. {
  29. Object(application: app);
  30. this.add_action_entries(action_entries, this);
  31. this.title = LEVEL_EDITOR_WINDOW_TITLE;
  32. this.key_press_event.connect(this.on_key_press);
  33. this.key_release_event.connect(this.on_key_release);
  34. this.window_state_event.connect(this.on_window_state_event);
  35. this.delete_event.connect(this.on_delete_event);
  36. this.focus_out_event.connect(this.on_focus_out);
  37. _fullscreen = false;
  38. }
  39. private void on_fullscreen(GLib.SimpleAction action, GLib.Variant? param)
  40. {
  41. if (_fullscreen)
  42. unfullscreen();
  43. else
  44. fullscreen();
  45. }
  46. private bool on_key_press(Gdk.EventKey ev)
  47. {
  48. LevelEditorApplication app = (LevelEditorApplication)application;
  49. string str = "";
  50. if (ev.keyval == Gdk.Key.Control_L)
  51. str += LevelEditorApi.key_down("ctrl_left");
  52. else if (ev.keyval == Gdk.Key.Shift_L)
  53. str += LevelEditorApi.key_down("shift_left");
  54. else if (ev.keyval == Gdk.Key.Alt_L)
  55. str += LevelEditorApi.key_down("alt_left");
  56. if (str.length != 0) {
  57. app._editor.send_script(str);
  58. app._editor.send(DeviceApi.frame());
  59. }
  60. return Gdk.EVENT_PROPAGATE;
  61. }
  62. private bool on_key_release(Gdk.EventKey ev)
  63. {
  64. LevelEditorApplication app = (LevelEditorApplication)application;
  65. string str = "";
  66. if (ev.keyval == Gdk.Key.Control_L)
  67. str += LevelEditorApi.key_up("ctrl_left");
  68. else if (ev.keyval == Gdk.Key.Shift_L)
  69. str += LevelEditorApi.key_up("shift_left");
  70. else if (ev.keyval == Gdk.Key.Alt_L)
  71. str += LevelEditorApi.key_up("alt_left");
  72. if (str.length != 0) {
  73. app._editor.send_script(str);
  74. app._editor.send(DeviceApi.frame());
  75. }
  76. return Gdk.EVENT_PROPAGATE;
  77. }
  78. private bool on_window_state_event(Gdk.EventWindowState ev)
  79. {
  80. _fullscreen = (ev.new_window_state & Gdk.WindowState.FULLSCREEN) != 0;
  81. return Gdk.EVENT_STOP;
  82. }
  83. private bool on_delete_event()
  84. {
  85. LevelEditorApplication app = (LevelEditorApplication)application;
  86. if (app.should_quit())
  87. app.stop_backend_and_quit();
  88. return Gdk.EVENT_STOP; // Keep window alive.
  89. }
  90. private bool on_focus_out(Gdk.EventFocus ev)
  91. {
  92. LevelEditorApplication app = (LevelEditorApplication)application;
  93. app._editor.send_script(LevelEditorApi.key_up("ctrl_left"));
  94. app._editor.send_script(LevelEditorApi.key_up("shift_left"));
  95. app._editor.send_script(LevelEditorApi.key_up("alt_left"));
  96. return Gdk.EVENT_PROPAGATE;
  97. }
  98. }
  99. public enum StartGame
  100. {
  101. NORMAL,
  102. TEST
  103. }
  104. public class LevelEditorApplication : Gtk.Application
  105. {
  106. // Constants
  107. private const GLib.ActionEntry[] action_entries_file =
  108. {
  109. // parameter type
  110. // name activate() | state
  111. // | | | |
  112. { "menu-file", null, null, null },
  113. { "new-level", on_new_level, null, null },
  114. { "open-level", on_open_level, "s", null },
  115. { "new-project", on_new_project, null, null },
  116. { "open-project", on_open_project, "s", null },
  117. { "save", on_save, null, null },
  118. { "save-as", on_save_as, null, null },
  119. { "import", on_import, "s", null },
  120. { "preferences", on_preferences, null, null },
  121. { "deploy", on_deploy, null, null },
  122. { "close", on_close, null, null },
  123. { "quit", on_quit, null, null },
  124. { "open-resource", on_open_resource, "s", null }
  125. };
  126. private const GLib.ActionEntry[] action_entries_edit =
  127. {
  128. { "menu-edit", null, null, null },
  129. { "undo", on_undo, null, null },
  130. { "redo", on_redo, null, null },
  131. { "duplicate", on_duplicate, null, null },
  132. { "delete", on_delete, null, null },
  133. { "tool", on_tool_changed, "s", "'move'" },
  134. { "snap", on_snap_mode_changed, "s", "'relative'" },
  135. { "reference-system", on_reference_system_changed, "s", "'local'" },
  136. { "snap-to-grid", on_snap_to_grid, null, "true" },
  137. { "menu-grid", null, null, null },
  138. { "grid-show", on_show_grid, null, "true" },
  139. { "grid-custom", on_custom_grid, null, null },
  140. { "grid-preset", on_grid_changed, "s", "'1'" },
  141. { "menu-rotation-snap", null, null, null },
  142. { "rotation-snap-custom", on_rotation_snap, null, null },
  143. { "rotation-snap-preset", on_rotation_snap_changed, "s", "'15'" }
  144. };
  145. private const GLib.ActionEntry[] action_entries_create =
  146. {
  147. { "menu-create", null, null, null },
  148. { "menu-primitives", null, null, null },
  149. { "primitive-cube", on_create_primitive, null, null },
  150. { "primitive-sphere", on_create_primitive, null, null },
  151. { "primitive-cone", on_create_primitive, null, null },
  152. { "primitive-cylinder", on_create_primitive, null, null },
  153. { "primitive-plane", on_create_primitive, null, null },
  154. { "camera", on_create_primitive, null, null },
  155. { "light", on_create_primitive, null, null },
  156. { "sound-source", on_create_primitive, null, null },
  157. { "unit-empty", on_create_unit, null, null }
  158. };
  159. private const GLib.ActionEntry[] action_entries_camera =
  160. {
  161. { "menu-camera", null, null, null },
  162. { "camera-view", on_camera_view, "s", "'perspective'" }
  163. };
  164. private const GLib.ActionEntry[] action_entries_view =
  165. {
  166. { "menu-view", null, null, null },
  167. { "resource-chooser", on_resource_chooser, null, null },
  168. { "project-browser", on_project_browser, null, null },
  169. { "console", on_console, null, null },
  170. { "statusbar", on_statusbar, null, null },
  171. { "inspector", on_inspector, null, null },
  172. { "debug-render-world", on_debug_render_world, null, "false" },
  173. { "debug-physics-world", on_debug_physics_world, null, "false" }
  174. };
  175. private const GLib.ActionEntry[] action_entries_debug =
  176. {
  177. { "menu-debug", null, null, null },
  178. { "test-level", on_run_game, null, null },
  179. { "run-game", on_run_game, null, null },
  180. { "build-data", on_build_data, null, null },
  181. { "reload-lua", on_refresh_lua, null, null },
  182. { "restart-editor-view", on_restart_editor_view, null, null }
  183. };
  184. private const GLib.ActionEntry[] action_entries_help =
  185. {
  186. { "menu-help", null, null, null },
  187. { "manual", on_manual, null, null },
  188. { "report-issue", on_report_issue, null, null },
  189. { "browse-logs", on_browse_logs, null, null },
  190. { "changelog", on_changelog, null, null },
  191. { "about", on_about, null, null }
  192. };
  193. private const GLib.ActionEntry[] action_entries_project =
  194. {
  195. { "delete-file", on_delete_file, "s", null }
  196. };
  197. // Command line options
  198. private string? _source_dir = null;
  199. private string _level_resource = "";
  200. private User _user;
  201. private Hashtable _settings;
  202. // Subprocess launcher service.
  203. private SubprocessLauncher _subprocess_launcher;
  204. // Editor state
  205. private double _grid_size;
  206. private double _rotation_snap;
  207. private bool _show_grid;
  208. private bool _snap_to_grid;
  209. private bool _debug_render_world;
  210. private bool _debug_physics_world;
  211. private LevelEditorApi.ToolType _tool_type;
  212. private LevelEditorApi.ToolType _tool_type_prev;
  213. private LevelEditorApi.SnapMode _snap_mode;
  214. private LevelEditorApi.ReferenceSystem _reference_system;
  215. // Project state
  216. private string _placeable_type;
  217. private string _placeable_name;
  218. // Accelerators
  219. private string[] _tool_place_accels;
  220. private string[] _tool_move_accels;
  221. private string[] _tool_rotate_accels;
  222. private string[] _tool_scale_accels;
  223. private string[] _delete_accels;
  224. private string[] _camera_view_perspective_accels;
  225. private string[] _camera_view_front_accels;
  226. private string[] _camera_view_back_accels;
  227. private string[] _camera_view_right_accels;
  228. private string[] _camera_view_left_accels;
  229. private string[] _camera_view_top_accels;
  230. private string[] _camera_view_bottom_accels;
  231. // Engine connections
  232. private uint32 _compiler_process;
  233. private uint32 _editor_process;
  234. private uint32 _resource_preview_process;
  235. private uint32 _game_process;
  236. private GLib.SourceFunc _stop_data_compiler_callback = null;
  237. private GLib.SourceFunc _stop_editor_callback = null;
  238. private GLib.SourceFunc _stop_game_callback = null;
  239. private GLib.SourceFunc _stop_resource_preview_callback = null;
  240. private ConsoleClient _compiler;
  241. public ConsoleClient _editor;
  242. private ConsoleClient _resource_preview;
  243. private ConsoleClient _game;
  244. // Level data
  245. private UndoRedo _undo_redo;
  246. private Database _database;
  247. private Project _project;
  248. private ProjectStore _project_store;
  249. private Level _level;
  250. private DataCompiler _data_compiler;
  251. // Widgets
  252. private Gtk.CssProvider _css_provider;
  253. private ProjectBrowser _project_browser;
  254. private EditorView _editor_view;
  255. private EditorView _resource_preview_view;
  256. private LevelTreeView _level_treeview;
  257. private LevelLayersTreeView _level_layers_treeview;
  258. private PropertiesView _properties_view;
  259. private PreferencesDialog _preferences_dialog;
  260. private ResourceChooser _resource_chooser;
  261. private Gtk.Popover _resource_popover;
  262. private Gtk.Overlay _editor_view_overlay;
  263. private Gtk.Stack _project_stack;
  264. private Gtk.Label _project_stack_compiling_data_label;
  265. private Gtk.Label _project_stack_connecting_to_data_compiler_label;
  266. private Gtk.Label _project_stack_compiler_crashed_label;
  267. private Gtk.Label _project_stack_compiler_failed_compilation_label;
  268. private Gtk.Stack _editor_stack;
  269. private Gtk.Label _editor_stack_compiling_data_label;
  270. private Gtk.Label _editor_stack_connecting_to_data_compiler_label;
  271. private Gtk.Label _editor_stack_compiler_crashed_label;
  272. private Gtk.Label _editor_stack_compiler_failed_compilation_label;
  273. private Gtk.Label _editor_stack_disconnected_label;
  274. private Gtk.Label _editor_stack_oops_label;
  275. public Gtk.Stack _resource_preview_stack;
  276. public Gtk.Label _resource_preview_disconnected_label;
  277. public Gtk.Label _resource_preview_oops_label;
  278. public Gtk.Label _resource_preview_no_preview_label;
  279. private Gtk.Stack _inspector_stack;
  280. private Gtk.Label _inspector_stack_compiling_data_label;
  281. private Gtk.Label _inspector_stack_connecting_to_data_compiler_label;
  282. private Gtk.Label _inspector_stack_compiler_crashed_label;
  283. private Gtk.Label _inspector_stack_compiler_failed_compilation_label;
  284. private Gtk.Toolbar _toolbar;
  285. private Gtk.ToolButton _toolbar_run;
  286. private Gtk.Notebook _level_tree_view_notebook;
  287. private Gtk.Paned _editor_pane;
  288. private Gtk.Paned _content_pane;
  289. private Gtk.Paned _inspector_pane;
  290. private Gtk.Paned _main_pane;
  291. private Statusbar _statusbar;
  292. private Gtk.Box _main_vbox;
  293. private Gtk.FileFilter _file_filter;
  294. private Gtk.ComboBoxText _combo;
  295. private PanelNewProject _panel_new_project;
  296. private PanelProjectsList _panel_projects_list;
  297. private PanelWelcome _panel_welcome;
  298. private Gtk.Stack _main_stack;
  299. private uint _save_timer_id;
  300. public LevelEditorApplication(SubprocessLauncher subprocess_launcher)
  301. {
  302. Object(application_id: "org.crown.level_editor"
  303. , flags: GLib.ApplicationFlags.FLAGS_NONE
  304. );
  305. _subprocess_launcher = subprocess_launcher;
  306. }
  307. public Theme theme_name_to_enum(string theme)
  308. {
  309. if (theme == "dark")
  310. return Theme.DARK;
  311. else if (theme == "light")
  312. return Theme.LIGHT;
  313. else
  314. return Theme.COUNT;
  315. }
  316. public void set_theme_from_name(string theme_name)
  317. {
  318. Theme theme = theme_name_to_enum(theme_name);
  319. set_theme(theme);
  320. }
  321. public void set_theme(Theme theme)
  322. {
  323. if (theme == Theme.COUNT)
  324. return;
  325. string css = "/org/crown/level_editor/theme/Adwaita/gtk%s.css".printf(theme == Theme.DARK ? "-dark" : "");
  326. _css_provider.load_from_resource(css);
  327. }
  328. protected override void startup()
  329. {
  330. base.startup();
  331. Intl.setlocale(LocaleCategory.ALL, "C");
  332. _css_provider = new Gtk.CssProvider();
  333. var default_screen = Gdk.Display.get_default().get_default_screen();
  334. Gtk.StyleContext.add_provider_for_screen(default_screen
  335. , _css_provider
  336. , STYLE_PROVIDER_PRIORITY_APPLICATION
  337. );
  338. _settings = SJSON.load_from_path(_settings_file.get_path());
  339. // Set theme.
  340. set_theme(Theme.DARK);
  341. if (_settings.has_key("preferences")) {
  342. Hashtable preferences = (Hashtable)_settings["preferences"];
  343. if (preferences.has_key("theme"))
  344. set_theme_from_name((string)preferences["theme"]);
  345. }
  346. // HACK: register CrownClamp type within GObject's type system to
  347. // make GtkBuilder able to find it when creating the widget from
  348. // .ui files.
  349. // https://stackoverflow.com/questions/24235937/custom-gtk-widget-with-template-ui
  350. new Clamp().get_type().ensure();
  351. this.add_action_entries(action_entries_file, this);
  352. this.add_action_entries(action_entries_edit, this);
  353. this.add_action_entries(action_entries_create, this);
  354. this.add_action_entries(action_entries_camera, this);
  355. this.add_action_entries(action_entries_view, this);
  356. this.add_action_entries(action_entries_debug, this);
  357. this.add_action_entries(action_entries_help, this);
  358. this.add_action_entries(action_entries_project, this);
  359. _tool_place_accels = this.get_accels_for_action("app.tool::place");
  360. _tool_move_accels = this.get_accels_for_action("app.tool::move");
  361. _tool_rotate_accels = this.get_accels_for_action("app.tool::rotate");
  362. _tool_scale_accels = this.get_accels_for_action("app.tool::scale");
  363. _delete_accels = this.get_accels_for_action("app.delete");
  364. _camera_view_perspective_accels = this.get_accels_for_action("app.camera-view::perspective");
  365. _camera_view_front_accels = this.get_accels_for_action("app.camera-view::front");
  366. _camera_view_back_accels = this.get_accels_for_action("app.camera-view::back");
  367. _camera_view_right_accels = this.get_accels_for_action("app.camera-view::right");
  368. _camera_view_left_accels = this.get_accels_for_action("app.camera-view::left");
  369. _camera_view_top_accels = this.get_accels_for_action("app.camera-view::top");
  370. _camera_view_bottom_accels = this.get_accels_for_action("app.camera-view::bottom");
  371. _compiler = new ConsoleClient();
  372. _compiler.connected.connect(on_data_compiler_connected);
  373. _compiler.message_received.connect(on_message_received);
  374. _data_compiler = new DataCompiler(_compiler);
  375. uint32 undo_redo_size = DEFAULT_UNDO_REDO_MAX_SIZE;
  376. if (_settings.has_key("preferences")) {
  377. Hashtable preferences = (Hashtable)_settings["preferences"];
  378. if (preferences.has_key("undo_redo_max_size"))
  379. undo_redo_size = (uint32)(double)preferences["undo_redo_max_size"];
  380. }
  381. _undo_redo = new UndoRedo(undo_redo_size*1024*1024);
  382. _database = new Database(_undo_redo);
  383. _database.key_changed.connect(() => { update_active_window_title(); });
  384. _project = new Project(_database, _data_compiler);
  385. _project.set_toolchain_dir(_toolchain_dir.get_path());
  386. _project.register_importer("Sprite", { "png" }, SpriteResource.import, 0.0);
  387. _project.register_importer("Mesh", { "mesh" }, MeshResource.import, 1.0);
  388. _project.register_importer("Sound", { "wav" }, SoundResource.import, 2.0);
  389. _project.register_importer("Texture", { "png", "tga", "dds", "ktx", "pvr" }, TextureResource.import, 2.0);
  390. _editor = new ConsoleClient();
  391. _editor.connected.connect(on_editor_connected);
  392. _editor.message_received.connect(on_message_received);
  393. _resource_preview = new ConsoleClient();
  394. _resource_preview.connected.connect(on_resource_preview_connected);
  395. _resource_preview.message_received.connect(on_resource_preview_message_received);
  396. _game = new ConsoleClient();
  397. _game.connected.connect(on_game_connected);
  398. _game.message_received.connect(on_message_received);
  399. _level = new Level(_database, _editor, _project);
  400. // Editor state
  401. _grid_size = 1.0;
  402. _rotation_snap = 15.0;
  403. _show_grid = true;
  404. _snap_to_grid = true;
  405. _debug_render_world = false;
  406. _debug_physics_world = false;
  407. _tool_type = LevelEditorApi.ToolType.MOVE;
  408. _tool_type_prev = _tool_type;
  409. _snap_mode = LevelEditorApi.SnapMode.RELATIVE;
  410. _reference_system = LevelEditorApi.ReferenceSystem.LOCAL;
  411. // Project state
  412. _placeable_type = "";
  413. _placeable_name = "";
  414. // Engine connections
  415. _compiler_process = uint32.MAX;
  416. _editor_process = uint32.MAX;
  417. _resource_preview_process = uint32.MAX;
  418. _game_process = uint32.MAX;
  419. _project_store = new ProjectStore(_project);
  420. // Widgets
  421. _preferences_dialog = new PreferencesDialog(this);
  422. _preferences_dialog.delete_event.connect(() => { _preferences_dialog.hide(); return Gdk.EVENT_STOP; });
  423. _combo = new Gtk.ComboBoxText();
  424. _combo.append("editor", "Editor");
  425. _combo.append("game", "Game");
  426. _combo.set_active_id("editor");
  427. _console_view = new ConsoleView(_project, _combo, _preferences_dialog);
  428. _project_browser = new ProjectBrowser(this, _project, _project_store);
  429. _level_treeview = new LevelTreeView(_database, _level);
  430. _level_layers_treeview = new LevelLayersTreeView(_database, _level);
  431. _properties_view = new PropertiesView(_level, _project_store);
  432. _project_stack = new Gtk.Stack();
  433. _project_stack.add(_project_browser);
  434. _project_stack_compiling_data_label = compiling_data_label();
  435. _project_stack.add(_project_stack_compiling_data_label);
  436. _project_stack_connecting_to_data_compiler_label = connecting_to_data_compiler_label();
  437. _project_stack.add(_project_stack_connecting_to_data_compiler_label);
  438. _project_stack_compiler_crashed_label = compiler_crashed_label();
  439. _project_stack.add(_project_stack_compiler_crashed_label);
  440. _project_stack_compiler_failed_compilation_label = compiler_failed_compilation_label();
  441. _project_stack.add(_project_stack_compiler_failed_compilation_label);
  442. _editor_stack = new Gtk.Stack();
  443. _editor_stack_compiling_data_label = compiling_data_label();
  444. _editor_stack.add(_editor_stack_compiling_data_label);
  445. _editor_stack_connecting_to_data_compiler_label = connecting_to_data_compiler_label();
  446. _editor_stack.add(_editor_stack_connecting_to_data_compiler_label);
  447. _editor_stack_compiler_crashed_label = compiler_crashed_label();
  448. _editor_stack.add(_editor_stack_compiler_crashed_label);
  449. _editor_stack_compiler_failed_compilation_label = compiler_failed_compilation_label();
  450. _editor_stack.add(_editor_stack_compiler_failed_compilation_label);
  451. _editor_stack_disconnected_label = new Gtk.Label("Disconnected.");
  452. _editor_stack.add(_editor_stack_disconnected_label);
  453. _editor_stack_oops_label = new Gtk.Label(null);
  454. _editor_stack_oops_label.set_markup("Something went wrong.\rTry to <a href=\"restart\">restart this view</a>.");
  455. _editor_stack_oops_label.activate_link.connect(() => {
  456. activate_action("restart-editor-view", null);
  457. return true;
  458. });
  459. _editor_stack.add(_editor_stack_oops_label);
  460. _resource_preview_stack = new Gtk.Stack();
  461. _resource_preview_no_preview_label = new Gtk.Label("No Preview");
  462. _resource_preview_no_preview_label.set_size_request(300, 300);
  463. _resource_preview_stack.add(_resource_preview_no_preview_label);
  464. _resource_preview_disconnected_label = new Gtk.Label("Disconnected");
  465. _resource_preview_stack.add(_resource_preview_disconnected_label);
  466. _resource_preview_oops_label = new Gtk.Label(null);
  467. _resource_preview_oops_label.set_markup("Something went wrong.\rTry to <a href=\"restart\">restart this view</a>.");
  468. _resource_preview_oops_label.activate_link.connect(() => {
  469. restart_resource_preview.begin((obj, res) => {
  470. restart_resource_preview.end(res);
  471. });
  472. return true;
  473. });
  474. _resource_preview_stack.add(_resource_preview_oops_label);
  475. _inspector_stack = new Gtk.Stack();
  476. _inspector_stack_compiling_data_label = compiling_data_label();
  477. _inspector_stack.add(_inspector_stack_compiling_data_label);
  478. _inspector_stack_connecting_to_data_compiler_label = connecting_to_data_compiler_label();
  479. _inspector_stack.add(_inspector_stack_connecting_to_data_compiler_label);
  480. _inspector_stack_compiler_crashed_label = compiler_crashed_label();
  481. _inspector_stack.add(_inspector_stack_compiler_crashed_label);
  482. _inspector_stack_compiler_failed_compilation_label = compiler_failed_compilation_label();
  483. _inspector_stack.add(_inspector_stack_compiler_failed_compilation_label);
  484. Gtk.Builder builder = new Gtk.Builder.from_resource("/org/crown/level_editor/ui/toolbar.ui");
  485. _toolbar = builder.get_object("toolbar") as Gtk.Toolbar;
  486. _toolbar_run = builder.get_object("run") as Gtk.ToolButton;
  487. _editor_view_overlay = new Gtk.Overlay();
  488. _editor_view_overlay.add_overlay(_toolbar);
  489. _resource_popover = new Gtk.Popover(_toolbar);
  490. _resource_popover.key_press_event.connect((ev) => {
  491. if (ev.keyval == Gdk.Key.Escape) {
  492. // Do not transition-animate (i.e. call hide() instead of popdown()).
  493. _resource_popover.hide();
  494. return Gdk.EVENT_STOP;
  495. }
  496. return Gdk.EVENT_PROPAGATE;
  497. });
  498. _resource_popover.button_press_event.connect((ev) => {
  499. // Do not transition-animate (i.e. call hide() instead of popdown()).
  500. // See: https://gitlab.gnome.org/GNOME/gtk/-/blob/3.22.30/gtk/gtkpopover.c
  501. Gtk.Widget child = _resource_popover.get_child();
  502. Gtk.Widget event_widget = Gtk.get_event_widget(ev);
  503. if (child != null && ev.window == event_widget.get_window()) {
  504. Gtk.Allocation child_alloc;
  505. child.get_allocation(out child_alloc);
  506. if ((int)ev.x < child_alloc.x
  507. || (int)ev.x > child_alloc.x + child_alloc.width
  508. || (int)ev.y < child_alloc.y
  509. || (int)ev.y > child_alloc.y + child_alloc.height
  510. ) {
  511. _resource_popover.hide();
  512. }
  513. } else if (!event_widget.is_ancestor(_resource_popover)) {
  514. _resource_popover.hide();
  515. }
  516. return Gdk.EVENT_PROPAGATE;
  517. });
  518. _resource_popover.events |= Gdk.EventMask.STRUCTURE_MASK; // unmap_event
  519. _resource_popover.unmap_event.connect(() => {
  520. // Redraw the editor view when the popover is not on-screen anymore.
  521. device_frame_delayed(16, _editor);
  522. return Gdk.EVENT_PROPAGATE;
  523. });
  524. _resource_popover.delete_event.connect(() => {
  525. // Do not destroy the widget.
  526. _resource_popover.hide();
  527. return Gdk.EVENT_STOP;
  528. });
  529. _resource_popover.modal = true;
  530. _resource_chooser = new ResourceChooser(_project, _project_store, _resource_preview_stack, _resource_preview);
  531. _resource_chooser.resource_selected.connect(on_resource_browser_resource_selected);
  532. _resource_chooser.destroy.connect(() => {
  533. stop_resource_preview.begin((obj, res) => {
  534. stop_resource_preview.end(res);
  535. });
  536. });
  537. _resource_popover.add(_resource_chooser);
  538. _level_tree_view_notebook = new Notebook();
  539. _level_tree_view_notebook.show_border = false;
  540. _level_tree_view_notebook.append_page(_level_treeview, new Gtk.Image.from_icon_name("level-tree", IconSize.SMALL_TOOLBAR));
  541. _level_tree_view_notebook.append_page(_level_layers_treeview, new Gtk.Image.from_icon_name("level-layers", IconSize.SMALL_TOOLBAR));
  542. _editor_pane = new Gtk.Paned(Gtk.Orientation.HORIZONTAL);
  543. _editor_pane.pack1(_project_stack, false, false);
  544. _editor_pane.pack2(_editor_stack, true, false);
  545. _content_pane = new Gtk.Paned(Gtk.Orientation.VERTICAL);
  546. _content_pane.pack1(_editor_pane, true, false);
  547. _content_pane.pack2(_console_view, false, false);
  548. _inspector_pane = new Gtk.Paned(Gtk.Orientation.VERTICAL);
  549. _inspector_pane.pack1(_level_tree_view_notebook, true, false);
  550. _inspector_pane.pack2(_properties_view, false, false);
  551. _inspector_stack.add(_inspector_pane);
  552. _main_pane = new Gtk.Paned(Gtk.Orientation.HORIZONTAL);
  553. _main_pane.pack1(_content_pane, true, false);
  554. _main_pane.pack2(_inspector_stack, false, false);
  555. _statusbar = new Statusbar();
  556. _main_vbox = new Gtk.Box(Gtk.Orientation.VERTICAL, 0);
  557. _main_vbox.pack_start(_main_pane, true, true, 0);
  558. _main_vbox.pack_start(_statusbar, false, false, 0);
  559. _main_vbox.set_visible(true);
  560. _file_filter = new Gtk.FileFilter();
  561. _file_filter.set_filter_name("Level (*.level)");
  562. _file_filter.add_pattern("*.level");
  563. _user = new User();
  564. _panel_new_project = new PanelNewProject(this, _user, _project);
  565. _panel_new_project.fill_templates_list(_templates_dir.get_path());
  566. _panel_welcome = new PanelWelcome();
  567. _panel_projects_list = new PanelProjectsList(this, _user);
  568. _panel_welcome.pack_start(_panel_projects_list);
  569. _panel_welcome.set_visible(true); // To make Gtk.Stack work...
  570. _main_stack = new Gtk.Stack();
  571. _main_stack.add_named(_panel_welcome, "panel_welcome");
  572. _main_stack.add_named(_panel_new_project, "panel_new_project");
  573. _main_stack.add_named(_main_vbox, "main_vbox");
  574. _preferences_dialog.decode(_settings);
  575. // Delete expired logs
  576. if (_preferences_dialog._log_delete_after_days.value != 0) {
  577. try {
  578. FileEnumerator enumerator = _logs_dir.enumerate_children("standard::*"
  579. , FileQueryInfoFlags.NOFOLLOW_SYMLINKS
  580. );
  581. GLib.FileInfo info = null;
  582. while ((info = enumerator.next_file()) != null) {
  583. if (info.get_file_type() != GLib.FileType.REGULAR)
  584. continue; // Skip anything but regular files
  585. // Parse DateTime from log filename
  586. int year = 1970;
  587. int month = 1;
  588. int day = 1;
  589. if (info.get_name().scanf("%d-%d-%d.log", &year, &month, &day) != 3)
  590. continue; // Skip malformed filenames
  591. GLib.DateTime time_log = new GLib.DateTime.local(year, month, day, 0, 0, 0.0);
  592. if (time_log == null)
  593. continue; // Skip invalid dates
  594. GLib.DateTime time_now = new GLib.DateTime.now_local();
  595. if (time_now.difference(time_log) <= GLib.TimeSpan.DAY*_preferences_dialog._log_delete_after_days.value)
  596. continue; // Skip if date is within range
  597. // Delete
  598. GLib.File log_file = _logs_dir.resolve_relative_path(info.get_name());
  599. log_file.delete();
  600. }
  601. }
  602. catch (GLib.Error e) {
  603. loge(e.message);
  604. }
  605. }
  606. _user.load(_user_file.get_path());
  607. _console_view._entry_history.load(_console_history_file.get_path());
  608. if (_source_dir == null) {
  609. show_panel("panel_welcome");
  610. } else {
  611. show_panel("main_vbox");
  612. restart_backend.begin(_source_dir, _level_resource);
  613. }
  614. }
  615. protected override void activate()
  616. {
  617. if (this.active_window == null) {
  618. LevelEditorWindow win = new LevelEditorWindow(this);
  619. win.set_default_size(WINDOW_DEFAULT_WIDTH, WINDOW_DEFAULT_HEIGHT);
  620. win.add(_main_stack);
  621. try {
  622. win.icon = IconTheme.get_default().load_icon(CROWN_ICON_NAME, 256, 0);
  623. }
  624. catch (Error e) {
  625. loge(e.message);
  626. }
  627. }
  628. this.active_window.show_all();
  629. this.active_window.maximize();
  630. }
  631. protected override bool local_command_line(ref unowned string[] args, out int exit_status)
  632. {
  633. if (args.length > 1) {
  634. if (!GLib.FileUtils.test(args[1], FileTest.EXISTS) || !GLib.FileUtils.test(args[1], FileTest.IS_DIR)) {
  635. loge("Source directory does not exist or it is not a directory");
  636. exit_status = 1;
  637. return true;
  638. }
  639. _source_dir = args[1];
  640. }
  641. if (args.length > 2) {
  642. // Validation is done below after the Project object instantiation
  643. _level_resource = args[2];
  644. }
  645. exit_status = 0;
  646. return false;
  647. }
  648. protected override int command_line(ApplicationCommandLine command_line)
  649. {
  650. this.activate();
  651. return 0;
  652. }
  653. public ConsoleClient? current_selected_client()
  654. {
  655. if (_combo.get_active_id() == "editor")
  656. return _editor;
  657. else if (_combo.get_active_id() == "game")
  658. return _game;
  659. else
  660. return null;
  661. }
  662. private void on_resource_browser_resource_selected(string type, string name)
  663. {
  664. set_placeable(type, name);
  665. activate_action("tool", new GLib.Variant.string("place"));
  666. }
  667. private void on_data_compiler_connected(string address, int port)
  668. {
  669. logi("Connected to data_compiler@%s:%d".printf(address, port));
  670. _compiler.receive_async();
  671. }
  672. private void on_data_compiler_disconnected()
  673. {
  674. logi("Disconnected from data_compiler");
  675. if (_stop_data_compiler_callback != null)
  676. _stop_data_compiler_callback();
  677. }
  678. private async void on_data_compiler_disconnected_unexpected()
  679. {
  680. logw("Disconnected from data_compiler unexpectedly");
  681. try {
  682. if (_compiler_process != uint32.MAX)
  683. _subprocess_launcher.wait(_compiler_process);
  684. _compiler_process = uint32.MAX;
  685. } catch (GLib.Error e) {
  686. loge(e.message);
  687. }
  688. yield stop_heads();
  689. // Reset the callback
  690. _data_compiler.finished(false);
  691. _project_stack.set_visible_child(_project_stack_compiler_crashed_label);
  692. _editor_stack.set_visible_child(_editor_stack_compiler_crashed_label);
  693. _inspector_stack.set_visible_child(_inspector_stack_compiler_crashed_label);
  694. }
  695. private void on_editor_connected(string address, int port)
  696. {
  697. logi("Connected to level_editor@%s:%d".printf(address, port));
  698. // Start receiving data from the editor view.
  699. _editor.receive_async();
  700. // Update editor view with current editor state.
  701. _level.send_level();
  702. send_state();
  703. _preferences_dialog.apply();
  704. }
  705. private void on_editor_disconnected()
  706. {
  707. logi("Disconnected from editor");
  708. if (_stop_editor_callback != null)
  709. _stop_editor_callback();
  710. }
  711. private void on_editor_disconnected_unexpected()
  712. {
  713. logw("Disconnected from editor unexpectedly");
  714. try {
  715. if (_editor_process != uint32.MAX)
  716. _subprocess_launcher.wait(_editor_process);
  717. _editor_process = uint32.MAX;
  718. } catch (GLib.Error e) {
  719. loge(e.message);
  720. }
  721. _editor_stack.set_visible_child(_editor_stack_oops_label);
  722. }
  723. private void on_resource_preview_connected(string address, int port)
  724. {
  725. logi("Connected to preview@%s:%d".printf(address, port));
  726. // Start receiving data from the editor view.
  727. _resource_preview.receive_async();
  728. }
  729. private void on_resource_preview_disconnected()
  730. {
  731. logi("Disconnected from preview");
  732. if (_stop_resource_preview_callback != null)
  733. _stop_resource_preview_callback();
  734. }
  735. private void on_resource_preview_disconnected_unexpected()
  736. {
  737. logw("Disconnected from preview unexpectedly");
  738. try {
  739. if (_resource_preview_process != uint32.MAX)
  740. _subprocess_launcher.wait(_resource_preview_process);
  741. _resource_preview_process = uint32.MAX;
  742. } catch (GLib.Error e) {
  743. loge(e.message);
  744. }
  745. _resource_preview_stack.set_visible_child(_resource_preview_oops_label);
  746. }
  747. private void on_resource_preview_message_received(ConsoleClient client, uint8[] json)
  748. {
  749. // Ignore the message content.
  750. client.receive_async();
  751. }
  752. private void on_game_connected(string address, int port)
  753. {
  754. logi("Connected to game@%s:%d".printf(address, port));
  755. _game.receive_async();
  756. _combo.set_active_id("game");
  757. }
  758. private void on_game_disconnected()
  759. {
  760. logi("Disconnected from game");
  761. _project.delete_garbage();
  762. _combo.set_active_id("editor");
  763. _toolbar_run.icon_name = "game-run";
  764. if (_stop_game_callback != null)
  765. _stop_game_callback();
  766. }
  767. private void on_game_disconnected_externally()
  768. {
  769. on_game_disconnected();
  770. try {
  771. if (_game_process != uint32.MAX)
  772. _subprocess_launcher.wait(_game_process);
  773. _game_process = uint32.MAX;
  774. } catch (GLib.Error e) {
  775. loge(e.message);
  776. }
  777. }
  778. private void on_message_received(ConsoleClient client, uint8[] json)
  779. {
  780. Hashtable msg = JSON.decode(json) as Hashtable;
  781. string msg_type = msg["type"] as string;
  782. if (msg_type == "message") {
  783. log((string)msg["system"], (string)msg["severity"], (string)msg["message"]);
  784. } else if (msg_type == "add_file") {
  785. string path = (string)msg["path"];
  786. _project.add_file(path);
  787. } else if (msg_type == "remove_file") {
  788. string path = (string)msg["path"];
  789. _project.remove_file(path);
  790. } else if (msg_type == "add_tree") {
  791. string path = (string)msg["path"];
  792. _project.add_tree(path);
  793. } else if (msg_type == "remove_tree") {
  794. string path = (string)msg["path"];
  795. _project.remove_tree(path);
  796. } else if (msg_type == "compile") {
  797. // Guid id = Guid.parse((string)msg["id"]);
  798. if (msg.has_key("start")) {
  799. // FIXME
  800. } else if (msg.has_key("success")) {
  801. _data_compiler.finished((bool)msg["success"]);
  802. }
  803. } else if (msg_type == "unit_spawned") {
  804. string id = (string)msg["id"];
  805. string name = (string)msg["name"];
  806. ArrayList<Value?> pos = (ArrayList<Value?>)msg["position"];
  807. ArrayList<Value?> rot = (ArrayList<Value?>)msg["rotation"];
  808. ArrayList<Value?> scl = (ArrayList<Value?>)msg["scale"];
  809. _level.on_unit_spawned(Guid.parse(id)
  810. , name
  811. , Vector3.from_array(pos)
  812. , Quaternion.from_array(rot)
  813. , Vector3.from_array(scl)
  814. );
  815. } else if (msg_type == "sound_spawned") {
  816. string id = (string)msg["id"];
  817. string name = (string)msg["name"];
  818. ArrayList<Value?> pos = (ArrayList<Value?>)msg["position"];
  819. ArrayList<Value?> rot = (ArrayList<Value?>)msg["rotation"];
  820. ArrayList<Value?> scl = (ArrayList<Value?>)msg["scale"];
  821. double range = (double)msg["range"];
  822. double volume = (double)msg["volume"];
  823. bool loop = (bool)msg["loop"];
  824. _level.on_sound_spawned(Guid.parse(id)
  825. , name
  826. , Vector3.from_array(pos)
  827. , Quaternion.from_array(rot)
  828. , Vector3.from_array(scl)
  829. , range
  830. , volume
  831. , loop
  832. );
  833. } else if (msg_type == "move_objects") {
  834. Hashtable ids = (Hashtable)msg["ids"];
  835. Hashtable new_positions = (Hashtable)msg["new_positions"];
  836. Hashtable new_rotations = (Hashtable)msg["new_rotations"];
  837. Hashtable new_scales = (Hashtable)msg["new_scales"];
  838. ArrayList<string> keys = new ArrayList<string>.wrap(ids.keys.to_array());
  839. keys.sort(Gee.Functions.get_compare_func_for(typeof(string)));
  840. Guid[] n_ids = new Guid[keys.size];
  841. Vector3[] n_positions = new Vector3[keys.size];
  842. Quaternion[] n_rotations = new Quaternion[keys.size];
  843. Vector3[] n_scales = new Vector3[keys.size];
  844. for (int i = 0; i < keys.size; ++i) {
  845. string k = keys[i];
  846. n_ids[i] = Guid.parse((string)ids[k]);
  847. n_positions[i] = Vector3.from_array((ArrayList<Value?>)(new_positions[k]));
  848. n_rotations[i] = Quaternion.from_array((ArrayList<Value?>)new_rotations[k]);
  849. n_scales[i] = Vector3.from_array((ArrayList<Value?>)new_scales[k]);
  850. }
  851. _level.on_move_objects(n_ids, n_positions, n_rotations, n_scales);
  852. } else if (msg_type == "selection") {
  853. Hashtable objects = (Hashtable)msg["objects"];
  854. ArrayList<string> keys = new ArrayList<string>.wrap(objects.keys.to_array());
  855. keys.sort(Gee.Functions.get_compare_func_for(typeof(string)));
  856. Guid[] ids = new Guid[keys.size];
  857. for (int i = 0; i < keys.size; ++i) {
  858. string k = keys[i];
  859. ids[i] = Guid.parse((string)objects[k]);
  860. }
  861. _level.on_selection(ids);
  862. } else if (msg_type == "error") {
  863. loge((string)msg["message"]);
  864. } else {
  865. loge("Unknown message type: " + msg_type);
  866. }
  867. // Receive next message
  868. client.receive_async();
  869. }
  870. private void append_editor_state(StringBuilder sb)
  871. {
  872. // This state is common to any project.
  873. sb.append(LevelEditorApi.set_grid_size(_grid_size));
  874. sb.append(LevelEditorApi.set_rotation_snap(_rotation_snap));
  875. sb.append(LevelEditorApi.enable_show_grid(_show_grid));
  876. sb.append(LevelEditorApi.enable_snap_to_grid(_snap_to_grid));
  877. sb.append(LevelEditorApi.enable_debug_render_world(_debug_render_world));
  878. sb.append(LevelEditorApi.enable_debug_physics_world(_debug_physics_world));
  879. sb.append(LevelEditorApi.set_tool_type(_tool_type));
  880. sb.append(LevelEditorApi.set_snap_mode(_snap_mode));
  881. sb.append(LevelEditorApi.set_reference_system(_reference_system));
  882. }
  883. private void append_project_state(StringBuilder sb)
  884. {
  885. // This state is not guaranteed to be applicable to any project.
  886. if (_placeable_type != "")
  887. sb.append(LevelEditorApi.set_placeable(_placeable_type, _placeable_name));
  888. }
  889. private void send_state()
  890. {
  891. StringBuilder sb = new StringBuilder();
  892. append_editor_state(sb);
  893. append_project_state(sb);
  894. _editor.send_script(sb.str);
  895. _editor.send(DeviceApi.frame());
  896. }
  897. private bool on_button_press(Gdk.EventButton ev)
  898. {
  899. return Gdk.EVENT_STOP;
  900. }
  901. private bool on_button_release(Gdk.EventButton ev)
  902. {
  903. return Gdk.EVENT_STOP;
  904. }
  905. Gtk.Label compiling_data_label()
  906. {
  907. return new Gtk.Label("Compiling resources, please wait...");
  908. }
  909. Gtk.Label connecting_to_data_compiler_label()
  910. {
  911. return new Gtk.Label("Connecting to Data Compiler...");
  912. }
  913. Gtk.Label compiler_crashed_label()
  914. {
  915. Gtk.Label label = new Gtk.Label(null);
  916. label.set_markup("Data Compiler disconnected.\rTry to <a href=\"restart\">restart the compiler</a> to continue.");
  917. label.activate_link.connect(() => {
  918. restart_backend.begin(_project.source_dir(), _level._name != null ? _level._name : "");
  919. return true;
  920. });
  921. return label;
  922. }
  923. Gtk.Label compiler_failed_compilation_label()
  924. {
  925. Gtk.Label label = new Gtk.Label(null);
  926. label.set_markup("Data compilation failed.\rFix errors and <a href=\"restart\">restart the compiler</a> to continue.");
  927. label.activate_link.connect(() => {
  928. restart_backend.begin(_project.source_dir(), _level._name != null ? _level._name : "");
  929. return true;
  930. });
  931. return label;
  932. }
  933. public async void restart_backend(string source_dir, string level_name)
  934. {
  935. string sd = source_dir.dup();
  936. string ln = level_name.dup();
  937. yield stop_backend();
  938. // Reset project state.
  939. _placeable_type = "";
  940. _placeable_name = "";
  941. // Load project and level if any.
  942. logi("Loading project: `%s`...".printf(sd));
  943. _project.load(sd);
  944. // Spawn the data compiler.
  945. string args[] =
  946. {
  947. ENGINE_EXE,
  948. "--source-dir",
  949. _project.source_dir(),
  950. "--data-dir",
  951. _project.data_dir(),
  952. "--map-source-dir",
  953. "core",
  954. _project.toolchain_dir(),
  955. "--server",
  956. "--wait-console"
  957. };
  958. try {
  959. _compiler_process = _subprocess_launcher.spawnv_async(subprocess_flags(), args, ENGINE_DIR);
  960. } catch (Error e) {
  961. loge(e.message);
  962. }
  963. // It is an error if the data compiler. disconnects after here.
  964. _compiler.disconnected.disconnect(on_data_compiler_disconnected);
  965. _compiler.disconnected.connect(on_data_compiler_disconnected_unexpected);
  966. _project_stack.set_visible_child(_project_stack_connecting_to_data_compiler_label);
  967. _editor_stack.set_visible_child(_editor_stack_connecting_to_data_compiler_label);
  968. _inspector_stack.set_visible_child(_inspector_stack_connecting_to_data_compiler_label);
  969. int tries = yield _compiler.connect_async(DATA_COMPILER_ADDRESS
  970. , DATA_COMPILER_TCP_PORT
  971. , DATA_COMPILER_CONNECTION_TRIES
  972. , DATA_COMPILER_CONNECTION_INTERVAL
  973. );
  974. if (tries == DATA_COMPILER_CONNECTION_TRIES) {
  975. loge("Cannot connect to data_compiler");
  976. return;
  977. }
  978. _project_stack.set_visible_child(_project_stack_compiling_data_label);
  979. _editor_stack.set_visible_child(_editor_stack_compiling_data_label);
  980. _inspector_stack.set_visible_child(_inspector_stack_compiling_data_label);
  981. // Compile data.
  982. _data_compiler.compile.begin(_project.data_dir(), _project.platform(), (obj, res) => {
  983. if (_data_compiler.compile.end(res)) {
  984. load_level(ln);
  985. // If successful, start the level editor.
  986. restart_editor.begin((obj, res) => {
  987. restart_editor.end(res);
  988. _project_stack.set_visible_child(_project_browser);
  989. _inspector_stack.set_visible_child(_inspector_pane);
  990. });
  991. } else {
  992. _project_stack.set_visible_child(_project_stack_compiler_failed_compilation_label);
  993. _editor_stack.set_visible_child(_editor_stack_compiler_failed_compilation_label);
  994. _inspector_stack.set_visible_child(_inspector_stack_compiler_failed_compilation_label);
  995. }
  996. });
  997. }
  998. public async void stop_heads()
  999. {
  1000. yield stop_game();
  1001. yield stop_editor();
  1002. }
  1003. public async void stop_backend()
  1004. {
  1005. yield stop_heads();
  1006. yield stop_data_compiler();
  1007. _level.reset();
  1008. _project.reset();
  1009. this.active_window.title = LEVEL_EDITOR_WINDOW_TITLE;
  1010. }
  1011. private async void stop_data_compiler()
  1012. {
  1013. if (_compiler != null) {
  1014. // Reset "disconnected" signal.
  1015. _compiler.disconnected.disconnect(on_data_compiler_disconnected);
  1016. _compiler.disconnected.disconnect(on_data_compiler_disconnected_unexpected);
  1017. // Explicit call to this function should not produce error messages.
  1018. _compiler.disconnected.connect(on_data_compiler_disconnected);
  1019. if (_compiler.is_connected()) {
  1020. _stop_data_compiler_callback = stop_data_compiler.callback;
  1021. _compiler.send(DataCompilerApi.quit());
  1022. yield; // Wait for ConsoleClient to disconnect.
  1023. _stop_data_compiler_callback = null;
  1024. }
  1025. }
  1026. try {
  1027. if (_compiler_process != uint32.MAX)
  1028. _subprocess_launcher.wait(_compiler_process);
  1029. _compiler_process = uint32.MAX;
  1030. } catch (GLib.Error e) {
  1031. loge(e.message);
  1032. }
  1033. }
  1034. private async void start_editor(uint window_xid)
  1035. {
  1036. if (window_xid == 0)
  1037. return;
  1038. // Spawn the level editor.
  1039. string args[] =
  1040. {
  1041. ENGINE_EXE,
  1042. "--data-dir",
  1043. _project.data_dir(),
  1044. "--boot-dir",
  1045. LEVEL_EDITOR_BOOT_DIR,
  1046. "--parent-window",
  1047. window_xid.to_string(),
  1048. "--wait-console",
  1049. "--pumped"
  1050. };
  1051. try {
  1052. _editor_process = _subprocess_launcher.spawnv_async(subprocess_flags(), args, ENGINE_DIR);
  1053. } catch (Error e) {
  1054. loge(e.message);
  1055. }
  1056. // It is an error if the level editor disconnects after here.
  1057. _editor.disconnected.disconnect(on_editor_disconnected);
  1058. _editor.disconnected.connect(on_editor_disconnected_unexpected);
  1059. // Try to connect to the level editor.
  1060. int tries = yield _editor.connect_async(EDITOR_ADDRESS
  1061. , EDITOR_TCP_PORT
  1062. , EDITOR_CONNECTION_TRIES
  1063. , EDITOR_CONNECTION_INTERVAL
  1064. );
  1065. if (tries == EDITOR_CONNECTION_TRIES) {
  1066. loge("Cannot connect to level_editor");
  1067. return;
  1068. }
  1069. }
  1070. private async void start_resource_preview(uint window_xid)
  1071. {
  1072. if (window_xid == 0)
  1073. return;
  1074. // Spawn unit_preview.
  1075. string args[] =
  1076. {
  1077. ENGINE_EXE,
  1078. "--data-dir",
  1079. _project.data_dir(),
  1080. "--boot-dir",
  1081. UNIT_PREVIEW_BOOT_DIR,
  1082. "--parent-window",
  1083. window_xid.to_string(),
  1084. "--console-port",
  1085. UNIT_PREVIEW_TCP_PORT.to_string(),
  1086. "--wait-console",
  1087. "--pumped"
  1088. };
  1089. try {
  1090. _resource_preview_process = _subprocess_launcher.spawnv_async(subprocess_flags(), args, ENGINE_DIR);
  1091. } catch (Error e) {
  1092. loge(e.message);
  1093. }
  1094. // It is an error if the unit_preview disconnects after here.
  1095. _resource_preview.disconnected.disconnect(on_resource_preview_disconnected);
  1096. _resource_preview.disconnected.connect(on_resource_preview_disconnected_unexpected);
  1097. // Try to connect to unit_preview.
  1098. int tries = yield _resource_preview.connect_async(UNIT_PREVIEW_ADDRESS
  1099. , UNIT_PREVIEW_TCP_PORT
  1100. , EDITOR_CONNECTION_TRIES
  1101. , EDITOR_CONNECTION_INTERVAL
  1102. );
  1103. if (tries == EDITOR_CONNECTION_TRIES) {
  1104. loge("Cannot connect to unit_preview.");
  1105. return;
  1106. }
  1107. // FIXME: This should be done in ResourceChooser.
  1108. _resource_chooser._tree_view.set_cursor(new Gtk.TreePath.first(), null, false);
  1109. }
  1110. private async void stop_editor()
  1111. {
  1112. yield stop_resource_preview();
  1113. if (_editor != null) {
  1114. // Reset "disconnected" signal.
  1115. _editor.disconnected.disconnect(on_editor_disconnected);
  1116. _editor.disconnected.disconnect(on_editor_disconnected_unexpected);
  1117. // Explicit call to this function should not produce error messages.
  1118. _editor.disconnected.connect(on_editor_disconnected);
  1119. if (_editor.is_connected()) {
  1120. _stop_editor_callback = stop_editor.callback;
  1121. _editor.send_script("Device.quit()");
  1122. yield; // Wait for ConsoleClient to disconnect.
  1123. _stop_editor_callback = null;
  1124. _editor_stack.set_visible_child(_editor_stack_disconnected_label);
  1125. }
  1126. }
  1127. try {
  1128. if (_editor_process != uint32.MAX)
  1129. _subprocess_launcher.wait(_editor_process);
  1130. _editor_process = uint32.MAX;
  1131. } catch (GLib.Error e) {
  1132. loge(e.message);
  1133. }
  1134. }
  1135. public async void stop_resource_preview()
  1136. {
  1137. if (_resource_preview != null) {
  1138. // Reset "disconnected" signal.
  1139. _resource_preview.disconnected.disconnect(on_resource_preview_disconnected);
  1140. _resource_preview.disconnected.disconnect(on_resource_preview_disconnected_unexpected);
  1141. // Explicit call to this function should not produce error messages.
  1142. _resource_preview.disconnected.connect(on_resource_preview_disconnected);
  1143. if (_resource_preview.is_connected()) {
  1144. _stop_resource_preview_callback = stop_resource_preview.callback;
  1145. _resource_preview.send_script("Device.quit()");
  1146. yield; // Wait for ConsoleClient to disconnect.
  1147. _stop_resource_preview_callback = null;
  1148. _resource_preview_stack.set_visible_child(_resource_preview_disconnected_label);
  1149. }
  1150. }
  1151. try {
  1152. if (_resource_preview_process != uint32.MAX)
  1153. _subprocess_launcher.wait(_resource_preview_process);
  1154. _resource_preview_process = uint32.MAX;
  1155. } catch (GLib.Error e) {
  1156. loge(e.message);
  1157. }
  1158. }
  1159. private async void restart_editor()
  1160. {
  1161. yield stop_editor();
  1162. if (_editor_view != null) {
  1163. _editor_view_overlay.remove(_editor_view);
  1164. _editor_stack.remove(_editor_view_overlay);
  1165. _editor_view = null;
  1166. }
  1167. _editor_view = new EditorView(_editor);
  1168. _editor_view.realized.connect(on_editor_view_realized);
  1169. _editor_view.button_press_event.connect(on_button_press);
  1170. _editor_view.button_release_event.connect(on_button_release);
  1171. _editor_view_overlay.add(_editor_view);
  1172. _editor_view_overlay.show_all();
  1173. _editor_stack.add(_editor_view_overlay);
  1174. _editor_stack.set_visible_child(_editor_view_overlay);
  1175. yield restart_resource_preview();
  1176. }
  1177. private async void restart_resource_preview()
  1178. {
  1179. yield stop_resource_preview();
  1180. if (_resource_preview_view != null) {
  1181. _resource_preview_stack.remove(_resource_preview_view);
  1182. _resource_preview_view = null;
  1183. }
  1184. _resource_preview_view = new EditorView(_resource_preview, false);
  1185. _resource_preview_view.set_size_request(300, 300);
  1186. _resource_preview_view.realized.connect(on_resource_preview_view_realized);
  1187. _resource_preview_view.show_all();
  1188. _resource_preview_stack.add(_resource_preview_view);
  1189. _resource_preview_stack.set_visible_child(_resource_preview_view);
  1190. }
  1191. private async void start_game(StartGame sg)
  1192. {
  1193. // Save test level to file
  1194. _database.dump(_project._level_editor_test_level.get_path(), _level._id);
  1195. // Save temporary package to reference test level
  1196. ArrayList<Value?> level = new ArrayList<Value?>();
  1197. level.add("_level_editor_test");
  1198. Hashtable package = new Hashtable();
  1199. package["level"] = level;
  1200. SJSON.save(package, _project._level_editor_test_package.get_path());
  1201. bool success = yield _data_compiler.compile(_project.data_dir(), _project.platform());
  1202. if (!success) {
  1203. _toolbar_run.icon_name = "game-run";
  1204. return;
  1205. }
  1206. // Spawn the game.
  1207. string args[] =
  1208. {
  1209. ENGINE_EXE,
  1210. "--data-dir",
  1211. _project.data_dir(),
  1212. "--console-port",
  1213. GAME_TCP_PORT.to_string(),
  1214. "--wait-console",
  1215. "--lua-string",
  1216. sg == StartGame.TEST ? "TEST=true" : ""
  1217. };
  1218. try {
  1219. _game_process = _subprocess_launcher.spawnv_async(subprocess_flags(), args, ENGINE_DIR);
  1220. } catch (Error e) {
  1221. loge(e.message);
  1222. }
  1223. // Try to connect to the game.
  1224. int tries = yield _game.connect_async(GAME_ADDRESS
  1225. , GAME_TCP_PORT
  1226. , GAME_CONNECTION_TRIES
  1227. , GAME_CONNECTION_INTERVAL
  1228. );
  1229. if (tries == GAME_CONNECTION_TRIES) {
  1230. loge("Cannot connect to game");
  1231. return;
  1232. }
  1233. }
  1234. private async void stop_game()
  1235. {
  1236. if (_game != null) {
  1237. // Reset "disconnected" signal.
  1238. _game.disconnected.disconnect(on_game_disconnected);
  1239. _game.disconnected.disconnect(on_game_disconnected_externally);
  1240. // Explicit call to this function should not produce error messages.
  1241. _game.disconnected.connect(on_game_disconnected);
  1242. if (_game.is_connected()) {
  1243. _stop_game_callback = stop_game.callback;
  1244. _game.send_script("Device.quit()");
  1245. yield; // Wait for SocketClient to disconnect.
  1246. _stop_game_callback = null;
  1247. }
  1248. }
  1249. try {
  1250. if (_game_process != uint32.MAX)
  1251. _subprocess_launcher.wait(_game_process);
  1252. _game_process = uint32.MAX;
  1253. } catch (GLib.Error e) {
  1254. loge(e.message);
  1255. }
  1256. }
  1257. private void deploy_game()
  1258. {
  1259. Gtk.FileChooserDialog fcd = new Gtk.FileChooserDialog("Select destination directory..."
  1260. , this.active_window
  1261. , FileChooserAction.SELECT_FOLDER
  1262. , "Cancel"
  1263. , ResponseType.CANCEL
  1264. , "Open"
  1265. , ResponseType.ACCEPT
  1266. );
  1267. if (fcd.run() == ResponseType.ACCEPT) {
  1268. GLib.File data_dir = File.new_for_path(fcd.get_filename());
  1269. string args[] =
  1270. {
  1271. ENGINE_EXE,
  1272. "--source-dir",
  1273. _project.source_dir(),
  1274. "--map-source-dir",
  1275. "core",
  1276. _project.toolchain_dir(),
  1277. "--data-dir",
  1278. data_dir.get_path(),
  1279. "--compile"
  1280. };
  1281. try {
  1282. uint32 compiler = _subprocess_launcher.spawnv_async(subprocess_flags(), args, ENGINE_DIR);
  1283. int exit_status = _subprocess_launcher.wait(compiler);
  1284. if (exit_status == 0) {
  1285. string game_name = DEPLOY_DEFAULT_NAME;
  1286. GLib.File engine_exe_src = File.new_for_path(DEPLOY_EXE);
  1287. GLib.File engine_exe_dst = File.new_for_path(Path.build_filename(data_dir.get_path(), game_name + EXE_SUFFIX));
  1288. engine_exe_src.copy(engine_exe_dst, FileCopyFlags.OVERWRITE);
  1289. #if CROWN_PLATFORM_WINDOWS
  1290. string lua51_name = "lua51.dll";
  1291. GLib.File lua51_dll_src = File.new_for_path(lua51_name);
  1292. GLib.File lua51_dll_dst = File.new_for_path(Path.build_filename(data_dir.get_path(), lua51_name));
  1293. lua51_dll_src.copy(lua51_dll_dst, FileCopyFlags.OVERWRITE);
  1294. string openal_name = "openal-release.dll";
  1295. GLib.File openal_dll_src = File.new_for_path(openal_name);
  1296. GLib.File openal_dll_dst = File.new_for_path(Path.build_filename(data_dir.get_path(), openal_name));
  1297. openal_dll_src.copy(openal_dll_dst, FileCopyFlags.OVERWRITE);
  1298. #endif
  1299. logi("Project deployed to `%s`".printf(data_dir.get_path()));
  1300. }
  1301. }
  1302. catch (Error e) {
  1303. loge("%s".printf(e.message));
  1304. loge("Failed to deploy project");
  1305. }
  1306. }
  1307. fcd.destroy();
  1308. }
  1309. private async void on_editor_view_realized()
  1310. {
  1311. start_editor.begin(_editor_view.window_id);
  1312. }
  1313. private async void on_resource_preview_view_realized()
  1314. {
  1315. start_resource_preview.begin(_resource_preview_view.window_id);
  1316. }
  1317. private void on_tool_changed(GLib.SimpleAction action, GLib.Variant? param)
  1318. {
  1319. string name = param.get_string();
  1320. if (name == "place") {
  1321. // Store previous tool for it to be restored later.
  1322. if (_tool_type != LevelEditorApi.ToolType.PLACE)
  1323. _tool_type_prev = _tool_type;
  1324. _tool_type = LevelEditorApi.ToolType.PLACE;
  1325. } else if (name == "move") {
  1326. _tool_type = LevelEditorApi.ToolType.MOVE;
  1327. } else if (name == "rotate") {
  1328. _tool_type = LevelEditorApi.ToolType.ROTATE;
  1329. } else if (name == "scale") {
  1330. _tool_type = LevelEditorApi.ToolType.SCALE;
  1331. }
  1332. _editor_view.grab_focus();
  1333. send_state();
  1334. action.set_state(param);
  1335. }
  1336. private void on_snap_mode_changed(GLib.SimpleAction action, GLib.Variant? param)
  1337. {
  1338. string name = param.get_string();
  1339. if (name == "relative")
  1340. _snap_mode = LevelEditorApi.SnapMode.RELATIVE;
  1341. else if (name == "absolute")
  1342. _snap_mode = LevelEditorApi.SnapMode.ABSOLUTE;
  1343. send_state();
  1344. action.set_state(param);
  1345. }
  1346. private void on_reference_system_changed(GLib.SimpleAction action, GLib.Variant? param)
  1347. {
  1348. string name = param.get_string();
  1349. if (name == "local")
  1350. _reference_system = LevelEditorApi.ReferenceSystem.LOCAL;
  1351. else if (name == "world")
  1352. _reference_system = LevelEditorApi.ReferenceSystem.WORLD;
  1353. send_state();
  1354. action.set_state(param);
  1355. }
  1356. private void on_grid_changed(GLib.SimpleAction action, GLib.Variant? param)
  1357. {
  1358. _grid_size = float.parse(param.get_string());
  1359. send_state();
  1360. action.set_state(param);
  1361. }
  1362. private void on_rotation_snap_changed(GLib.SimpleAction action, GLib.Variant? param)
  1363. {
  1364. _rotation_snap = float.parse(param.get_string());
  1365. send_state();
  1366. action.set_state(param);
  1367. }
  1368. private void new_level()
  1369. {
  1370. _level.load_from_path(LEVEL_EMPTY);
  1371. _level.send_level();
  1372. }
  1373. private void update_active_window_title()
  1374. {
  1375. string title = "";
  1376. if (_level._name != null) {
  1377. title += (_level._name == LEVEL_EMPTY) ? "untitled" : _level._name;
  1378. if (_database.changed())
  1379. title += " • ";
  1380. title += " - ";
  1381. }
  1382. title += LEVEL_EDITOR_WINDOW_TITLE;
  1383. if (this.active_window.title != title)
  1384. this.active_window.title = title;
  1385. }
  1386. private void load_level(string name)
  1387. {
  1388. if (name == _level._name)
  1389. return;
  1390. string resource_name = name != "" ? name : LEVEL_EMPTY;
  1391. if (_level.load_from_path(resource_name) != 0) {
  1392. loge("Unable to load level %s".printf(resource_name));
  1393. return;
  1394. }
  1395. if (_editor.is_connected()) {
  1396. _level.send_level();
  1397. send_state();
  1398. }
  1399. update_active_window_title();
  1400. }
  1401. private bool save_as(string? filename)
  1402. {
  1403. string path = filename;
  1404. if (path == null) {
  1405. Gtk.FileChooserDialog fcd = new Gtk.FileChooserDialog("Save As..."
  1406. , this.active_window
  1407. , FileChooserAction.SAVE
  1408. , "Cancel"
  1409. , ResponseType.CANCEL
  1410. , "Save"
  1411. , ResponseType.ACCEPT
  1412. );
  1413. fcd.add_filter(_file_filter);
  1414. fcd.set_current_folder(_project.source_dir());
  1415. int rt = ResponseType.CANCEL;
  1416. do {
  1417. // Select the file
  1418. rt = fcd.run();
  1419. if (rt != ResponseType.ACCEPT) {
  1420. fcd.destroy();
  1421. return false;
  1422. }
  1423. path = fcd.get_filename();
  1424. // Append file extension
  1425. if (!path.has_suffix(".level"))
  1426. path += ".level";
  1427. // Check if the file is within the source directory
  1428. if (!_project.path_is_within_source_dir(path)) {
  1429. Gtk.MessageDialog md = new Gtk.MessageDialog(fcd
  1430. , DialogFlags.MODAL
  1431. , MessageType.WARNING
  1432. , Gtk.ButtonsType.OK
  1433. , "The file must be within the source directory."
  1434. );
  1435. md.set_default_response(ResponseType.OK);
  1436. md.run();
  1437. md.destroy();
  1438. fcd.set_current_folder(_project.source_dir());
  1439. continue;
  1440. }
  1441. // Check if the file already exists
  1442. rt = ResponseType.YES;
  1443. if (GLib.FileUtils.test(path, FileTest.EXISTS)) {
  1444. Gtk.MessageDialog md = new Gtk.MessageDialog(fcd
  1445. , DialogFlags.MODAL
  1446. , MessageType.QUESTION
  1447. , Gtk.ButtonsType.YES_NO
  1448. , "A file named `%s` already exists.\nOverwrite?".printf(_project.basename(path))
  1449. );
  1450. md.set_default_response(ResponseType.NO);
  1451. rt = md.run();
  1452. md.destroy();
  1453. }
  1454. } while (rt != ResponseType.YES);
  1455. fcd.destroy();
  1456. }
  1457. // Save level
  1458. string resource_filename = _project.absolute_path_to_resource_filename(path);
  1459. string resource_path = _project.resource_filename_to_resource_path(resource_filename);
  1460. string resource_name = _project.resource_path_to_resource_name(resource_path);
  1461. _level.save(resource_name);
  1462. _statusbar.set_temporary_message("Saved %s".printf(_level._path));
  1463. update_active_window_title();
  1464. return true;
  1465. }
  1466. private bool save()
  1467. {
  1468. return save_as(_level._path);
  1469. }
  1470. private bool save_timeout()
  1471. {
  1472. if (_level._path != null)
  1473. save();
  1474. return GLib.Source.CONTINUE;
  1475. }
  1476. protected override void shutdown()
  1477. {
  1478. // Disable auto-save.
  1479. if (_save_timer_id > 0)
  1480. GLib.Source.remove(_save_timer_id);
  1481. // Save editor settings.
  1482. _user.save(_user_file.get_path());
  1483. _preferences_dialog.encode(_settings);
  1484. SJSON.save(_settings, _settings_file.get_path());
  1485. _console_view._entry_history.save(_console_history_file.get_path());
  1486. // Destroy widgets.
  1487. if (_resource_chooser != null)
  1488. _resource_chooser.destroy();
  1489. if (_preferences_dialog != null)
  1490. _preferences_dialog.destroy();
  1491. base.shutdown();
  1492. }
  1493. // Returns true if the level has been saved or the user decided it
  1494. // should be discarded.
  1495. public bool should_quit()
  1496. {
  1497. int rt = ResponseType.YES;
  1498. if (_database.changed())
  1499. rt = run_level_changed_dialog(this.active_window);
  1500. if (!_database.changed() || rt == ResponseType.YES && save() || rt == ResponseType.NO)
  1501. return true;
  1502. return false;
  1503. }
  1504. private void on_new_level(GLib.SimpleAction action, GLib.Variant? param)
  1505. {
  1506. int rt = ResponseType.YES;
  1507. if (_database.changed())
  1508. rt = run_level_changed_dialog(this.active_window);
  1509. if (!_database.changed() || rt == ResponseType.YES && save() || rt == ResponseType.NO) {
  1510. new_level();
  1511. send_state();
  1512. }
  1513. }
  1514. private void on_open_level_from_menubar(GLib.SimpleAction action, GLib.Variant? param)
  1515. {
  1516. string path = "";
  1517. Gtk.FileChooserDialog fcd = new Gtk.FileChooserDialog("Open Level..."
  1518. , this.active_window
  1519. , FileChooserAction.OPEN
  1520. , "Cancel"
  1521. , ResponseType.CANCEL
  1522. , "Open"
  1523. , ResponseType.ACCEPT
  1524. );
  1525. fcd.add_filter(_file_filter);
  1526. fcd.set_current_folder(_project.source_dir());
  1527. int err = 1;
  1528. int rt = ResponseType.CANCEL;
  1529. do {
  1530. // Select the file
  1531. rt = fcd.run();
  1532. if (rt != ResponseType.ACCEPT) {
  1533. fcd.destroy();
  1534. return;
  1535. }
  1536. path = fcd.get_filename();
  1537. err = 0;
  1538. // Append file extension
  1539. if (!path.has_suffix(".level"))
  1540. path += ".level";
  1541. if (!_project.path_is_within_source_dir(path)) {
  1542. Gtk.MessageDialog md = new Gtk.MessageDialog(fcd
  1543. , DialogFlags.MODAL
  1544. , MessageType.WARNING
  1545. , Gtk.ButtonsType.OK
  1546. , "The file must be within the source directory."
  1547. );
  1548. md.set_default_response(ResponseType.OK);
  1549. md.run();
  1550. md.destroy();
  1551. fcd.set_current_folder(_project.source_dir());
  1552. err = 1;
  1553. continue;
  1554. }
  1555. } while (err != 0);
  1556. fcd.destroy();
  1557. assert(path != "");
  1558. // Load level
  1559. string resource_filename = _project.absolute_path_to_resource_filename(path);
  1560. string resource_path = _project.resource_filename_to_resource_path(resource_filename);
  1561. string resource_name = _project.resource_path_to_resource_name(resource_path);
  1562. load_level(resource_name);
  1563. }
  1564. private void on_open_level(GLib.SimpleAction action, GLib.Variant? param)
  1565. {
  1566. int rt = ResponseType.YES;
  1567. string level_name = param.get_string();
  1568. if (level_name != "" && level_name == _level._name)
  1569. return;
  1570. if (_database.changed())
  1571. rt = run_level_changed_dialog(this.active_window);
  1572. if (!_database.changed() || rt == ResponseType.YES && save() || rt == ResponseType.NO) {
  1573. if (level_name != "")
  1574. load_level(level_name);
  1575. else // Action invoked from menubar File > Open Level...
  1576. on_open_level_from_menubar(action, param);
  1577. }
  1578. }
  1579. private void on_open_project(GLib.SimpleAction action, GLib.Variant? param)
  1580. {
  1581. int rt = ResponseType.YES;
  1582. if (_database.changed())
  1583. rt = run_level_changed_dialog(this.active_window);
  1584. if (!_database.changed() || rt == ResponseType.YES && save() || rt == ResponseType.NO) {
  1585. string source_dir;
  1586. source_dir = param.get_string();
  1587. if (source_dir == "") {
  1588. // Project opened from menubar.
  1589. rt = run_open_project_dialog(out source_dir, this.active_window);
  1590. if (rt != ResponseType.ACCEPT)
  1591. return;
  1592. }
  1593. if (_project.source_dir() == source_dir)
  1594. return;
  1595. this.show_panel("main_vbox", Gtk.StackTransitionType.NONE);
  1596. _user.add_or_touch_recent_project(source_dir, source_dir);
  1597. restart_backend.begin(source_dir, LEVEL_NONE);
  1598. }
  1599. }
  1600. private void on_new_project(GLib.SimpleAction action, GLib.Variant? param)
  1601. {
  1602. int rt = ResponseType.YES;
  1603. if (_database.changed())
  1604. rt = run_level_changed_dialog(this.active_window);
  1605. if (!_database.changed() || rt == ResponseType.YES && save() || rt == ResponseType.NO) {
  1606. stop_backend.begin((obj, res) => {
  1607. stop_backend.end(res);
  1608. show_panel("panel_new_project");
  1609. });
  1610. }
  1611. }
  1612. private void on_save(GLib.SimpleAction action, GLib.Variant? param)
  1613. {
  1614. save();
  1615. }
  1616. private void on_save_as(GLib.SimpleAction action, GLib.Variant? param)
  1617. {
  1618. save_as(null);
  1619. }
  1620. private void on_import(GLib.SimpleAction action, GLib.Variant? param)
  1621. {
  1622. string destination_dir = param.get_string();
  1623. _project.import(destination_dir != "" ? destination_dir : null, this.active_window);
  1624. }
  1625. private void on_preferences(GLib.SimpleAction action, GLib.Variant? param)
  1626. {
  1627. _preferences_dialog.set_transient_for(this.active_window);
  1628. _preferences_dialog.set_position(Gtk.WindowPosition.CENTER_ON_PARENT);
  1629. _preferences_dialog.show_all();
  1630. }
  1631. private void on_deploy(GLib.SimpleAction action, GLib.Variant? param)
  1632. {
  1633. deploy_game();
  1634. }
  1635. private int run_level_changed_dialog(Gtk.Window? parent)
  1636. {
  1637. Gtk.MessageDialog md = new Gtk.MessageDialog(parent
  1638. , Gtk.DialogFlags.MODAL
  1639. , Gtk.MessageType.WARNING
  1640. , Gtk.ButtonsType.NONE
  1641. , "Save changes to Level before closing?"
  1642. );
  1643. md.add_button("Close _without Saving", ResponseType.NO);
  1644. md.add_button("_Cancel", ResponseType.CANCEL);
  1645. md.add_button("_Save", ResponseType.YES);
  1646. md.set_default_response(ResponseType.YES);
  1647. int rt = md.run();
  1648. md.destroy();
  1649. return rt;
  1650. }
  1651. public int run_open_project_dialog(out string source_dir, Gtk.Window? parent)
  1652. {
  1653. Gtk.FileChooserDialog fcd = new Gtk.FileChooserDialog("Open Project..."
  1654. , parent
  1655. , FileChooserAction.SELECT_FOLDER
  1656. , "Cancel"
  1657. , ResponseType.CANCEL
  1658. , "Open"
  1659. , ResponseType.ACCEPT
  1660. );
  1661. int rt = fcd.run();
  1662. source_dir = fcd.get_filename();
  1663. fcd.destroy();
  1664. return rt;
  1665. }
  1666. private void on_close(GLib.SimpleAction action, GLib.Variant? param)
  1667. {
  1668. int rt = ResponseType.YES;
  1669. if (_database.changed())
  1670. rt = run_level_changed_dialog(this.active_window);
  1671. if (!_database.changed() || rt == ResponseType.YES && save() || rt == ResponseType.NO) {
  1672. stop_backend.begin((obj, res) => {
  1673. stop_backend.end(res);
  1674. show_panel("panel_welcome");
  1675. });
  1676. }
  1677. }
  1678. public void stop_backend_and_quit()
  1679. {
  1680. stop_backend.begin((obj, res) => {
  1681. stop_backend.end(res);
  1682. this.quit();
  1683. });
  1684. }
  1685. private void on_quit(GLib.SimpleAction action, GLib.Variant? param)
  1686. {
  1687. if (should_quit())
  1688. stop_backend_and_quit();
  1689. }
  1690. public static bool is_image_file(string path)
  1691. {
  1692. return path.has_suffix(".png")
  1693. || path.has_suffix(".tga")
  1694. ;
  1695. }
  1696. private void on_open_resource(GLib.SimpleAction action, GLib.Variant? param)
  1697. {
  1698. if (param == null)
  1699. return;
  1700. string resource_path = param.get_string();
  1701. string? type = Crown.resource_type(resource_path);
  1702. string? name = Crown.resource_name(type, resource_path);
  1703. if (type == null || name == null)
  1704. return;
  1705. if (type == "level") {
  1706. activate_action("open-level", name);
  1707. return;
  1708. }
  1709. GLib.AppInfo? app = null;
  1710. if (type == "lua") {
  1711. app = _preferences_dialog._lua_external_tool_button.get_app_info();
  1712. } else if (is_image_file(resource_path)) {
  1713. app = _preferences_dialog._image_external_tool_button.get_app_info();
  1714. }
  1715. try {
  1716. GLib.File file = GLib.File.new_for_path(_project.resource_path_to_absolute_path(resource_path));
  1717. if (app == null)
  1718. app = file.query_default_handler();
  1719. GLib.List<GLib.File> files = new GLib.List<GLib.File>();
  1720. files.append(file);
  1721. app.launch(files, null);
  1722. }
  1723. catch (Error e) {
  1724. loge(e.message);
  1725. }
  1726. }
  1727. private void on_show_grid(GLib.SimpleAction action, GLib.Variant? param)
  1728. {
  1729. _show_grid = !action.get_state().get_boolean();
  1730. send_state();
  1731. action.set_state(new GLib.Variant.boolean(_show_grid));
  1732. }
  1733. private void on_custom_grid()
  1734. {
  1735. Gtk.Dialog dg = new Gtk.Dialog.with_buttons("Grid size"
  1736. , this.active_window
  1737. , DialogFlags.MODAL
  1738. , "Cancel"
  1739. , ResponseType.CANCEL
  1740. , "Ok"
  1741. , ResponseType.OK
  1742. , null
  1743. );
  1744. EntryDouble sb = new EntryDouble(_grid_size, 0.1, 1000);
  1745. sb.activate.connect(() => { dg.response(ResponseType.OK); });
  1746. dg.get_content_area().add(sb);
  1747. dg.skip_taskbar_hint = true;
  1748. dg.show_all();
  1749. if (dg.run() == ResponseType.OK) {
  1750. _grid_size = sb.value;
  1751. send_state();
  1752. }
  1753. dg.destroy();
  1754. }
  1755. private void on_rotation_snap(GLib.SimpleAction action, GLib.Variant? param)
  1756. {
  1757. Gtk.Dialog dg = new Gtk.Dialog.with_buttons("Rotation snap"
  1758. , this.active_window
  1759. , DialogFlags.MODAL
  1760. , "Cancel"
  1761. , ResponseType.CANCEL
  1762. , "Ok"
  1763. , ResponseType.OK
  1764. , null
  1765. );
  1766. EntryDouble sb = new EntryDouble(_rotation_snap, 1.0, 180.0);
  1767. sb.activate.connect(() => { dg.response(ResponseType.OK); });
  1768. dg.get_content_area().add(sb);
  1769. dg.skip_taskbar_hint = true;
  1770. dg.show_all();
  1771. if (dg.run() == ResponseType.OK) {
  1772. _rotation_snap = sb.value;
  1773. send_state();
  1774. }
  1775. dg.destroy();
  1776. }
  1777. private void on_create_primitive(GLib.SimpleAction action, GLib.Variant? param)
  1778. {
  1779. if (action.name == "primitive-cube")
  1780. set_placeable("unit", "core/units/primitives/cube");
  1781. else if (action.name == "primitive-sphere")
  1782. set_placeable("unit", "core/units/primitives/sphere");
  1783. else if (action.name == "primitive-cone")
  1784. set_placeable("unit", "core/units/primitives/cone");
  1785. else if (action.name == "primitive-cylinder")
  1786. set_placeable("unit", "core/units/primitives/cylinder");
  1787. else if (action.name == "primitive-plane")
  1788. set_placeable("unit", "core/units/primitives/plane");
  1789. else if (action.name == "camera")
  1790. set_placeable("unit", "core/units/camera");
  1791. else if (action.name == "light")
  1792. set_placeable("unit", "core/units/light");
  1793. else if (action.name == "sound-source")
  1794. set_placeable("sound", "");
  1795. activate_action("tool", new GLib.Variant.string("place"));
  1796. }
  1797. private void on_create_unit(GLib.SimpleAction action, GLib.Variant? param)
  1798. {
  1799. _level.spawn_empty_unit();
  1800. }
  1801. private void on_camera_view(GLib.SimpleAction action, GLib.Variant? param)
  1802. {
  1803. string name = param.get_string();
  1804. if (name == "perspective")
  1805. _editor.send_script("LevelEditor:camera_view_perspective()");
  1806. else if (name == "front")
  1807. _editor.send_script("LevelEditor:camera_view_front()");
  1808. else if (name == "back")
  1809. _editor.send_script("LevelEditor:camera_view_back()");
  1810. else if (name == "right")
  1811. _editor.send_script("LevelEditor:camera_view_right()");
  1812. else if (name == "left")
  1813. _editor.send_script("LevelEditor:camera_view_left()");
  1814. else if (name == "top")
  1815. _editor.send_script("LevelEditor:camera_view_top()");
  1816. else if (name == "bottom")
  1817. _editor.send_script("LevelEditor:camera_view_bottom()");
  1818. _editor.send(DeviceApi.frame());
  1819. action.set_state(param);
  1820. }
  1821. private void on_resource_chooser(GLib.SimpleAction action, GLib.Variant? param)
  1822. {
  1823. _resource_popover.show_all();
  1824. }
  1825. private void on_project_browser(GLib.SimpleAction action, GLib.Variant? param)
  1826. {
  1827. if (_project_stack.is_visible()) {
  1828. _project_stack.hide();
  1829. } else {
  1830. _project_stack.show_all();
  1831. }
  1832. }
  1833. private void on_console(GLib.SimpleAction action, GLib.Variant? param)
  1834. {
  1835. if (_console_view.is_visible()) {
  1836. if (_console_view._entry.has_focus)
  1837. _console_view.hide();
  1838. else
  1839. _console_view._entry.grab_focus_without_selecting();
  1840. } else {
  1841. _console_view.show_all();
  1842. _console_view._entry.grab_focus_without_selecting();
  1843. }
  1844. }
  1845. private void on_statusbar(GLib.SimpleAction action, GLib.Variant? param)
  1846. {
  1847. if (_statusbar.is_visible()) {
  1848. _statusbar.hide();
  1849. } else {
  1850. _statusbar.show_all();
  1851. }
  1852. }
  1853. private void on_inspector(GLib.SimpleAction action, GLib.Variant? param)
  1854. {
  1855. if (_inspector_stack.is_visible()) {
  1856. _inspector_stack.hide();
  1857. } else {
  1858. _inspector_stack.show_all();
  1859. }
  1860. }
  1861. private void on_restart_editor_view(GLib.SimpleAction action, GLib.Variant? param)
  1862. {
  1863. restart_editor.begin((obj, res) => {
  1864. restart_editor.end(res);
  1865. });
  1866. }
  1867. private void on_build_data(GLib.SimpleAction action, GLib.Variant? param)
  1868. {
  1869. _data_compiler.compile.begin(_project.data_dir(), _project.platform(), (obj, res) => {
  1870. _data_compiler.compile.end(res);
  1871. });
  1872. }
  1873. private void on_refresh_lua(GLib.SimpleAction action, GLib.Variant? param)
  1874. {
  1875. _data_compiler.compile.begin(_project.data_dir(), _project.platform(), (obj, res) => {
  1876. if (_data_compiler.compile.end(res)) {
  1877. _editor.send(DeviceApi.refresh());
  1878. _editor.send(DeviceApi.frame());
  1879. _game.send(DeviceApi.refresh());
  1880. _game.send(DeviceApi.frame());
  1881. }
  1882. });
  1883. }
  1884. private void on_snap_to_grid(GLib.SimpleAction action, GLib.Variant? param)
  1885. {
  1886. _snap_to_grid = !action.get_state().get_boolean();
  1887. send_state();
  1888. action.set_state(new GLib.Variant.boolean(_snap_to_grid));
  1889. }
  1890. private void on_debug_render_world(GLib.SimpleAction action, GLib.Variant? param)
  1891. {
  1892. _debug_render_world = !action.get_state().get_boolean();
  1893. send_state();
  1894. action.set_state(new GLib.Variant.boolean(_debug_render_world));
  1895. }
  1896. private void on_debug_physics_world(GLib.SimpleAction action, GLib.Variant? param)
  1897. {
  1898. _debug_physics_world = !action.get_state().get_boolean();
  1899. send_state();
  1900. action.set_state(new GLib.Variant.boolean(_debug_physics_world));
  1901. }
  1902. private void on_run_game(GLib.SimpleAction action, GLib.Variant? param)
  1903. {
  1904. string icon_name_displayed = _toolbar_run.icon_name;
  1905. stop_game.begin((obj, res) => {
  1906. stop_game.end(res);
  1907. if (icon_name_displayed == "game-run") {
  1908. // Always change icon state regardless of failures
  1909. _toolbar_run.icon_name = "game-stop";
  1910. //
  1911. _game.disconnected.disconnect(on_game_disconnected);
  1912. _game.disconnected.connect(on_game_disconnected_externally);
  1913. start_game.begin(action.name == "test-level" ? StartGame.TEST : StartGame.NORMAL);
  1914. }
  1915. });
  1916. }
  1917. private void on_undo(GLib.SimpleAction action, GLib.Variant? param)
  1918. {
  1919. int id = _database.undo();
  1920. if (id != -1)
  1921. _statusbar.set_temporary_message("Undo: " + ActionNames[id]);
  1922. }
  1923. private void on_redo(GLib.SimpleAction action, GLib.Variant? param)
  1924. {
  1925. int id = _database.redo();
  1926. if (id != -1)
  1927. _statusbar.set_temporary_message("Redo: " + ActionNames[id]);
  1928. }
  1929. private void on_duplicate(GLib.SimpleAction action, GLib.Variant? param)
  1930. {
  1931. _level.duplicate_selected_objects();
  1932. }
  1933. private void on_delete(GLib.SimpleAction action, GLib.Variant? param)
  1934. {
  1935. _level.destroy_selected_objects();
  1936. }
  1937. private void on_manual(GLib.SimpleAction action, GLib.Variant? param)
  1938. {
  1939. try {
  1940. AppInfo.launch_default_for_uri("https://docs.crownengine.org/html/v" + CROWN_VERSION, null);
  1941. }
  1942. catch (Error e) {
  1943. loge(e.message);
  1944. }
  1945. }
  1946. private void on_report_issue(GLib.SimpleAction action, GLib.Variant? param)
  1947. {
  1948. try {
  1949. AppInfo.launch_default_for_uri("https://github.com/crownengine/crown/issues", null);
  1950. }
  1951. catch (Error e) {
  1952. loge(e.message);
  1953. }
  1954. }
  1955. private void on_browse_logs(GLib.SimpleAction action, GLib.Variant? param)
  1956. {
  1957. open_directory(_logs_dir.get_path());
  1958. }
  1959. private void on_changelog(GLib.SimpleAction action, GLib.Variant? param)
  1960. {
  1961. try {
  1962. AppInfo.launch_default_for_uri("https://docs.crownengine.org/html/v" + CROWN_VERSION + "/changelog.html", null);
  1963. }
  1964. catch (Error e) {
  1965. loge(e.message);
  1966. }
  1967. }
  1968. private void on_about(GLib.SimpleAction action, GLib.Variant? param)
  1969. {
  1970. Gtk.AboutDialog dlg = new Gtk.AboutDialog();
  1971. dlg.set_destroy_with_parent(true);
  1972. dlg.set_transient_for(this.active_window);
  1973. dlg.set_modal(true);
  1974. dlg.set_logo_icon_name(CROWN_ICON_NAME);
  1975. dlg.program_name = LEVEL_EDITOR_WINDOW_TITLE;
  1976. dlg.version = CROWN_VERSION;
  1977. dlg.website = "https://www.crownengine.org";
  1978. dlg.copyright = "Copyright (c) 2012-2022 Daniele Bartolini et al.";
  1979. dlg.license = "Crown Game Engine."
  1980. + "\nCopyright (c) 2012-2022 Daniele Bartolini et al."
  1981. + "\n"
  1982. + "\nPermission is hereby granted, free of charge, to any person"
  1983. + "\nobtaining a copy of this software and associated documentation"
  1984. + "\nfiles (the \"Software\"), to deal in the Software without"
  1985. + "\nrestriction, including without limitation the rights to use,"
  1986. + "\ncopy, modify, merge, publish, distribute, sublicense, and/or sell"
  1987. + "\ncopies of the Software, and to permit persons to whom the"
  1988. + "\nSoftware is furnished to do so, subject to the following"
  1989. + "\nconditions:"
  1990. + "\n"
  1991. + "\nThe above copyright notice and this permission notice shall be"
  1992. + "\nincluded in all copies or substantial portions of the Software."
  1993. + "\n"
  1994. + "\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,"
  1995. + "\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES"
  1996. + "\nOF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND"
  1997. + "\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT"
  1998. + "\nHOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,"
  1999. + "\nWHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING"
  2000. + "\nFROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR"
  2001. + "\nOTHER DEALINGS IN THE SOFTWARE."
  2002. + "\n"
  2003. ;
  2004. dlg.authors =
  2005. {
  2006. "Daniele Bartolini",
  2007. "Simone Boscaratto",
  2008. "Michele Rossi",
  2009. "Raphael de Vasconcelos Nascimento"
  2010. };
  2011. dlg.artists =
  2012. {
  2013. "Michela Iacchelli - Pepper logo",
  2014. "Giulia Gazzoli - Crown logo"
  2015. };
  2016. dlg.run();
  2017. dlg.destroy();
  2018. }
  2019. private void on_delete_file(GLib.SimpleAction action, GLib.Variant? param)
  2020. {
  2021. if (param == null)
  2022. return;
  2023. string resource_path = param.get_string();
  2024. string? type = Crown.resource_type(resource_path);
  2025. string? name = Crown.resource_name(type, resource_path);
  2026. if (type == null || name == null)
  2027. return;
  2028. if (name == _level._name) {
  2029. int rt = ResponseType.YES;
  2030. if (_database.changed())
  2031. rt = run_level_changed_dialog(this.active_window);
  2032. if (!_database.changed() || rt == ResponseType.YES && save() || rt == ResponseType.NO) {
  2033. new_level();
  2034. send_state();
  2035. _project.delete_resource(type, name);
  2036. }
  2037. } else {
  2038. _project.delete_resource(type, name);
  2039. }
  2040. }
  2041. public void set_autosave_timer(uint minutes)
  2042. {
  2043. if (_save_timer_id > 0)
  2044. GLib.Source.remove(_save_timer_id);
  2045. _save_timer_id = GLib.Timeout.add_seconds(minutes*60, save_timeout);
  2046. }
  2047. public void menu_set_enabled(bool enabled, GLib.ActionEntry[] entries, string[]? whitelist = null)
  2048. {
  2049. for (int ii = 0; ii < entries.length; ++ii) {
  2050. string action_name = entries[ii].name;
  2051. int jj = 0;
  2052. if (whitelist != null) {
  2053. for (; jj < whitelist.length; ++jj) {
  2054. if (action_name == whitelist[jj])
  2055. break;
  2056. }
  2057. }
  2058. if (whitelist == null || whitelist != null && jj == whitelist.length) {
  2059. GLib.SimpleAction sa = this.lookup_action(action_name) as GLib.SimpleAction;
  2060. if (sa != null)
  2061. sa.set_enabled(enabled);
  2062. }
  2063. }
  2064. }
  2065. private void set_conflicting_accels(bool on)
  2066. {
  2067. if (on) {
  2068. this.set_accels_for_action("app.tool::place", _tool_place_accels);
  2069. this.set_accels_for_action("app.tool::move", _tool_move_accels);
  2070. this.set_accels_for_action("app.tool::rotate", _tool_rotate_accels);
  2071. this.set_accels_for_action("app.tool::scale", _tool_scale_accels);
  2072. this.set_accels_for_action("app.delete", _delete_accels);
  2073. this.set_accels_for_action("app.camera-view::perspective", _camera_view_perspective_accels);
  2074. this.set_accels_for_action("app.camera-view::front", _camera_view_front_accels);
  2075. this.set_accels_for_action("app.camera-view::back", _camera_view_back_accels);
  2076. this.set_accels_for_action("app.camera-view::right", _camera_view_right_accels);
  2077. this.set_accels_for_action("app.camera-view::left", _camera_view_left_accels);
  2078. this.set_accels_for_action("app.camera-view::top", _camera_view_top_accels);
  2079. this.set_accels_for_action("app.camera-view::bottom", _camera_view_bottom_accels);
  2080. } else {
  2081. this.set_accels_for_action("app.tool::place", {});
  2082. this.set_accels_for_action("app.tool::move", {});
  2083. this.set_accels_for_action("app.tool::rotate", {});
  2084. this.set_accels_for_action("app.tool::scale", {});
  2085. this.set_accels_for_action("app.delete", {});
  2086. this.set_accels_for_action("app.camera-view::perspective", {});
  2087. this.set_accels_for_action("app.camera-view::front", {});
  2088. this.set_accels_for_action("app.camera-view::back", {});
  2089. this.set_accels_for_action("app.camera-view::right", {});
  2090. this.set_accels_for_action("app.camera-view::left", {});
  2091. this.set_accels_for_action("app.camera-view::top", {});
  2092. this.set_accels_for_action("app.camera-view::bottom", {});
  2093. }
  2094. }
  2095. public void entry_any_focus_in(Gtk.Widget widget)
  2096. {
  2097. set_conflicting_accels(false);
  2098. }
  2099. public void entry_any_focus_out(Gtk.Widget widget)
  2100. {
  2101. set_conflicting_accels(true);
  2102. }
  2103. public void show_panel(string name, Gtk.StackTransitionType stt = Gtk.StackTransitionType.NONE)
  2104. {
  2105. _main_stack.set_visible_child_full(name, stt);
  2106. if (name == "main_vbox") {
  2107. // FIXME: save/restore last known window state
  2108. int win_w;
  2109. int win_h;
  2110. this.active_window.get_size(out win_w, out win_h);
  2111. _editor_pane.set_position(210);
  2112. _content_pane.set_position(win_h - 250);
  2113. _inspector_pane.set_position(win_h - 600);
  2114. _main_pane.set_position(win_w - 375);
  2115. menu_set_enabled(true, action_entries_file);
  2116. menu_set_enabled(true, action_entries_edit);
  2117. menu_set_enabled(true, action_entries_create);
  2118. menu_set_enabled(true, action_entries_camera);
  2119. menu_set_enabled(true, action_entries_view);
  2120. menu_set_enabled(true, action_entries_debug);
  2121. menu_set_enabled(true, action_entries_help);
  2122. } else if (name == "panel_welcome"
  2123. || name == "panel_new_project"
  2124. || name == "panel_projects_list"
  2125. ) {
  2126. menu_set_enabled(false, action_entries_file, {"new-project", "open-project", "quit"});
  2127. menu_set_enabled(false, action_entries_edit);
  2128. menu_set_enabled(false, action_entries_create);
  2129. menu_set_enabled(false, action_entries_camera);
  2130. menu_set_enabled(false, action_entries_view);
  2131. menu_set_enabled(false, action_entries_debug);
  2132. menu_set_enabled(true, action_entries_help);
  2133. }
  2134. }
  2135. public void set_placeable(string type, string name)
  2136. {
  2137. _placeable_type = type;
  2138. _placeable_name = name;
  2139. _editor.send_script(LevelEditorApi.set_placeable(type, name));
  2140. }
  2141. public void activate_last_tool_before_place()
  2142. {
  2143. const string type_to_name[] =
  2144. {
  2145. "place",
  2146. "move",
  2147. "rotate",
  2148. "scale"
  2149. };
  2150. GLib.static_assert(type_to_name.length == LevelEditorApi.ToolType.COUNT);
  2151. if (_tool_type != LevelEditorApi.ToolType.PLACE)
  2152. return;
  2153. activate_action("tool", new GLib.Variant.string(type_to_name[_tool_type_prev]));
  2154. }
  2155. }
  2156. // Global paths
  2157. public static GLib.File _toolchain_dir;
  2158. public static GLib.File _templates_dir;
  2159. public static GLib.File _config_dir;
  2160. public static GLib.File _logs_dir;
  2161. public static GLib.File _cache_dir;
  2162. public static GLib.File _documents_dir;
  2163. public static GLib.File _log_file;
  2164. public static GLib.File _settings_file;
  2165. public static GLib.File _user_file;
  2166. public static GLib.File _console_history_file;
  2167. public static GLib.FileStream _log_stream;
  2168. public static ConsoleView _console_view;
  2169. public static bool _console_view_valid = false;
  2170. public static string _log_prefix;
  2171. public static void log(string system, string severity, string message)
  2172. {
  2173. GLib.DateTime now = new GLib.DateTime.now_local();
  2174. int now_us = now.get_microsecond();
  2175. string now_str = now.format("%H:%M:%S");
  2176. string plain_text_line = "%s.%06d %.4s %s: %s\n".printf(now_str
  2177. , now_us
  2178. , severity.ascii_up()
  2179. , system
  2180. , message
  2181. );
  2182. if (_log_stream != null) {
  2183. _log_stream.puts(plain_text_line);
  2184. _log_stream.flush();
  2185. }
  2186. if (_console_view_valid) {
  2187. string line = "%s.%06d %s: %s\n".printf(now_str
  2188. , now_us
  2189. , system
  2190. , message
  2191. );
  2192. _console_view.log(severity, line);
  2193. }
  2194. }
  2195. public static void logi(string message)
  2196. {
  2197. log(_log_prefix, "info", message);
  2198. }
  2199. public static void logw(string message)
  2200. {
  2201. log(_log_prefix, "warning", message);
  2202. }
  2203. public static void loge(string message)
  2204. {
  2205. log(_log_prefix, "error", message);
  2206. }
  2207. public void open_directory(string directory)
  2208. {
  2209. #if CROWN_PLATFORM_LINUX
  2210. try {
  2211. GLib.AppInfo.launch_default_for_uri("file://" + directory, null);
  2212. }
  2213. catch (Error e) {
  2214. loge(e.message);
  2215. }
  2216. #else
  2217. GLib.SubprocessLauncher sl = new GLib.SubprocessLauncher(subprocess_flags());
  2218. try {
  2219. sl.spawnv({ "explorer.exe", directory, null });
  2220. }
  2221. catch (Error e) {
  2222. loge(e.message);
  2223. }
  2224. #endif
  2225. }
  2226. public static GLib.SubprocessFlags subprocess_flags()
  2227. {
  2228. GLib.SubprocessFlags flags = SubprocessFlags.NONE;
  2229. #if !CROWN_DEBUG
  2230. flags |= SubprocessFlags.STDOUT_SILENCE | SubprocessFlags.STDERR_SILENCE;
  2231. #endif
  2232. return flags;
  2233. }
  2234. public static bool is_directory_empty(string path)
  2235. {
  2236. GLib.File file = GLib.File.new_for_path(path);
  2237. try {
  2238. FileEnumerator enumerator = file.enumerate_children("standard::*"
  2239. , FileQueryInfoFlags.NOFOLLOW_SYMLINKS
  2240. );
  2241. return enumerator.next_file() == null;
  2242. }
  2243. catch (GLib.Error e) {
  2244. loge(e.message);
  2245. }
  2246. return false;
  2247. }
  2248. private void device_frame_delayed(uint delay_ms, ConsoleClient client)
  2249. {
  2250. // FIXME: find a way to time exactly when it is effective to queue a redraw.
  2251. // See: https://blogs.gnome.org/jnelson/2010/10/13/those-realize-map-widget-signals/
  2252. GLib.Timeout.add_full(GLib.Priority.DEFAULT, delay_ms, () => {
  2253. client.send(DeviceApi.frame());
  2254. return GLib.Source.REMOVE;
  2255. });
  2256. }
  2257. public static int main(string[] args)
  2258. {
  2259. // Redirect GLib logs to internal log*().
  2260. GLib.set_print_handler((msg) => { logi(msg); });
  2261. GLib.set_printerr_handler((msg) => { loge(msg); });
  2262. GLib.Log.set_writer_func((log_level, fields) => {
  2263. foreach (var field in fields) {
  2264. if (field.key == "MESSAGE") {
  2265. switch (log_level) {
  2266. case LEVEL_DEBUG:
  2267. #if CROWN_DEBUG
  2268. logi((string)field.value);
  2269. #endif
  2270. break;
  2271. case LEVEL_INFO:
  2272. case LEVEL_MESSAGE:
  2273. logi((string)field.value);
  2274. break;
  2275. case LEVEL_CRITICAL:
  2276. case LEVEL_WARNING:
  2277. logw((string)field.value);
  2278. break;
  2279. case LEVEL_ERROR:
  2280. loge((string)field.value);
  2281. break;
  2282. default:
  2283. logw((string)field.value);
  2284. break;
  2285. }
  2286. return GLib.LogWriterOutput.HANDLED;
  2287. }
  2288. }
  2289. return GLib.LogWriterOutput.UNHANDLED;
  2290. });
  2291. // If args does not contain --child, spawn the launcher.
  2292. int ii;
  2293. for (ii = 0; ii < args.length; ++ii) {
  2294. if (args[ii] == "--child")
  2295. break;
  2296. }
  2297. if (ii == args.length) {
  2298. _log_prefix = "launcher";
  2299. return launcher_main(args);
  2300. }
  2301. _log_prefix = "editor";
  2302. // Remove --child from args for backward compatibility.
  2303. if (args.length > 1)
  2304. args = args[0 : args.length - 1];
  2305. // Global paths
  2306. _config_dir = GLib.File.new_for_path(GLib.Path.build_filename(GLib.Environment.get_user_config_dir(), "crown"));
  2307. try {
  2308. _config_dir.make_directory();
  2309. } catch (Error e) {
  2310. /* Nobody cares */
  2311. }
  2312. _logs_dir = GLib.File.new_for_path(GLib.Path.build_filename(_config_dir.get_path(), "logs"));
  2313. try {
  2314. _logs_dir.make_directory();
  2315. } catch (Error e) {
  2316. /* Nobody cares */
  2317. }
  2318. _documents_dir = GLib.File.new_for_path(GLib.Environment.get_user_special_dir(GLib.UserDirectory.DOCUMENTS));
  2319. _cache_dir = GLib.File.new_for_path(GLib.Path.build_filename(GLib.Environment.get_user_cache_dir(), "crown"));
  2320. try {
  2321. _cache_dir.make_directory();
  2322. } catch (Error e) {
  2323. /* Nobody cares */
  2324. }
  2325. _log_file = GLib.File.new_for_path(GLib.Path.build_filename(_logs_dir.get_path(), new GLib.DateTime.now_local().format("%Y-%m-%d") + ".log"));
  2326. _settings_file = GLib.File.new_for_path(GLib.Path.build_filename(_config_dir.get_path(), "settings.sjson"));
  2327. _user_file = GLib.File.new_for_path(GLib.Path.build_filename(_config_dir.get_path(), "user.sjson"));
  2328. _console_history_file = GLib.File.new_for_path(GLib.Path.build_filename(_cache_dir.get_path(), "console_history.txt"));
  2329. _log_stream = GLib.FileStream.open(_log_file.get_path(), "a");
  2330. // Connect to SubprocessLauncher service.
  2331. SubprocessLauncher subprocess_launcher;
  2332. try {
  2333. subprocess_launcher = GLib.Bus.get_proxy_sync(GLib.BusType.SESSION
  2334. , "org.crownengine.SubprocessLauncher"
  2335. , "/org/crownengine/subprocess_launcher"
  2336. );
  2337. } catch (IOError e) {
  2338. loge(e.message);
  2339. return 1;
  2340. }
  2341. // Find toolchain path, more desirable paths come first.
  2342. ii = 0;
  2343. string toolchain_paths[] =
  2344. {
  2345. ".",
  2346. "../..",
  2347. "../../../samples"
  2348. };
  2349. for (ii = 0; ii < toolchain_paths.length; ++ii) {
  2350. string path = Path.build_filename(toolchain_paths[ii], "core");
  2351. if (GLib.FileUtils.test(path, FileTest.EXISTS) && GLib.FileUtils.test(path, FileTest.IS_DIR)) {
  2352. _toolchain_dir = File.new_for_path(path).get_parent();
  2353. break;
  2354. }
  2355. }
  2356. if (ii == toolchain_paths.length) {
  2357. loge("Unable to find the toolchain directory");
  2358. return 1;
  2359. }
  2360. // Find templates path, more desirable paths come first.
  2361. string templates_path[] =
  2362. {
  2363. ".",
  2364. "../..",
  2365. "../../.."
  2366. };
  2367. for (ii = 0; ii < templates_path.length; ++ii) {
  2368. string path = Path.build_filename(templates_path[ii], "samples");
  2369. if (GLib.FileUtils.test(path, FileTest.EXISTS) && GLib.FileUtils.test(path, FileTest.IS_DIR)) {
  2370. _templates_dir = File.new_for_path(path);
  2371. break;
  2372. }
  2373. }
  2374. if (ii == templates_path.length) {
  2375. loge("Unable to find the templates directory");
  2376. return 1;
  2377. }
  2378. #if CROWN_PLATFORM_LINUX
  2379. Gdk.set_allowed_backends("x11");
  2380. #endif
  2381. LevelEditorApplication app = new LevelEditorApplication(subprocess_launcher);
  2382. return app.run(args);
  2383. }
  2384. } /* namespace Crown */