project_manager.cpp 102 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071
  1. /**************************************************************************/
  2. /* project_manager.cpp */
  3. /**************************************************************************/
  4. /* This file is part of: */
  5. /* GODOT ENGINE */
  6. /* https://godotengine.org */
  7. /**************************************************************************/
  8. /* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
  9. /* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
  10. /* */
  11. /* Permission is hereby granted, free of charge, to any person obtaining */
  12. /* a copy of this software and associated documentation files (the */
  13. /* "Software"), to deal in the Software without restriction, including */
  14. /* without limitation the rights to use, copy, modify, merge, publish, */
  15. /* distribute, sublicense, and/or sell copies of the Software, and to */
  16. /* permit persons to whom the Software is furnished to do so, subject to */
  17. /* the following conditions: */
  18. /* */
  19. /* The above copyright notice and this permission notice shall be */
  20. /* included in all copies or substantial portions of the Software. */
  21. /* */
  22. /* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
  23. /* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
  24. /* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
  25. /* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
  26. /* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
  27. /* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
  28. /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
  29. /**************************************************************************/
  30. #include "project_manager.h"
  31. #include "core/config/project_settings.h"
  32. #include "core/io/config_file.h"
  33. #include "core/io/dir_access.h"
  34. #include "core/io/file_access.h"
  35. #include "core/io/resource_saver.h"
  36. #include "core/io/stream_peer_tls.h"
  37. #include "core/io/zip_io.h"
  38. #include "core/os/keyboard.h"
  39. #include "core/os/os.h"
  40. #include "core/string/translation.h"
  41. #include "core/version.h"
  42. #include "editor/editor_file_dialog.h"
  43. #include "editor/editor_paths.h"
  44. #include "editor/editor_scale.h"
  45. #include "editor/editor_settings.h"
  46. #include "editor/editor_themes.h"
  47. #include "editor/editor_vcs_interface.h"
  48. #include "main/main.h"
  49. #include "scene/gui/center_container.h"
  50. #include "scene/gui/check_box.h"
  51. #include "scene/gui/line_edit.h"
  52. #include "scene/gui/margin_container.h"
  53. #include "scene/gui/panel_container.h"
  54. #include "scene/gui/separator.h"
  55. #include "scene/gui/texture_rect.h"
  56. #include "scene/main/window.h"
  57. #include "servers/display_server.h"
  58. #include "servers/navigation_server_3d.h"
  59. #include "servers/physics_server_2d.h"
  60. constexpr int GODOT4_CONFIG_VERSION = 5;
  61. class ProjectDialog : public ConfirmationDialog {
  62. GDCLASS(ProjectDialog, ConfirmationDialog);
  63. public:
  64. bool is_folder_empty = true;
  65. enum Mode {
  66. MODE_NEW,
  67. MODE_IMPORT,
  68. MODE_INSTALL,
  69. MODE_RENAME,
  70. };
  71. private:
  72. enum MessageType {
  73. MESSAGE_ERROR,
  74. MESSAGE_WARNING,
  75. MESSAGE_SUCCESS,
  76. };
  77. enum InputType {
  78. PROJECT_PATH,
  79. INSTALL_PATH,
  80. };
  81. Mode mode;
  82. Button *browse;
  83. Button *install_browse;
  84. Button *create_dir;
  85. Container *name_container;
  86. Container *path_container;
  87. Container *install_path_container;
  88. Container *renderer_container;
  89. Label *renderer_info;
  90. HBoxContainer *default_files_container;
  91. Ref<ButtonGroup> renderer_button_group;
  92. Label *msg;
  93. LineEdit *project_path;
  94. LineEdit *project_name;
  95. LineEdit *install_path;
  96. TextureRect *status_rect;
  97. TextureRect *install_status_rect;
  98. EditorFileDialog *fdialog;
  99. EditorFileDialog *fdialog_install;
  100. OptionButton *vcs_metadata_selection;
  101. String zip_path;
  102. String zip_title;
  103. AcceptDialog *dialog_error;
  104. String fav_dir;
  105. String created_folder_path;
  106. void set_message(const String &p_msg, MessageType p_type = MESSAGE_SUCCESS, InputType input_type = PROJECT_PATH) {
  107. msg->set_text(p_msg);
  108. Ref<Texture2D> current_path_icon = status_rect->get_texture();
  109. Ref<Texture2D> current_install_icon = install_status_rect->get_texture();
  110. Ref<Texture2D> new_icon;
  111. switch (p_type) {
  112. case MESSAGE_ERROR: {
  113. msg->add_theme_color_override("font_color", msg->get_theme_color(SNAME("error_color"), SNAME("Editor")));
  114. msg->set_modulate(Color(1, 1, 1, 1));
  115. new_icon = msg->get_theme_icon(SNAME("StatusError"), SNAME("EditorIcons"));
  116. } break;
  117. case MESSAGE_WARNING: {
  118. msg->add_theme_color_override("font_color", msg->get_theme_color(SNAME("warning_color"), SNAME("Editor")));
  119. msg->set_modulate(Color(1, 1, 1, 1));
  120. new_icon = msg->get_theme_icon(SNAME("StatusWarning"), SNAME("EditorIcons"));
  121. } break;
  122. case MESSAGE_SUCCESS: {
  123. msg->set_modulate(Color(1, 1, 1, 0));
  124. new_icon = msg->get_theme_icon(SNAME("StatusSuccess"), SNAME("EditorIcons"));
  125. } break;
  126. }
  127. if (current_path_icon != new_icon && input_type == PROJECT_PATH) {
  128. status_rect->set_texture(new_icon);
  129. } else if (current_install_icon != new_icon && input_type == INSTALL_PATH) {
  130. install_status_rect->set_texture(new_icon);
  131. }
  132. Size2i window_size = get_size();
  133. Size2 contents_min_size = get_contents_minimum_size();
  134. if (window_size.x < contents_min_size.x || window_size.y < contents_min_size.y) {
  135. set_size(window_size.max(contents_min_size));
  136. }
  137. }
  138. String _test_path() {
  139. Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
  140. String valid_path, valid_install_path;
  141. if (d->change_dir(project_path->get_text()) == OK) {
  142. valid_path = project_path->get_text();
  143. } else if (d->change_dir(project_path->get_text().strip_edges()) == OK) {
  144. valid_path = project_path->get_text().strip_edges();
  145. } else if (project_path->get_text().ends_with(".zip")) {
  146. if (d->file_exists(project_path->get_text())) {
  147. valid_path = project_path->get_text();
  148. }
  149. } else if (project_path->get_text().strip_edges().ends_with(".zip")) {
  150. if (d->file_exists(project_path->get_text().strip_edges())) {
  151. valid_path = project_path->get_text().strip_edges();
  152. }
  153. }
  154. if (valid_path.is_empty()) {
  155. set_message(TTR("The path specified doesn't exist."), MESSAGE_ERROR);
  156. get_ok_button()->set_disabled(true);
  157. return "";
  158. }
  159. if (mode == MODE_IMPORT && valid_path.ends_with(".zip")) {
  160. if (d->change_dir(install_path->get_text()) == OK) {
  161. valid_install_path = install_path->get_text();
  162. } else if (d->change_dir(install_path->get_text().strip_edges()) == OK) {
  163. valid_install_path = install_path->get_text().strip_edges();
  164. }
  165. if (valid_install_path.is_empty()) {
  166. set_message(TTR("The path specified doesn't exist."), MESSAGE_ERROR, INSTALL_PATH);
  167. get_ok_button()->set_disabled(true);
  168. return "";
  169. }
  170. }
  171. if (mode == MODE_IMPORT || mode == MODE_RENAME) {
  172. if (!valid_path.is_empty() && !d->file_exists("project.godot")) {
  173. if (valid_path.ends_with(".zip")) {
  174. Ref<FileAccess> io_fa;
  175. zlib_filefunc_def io = zipio_create_io(&io_fa);
  176. unzFile pkg = unzOpen2(valid_path.utf8().get_data(), &io);
  177. if (!pkg) {
  178. set_message(TTR("Error opening package file (it's not in ZIP format)."), MESSAGE_ERROR);
  179. get_ok_button()->set_disabled(true);
  180. unzClose(pkg);
  181. return "";
  182. }
  183. int ret = unzGoToFirstFile(pkg);
  184. while (ret == UNZ_OK) {
  185. unz_file_info info;
  186. char fname[16384];
  187. ret = unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0);
  188. if (ret != UNZ_OK) {
  189. break;
  190. }
  191. if (String::utf8(fname).ends_with("project.godot")) {
  192. break;
  193. }
  194. ret = unzGoToNextFile(pkg);
  195. }
  196. if (ret == UNZ_END_OF_LIST_OF_FILE) {
  197. set_message(TTR("Invalid \".zip\" project file; it doesn't contain a \"project.godot\" file."), MESSAGE_ERROR);
  198. get_ok_button()->set_disabled(true);
  199. unzClose(pkg);
  200. return "";
  201. }
  202. unzClose(pkg);
  203. // check if the specified install folder is empty, even though this is not an error, it is good to check here
  204. d->list_dir_begin();
  205. is_folder_empty = true;
  206. String n = d->get_next();
  207. while (!n.is_empty()) {
  208. if (!n.begins_with(".")) {
  209. // Allow `.`, `..` (reserved current/parent folder names)
  210. // and hidden files/folders to be present.
  211. // For instance, this lets users initialize a Git repository
  212. // and still be able to create a project in the directory afterwards.
  213. is_folder_empty = false;
  214. break;
  215. }
  216. n = d->get_next();
  217. }
  218. d->list_dir_end();
  219. if (!is_folder_empty) {
  220. set_message(TTR("Please choose an empty folder."), MESSAGE_WARNING, INSTALL_PATH);
  221. get_ok_button()->set_disabled(true);
  222. return "";
  223. }
  224. } else {
  225. set_message(TTR("Please choose a \"project.godot\" or \".zip\" file."), MESSAGE_ERROR);
  226. install_path_container->hide();
  227. get_ok_button()->set_disabled(true);
  228. return "";
  229. }
  230. } else if (valid_path.ends_with("zip")) {
  231. set_message(TTR("This directory already contains a Godot project."), MESSAGE_ERROR, INSTALL_PATH);
  232. get_ok_button()->set_disabled(true);
  233. return "";
  234. }
  235. } else {
  236. // check if the specified folder is empty, even though this is not an error, it is good to check here
  237. d->list_dir_begin();
  238. is_folder_empty = true;
  239. String n = d->get_next();
  240. while (!n.is_empty()) {
  241. if (!n.begins_with(".")) {
  242. // Allow `.`, `..` (reserved current/parent folder names)
  243. // and hidden files/folders to be present.
  244. // For instance, this lets users initialize a Git repository
  245. // and still be able to create a project in the directory afterwards.
  246. is_folder_empty = false;
  247. break;
  248. }
  249. n = d->get_next();
  250. }
  251. d->list_dir_end();
  252. if (!is_folder_empty) {
  253. set_message(TTR("The selected path is not empty. Choosing an empty folder is highly recommended."), MESSAGE_WARNING);
  254. get_ok_button()->set_disabled(false);
  255. return valid_path;
  256. }
  257. }
  258. set_message("");
  259. set_message("", MESSAGE_SUCCESS, INSTALL_PATH);
  260. get_ok_button()->set_disabled(false);
  261. return valid_path;
  262. }
  263. void _path_text_changed(const String &p_path) {
  264. String sp = _test_path();
  265. if (!sp.is_empty()) {
  266. // If the project name is empty or default, infer the project name from the selected folder name
  267. if (project_name->get_text().strip_edges().is_empty() || project_name->get_text().strip_edges() == TTR("New Game Project")) {
  268. sp = sp.replace("\\", "/");
  269. int lidx = sp.rfind("/");
  270. if (lidx != -1) {
  271. sp = sp.substr(lidx + 1, sp.length()).capitalize();
  272. }
  273. if (sp.is_empty() && mode == MODE_IMPORT) {
  274. sp = TTR("Imported Project");
  275. }
  276. project_name->set_text(sp);
  277. _text_changed(sp);
  278. }
  279. }
  280. if (!created_folder_path.is_empty() && created_folder_path != p_path) {
  281. _remove_created_folder();
  282. }
  283. }
  284. void _file_selected(const String &p_path) {
  285. String p = p_path;
  286. if (mode == MODE_IMPORT) {
  287. if (p.ends_with("project.godot")) {
  288. p = p.get_base_dir();
  289. install_path_container->hide();
  290. get_ok_button()->set_disabled(false);
  291. } else if (p.ends_with(".zip")) {
  292. install_path->set_text(p.get_base_dir());
  293. install_path_container->show();
  294. get_ok_button()->set_disabled(false);
  295. } else {
  296. set_message(TTR("Please choose a \"project.godot\" or \".zip\" file."), MESSAGE_ERROR);
  297. get_ok_button()->set_disabled(true);
  298. return;
  299. }
  300. }
  301. String sp = p.simplify_path();
  302. project_path->set_text(sp);
  303. _path_text_changed(sp);
  304. if (p.ends_with(".zip")) {
  305. install_path->call_deferred(SNAME("grab_focus"));
  306. } else {
  307. get_ok_button()->call_deferred(SNAME("grab_focus"));
  308. }
  309. }
  310. void _path_selected(const String &p_path) {
  311. String sp = p_path.simplify_path();
  312. project_path->set_text(sp);
  313. _path_text_changed(sp);
  314. get_ok_button()->call_deferred(SNAME("grab_focus"));
  315. }
  316. void _install_path_selected(const String &p_path) {
  317. String sp = p_path.simplify_path();
  318. install_path->set_text(sp);
  319. _path_text_changed(sp);
  320. get_ok_button()->call_deferred(SNAME("grab_focus"));
  321. }
  322. void _browse_path() {
  323. fdialog->set_current_dir(project_path->get_text());
  324. if (mode == MODE_IMPORT) {
  325. fdialog->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_FILE);
  326. fdialog->clear_filters();
  327. fdialog->add_filter("project.godot", vformat("%s %s", VERSION_NAME, TTR("Project")));
  328. fdialog->add_filter("*.zip", TTR("ZIP File"));
  329. } else {
  330. fdialog->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_DIR);
  331. }
  332. fdialog->popup_file_dialog();
  333. }
  334. void _browse_install_path() {
  335. fdialog_install->set_current_dir(install_path->get_text());
  336. fdialog_install->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_DIR);
  337. fdialog_install->popup_file_dialog();
  338. }
  339. void _create_folder() {
  340. const String project_name_no_edges = project_name->get_text().strip_edges();
  341. if (project_name_no_edges.is_empty() || !created_folder_path.is_empty() || project_name_no_edges.ends_with(".")) {
  342. set_message(TTR("Invalid project name."), MESSAGE_WARNING);
  343. return;
  344. }
  345. Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
  346. if (d->change_dir(project_path->get_text()) == OK) {
  347. if (!d->dir_exists(project_name_no_edges)) {
  348. if (d->make_dir(project_name_no_edges) == OK) {
  349. d->change_dir(project_name_no_edges);
  350. String dir_str = d->get_current_dir();
  351. project_path->set_text(dir_str);
  352. _path_text_changed(dir_str);
  353. created_folder_path = d->get_current_dir();
  354. create_dir->set_disabled(true);
  355. } else {
  356. dialog_error->set_text(TTR("Couldn't create folder."));
  357. dialog_error->popup_centered();
  358. }
  359. } else {
  360. dialog_error->set_text(TTR("There is already a folder in this path with the specified name."));
  361. dialog_error->popup_centered();
  362. }
  363. }
  364. }
  365. void _text_changed(const String &p_text) {
  366. if (mode != MODE_NEW) {
  367. return;
  368. }
  369. _test_path();
  370. if (p_text.strip_edges().is_empty()) {
  371. set_message(TTR("It would be a good idea to name your project."), MESSAGE_ERROR);
  372. }
  373. }
  374. void _nonempty_confirmation_ok_pressed() {
  375. is_folder_empty = true;
  376. ok_pressed();
  377. }
  378. void _renderer_selected() {
  379. String renderer_type = renderer_button_group->get_pressed_button()->get_meta(SNAME("rendering_method"));
  380. if (renderer_type == "forward_plus") {
  381. renderer_info->set_text(
  382. String::utf8("• ") + TTR("Supports desktop platforms only.") +
  383. String::utf8("\n• ") + TTR("Advanced 3D graphics available.") +
  384. String::utf8("\n• ") + TTR("Can scale to large complex scenes.") +
  385. String::utf8("\n• ") + TTR("Uses RenderingDevice backend.") +
  386. String::utf8("\n• ") + TTR("Slower rendering of simple scenes."));
  387. } else if (renderer_type == "mobile") {
  388. renderer_info->set_text(
  389. String::utf8("• ") + TTR("Supports desktop + mobile platforms.") +
  390. String::utf8("\n• ") + TTR("Less advanced 3D graphics.") +
  391. String::utf8("\n• ") + TTR("Less scalable for complex scenes.") +
  392. String::utf8("\n• ") + TTR("Uses RenderingDevice backend.") +
  393. String::utf8("\n• ") + TTR("Fast rendering of simple scenes."));
  394. } else if (renderer_type == "gl_compatibility") {
  395. renderer_info->set_text(
  396. String::utf8("• ") + TTR("Supports desktop, mobile + web platforms.") +
  397. String::utf8("\n• ") + TTR("Least advanced 3D graphics (currently work-in-progress).") +
  398. String::utf8("\n• ") + TTR("Intended for low-end/older devices.") +
  399. String::utf8("\n• ") + TTR("Uses OpenGL 3 backend (OpenGL 3.3/ES 3.0/WebGL2).") +
  400. String::utf8("\n• ") + TTR("Fastest rendering of simple scenes."));
  401. } else {
  402. WARN_PRINT("Unknown renderer type. Please report this as a bug on GitHub.");
  403. }
  404. }
  405. void ok_pressed() override {
  406. String dir = project_path->get_text();
  407. if (mode == MODE_RENAME) {
  408. String dir2 = _test_path();
  409. if (dir2.is_empty()) {
  410. set_message(TTR("Invalid project path (changed anything?)."), MESSAGE_ERROR);
  411. return;
  412. }
  413. // Load project.godot as ConfigFile to set the new name.
  414. ConfigFile cfg;
  415. String project_godot = dir2.path_join("project.godot");
  416. Error err = cfg.load(project_godot);
  417. if (err != OK) {
  418. set_message(vformat(TTR("Couldn't load project at '%s' (error %d). It may be missing or corrupted."), project_godot, err), MESSAGE_ERROR);
  419. } else {
  420. cfg.set_value("application", "config/name", project_name->get_text().strip_edges());
  421. err = cfg.save(project_godot);
  422. if (err != OK) {
  423. set_message(vformat(TTR("Couldn't save project at '%s' (error %d)."), project_godot, err), MESSAGE_ERROR);
  424. }
  425. }
  426. hide();
  427. emit_signal(SNAME("projects_updated"));
  428. } else {
  429. if (mode == MODE_IMPORT) {
  430. if (project_path->get_text().ends_with(".zip")) {
  431. mode = MODE_INSTALL;
  432. ok_pressed();
  433. return;
  434. }
  435. } else {
  436. if (mode == MODE_NEW) {
  437. // Before we create a project, check that the target folder is empty.
  438. // If not, we need to ask the user if they're sure they want to do this.
  439. if (!is_folder_empty) {
  440. ConfirmationDialog *cd = memnew(ConfirmationDialog);
  441. cd->set_title(TTR("Warning: This folder is not empty"));
  442. cd->set_text(TTR("You are about to create a Godot project in a non-empty folder.\nThe entire contents of this folder will be imported as project resources!\n\nAre you sure you wish to continue?"));
  443. cd->get_ok_button()->connect("pressed", callable_mp(this, &ProjectDialog::_nonempty_confirmation_ok_pressed));
  444. get_parent()->add_child(cd);
  445. cd->popup_centered();
  446. cd->grab_focus();
  447. return;
  448. }
  449. PackedStringArray project_features = ProjectSettings::get_required_features();
  450. ProjectSettings::CustomMap initial_settings;
  451. // Be sure to change this code if/when renderers are changed.
  452. // Default values are "forward_plus" for the main setting, "mobile" for the mobile override,
  453. // and "gl_compatibility" for the web override.
  454. String renderer_type = renderer_button_group->get_pressed_button()->get_meta(SNAME("rendering_method"));
  455. initial_settings["rendering/renderer/rendering_method"] = renderer_type;
  456. EditorSettings::get_singleton()->set("project_manager/default_renderer", renderer_type);
  457. EditorSettings::get_singleton()->save();
  458. if (renderer_type == "forward_plus") {
  459. project_features.push_back("Forward Plus");
  460. } else if (renderer_type == "mobile") {
  461. project_features.push_back("Mobile");
  462. } else if (renderer_type == "gl_compatibility") {
  463. project_features.push_back("GL Compatibility");
  464. // Also change the default rendering method for the mobile override.
  465. initial_settings["rendering/renderer/rendering_method.mobile"] = "gl_compatibility";
  466. } else {
  467. WARN_PRINT("Unknown renderer type. Please report this as a bug on GitHub.");
  468. }
  469. project_features.sort();
  470. initial_settings["application/config/features"] = project_features;
  471. initial_settings["application/config/name"] = project_name->get_text().strip_edges();
  472. initial_settings["application/config/icon"] = "res://icon.svg";
  473. if (ProjectSettings::get_singleton()->save_custom(dir.path_join("project.godot"), initial_settings, Vector<String>(), false) != OK) {
  474. set_message(TTR("Couldn't create project.godot in project path."), MESSAGE_ERROR);
  475. } else {
  476. // Store default project icon in SVG format.
  477. Error err;
  478. Ref<FileAccess> fa_icon = FileAccess::open(dir.path_join("icon.svg"), FileAccess::WRITE, &err);
  479. fa_icon->store_string(get_default_project_icon());
  480. if (err != OK) {
  481. set_message(TTR("Couldn't create icon.svg in project path."), MESSAGE_ERROR);
  482. }
  483. EditorVCSInterface::create_vcs_metadata_files(EditorVCSInterface::VCSMetadata(vcs_metadata_selection->get_selected()), dir);
  484. }
  485. } else if (mode == MODE_INSTALL) {
  486. if (project_path->get_text().ends_with(".zip")) {
  487. dir = install_path->get_text();
  488. zip_path = project_path->get_text();
  489. }
  490. Ref<FileAccess> io_fa;
  491. zlib_filefunc_def io = zipio_create_io(&io_fa);
  492. unzFile pkg = unzOpen2(zip_path.utf8().get_data(), &io);
  493. if (!pkg) {
  494. dialog_error->set_text(TTR("Error opening package file, not in ZIP format."));
  495. dialog_error->popup_centered();
  496. return;
  497. }
  498. // Find the zip_root
  499. String zip_root;
  500. int ret = unzGoToFirstFile(pkg);
  501. while (ret == UNZ_OK) {
  502. unz_file_info info;
  503. char fname[16384];
  504. unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0);
  505. String name = String::utf8(fname);
  506. if (name.ends_with("project.godot")) {
  507. zip_root = name.substr(0, name.rfind("project.godot"));
  508. break;
  509. }
  510. ret = unzGoToNextFile(pkg);
  511. }
  512. ret = unzGoToFirstFile(pkg);
  513. Vector<String> failed_files;
  514. while (ret == UNZ_OK) {
  515. //get filename
  516. unz_file_info info;
  517. char fname[16384];
  518. ret = unzGetCurrentFileInfo(pkg, &info, fname, 16384, nullptr, 0, nullptr, 0);
  519. if (ret != UNZ_OK) {
  520. break;
  521. }
  522. String path = String::utf8(fname);
  523. if (path.is_empty() || path == zip_root || !zip_root.is_subsequence_of(path)) {
  524. //
  525. } else if (path.ends_with("/")) { // a dir
  526. path = path.substr(0, path.length() - 1);
  527. String rel_path = path.substr(zip_root.length());
  528. Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
  529. da->make_dir(dir.path_join(rel_path));
  530. } else {
  531. Vector<uint8_t> uncomp_data;
  532. uncomp_data.resize(info.uncompressed_size);
  533. String rel_path = path.substr(zip_root.length());
  534. //read
  535. unzOpenCurrentFile(pkg);
  536. ret = unzReadCurrentFile(pkg, uncomp_data.ptrw(), uncomp_data.size());
  537. ERR_BREAK_MSG(ret < 0, vformat("An error occurred while attempting to read from file: %s. This file will not be used.", rel_path));
  538. unzCloseCurrentFile(pkg);
  539. Ref<FileAccess> f = FileAccess::open(dir.path_join(rel_path), FileAccess::WRITE);
  540. if (f.is_valid()) {
  541. f->store_buffer(uncomp_data.ptr(), uncomp_data.size());
  542. } else {
  543. failed_files.push_back(rel_path);
  544. }
  545. }
  546. ret = unzGoToNextFile(pkg);
  547. }
  548. unzClose(pkg);
  549. if (failed_files.size()) {
  550. String err_msg = TTR("The following files failed extraction from package:") + "\n\n";
  551. for (int i = 0; i < failed_files.size(); i++) {
  552. if (i > 15) {
  553. err_msg += "\nAnd " + itos(failed_files.size() - i) + " more files.";
  554. break;
  555. }
  556. err_msg += failed_files[i] + "\n";
  557. }
  558. dialog_error->set_text(err_msg);
  559. dialog_error->popup_centered();
  560. } else if (!project_path->get_text().ends_with(".zip")) {
  561. dialog_error->set_text(TTR("Package installed successfully!"));
  562. dialog_error->popup_centered();
  563. }
  564. }
  565. }
  566. dir = dir.replace("\\", "/");
  567. if (dir.ends_with("/")) {
  568. dir = dir.substr(0, dir.length() - 1);
  569. }
  570. hide();
  571. emit_signal(SNAME("project_created"), dir);
  572. }
  573. }
  574. void _remove_created_folder() {
  575. if (!created_folder_path.is_empty()) {
  576. Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
  577. d->remove(created_folder_path);
  578. create_dir->set_disabled(false);
  579. created_folder_path = "";
  580. }
  581. }
  582. void cancel_pressed() override {
  583. _remove_created_folder();
  584. project_path->clear();
  585. _path_text_changed("");
  586. project_name->clear();
  587. _text_changed("");
  588. if (status_rect->get_texture() == msg->get_theme_icon(SNAME("StatusError"), SNAME("EditorIcons"))) {
  589. msg->show();
  590. }
  591. if (install_status_rect->get_texture() == msg->get_theme_icon(SNAME("StatusError"), SNAME("EditorIcons"))) {
  592. msg->show();
  593. }
  594. }
  595. void _notification(int p_what) {
  596. switch (p_what) {
  597. case NOTIFICATION_WM_CLOSE_REQUEST: {
  598. _remove_created_folder();
  599. } break;
  600. }
  601. }
  602. protected:
  603. static void _bind_methods() {
  604. ADD_SIGNAL(MethodInfo("project_created"));
  605. ADD_SIGNAL(MethodInfo("projects_updated"));
  606. }
  607. public:
  608. void set_zip_path(const String &p_path) {
  609. zip_path = p_path;
  610. }
  611. void set_zip_title(const String &p_title) {
  612. zip_title = p_title;
  613. }
  614. void set_mode(Mode p_mode) {
  615. mode = p_mode;
  616. }
  617. void set_project_path(const String &p_path) {
  618. project_path->set_text(p_path);
  619. }
  620. void show_dialog() {
  621. if (mode == MODE_RENAME) {
  622. project_path->set_editable(false);
  623. browse->hide();
  624. install_browse->hide();
  625. set_title(TTR("Rename Project"));
  626. set_ok_button_text(TTR("Rename"));
  627. name_container->show();
  628. status_rect->hide();
  629. msg->hide();
  630. install_path_container->hide();
  631. install_status_rect->hide();
  632. renderer_container->hide();
  633. default_files_container->hide();
  634. get_ok_button()->set_disabled(false);
  635. // Fetch current name from project.godot to prefill the text input.
  636. ConfigFile cfg;
  637. String project_godot = project_path->get_text().path_join("project.godot");
  638. Error err = cfg.load(project_godot);
  639. if (err != OK) {
  640. set_message(vformat(TTR("Couldn't load project at '%s' (error %d). It may be missing or corrupted."), project_godot, err), MESSAGE_ERROR);
  641. status_rect->show();
  642. msg->show();
  643. get_ok_button()->set_disabled(true);
  644. } else {
  645. String cur_name = cfg.get_value("application", "config/name", "");
  646. project_name->set_text(cur_name);
  647. _text_changed(cur_name);
  648. }
  649. project_name->call_deferred(SNAME("grab_focus"));
  650. create_dir->hide();
  651. } else {
  652. fav_dir = EDITOR_GET("filesystem/directories/default_project_path");
  653. if (!fav_dir.is_empty()) {
  654. project_path->set_text(fav_dir);
  655. fdialog->set_current_dir(fav_dir);
  656. } else {
  657. Ref<DirAccess> d = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
  658. project_path->set_text(d->get_current_dir());
  659. fdialog->set_current_dir(d->get_current_dir());
  660. }
  661. String proj = TTR("New Game Project");
  662. project_name->set_text(proj);
  663. _text_changed(proj);
  664. project_path->set_editable(true);
  665. browse->set_disabled(false);
  666. browse->show();
  667. install_browse->set_disabled(false);
  668. install_browse->show();
  669. create_dir->show();
  670. status_rect->show();
  671. install_status_rect->show();
  672. msg->show();
  673. if (mode == MODE_IMPORT) {
  674. set_title(TTR("Import Existing Project"));
  675. set_ok_button_text(TTR("Import & Edit"));
  676. name_container->hide();
  677. install_path_container->hide();
  678. renderer_container->hide();
  679. default_files_container->hide();
  680. project_path->grab_focus();
  681. } else if (mode == MODE_NEW) {
  682. set_title(TTR("Create New Project"));
  683. set_ok_button_text(TTR("Create & Edit"));
  684. name_container->show();
  685. install_path_container->hide();
  686. renderer_container->show();
  687. default_files_container->show();
  688. project_name->call_deferred(SNAME("grab_focus"));
  689. project_name->call_deferred(SNAME("select_all"));
  690. } else if (mode == MODE_INSTALL) {
  691. set_title(TTR("Install Project:") + " " + zip_title);
  692. set_ok_button_text(TTR("Install & Edit"));
  693. project_name->set_text(zip_title);
  694. name_container->show();
  695. install_path_container->hide();
  696. renderer_container->hide();
  697. default_files_container->hide();
  698. project_path->grab_focus();
  699. }
  700. _test_path();
  701. }
  702. popup_centered(Size2(500, 0) * EDSCALE);
  703. }
  704. ProjectDialog() {
  705. VBoxContainer *vb = memnew(VBoxContainer);
  706. add_child(vb);
  707. name_container = memnew(VBoxContainer);
  708. vb->add_child(name_container);
  709. Label *l = memnew(Label);
  710. l->set_text(TTR("Project Name:"));
  711. name_container->add_child(l);
  712. HBoxContainer *pnhb = memnew(HBoxContainer);
  713. name_container->add_child(pnhb);
  714. project_name = memnew(LineEdit);
  715. project_name->set_h_size_flags(Control::SIZE_EXPAND_FILL);
  716. pnhb->add_child(project_name);
  717. create_dir = memnew(Button);
  718. pnhb->add_child(create_dir);
  719. create_dir->set_text(TTR("Create Folder"));
  720. create_dir->connect("pressed", callable_mp(this, &ProjectDialog::_create_folder));
  721. path_container = memnew(VBoxContainer);
  722. vb->add_child(path_container);
  723. l = memnew(Label);
  724. l->set_text(TTR("Project Path:"));
  725. path_container->add_child(l);
  726. HBoxContainer *pphb = memnew(HBoxContainer);
  727. path_container->add_child(pphb);
  728. project_path = memnew(LineEdit);
  729. project_path->set_h_size_flags(Control::SIZE_EXPAND_FILL);
  730. project_path->set_structured_text_bidi_override(TextServer::STRUCTURED_TEXT_FILE);
  731. pphb->add_child(project_path);
  732. install_path_container = memnew(VBoxContainer);
  733. vb->add_child(install_path_container);
  734. l = memnew(Label);
  735. l->set_text(TTR("Project Installation Path:"));
  736. install_path_container->add_child(l);
  737. HBoxContainer *iphb = memnew(HBoxContainer);
  738. install_path_container->add_child(iphb);
  739. install_path = memnew(LineEdit);
  740. install_path->set_h_size_flags(Control::SIZE_EXPAND_FILL);
  741. install_path->set_structured_text_bidi_override(TextServer::STRUCTURED_TEXT_FILE);
  742. iphb->add_child(install_path);
  743. // status icon
  744. status_rect = memnew(TextureRect);
  745. status_rect->set_stretch_mode(TextureRect::STRETCH_KEEP_CENTERED);
  746. pphb->add_child(status_rect);
  747. browse = memnew(Button);
  748. browse->set_text(TTR("Browse"));
  749. browse->connect("pressed", callable_mp(this, &ProjectDialog::_browse_path));
  750. pphb->add_child(browse);
  751. // install status icon
  752. install_status_rect = memnew(TextureRect);
  753. install_status_rect->set_stretch_mode(TextureRect::STRETCH_KEEP_CENTERED);
  754. iphb->add_child(install_status_rect);
  755. install_browse = memnew(Button);
  756. install_browse->set_text(TTR("Browse"));
  757. install_browse->connect("pressed", callable_mp(this, &ProjectDialog::_browse_install_path));
  758. iphb->add_child(install_browse);
  759. msg = memnew(Label);
  760. msg->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
  761. vb->add_child(msg);
  762. // Renderer selection.
  763. renderer_container = memnew(VBoxContainer);
  764. vb->add_child(renderer_container);
  765. l = memnew(Label);
  766. l->set_text(TTR("Renderer:"));
  767. renderer_container->add_child(l);
  768. HBoxContainer *rshc = memnew(HBoxContainer);
  769. renderer_container->add_child(rshc);
  770. renderer_button_group.instantiate();
  771. // Left hand side, used for checkboxes to select renderer.
  772. Container *rvb = memnew(VBoxContainer);
  773. rshc->add_child(rvb);
  774. String default_renderer_type = "forward_plus";
  775. if (EditorSettings::get_singleton()->has_setting("project_manager/default_renderer")) {
  776. default_renderer_type = EditorSettings::get_singleton()->get_setting("project_manager/default_renderer");
  777. }
  778. Button *rs_button = memnew(CheckBox);
  779. rs_button->set_button_group(renderer_button_group);
  780. rs_button->set_text(TTR("Forward+"));
  781. #if defined(WEB_ENABLED)
  782. rs_button->set_disabled(true);
  783. #endif
  784. rs_button->set_meta(SNAME("rendering_method"), "forward_plus");
  785. rs_button->connect("pressed", callable_mp(this, &ProjectDialog::_renderer_selected));
  786. rvb->add_child(rs_button);
  787. if (default_renderer_type == "forward_plus") {
  788. rs_button->set_pressed(true);
  789. }
  790. rs_button = memnew(CheckBox);
  791. rs_button->set_button_group(renderer_button_group);
  792. rs_button->set_text(TTR("Mobile"));
  793. #if defined(WEB_ENABLED)
  794. rs_button->set_disabled(true);
  795. #endif
  796. rs_button->set_meta(SNAME("rendering_method"), "mobile");
  797. rs_button->connect("pressed", callable_mp(this, &ProjectDialog::_renderer_selected));
  798. rvb->add_child(rs_button);
  799. if (default_renderer_type == "mobile") {
  800. rs_button->set_pressed(true);
  801. }
  802. rs_button = memnew(CheckBox);
  803. rs_button->set_button_group(renderer_button_group);
  804. rs_button->set_text(TTR("Compatibility"));
  805. #if !defined(GLES3_ENABLED)
  806. rs_button->set_disabled(true);
  807. #endif
  808. rs_button->set_meta(SNAME("rendering_method"), "gl_compatibility");
  809. rs_button->connect("pressed", callable_mp(this, &ProjectDialog::_renderer_selected));
  810. rvb->add_child(rs_button);
  811. #if defined(GLES3_ENABLED)
  812. if (default_renderer_type == "gl_compatibility") {
  813. rs_button->set_pressed(true);
  814. }
  815. #endif
  816. rshc->add_child(memnew(VSeparator));
  817. // Right hand side, used for text explaining each choice.
  818. rvb = memnew(VBoxContainer);
  819. rvb->set_h_size_flags(Control::SIZE_EXPAND_FILL);
  820. rshc->add_child(rvb);
  821. renderer_info = memnew(Label);
  822. renderer_info->set_modulate(Color(1, 1, 1, 0.7));
  823. rvb->add_child(renderer_info);
  824. _renderer_selected();
  825. l = memnew(Label);
  826. l->set_text(TTR("The renderer can be changed later, but scenes may need to be adjusted."));
  827. // Add some extra spacing to separate it from the list above and the buttons below.
  828. l->set_custom_minimum_size(Size2(0, 40) * EDSCALE);
  829. l->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
  830. l->set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER);
  831. l->set_modulate(Color(1, 1, 1, 0.7));
  832. renderer_container->add_child(l);
  833. default_files_container = memnew(HBoxContainer);
  834. vb->add_child(default_files_container);
  835. l = memnew(Label);
  836. l->set_text(TTR("Version Control Metadata:"));
  837. default_files_container->add_child(l);
  838. vcs_metadata_selection = memnew(OptionButton);
  839. vcs_metadata_selection->set_custom_minimum_size(Size2(100, 20));
  840. vcs_metadata_selection->add_item(TTR("None"), (int)EditorVCSInterface::VCSMetadata::NONE);
  841. vcs_metadata_selection->add_item(TTR("Git"), (int)EditorVCSInterface::VCSMetadata::GIT);
  842. vcs_metadata_selection->select((int)EditorVCSInterface::VCSMetadata::GIT);
  843. default_files_container->add_child(vcs_metadata_selection);
  844. Control *spacer = memnew(Control);
  845. spacer->set_h_size_flags(Control::SIZE_EXPAND_FILL);
  846. default_files_container->add_child(spacer);
  847. fdialog = memnew(EditorFileDialog);
  848. fdialog->set_previews_enabled(false); //Crucial, otherwise the engine crashes.
  849. fdialog->set_access(EditorFileDialog::ACCESS_FILESYSTEM);
  850. fdialog_install = memnew(EditorFileDialog);
  851. fdialog_install->set_previews_enabled(false); //Crucial, otherwise the engine crashes.
  852. fdialog_install->set_access(EditorFileDialog::ACCESS_FILESYSTEM);
  853. add_child(fdialog);
  854. add_child(fdialog_install);
  855. project_name->connect("text_changed", callable_mp(this, &ProjectDialog::_text_changed));
  856. project_path->connect("text_changed", callable_mp(this, &ProjectDialog::_path_text_changed));
  857. install_path->connect("text_changed", callable_mp(this, &ProjectDialog::_path_text_changed));
  858. fdialog->connect("dir_selected", callable_mp(this, &ProjectDialog::_path_selected));
  859. fdialog->connect("file_selected", callable_mp(this, &ProjectDialog::_file_selected));
  860. fdialog_install->connect("dir_selected", callable_mp(this, &ProjectDialog::_install_path_selected));
  861. fdialog_install->connect("file_selected", callable_mp(this, &ProjectDialog::_install_path_selected));
  862. set_hide_on_ok(false);
  863. mode = MODE_NEW;
  864. dialog_error = memnew(AcceptDialog);
  865. add_child(dialog_error);
  866. }
  867. };
  868. class ProjectListItemControl : public HBoxContainer {
  869. GDCLASS(ProjectListItemControl, HBoxContainer)
  870. public:
  871. TextureButton *favorite_button;
  872. TextureRect *icon;
  873. bool icon_needs_reload;
  874. bool hover;
  875. ProjectListItemControl() {
  876. favorite_button = nullptr;
  877. icon = nullptr;
  878. icon_needs_reload = true;
  879. hover = false;
  880. set_focus_mode(FocusMode::FOCUS_ALL);
  881. }
  882. void set_is_favorite(bool fav) {
  883. favorite_button->set_modulate(fav ? Color(1, 1, 1, 1) : Color(1, 1, 1, 0.2));
  884. }
  885. void _notification(int p_what) {
  886. switch (p_what) {
  887. case NOTIFICATION_MOUSE_ENTER: {
  888. hover = true;
  889. queue_redraw();
  890. } break;
  891. case NOTIFICATION_MOUSE_EXIT: {
  892. hover = false;
  893. queue_redraw();
  894. } break;
  895. case NOTIFICATION_DRAW: {
  896. if (hover) {
  897. draw_style_box(get_theme_stylebox(SNAME("hover"), SNAME("Tree")), Rect2(Point2(), get_size()));
  898. }
  899. } break;
  900. }
  901. }
  902. };
  903. class ProjectList : public ScrollContainer {
  904. GDCLASS(ProjectList, ScrollContainer)
  905. public:
  906. static const char *SIGNAL_SELECTION_CHANGED;
  907. static const char *SIGNAL_PROJECT_ASK_OPEN;
  908. enum MenuOptions {
  909. GLOBAL_NEW_WINDOW,
  910. GLOBAL_OPEN_PROJECT
  911. };
  912. // Can often be passed by copy
  913. struct Item {
  914. String project_name;
  915. String description;
  916. String path;
  917. String icon;
  918. String main_scene;
  919. PackedStringArray unsupported_features;
  920. uint64_t last_edited = 0;
  921. bool favorite = false;
  922. bool grayed = false;
  923. bool missing = false;
  924. int version = 0;
  925. ProjectListItemControl *control = nullptr;
  926. Item() {}
  927. Item(const String &p_name,
  928. const String &p_description,
  929. const String &p_path,
  930. const String &p_icon,
  931. const String &p_main_scene,
  932. const PackedStringArray &p_unsupported_features,
  933. uint64_t p_last_edited,
  934. bool p_favorite,
  935. bool p_grayed,
  936. bool p_missing,
  937. int p_version) {
  938. project_name = p_name;
  939. description = p_description;
  940. path = p_path;
  941. icon = p_icon;
  942. main_scene = p_main_scene;
  943. unsupported_features = p_unsupported_features;
  944. last_edited = p_last_edited;
  945. favorite = p_favorite;
  946. grayed = p_grayed;
  947. missing = p_missing;
  948. version = p_version;
  949. control = nullptr;
  950. }
  951. _FORCE_INLINE_ bool operator==(const Item &l) const {
  952. return path == l.path;
  953. }
  954. };
  955. bool project_opening_initiated;
  956. ProjectList();
  957. ~ProjectList();
  958. void _global_menu_new_window(const Variant &p_tag);
  959. void _global_menu_open_project(const Variant &p_tag);
  960. void update_dock_menu();
  961. void migrate_config();
  962. void load_projects();
  963. void set_search_term(String p_search_term);
  964. void set_order_option(int p_option);
  965. void sort_projects();
  966. int get_project_count() const;
  967. void select_project(int p_index);
  968. void select_first_visible_project();
  969. void erase_selected_projects(bool p_delete_project_contents);
  970. Vector<Item> get_selected_projects() const;
  971. const HashSet<String> &get_selected_project_keys() const;
  972. void ensure_project_visible(int p_index);
  973. int get_single_selected_index() const;
  974. bool is_any_project_missing() const;
  975. void erase_missing_projects();
  976. int refresh_project(const String &dir_path);
  977. void add_project(const String &dir_path, bool favorite);
  978. void save_config();
  979. void set_project_version(const String &p_project_path, int version);
  980. private:
  981. static void _bind_methods();
  982. void _notification(int p_what);
  983. void _panel_draw(Node *p_hb);
  984. void _panel_input(const Ref<InputEvent> &p_ev, Node *p_hb);
  985. void _favorite_pressed(Node *p_hb);
  986. void _show_project(const String &p_path);
  987. void select_range(int p_begin, int p_end);
  988. void toggle_select(int p_index);
  989. void create_project_item_control(int p_index);
  990. void remove_project(int p_index, bool p_update_settings);
  991. void update_icons_async();
  992. void load_project_icon(int p_index);
  993. static Item load_project_data(const String &p_property_key, bool p_favorite);
  994. String _search_term;
  995. FilterOption _order_option;
  996. HashSet<String> _selected_project_paths;
  997. String _last_clicked; // Project key
  998. VBoxContainer *_scroll_children;
  999. int _icon_load_index;
  1000. Vector<Item> _projects;
  1001. ConfigFile _config;
  1002. String _config_path;
  1003. };
  1004. struct ProjectListComparator {
  1005. FilterOption order_option = FilterOption::EDIT_DATE;
  1006. // operator<
  1007. _FORCE_INLINE_ bool operator()(const ProjectList::Item &a, const ProjectList::Item &b) const {
  1008. if (a.favorite && !b.favorite) {
  1009. return true;
  1010. }
  1011. if (b.favorite && !a.favorite) {
  1012. return false;
  1013. }
  1014. switch (order_option) {
  1015. case PATH:
  1016. return a.path < b.path;
  1017. case EDIT_DATE:
  1018. return a.last_edited > b.last_edited;
  1019. default:
  1020. return a.project_name < b.project_name;
  1021. }
  1022. }
  1023. };
  1024. ProjectList::ProjectList() {
  1025. _order_option = FilterOption::EDIT_DATE;
  1026. _scroll_children = memnew(VBoxContainer);
  1027. _scroll_children->set_h_size_flags(Control::SIZE_EXPAND_FILL);
  1028. add_child(_scroll_children);
  1029. _icon_load_index = 0;
  1030. project_opening_initiated = false;
  1031. _config_path = EditorPaths::get_singleton()->get_data_dir().path_join("projects.cfg");
  1032. }
  1033. ProjectList::~ProjectList() {
  1034. }
  1035. void ProjectList::update_icons_async() {
  1036. _icon_load_index = 0;
  1037. set_process(true);
  1038. }
  1039. void ProjectList::_notification(int p_what) {
  1040. switch (p_what) {
  1041. case NOTIFICATION_PROCESS: {
  1042. // Load icons as a coroutine to speed up launch when you have hundreds of projects
  1043. if (_icon_load_index < _projects.size()) {
  1044. Item &item = _projects.write[_icon_load_index];
  1045. if (item.control->icon_needs_reload) {
  1046. load_project_icon(_icon_load_index);
  1047. }
  1048. _icon_load_index++;
  1049. } else {
  1050. set_process(false);
  1051. }
  1052. } break;
  1053. }
  1054. }
  1055. void ProjectList::load_project_icon(int p_index) {
  1056. Item &item = _projects.write[p_index];
  1057. Ref<Texture2D> default_icon = get_theme_icon(SNAME("DefaultProjectIcon"), SNAME("EditorIcons"));
  1058. Ref<Texture2D> icon;
  1059. if (!item.icon.is_empty()) {
  1060. Ref<Image> img;
  1061. img.instantiate();
  1062. Error err = img->load(item.icon.replace_first("res://", item.path + "/"));
  1063. if (err == OK) {
  1064. img->resize(default_icon->get_width(), default_icon->get_height(), Image::INTERPOLATE_LANCZOS);
  1065. icon = ImageTexture::create_from_image(img);
  1066. }
  1067. }
  1068. if (icon.is_null()) {
  1069. icon = default_icon;
  1070. }
  1071. // The default project icon is 128×128 to look crisp on hiDPI displays,
  1072. // but we want the actual displayed size to be 64×64 on loDPI displays.
  1073. item.control->icon->set_expand_mode(TextureRect::EXPAND_IGNORE_SIZE);
  1074. item.control->icon->set_custom_minimum_size(Size2(64, 64) * EDSCALE);
  1075. item.control->icon->set_stretch_mode(TextureRect::STRETCH_KEEP_ASPECT_CENTERED);
  1076. item.control->icon->set_texture(icon);
  1077. item.control->icon_needs_reload = false;
  1078. }
  1079. // Load project data from p_property_key and return it in a ProjectList::Item. p_favorite is passed directly into the Item.
  1080. ProjectList::Item ProjectList::load_project_data(const String &p_path, bool p_favorite) {
  1081. String conf = p_path.path_join("project.godot");
  1082. bool grayed = false;
  1083. bool missing = false;
  1084. Ref<ConfigFile> cf = memnew(ConfigFile);
  1085. Error cf_err = cf->load(conf);
  1086. int config_version = 0;
  1087. String project_name = TTR("Unnamed Project");
  1088. if (cf_err == OK) {
  1089. String cf_project_name = static_cast<String>(cf->get_value("application", "config/name", ""));
  1090. if (!cf_project_name.is_empty()) {
  1091. project_name = cf_project_name.xml_unescape();
  1092. }
  1093. config_version = (int)cf->get_value("", "config_version", 0);
  1094. }
  1095. if (config_version > ProjectSettings::CONFIG_VERSION) {
  1096. // Comes from an incompatible (more recent) Godot version, gray it out.
  1097. grayed = true;
  1098. }
  1099. const String description = cf->get_value("application", "config/description", "");
  1100. const String icon = cf->get_value("application", "config/icon", "");
  1101. const String main_scene = cf->get_value("application", "run/main_scene", "");
  1102. PackedStringArray project_features = cf->get_value("application", "config/features", PackedStringArray());
  1103. PackedStringArray unsupported_features = ProjectSettings::get_unsupported_features(project_features);
  1104. uint64_t last_edited = 0;
  1105. if (cf_err == OK) {
  1106. // The modification date marks the date the project was last edited.
  1107. // This is because the `project.godot` file will always be modified
  1108. // when editing a project (but not when running it).
  1109. last_edited = FileAccess::get_modified_time(conf);
  1110. String fscache = p_path.path_join(".fscache");
  1111. if (FileAccess::exists(fscache)) {
  1112. uint64_t cache_modified = FileAccess::get_modified_time(fscache);
  1113. if (cache_modified > last_edited) {
  1114. last_edited = cache_modified;
  1115. }
  1116. }
  1117. } else {
  1118. grayed = true;
  1119. missing = true;
  1120. print_line("Project is missing: " + conf);
  1121. }
  1122. return Item(project_name, description, p_path, icon, main_scene, unsupported_features, last_edited, p_favorite, grayed, missing, config_version);
  1123. }
  1124. void ProjectList::migrate_config() {
  1125. // Proposal #1637 moved the project list from editor settings to a separate config file
  1126. // If the new config file doesn't exist, populate it from EditorSettings
  1127. if (FileAccess::exists(_config_path)) {
  1128. return;
  1129. }
  1130. List<PropertyInfo> properties;
  1131. EditorSettings::get_singleton()->get_property_list(&properties);
  1132. for (const PropertyInfo &E : properties) {
  1133. // This is actually something like "projects/C:::Documents::Godot::Projects::MyGame"
  1134. String property_key = E.name;
  1135. if (!property_key.begins_with("projects/")) {
  1136. continue;
  1137. }
  1138. String path = EDITOR_GET(property_key);
  1139. print_line("Migrating legacy project '" + path + "'.");
  1140. String favoriteKey = "favorite_projects/" + property_key.get_slice("/", 1);
  1141. bool favorite = EditorSettings::get_singleton()->has_setting(favoriteKey);
  1142. add_project(path, favorite);
  1143. if (favorite) {
  1144. EditorSettings::get_singleton()->erase(favoriteKey);
  1145. }
  1146. EditorSettings::get_singleton()->erase(property_key);
  1147. }
  1148. save_config();
  1149. }
  1150. void ProjectList::load_projects() {
  1151. // This is a full, hard reload of the list. Don't call this unless really required, it's expensive.
  1152. // If you have 150 projects, it may read through 150 files on your disk at once + load 150 icons.
  1153. // Clear whole list
  1154. for (int i = 0; i < _projects.size(); ++i) {
  1155. Item &project = _projects.write[i];
  1156. CRASH_COND(project.control == nullptr);
  1157. memdelete(project.control); // Why not queue_free()?
  1158. }
  1159. _projects.clear();
  1160. _last_clicked = "";
  1161. _selected_project_paths.clear();
  1162. List<String> sections;
  1163. _config.load(_config_path);
  1164. _config.get_sections(&sections);
  1165. for (const String &path : sections) {
  1166. bool favorite = _config.get_value(path, "favorite", false);
  1167. _projects.push_back(load_project_data(path, favorite));
  1168. }
  1169. // Create controls
  1170. for (int i = 0; i < _projects.size(); ++i) {
  1171. create_project_item_control(i);
  1172. }
  1173. sort_projects();
  1174. set_v_scroll(0);
  1175. update_icons_async();
  1176. update_dock_menu();
  1177. }
  1178. void ProjectList::update_dock_menu() {
  1179. if (!DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_GLOBAL_MENU)) {
  1180. return;
  1181. }
  1182. DisplayServer::get_singleton()->global_menu_clear("_dock");
  1183. int favs_added = 0;
  1184. int total_added = 0;
  1185. for (int i = 0; i < _projects.size(); ++i) {
  1186. if (!_projects[i].grayed && !_projects[i].missing) {
  1187. if (_projects[i].favorite) {
  1188. favs_added++;
  1189. } else {
  1190. if (favs_added != 0) {
  1191. DisplayServer::get_singleton()->global_menu_add_separator("_dock");
  1192. }
  1193. favs_added = 0;
  1194. }
  1195. DisplayServer::get_singleton()->global_menu_add_item("_dock", _projects[i].project_name + " ( " + _projects[i].path + " )", callable_mp(this, &ProjectList::_global_menu_open_project), Callable(), i);
  1196. total_added++;
  1197. }
  1198. }
  1199. if (total_added != 0) {
  1200. DisplayServer::get_singleton()->global_menu_add_separator("_dock");
  1201. }
  1202. DisplayServer::get_singleton()->global_menu_add_item("_dock", TTR("New Window"), callable_mp(this, &ProjectList::_global_menu_new_window));
  1203. }
  1204. void ProjectList::_global_menu_new_window(const Variant &p_tag) {
  1205. List<String> args;
  1206. args.push_back("-p");
  1207. OS::get_singleton()->create_instance(args);
  1208. }
  1209. void ProjectList::_global_menu_open_project(const Variant &p_tag) {
  1210. int idx = (int)p_tag;
  1211. if (idx >= 0 && idx < _projects.size()) {
  1212. String conf = _projects[idx].path.path_join("project.godot");
  1213. List<String> args;
  1214. args.push_back(conf);
  1215. OS::get_singleton()->create_instance(args);
  1216. }
  1217. }
  1218. void ProjectList::create_project_item_control(int p_index) {
  1219. // Will be added last in the list, so make sure indexes match
  1220. ERR_FAIL_COND(p_index != _scroll_children->get_child_count());
  1221. Item &item = _projects.write[p_index];
  1222. ERR_FAIL_COND(item.control != nullptr); // Already created
  1223. Ref<Texture2D> favorite_icon = get_theme_icon(SNAME("Favorites"), SNAME("EditorIcons"));
  1224. Color font_color = get_theme_color(SNAME("font_color"), SNAME("Tree"));
  1225. ProjectListItemControl *hb = memnew(ProjectListItemControl);
  1226. hb->connect("draw", callable_mp(this, &ProjectList::_panel_draw).bind(hb));
  1227. hb->connect("gui_input", callable_mp(this, &ProjectList::_panel_input).bind(hb));
  1228. hb->add_theme_constant_override("separation", 10 * EDSCALE);
  1229. hb->set_tooltip_text(item.description);
  1230. VBoxContainer *favorite_box = memnew(VBoxContainer);
  1231. favorite_box->set_name("FavoriteBox");
  1232. TextureButton *favorite = memnew(TextureButton);
  1233. favorite->set_name("FavoriteButton");
  1234. favorite->set_texture_normal(favorite_icon);
  1235. // This makes the project's "hover" style display correctly when hovering the favorite icon.
  1236. favorite->set_mouse_filter(MOUSE_FILTER_PASS);
  1237. favorite->connect("pressed", callable_mp(this, &ProjectList::_favorite_pressed).bind(hb));
  1238. favorite_box->add_child(favorite);
  1239. favorite_box->set_alignment(BoxContainer::ALIGNMENT_CENTER);
  1240. hb->add_child(favorite_box);
  1241. hb->favorite_button = favorite;
  1242. hb->set_is_favorite(item.favorite);
  1243. TextureRect *tf = memnew(TextureRect);
  1244. // The project icon may not be loaded by the time the control is displayed,
  1245. // so use a loading placeholder.
  1246. tf->set_texture(get_theme_icon(SNAME("ProjectIconLoading"), SNAME("EditorIcons")));
  1247. tf->set_v_size_flags(SIZE_SHRINK_CENTER);
  1248. if (item.missing) {
  1249. tf->set_modulate(Color(1, 1, 1, 0.5));
  1250. }
  1251. hb->add_child(tf);
  1252. hb->icon = tf;
  1253. VBoxContainer *vb = memnew(VBoxContainer);
  1254. if (item.grayed) {
  1255. vb->set_modulate(Color(1, 1, 1, 0.5));
  1256. }
  1257. vb->set_h_size_flags(Control::SIZE_EXPAND_FILL);
  1258. hb->add_child(vb);
  1259. Control *ec = memnew(Control);
  1260. ec->set_custom_minimum_size(Size2(0, 1));
  1261. ec->set_mouse_filter(MOUSE_FILTER_PASS);
  1262. vb->add_child(ec);
  1263. { // Top half, title and unsupported features labels.
  1264. HBoxContainer *title_hb = memnew(HBoxContainer);
  1265. vb->add_child(title_hb);
  1266. Label *title = memnew(Label(!item.missing ? item.project_name : TTR("Missing Project")));
  1267. title->set_h_size_flags(Control::SIZE_EXPAND_FILL);
  1268. title->add_theme_font_override("font", get_theme_font(SNAME("title"), SNAME("EditorFonts")));
  1269. title->add_theme_font_size_override("font_size", get_theme_font_size(SNAME("title_size"), SNAME("EditorFonts")));
  1270. title->add_theme_color_override("font_color", font_color);
  1271. title->set_clip_text(true);
  1272. title_hb->add_child(title);
  1273. String unsupported_features_str = String(", ").join(item.unsupported_features);
  1274. int length = unsupported_features_str.length();
  1275. if (length > 0) {
  1276. Label *unsupported_label = memnew(Label(unsupported_features_str));
  1277. unsupported_label->set_custom_minimum_size(Size2(length * 15, 10) * EDSCALE);
  1278. unsupported_label->add_theme_font_override("font", get_theme_font(SNAME("title"), SNAME("EditorFonts")));
  1279. unsupported_label->add_theme_color_override("font_color", get_theme_color(SNAME("warning_color"), SNAME("Editor")));
  1280. unsupported_label->set_clip_text(true);
  1281. unsupported_label->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_RIGHT);
  1282. title_hb->add_child(unsupported_label);
  1283. Control *spacer = memnew(Control());
  1284. spacer->set_custom_minimum_size(Size2(10, 10));
  1285. title_hb->add_child(spacer);
  1286. }
  1287. }
  1288. { // Bottom half, containing the path and view folder button.
  1289. HBoxContainer *path_hb = memnew(HBoxContainer);
  1290. path_hb->set_h_size_flags(Control::SIZE_EXPAND_FILL);
  1291. vb->add_child(path_hb);
  1292. Button *show = memnew(Button);
  1293. // Display a folder icon if the project directory can be opened, or a "broken file" icon if it can't.
  1294. show->set_icon(get_theme_icon(!item.missing ? SNAME("Load") : SNAME("FileBroken"), SNAME("EditorIcons")));
  1295. show->set_flat(true);
  1296. if (!item.grayed) {
  1297. // Don't make the icon less prominent if the parent is already grayed out.
  1298. show->set_modulate(Color(1, 1, 1, 0.5));
  1299. }
  1300. path_hb->add_child(show);
  1301. if (!item.missing) {
  1302. #if !defined(ANDROID_ENABLED) && !defined(WEB_ENABLED)
  1303. show->connect("pressed", callable_mp(this, &ProjectList::_show_project).bind(item.path));
  1304. show->set_tooltip_text(TTR("Show in File Manager"));
  1305. #else
  1306. // Opening the system file manager is not supported on the Android and web editors.
  1307. show->hide();
  1308. #endif
  1309. } else {
  1310. show->set_tooltip_text(TTR("Error: Project is missing on the filesystem."));
  1311. }
  1312. Label *fpath = memnew(Label(item.path));
  1313. fpath->set_structured_text_bidi_override(TextServer::STRUCTURED_TEXT_FILE);
  1314. path_hb->add_child(fpath);
  1315. fpath->set_h_size_flags(Control::SIZE_EXPAND_FILL);
  1316. fpath->set_modulate(Color(1, 1, 1, 0.5));
  1317. fpath->add_theme_color_override("font_color", font_color);
  1318. fpath->set_clip_text(true);
  1319. }
  1320. _scroll_children->add_child(hb);
  1321. item.control = hb;
  1322. }
  1323. void ProjectList::set_search_term(String p_search_term) {
  1324. _search_term = p_search_term;
  1325. }
  1326. void ProjectList::set_order_option(int p_option) {
  1327. FilterOption selected = (FilterOption)p_option;
  1328. EditorSettings::get_singleton()->set("project_manager/sorting_order", p_option);
  1329. EditorSettings::get_singleton()->save();
  1330. _order_option = selected;
  1331. sort_projects();
  1332. }
  1333. void ProjectList::sort_projects() {
  1334. SortArray<Item, ProjectListComparator> sorter;
  1335. sorter.compare.order_option = _order_option;
  1336. sorter.sort(_projects.ptrw(), _projects.size());
  1337. for (int i = 0; i < _projects.size(); ++i) {
  1338. Item &item = _projects.write[i];
  1339. bool item_visible = true;
  1340. if (!_search_term.is_empty()) {
  1341. String search_path;
  1342. if (_search_term.contains("/")) {
  1343. // Search path will match the whole path
  1344. search_path = item.path;
  1345. } else {
  1346. // Search path will only match the last path component to make searching more strict
  1347. search_path = item.path.get_file();
  1348. }
  1349. // When searching, display projects whose name or path contain the search term
  1350. item_visible = item.project_name.findn(_search_term) != -1 || search_path.findn(_search_term) != -1;
  1351. }
  1352. item.control->set_visible(item_visible);
  1353. }
  1354. for (int i = 0; i < _projects.size(); ++i) {
  1355. Item &item = _projects.write[i];
  1356. item.control->get_parent()->move_child(item.control, i);
  1357. }
  1358. // Rewind the coroutine because order of projects changed
  1359. update_icons_async();
  1360. update_dock_menu();
  1361. }
  1362. const HashSet<String> &ProjectList::get_selected_project_keys() const {
  1363. // Faster if that's all you need
  1364. return _selected_project_paths;
  1365. }
  1366. Vector<ProjectList::Item> ProjectList::get_selected_projects() const {
  1367. Vector<Item> items;
  1368. if (_selected_project_paths.size() == 0) {
  1369. return items;
  1370. }
  1371. items.resize(_selected_project_paths.size());
  1372. int j = 0;
  1373. for (int i = 0; i < _projects.size(); ++i) {
  1374. const Item &item = _projects[i];
  1375. if (_selected_project_paths.has(item.path)) {
  1376. items.write[j++] = item;
  1377. }
  1378. }
  1379. ERR_FAIL_COND_V(j != items.size(), items);
  1380. return items;
  1381. }
  1382. void ProjectList::ensure_project_visible(int p_index) {
  1383. const Item &item = _projects[p_index];
  1384. ensure_control_visible(item.control);
  1385. }
  1386. int ProjectList::get_single_selected_index() const {
  1387. if (_selected_project_paths.size() == 0) {
  1388. // Default selection
  1389. return 0;
  1390. }
  1391. String key;
  1392. if (_selected_project_paths.size() == 1) {
  1393. // Only one selected
  1394. key = *_selected_project_paths.begin();
  1395. } else {
  1396. // Multiple selected, consider the last clicked one as "main"
  1397. key = _last_clicked;
  1398. }
  1399. for (int i = 0; i < _projects.size(); ++i) {
  1400. if (_projects[i].path == key) {
  1401. return i;
  1402. }
  1403. }
  1404. return 0;
  1405. }
  1406. void ProjectList::remove_project(int p_index, bool p_update_config) {
  1407. const Item item = _projects[p_index]; // Take a copy
  1408. _selected_project_paths.erase(item.path);
  1409. if (_last_clicked == item.path) {
  1410. _last_clicked = "";
  1411. }
  1412. memdelete(item.control);
  1413. _projects.remove_at(p_index);
  1414. if (p_update_config) {
  1415. _config.erase_section(item.path);
  1416. // Not actually saving the file, in case you are doing more changes to settings
  1417. }
  1418. update_dock_menu();
  1419. }
  1420. bool ProjectList::is_any_project_missing() const {
  1421. for (int i = 0; i < _projects.size(); ++i) {
  1422. if (_projects[i].missing) {
  1423. return true;
  1424. }
  1425. }
  1426. return false;
  1427. }
  1428. void ProjectList::erase_missing_projects() {
  1429. if (_projects.is_empty()) {
  1430. return;
  1431. }
  1432. int deleted_count = 0;
  1433. int remaining_count = 0;
  1434. for (int i = 0; i < _projects.size(); ++i) {
  1435. const Item &item = _projects[i];
  1436. if (item.missing) {
  1437. remove_project(i, true);
  1438. --i;
  1439. ++deleted_count;
  1440. } else {
  1441. ++remaining_count;
  1442. }
  1443. }
  1444. print_line("Removed " + itos(deleted_count) + " projects from the list, remaining " + itos(remaining_count) + " projects");
  1445. save_config();
  1446. }
  1447. int ProjectList::refresh_project(const String &dir_path) {
  1448. // Reloads information about a specific project.
  1449. // If it wasn't loaded and should be in the list, it is added (i.e new project).
  1450. // If it isn't in the list anymore, it is removed.
  1451. // If it is in the list but doesn't exist anymore, it is marked as missing.
  1452. bool should_be_in_list = _config.has_section(dir_path);
  1453. bool is_favourite = _config.get_value(dir_path, "favorite", false);
  1454. bool was_selected = _selected_project_paths.has(dir_path);
  1455. // Remove item in any case
  1456. for (int i = 0; i < _projects.size(); ++i) {
  1457. const Item &existing_item = _projects[i];
  1458. if (existing_item.path == dir_path) {
  1459. remove_project(i, false);
  1460. break;
  1461. }
  1462. }
  1463. int index = -1;
  1464. if (should_be_in_list) {
  1465. // Recreate it with updated info
  1466. Item item = load_project_data(dir_path, is_favourite);
  1467. _projects.push_back(item);
  1468. create_project_item_control(_projects.size() - 1);
  1469. sort_projects();
  1470. for (int i = 0; i < _projects.size(); ++i) {
  1471. if (_projects[i].path == dir_path) {
  1472. if (was_selected) {
  1473. select_project(i);
  1474. ensure_project_visible(i);
  1475. }
  1476. load_project_icon(i);
  1477. index = i;
  1478. break;
  1479. }
  1480. }
  1481. }
  1482. return index;
  1483. }
  1484. void ProjectList::add_project(const String &dir_path, bool favorite) {
  1485. if (!_config.has_section(dir_path)) {
  1486. _config.set_value(dir_path, "favorite", favorite);
  1487. }
  1488. }
  1489. void ProjectList::save_config() {
  1490. _config.save(_config_path);
  1491. }
  1492. void ProjectList::set_project_version(const String &p_project_path, int p_version) {
  1493. for (ProjectList::Item &E : _projects) {
  1494. if (E.path == p_project_path) {
  1495. E.version = p_version;
  1496. break;
  1497. }
  1498. }
  1499. }
  1500. int ProjectList::get_project_count() const {
  1501. return _projects.size();
  1502. }
  1503. void ProjectList::select_project(int p_index) {
  1504. Vector<Item> previous_selected_items = get_selected_projects();
  1505. _selected_project_paths.clear();
  1506. for (int i = 0; i < previous_selected_items.size(); ++i) {
  1507. previous_selected_items[i].control->queue_redraw();
  1508. }
  1509. toggle_select(p_index);
  1510. }
  1511. void ProjectList::select_first_visible_project() {
  1512. bool found = false;
  1513. for (int i = 0; i < _projects.size(); i++) {
  1514. if (_projects[i].control->is_visible()) {
  1515. select_project(i);
  1516. found = true;
  1517. break;
  1518. }
  1519. }
  1520. if (!found) {
  1521. // Deselect all projects if there are no visible projects in the list.
  1522. _selected_project_paths.clear();
  1523. }
  1524. }
  1525. inline void sort(int &a, int &b) {
  1526. if (a > b) {
  1527. int temp = a;
  1528. a = b;
  1529. b = temp;
  1530. }
  1531. }
  1532. void ProjectList::select_range(int p_begin, int p_end) {
  1533. sort(p_begin, p_end);
  1534. select_project(p_begin);
  1535. for (int i = p_begin + 1; i <= p_end; ++i) {
  1536. toggle_select(i);
  1537. }
  1538. }
  1539. void ProjectList::toggle_select(int p_index) {
  1540. Item &item = _projects.write[p_index];
  1541. if (_selected_project_paths.has(item.path)) {
  1542. _selected_project_paths.erase(item.path);
  1543. } else {
  1544. _selected_project_paths.insert(item.path);
  1545. }
  1546. item.control->queue_redraw();
  1547. }
  1548. void ProjectList::erase_selected_projects(bool p_delete_project_contents) {
  1549. if (_selected_project_paths.size() == 0) {
  1550. return;
  1551. }
  1552. for (int i = 0; i < _projects.size(); ++i) {
  1553. Item &item = _projects.write[i];
  1554. if (_selected_project_paths.has(item.path) && item.control->is_visible()) {
  1555. _config.erase_section(item.path);
  1556. if (p_delete_project_contents) {
  1557. OS::get_singleton()->move_to_trash(item.path);
  1558. }
  1559. memdelete(item.control);
  1560. _projects.remove_at(i);
  1561. --i;
  1562. }
  1563. }
  1564. save_config();
  1565. _selected_project_paths.clear();
  1566. _last_clicked = "";
  1567. update_dock_menu();
  1568. }
  1569. // Draws selected project highlight
  1570. void ProjectList::_panel_draw(Node *p_hb) {
  1571. Control *hb = Object::cast_to<Control>(p_hb);
  1572. if (is_layout_rtl() && get_v_scroll_bar()->is_visible_in_tree()) {
  1573. hb->draw_line(Point2(get_v_scroll_bar()->get_minimum_size().x, hb->get_size().y + 1), Point2(hb->get_size().x, hb->get_size().y + 1), get_theme_color(SNAME("guide_color"), SNAME("Tree")));
  1574. } else {
  1575. hb->draw_line(Point2(0, hb->get_size().y + 1), Point2(hb->get_size().x, hb->get_size().y + 1), get_theme_color(SNAME("guide_color"), SNAME("Tree")));
  1576. }
  1577. String key = _projects[p_hb->get_index()].path;
  1578. if (_selected_project_paths.has(key)) {
  1579. hb->draw_style_box(get_theme_stylebox(SNAME("selected"), SNAME("Tree")), Rect2(Point2(), hb->get_size()));
  1580. }
  1581. }
  1582. // Input for each item in the list
  1583. void ProjectList::_panel_input(const Ref<InputEvent> &p_ev, Node *p_hb) {
  1584. Ref<InputEventMouseButton> mb = p_ev;
  1585. int clicked_index = p_hb->get_index();
  1586. const Item &clicked_project = _projects[clicked_index];
  1587. if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) {
  1588. if (mb->is_shift_pressed() && _selected_project_paths.size() > 0 && !_last_clicked.is_empty() && clicked_project.path != _last_clicked) {
  1589. int anchor_index = -1;
  1590. for (int i = 0; i < _projects.size(); ++i) {
  1591. const Item &p = _projects[i];
  1592. if (p.path == _last_clicked) {
  1593. anchor_index = p.control->get_index();
  1594. break;
  1595. }
  1596. }
  1597. CRASH_COND(anchor_index == -1);
  1598. select_range(anchor_index, clicked_index);
  1599. } else if (mb->is_ctrl_pressed()) {
  1600. toggle_select(clicked_index);
  1601. } else {
  1602. _last_clicked = clicked_project.path;
  1603. select_project(clicked_index);
  1604. }
  1605. emit_signal(SNAME(SIGNAL_SELECTION_CHANGED));
  1606. // Do not allow opening a project more than once using a single project manager instance.
  1607. // Opening the same project in several editor instances at once can lead to various issues.
  1608. if (!mb->is_ctrl_pressed() && mb->is_double_click() && !project_opening_initiated) {
  1609. emit_signal(SNAME(SIGNAL_PROJECT_ASK_OPEN));
  1610. }
  1611. }
  1612. }
  1613. void ProjectList::_favorite_pressed(Node *p_hb) {
  1614. ProjectListItemControl *control = Object::cast_to<ProjectListItemControl>(p_hb);
  1615. int index = control->get_index();
  1616. Item item = _projects.write[index]; // Take copy
  1617. item.favorite = !item.favorite;
  1618. _config.set_value(item.path, "favorite", item.favorite);
  1619. save_config();
  1620. _projects.write[index] = item;
  1621. control->set_is_favorite(item.favorite);
  1622. sort_projects();
  1623. if (item.favorite) {
  1624. for (int i = 0; i < _projects.size(); ++i) {
  1625. if (_projects[i].path == item.path) {
  1626. ensure_project_visible(i);
  1627. break;
  1628. }
  1629. }
  1630. }
  1631. update_dock_menu();
  1632. }
  1633. void ProjectList::_show_project(const String &p_path) {
  1634. OS::get_singleton()->shell_open(String("file://") + p_path);
  1635. }
  1636. const char *ProjectList::SIGNAL_SELECTION_CHANGED = "selection_changed";
  1637. const char *ProjectList::SIGNAL_PROJECT_ASK_OPEN = "project_ask_open";
  1638. void ProjectList::_bind_methods() {
  1639. ADD_SIGNAL(MethodInfo(SIGNAL_SELECTION_CHANGED));
  1640. ADD_SIGNAL(MethodInfo(SIGNAL_PROJECT_ASK_OPEN));
  1641. }
  1642. ProjectManager *ProjectManager::singleton = nullptr;
  1643. void ProjectManager::_notification(int p_what) {
  1644. switch (p_what) {
  1645. case NOTIFICATION_TRANSLATION_CHANGED:
  1646. case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: {
  1647. settings_hb->set_anchors_and_offsets_preset(Control::PRESET_TOP_RIGHT);
  1648. queue_redraw();
  1649. } break;
  1650. case NOTIFICATION_ENTER_TREE: {
  1651. search_box->set_right_icon(get_theme_icon(SNAME("Search"), SNAME("EditorIcons")));
  1652. search_box->set_clear_button_enabled(true);
  1653. create_btn->set_icon(get_theme_icon(SNAME("Add"), SNAME("EditorIcons")));
  1654. import_btn->set_icon(get_theme_icon(SNAME("Load"), SNAME("EditorIcons")));
  1655. scan_btn->set_icon(get_theme_icon(SNAME("Search"), SNAME("EditorIcons")));
  1656. open_btn->set_icon(get_theme_icon(SNAME("Edit"), SNAME("EditorIcons")));
  1657. run_btn->set_icon(get_theme_icon(SNAME("Play"), SNAME("EditorIcons")));
  1658. rename_btn->set_icon(get_theme_icon(SNAME("Rename"), SNAME("EditorIcons")));
  1659. erase_btn->set_icon(get_theme_icon(SNAME("Remove"), SNAME("EditorIcons")));
  1660. erase_missing_btn->set_icon(get_theme_icon(SNAME("Clear"), SNAME("EditorIcons")));
  1661. Engine::get_singleton()->set_editor_hint(false);
  1662. } break;
  1663. case NOTIFICATION_RESIZED: {
  1664. if (open_templates && open_templates->is_visible()) {
  1665. open_templates->popup_centered();
  1666. }
  1667. if (asset_library) {
  1668. real_t size = get_size().x / EDSCALE;
  1669. // Adjust names of tabs to fit the new size.
  1670. if (size < 650) {
  1671. local_projects_hb->set_name(TTR("Local"));
  1672. asset_library->set_name(TTR("Asset Library"));
  1673. } else {
  1674. local_projects_hb->set_name(TTR("Local Projects"));
  1675. asset_library->set_name(TTR("Asset Library Projects"));
  1676. }
  1677. }
  1678. } break;
  1679. case NOTIFICATION_READY: {
  1680. int default_sorting = (int)EDITOR_GET("project_manager/sorting_order");
  1681. filter_option->select(default_sorting);
  1682. _project_list->set_order_option(default_sorting);
  1683. #ifndef ANDROID_ENABLED
  1684. if (_project_list->get_project_count() >= 1) {
  1685. // Focus on the search box immediately to allow the user
  1686. // to search without having to reach for their mouse
  1687. search_box->grab_focus();
  1688. }
  1689. #endif
  1690. if (asset_library) {
  1691. // Removes extra border margins.
  1692. asset_library->add_theme_style_override("panel", memnew(StyleBoxEmpty));
  1693. // Suggest browsing asset library to get templates/demos.
  1694. if (open_templates && _project_list->get_project_count() == 0) {
  1695. open_templates->popup_centered();
  1696. }
  1697. }
  1698. } break;
  1699. case NOTIFICATION_VISIBILITY_CHANGED: {
  1700. set_process_shortcut_input(is_visible_in_tree());
  1701. } break;
  1702. case NOTIFICATION_WM_CLOSE_REQUEST: {
  1703. _dim_window();
  1704. } break;
  1705. case NOTIFICATION_WM_ABOUT: {
  1706. _show_about();
  1707. } break;
  1708. }
  1709. }
  1710. Ref<Texture2D> ProjectManager::_file_dialog_get_icon(const String &p_path) {
  1711. if (p_path.get_extension().to_lower() == "godot") {
  1712. return singleton->icon_type_cache["GodotMonochrome"];
  1713. }
  1714. return singleton->icon_type_cache["Object"];
  1715. }
  1716. Ref<Texture2D> ProjectManager::_file_dialog_get_thumbnail(const String &p_path) {
  1717. if (p_path.get_extension().to_lower() == "godot") {
  1718. return singleton->icon_type_cache["GodotFile"];
  1719. }
  1720. return Ref<Texture2D>();
  1721. }
  1722. void ProjectManager::_build_icon_type_cache(Ref<Theme> p_theme) {
  1723. List<StringName> tl;
  1724. p_theme->get_icon_list(SNAME("EditorIcons"), &tl);
  1725. for (List<StringName>::Element *E = tl.front(); E; E = E->next()) {
  1726. icon_type_cache[E->get()] = p_theme->get_icon(E->get(), SNAME("EditorIcons"));
  1727. }
  1728. }
  1729. void ProjectManager::_dim_window() {
  1730. // This method must be called before calling `get_tree()->quit()`.
  1731. // Otherwise, its effect won't be visible
  1732. // Dim the project manager window while it's quitting to make it clearer that it's busy.
  1733. // No transition is applied, as the effect needs to be visible immediately
  1734. float c = 0.5f;
  1735. Color dim_color = Color(c, c, c);
  1736. set_modulate(dim_color);
  1737. }
  1738. void ProjectManager::_update_project_buttons() {
  1739. Vector<ProjectList::Item> selected_projects = _project_list->get_selected_projects();
  1740. bool empty_selection = selected_projects.is_empty();
  1741. bool is_missing_project_selected = false;
  1742. for (int i = 0; i < selected_projects.size(); ++i) {
  1743. if (selected_projects[i].missing) {
  1744. is_missing_project_selected = true;
  1745. break;
  1746. }
  1747. }
  1748. erase_btn->set_disabled(empty_selection);
  1749. open_btn->set_disabled(empty_selection || is_missing_project_selected);
  1750. rename_btn->set_disabled(empty_selection || is_missing_project_selected);
  1751. run_btn->set_disabled(empty_selection || is_missing_project_selected);
  1752. erase_missing_btn->set_disabled(!_project_list->is_any_project_missing());
  1753. }
  1754. void ProjectManager::shortcut_input(const Ref<InputEvent> &p_ev) {
  1755. ERR_FAIL_COND(p_ev.is_null());
  1756. Ref<InputEventKey> k = p_ev;
  1757. if (k.is_valid()) {
  1758. if (!k->is_pressed()) {
  1759. return;
  1760. }
  1761. // Pressing Command + Q quits the Project Manager
  1762. // This is handled by the platform implementation on macOS,
  1763. // so only define the shortcut on other platforms
  1764. #ifndef MACOS_ENABLED
  1765. if (k->get_keycode_with_modifiers() == (KeyModifierMask::META | Key::Q)) {
  1766. _dim_window();
  1767. get_tree()->quit();
  1768. }
  1769. #endif
  1770. if (tabs->get_current_tab() != 0) {
  1771. return;
  1772. }
  1773. bool keycode_handled = true;
  1774. switch (k->get_keycode()) {
  1775. case Key::ENTER: {
  1776. _open_selected_projects_ask();
  1777. } break;
  1778. case Key::HOME: {
  1779. if (_project_list->get_project_count() > 0) {
  1780. _project_list->select_project(0);
  1781. _update_project_buttons();
  1782. }
  1783. } break;
  1784. case Key::END: {
  1785. if (_project_list->get_project_count() > 0) {
  1786. _project_list->select_project(_project_list->get_project_count() - 1);
  1787. _update_project_buttons();
  1788. }
  1789. } break;
  1790. case Key::UP: {
  1791. if (k->is_shift_pressed()) {
  1792. break;
  1793. }
  1794. int index = _project_list->get_single_selected_index();
  1795. if (index > 0) {
  1796. _project_list->select_project(index - 1);
  1797. _project_list->ensure_project_visible(index - 1);
  1798. _update_project_buttons();
  1799. }
  1800. break;
  1801. }
  1802. case Key::DOWN: {
  1803. if (k->is_shift_pressed()) {
  1804. break;
  1805. }
  1806. int index = _project_list->get_single_selected_index();
  1807. if (index + 1 < _project_list->get_project_count()) {
  1808. _project_list->select_project(index + 1);
  1809. _project_list->ensure_project_visible(index + 1);
  1810. _update_project_buttons();
  1811. }
  1812. } break;
  1813. case Key::F: {
  1814. if (k->is_command_or_control_pressed()) {
  1815. this->search_box->grab_focus();
  1816. } else {
  1817. keycode_handled = false;
  1818. }
  1819. } break;
  1820. default: {
  1821. keycode_handled = false;
  1822. } break;
  1823. }
  1824. if (keycode_handled) {
  1825. accept_event();
  1826. }
  1827. }
  1828. }
  1829. void ProjectManager::_load_recent_projects() {
  1830. _project_list->set_search_term(search_box->get_text().strip_edges());
  1831. _project_list->load_projects();
  1832. _update_project_buttons();
  1833. tabs->set_current_tab(0);
  1834. }
  1835. void ProjectManager::_on_projects_updated() {
  1836. Vector<ProjectList::Item> selected_projects = _project_list->get_selected_projects();
  1837. int index = 0;
  1838. for (int i = 0; i < selected_projects.size(); ++i) {
  1839. index = _project_list->refresh_project(selected_projects[i].path);
  1840. }
  1841. if (index != -1) {
  1842. _project_list->ensure_project_visible(index);
  1843. }
  1844. _project_list->update_dock_menu();
  1845. }
  1846. void ProjectManager::_on_project_created(const String &dir) {
  1847. _project_list->add_project(dir, false);
  1848. _project_list->save_config();
  1849. search_box->clear();
  1850. int i = _project_list->refresh_project(dir);
  1851. _project_list->select_project(i);
  1852. _project_list->ensure_project_visible(i);
  1853. _open_selected_projects_ask();
  1854. _project_list->update_dock_menu();
  1855. }
  1856. void ProjectManager::_confirm_update_settings() {
  1857. _open_selected_projects();
  1858. }
  1859. void ProjectManager::_open_selected_projects() {
  1860. // Show loading text to tell the user that the project manager is busy loading.
  1861. // This is especially important for the Web project manager.
  1862. loading_label->show();
  1863. const HashSet<String> &selected_list = _project_list->get_selected_project_keys();
  1864. for (const String &path : selected_list) {
  1865. String conf = path.path_join("project.godot");
  1866. if (!FileAccess::exists(conf)) {
  1867. dialog_error->set_text(vformat(TTR("Can't open project at '%s'."), path));
  1868. dialog_error->popup_centered();
  1869. return;
  1870. }
  1871. print_line("Editing project: " + path);
  1872. List<String> args;
  1873. for (const String &a : Main::get_forwardable_cli_arguments(Main::CLI_SCOPE_TOOL)) {
  1874. args.push_back(a);
  1875. }
  1876. args.push_back("--path");
  1877. args.push_back(path);
  1878. args.push_back("--editor");
  1879. Error err = OS::get_singleton()->create_instance(args);
  1880. ERR_FAIL_COND(err);
  1881. }
  1882. _project_list->project_opening_initiated = true;
  1883. _dim_window();
  1884. get_tree()->quit();
  1885. }
  1886. void ProjectManager::_open_selected_projects_ask() {
  1887. const HashSet<String> &selected_list = _project_list->get_selected_project_keys();
  1888. if (selected_list.size() < 1) {
  1889. return;
  1890. }
  1891. const Size2i popup_min_size = Size2i(600.0 * EDSCALE, 400.0 * EDSCALE);
  1892. if (selected_list.size() > 1) {
  1893. multi_open_ask->set_text(vformat(TTR("You requested to open %d projects in parallel. Do you confirm?\nNote that usual checks for engine version compatibility will be bypassed."), selected_list.size()));
  1894. multi_open_ask->popup_centered(popup_min_size);
  1895. return;
  1896. }
  1897. ProjectList::Item project = _project_list->get_selected_projects()[0];
  1898. if (project.missing) {
  1899. return;
  1900. }
  1901. // Update the project settings or don't open.
  1902. const int config_version = project.version;
  1903. PackedStringArray unsupported_features = project.unsupported_features;
  1904. Label *ask_update_label = ask_update_settings->get_label();
  1905. ask_update_label->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_LEFT); // Reset in case of previous center align.
  1906. full_convert_button->hide();
  1907. ask_update_settings->get_ok_button()->set_text("OK");
  1908. // Check if the config_version property was empty or 0.
  1909. if (config_version == 0) {
  1910. ask_update_settings->set_text(vformat(TTR("The selected project \"%s\" does not specify its supported Godot version in its configuration file (\"project.godot\").\n\nProject path: %s\n\nIf you proceed with opening it, it will be converted to Godot's current configuration file format.\n\nWarning: You won't be able to open the project with previous versions of the engine anymore."), project.project_name, project.path));
  1911. ask_update_settings->popup_centered(popup_min_size);
  1912. return;
  1913. }
  1914. // Check if we need to convert project settings from an earlier engine version.
  1915. if (config_version < ProjectSettings::CONFIG_VERSION) {
  1916. if (config_version == GODOT4_CONFIG_VERSION - 1 && ProjectSettings::CONFIG_VERSION == GODOT4_CONFIG_VERSION) { // Conversion from Godot 3 to 4.
  1917. full_convert_button->show();
  1918. ask_update_settings->set_text(vformat(TTR("The selected project \"%s\" was generated by Godot 3.x, and needs to be converted for Godot 4.x.\n\nProject path: %s\n\nYou have three options:\n- Convert only the configuration file (\"project.godot\"). Use this to open the project without attempting to convert its scenes, resources and scripts.\n- Convert the entire project including its scenes, resources and scripts (recommended if you are upgrading).\n- Do nothing and go back.\n\nWarning: If you select a conversion option, you won't be able to open the project with previous versions of the engine anymore."), project.project_name, project.path));
  1919. ask_update_settings->get_ok_button()->set_text(TTR("Convert project.godot Only"));
  1920. } else {
  1921. ask_update_settings->set_text(vformat(TTR("The selected project \"%s\" was generated by an older engine version, and needs to be converted for this version.\n\nProject path: %s\n\nDo you want to convert it?\n\nWarning: You won't be able to open the project with previous versions of the engine anymore."), project.project_name, project.path));
  1922. ask_update_settings->get_ok_button()->set_text(TTR("Convert project.godot"));
  1923. }
  1924. ask_update_settings->popup_centered(popup_min_size);
  1925. ask_update_settings->get_cancel_button()->grab_focus(); // To prevent accidents.
  1926. return;
  1927. }
  1928. // Check if the file was generated by a newer, incompatible engine version.
  1929. if (config_version > ProjectSettings::CONFIG_VERSION) {
  1930. dialog_error->set_text(vformat(TTR("Can't open project \"%s\" at the following path:\n\n%s\n\nThe project settings were created by a newer engine version, whose settings are not compatible with this version."), project.project_name, project.path));
  1931. dialog_error->popup_centered(popup_min_size);
  1932. return;
  1933. }
  1934. // Check if the project is using features not supported by this build of Godot.
  1935. if (!unsupported_features.is_empty()) {
  1936. String warning_message = "";
  1937. for (int i = 0; i < unsupported_features.size(); i++) {
  1938. String feature = unsupported_features[i];
  1939. if (feature == "Double Precision") {
  1940. warning_message += TTR("Warning: This project uses double precision floats, but this version of\nGodot uses single precision floats. Opening this project may cause data loss.\n\n");
  1941. unsupported_features.remove_at(i);
  1942. i--;
  1943. } else if (feature == "C#") {
  1944. warning_message += TTR("Warning: This project uses C#, but this build of Godot does not have\nthe Mono module. If you proceed you will not be able to use any C# scripts.\n\n");
  1945. unsupported_features.remove_at(i);
  1946. i--;
  1947. } else if (feature.substr(0, 3).is_numeric()) {
  1948. warning_message += vformat(TTR("Warning: This project was built in Godot %s.\nOpening will upgrade or downgrade the project to Godot %s.\n\n"), Variant(feature), Variant(VERSION_BRANCH));
  1949. unsupported_features.remove_at(i);
  1950. i--;
  1951. }
  1952. }
  1953. if (!unsupported_features.is_empty()) {
  1954. String unsupported_features_str = String(", ").join(unsupported_features);
  1955. warning_message += vformat(TTR("Warning: This project uses the following features not supported by this build of Godot:\n\n%s\n\n"), unsupported_features_str);
  1956. }
  1957. warning_message += TTR("Open anyway? Project will be modified.");
  1958. ask_update_label->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER);
  1959. ask_update_settings->set_text(warning_message);
  1960. ask_update_settings->popup_centered(popup_min_size);
  1961. return;
  1962. }
  1963. // Open if the project is up-to-date.
  1964. _open_selected_projects();
  1965. }
  1966. void ProjectManager::_full_convert_button_pressed() {
  1967. ask_update_settings->hide();
  1968. ask_full_convert_dialog->popup_centered(Size2i(600.0 * EDSCALE, 400.0 * EDSCALE));
  1969. ask_full_convert_dialog->get_cancel_button()->grab_focus();
  1970. }
  1971. void ProjectManager::_perform_full_project_conversion() {
  1972. Vector<ProjectList::Item> selected_list = _project_list->get_selected_projects();
  1973. if (selected_list.is_empty()) {
  1974. return;
  1975. }
  1976. const String &path = selected_list[0].path;
  1977. print_line("Converting project: " + path);
  1978. Ref<ConfigFile> cf;
  1979. cf.instantiate();
  1980. cf->load(path.path_join("project.godot"));
  1981. cf->set_value("", "config_version", GODOT4_CONFIG_VERSION);
  1982. cf->save(path.path_join("project.godot"));
  1983. _project_list->set_project_version(path, GODOT4_CONFIG_VERSION);
  1984. List<String> args;
  1985. args.push_back("--path");
  1986. args.push_back(path);
  1987. args.push_back("--convert-3to4");
  1988. Error err = OS::get_singleton()->create_instance(args);
  1989. ERR_FAIL_COND(err);
  1990. }
  1991. void ProjectManager::_run_project_confirm() {
  1992. Vector<ProjectList::Item> selected_list = _project_list->get_selected_projects();
  1993. for (int i = 0; i < selected_list.size(); ++i) {
  1994. const String &selected_main = selected_list[i].main_scene;
  1995. if (selected_main.is_empty()) {
  1996. run_error_diag->set_text(TTR("Can't run project: no main scene defined.\nPlease edit the project and set the main scene in the Project Settings under the \"Application\" category."));
  1997. run_error_diag->popup_centered();
  1998. continue;
  1999. }
  2000. const String &path = selected_list[i].path;
  2001. // `.substr(6)` on `ProjectSettings::get_singleton()->get_imported_files_path()` strips away the leading "res://".
  2002. if (!DirAccess::exists(path.path_join(ProjectSettings::get_singleton()->get_imported_files_path().substr(6)))) {
  2003. run_error_diag->set_text(TTR("Can't run project: Assets need to be imported.\nPlease edit the project to trigger the initial import."));
  2004. run_error_diag->popup_centered();
  2005. continue;
  2006. }
  2007. print_line("Running project: " + path);
  2008. List<String> args;
  2009. for (const String &a : Main::get_forwardable_cli_arguments(Main::CLI_SCOPE_PROJECT)) {
  2010. args.push_back(a);
  2011. }
  2012. args.push_back("--path");
  2013. args.push_back(path);
  2014. Error err = OS::get_singleton()->create_instance(args);
  2015. ERR_FAIL_COND(err);
  2016. }
  2017. }
  2018. void ProjectManager::_run_project() {
  2019. const HashSet<String> &selected_list = _project_list->get_selected_project_keys();
  2020. if (selected_list.size() < 1) {
  2021. return;
  2022. }
  2023. if (selected_list.size() > 1) {
  2024. multi_run_ask->set_text(vformat(TTR("Are you sure to run %d projects at once?"), selected_list.size()));
  2025. multi_run_ask->popup_centered();
  2026. } else {
  2027. _run_project_confirm();
  2028. }
  2029. }
  2030. void ProjectManager::_scan_dir(const String &path) {
  2031. Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
  2032. Error error = da->change_dir(path);
  2033. ERR_FAIL_COND_MSG(error != OK, "Could not scan directory at: " + path);
  2034. da->list_dir_begin();
  2035. String n = da->get_next();
  2036. while (!n.is_empty()) {
  2037. if (da->current_is_dir() && !n.begins_with(".")) {
  2038. _scan_dir(da->get_current_dir().path_join(n));
  2039. } else if (n == "project.godot") {
  2040. _project_list->add_project(da->get_current_dir(), false);
  2041. }
  2042. n = da->get_next();
  2043. }
  2044. da->list_dir_end();
  2045. }
  2046. void ProjectManager::_scan_begin(const String &p_base) {
  2047. print_line("Scanning projects at: " + p_base);
  2048. _scan_dir(p_base);
  2049. _project_list->save_config();
  2050. _load_recent_projects();
  2051. }
  2052. void ProjectManager::_scan_projects() {
  2053. scan_dir->popup_file_dialog();
  2054. }
  2055. void ProjectManager::_new_project() {
  2056. npdialog->set_mode(ProjectDialog::MODE_NEW);
  2057. npdialog->show_dialog();
  2058. }
  2059. void ProjectManager::_import_project() {
  2060. npdialog->set_mode(ProjectDialog::MODE_IMPORT);
  2061. npdialog->show_dialog();
  2062. }
  2063. void ProjectManager::_rename_project() {
  2064. const HashSet<String> &selected_list = _project_list->get_selected_project_keys();
  2065. if (selected_list.size() == 0) {
  2066. return;
  2067. }
  2068. for (const String &E : selected_list) {
  2069. npdialog->set_project_path(E);
  2070. npdialog->set_mode(ProjectDialog::MODE_RENAME);
  2071. npdialog->show_dialog();
  2072. }
  2073. }
  2074. void ProjectManager::_erase_project_confirm() {
  2075. _project_list->erase_selected_projects(delete_project_contents->is_pressed());
  2076. _update_project_buttons();
  2077. }
  2078. void ProjectManager::_erase_missing_projects_confirm() {
  2079. _project_list->erase_missing_projects();
  2080. _update_project_buttons();
  2081. }
  2082. void ProjectManager::_erase_project() {
  2083. const HashSet<String> &selected_list = _project_list->get_selected_project_keys();
  2084. if (selected_list.size() == 0) {
  2085. return;
  2086. }
  2087. String confirm_message;
  2088. if (selected_list.size() >= 2) {
  2089. confirm_message = vformat(TTR("Remove %d projects from the list?"), selected_list.size());
  2090. } else {
  2091. confirm_message = TTR("Remove this project from the list?");
  2092. }
  2093. erase_ask_label->set_text(confirm_message);
  2094. delete_project_contents->set_pressed(false);
  2095. erase_ask->popup_centered();
  2096. }
  2097. void ProjectManager::_erase_missing_projects() {
  2098. erase_missing_ask->set_text(TTR("Remove all missing projects from the list?\nThe project folders' contents won't be modified."));
  2099. erase_missing_ask->popup_centered();
  2100. }
  2101. void ProjectManager::_show_about() {
  2102. about->popup_centered(Size2(780, 500) * EDSCALE);
  2103. }
  2104. void ProjectManager::_language_selected(int p_id) {
  2105. String lang = language_btn->get_item_metadata(p_id);
  2106. EditorSettings::get_singleton()->set("interface/editor/editor_language", lang);
  2107. language_restart_ask->set_text(TTR("Language changed.\nThe interface will update after restarting the editor or project manager."));
  2108. language_restart_ask->popup_centered();
  2109. }
  2110. void ProjectManager::_restart_confirm() {
  2111. List<String> args = OS::get_singleton()->get_cmdline_args();
  2112. Error err = OS::get_singleton()->create_instance(args);
  2113. ERR_FAIL_COND(err);
  2114. _dim_window();
  2115. get_tree()->quit();
  2116. }
  2117. void ProjectManager::_install_project(const String &p_zip_path, const String &p_title) {
  2118. npdialog->set_mode(ProjectDialog::MODE_INSTALL);
  2119. npdialog->set_zip_path(p_zip_path);
  2120. npdialog->set_zip_title(p_title);
  2121. npdialog->show_dialog();
  2122. }
  2123. void ProjectManager::_files_dropped(PackedStringArray p_files) {
  2124. if (p_files.size() == 1 && p_files[0].ends_with(".zip")) {
  2125. const String file = p_files[0].get_file();
  2126. _install_project(p_files[0], file.substr(0, file.length() - 4).capitalize());
  2127. return;
  2128. }
  2129. HashSet<String> folders_set;
  2130. Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
  2131. for (int i = 0; i < p_files.size(); i++) {
  2132. String file = p_files[i];
  2133. folders_set.insert(da->dir_exists(file) ? file : file.get_base_dir());
  2134. }
  2135. if (folders_set.size() > 0) {
  2136. PackedStringArray folders;
  2137. for (const String &E : folders_set) {
  2138. folders.push_back(E);
  2139. }
  2140. bool confirm = true;
  2141. if (folders.size() == 1) {
  2142. Ref<DirAccess> dir = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
  2143. if (dir->change_dir(folders[0]) == OK) {
  2144. dir->list_dir_begin();
  2145. String file = dir->get_next();
  2146. while (confirm && !file.is_empty()) {
  2147. if (!dir->current_is_dir() && file.ends_with("project.godot")) {
  2148. confirm = false;
  2149. }
  2150. file = dir->get_next();
  2151. }
  2152. dir->list_dir_end();
  2153. }
  2154. }
  2155. if (confirm) {
  2156. multi_scan_ask->get_ok_button()->disconnect("pressed", callable_mp(this, &ProjectManager::_scan_multiple_folders));
  2157. multi_scan_ask->get_ok_button()->connect("pressed", callable_mp(this, &ProjectManager::_scan_multiple_folders).bind(folders));
  2158. multi_scan_ask->set_text(
  2159. vformat(TTR("Are you sure to scan %s folders for existing Godot projects?\nThis could take a while."), folders.size()));
  2160. multi_scan_ask->popup_centered();
  2161. } else {
  2162. _scan_multiple_folders(folders);
  2163. }
  2164. }
  2165. }
  2166. void ProjectManager::_scan_multiple_folders(PackedStringArray p_files) {
  2167. for (int i = 0; i < p_files.size(); i++) {
  2168. _scan_begin(p_files.get(i));
  2169. }
  2170. }
  2171. void ProjectManager::_on_order_option_changed(int p_idx) {
  2172. if (is_inside_tree()) {
  2173. _project_list->set_order_option(p_idx);
  2174. }
  2175. }
  2176. void ProjectManager::_on_tab_changed(int p_tab) {
  2177. #ifndef ANDROID_ENABLED
  2178. if (p_tab == 0) { // Projects
  2179. // Automatically grab focus when the user moves from the Templates tab
  2180. // back to the Projects tab.
  2181. search_box->grab_focus();
  2182. }
  2183. // The Templates tab's search field is focused on display in the asset
  2184. // library editor plugin code.
  2185. #endif
  2186. }
  2187. void ProjectManager::_on_search_term_changed(const String &p_term) {
  2188. _project_list->set_search_term(p_term);
  2189. _project_list->sort_projects();
  2190. // Select the first visible project in the list.
  2191. // This makes it possible to open a project without ever touching the mouse,
  2192. // as the search field is automatically focused on startup.
  2193. _project_list->select_first_visible_project();
  2194. _update_project_buttons();
  2195. }
  2196. void ProjectManager::_bind_methods() {
  2197. ClassDB::bind_method("_update_project_buttons", &ProjectManager::_update_project_buttons);
  2198. ClassDB::bind_method("_version_button_pressed", &ProjectManager::_version_button_pressed);
  2199. }
  2200. void ProjectManager::_open_asset_library() {
  2201. asset_library->disable_community_support();
  2202. tabs->set_current_tab(1);
  2203. }
  2204. void ProjectManager::_version_button_pressed() {
  2205. DisplayServer::get_singleton()->clipboard_set(version_btn->get_text());
  2206. }
  2207. ProjectManager::ProjectManager() {
  2208. singleton = this;
  2209. // load settings
  2210. if (!EditorSettings::get_singleton()) {
  2211. EditorSettings::create();
  2212. }
  2213. // Turn off some servers we aren't going to be using in the Project Manager.
  2214. NavigationServer3D::get_singleton()->set_active(false);
  2215. PhysicsServer3D::get_singleton()->set_active(false);
  2216. PhysicsServer2D::get_singleton()->set_active(false);
  2217. EditorSettings::get_singleton()->set_optimize_save(false); //just write settings as they came
  2218. {
  2219. int display_scale = EDITOR_GET("interface/editor/display_scale");
  2220. switch (display_scale) {
  2221. case 0:
  2222. // Try applying a suitable display scale automatically.
  2223. editor_set_scale(EditorSettings::get_singleton()->get_auto_display_scale());
  2224. break;
  2225. case 1:
  2226. editor_set_scale(0.75);
  2227. break;
  2228. case 2:
  2229. editor_set_scale(1.0);
  2230. break;
  2231. case 3:
  2232. editor_set_scale(1.25);
  2233. break;
  2234. case 4:
  2235. editor_set_scale(1.5);
  2236. break;
  2237. case 5:
  2238. editor_set_scale(1.75);
  2239. break;
  2240. case 6:
  2241. editor_set_scale(2.0);
  2242. break;
  2243. default:
  2244. editor_set_scale(EDITOR_GET("interface/editor/custom_display_scale"));
  2245. break;
  2246. }
  2247. EditorFileDialog::get_icon_func = &ProjectManager::_file_dialog_get_icon;
  2248. EditorFileDialog::get_thumbnail_func = &ProjectManager::_file_dialog_get_thumbnail;
  2249. }
  2250. // TRANSLATORS: This refers to the application where users manage their Godot projects.
  2251. DisplayServer::get_singleton()->window_set_title(VERSION_NAME + String(" - ") + TTR("Project Manager", "Application"));
  2252. EditorFileDialog::set_default_show_hidden_files(EDITOR_GET("filesystem/file_dialog/show_hidden_files"));
  2253. int swap_cancel_ok = EDITOR_GET("interface/editor/accept_dialog_cancel_ok_buttons");
  2254. if (swap_cancel_ok != 0) { // 0 is auto, set in register_scene based on DisplayServer.
  2255. // Swap on means OK first.
  2256. AcceptDialog::set_swap_cancel_ok(swap_cancel_ok == 2);
  2257. }
  2258. Ref<Theme> theme = create_custom_theme();
  2259. set_theme(theme);
  2260. DisplayServer::set_early_window_clear_color_override(true, theme->get_color(SNAME("background"), SNAME("Editor")));
  2261. set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT);
  2262. Panel *panel = memnew(Panel);
  2263. add_child(panel);
  2264. panel->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT);
  2265. panel->add_theme_style_override("panel", get_theme_stylebox(SNAME("Background"), SNAME("EditorStyles")));
  2266. VBoxContainer *vb = memnew(VBoxContainer);
  2267. panel->add_child(vb);
  2268. vb->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT, Control::PRESET_MODE_MINSIZE, 8 * EDSCALE);
  2269. Control *center_box = memnew(Control);
  2270. center_box->set_v_size_flags(Control::SIZE_EXPAND_FILL);
  2271. vb->add_child(center_box);
  2272. tabs = memnew(TabContainer);
  2273. center_box->add_child(tabs);
  2274. tabs->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT);
  2275. tabs->connect("tab_changed", callable_mp(this, &ProjectManager::_on_tab_changed));
  2276. local_projects_hb = memnew(HBoxContainer);
  2277. local_projects_hb->set_name(TTR("Local Projects"));
  2278. tabs->add_child(local_projects_hb);
  2279. {
  2280. // Projects + search bar
  2281. VBoxContainer *search_tree_vb = memnew(VBoxContainer);
  2282. local_projects_hb->add_child(search_tree_vb);
  2283. search_tree_vb->set_h_size_flags(Control::SIZE_EXPAND_FILL);
  2284. HBoxContainer *hb = memnew(HBoxContainer);
  2285. hb->set_h_size_flags(Control::SIZE_EXPAND_FILL);
  2286. search_tree_vb->add_child(hb);
  2287. search_box = memnew(LineEdit);
  2288. search_box->set_placeholder(TTR("Filter Projects"));
  2289. search_box->set_tooltip_text(TTR("This field filters projects by name and last path component.\nTo filter projects by name and full path, the query must contain at least one `/` character."));
  2290. search_box->connect("text_changed", callable_mp(this, &ProjectManager::_on_search_term_changed));
  2291. search_box->set_h_size_flags(Control::SIZE_EXPAND_FILL);
  2292. hb->add_child(search_box);
  2293. loading_label = memnew(Label(TTR("Loading, please wait...")));
  2294. loading_label->add_theme_font_override("font", get_theme_font(SNAME("bold"), SNAME("EditorFonts")));
  2295. loading_label->set_h_size_flags(Control::SIZE_EXPAND_FILL);
  2296. hb->add_child(loading_label);
  2297. // The loading label is shown later.
  2298. loading_label->hide();
  2299. Label *sort_label = memnew(Label);
  2300. sort_label->set_text(TTR("Sort:"));
  2301. hb->add_child(sort_label);
  2302. filter_option = memnew(OptionButton);
  2303. filter_option->set_clip_text(true);
  2304. filter_option->set_h_size_flags(Control::SIZE_EXPAND_FILL);
  2305. filter_option->connect("item_selected", callable_mp(this, &ProjectManager::_on_order_option_changed));
  2306. hb->add_child(filter_option);
  2307. Vector<String> sort_filter_titles;
  2308. sort_filter_titles.push_back(TTR("Last Edited"));
  2309. sort_filter_titles.push_back(TTR("Name"));
  2310. sort_filter_titles.push_back(TTR("Path"));
  2311. for (int i = 0; i < sort_filter_titles.size(); i++) {
  2312. filter_option->add_item(sort_filter_titles[i]);
  2313. }
  2314. PanelContainer *pc = memnew(PanelContainer);
  2315. pc->add_theme_style_override("panel", get_theme_stylebox(SNAME("panel"), SNAME("Tree")));
  2316. pc->set_v_size_flags(Control::SIZE_EXPAND_FILL);
  2317. search_tree_vb->add_child(pc);
  2318. _project_list = memnew(ProjectList);
  2319. _project_list->connect(ProjectList::SIGNAL_SELECTION_CHANGED, callable_mp(this, &ProjectManager::_update_project_buttons));
  2320. _project_list->connect(ProjectList::SIGNAL_PROJECT_ASK_OPEN, callable_mp(this, &ProjectManager::_open_selected_projects_ask));
  2321. _project_list->set_horizontal_scroll_mode(ScrollContainer::SCROLL_MODE_DISABLED);
  2322. pc->add_child(_project_list);
  2323. }
  2324. {
  2325. // Project tab side bar
  2326. VBoxContainer *tree_vb = memnew(VBoxContainer);
  2327. tree_vb->set_custom_minimum_size(Size2(120, 120));
  2328. local_projects_hb->add_child(tree_vb);
  2329. const int btn_h_separation = int(6 * EDSCALE);
  2330. create_btn = memnew(Button);
  2331. create_btn->set_text(TTR("New Project"));
  2332. create_btn->add_theme_constant_override("h_separation", btn_h_separation);
  2333. create_btn->set_shortcut(ED_SHORTCUT("project_manager/new_project", TTR("New Project"), KeyModifierMask::CMD_OR_CTRL | Key::N));
  2334. create_btn->connect("pressed", callable_mp(this, &ProjectManager::_new_project));
  2335. tree_vb->add_child(create_btn);
  2336. import_btn = memnew(Button);
  2337. import_btn->set_text(TTR("Import"));
  2338. import_btn->add_theme_constant_override("h_separation", btn_h_separation);
  2339. import_btn->set_shortcut(ED_SHORTCUT("project_manager/import_project", TTR("Import Project"), KeyModifierMask::CMD_OR_CTRL | Key::I));
  2340. import_btn->connect("pressed", callable_mp(this, &ProjectManager::_import_project));
  2341. tree_vb->add_child(import_btn);
  2342. scan_btn = memnew(Button);
  2343. scan_btn->set_text(TTR("Scan"));
  2344. scan_btn->add_theme_constant_override("h_separation", btn_h_separation);
  2345. scan_btn->set_shortcut(ED_SHORTCUT("project_manager/scan_projects", TTR("Scan Projects"), KeyModifierMask::CMD_OR_CTRL | Key::S));
  2346. scan_btn->connect("pressed", callable_mp(this, &ProjectManager::_scan_projects));
  2347. tree_vb->add_child(scan_btn);
  2348. tree_vb->add_child(memnew(HSeparator));
  2349. open_btn = memnew(Button);
  2350. open_btn->set_text(TTR("Edit"));
  2351. open_btn->add_theme_constant_override("h_separation", btn_h_separation);
  2352. open_btn->set_shortcut(ED_SHORTCUT("project_manager/edit_project", TTR("Edit Project"), KeyModifierMask::CMD_OR_CTRL | Key::E));
  2353. open_btn->connect("pressed", callable_mp(this, &ProjectManager::_open_selected_projects_ask));
  2354. tree_vb->add_child(open_btn);
  2355. run_btn = memnew(Button);
  2356. run_btn->set_text(TTR("Run"));
  2357. run_btn->add_theme_constant_override("h_separation", btn_h_separation);
  2358. run_btn->set_shortcut(ED_SHORTCUT("project_manager/run_project", TTR("Run Project"), KeyModifierMask::CMD_OR_CTRL | Key::R));
  2359. run_btn->connect("pressed", callable_mp(this, &ProjectManager::_run_project));
  2360. tree_vb->add_child(run_btn);
  2361. rename_btn = memnew(Button);
  2362. rename_btn->set_text(TTR("Rename"));
  2363. rename_btn->add_theme_constant_override("h_separation", btn_h_separation);
  2364. // The F2 shortcut isn't overridden with Enter on macOS as Enter is already used to edit a project.
  2365. rename_btn->set_shortcut(ED_SHORTCUT("project_manager/rename_project", TTR("Rename Project"), Key::F2));
  2366. rename_btn->connect("pressed", callable_mp(this, &ProjectManager::_rename_project));
  2367. tree_vb->add_child(rename_btn);
  2368. erase_btn = memnew(Button);
  2369. erase_btn->set_text(TTR("Remove"));
  2370. erase_btn->add_theme_constant_override("h_separation", btn_h_separation);
  2371. erase_btn->set_shortcut(ED_SHORTCUT("project_manager/remove_project", TTR("Remove Project"), Key::KEY_DELETE));
  2372. erase_btn->connect("pressed", callable_mp(this, &ProjectManager::_erase_project));
  2373. tree_vb->add_child(erase_btn);
  2374. erase_missing_btn = memnew(Button);
  2375. erase_missing_btn->set_text(TTR("Remove Missing"));
  2376. erase_missing_btn->add_theme_constant_override("h_separation", btn_h_separation);
  2377. erase_missing_btn->connect("pressed", callable_mp(this, &ProjectManager::_erase_missing_projects));
  2378. tree_vb->add_child(erase_missing_btn);
  2379. tree_vb->add_spacer();
  2380. about_btn = memnew(Button);
  2381. about_btn->set_text(TTR("About"));
  2382. about_btn->connect("pressed", callable_mp(this, &ProjectManager::_show_about));
  2383. tree_vb->add_child(about_btn);
  2384. }
  2385. {
  2386. // Version info and language options
  2387. settings_hb = memnew(HBoxContainer);
  2388. settings_hb->set_alignment(BoxContainer::ALIGNMENT_END);
  2389. settings_hb->set_h_grow_direction(Control::GROW_DIRECTION_BEGIN);
  2390. settings_hb->set_anchors_and_offsets_preset(Control::PRESET_TOP_RIGHT);
  2391. // A VBoxContainer that contains a dummy Control node to adjust the LinkButton's vertical position.
  2392. VBoxContainer *spacer_vb = memnew(VBoxContainer);
  2393. settings_hb->add_child(spacer_vb);
  2394. Control *v_spacer = memnew(Control);
  2395. spacer_vb->add_child(v_spacer);
  2396. version_btn = memnew(LinkButton);
  2397. String hash = String(VERSION_HASH);
  2398. if (hash.length() != 0) {
  2399. hash = " " + vformat("[%s]", hash.left(9));
  2400. }
  2401. version_btn->set_text("v" VERSION_FULL_BUILD + hash);
  2402. // Fade the version label to be less prominent, but still readable.
  2403. version_btn->set_self_modulate(Color(1, 1, 1, 0.6));
  2404. version_btn->set_underline_mode(LinkButton::UNDERLINE_MODE_ON_HOVER);
  2405. version_btn->set_tooltip_text(TTR("Click to copy."));
  2406. version_btn->connect("pressed", callable_mp(this, &ProjectManager::_version_button_pressed));
  2407. spacer_vb->add_child(version_btn);
  2408. // Add a small horizontal spacer between the version and language buttons
  2409. // to distinguish them.
  2410. Control *h_spacer = memnew(Control);
  2411. settings_hb->add_child(h_spacer);
  2412. language_btn = memnew(OptionButton);
  2413. language_btn->set_icon(get_theme_icon(SNAME("Environment"), SNAME("EditorIcons")));
  2414. language_btn->set_focus_mode(Control::FOCUS_NONE);
  2415. language_btn->set_fit_to_longest_item(false);
  2416. language_btn->set_flat(true);
  2417. language_btn->connect("item_selected", callable_mp(this, &ProjectManager::_language_selected));
  2418. #ifdef ANDROID_ENABLED
  2419. // The language selection dropdown doesn't work on Android (as the setting isn't saved), see GH-60353.
  2420. // Also, the dropdown it spawns is very tall and can't be scrolled without a hardware mouse.
  2421. // Hiding the language selection dropdown also leaves more space for the version label to display.
  2422. language_btn->hide();
  2423. #endif
  2424. Vector<String> editor_languages;
  2425. List<PropertyInfo> editor_settings_properties;
  2426. EditorSettings::get_singleton()->get_property_list(&editor_settings_properties);
  2427. for (const PropertyInfo &pi : editor_settings_properties) {
  2428. if (pi.name == "interface/editor/editor_language") {
  2429. editor_languages = pi.hint_string.split(",");
  2430. break;
  2431. }
  2432. }
  2433. String current_lang = EDITOR_GET("interface/editor/editor_language");
  2434. language_btn->set_text(current_lang);
  2435. for (int i = 0; i < editor_languages.size(); i++) {
  2436. String lang = editor_languages[i];
  2437. String lang_name = TranslationServer::get_singleton()->get_locale_name(lang);
  2438. language_btn->add_item(vformat("[%s] %s", lang, lang_name), i);
  2439. language_btn->set_item_metadata(i, lang);
  2440. if (current_lang == lang) {
  2441. language_btn->select(i);
  2442. }
  2443. }
  2444. settings_hb->add_child(language_btn);
  2445. center_box->add_child(settings_hb);
  2446. }
  2447. if (AssetLibraryEditorPlugin::is_available()) {
  2448. asset_library = memnew(EditorAssetLibrary(true));
  2449. asset_library->set_name(TTR("Asset Library Projects"));
  2450. tabs->add_child(asset_library);
  2451. asset_library->connect("install_asset", callable_mp(this, &ProjectManager::_install_project));
  2452. } else {
  2453. print_verbose("Asset Library not available (due to using Web editor, or SSL support disabled).");
  2454. }
  2455. {
  2456. // Dialogs
  2457. language_restart_ask = memnew(ConfirmationDialog);
  2458. language_restart_ask->set_ok_button_text(TTR("Restart Now"));
  2459. language_restart_ask->get_ok_button()->connect("pressed", callable_mp(this, &ProjectManager::_restart_confirm));
  2460. language_restart_ask->set_cancel_button_text(TTR("Continue"));
  2461. add_child(language_restart_ask);
  2462. scan_dir = memnew(EditorFileDialog);
  2463. scan_dir->set_previews_enabled(false);
  2464. scan_dir->set_access(EditorFileDialog::ACCESS_FILESYSTEM);
  2465. scan_dir->set_file_mode(EditorFileDialog::FILE_MODE_OPEN_DIR);
  2466. scan_dir->set_title(TTR("Select a Folder to Scan")); // must be after mode or it's overridden
  2467. scan_dir->set_current_dir(EDITOR_GET("filesystem/directories/default_project_path"));
  2468. add_child(scan_dir);
  2469. scan_dir->connect("dir_selected", callable_mp(this, &ProjectManager::_scan_begin));
  2470. erase_missing_ask = memnew(ConfirmationDialog);
  2471. erase_missing_ask->set_ok_button_text(TTR("Remove All"));
  2472. erase_missing_ask->get_ok_button()->connect("pressed", callable_mp(this, &ProjectManager::_erase_missing_projects_confirm));
  2473. add_child(erase_missing_ask);
  2474. erase_ask = memnew(ConfirmationDialog);
  2475. erase_ask->set_ok_button_text(TTR("Remove"));
  2476. erase_ask->get_ok_button()->connect("pressed", callable_mp(this, &ProjectManager::_erase_project_confirm));
  2477. add_child(erase_ask);
  2478. VBoxContainer *erase_ask_vb = memnew(VBoxContainer);
  2479. erase_ask->add_child(erase_ask_vb);
  2480. erase_ask_label = memnew(Label);
  2481. erase_ask_vb->add_child(erase_ask_label);
  2482. delete_project_contents = memnew(CheckBox);
  2483. delete_project_contents->set_text(TTR("Also delete project contents (no undo!)"));
  2484. erase_ask_vb->add_child(delete_project_contents);
  2485. multi_open_ask = memnew(ConfirmationDialog);
  2486. multi_open_ask->set_ok_button_text(TTR("Edit"));
  2487. multi_open_ask->get_ok_button()->connect("pressed", callable_mp(this, &ProjectManager::_open_selected_projects));
  2488. add_child(multi_open_ask);
  2489. multi_run_ask = memnew(ConfirmationDialog);
  2490. multi_run_ask->set_ok_button_text(TTR("Run"));
  2491. multi_run_ask->get_ok_button()->connect("pressed", callable_mp(this, &ProjectManager::_run_project_confirm));
  2492. add_child(multi_run_ask);
  2493. multi_scan_ask = memnew(ConfirmationDialog);
  2494. multi_scan_ask->set_ok_button_text(TTR("Scan"));
  2495. add_child(multi_scan_ask);
  2496. ask_update_settings = memnew(ConfirmationDialog);
  2497. ask_update_settings->set_autowrap(true);
  2498. ask_update_settings->get_ok_button()->connect("pressed", callable_mp(this, &ProjectManager::_confirm_update_settings));
  2499. full_convert_button = ask_update_settings->add_button("Convert Full Project", !GLOBAL_GET("gui/common/swap_cancel_ok"));
  2500. full_convert_button->connect("pressed", callable_mp(this, &ProjectManager::_full_convert_button_pressed));
  2501. add_child(ask_update_settings);
  2502. ask_full_convert_dialog = memnew(ConfirmationDialog);
  2503. ask_full_convert_dialog->set_autowrap(true);
  2504. ask_full_convert_dialog->set_text(TTR("This option will perform full project conversion, updating scenes, resources and scripts from Godot 3.x to work in Godot 4.0.\n\nNote that this is a best-effort conversion, i.e. it makes upgrading the project easier, but it will not open out-of-the-box and will still require manual adjustments.\n\nIMPORTANT: Make sure to backup your project before converting, as this operation makes it impossible to open it in older versions of Godot."));
  2505. ask_full_convert_dialog->connect("confirmed", callable_mp(this, &ProjectManager::_perform_full_project_conversion));
  2506. add_child(ask_full_convert_dialog);
  2507. npdialog = memnew(ProjectDialog);
  2508. npdialog->connect("projects_updated", callable_mp(this, &ProjectManager::_on_projects_updated));
  2509. npdialog->connect("project_created", callable_mp(this, &ProjectManager::_on_project_created));
  2510. add_child(npdialog);
  2511. run_error_diag = memnew(AcceptDialog);
  2512. run_error_diag->set_title(TTR("Can't run project"));
  2513. add_child(run_error_diag);
  2514. dialog_error = memnew(AcceptDialog);
  2515. add_child(dialog_error);
  2516. if (asset_library) {
  2517. open_templates = memnew(ConfirmationDialog);
  2518. open_templates->set_text(TTR("You currently don't have any projects.\nWould you like to explore official example projects in the Asset Library?"));
  2519. open_templates->set_ok_button_text(TTR("Open Asset Library"));
  2520. open_templates->connect("confirmed", callable_mp(this, &ProjectManager::_open_asset_library));
  2521. add_child(open_templates);
  2522. }
  2523. about = memnew(EditorAbout);
  2524. add_child(about);
  2525. _build_icon_type_cache(get_theme());
  2526. }
  2527. _project_list->migrate_config();
  2528. _load_recent_projects();
  2529. Ref<DirAccess> dir_access = DirAccess::create(DirAccess::AccessType::ACCESS_FILESYSTEM);
  2530. String default_project_path = EDITOR_GET("filesystem/directories/default_project_path");
  2531. if (!dir_access->dir_exists(default_project_path)) {
  2532. Error error = dir_access->make_dir_recursive(default_project_path);
  2533. if (error != OK) {
  2534. ERR_PRINT("Could not create default project directory at: " + default_project_path);
  2535. }
  2536. }
  2537. String autoscan_path = EDITOR_GET("filesystem/directories/autoscan_project_path");
  2538. if (!autoscan_path.is_empty()) {
  2539. if (dir_access->dir_exists(autoscan_path)) {
  2540. _scan_begin(autoscan_path);
  2541. } else {
  2542. Error error = dir_access->make_dir_recursive(autoscan_path);
  2543. if (error != OK) {
  2544. ERR_PRINT("Could not create project autoscan directory at: " + autoscan_path);
  2545. }
  2546. }
  2547. }
  2548. SceneTree::get_singleton()->get_root()->connect("files_dropped", callable_mp(this, &ProjectManager::_files_dropped));
  2549. // Define a minimum window size to prevent UI elements from overlapping or being cut off.
  2550. Window *w = Object::cast_to<Window>(SceneTree::get_singleton()->get_root());
  2551. if (w) {
  2552. w->set_min_size(Size2(520, 350) * EDSCALE);
  2553. }
  2554. // Resize the bootsplash window based on Editor display scale EDSCALE.
  2555. float scale_factor = MAX(1, EDSCALE);
  2556. if (scale_factor > 1.0) {
  2557. Vector2i window_size = DisplayServer::get_singleton()->window_get_size();
  2558. Rect2i screen_rect = DisplayServer::get_singleton()->screen_get_usable_rect(DisplayServer::get_singleton()->window_get_current_screen());
  2559. window_size *= scale_factor;
  2560. DisplayServer::get_singleton()->window_set_size(window_size);
  2561. if (screen_rect.size != Vector2i()) {
  2562. Vector2i window_position;
  2563. window_position.x = screen_rect.position.x + (screen_rect.size.x - window_size.x) / 2;
  2564. window_position.y = screen_rect.position.y + (screen_rect.size.y - window_size.y) / 2;
  2565. DisplayServer::get_singleton()->window_set_position(window_position);
  2566. }
  2567. }
  2568. OS::get_singleton()->set_low_processor_usage_mode(true);
  2569. }
  2570. ProjectManager::~ProjectManager() {
  2571. singleton = nullptr;
  2572. if (EditorSettings::get_singleton()) {
  2573. EditorSettings::destroy();
  2574. }
  2575. }