EditorInspectorWindow.as 38 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091
  1. // Urho3D editor attribute inspector window handling
  2. #include "Scripts/Editor/AttributeEditor.as"
  3. Window@ attributeInspectorWindow;
  4. UIElement@ parentContainer;
  5. UIElement@ inspectorLockButton;
  6. bool applyMaterialList = true;
  7. bool attributesDirty = false;
  8. bool attributesFullDirty = false;
  9. const String STRIKED_OUT = "——"; // Two unicode EM DASH (U+2014)
  10. const StringHash NODE_IDS_VAR("NodeIDs");
  11. const StringHash COMPONENT_IDS_VAR("ComponentIDs");
  12. const StringHash UI_ELEMENT_IDS_VAR("UIElementIDs");
  13. const int LABEL_WIDTH = 30;
  14. // Constants for accessing xmlResources
  15. Array<XMLFile@> xmlResources;
  16. const uint ATTRIBUTE_RES = 0;
  17. const uint VARIABLE_RES = 1;
  18. const uint STYLE_RES = 2;
  19. const uint TAGS_RES = 3;
  20. uint nodeContainerIndex = M_MAX_UNSIGNED;
  21. uint componentContainerStartIndex = 0;
  22. uint elementContainerIndex = M_MAX_UNSIGNED;
  23. // Script Attribute session storage
  24. VariantMap scriptAttributes;
  25. const uint SCRIPTINSTANCE_ATTRIBUTE_IGNORE = 5;
  26. const uint LUASCRIPTINSTANCE_ATTRIBUTE_IGNORE = 4;
  27. // Node or UIElement hash-to-varname reverse mapping
  28. VariantMap globalVarNames;
  29. bool inspectorLocked = false;
  30. void InitXMLResources()
  31. {
  32. String[] resources = { "UI/EditorInspector_Attribute.xml", "UI/EditorInspector_Variable.xml",
  33. "UI/EditorInspector_Style.xml", "UI/EditorInspector_Tags.xml" };
  34. for (uint i = 0; i < resources.length; ++i)
  35. xmlResources.Push(cache.GetResource("XMLFile", resources[i]));
  36. }
  37. /// Delete all child containers in the inspector list.
  38. void DeleteAllContainers()
  39. {
  40. parentContainer.RemoveAllChildren();
  41. nodeContainerIndex = M_MAX_UNSIGNED;
  42. componentContainerStartIndex = 0;
  43. elementContainerIndex = M_MAX_UNSIGNED;
  44. }
  45. /// Get container at the specified index in the inspector list, the container must be created before.
  46. UIElement@ GetContainer(uint index)
  47. {
  48. return parentContainer.children[index];
  49. }
  50. /// Get node container in the inspector list, create the container if it is not yet available.
  51. UIElement@ GetNodeContainer()
  52. {
  53. if (nodeContainerIndex != M_MAX_UNSIGNED)
  54. return GetContainer(nodeContainerIndex);
  55. nodeContainerIndex = parentContainer.numChildren;
  56. parentContainer.LoadChildXML(xmlResources[ATTRIBUTE_RES], uiStyle);
  57. UIElement@ container = GetContainer(nodeContainerIndex);
  58. container.LoadChildXML(xmlResources[VARIABLE_RES], uiStyle);
  59. SubscribeToEvent(container.GetChild("ResetToDefault", true), "Released", "HandleResetToDefault");
  60. SubscribeToEvent(container.GetChild("NewVarDropDown", true), "ItemSelected", "CreateNodeVariable");
  61. SubscribeToEvent(container.GetChild("DeleteVarButton", true), "Released", "DeleteNodeVariable");
  62. ++componentContainerStartIndex;
  63. parentContainer.LoadChildXML(xmlResources[TAGS_RES], uiStyle);
  64. parentContainer.GetChild("TagsLabel", true).SetFixedWidth(LABEL_WIDTH);
  65. LineEdit@ tagEdit = parentContainer.GetChild("TagsEdit", true);
  66. SubscribeToEvent(tagEdit, "TextChanged", "HandleTagsEdit");
  67. UIElement@ tagSelect = parentContainer.GetChild("TagsSelect", true);
  68. SubscribeToEvent(tagSelect, "Released", "HandleTagsSelect");
  69. ++componentContainerStartIndex;
  70. return container;
  71. }
  72. /// Get component container at the specified index, create the container if it is not yet available at the specified index.
  73. UIElement@ GetComponentContainer(uint index)
  74. {
  75. if (componentContainerStartIndex + index < parentContainer.numChildren)
  76. return GetContainer(componentContainerStartIndex + index);
  77. UIElement@ container;
  78. for (uint i = parentContainer.numChildren; i <= componentContainerStartIndex + index; ++i)
  79. {
  80. parentContainer.LoadChildXML(xmlResources[ATTRIBUTE_RES], uiStyle);
  81. container = GetContainer(i);
  82. SubscribeToEvent(container.GetChild("ResetToDefault", true), "Released", "HandleResetToDefault");
  83. }
  84. return container;
  85. }
  86. /// Get UI-element container, create the container if it is not yet available.
  87. UIElement@ GetUIElementContainer()
  88. {
  89. if (elementContainerIndex != M_MAX_UNSIGNED)
  90. return GetContainer(elementContainerIndex);
  91. elementContainerIndex = parentContainer.numChildren;
  92. parentContainer.LoadChildXML(xmlResources[ATTRIBUTE_RES], uiStyle);
  93. parentContainer.LoadChildXML(xmlResources[TAGS_RES], uiStyle);
  94. parentContainer.GetChild("TagsLabel", true).SetFixedWidth(LABEL_WIDTH);
  95. UIElement@ container = GetContainer(elementContainerIndex);
  96. container.LoadChildXML(xmlResources[VARIABLE_RES], uiStyle);
  97. container.LoadChildXML(xmlResources[STYLE_RES], uiStyle);
  98. DropDownList@ styleList = container.GetChild("StyleDropDown", true);
  99. styleList.placeholderText = STRIKED_OUT;
  100. styleList.parent.GetChild("StyleDropDownLabel").SetFixedWidth(LABEL_WIDTH);
  101. PopulateStyleList(styleList);
  102. SubscribeToEvent(container.GetChild("ResetToDefault", true), "Released", "HandleResetToDefault");
  103. SubscribeToEvent(container.GetChild("NewVarDropDown", true), "ItemSelected", "CreateUIElementVariable");
  104. SubscribeToEvent(container.GetChild("DeleteVarButton", true), "Released", "DeleteUIElementVariable");
  105. SubscribeToEvent(styleList, "ItemSelected", "HandleStyleItemSelected");
  106. LineEdit@ tagEdit = parentContainer.GetChild("TagsEdit", true);
  107. SubscribeToEvent(tagEdit, "TextChanged", "HandleTagsEdit");
  108. UIElement@ tagSelect = parentContainer.GetChild("TagsSelect", true);
  109. SubscribeToEvent(tagSelect, "Released", "HandleTagsSelect");
  110. return container;
  111. }
  112. void CreateAttributeInspectorWindow()
  113. {
  114. if (attributeInspectorWindow !is null)
  115. return;
  116. InitResourcePicker();
  117. InitVectorStructs();
  118. InitXMLResources();
  119. attributeInspectorWindow = LoadEditorUI("UI/EditorInspectorWindow.xml");
  120. parentContainer = attributeInspectorWindow.GetChild("ParentContainer");
  121. ui.root.AddChild(attributeInspectorWindow);
  122. int height = Min(ui.root.height - 60, 500);
  123. attributeInspectorWindow.SetSize(344, height);
  124. attributeInspectorWindow.SetPosition(ui.root.width - 10 - attributeInspectorWindow.width, 100);
  125. attributeInspectorWindow.opacity = uiMaxOpacity;
  126. attributeInspectorWindow.BringToFront();
  127. inspectorLockButton = attributeInspectorWindow.GetChild("LockButton", true);
  128. UpdateAttributeInspector();
  129. SubscribeToEvent(inspectorLockButton, "Pressed", "ToggleInspectorLock");
  130. SubscribeToEvent(attributeInspectorWindow.GetChild("CloseButton", true), "Pressed", "HideAttributeInspectorWindow");
  131. SubscribeToEvent(attributeInspectorWindow, "LayoutUpdated", "HandleWindowLayoutUpdated");
  132. }
  133. void DisableInspectorLock()
  134. {
  135. inspectorLocked = false;
  136. if (inspectorLockButton !is null)
  137. inspectorLockButton.style = "Button";
  138. UpdateAttributeInspector(true);
  139. }
  140. void EnableInspectorLock()
  141. {
  142. inspectorLocked = true;
  143. if (inspectorLockButton !is null)
  144. inspectorLockButton.style = "ToggledButton";
  145. }
  146. void ToggleInspectorLock()
  147. {
  148. if (inspectorLocked)
  149. DisableInspectorLock();
  150. else
  151. EnableInspectorLock();
  152. }
  153. bool ToggleAttributeInspectorWindow()
  154. {
  155. if (attributeInspectorWindow.visible == false)
  156. ShowAttributeInspectorWindow();
  157. else
  158. HideAttributeInspectorWindow();
  159. return true;
  160. }
  161. void ShowAttributeInspectorWindow()
  162. {
  163. attributeInspectorWindow.visible = true;
  164. attributeInspectorWindow.BringToFront();
  165. }
  166. void HideAttributeInspectorWindow()
  167. {
  168. attributeInspectorWindow.visible = false;
  169. }
  170. /// Handle main window layout updated event by positioning elements that needs manually-positioning (elements that are children of UI-element container with "Free" layout-mode).
  171. void HandleWindowLayoutUpdated()
  172. {
  173. // When window resize and so the list's width is changed, adjust the 'Is enabled' container width and icon panel width so that their children stay at the right most position
  174. for (uint i = 0; i < parentContainer.numChildren; ++i)
  175. {
  176. UIElement@ container = GetContainer(i);
  177. ListView@ list = container.GetChild("AttributeList");
  178. if (list is null)
  179. continue;
  180. int width = list.width;
  181. // Adjust the icon panel's width
  182. UIElement@ panel = container.GetChild("IconsPanel", true);
  183. if (panel !is null)
  184. panel.width = width;
  185. // At the moment, only 'Is Enabled' container (place-holder + check box) is being created as child of the list view instead of as list item
  186. for (uint j = 0; j < list.numChildren; ++j)
  187. {
  188. UIElement@ element = list.children[j];
  189. if (!element.internal)
  190. {
  191. element.SetFixedWidth(width);
  192. UIElement@ title = container.GetChild("TitleText");
  193. element.position = IntVector2(0, (title.screenPosition - list.screenPosition).y);
  194. // Adjust icon panel's width one more time to cater for the space occupied by 'Is Enabled' check box
  195. if (panel !is null)
  196. panel.width = width - element.children[1].width - panel.layoutSpacing;
  197. break;
  198. }
  199. }
  200. }
  201. }
  202. Array<Serializable@> ToSerializableArray(Array<Node@> nodes)
  203. {
  204. Array<Serializable@> serializables;
  205. for (uint i = 0; i < nodes.length; ++i)
  206. serializables.Push(nodes[i]);
  207. return serializables;
  208. }
  209. /// Update the whole attribute inspector window, when fullUpdate flag is set to true then first delete all the containers and repopulate them again from scratch.
  210. /// The fullUpdate flag is usually set to true when the structure of the attributes are different than the existing attributes in the list.
  211. void UpdateAttributeInspector(bool fullUpdate = true)
  212. {
  213. if (inspectorLocked)
  214. return;
  215. attributesDirty = false;
  216. if (fullUpdate)
  217. attributesFullDirty = false;
  218. // If full update delete all containers and add them back as necessary
  219. if (fullUpdate)
  220. DeleteAllContainers();
  221. // Update all ScriptInstances/LuaScriptInstances
  222. UpdateScriptInstances();
  223. if (!editNodes.empty)
  224. {
  225. UIElement@ container = GetNodeContainer();
  226. Text@ nodeTitle = container.GetChild("TitleText");
  227. String nodeType;
  228. if (editNode !is null)
  229. {
  230. String idStr;
  231. if (editNode.id >= FIRST_LOCAL_ID)
  232. idStr = " (Local ID " + String(editNode.id) + ")";
  233. else
  234. idStr = " (ID " + String(editNode.id) + ")";
  235. nodeType = editNode.typeName;
  236. nodeTitle.text = nodeType + idStr;
  237. LineEdit@ tagEdit = parentContainer.GetChild("TagsEdit", true);
  238. tagEdit.text = Join(editNode.tags, ";");
  239. }
  240. else
  241. {
  242. nodeType = editNodes[0].typeName;
  243. nodeTitle.text = nodeType + " (ID " + STRIKED_OUT + " : " + editNodes.length + "x)";
  244. }
  245. IconizeUIElement(nodeTitle, nodeType);
  246. ListView@ list = container.GetChild("AttributeList");
  247. Array<Serializable@> nodes = ToSerializableArray(editNodes);
  248. UpdateAttributes(nodes, list, fullUpdate);
  249. if (fullUpdate)
  250. {
  251. //\todo Avoid hardcoding
  252. // Resize the node editor according to the number of variables, up to a certain maximum
  253. uint maxAttrs = Clamp(list.contentElement.numChildren, MIN_NODE_ATTRIBUTES, MAX_NODE_ATTRIBUTES);
  254. list.SetFixedHeight(maxAttrs * ATTR_HEIGHT + 2);
  255. container.SetFixedHeight(maxAttrs * ATTR_HEIGHT + 58);
  256. }
  257. // Set icon's target in the icon panel
  258. SetAttributeEditorID(container.GetChild("ResetToDefault", true), nodes);
  259. }
  260. if (!editComponents.empty)
  261. {
  262. uint numEditableComponents = editComponents.length / numEditableComponentsPerNode;
  263. String multiplierText;
  264. if (numEditableComponents > 1)
  265. multiplierText = " (" + numEditableComponents + "x)";
  266. for (uint j = 0; j < numEditableComponentsPerNode; ++j)
  267. {
  268. UIElement@ container = GetComponentContainer(j);
  269. Text@ componentTitle = container.GetChild("TitleText");
  270. componentTitle.text = GetComponentTitle(editComponents[j * numEditableComponents]) + multiplierText;
  271. IconizeUIElement(componentTitle, editComponents[j * numEditableComponents].typeName);
  272. SetIconEnabledColor(componentTitle, editComponents[j * numEditableComponents].enabledEffective);
  273. Array<Serializable@> components;
  274. for (uint i = 0; i < numEditableComponents; ++i)
  275. {
  276. Component@ component = editComponents[j * numEditableComponents + i];
  277. components.Push(component);
  278. }
  279. UpdateAttributes(components, container.GetChild("AttributeList"), fullUpdate);
  280. SetAttributeEditorID(container.GetChild("ResetToDefault", true), components);
  281. }
  282. }
  283. if (!editUIElements.empty)
  284. {
  285. UIElement@ container = GetUIElementContainer();
  286. Text@ titleText = container.GetChild("TitleText");
  287. DropDownList@ styleList = container.GetChild("StyleDropDown", true);
  288. String elementType;
  289. if (editUIElement !is null)
  290. {
  291. elementType = editUIElement.typeName;
  292. titleText.text = elementType + " [ID " + GetUIElementID(editUIElement).ToString() + "]";
  293. SetStyleListSelection(styleList, editUIElement.style);
  294. LineEdit@ tagEdit = parentContainer.GetChild("TagsEdit", true);
  295. tagEdit.text = Join(editUIElement.tags, ";");
  296. }
  297. else
  298. {
  299. elementType = editUIElements[0].typeName;
  300. String appliedStyle = cast<UIElement>(editUIElements[0]).style;
  301. bool sameType = true;
  302. bool sameStyle = true;
  303. for (uint i = 1; i < editUIElements.length; ++i)
  304. {
  305. if (editUIElements[i].typeName != elementType)
  306. {
  307. sameType = false;
  308. sameStyle = false;
  309. break;
  310. }
  311. if (sameStyle && cast<UIElement>(editUIElements[i]).style != appliedStyle)
  312. sameStyle = false;
  313. }
  314. titleText.text = (sameType ? elementType : "Mixed type") + " [ID " + STRIKED_OUT + " : " + editUIElements.length + "x]";
  315. SetStyleListSelection(SetEditable(styleList, sameStyle), sameStyle ? appliedStyle : STRIKED_OUT);
  316. if (!sameType)
  317. elementType.Clear(); // No icon
  318. }
  319. IconizeUIElement(titleText, elementType);
  320. UpdateAttributes(editUIElements, container.GetChild("AttributeList"), fullUpdate);
  321. SetAttributeEditorID(container.GetChild("ResetToDefault", true), editUIElements);
  322. }
  323. if (parentContainer.numChildren > 0)
  324. UpdateAttributeInspectorIcons();
  325. else
  326. {
  327. // No editables, insert a dummy component container to show the information
  328. Text@ titleText = GetComponentContainer(0).GetChild("TitleText");
  329. titleText.text = "Select editable objects";
  330. titleText.autoLocalizable = true;
  331. UIElement@ panel = titleText.GetChild("IconsPanel");
  332. panel.visible = false;
  333. }
  334. // Adjust size and position of manual-layout UI-elements, e.g. icons panel
  335. if (fullUpdate)
  336. HandleWindowLayoutUpdated();
  337. }
  338. void UpdateScriptInstances()
  339. {
  340. Array<Component@>@ components = scene.GetComponents("ScriptInstance", true);
  341. for (uint i = 0; i < components.length; i++)
  342. UpdateScriptAttributes(components[i]);
  343. components = scene.GetComponents("LuaScriptInstance", true);
  344. for (uint i = 0; i < components.length; i++)
  345. UpdateScriptAttributes(components[i]);
  346. }
  347. String GetComponentAttributeHash(Component@ component, uint index)
  348. {
  349. // We won't consider the main attributes, as they won't reset when an error occurs.
  350. if (component.typeName == "ScriptInstance")
  351. {
  352. if (index <= SCRIPTINSTANCE_ATTRIBUTE_IGNORE)
  353. return "";
  354. }
  355. else
  356. {
  357. if (index <= LUASCRIPTINSTANCE_ATTRIBUTE_IGNORE)
  358. return "";
  359. }
  360. AttributeInfo attributeInfo = component.attributeInfos[index];
  361. Variant attribute = component.attributes[index];
  362. return String(component.id) + "-" + attributeInfo.name + "-" + attribute.typeName;
  363. }
  364. void UpdateScriptAttributes(Component@ component)
  365. {
  366. for (uint i = Min(SCRIPTINSTANCE_ATTRIBUTE_IGNORE, LUASCRIPTINSTANCE_ATTRIBUTE_IGNORE) + 1; i < component.numAttributes; i++)
  367. {
  368. Variant attribute = component.attributes[i];
  369. // Component/node ID's are always unique within a scene, based on a simple increment.
  370. // This makes for a simple method of mapping a components attributes unique and consistent.
  371. // We will also use the type name in the hash to be able to recall and differentiate type changes.
  372. String hash = GetComponentAttributeHash(component, i);
  373. if (hash.empty)
  374. continue;
  375. if (!scriptAttributes.Contains(hash))
  376. {
  377. // set the initial value to the default value.
  378. scriptAttributes[hash] = attribute;
  379. }
  380. else
  381. {
  382. // recall the previously stored value
  383. component.attributes[i] = scriptAttributes[hash];
  384. }
  385. }
  386. component.ApplyAttributes();
  387. }
  388. /// Update the attribute list of the node container.
  389. void UpdateNodeAttributes()
  390. {
  391. bool fullUpdate = false;
  392. UpdateAttributes(ToSerializableArray(editNodes), GetNodeContainer().GetChild("AttributeList"), fullUpdate);
  393. }
  394. /// Update the icons enabled color based on the internal state of the objects.
  395. /// For node and component, based on "enabled" property.
  396. /// For ui-element, based on "visible" property.
  397. void UpdateAttributeInspectorIcons()
  398. {
  399. if (!editNodes.empty)
  400. {
  401. Text@ nodeTitle = GetNodeContainer().GetChild("TitleText");
  402. if (editNode !is null)
  403. SetIconEnabledColor(nodeTitle, editNode.enabled);
  404. else if (editNodes.length > 0)
  405. {
  406. bool hasSameEnabledState = true;
  407. for (uint i = 1; i < editNodes.length; ++i)
  408. {
  409. if (editNodes[i].enabled != editNodes[0].enabled)
  410. {
  411. hasSameEnabledState = false;
  412. break;
  413. }
  414. }
  415. SetIconEnabledColor(nodeTitle, editNodes[0].enabled, !hasSameEnabledState);
  416. }
  417. }
  418. if (!editComponents.empty)
  419. {
  420. uint numEditableComponents = editComponents.length / numEditableComponentsPerNode;
  421. for (uint j = 0; j < numEditableComponentsPerNode; ++j)
  422. {
  423. Text@ componentTitle = GetComponentContainer(j).GetChild("TitleText");
  424. bool enabledEffective = editComponents[j * numEditableComponents].enabledEffective;
  425. bool hasSameEnabledState = true;
  426. for (uint i = 1; i < numEditableComponents; ++i)
  427. {
  428. if (editComponents[j * numEditableComponents + i].enabledEffective != enabledEffective)
  429. {
  430. hasSameEnabledState = false;
  431. break;
  432. }
  433. }
  434. SetIconEnabledColor(componentTitle, enabledEffective, !hasSameEnabledState);
  435. }
  436. }
  437. if (!editUIElements.empty)
  438. {
  439. Text@ elementTitle = GetUIElementContainer().GetChild("TitleText");
  440. if (editUIElement !is null)
  441. SetIconEnabledColor(elementTitle, editUIElement.visible);
  442. else if (editUIElements.length > 0)
  443. {
  444. bool hasSameVisibleState = true;
  445. bool visible = cast<UIElement>(editUIElements[0]).visible;
  446. for (uint i = 1; i < editUIElements.length; ++i)
  447. {
  448. if (cast<UIElement>(editUIElements[i]).visible != visible)
  449. {
  450. hasSameVisibleState = false;
  451. break;
  452. }
  453. }
  454. SetIconEnabledColor(elementTitle, visible, !hasSameVisibleState);
  455. }
  456. }
  457. }
  458. /// Return true if the edit attribute action should continue.
  459. bool PreEditAttribute(Array<Serializable@>@ serializables, uint index)
  460. {
  461. return true;
  462. }
  463. /// Call after the attribute values in the target serializables have been edited.
  464. void PostEditAttribute(Array<Serializable@>@ serializables, uint index, const Array<Variant>& oldValues)
  465. {
  466. // Create undo actions for the edits
  467. EditActionGroup group;
  468. for (uint i = 0; i < serializables.length; ++i)
  469. {
  470. EditAttributeAction action;
  471. action.Define(serializables[i], index, oldValues[i]);
  472. group.actions.Push(action);
  473. }
  474. SaveEditActionGroup(group);
  475. // If a UI-element changing its 'Is Modal' attribute, clear the hierarchy list selection
  476. int itemType = GetType(serializables[0]);
  477. if (itemType == ITEM_UI_ELEMENT && serializables[0].attributeInfos[index].name == "Is Modal")
  478. hierarchyList.ClearSelection();
  479. for (uint i = 0; i < serializables.length; ++i)
  480. {
  481. PostEditAttribute(serializables[i], index);
  482. if (itemType == ITEM_UI_ELEMENT)
  483. SetUIElementModified(serializables[i]);
  484. }
  485. if (itemType != ITEM_UI_ELEMENT)
  486. SetSceneModified();
  487. }
  488. /// Call after the attribute values in the target serializables have been edited.
  489. void PostEditAttribute(Serializable@ serializable, uint index)
  490. {
  491. // If a StaticModel/AnimatedModel/Skybox model was changed, apply a possibly different material list
  492. if (applyMaterialList && serializable.attributeInfos[index].name == "Model")
  493. {
  494. StaticModel@ staticModel = cast<StaticModel>(serializable);
  495. if (staticModel !is null)
  496. staticModel.ApplyMaterialList();
  497. }
  498. // If a CollisionShape changed the shape type to trimesh or convex, and a collision model is not set,
  499. // try to get it from a StaticModel in the same node
  500. if (serializable.typeName == "CollisionShape" && serializable.attributeInfos[index].name == "Shape Type")
  501. {
  502. int shapeType = serializable.GetAttribute("Shape Type").GetInt();
  503. if ((shapeType == 6 || shapeType == 7) && serializable.GetAttribute("CustomGeometry ComponentID").GetInt() == 0 &&
  504. serializable.GetAttribute("Model").GetResourceRef().name.Trimmed().length == 0)
  505. {
  506. Node@ ownerNode = cast<Component>(serializable).node;
  507. if (ownerNode !is null)
  508. {
  509. StaticModel@ staticModel = ownerNode.GetComponent("StaticModel");
  510. if (staticModel !is null)
  511. {
  512. serializable.SetAttribute("Model", staticModel.GetAttribute("Model"));
  513. serializable.ApplyAttributes();
  514. }
  515. }
  516. }
  517. }
  518. }
  519. /// Store the IDs of the actual serializable objects into user-defined variable of the 'attribute editor' (e.g. line-edit, drop-down-list, etc).
  520. void SetAttributeEditorID(UIElement@ attrEdit, Array<Serializable@>@ serializables)
  521. {
  522. if (serializables is null || serializables.length == 0)
  523. return;
  524. // All target serializables must be either nodes, ui-elements, or components
  525. Array<Variant> ids;
  526. switch (GetType(serializables[0]))
  527. {
  528. case ITEM_NODE:
  529. for (uint i = 0; i < serializables.length; ++i)
  530. ids.Push(cast<Node>(serializables[i]).id);
  531. attrEdit.vars[NODE_IDS_VAR] = ids;
  532. break;
  533. case ITEM_COMPONENT:
  534. for (uint i = 0; i < serializables.length; ++i)
  535. ids.Push(cast<Component>(serializables[i]).id);
  536. attrEdit.vars[COMPONENT_IDS_VAR] = ids;
  537. break;
  538. case ITEM_UI_ELEMENT:
  539. for (uint i = 0; i < serializables.length; ++i)
  540. ids.Push(GetUIElementID(cast<UIElement>(serializables[i])));
  541. attrEdit.vars[UI_ELEMENT_IDS_VAR] = ids;
  542. break;
  543. default:
  544. break;
  545. }
  546. }
  547. /// Return the actual serializable objects based on the IDs stored in the user-defined variable of the 'attribute editor'.
  548. Array<Serializable@>@ GetAttributeEditorTargets(UIElement@ attrEdit)
  549. {
  550. Array<Serializable@> ret;
  551. Variant variant = attrEdit.GetVar(NODE_IDS_VAR);
  552. if (!variant.empty)
  553. {
  554. Array<Variant>@ ids = variant.GetVariantVector();
  555. for (uint i = 0; i < ids.length; ++i)
  556. {
  557. Node@ node = editorScene.GetNode(ids[i].GetUInt());
  558. if (node !is null)
  559. ret.Push(node);
  560. }
  561. }
  562. else
  563. {
  564. variant = attrEdit.GetVar(COMPONENT_IDS_VAR);
  565. if (!variant.empty)
  566. {
  567. Array<Variant>@ ids = variant.GetVariantVector();
  568. for (uint i = 0; i < ids.length; ++i)
  569. {
  570. Component@ component = editorScene.GetComponent(ids[i].GetUInt());
  571. if (component !is null)
  572. ret.Push(component);
  573. }
  574. }
  575. else
  576. {
  577. variant = attrEdit.GetVar(UI_ELEMENT_IDS_VAR);
  578. if (!variant.empty)
  579. {
  580. Array<Variant>@ ids = variant.GetVariantVector();
  581. for (uint i = 0; i < ids.length; ++i)
  582. {
  583. UIElement@ element = editorUIElement.GetChild(UI_ELEMENT_ID_VAR, ids[i], true);
  584. if (element !is null)
  585. ret.Push(element);
  586. }
  587. }
  588. }
  589. }
  590. return ret;
  591. }
  592. void HandleTagsEdit(StringHash eventType, VariantMap& eventData)
  593. {
  594. LineEdit@ lineEdit = eventData["Element"].GetPtr();
  595. Array<String> tags = lineEdit.text.Split(';');
  596. if (editUIElement !is null)
  597. {
  598. editUIElement.RemoveAllTags();
  599. for (uint i = 0; i < tags.length; i++)
  600. editUIElement.AddTag(tags[i].Trimmed());
  601. }
  602. else if (editNode !is null)
  603. {
  604. editNode.RemoveAllTags();
  605. for (uint i = 0; i < tags.length; i++)
  606. editNode.AddTag(tags[i].Trimmed());
  607. }
  608. }
  609. void HandleTagsSelect(StringHash eventType, VariantMap& eventData)
  610. {
  611. UIElement@ tagSelect = eventData["Element"].GetPtr();
  612. Array<UIElement@> actions;
  613. String Indicator = "* ";
  614. // In first priority changes to UIElement
  615. if (editUIElement !is null)
  616. {
  617. // 1. Add established tags from current editable UIElement to menu
  618. Array<String> elementTags = editUIElement.tags;
  619. for (int i =0; i < elementTags.length; i++)
  620. {
  621. bool isHasTag = editUIElement.HasTag(elementTags[i]);
  622. String taggedIndicator = (isHasTag ? Indicator : "");
  623. actions.Push(CreateContextMenuItem(taggedIndicator + elementTags[i], "HandleTagsMenuSelection", elementTags[i]));
  624. }
  625. // 2. Add default tags
  626. Array<String> stdTags = defaultTags.Split(';');
  627. for (int i=0; i < stdTags.length; i++)
  628. {
  629. bool isHasTag = editUIElement.HasTag(stdTags[i]);
  630. // Add this tag into menu if only Node not tadded with it yet, otherwise it showed on step 1.
  631. if (!isHasTag)
  632. {
  633. String taggedIndicator = (isHasTag ? Indicator : "");
  634. actions.Push(CreateContextMenuItem(taggedIndicator + stdTags[i], "HandleTagsMenuSelection", stdTags[i]));
  635. }
  636. }
  637. }
  638. else if (editNode !is null)
  639. {
  640. // 1. Add established tags from Node to menu
  641. Array<String> nodeTags = editNode.tags;
  642. for (int i =0; i < nodeTags.length; i++)
  643. {
  644. bool isHasTag = editNode.HasTag(nodeTags[i]);
  645. String taggedIndicator = (isHasTag ? Indicator : "");
  646. actions.Push(CreateContextMenuItem(taggedIndicator + nodeTags[i], "HandleTagsMenuSelection", nodeTags[i]));
  647. }
  648. Array<String> sceneTags = editorScene.tags;
  649. // 2. Add tags from Scene.tags (In this scenario Scene.tags used as storage for frequently used tags in current Scene only)
  650. for (int i =0; i < sceneTags.length; i++)
  651. {
  652. bool isHasTag = editNode.HasTag(sceneTags[i]);
  653. // Add this tag into menu if only Node not tadded with it yet, otherwise it showed on step 1.
  654. if (!isHasTag)
  655. {
  656. String taggedIndicator = (isHasTag ? Indicator : "");
  657. actions.Push(CreateContextMenuItem(taggedIndicator + sceneTags[i], "HandleTagsMenuSelection", sceneTags[i]));
  658. }
  659. }
  660. // 3. Add default tags
  661. Array<String> stdTags = defaultTags.Split(';');
  662. for (int i=0; i<stdTags.length; i++)
  663. {
  664. bool isHasTag = editNode.HasTag(stdTags[i]);
  665. // Add this tag into menu if only Node not tadded with it yet, otherwise it showed on step 1.
  666. if (!isHasTag)
  667. {
  668. String taggedIndicator = (isHasTag ? Indicator : "");
  669. actions.Push(CreateContextMenuItem(taggedIndicator + stdTags[i], "HandleTagsMenuSelection", stdTags[i]));
  670. }
  671. }
  672. }
  673. // if any action has been added, add also Reset and Cancel and show menu
  674. if (actions.length > 0)
  675. {
  676. actions.Push(CreateContextMenuItem("Reset", "HandleTagsMenuSelection", "Reset"));
  677. actions.Push(CreateContextMenuItem("Cancel", "HandleTagsMenuSelectionDivisor"));
  678. ActivateContextMenu(actions);
  679. }
  680. }
  681. void HandleTagsMenuSelectionDivisor()
  682. {
  683. //do nothing
  684. }
  685. void HandleTagsMenuSelection()
  686. {
  687. Menu@ menu = GetEventSender();
  688. if (menu is null)
  689. return;
  690. String menuSelectedTag = menu.name;
  691. // In first priority changes to UIElement
  692. if (editUIElement !is null)
  693. {
  694. if (menuSelectedTag == "Reset")
  695. {
  696. editUIElement.RemoveAllTags();
  697. UpdateAttributeInspector();
  698. return;
  699. }
  700. if (!editUIElement.HasTag(menuSelectedTag))
  701. {
  702. editUIElement.AddTag(menuSelectedTag.Trimmed());
  703. }
  704. else
  705. {
  706. editUIElement.RemoveTag(menuSelectedTag.Trimmed());
  707. }
  708. }
  709. else if (editNode !is null)
  710. {
  711. if (menuSelectedTag == "Reset")
  712. {
  713. editNode.RemoveAllTags();
  714. UpdateAttributeInspector();
  715. return;
  716. }
  717. if (!editNode.HasTag(menuSelectedTag))
  718. {
  719. editNode.AddTag(menuSelectedTag.Trimmed());
  720. }
  721. else
  722. {
  723. editNode.RemoveTag(menuSelectedTag.Trimmed());
  724. }
  725. }
  726. UpdateAttributeInspector();
  727. }
  728. /// Handle reset to default event, sent when reset icon in the icon-panel is clicked.
  729. void HandleResetToDefault(StringHash eventType, VariantMap& eventData)
  730. {
  731. ui.cursor.shape = CS_BUSY;
  732. UIElement@ button = eventData["Element"].GetPtr();
  733. Array<Serializable@>@ serializables = GetAttributeEditorTargets(button);
  734. if (serializables.empty)
  735. return;
  736. // Group for storing undo actions
  737. EditActionGroup group;
  738. // Reset target serializables to their default values
  739. for (uint i = 0; i < serializables.length; ++i)
  740. {
  741. Serializable@ target = serializables[i];
  742. ResetAttributesAction action;
  743. action.Define(target);
  744. group.actions.Push(action);
  745. target.ResetToDefault();
  746. if (action.targetType == ITEM_UI_ELEMENT)
  747. {
  748. action.SetInternalVars(target);
  749. SetUIElementModified(target);
  750. }
  751. target.ApplyAttributes();
  752. for (uint j = 0; j < target.numAttributes; ++j)
  753. PostEditAttribute(target, j);
  754. }
  755. SaveEditActionGroup(group);
  756. if (GetType(serializables[0]) != ITEM_UI_ELEMENT)
  757. SetSceneModified();
  758. attributesFullDirty = true;
  759. }
  760. /// Handle create new user-defined variable event for node target.
  761. void CreateNodeVariable(StringHash eventType, VariantMap& eventData)
  762. {
  763. if (editNodes.empty)
  764. return;
  765. String newName = ExtractVariableName(eventData);
  766. if (newName.empty)
  767. return;
  768. // Create scene variable
  769. editorScene.RegisterVar(newName);
  770. globalVarNames[newName] = newName;
  771. Variant newValue = ExtractVariantType(eventData);
  772. // If we overwrite an existing variable, must recreate the attribute-editor(s) for the correct type
  773. bool overwrite = false;
  774. for (uint i = 0; i < editNodes.length; ++i)
  775. {
  776. overwrite = overwrite || editNodes[i].vars.Contains(newName);
  777. editNodes[i].vars[newName] = newValue;
  778. }
  779. if (overwrite)
  780. attributesFullDirty = true;
  781. else
  782. attributesDirty = true;
  783. }
  784. /// Handle delete existing user-defined variable event for node target.
  785. void DeleteNodeVariable(StringHash eventType, VariantMap& eventData)
  786. {
  787. if (editNodes.empty)
  788. return;
  789. String delName = ExtractVariableName(eventData);
  790. if (delName.empty)
  791. return;
  792. bool erased = false;
  793. for (uint i = 0; i < editNodes.length; ++i)
  794. {
  795. // \todo Should first check whether var in question is editable
  796. erased = editNodes[i].vars.Erase(delName) || erased;
  797. }
  798. if (erased)
  799. {
  800. attributesDirty = true;
  801. // If the attribute is not defined in any other node, unregister from the scene
  802. // to prevent it from being unnecessarily saved; the global var list will still hold it
  803. // to keep the hash-name mapping known in case it's in use in other scenes
  804. Array<Node@>@ allChildren = editorScene.GetChildren(true);
  805. StringHash delNameHash(delName);
  806. bool inUse = false;
  807. for (uint i = 0; i < allChildren.length; ++i)
  808. {
  809. if (allChildren[i].vars.Contains(delNameHash))
  810. {
  811. inUse = true;
  812. break;
  813. }
  814. }
  815. if (!inUse)
  816. editorScene.UnregisterVar(delName);
  817. }
  818. }
  819. /// Handle create new user-defined variable event for ui-element target.
  820. void CreateUIElementVariable(StringHash eventType, VariantMap& eventData)
  821. {
  822. if (editUIElements.empty)
  823. return;
  824. String newName = ExtractVariableName(eventData);
  825. if (newName.empty)
  826. return;
  827. // Create UIElement variable
  828. globalVarNames[newName] = newName;
  829. Variant newValue = ExtractVariantType(eventData);
  830. // If we overwrite an existing variable, must recreate the attribute-editor(s) for the correct type
  831. bool overwrite = false;
  832. for (uint i = 0; i < editUIElements.length; ++i)
  833. {
  834. UIElement@ element = cast<UIElement>(editUIElements[i]);
  835. overwrite = overwrite || element.vars.Contains(newName);
  836. element.vars[newName] = newValue;
  837. }
  838. if (overwrite)
  839. attributesFullDirty = true;
  840. else
  841. attributesDirty = true;
  842. }
  843. /// Handle delete existing user-defined variable event for ui-element target.
  844. void DeleteUIElementVariable(StringHash eventType, VariantMap& eventData)
  845. {
  846. if (editUIElements.empty)
  847. return;
  848. String delName = ExtractVariableName(eventData);
  849. if (delName.empty)
  850. return;
  851. // Note: intentionally do not unregister the variable name here as the same variable name may still be used by other attribute list
  852. bool erased = false;
  853. for (uint i = 0; i < editUIElements.length; ++i)
  854. {
  855. // \todo Should first check whether var in question is editable
  856. erased = cast<UIElement>(editUIElements[i]).vars.Erase(delName) || erased;
  857. }
  858. if (erased)
  859. attributesDirty = true;
  860. }
  861. String ExtractVariableName(VariantMap& eventData)
  862. {
  863. UIElement@ element = eventData["Element"].GetPtr();
  864. LineEdit@ nameEdit = element.parent.GetChild("VarNameEdit");
  865. return nameEdit.text.Trimmed();
  866. }
  867. Variant ExtractVariantType(VariantMap& eventData)
  868. {
  869. DropDownList@ dropDown = eventData["Element"].GetPtr();
  870. switch (dropDown.selection)
  871. {
  872. case 0:
  873. return int(0);
  874. case 1:
  875. return false;
  876. case 2:
  877. return float(0.0);
  878. case 3:
  879. return Variant(String());
  880. case 4:
  881. return Variant(Vector3());
  882. case 5:
  883. return Variant(Color());
  884. }
  885. return Variant(); // This should not happen
  886. }
  887. /// Get back the human-readable variable name from the StringHash.
  888. String GetVarName(StringHash hash)
  889. {
  890. // First try to get it from scene
  891. String name = editorScene.GetVarName(hash);
  892. // Then from the global variable reverse mappings
  893. if (name.empty && globalVarNames.Contains(hash))
  894. name = globalVarNames[hash].ToString();
  895. return name;
  896. }
  897. bool inSetStyleListSelection = false;
  898. /// Select/highlight the matching style in the style drop-down-list based on specified style.
  899. void SetStyleListSelection(DropDownList@ styleList, const String&in style)
  900. {
  901. // Prevent infinite loop upon initial style selection
  902. inSetStyleListSelection = true;
  903. uint selection = M_MAX_UNSIGNED;
  904. String styleName = style.empty ? "auto" : style;
  905. Array<UIElement@> items = styleList.GetItems();
  906. for (uint i = 0; i < items.length; ++i)
  907. {
  908. Text@ element = cast<Text>(items[i]);
  909. if (element is null)
  910. continue; // It may be a divider
  911. if (element.text == styleName)
  912. {
  913. selection = i;
  914. break;
  915. }
  916. }
  917. styleList.selection = selection;
  918. inSetStyleListSelection = false;
  919. }
  920. /// Handle the style change of the target ui-elements event when a new style is picked from the style drop-down-list.
  921. void HandleStyleItemSelected(StringHash eventType, VariantMap& eventData)
  922. {
  923. if (inSetStyleListSelection || editUIElements.empty)
  924. return;
  925. ui.cursor.shape = CS_BUSY;
  926. DropDownList@ styleList = eventData["Element"].GetPtr();
  927. Text@ text = cast<Text>(styleList.selectedItem);
  928. if (text is null)
  929. return;
  930. String newStyle = text.text;
  931. if (newStyle == "auto")
  932. newStyle.Clear();
  933. // Group for storing undo actions
  934. EditActionGroup group;
  935. // Apply new style to selected UI-elements
  936. for (uint i = 0; i < editUIElements.length; ++i)
  937. {
  938. UIElement@ element = editUIElements[i];
  939. ApplyUIElementStyleAction action;
  940. action.Define(element, newStyle);
  941. group.actions.Push(action);
  942. // Use the Redo() to actually do the action
  943. action.Redo();
  944. }
  945. SaveEditActionGroup(group);
  946. }