EditorUI.as 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575
  1. // Urho3D editor user interface
  2. XMLFile@ uiStyle;
  3. UIElement@ uiMenuBar;
  4. FileSelector@ uiFileSelector;
  5. const ShortStringHash windowType("Window");
  6. Array<String> uiSceneFilters = {"*.xml", "*.bin", "*.*"};
  7. Array<String> uiAllFilters = {"*.*"};
  8. Array<String> uiScriptFilters = {"*.as", "*.*"};
  9. uint uiSceneFilter = 0;
  10. uint uiNodeFilter = 0;
  11. uint uiImportFilter = 0;
  12. uint uiScriptFilter = 0;
  13. String uiScenePath = fileSystem.programDir + "Data/Scenes";
  14. String uiNodePath = fileSystem.programDir + "Data/Objects";
  15. String uiImportPath;
  16. String uiScriptPath = fileSystem.programDir + "Data/Scripts";
  17. bool uiHidden = false;
  18. float uiMinOpacity = 0.3;
  19. float uiMaxOpacity = 0.7;
  20. void CreateUI()
  21. {
  22. uiStyle = cache.GetResource("XMLFile", "UI/DefaultStyle.xml");
  23. ui.root.defaultStyle = uiStyle;
  24. CreateCursor();
  25. CreateMenuBar();
  26. CreateSceneWindow();
  27. CreateNodeWindow();
  28. CreateEditorSettingsDialog();
  29. CreateEditorPreferencesDialog();
  30. CreateStatsBar();
  31. CreateConsole();
  32. CreateDebugHud();
  33. SubscribeToEvent("ScreenMode", "ResizeUI");
  34. SubscribeToEvent("MenuSelected", "HandleMenuSelected");
  35. SubscribeToEvent("KeyDown", "HandleKeyDown");
  36. SubscribeToEvent("KeyUp", "UnhideUI");
  37. SubscribeToEvent("MouseButtonUp", "UnhideUI");
  38. }
  39. void ResizeUI()
  40. {
  41. // Resize menu bar
  42. uiMenuBar.SetFixedWidth(graphics.width);
  43. // Relayout stats bar
  44. Font@ font = cache.GetResource("Font", "Fonts/Anonymous Pro.ttf");
  45. if (graphics.width >= 1200)
  46. {
  47. SetupStatsBarText(editorModeText, font, 0, 24, HA_LEFT, VA_TOP);
  48. SetupStatsBarText(renderStatsText, font, 0, 24, HA_RIGHT, VA_TOP);
  49. }
  50. else
  51. {
  52. SetupStatsBarText(editorModeText, font, 0, 24, HA_LEFT, VA_TOP);
  53. SetupStatsBarText(renderStatsText, font, 0, 36, HA_LEFT, VA_TOP);
  54. }
  55. // Relayout windows
  56. Array<UIElement@> children = ui.root.GetChildren();
  57. for (uint i = 0; i < children.length; ++i)
  58. {
  59. if (children[i].type == windowType)
  60. AdjustPosition(children[i]);
  61. }
  62. }
  63. void AdjustPosition(Window@ window)
  64. {
  65. IntVector2 position = window.position;
  66. IntVector2 size = window.size;
  67. IntVector2 extend = position + size;
  68. if (extend.x > graphics.width)
  69. position.x = Max(20, graphics.width - size.x - 20);
  70. if (extend.y > graphics.height)
  71. position.y = Max(40, graphics.height - size.y - 20);
  72. window.position = position;
  73. }
  74. void CreateCursor()
  75. {
  76. Cursor@ cursor = Cursor("Cursor");
  77. cursor.style = uiStyle;
  78. cursor.SetPosition(graphics.width / 2, graphics.height / 2);
  79. ui.cursor = cursor;
  80. if (GetPlatform() == "Android" || GetPlatform() == "iOS")
  81. ui.cursor.visible = false;
  82. }
  83. void CreateMenuBar()
  84. {
  85. uiMenuBar = BorderImage("MenuBar");
  86. uiMenuBar.active = true;
  87. uiMenuBar.SetStyle(uiStyle, "EditorMenuBar");
  88. uiMenuBar.SetLayout(LM_HORIZONTAL);
  89. uiMenuBar.opacity = uiMaxOpacity;
  90. uiMenuBar.SetFixedWidth(graphics.width);
  91. ui.root.AddChild(uiMenuBar);
  92. {
  93. Menu@ fileMenu = CreateMenu("File");
  94. Window@ filePopup = fileMenu.popup;
  95. filePopup.AddChild(CreateMenuItem("New scene", 0, 0));
  96. filePopup.AddChild(CreateMenuItem("Open scene...", 'O', QUAL_CTRL));
  97. filePopup.AddChild(CreateMenuItem("Save scene", 'S', QUAL_CTRL));
  98. filePopup.AddChild(CreateMenuItem("Save scene as...", 'S', QUAL_SHIFT | QUAL_CTRL));
  99. filePopup.AddChild(CreateMenuDivider());
  100. Menu@ loadNodeMenu = CreateMenuItem("Load node", 0, 0);
  101. Window@ loadNodePopup = CreatePopup(loadNodeMenu);
  102. loadNodeMenu.popupOffset = IntVector2(loadNodeMenu.width, 0);
  103. loadNodePopup.AddChild(CreateMenuItem("As replicated...", 0, 0));
  104. loadNodePopup.AddChild(CreateMenuItem("As local...", 0, 0));
  105. filePopup.AddChild(loadNodeMenu);
  106. filePopup.AddChild(CreateMenuItem("Save node as...", 0, 0));
  107. filePopup.AddChild(CreateMenuDivider());
  108. filePopup.AddChild(CreateMenuItem("Import model...", 0, 0));
  109. filePopup.AddChild(CreateMenuItem("Import scene...", 0, 0));
  110. filePopup.AddChild(CreateMenuItem("Run script...", 0, 0));
  111. filePopup.AddChild(CreateMenuDivider());
  112. filePopup.AddChild(CreateMenuItem("Set resource path...", 0, 0));
  113. filePopup.AddChild(CreateMenuDivider());
  114. filePopup.AddChild(CreateMenuItem("Exit", 0, 0));
  115. uiMenuBar.AddChild(fileMenu);
  116. }
  117. {
  118. Menu@ editMenu = CreateMenu("Edit");
  119. Window@ editPopup = editMenu.popup;
  120. editPopup.AddChild(CreateMenuItem("Cut", 'X', QUAL_CTRL));
  121. editPopup.AddChild(CreateMenuItem("Copy", 'C', QUAL_CTRL));
  122. editPopup.AddChild(CreateMenuItem("Paste", 'V', QUAL_CTRL));
  123. editPopup.AddChild(CreateMenuItem("Delete", KEY_DELETE, QUAL_ANY));
  124. editPopup.AddChild(CreateMenuItem("Select all", 'A', QUAL_CTRL));
  125. editPopup.AddChild(CreateMenuDivider());
  126. editPopup.AddChild(CreateMenuItem("Reset position", 0, 0));
  127. editPopup.AddChild(CreateMenuItem("Reset rotation", 0, 0));
  128. editPopup.AddChild(CreateMenuItem("Reset scale", 0, 0));
  129. editPopup.AddChild(CreateMenuItem("Unparent", 'U', QUAL_CTRL));
  130. editPopup.AddChild(CreateMenuDivider());
  131. editPopup.AddChild(CreateMenuItem("Toggle update", 'P', QUAL_CTRL));
  132. uiMenuBar.AddChild(editMenu);
  133. }
  134. {
  135. Menu@ createMenu = CreateMenu("Create");
  136. Window@ createPopup = createMenu.popup;
  137. createPopup.AddChild(CreateMenuItem("Box", 0, 0));
  138. createPopup.AddChild(CreateMenuItem("Cone", 0, 0));
  139. createPopup.AddChild(CreateMenuItem("Cylinder", 0, 0));
  140. createPopup.AddChild(CreateMenuItem("Plane", 0, 0));
  141. createPopup.AddChild(CreateMenuItem("Pyramid", 0, 0));
  142. createPopup.AddChild(CreateMenuItem("Sphere", 0, 0));
  143. uiMenuBar.AddChild(createMenu);
  144. }
  145. {
  146. Menu@ fileMenu = CreateMenu("View");
  147. Window@ filePopup = fileMenu.popup;
  148. filePopup.AddChild(CreateMenuItem("Hierarchy", 'H', QUAL_CTRL));
  149. filePopup.AddChild(CreateMenuItem("Attribute inspector", 'N', QUAL_CTRL));
  150. filePopup.AddChild(CreateMenuItem("Editor settings", 0, 0));
  151. filePopup.AddChild(CreateMenuItem("Editor preferences", 0, 0));
  152. uiMenuBar.AddChild(fileMenu);
  153. }
  154. BorderImage@ spacer = BorderImage("MenuBarSpacer");
  155. spacer.SetStyle(uiStyle, "EditorMenuBar");
  156. uiMenuBar.AddChild(spacer);
  157. }
  158. Menu@ CreateMenuItem(const String&in title, int accelKey, int accelQual, int padding = 16)
  159. {
  160. Menu@ menu = Menu(title);
  161. menu.style = uiStyle;
  162. menu.SetLayout(LM_HORIZONTAL, 0, IntRect(padding, 2, padding, 2));
  163. if (accelKey != 0)
  164. menu.SetAccelerator(accelKey, accelQual);
  165. Text@ menuText = Text();
  166. menuText.SetStyle(uiStyle, "EditorMenuText");
  167. menuText.text = title;
  168. menu.AddChild(menuText);
  169. return menu;
  170. }
  171. BorderImage@ CreateMenuDivider()
  172. {
  173. BorderImage@ divider = BorderImage();
  174. divider.SetStyle(uiStyle, "EditorDivider");
  175. return divider;
  176. }
  177. Window@ CreatePopup(Menu@ baseMenu)
  178. {
  179. Window@ popup = Window();
  180. popup.style = uiStyle;
  181. popup.SetLayout(LM_VERTICAL, 1, IntRect(2, 6, 2, 6));
  182. baseMenu.popup = popup;
  183. baseMenu.popupOffset = IntVector2(0, baseMenu.height);
  184. return popup;
  185. }
  186. Menu@ CreateMenu(const String&in title)
  187. {
  188. Menu@ menu = CreateMenuItem(title, 0, 0, 8);
  189. menu.name = "";
  190. menu.SetFixedWidth(menu.width);
  191. CreatePopup(menu);
  192. return menu;
  193. }
  194. void CreateFileSelector(const String&in title, const String&in ok, const String&in cancel, const String&in initialPath, Array<String>@ filters,
  195. uint initialFilter)
  196. {
  197. // Within the editor UI, the file selector is a kind of a "singleton". When the previous one is overwritten, also
  198. // the events subscribed from it are disconnected, so new ones are safe to subscribe.
  199. uiFileSelector = FileSelector();
  200. uiFileSelector.style = uiStyle;
  201. uiFileSelector.title = title;
  202. uiFileSelector.path = initialPath;
  203. uiFileSelector.SetButtonTexts(ok, cancel);
  204. uiFileSelector.SetFilters(filters, initialFilter);
  205. CenterDialog(uiFileSelector.window);
  206. }
  207. void CloseFileSelector(uint&out filterIndex, String&out path)
  208. {
  209. // Save filter & path for next time
  210. filterIndex = uiFileSelector.filterIndex;
  211. path = uiFileSelector.path;
  212. uiFileSelector = null;
  213. }
  214. void CloseFileSelector()
  215. {
  216. uiFileSelector = null;
  217. }
  218. void CreateConsole()
  219. {
  220. Console@ console = engine.CreateConsole();
  221. console.style = uiStyle;
  222. console.numRows = 16;
  223. }
  224. void CreateDebugHud()
  225. {
  226. engine.CreateDebugHud();
  227. debugHud.style = uiStyle;
  228. debugHud.mode = DEBUGHUD_SHOW_NONE;
  229. }
  230. void CenterDialog(UIElement@ element)
  231. {
  232. IntVector2 size = element.size;
  233. element.SetPosition((graphics.width - size.x) / 2, (graphics.height - size.y) / 2);
  234. }
  235. void UpdateWindowTitle()
  236. {
  237. String sceneName = GetFileNameAndExtension(sceneFileName);
  238. if (sceneName.empty)
  239. sceneName = "Untitled";
  240. if (sceneModified)
  241. sceneName += "*";
  242. graphics.windowTitle = "Urho3D editor - " + sceneName;
  243. }
  244. void HandlePopup(Menu@ menu)
  245. {
  246. // Close the top level menu now unless the selected menu item has another popup
  247. if (menu.popup !is null)
  248. return;
  249. for (;;)
  250. {
  251. UIElement@ menuParent = menu.parent;
  252. if (menuParent is null)
  253. break;
  254. Menu@ nextMenu = menuParent.vars["Origin"].GetUIElement();
  255. if (nextMenu is null)
  256. break;
  257. else
  258. menu = nextMenu;
  259. }
  260. if (menu.parent is uiMenuBar)
  261. menu.showPopup = false;
  262. }
  263. void HandleMenuSelected(StringHash eventType, VariantMap& eventData)
  264. {
  265. Menu@ menu = eventData["Element"].GetUIElement();
  266. if (menu is null)
  267. return;
  268. String action = menu.name;
  269. if (action.empty)
  270. return;
  271. HandlePopup(menu);
  272. if (uiFileSelector is null)
  273. {
  274. if (action == "New scene")
  275. ResetScene();
  276. else if (action == "Open scene...")
  277. {
  278. CreateFileSelector("Open scene", "Open", "Cancel", uiScenePath, uiSceneFilters, uiSceneFilter);
  279. SubscribeToEvent(uiFileSelector, "FileSelected", "HandleOpenSceneFile");
  280. }
  281. else if (action == "Save scene" && !sceneFileName.empty)
  282. SaveScene(sceneFileName);
  283. else if (action == "Save scene as..." || action == "Save scene")
  284. {
  285. CreateFileSelector("Save scene as", "Save", "Cancel", uiScenePath, uiSceneFilters, uiSceneFilter);
  286. uiFileSelector.fileName = GetFileNameAndExtension(sceneFileName);
  287. SubscribeToEvent(uiFileSelector, "FileSelected", "HandleSaveSceneFile");
  288. }
  289. else if (action == "As replicated...")
  290. {
  291. instantiateMode = REPLICATED;
  292. CreateFileSelector("Load node", "Load", "Cancel", uiNodePath, uiSceneFilters, uiNodeFilter);
  293. SubscribeToEvent(uiFileSelector, "FileSelected", "HandleLoadNodeFile");
  294. }
  295. else if (action == "As local...")
  296. {
  297. instantiateMode = LOCAL;
  298. CreateFileSelector("Load node", "Load", "Cancel", uiNodePath, uiSceneFilters, uiNodeFilter);
  299. SubscribeToEvent(uiFileSelector, "FileSelected", "HandleLoadNodeFile");
  300. }
  301. else if (action == "Save node as...")
  302. {
  303. if (selectedNodes.length == 1 && selectedNodes[0] !is editorScene)
  304. {
  305. CreateFileSelector("Save node", "Save", "Cancel", uiNodePath, uiSceneFilters, uiNodeFilter);
  306. uiFileSelector.fileName = GetFileNameAndExtension(instantiateFileName);
  307. SubscribeToEvent(uiFileSelector, "FileSelected", "HandleSaveNodeFile");
  308. }
  309. }
  310. else if (action == "Import model...")
  311. {
  312. CreateFileSelector("Import model", "Import", "Cancel", uiImportPath, uiAllFilters, uiImportFilter);
  313. SubscribeToEvent(uiFileSelector, "FileSelected", "HandleImportModel");
  314. }
  315. else if (action == "Import scene...")
  316. {
  317. CreateFileSelector("Import scene", "Import", "Cancel", uiImportPath, uiAllFilters, uiImportFilter);
  318. SubscribeToEvent(uiFileSelector, "FileSelected", "HandleImportScene");
  319. }
  320. else if (action == "Run script...")
  321. {
  322. CreateFileSelector("Run script", "Run", "Cancel", uiScriptPath, uiScriptFilters, uiScriptFilter);
  323. SubscribeToEvent(uiFileSelector, "FileSelected", "HandleRunScript");
  324. }
  325. else if (action == "Set resource path...")
  326. {
  327. CreateFileSelector("Set resource path", "Set", "Cancel", sceneResourcePath, uiAllFilters, 0);
  328. uiFileSelector.directoryMode = true;
  329. SubscribeToEvent(uiFileSelector, "FileSelected", "HandleResourcePath");
  330. }
  331. }
  332. if (action == "Hierarchy")
  333. ShowSceneWindow();
  334. else if (action == "Attribute inspector")
  335. ShowNodeWindow();
  336. else if (action == "Editor settings")
  337. ShowEditorSettingsDialog();
  338. else if (action == "Editor preferences")
  339. ShowEditorPreferencesDialog();
  340. else if (action == "Cut")
  341. SceneCut();
  342. else if (action == "Copy")
  343. SceneCopy();
  344. else if (action == "Paste")
  345. ScenePaste();
  346. else if (action == "Delete")
  347. SceneDelete();
  348. else if (action == "Reset position")
  349. SceneResetPosition();
  350. else if (action == "Reset rotation")
  351. SceneResetRotation();
  352. else if (action == "Reset scale")
  353. SceneResetScale();
  354. else if (action == "Unparent")
  355. SceneUnparent();
  356. else if (action == "Select all")
  357. SceneSelectAll();
  358. else if (action == "Toggle update")
  359. ToggleUpdate();
  360. else if (action == "Box" || action == "Cone" || action == "Cylinder" || action == "Plane" ||
  361. action == "Pyramid" || action == "Sphere")
  362. CreateBuiltinObject(action);
  363. else if (action == "Exit")
  364. engine.Exit();
  365. }
  366. String ExtractFileName(VariantMap& eventData)
  367. {
  368. String fileName;
  369. // Check for OK
  370. if (eventData["OK"].GetBool())
  371. fileName = eventData["FileName"].GetString();
  372. return fileName;
  373. }
  374. void HandleOpenSceneFile(StringHash eventType, VariantMap& eventData)
  375. {
  376. CloseFileSelector(uiSceneFilter, uiScenePath);
  377. LoadScene(ExtractFileName(eventData));
  378. }
  379. void HandleSaveSceneFile(StringHash eventType, VariantMap& eventData)
  380. {
  381. CloseFileSelector(uiSceneFilter, uiScenePath);
  382. SaveScene(ExtractFileName(eventData));
  383. }
  384. void HandleLoadNodeFile(StringHash eventType, VariantMap& eventData)
  385. {
  386. CloseFileSelector(uiNodeFilter, uiNodePath);
  387. LoadNode(ExtractFileName(eventData));
  388. }
  389. void HandleSaveNodeFile(StringHash eventType, VariantMap& eventData)
  390. {
  391. CloseFileSelector(uiNodeFilter, uiNodePath);
  392. SaveNode(ExtractFileName(eventData));
  393. }
  394. void HandleImportModel(StringHash eventType, VariantMap& eventData)
  395. {
  396. CloseFileSelector(uiImportFilter, uiImportPath);
  397. ImportModel(ExtractFileName(eventData));
  398. }
  399. void HandleImportScene(StringHash eventType, VariantMap& eventData)
  400. {
  401. CloseFileSelector(uiImportFilter, uiImportPath);
  402. ImportScene(ExtractFileName(eventData));
  403. }
  404. void ExecuteScript(const String&in fileName)
  405. {
  406. if (fileName.empty)
  407. return;
  408. File@ file = File(fileName, FILE_READ);
  409. if (file.open)
  410. {
  411. String scriptCode;
  412. while (!file.eof)
  413. scriptCode += file.ReadLine() + "\n";
  414. file.Close();
  415. if (script.Execute(scriptCode))
  416. log.Info("Script " + fileName + " ran successfully");
  417. }
  418. }
  419. void HandleRunScript(StringHash eventType, VariantMap& eventData)
  420. {
  421. CloseFileSelector(uiScriptFilter, uiScriptPath);
  422. ExecuteScript(ExtractFileName(eventData));
  423. }
  424. void HandleResourcePath(StringHash eventType, VariantMap& eventData)
  425. {
  426. CloseFileSelector();
  427. SetResourcePath(ExtractFileName(eventData), false);
  428. }
  429. void HandleKeyDown(StringHash eventType, VariantMap& eventData)
  430. {
  431. int key = eventData["Key"].GetInt();
  432. if (key == KEY_F1)
  433. console.Toggle();
  434. if (key == KEY_ESC)
  435. {
  436. UIElement@ front = ui.frontElement;
  437. if (console.visible)
  438. console.visible = false;
  439. else if (uiFileSelector !is null && front is uiFileSelector.window)
  440. CloseFileSelector();
  441. else if (front is settingsDialog)
  442. {
  443. ui.focusElement = null;
  444. front.visible = false;
  445. }
  446. }
  447. if (key == KEY_F2)
  448. ToggleRenderingDebug();
  449. if (key == KEY_F3)
  450. TogglePhysicsDebug();
  451. if (key == KEY_F4)
  452. ToggleOctreeDebug();
  453. if (eventData["Qualifiers"].GetInt() == QUAL_CTRL)
  454. {
  455. if (key == '1')
  456. editMode = EDIT_MOVE;
  457. else if (key == '2')
  458. editMode = EDIT_ROTATE;
  459. else if (key == '3')
  460. editMode = EDIT_SCALE;
  461. if (key == '4')
  462. editMode = EDIT_SELECT;
  463. else if (key == '5')
  464. axisMode = AxisMode(axisMode ^ AXIS_LOCAL);
  465. else if (key == '6')
  466. {
  467. --pickMode;
  468. if (pickMode < PICK_GEOMETRIES)
  469. pickMode = MAX_PICK_MODES - 1;
  470. }
  471. else if (key == '7')
  472. {
  473. ++pickMode;
  474. if (pickMode >= MAX_PICK_MODES)
  475. pickMode = PICK_GEOMETRIES;
  476. }
  477. else if (key == 'W')
  478. {
  479. fillMode = FillMode(fillMode + 1);
  480. if (fillMode > FILL_POINT)
  481. fillMode = FILL_SOLID;
  482. // Update camera fill mode
  483. camera.fillMode = fillMode;
  484. }
  485. else
  486. SteppedObjectManipulation(key);
  487. }
  488. }
  489. void UnhideUI()
  490. {
  491. HideUI(false);
  492. }
  493. void HideUI(bool hide = true)
  494. {
  495. if (uiHidden == hide)
  496. return;
  497. float opacity = (uiHidden = hide) ? uiMinOpacity : uiMaxOpacity;
  498. Array<UIElement@> children = ui.root.GetChildren();
  499. for (uint i = 0; i < children.length; ++i)
  500. {
  501. if (children[i].type != textType)
  502. children[i].opacity = opacity;
  503. }
  504. }