EditorUIElement.as 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670
  1. // Urho3D editor UI-element handling
  2. UIElement@ editorUIElement;
  3. XMLFile@ uiElementDefaultStyle;
  4. Array<String> availableStyles;
  5. UIElement@ editUIElement;
  6. Array<Serializable@> selectedUIElements;
  7. Array<Serializable@> editUIElements;
  8. Array<XMLFile@> uiElementCopyBuffer;
  9. bool suppressUIElementChanges = false;
  10. // Registered UIElement user variable reverse mappings
  11. VariantMap uiElementVarNames;
  12. const StringHash FILENAME_VAR("FileName");
  13. const StringHash MODIFIED_VAR("Modified");
  14. const StringHash CHILD_ELEMENT_FILENAME_VAR("ChildElemFileName");
  15. void ClearUIElementSelection()
  16. {
  17. editUIElement = null;
  18. selectedUIElements.Clear();
  19. editUIElements.Clear();
  20. }
  21. void CreateRootUIElement()
  22. {
  23. // Create a root UIElement only once here, do not confuse this with ui.root itself
  24. editorUIElement = ui.root.CreateChild("UIElement");
  25. editorUIElement.name = "UI";
  26. editorUIElement.SetSize(graphics.width, graphics.height);
  27. editorUIElement.traversalMode = TM_DEPTH_FIRST; // This is needed for root-like element to prevent artifacts
  28. editorUIElement.priority = -1000; // All user-created UI elements have lowest priority so they do not cover editor's windows
  29. // This is needed to distinguish our own element events from Editor's UI element events
  30. editorUIElement.elementEventSender = true;
  31. SubscribeToEvent(editorUIElement, "ElementAdded", "HandleUIElementAdded");
  32. SubscribeToEvent(editorUIElement, "ElementRemoved", "HandleUIElementRemoved");
  33. // Since this root UIElement is not being handled by above handlers, update it into hierarchy list manually as another list root item
  34. UpdateHierarchyItem(M_MAX_UNSIGNED, editorUIElement, null);
  35. }
  36. bool NewUIElement(const String&in typeName)
  37. {
  38. // If no edit element then parented to root
  39. UIElement@ parent = editUIElement !is null ? editUIElement : editorUIElement;
  40. UIElement@ element = parent.CreateChild(typeName);
  41. if (element !is null)
  42. {
  43. // Use the predefined UI style if set, otherwise use editor's own UI style
  44. XMLFile@ defaultStyle = uiElementDefaultStyle !is null ? uiElementDefaultStyle : uiStyle;
  45. if (editUIElement is null)
  46. {
  47. // If parented to root, set the internal variables
  48. element.vars[FILENAME_VAR] = "";
  49. element.vars[MODIFIED_VAR] = false;
  50. // set a default UI style
  51. element.defaultStyle = defaultStyle;
  52. // and position the newly created element at center
  53. CenterDialog(element);
  54. }
  55. // Apply the auto style
  56. element.style = AUTO_STYLE;
  57. // Do not allow UI subsystem to reorder children while editing the element in the editor
  58. element.sortChildren = false;
  59. // Create an undo action for the create
  60. CreateUIElementAction action;
  61. action.Define(element);
  62. SaveEditAction(action);
  63. SetUIElementModified(element);
  64. FocusUIElement(element);
  65. }
  66. return true;
  67. }
  68. void ResetSortChildren(UIElement@ element)
  69. {
  70. element.sortChildren = false;
  71. // Perform the action recursively for child elements
  72. for (uint i = 0; i < element.numChildren; ++i)
  73. ResetSortChildren(element.children[i]);
  74. }
  75. void OpenUILayout(const String&in fileName)
  76. {
  77. if (fileName.empty)
  78. return;
  79. ui.cursor.shape = CS_BUSY;
  80. // Check if the UI element has been opened before
  81. if (editorUIElement.GetChild(FILENAME_VAR, Variant(fileName)) !is null)
  82. {
  83. MessageBox("UI element is already opened.\n" + fileName);
  84. return;
  85. }
  86. // Always load from the filesystem, not from resource paths
  87. if (!fileSystem.FileExists(fileName))
  88. {
  89. MessageBox("No such file.\n" + fileName);
  90. return;
  91. }
  92. File file(fileName, FILE_READ);
  93. if (!file.open)
  94. {
  95. MessageBox("Could not open file.\n" + fileName);
  96. return;
  97. }
  98. // Add the UI layout's resource path in case it's necessary
  99. SetResourcePath(GetPath(fileName), true, true);
  100. XMLFile@ xmlFile = XMLFile();
  101. xmlFile.Load(file);
  102. suppressUIElementChanges = true;
  103. // If uiElementDefaultStyle is not set then automatically fallback to use the editor's own default style
  104. UIElement@ element = ui.LoadLayout(xmlFile, uiElementDefaultStyle);
  105. if (element !is null)
  106. {
  107. element.vars[FILENAME_VAR] = fileName;
  108. element.vars[MODIFIED_VAR] = false;
  109. // Do not allow UI subsystem to reorder children while editing the element in the editor
  110. ResetSortChildren(element);
  111. // Register variable names from the 'enriched' XMLElement, if any
  112. RegisterUIElementVar(xmlFile.root);
  113. editorUIElement.AddChild(element);
  114. UpdateHierarchyItem(element);
  115. FocusUIElement(element);
  116. ClearEditActions();
  117. }
  118. else
  119. MessageBox("Could not load UI layout successfully!\nSee Urho3D.log for more detail.");
  120. suppressUIElementChanges = false;
  121. }
  122. bool CloseUILayout()
  123. {
  124. ui.cursor.shape = CS_BUSY;
  125. if (messageBoxCallback is null)
  126. {
  127. for (uint i = 0; i < selectedUIElements.length; ++i)
  128. {
  129. UIElement@ element = GetTopLevelUIElement(selectedUIElements[i]);
  130. if (element !is null && element.vars[MODIFIED_VAR].GetBool())
  131. {
  132. MessageBox@ messageBox = MessageBox("UI layout has been modified.\nContinue to close?", "Warning");
  133. if (messageBox.window !is null)
  134. {
  135. Button@ cancelButton = messageBox.window.GetChild("CancelButton", true);
  136. cancelButton.visible = true;
  137. cancelButton.focus = true;
  138. SubscribeToEvent(messageBox, "MessageACK", "HandleMessageAcknowledgement");
  139. messageBoxCallback = @CloseUILayout;
  140. return false;
  141. }
  142. }
  143. }
  144. }
  145. else
  146. messageBoxCallback = null;
  147. suppressUIElementChanges = true;
  148. for (uint i = 0; i < selectedUIElements.length; ++i)
  149. {
  150. UIElement@ element = GetTopLevelUIElement(selectedUIElements[i]);
  151. if (element !is null)
  152. {
  153. element.Remove();
  154. UpdateHierarchyItem(GetListIndex(element), null, null);
  155. }
  156. }
  157. hierarchyList.ClearSelection();
  158. ClearEditActions();
  159. suppressUIElementChanges = false;
  160. return true;
  161. }
  162. bool CloseAllUILayouts()
  163. {
  164. ui.cursor.shape = CS_BUSY;
  165. if (messageBoxCallback is null)
  166. {
  167. for (uint i = 0; i < editorUIElement.numChildren; ++i)
  168. {
  169. UIElement@ element = editorUIElement.children[i];
  170. if (element !is null && element.vars[MODIFIED_VAR].GetBool())
  171. {
  172. MessageBox@ messageBox = MessageBox("UI layout has been modified.\nContinue to close?", "Warning");
  173. if (messageBox.window !is null)
  174. {
  175. Button@ cancelButton = messageBox.window.GetChild("CancelButton", true);
  176. cancelButton.visible = true;
  177. cancelButton.focus = true;
  178. SubscribeToEvent(messageBox, "MessageACK", "HandleMessageAcknowledgement");
  179. messageBoxCallback = @CloseAllUILayouts;
  180. return false;
  181. }
  182. }
  183. }
  184. }
  185. else
  186. messageBoxCallback = null;
  187. suppressUIElementChanges = true;
  188. editorUIElement.RemoveAllChildren();
  189. UpdateHierarchyItem(editorUIElement, true);
  190. // Reset element ID number generator
  191. uiElementNextID = UI_ELEMENT_BASE_ID + 1;
  192. hierarchyList.ClearSelection();
  193. ClearEditActions();
  194. suppressUIElementChanges = false;
  195. return true;
  196. }
  197. bool SaveUILayout(const String&in fileName)
  198. {
  199. if (fileName.empty)
  200. return false;
  201. ui.cursor.shape = CS_BUSY;
  202. MakeBackup(fileName);
  203. File file(fileName, FILE_WRITE);
  204. if (!file.open)
  205. {
  206. MessageBox("Could not open file.\n" + fileName);
  207. return false;
  208. }
  209. UIElement@ element = GetTopLevelUIElement(editUIElement);
  210. if (element is null)
  211. return false;
  212. XMLFile@ elementData = XMLFile();
  213. XMLElement rootElem = elementData.CreateRoot("element");
  214. bool success = element.SaveXML(rootElem);
  215. RemoveBackup(success, fileName);
  216. if (success)
  217. {
  218. FilterInternalVars(rootElem);
  219. success = elementData.Save(file);
  220. if (success)
  221. {
  222. element.vars[FILENAME_VAR] = fileName;
  223. SetUIElementModified(element, false);
  224. }
  225. }
  226. if (!success)
  227. MessageBox("Could not save UI layout successfully!\nSee Urho3D.log for more detail.");
  228. return success;
  229. }
  230. bool SaveUILayoutWithExistingName()
  231. {
  232. ui.cursor.shape = CS_BUSY;
  233. UIElement@ element = GetTopLevelUIElement(editUIElement);
  234. if (element is null)
  235. return false;
  236. String fileName = element.GetVar(FILENAME_VAR).GetString();
  237. if (fileName.empty)
  238. return PickFile(); // No name yet, so pick one
  239. else
  240. return SaveUILayout(fileName);
  241. }
  242. void LoadChildUIElement(const String&in fileName)
  243. {
  244. if (fileName.empty)
  245. return;
  246. ui.cursor.shape = CS_BUSY;
  247. if (!fileSystem.FileExists(fileName))
  248. {
  249. MessageBox("No such file.\n" + fileName);
  250. return;
  251. }
  252. File file(fileName, FILE_READ);
  253. if (!file.open)
  254. {
  255. MessageBox("Could not open file.\n" + fileName);
  256. return;
  257. }
  258. XMLFile@ xmlFile = XMLFile();
  259. xmlFile.Load(file);
  260. suppressUIElementChanges = true;
  261. if (editUIElement.LoadChildXML(xmlFile, uiElementDefaultStyle !is null ? uiElementDefaultStyle : uiStyle))
  262. {
  263. XMLElement rootElem = xmlFile.root;
  264. uint index = rootElem.HasAttribute("index") ? rootElem.GetUInt("index") : editUIElement.numChildren - 1;
  265. UIElement@ element = editUIElement.children[index];
  266. ResetSortChildren(element);
  267. RegisterUIElementVar(xmlFile.root);
  268. element.vars[CHILD_ELEMENT_FILENAME_VAR] = fileName;
  269. if (index == editUIElement.numChildren - 1)
  270. UpdateHierarchyItem(element);
  271. else
  272. // If not last child, find the list index of the next sibling as the insertion index
  273. UpdateHierarchyItem(GetListIndex(editUIElement.children[index + 1]), element, hierarchyList.items[GetListIndex(editUIElement)]);
  274. SetUIElementModified(element);
  275. // Create an undo action for the load
  276. CreateUIElementAction action;
  277. action.Define(element);
  278. SaveEditAction(action);
  279. FocusUIElement(element);
  280. }
  281. suppressUIElementChanges = false;
  282. }
  283. bool SaveChildUIElement(const String&in fileName)
  284. {
  285. if (fileName.empty)
  286. return false;
  287. ui.cursor.shape = CS_BUSY;
  288. MakeBackup(fileName);
  289. File file(fileName, FILE_WRITE);
  290. if (!file.open)
  291. {
  292. MessageBox("Could not open file.\n" + fileName);
  293. return false;
  294. }
  295. XMLFile@ elementData = XMLFile();
  296. XMLElement rootElem = elementData.CreateRoot("element");
  297. bool success = editUIElement.SaveXML(rootElem);
  298. RemoveBackup(success, fileName);
  299. if (success)
  300. {
  301. FilterInternalVars(rootElem);
  302. success = elementData.Save(file);
  303. if (success)
  304. editUIElement.vars[CHILD_ELEMENT_FILENAME_VAR] = fileName;
  305. }
  306. if (!success)
  307. MessageBox("Could not save child UI element successfully!\nSee Urho3D.log for more detail.");
  308. return success;
  309. }
  310. void SetUIElementDefaultStyle(const String&in fileName)
  311. {
  312. if (fileName.empty)
  313. return;
  314. ui.cursor.shape = CS_BUSY;
  315. // Always load from the filesystem, not from resource paths
  316. if (!fileSystem.FileExists(fileName))
  317. {
  318. MessageBox("No such file.\n" + fileName);
  319. return;
  320. }
  321. File file(fileName, FILE_READ);
  322. if (!file.open)
  323. {
  324. MessageBox("Could not open file.\n" + fileName);
  325. return;
  326. }
  327. uiElementDefaultStyle = XMLFile();
  328. uiElementDefaultStyle.Load(file);
  329. // Remove the existing style list to ensure it gets repopulated again with the new default style file
  330. availableStyles.Clear();
  331. // Refresh Attribute Inspector when it is currently showing attributes of UI-element item type as the existing styles in the style drop down list are not valid anymore
  332. if (!editUIElements.empty)
  333. attributesFullDirty = true;
  334. }
  335. // Prepare XPath query object only once and use it multiple times
  336. XPathQuery filterInternalVarsQuery("//attribute[@name='Variables']/variant");
  337. void FilterInternalVars(XMLElement source)
  338. {
  339. XPathResultSet resultSet = filterInternalVarsQuery.Evaluate(source);
  340. XMLElement resultElem = resultSet.firstResult;
  341. while (resultElem.notNull)
  342. {
  343. String name = GetVariableName(resultElem.GetUInt("hash"));
  344. if (name.empty)
  345. {
  346. XMLElement parent = resultElem.parent;
  347. // If variable name is empty (or unregistered) then it is an internal variable and should be removed
  348. if (parent.RemoveChild(resultElem))
  349. {
  350. // If parent does not have any children anymore then remove the parent also
  351. if (!parent.HasChild("variant"))
  352. parent.parent.RemoveChild(parent);
  353. }
  354. }
  355. else
  356. // If it is registered then it is a user-defined variable, so 'enrich' the XMLElement to store the variable name in plaintext
  357. resultElem.SetAttribute("name", name);
  358. resultElem = resultElem.nextResult;
  359. }
  360. }
  361. XPathQuery registerUIElemenVarsQuery("//attribute[@name='Variables']/variant/@name");
  362. void RegisterUIElementVar(XMLElement source)
  363. {
  364. XPathResultSet resultSet = registerUIElemenVarsQuery.Evaluate(source);
  365. XMLElement resultAttr = resultSet.firstResult; // Since we are selecting attribute, the resultset is in attribute context
  366. while (resultAttr.notNull)
  367. {
  368. String name = resultAttr.GetAttribute();
  369. uiElementVarNames[name] = name;
  370. resultAttr = resultAttr.nextResult;
  371. }
  372. }
  373. UIElement@ GetTopLevelUIElement(UIElement@ element)
  374. {
  375. // Only top level UI-element contains the FILENAME_VAR
  376. while (element !is null && !element.vars.Contains(FILENAME_VAR))
  377. element = element.parent;
  378. return element;
  379. }
  380. void SetUIElementModified(UIElement@ element, bool flag = true)
  381. {
  382. element = GetTopLevelUIElement(element);
  383. if (element !is null && element.GetVar(MODIFIED_VAR).GetBool() != flag)
  384. {
  385. element.vars[MODIFIED_VAR] = flag;
  386. UpdateHierarchyItemText(GetListIndex(element), element.visible, GetUIElementTitle(element));
  387. }
  388. }
  389. XPathQuery availableStylesXPathQuery("/elements/element[@auto='false']/@type");
  390. void GetAvailableStyles()
  391. {
  392. // Use the predefined UI style if set, otherwise use editor's own UI style
  393. XMLFile@ defaultStyle = uiElementDefaultStyle !is null ? uiElementDefaultStyle : uiStyle;
  394. XMLElement rootElem = defaultStyle.root;
  395. XPathResultSet resultSet = availableStylesXPathQuery.Evaluate(rootElem);
  396. XMLElement resultElem = resultSet.firstResult;
  397. while (resultElem.notNull)
  398. {
  399. availableStyles.Push(resultElem.GetAttribute());
  400. resultElem = resultElem.nextResult;
  401. }
  402. availableStyles.Sort();
  403. }
  404. void PopulateStyleList(DropDownList@ styleList)
  405. {
  406. if (availableStyles.empty)
  407. GetAvailableStyles();
  408. for (uint i = 0; i < availableStyles.length; ++i)
  409. {
  410. Text@ choice = Text();
  411. styleList.AddItem(choice);
  412. choice.style = "EditorEnumAttributeText";
  413. choice.text = availableStyles[i];
  414. }
  415. }
  416. bool UIElementCut()
  417. {
  418. return UIElementCopy() && UIElementDelete();
  419. }
  420. bool UIElementCopy()
  421. {
  422. ui.cursor.shape = CS_BUSY;
  423. uiElementCopyBuffer.Clear();
  424. for (uint i = 0; i < selectedUIElements.length; ++i)
  425. {
  426. XMLFile@ xml = XMLFile();
  427. XMLElement rootElem = xml.CreateRoot("element");
  428. selectedUIElements[i].SaveXML(rootElem);
  429. uiElementCopyBuffer.Push(xml);
  430. }
  431. return true;
  432. }
  433. void ResetDuplicateID(UIElement@ element)
  434. {
  435. // If it is a duplicate copy then the element ID need to be regenerated by resetting it now to empty
  436. if (GetListIndex(element) != NO_ITEM)
  437. element.vars[UI_ELEMENT_ID_VAR] = Variant();
  438. // Perform the action recursively for child elements
  439. for (uint i = 0; i < element.numChildren; ++i)
  440. ResetDuplicateID(element.children[i]);
  441. }
  442. bool UIElementPaste()
  443. {
  444. ui.cursor.shape = CS_BUSY;
  445. // Group for storing undo actions
  446. EditActionGroup group;
  447. // Have to update manually because the element ID var is not set yet when the E_ELEMENTADDED event is sent
  448. suppressUIElementChanges = true;
  449. for (uint i = 0; i < uiElementCopyBuffer.length; ++i)
  450. {
  451. XMLElement rootElem = uiElementCopyBuffer[i].root;
  452. if (editUIElement.LoadChildXML(rootElem, null))
  453. {
  454. UIElement@ element = editUIElement.children[editUIElement.numChildren - 1];
  455. ResetDuplicateID(element);
  456. UpdateHierarchyItem(element);
  457. SetUIElementModified(editUIElement);
  458. // Create an undo action
  459. CreateUIElementAction action;
  460. action.Define(element);
  461. group.actions.Push(action);
  462. }
  463. }
  464. SaveEditActionGroup(group);
  465. suppressUIElementChanges = false;
  466. return true;
  467. }
  468. bool UIElementDelete()
  469. {
  470. ui.cursor.shape = CS_BUSY;
  471. BeginSelectionModify();
  472. // Clear the selection now to prevent deleted elements from being reselected
  473. hierarchyList.ClearSelection();
  474. // Group for storing undo actions
  475. EditActionGroup group;
  476. for (uint i = 0; i < selectedUIElements.length; ++i)
  477. {
  478. UIElement@ element = selectedUIElements[i];
  479. if (element.parent is null)
  480. continue; // Already deleted
  481. uint index = GetListIndex(element);
  482. // Create undo action
  483. DeleteUIElementAction action;
  484. action.Define(element);
  485. group.actions.Push(action);
  486. SetUIElementModified(element);
  487. element.Remove();
  488. // If deleting only one element, select the next item in the same index
  489. if (selectedUIElements.length == 1)
  490. hierarchyList.selection = index;
  491. }
  492. SaveEditActionGroup(group);
  493. EndSelectionModify();
  494. return true;
  495. }
  496. bool UIElementSelectAll()
  497. {
  498. BeginSelectionModify();
  499. Array<uint> indices;
  500. uint baseIndex = GetListIndex(editorUIElement);
  501. indices.Push(baseIndex);
  502. int baseIndent = hierarchyList.items[baseIndex].indent;
  503. for (uint i = baseIndex + 1; i < hierarchyList.numItems; ++i)
  504. {
  505. if (hierarchyList.items[i].indent <= baseIndent)
  506. break;
  507. indices.Push(i);
  508. }
  509. hierarchyList.SetSelections(indices);
  510. EndSelectionModify();
  511. return true;
  512. }
  513. bool UIElementResetToDefault()
  514. {
  515. ui.cursor.shape = CS_BUSY;
  516. // Group for storing undo actions
  517. EditActionGroup group;
  518. // Reset selected elements to their default values
  519. for (uint i = 0; i < selectedUIElements.length; ++i)
  520. {
  521. UIElement@ element = selectedUIElements[i];
  522. ResetAttributesAction action;
  523. action.Define(element);
  524. group.actions.Push(action);
  525. element.ResetToDefault();
  526. action.SetInternalVars(element);
  527. element.ApplyAttributes();
  528. for (uint j = 0; j < element.numAttributes; ++j)
  529. PostEditAttribute(element, j);
  530. SetUIElementModified(element);
  531. }
  532. SaveEditActionGroup(group);
  533. attributesFullDirty = true;
  534. return true;
  535. }
  536. bool UIElementChangeParent(UIElement@ sourceElement, UIElement@ targetElement)
  537. {
  538. ReparentUIElementAction action;
  539. action.Define(sourceElement, targetElement);
  540. SaveEditAction(action);
  541. sourceElement.parent = targetElement;
  542. SetUIElementModified(targetElement);
  543. return sourceElement.parent is targetElement;
  544. }