EditorUIElement.as 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658
  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 ShortStringHash FILENAME_VAR("FileName");
  13. const ShortStringHash MODIFIED_VAR("Modified");
  14. const ShortStringHash 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. Button@ cancelButton = messageBox.window.GetChild("CancelButton", true);
  134. cancelButton.visible = true;
  135. cancelButton.focus = true;
  136. SubscribeToEvent(messageBox, "MessageACK", "HandleMessageAcknowledgement");
  137. messageBoxCallback = @CloseUILayout;
  138. return false;
  139. }
  140. }
  141. }
  142. else
  143. messageBoxCallback = null;
  144. suppressUIElementChanges = true;
  145. for (uint i = 0; i < selectedUIElements.length; ++i)
  146. {
  147. UIElement@ element = GetTopLevelUIElement(selectedUIElements[i]);
  148. if (element !is null)
  149. {
  150. element.Remove();
  151. UpdateHierarchyItem(GetListIndex(element), null, null);
  152. }
  153. }
  154. hierarchyList.ClearSelection();
  155. ClearEditActions();
  156. suppressUIElementChanges = false;
  157. return true;
  158. }
  159. bool CloseAllUILayouts()
  160. {
  161. ui.cursor.shape = CS_BUSY;
  162. if (messageBoxCallback is null)
  163. {
  164. for (uint i = 0; i < editorUIElement.numChildren; ++i)
  165. {
  166. UIElement@ element = editorUIElement.children[i];
  167. if (element !is null && element.vars[MODIFIED_VAR].GetBool())
  168. {
  169. MessageBox@ messageBox = MessageBox("UI layout has been modified.\nContinue to close?", "Warning");
  170. Button@ cancelButton = messageBox.window.GetChild("CancelButton", true);
  171. cancelButton.visible = true;
  172. cancelButton.focus = true;
  173. SubscribeToEvent(messageBox, "MessageACK", "HandleMessageAcknowledgement");
  174. messageBoxCallback = @CloseAllUILayouts;
  175. return false;
  176. }
  177. }
  178. }
  179. else
  180. messageBoxCallback = null;
  181. suppressUIElementChanges = true;
  182. editorUIElement.RemoveAllChildren();
  183. UpdateHierarchyItem(editorUIElement, true);
  184. // Reset element ID number generator
  185. uiElementNextID = UI_ELEMENT_BASE_ID + 1;
  186. hierarchyList.ClearSelection();
  187. ClearEditActions();
  188. suppressUIElementChanges = false;
  189. return true;
  190. }
  191. bool SaveUILayout(const String&in fileName)
  192. {
  193. if (fileName.empty)
  194. return false;
  195. ui.cursor.shape = CS_BUSY;
  196. File file(fileName, FILE_WRITE);
  197. if (!file.open)
  198. {
  199. MessageBox("Could not open file.\n" + fileName);
  200. return false;
  201. }
  202. UIElement@ element = GetTopLevelUIElement(editUIElement);
  203. if (element is null)
  204. return false;
  205. XMLFile@ elementData = XMLFile();
  206. XMLElement rootElem = elementData.CreateRoot("element");
  207. bool success = element.SaveXML(rootElem);
  208. if (success)
  209. {
  210. FilterInternalVars(rootElem);
  211. success = elementData.Save(file);
  212. if (success)
  213. {
  214. element.vars[FILENAME_VAR] = fileName;
  215. SetUIElementModified(element, false);
  216. }
  217. }
  218. if (!success)
  219. MessageBox("Could not save UI layout successfully!\nSee Urho3D.log for more detail.");
  220. return success;
  221. }
  222. bool SaveUILayoutWithExistingName()
  223. {
  224. ui.cursor.shape = CS_BUSY;
  225. UIElement@ element = GetTopLevelUIElement(editUIElement);
  226. if (element is null)
  227. return false;
  228. String fileName = element.GetVar(FILENAME_VAR).GetString();
  229. if (fileName.empty)
  230. return PickFile(); // No name yet, so pick one
  231. else
  232. return SaveUILayout(fileName);
  233. }
  234. void LoadChildUIElement(const String&in fileName)
  235. {
  236. if (fileName.empty)
  237. return;
  238. ui.cursor.shape = CS_BUSY;
  239. if (!fileSystem.FileExists(fileName))
  240. {
  241. MessageBox("No such file.\n" + fileName);
  242. return;
  243. }
  244. File file(fileName, FILE_READ);
  245. if (!file.open)
  246. {
  247. MessageBox("Could not open file.\n" + fileName);
  248. return;
  249. }
  250. XMLFile@ xmlFile = XMLFile();
  251. xmlFile.Load(file);
  252. suppressUIElementChanges = true;
  253. if (editUIElement.LoadChildXML(xmlFile, uiElementDefaultStyle !is null ? uiElementDefaultStyle : uiStyle))
  254. {
  255. XMLElement rootElem = xmlFile.root;
  256. uint index = rootElem.HasAttribute("index") ? rootElem.GetUInt("index") : editUIElement.numChildren - 1;
  257. UIElement@ element = editUIElement.children[index];
  258. ResetSortChildren(element);
  259. RegisterUIElementVar(xmlFile.root);
  260. element.vars[CHILD_ELEMENT_FILENAME_VAR] = fileName;
  261. if (index == editUIElement.numChildren - 1)
  262. UpdateHierarchyItem(element);
  263. else
  264. // If not last child, find the list index of the next sibling as the insertion index
  265. UpdateHierarchyItem(GetListIndex(editUIElement.children[index + 1]), element, hierarchyList.items[GetListIndex(editUIElement)]);
  266. SetUIElementModified(element);
  267. // Create an undo action for the load
  268. CreateUIElementAction action;
  269. action.Define(element);
  270. SaveEditAction(action);
  271. FocusUIElement(element);
  272. }
  273. suppressUIElementChanges = false;
  274. }
  275. bool SaveChildUIElement(const String&in fileName)
  276. {
  277. if (fileName.empty)
  278. return false;
  279. ui.cursor.shape = CS_BUSY;
  280. File file(fileName, FILE_WRITE);
  281. if (!file.open)
  282. {
  283. MessageBox("Could not open file.\n" + fileName);
  284. return false;
  285. }
  286. XMLFile@ elementData = XMLFile();
  287. XMLElement rootElem = elementData.CreateRoot("element");
  288. bool success = editUIElement.SaveXML(rootElem);
  289. if (success)
  290. {
  291. FilterInternalVars(rootElem);
  292. success = elementData.Save(file);
  293. if (success)
  294. editUIElement.vars[CHILD_ELEMENT_FILENAME_VAR] = fileName;
  295. }
  296. if (!success)
  297. MessageBox("Could not save child UI element successfully!\nSee Urho3D.log for more detail.");
  298. return success;
  299. }
  300. void SetUIElementDefaultStyle(const String&in fileName)
  301. {
  302. if (fileName.empty)
  303. return;
  304. ui.cursor.shape = CS_BUSY;
  305. // Always load from the filesystem, not from resource paths
  306. if (!fileSystem.FileExists(fileName))
  307. {
  308. MessageBox("No such file.\n" + fileName);
  309. return;
  310. }
  311. File file(fileName, FILE_READ);
  312. if (!file.open)
  313. {
  314. MessageBox("Could not open file.\n" + fileName);
  315. return;
  316. }
  317. uiElementDefaultStyle = XMLFile();
  318. uiElementDefaultStyle.Load(file);
  319. // Remove the existing style list to ensure it gets repopulated again with the new default style file
  320. availableStyles.Clear();
  321. // 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
  322. if (!editUIElements.empty)
  323. attributesFullDirty = true;
  324. }
  325. // Prepare XPath query object only once and use it multiple times
  326. XPathQuery filterInternalVarsQuery("//attribute[@name='Variables']/variant");
  327. void FilterInternalVars(XMLElement source)
  328. {
  329. XPathResultSet resultSet = filterInternalVarsQuery.Evaluate(source);
  330. XMLElement resultElem = resultSet.firstResult;
  331. while (resultElem.notNull)
  332. {
  333. String name = GetVariableName(resultElem.GetUInt("hash"));
  334. if (name.empty)
  335. {
  336. XMLElement parent = resultElem.parent;
  337. // If variable name is empty (or unregistered) then it is an internal variable and should be removed
  338. if (parent.RemoveChild(resultElem))
  339. {
  340. // If parent does not have any children anymore then remove the parent also
  341. if (!parent.HasChild("variant"))
  342. parent.parent.RemoveChild(parent);
  343. }
  344. }
  345. else
  346. // If it is registered then it is a user-defined variable, so 'enrich' the XMLElement to store the variable name in plaintext
  347. resultElem.SetAttribute("name", name);
  348. resultElem = resultElem.nextResult;
  349. }
  350. }
  351. XPathQuery registerUIElemenVarsQuery("//attribute[@name='Variables']/variant/@name");
  352. void RegisterUIElementVar(XMLElement source)
  353. {
  354. XPathResultSet resultSet = registerUIElemenVarsQuery.Evaluate(source);
  355. XMLElement resultAttr = resultSet.firstResult; // Since we are selecting attribute, the resultset is in attribute context
  356. while (resultAttr.notNull)
  357. {
  358. String name = resultAttr.GetAttribute();
  359. uiElementVarNames[name] = name;
  360. resultAttr = resultAttr.nextResult;
  361. }
  362. }
  363. UIElement@ GetTopLevelUIElement(UIElement@ element)
  364. {
  365. // Only top level UI-element contains the FILENAME_VAR
  366. while (element !is null && !element.vars.Contains(FILENAME_VAR))
  367. element = element.parent;
  368. return element;
  369. }
  370. void SetUIElementModified(UIElement@ element, bool flag = true)
  371. {
  372. element = GetTopLevelUIElement(element);
  373. if (element !is null && element.GetVar(MODIFIED_VAR).GetBool() != flag)
  374. {
  375. element.vars[MODIFIED_VAR] = flag;
  376. UpdateHierarchyItemText(GetListIndex(element), element.visible, GetUIElementTitle(element));
  377. }
  378. }
  379. XPathQuery availableStylesXPathQuery("/elements/element[@auto='false']/@type");
  380. void GetAvailableStyles()
  381. {
  382. // Use the predefined UI style if set, otherwise use editor's own UI style
  383. XMLFile@ defaultStyle = uiElementDefaultStyle !is null ? uiElementDefaultStyle : uiStyle;
  384. XMLElement rootElem = defaultStyle.root;
  385. XPathResultSet resultSet = availableStylesXPathQuery.Evaluate(rootElem);
  386. XMLElement resultElem = resultSet.firstResult;
  387. while (resultElem.notNull)
  388. {
  389. availableStyles.Push(resultElem.GetAttribute());
  390. resultElem = resultElem.nextResult;
  391. }
  392. availableStyles.Sort();
  393. }
  394. void PopulateStyleList(DropDownList@ styleList)
  395. {
  396. if (availableStyles.empty)
  397. GetAvailableStyles();
  398. for (uint i = 0; i < availableStyles.length; ++i)
  399. {
  400. Text@ choice = Text();
  401. styleList.AddItem(choice);
  402. choice.style = "EditorEnumAttributeText";
  403. choice.text = availableStyles[i];
  404. }
  405. }
  406. bool UIElementCut()
  407. {
  408. return UIElementCopy() && UIElementDelete();
  409. }
  410. bool UIElementCopy()
  411. {
  412. ui.cursor.shape = CS_BUSY;
  413. uiElementCopyBuffer.Clear();
  414. for (uint i = 0; i < selectedUIElements.length; ++i)
  415. {
  416. XMLFile@ xml = XMLFile();
  417. XMLElement rootElem = xml.CreateRoot("element");
  418. selectedUIElements[i].SaveXML(rootElem);
  419. uiElementCopyBuffer.Push(xml);
  420. }
  421. return true;
  422. }
  423. void ResetDuplicateID(UIElement@ element)
  424. {
  425. // If it is a duplicate copy then the element ID need to be regenerated by resetting it now to empty
  426. if (GetListIndex(element) != NO_ITEM)
  427. element.vars[UI_ELEMENT_ID_VAR] = Variant();
  428. // Perform the action recursively for child elements
  429. for (uint i = 0; i < element.numChildren; ++i)
  430. ResetDuplicateID(element.children[i]);
  431. }
  432. bool UIElementPaste()
  433. {
  434. ui.cursor.shape = CS_BUSY;
  435. // Group for storing undo actions
  436. EditActionGroup group;
  437. // Have to update manually because the element ID var is not set yet when the E_ELEMENTADDED event is sent
  438. suppressUIElementChanges = true;
  439. for (uint i = 0; i < uiElementCopyBuffer.length; ++i)
  440. {
  441. XMLElement rootElem = uiElementCopyBuffer[i].root;
  442. if (editUIElement.LoadChildXML(rootElem, null))
  443. {
  444. UIElement@ element = editUIElement.children[editUIElement.numChildren - 1];
  445. ResetDuplicateID(element);
  446. UpdateHierarchyItem(element);
  447. SetUIElementModified(editUIElement);
  448. // Create an undo action
  449. CreateUIElementAction action;
  450. action.Define(element);
  451. group.actions.Push(action);
  452. }
  453. }
  454. SaveEditActionGroup(group);
  455. suppressUIElementChanges = false;
  456. return true;
  457. }
  458. bool UIElementDelete()
  459. {
  460. ui.cursor.shape = CS_BUSY;
  461. BeginSelectionModify();
  462. // Clear the selection now to prevent deleted elements from being reselected
  463. hierarchyList.ClearSelection();
  464. // Group for storing undo actions
  465. EditActionGroup group;
  466. for (uint i = 0; i < selectedUIElements.length; ++i)
  467. {
  468. UIElement@ element = selectedUIElements[i];
  469. if (element.parent is null)
  470. continue; // Already deleted
  471. uint index = GetListIndex(element);
  472. // Create undo action
  473. DeleteUIElementAction action;
  474. action.Define(element);
  475. group.actions.Push(action);
  476. SetUIElementModified(element);
  477. element.Remove();
  478. // If deleting only one element, select the next item in the same index
  479. if (selectedUIElements.length == 1)
  480. hierarchyList.selection = index;
  481. }
  482. SaveEditActionGroup(group);
  483. EndSelectionModify();
  484. return true;
  485. }
  486. bool UIElementSelectAll()
  487. {
  488. BeginSelectionModify();
  489. Array<uint> indices;
  490. uint baseIndex = GetListIndex(editorUIElement);
  491. indices.Push(baseIndex);
  492. int baseIndent = hierarchyList.items[baseIndex].indent;
  493. for (uint i = baseIndex + 1; i < hierarchyList.numItems; ++i)
  494. {
  495. if (hierarchyList.items[i].indent <= baseIndent)
  496. break;
  497. indices.Push(i);
  498. }
  499. hierarchyList.SetSelections(indices);
  500. EndSelectionModify();
  501. return true;
  502. }
  503. bool UIElementResetToDefault()
  504. {
  505. ui.cursor.shape = CS_BUSY;
  506. // Group for storing undo actions
  507. EditActionGroup group;
  508. // Reset selected elements to their default values
  509. for (uint i = 0; i < selectedUIElements.length; ++i)
  510. {
  511. UIElement@ element = selectedUIElements[i];
  512. ResetAttributesAction action;
  513. action.Define(element);
  514. group.actions.Push(action);
  515. element.ResetToDefault();
  516. action.SetInternalVars(element);
  517. element.ApplyAttributes();
  518. for (uint j = 0; j < element.numAttributes; ++j)
  519. PostEditAttribute(element, j);
  520. SetUIElementModified(element);
  521. }
  522. SaveEditActionGroup(group);
  523. attributesFullDirty = true;
  524. return true;
  525. }
  526. bool UIElementChangeParent(UIElement@ sourceElement, UIElement@ targetElement)
  527. {
  528. ReparentUIElementAction action;
  529. action.Define(sourceElement, targetElement);
  530. SaveEditAction(action);
  531. sourceElement.parent = targetElement;
  532. SetUIElementModified(targetElement);
  533. return sourceElement.parent is targetElement;
  534. }