Urho3D/bin/Data/Scripts/Editor/EditorUIElement.as

724 lines
22 KiB
ActionScript

// Urho3D editor UI-element handling
UIElement@ editorUIElement;
XMLFile@ uiElementDefaultStyle;
Array<String> availableStyles;
UIElement@ editUIElement;
Array<Serializable@> selectedUIElements;
Array<Serializable@> editUIElements;
Array<XMLFile@> uiElementCopyBuffer;
bool suppressUIElementChanges = false;
const StringHash FILENAME_VAR("FileName");
const StringHash MODIFIED_VAR("Modified");
const StringHash CHILD_ELEMENT_FILENAME_VAR("ChildElemFileName");
void ClearUIElementSelection()
{
editUIElement = null;
selectedUIElements.Clear();
editUIElements.Clear();
}
void CreateRootUIElement()
{
// Create a root UIElement only once here, do not confuse this with ui.root itself
editorUIElement = ui.root.CreateChild("UIElement");
editorUIElement.name = "UI";
editorUIElement.SetSize(graphics.width, graphics.height);
editorUIElement.traversalMode = TM_DEPTH_FIRST; // This is needed for root-like element to prevent artifacts
editorUIElement.priority = -1000; // All user-created UI elements have lowest priority so they do not cover editor's windows
// This is needed to distinguish our own element events from Editor's UI element events
editorUIElement.elementEventSender = true;
SubscribeToEvent(editorUIElement, "ElementAdded", "HandleUIElementAdded");
SubscribeToEvent(editorUIElement, "ElementRemoved", "HandleUIElementRemoved");
// Since this root UIElement is not being handled by above handlers, update it into hierarchy list manually as another list root item
UpdateHierarchyItem(M_MAX_UNSIGNED, editorUIElement, null);
}
bool NewUIElement(const String&in typeName)
{
// If no edit element then parented to root
UIElement@ parent = editUIElement !is null ? editUIElement : editorUIElement;
UIElement@ element = parent.CreateChild(typeName);
if (element !is null)
{
// Use the predefined UI style if set, otherwise use editor's own UI style
XMLFile@ defaultStyle = uiElementDefaultStyle !is null ? uiElementDefaultStyle : uiStyle;
if (editUIElement is null)
{
// If parented to root, set the internal variables
element.vars[FILENAME_VAR] = "";
element.vars[MODIFIED_VAR] = false;
// set a default UI style
element.defaultStyle = defaultStyle;
// and position the newly created element at center
CenterDialog(element);
}
// Apply the auto style
element.style = AUTO_STYLE;
// Do not allow UI subsystem to reorder children while editing the element in the editor
element.sortChildren = false;
// Create an undo action for the create
CreateUIElementAction action;
action.Define(element);
SaveEditAction(action);
SetUIElementModified(element);
FocusUIElement(element);
}
return true;
}
void ResetSortChildren(UIElement@ element)
{
element.sortChildren = false;
// Perform the action recursively for child elements
for (uint i = 0; i < element.numChildren; ++i)
ResetSortChildren(element.children[i]);
}
void OpenUILayout(const String&in fileName)
{
if (fileName.empty)
return;
ui.cursor.shape = CS_BUSY;
// Check if the UI element has been opened before
if (editorUIElement.GetChild(FILENAME_VAR, Variant(fileName)) !is null)
{
MessageBox("UI element is already opened.\n" + fileName);
return;
}
// Always load from the filesystem, not from resource paths
if (!fileSystem.FileExists(fileName))
{
MessageBox("No such file.\n" + fileName);
return;
}
File file(fileName, FILE_READ);
if (!file.open)
{
MessageBox("Could not open file.\n" + fileName);
return;
}
// Add the UI layout's resource path in case it's necessary
SetResourcePath(GetPath(fileName), true, true);
XMLFile@ xmlFile = XMLFile();
xmlFile.Load(file);
suppressUIElementChanges = true;
// If uiElementDefaultStyle is not set then automatically fallback to use the editor's own default style
UIElement@ element = ui.LoadLayout(xmlFile, uiElementDefaultStyle);
if (element !is null)
{
element.vars[FILENAME_VAR] = fileName;
element.vars[MODIFIED_VAR] = false;
// Do not allow UI subsystem to reorder children while editing the element in the editor
ResetSortChildren(element);
// Register variable names from the 'enriched' XMLElement, if any
RegisterUIElementVar(xmlFile.root);
editorUIElement.AddChild(element);
UpdateHierarchyItem(element);
FocusUIElement(element);
ClearEditActions();
}
else
MessageBox("Could not load UI layout successfully!\nSee Urho3D.log for more detail.");
suppressUIElementChanges = false;
}
bool CloseUILayout()
{
ui.cursor.shape = CS_BUSY;
if (messageBoxCallback is null)
{
for (uint i = 0; i < selectedUIElements.length; ++i)
{
UIElement@ element = GetTopLevelUIElement(selectedUIElements[i]);
if (element !is null && element.vars[MODIFIED_VAR].GetBool())
{
MessageBox@ messageBox = MessageBox("UI layout has been modified.\nContinue to close?", "Warning");
if (messageBox.window !is null)
{
Button@ cancelButton = messageBox.window.GetChild("CancelButton", true);
cancelButton.visible = true;
cancelButton.focus = true;
SubscribeToEvent(messageBox, "MessageACK", "HandleMessageAcknowledgement");
messageBoxCallback = @CloseUILayout;
return false;
}
}
}
}
else
messageBoxCallback = null;
suppressUIElementChanges = true;
for (uint i = 0; i < selectedUIElements.length; ++i)
{
UIElement@ element = GetTopLevelUIElement(selectedUIElements[i]);
if (element !is null)
{
element.Remove();
UpdateHierarchyItem(GetListIndex(element), null, null);
}
}
hierarchyList.ClearSelection();
ClearEditActions();
suppressUIElementChanges = false;
return true;
}
bool CloseAllUILayouts()
{
ui.cursor.shape = CS_BUSY;
if (messageBoxCallback is null)
{
for (uint i = 0; i < editorUIElement.numChildren; ++i)
{
UIElement@ element = editorUIElement.children[i];
if (element !is null && element.vars[MODIFIED_VAR].GetBool())
{
MessageBox@ messageBox = MessageBox("UI layout has been modified.\nContinue to close?", "Warning");
if (messageBox.window !is null)
{
Button@ cancelButton = messageBox.window.GetChild("CancelButton", true);
cancelButton.visible = true;
cancelButton.focus = true;
SubscribeToEvent(messageBox, "MessageACK", "HandleMessageAcknowledgement");
messageBoxCallback = @CloseAllUILayouts;
return false;
}
}
}
}
else
messageBoxCallback = null;
suppressUIElementChanges = true;
editorUIElement.RemoveAllChildren();
UpdateHierarchyItem(editorUIElement, true);
// Reset element ID number generator
uiElementNextID = UI_ELEMENT_BASE_ID + 1;
hierarchyList.ClearSelection();
ClearEditActions();
suppressUIElementChanges = false;
return true;
}
bool SaveUILayout(const String&in fileName)
{
if (fileName.empty)
return false;
ui.cursor.shape = CS_BUSY;
MakeBackup(fileName);
File file(fileName, FILE_WRITE);
if (!file.open)
{
MessageBox("Could not open file.\n" + fileName);
return false;
}
UIElement@ element = GetTopLevelUIElement(editUIElement);
if (element is null)
return false;
XMLFile@ elementData = XMLFile();
XMLElement rootElem = elementData.CreateRoot("element");
bool success = element.SaveXML(rootElem);
RemoveBackup(success, fileName);
if (success)
{
FilterInternalVars(rootElem);
success = elementData.Save(file);
if (success)
{
element.vars[FILENAME_VAR] = fileName;
SetUIElementModified(element, false);
}
}
if (!success)
MessageBox("Could not save UI layout successfully!\nSee Urho3D.log for more detail.");
return success;
}
bool SaveUILayoutWithExistingName()
{
ui.cursor.shape = CS_BUSY;
UIElement@ element = GetTopLevelUIElement(editUIElement);
if (element is null)
return false;
String fileName = element.GetVar(FILENAME_VAR).GetString();
if (fileName.empty)
return PickFile(); // No name yet, so pick one
else
return SaveUILayout(fileName);
}
void LoadChildUIElement(const String&in fileName)
{
if (fileName.empty)
return;
ui.cursor.shape = CS_BUSY;
if (!fileSystem.FileExists(fileName))
{
MessageBox("No such file.\n" + fileName);
return;
}
File file(fileName, FILE_READ);
if (!file.open)
{
MessageBox("Could not open file.\n" + fileName);
return;
}
XMLFile@ xmlFile = XMLFile();
xmlFile.Load(file);
suppressUIElementChanges = true;
if (editUIElement.LoadChildXML(xmlFile, uiElementDefaultStyle !is null ? uiElementDefaultStyle : uiStyle) !is null)
{
XMLElement rootElem = xmlFile.root;
uint index = rootElem.HasAttribute("index") ? rootElem.GetUInt("index") : editUIElement.numChildren - 1;
UIElement@ element = editUIElement.children[index];
ResetSortChildren(element);
RegisterUIElementVar(xmlFile.root);
element.vars[CHILD_ELEMENT_FILENAME_VAR] = fileName;
if (index == editUIElement.numChildren - 1)
UpdateHierarchyItem(element);
else
// If not last child, find the list index of the next sibling as the insertion index
UpdateHierarchyItem(GetListIndex(editUIElement.children[index + 1]), element, hierarchyList.items[GetListIndex(editUIElement)]);
SetUIElementModified(element);
// Create an undo action for the load
CreateUIElementAction action;
action.Define(element);
SaveEditAction(action);
FocusUIElement(element);
}
suppressUIElementChanges = false;
}
bool SaveChildUIElement(const String&in fileName)
{
if (fileName.empty)
return false;
ui.cursor.shape = CS_BUSY;
MakeBackup(fileName);
File file(fileName, FILE_WRITE);
if (!file.open)
{
MessageBox("Could not open file.\n" + fileName);
return false;
}
XMLFile@ elementData = XMLFile();
XMLElement rootElem = elementData.CreateRoot("element");
bool success = editUIElement.SaveXML(rootElem);
RemoveBackup(success, fileName);
if (success)
{
FilterInternalVars(rootElem);
success = elementData.Save(file);
if (success)
editUIElement.vars[CHILD_ELEMENT_FILENAME_VAR] = fileName;
}
if (!success)
MessageBox("Could not save child UI element successfully!\nSee Urho3D.log for more detail.");
return success;
}
void SetUIElementDefaultStyle(const String&in fileName)
{
if (fileName.empty)
return;
ui.cursor.shape = CS_BUSY;
// Always load from the filesystem, not from resource paths
if (!fileSystem.FileExists(fileName))
{
MessageBox("No such file.\n" + fileName);
return;
}
File file(fileName, FILE_READ);
if (!file.open)
{
MessageBox("Could not open file.\n" + fileName);
return;
}
uiElementDefaultStyle = XMLFile();
uiElementDefaultStyle.Load(file);
// Remove the existing style list to ensure it gets repopulated again with the new default style file
availableStyles.Clear();
// 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
if (!editUIElements.empty)
attributesFullDirty = true;
}
// Prepare XPath query object only once and use it multiple times
XPathQuery filterInternalVarsQuery("//attribute[@name='Variables']/variant");
void FilterInternalVars(XMLElement source)
{
XPathResultSet resultSet = filterInternalVarsQuery.Evaluate(source);
XMLElement resultElem = resultSet.firstResult;
while (resultElem.notNull)
{
String name = GetVarName(resultElem.GetUInt("hash"));
if (name.empty)
{
XMLElement parent = resultElem.parent;
// If variable name is empty (or unregistered) then it is an internal variable and should be removed
if (parent.RemoveChild(resultElem))
{
// If parent does not have any children anymore then remove the parent also
if (!parent.HasChild("variant"))
parent.parent.RemoveChild(parent);
}
}
else
// If it is registered then it is a user-defined variable, so 'enrich' the XMLElement to store the variable name in plaintext
resultElem.SetAttribute("name", name);
resultElem = resultElem.nextResult;
}
}
XPathQuery registerUIElemenVarsQuery("//attribute[@name='Variables']/variant/@name");
void RegisterUIElementVar(XMLElement source)
{
XPathResultSet resultSet = registerUIElemenVarsQuery.Evaluate(source);
XMLElement resultAttr = resultSet.firstResult; // Since we are selecting attribute, the resultset is in attribute context
while (resultAttr.notNull)
{
String name = resultAttr.GetAttribute();
globalVarNames[name] = name;
resultAttr = resultAttr.nextResult;
}
}
UIElement@ GetTopLevelUIElement(UIElement@ element)
{
// Only top level UI-element contains the FILENAME_VAR
while (element !is null && !element.vars.Contains(FILENAME_VAR))
element = element.parent;
return element;
}
void SetUIElementModified(UIElement@ element, bool flag = true)
{
element = GetTopLevelUIElement(element);
if (element !is null && element.GetVar(MODIFIED_VAR).GetBool() != flag)
{
element.vars[MODIFIED_VAR] = flag;
UpdateHierarchyItemText(GetListIndex(element), element.visible, GetUIElementTitle(element));
}
}
XPathQuery availableStylesXPathQuery("/elements/element[@auto='false']/@type");
void GetAvailableStyles()
{
// Use the predefined UI style if set, otherwise use editor's own UI style
XMLFile@ defaultStyle = uiElementDefaultStyle !is null ? uiElementDefaultStyle : uiStyle;
XMLElement rootElem = defaultStyle.root;
XPathResultSet resultSet = availableStylesXPathQuery.Evaluate(rootElem);
XMLElement resultElem = resultSet.firstResult;
while (resultElem.notNull)
{
availableStyles.Push(resultElem.GetAttribute());
resultElem = resultElem.nextResult;
}
availableStyles.Sort();
}
void PopulateStyleList(DropDownList@ styleList)
{
if (availableStyles.empty)
GetAvailableStyles();
for (uint i = 0; i < availableStyles.length; ++i)
{
Text@ choice = Text();
styleList.AddItem(choice);
choice.style = "EditorEnumAttributeText";
choice.text = availableStyles[i];
}
}
bool UIElementCut()
{
return UIElementCopy() && UIElementDelete();
}
bool UIElementCopy()
{
ui.cursor.shape = CS_BUSY;
uiElementCopyBuffer.Clear();
for (uint i = 0; i < selectedUIElements.length; ++i)
{
XMLFile@ xml = XMLFile();
XMLElement rootElem = xml.CreateRoot("element");
selectedUIElements[i].SaveXML(rootElem);
uiElementCopyBuffer.Push(xml);
}
return true;
}
void ResetDuplicateID(UIElement@ element)
{
// If it is a duplicate copy then the element ID need to be regenerated by resetting it now to empty
if (GetListIndex(element) != NO_ITEM)
element.vars[UI_ELEMENT_ID_VAR] = Variant();
// Perform the action recursively for child elements
for (uint i = 0; i < element.numChildren; ++i)
ResetDuplicateID(element.children[i]);
}
bool UIElementPaste(bool duplication = false)
{
ui.cursor.shape = CS_BUSY;
// Group for storing undo actions
EditActionGroup group;
// Have to update manually because the element ID var is not set yet when the E_ELEMENTADDED event is sent
suppressUIElementChanges = true;
for (uint i = 0; i < uiElementCopyBuffer.length; ++i)
{
XMLElement rootElem = uiElementCopyBuffer[i].root;
UIElement@ pasteElement;
if (!duplication)
pasteElement = editUIElement;
else
{
if (editUIElement.parent !is null)
pasteElement = editUIElement.parent;
else
pasteElement = editUIElement;
}
if (pasteElement.LoadChildXML(rootElem, null) !is null)
{
UIElement@ element = pasteElement.children[pasteElement.numChildren - 1];
ResetDuplicateID(element);
UpdateHierarchyItem(element);
SetUIElementModified(pasteElement);
// Create an undo action
CreateUIElementAction action;
action.Define(element);
group.actions.Push(action);
}
}
SaveEditActionGroup(group);
suppressUIElementChanges = false;
return true;
}
bool UIElementDuplicate()
{
ui.cursor.shape = CS_BUSY;
Array<XMLFile@> copy = uiElementCopyBuffer;
UIElementCopy();
UIElementPaste(true);
uiElementCopyBuffer = copy;
return true;
}
bool UIElementDelete()
{
ui.cursor.shape = CS_BUSY;
BeginSelectionModify();
// Clear the selection now to prevent deleted elements from being reselected
hierarchyList.ClearSelection();
// Group for storing undo actions
EditActionGroup group;
for (uint i = 0; i < selectedUIElements.length; ++i)
{
UIElement@ element = selectedUIElements[i];
if (element.parent is null)
continue; // Already deleted
uint index = GetListIndex(element);
// Create undo action
DeleteUIElementAction action;
action.Define(element);
group.actions.Push(action);
SetUIElementModified(element);
element.Remove();
// If deleting only one element, select the next item in the same index
if (selectedUIElements.length == 1)
hierarchyList.selection = index;
}
SaveEditActionGroup(group);
EndSelectionModify();
return true;
}
bool UIElementSelectAll()
{
BeginSelectionModify();
Array<uint> indices;
uint baseIndex = GetListIndex(editorUIElement);
indices.Push(baseIndex);
int baseIndent = hierarchyList.items[baseIndex].indent;
for (uint i = baseIndex + 1; i < hierarchyList.numItems; ++i)
{
if (hierarchyList.items[i].indent <= baseIndent)
break;
indices.Push(i);
}
hierarchyList.SetSelections(indices);
EndSelectionModify();
return true;
}
bool UIElementResetToDefault()
{
ui.cursor.shape = CS_BUSY;
// Group for storing undo actions
EditActionGroup group;
// Reset selected elements to their default values
for (uint i = 0; i < selectedUIElements.length; ++i)
{
UIElement@ element = selectedUIElements[i];
ResetAttributesAction action;
action.Define(element);
group.actions.Push(action);
element.ResetToDefault();
action.SetInternalVars(element);
element.ApplyAttributes();
for (uint j = 0; j < element.numAttributes; ++j)
PostEditAttribute(element, j);
SetUIElementModified(element);
}
SaveEditActionGroup(group);
attributesFullDirty = true;
return true;
}
bool UIElementChangeParent(UIElement@ sourceElement, UIElement@ targetElement)
{
ReparentUIElementAction action;
action.Define(sourceElement, targetElement);
SaveEditAction(action);
sourceElement.parent = targetElement;
SetUIElementModified(targetElement);
return sourceElement.parent is targetElement;
}
bool UIElementReorder(UIElement@ sourceElement, UIElement@ targetElement)
{
if (sourceElement is null || targetElement is null || sourceElement.parent is null || sourceElement.parent !is targetElement.parent)
return false;
if (sourceElement is targetElement)
return true; // No-op
UIElement@ parent = sourceElement.parent;
uint destIndex = parent.FindChild(targetElement);
Print("Reorder to dest index " + destIndex);
ReorderUIElementAction action;
action.Define(sourceElement, destIndex);
SaveEditAction(action);
PerformReorder(parent, sourceElement, destIndex);
return true;
}
void PerformReorder(UIElement@ parent, UIElement@ child, uint destIndex)
{
suppressSceneChanges = true;
parent.RemoveChild(child);
parent.InsertChild(destIndex, child);
UpdateHierarchyItem(parent); // Force update to make sure the order is current
SetUIElementModified(parent);
suppressSceneChanges = false;
}