From 3d32474e949127d473c96d8eb0fbb7c8d2cb5bed Mon Sep 17 00:00:00 2001 From: Stanislav Demyanovich Date: Fri, 23 Dec 2022 22:20:17 +0400 Subject: [PATCH 1/5] Separate editor data from samples data (draft). --- bin/Data/Materials/Editor/DebugIconLight.xml | 4 - bin/Editor.sh | 2 +- bin/EditorData/Editor/Fonts/Hack-Regular.ttf | Bin 0 -> 309408 bytes .../Editor/Fonts/Hack-fonts-LICENSE.txt | 20 + .../Editor/Materials}/BlueUnlit.xml | 0 .../Editor/Materials}/BrightBlueUnlit.xml | 0 .../Editor/Materials}/BrightGreenUnlit.xml | 0 .../Editor/Materials}/BrightRedUnlit.xml | 0 .../Editor/Materials}/DebugIconCamera.xml | 4 +- .../Materials}/DebugIconCollisionTrigger.xml | 2 +- .../Materials}/DebugIconCustomGeometry.xml | 4 +- .../Editor/Materials/DebugIconLight.xml | 4 + .../Materials}/DebugIconParticleEmitter.xml | 4 +- .../Editor/Materials}/DebugIconPointLight.xml | 4 +- .../Materials}/DebugIconSoundListener.xml | 4 +- .../Materials}/DebugIconSoundSource.xml | 4 +- .../Materials}/DebugIconSplinePathPoint.xml | 2 +- .../Editor/Materials}/DebugIconSpotLight.xml | 4 +- .../Editor/Materials}/DebugIconZone.xml | 2 +- .../Editor/Materials}/GreenUnlit.xml | 0 .../Editor/Materials}/RedUnlit.xml | 0 .../Editor/Materials}/TexturedUnlit.xml | 2 +- .../Editor/Models}/Axes.mdl | Bin bin/EditorData/Editor/Models/Box.mdl | Bin 0 -> 1436 bytes bin/EditorData/Editor/Models/Cone.mdl | Bin 0 -> 3868 bytes bin/EditorData/Editor/Models/Cylinder.mdl | Bin 0 -> 7620 bytes .../Editor/Models}/ImagePlane.mdl | Bin bin/EditorData/Editor/Models/Plane.mdl | Bin 0 -> 336 bytes .../Editor/Models}/RotateAxes.mdl | Bin .../Editor/Models}/ScaleAxes.mdl | Bin bin/EditorData/Editor/Models/Sphere.mdl | Bin 0 -> 42480 bytes bin/EditorData/Editor/Models/TeaPot.mdl | Bin 0 -> 507424 bytes .../Editor/Scripts}/AttributeEditor.as | 3120 ++++----- .../Editor}/Scripts/Editor.as | 912 +-- .../Editor/Scripts}/EditorActions.as | 2304 +++---- .../Editor/Scripts}/EditorColorWheel.as | 68 +- .../Editor/Scripts}/EditorCubeCapture.as | 0 .../Editor/Scripts}/EditorDuplicator.as | 42 +- .../Editor/Scripts}/EditorEventsHandlers.as | 0 .../Editor/Scripts}/EditorExport.as | 0 .../Editor/Scripts}/EditorGizmo.as | 942 +-- .../Editor/Scripts}/EditorHierarchyWindow.as | 3890 +++++------ .../Editor/Scripts}/EditorImport.as | 1152 ++-- .../Editor/Scripts}/EditorInspectorWindow.as | 2064 +++--- .../Editor/Scripts}/EditorLayers.as | 154 +- .../Editor/Scripts}/EditorMaterial.as | 2054 +++--- .../Editor/Scripts}/EditorParticleEffect.as | 44 +- .../Editor/Scripts}/EditorPreferences.as | 888 +-- .../Editor/Scripts}/EditorResourceBrowser.as | 3388 +++++----- .../Editor/Scripts}/EditorScene.as | 3328 +++++----- .../Editor/Scripts}/EditorSecondaryToolbar.as | 0 .../Editor/Scripts}/EditorSettings.as | 924 +-- .../Editor/Scripts}/EditorSoundType.as | 44 +- .../Editor/Scripts}/EditorSpawn.as | 2 +- .../Editor/Scripts}/EditorTerrain.as | 6 +- .../Editor/Scripts}/EditorToolBar.as | 0 .../Editor/Scripts}/EditorUI.as | 4536 ++++++------- .../Editor/Scripts}/EditorUIElement.as | 1446 ++--- .../Editor/Scripts}/EditorView.as | 5678 ++++++++--------- .../Editor/Scripts}/EditorViewDebugIcons.as | 8 +- .../Scripts}/EditorViewPaintSelection.as | 4 +- .../Scripts}/EditorViewSelectableOrigins.as | 22 +- .../Editor/Strings}/EditorStrings.json | 0 .../Editor/Textures}/BW.png | Bin .../Editor/Textures}/EditorIcons.png | Bin .../Editor/Textures}/EditorIcons.xml | 0 .../Editor/Textures}/HSV20.png | Bin .../Editor/Textures}/IconCamera.png | Bin .../Editor/Textures}/IconCollisionTrigger.png | Bin .../Editor/Textures}/IconCustomGeometry.png | Bin .../Editor/Textures}/IconLight.png | Bin .../Editor/Textures}/IconParticleEmitter.png | Bin .../Editor/Textures}/IconPointLight.png | Bin .../Editor/Textures}/IconSoundListener.png | Bin .../Editor/Textures}/IconSoundSource.png | Bin .../Editor/Textures}/IconSplinePathPoint.png | Bin .../Editor/Textures}/IconSpotLight.png | Bin .../Editor/Textures}/IconZone.png | Bin bin/EditorData/Editor/Textures/LogoLarge.png | Bin 0 -> 27159 bytes bin/EditorData/Editor/Textures/LogoLarge.xml | 4 + .../Editor/Textures}/NoPreviewAvailable.png | Bin .../Editor/Textures}/SelectionCircle.png | Bin .../Textures}/TerrainBrushes/large_dots.png | Bin .../Textures}/TerrainBrushes/large_hard.png | Bin .../Textures}/TerrainBrushes/large_med.png | Bin .../Textures}/TerrainBrushes/large_soft.png | Bin .../Textures}/TerrainBrushes/tiny_hard.png | Bin .../Textures}/TerrainBrushes/tiny_med.png | Bin .../Textures}/TerrainBrushes/tiny_soft.png | Bin bin/EditorData/Editor/Textures/UI.png | Bin 0 -> 19772 bytes bin/EditorData/Editor/Textures/UI.xml | 5 + bin/EditorData/Editor/Textures/UrhoIcon.png | Bin 0 -> 4686 bytes .../Editor}/UI/EditorColorWheel.xml | 4 +- .../Editor}/UI/EditorContextMenu.xml | 0 .../Editor}/UI/EditorDuplicator.xml | 0 .../Editor}/UI/EditorHierarchyWindow.xml | 0 .../Editor}/UI/EditorIcons.xml | 916 +-- .../Editor}/UI/EditorInspectorWindow.xml | 70 +- .../Editor}/UI/EditorInspector_Attribute.xml | 60 +- .../Editor}/UI/EditorInspector_Style.xml | 0 .../Editor}/UI/EditorInspector_Tags.xml | 0 .../Editor}/UI/EditorInspector_Variable.xml | 0 .../Editor}/UI/EditorLayersWindow.xml | 0 .../Editor}/UI/EditorMaterialWindow.xml | 924 +-- .../Editor}/UI/EditorParticleEffectWindow.xml | 0 .../Editor}/UI/EditorPreferencesDialog.xml | 810 +-- .../Editor}/UI/EditorQuickMenu.xml | 104 +- .../Editor}/UI/EditorResourceBrowser.xml | 2 +- .../Editor}/UI/EditorResourceFilterWindow.xml | 0 .../Editor}/UI/EditorSettingsDialog.xml | 1164 ++-- .../Editor}/UI/EditorSoundTypeWindow.xml | 0 .../Editor}/UI/EditorSpawnWindow.xml | 0 bin/EditorData/Editor/UI/EditorStyle.xml | 403 ++ .../Editor}/UI/EditorTerrainWindow.xml | 12 +- .../Editor}/UI/EditorViewport.xml | 0 115 files changed, 20998 insertions(+), 20566 deletions(-) delete mode 100644 bin/Data/Materials/Editor/DebugIconLight.xml create mode 100644 bin/EditorData/Editor/Fonts/Hack-Regular.ttf create mode 100644 bin/EditorData/Editor/Fonts/Hack-fonts-LICENSE.txt rename bin/{Data/Materials/Editor => EditorData/Editor/Materials}/BlueUnlit.xml (100%) rename bin/{Data/Materials/Editor => EditorData/Editor/Materials}/BrightBlueUnlit.xml (100%) rename bin/{Data/Materials/Editor => EditorData/Editor/Materials}/BrightGreenUnlit.xml (100%) rename bin/{Data/Materials/Editor => EditorData/Editor/Materials}/BrightRedUnlit.xml (100%) rename bin/{Data/Materials/Editor => EditorData/Editor/Materials}/DebugIconCamera.xml (50%) rename bin/{Data/Materials/Editor => EditorData/Editor/Materials}/DebugIconCollisionTrigger.xml (64%) rename bin/{Data/Materials/Editor => EditorData/Editor/Materials}/DebugIconCustomGeometry.xml (52%) create mode 100644 bin/EditorData/Editor/Materials/DebugIconLight.xml rename bin/{Data/Materials/Editor => EditorData/Editor/Materials}/DebugIconParticleEmitter.xml (52%) rename bin/{Data/Materials/Editor => EditorData/Editor/Materials}/DebugIconPointLight.xml (51%) rename bin/{Data/Materials/Editor => EditorData/Editor/Materials}/DebugIconSoundListener.xml (52%) rename bin/{Data/Materials/Editor => EditorData/Editor/Materials}/DebugIconSoundSource.xml (51%) rename bin/{Data/Materials/Editor => EditorData/Editor/Materials}/DebugIconSplinePathPoint.xml (68%) rename bin/{Data/Materials/Editor => EditorData/Editor/Materials}/DebugIconSpotLight.xml (50%) rename bin/{Data/Materials/Editor => EditorData/Editor/Materials}/DebugIconZone.xml (67%) rename bin/{Data/Materials/Editor => EditorData/Editor/Materials}/GreenUnlit.xml (100%) rename bin/{Data/Materials/Editor => EditorData/Editor/Materials}/RedUnlit.xml (100%) rename bin/{Data/Materials/Editor => EditorData/Editor/Materials}/TexturedUnlit.xml (65%) rename bin/{Data/Models/Editor => EditorData/Editor/Models}/Axes.mdl (100%) create mode 100644 bin/EditorData/Editor/Models/Box.mdl create mode 100644 bin/EditorData/Editor/Models/Cone.mdl create mode 100644 bin/EditorData/Editor/Models/Cylinder.mdl rename bin/{Data/Models/Editor => EditorData/Editor/Models}/ImagePlane.mdl (100%) create mode 100644 bin/EditorData/Editor/Models/Plane.mdl rename bin/{Data/Models/Editor => EditorData/Editor/Models}/RotateAxes.mdl (100%) rename bin/{Data/Models/Editor => EditorData/Editor/Models}/ScaleAxes.mdl (100%) create mode 100644 bin/EditorData/Editor/Models/Sphere.mdl create mode 100644 bin/EditorData/Editor/Models/TeaPot.mdl rename bin/{Data/Scripts/Editor => EditorData/Editor/Scripts}/AttributeEditor.as (97%) rename bin/{Data => EditorData/Editor}/Scripts/Editor.as (91%) rename bin/{Data/Scripts/Editor => EditorData/Editor/Scripts}/EditorActions.as (96%) rename bin/{Data/Scripts/Editor => EditorData/Editor/Scripts}/EditorColorWheel.as (89%) rename bin/{Data/Scripts/Editor => EditorData/Editor/Scripts}/EditorCubeCapture.as (100%) rename bin/{Data/Scripts/Editor => EditorData/Editor/Scripts}/EditorDuplicator.as (93%) rename bin/{Data/Scripts/Editor => EditorData/Editor/Scripts}/EditorEventsHandlers.as (100%) rename bin/{Data/Scripts/Editor => EditorData/Editor/Scripts}/EditorExport.as (100%) rename bin/{Data/Scripts/Editor => EditorData/Editor/Scripts}/EditorGizmo.as (89%) rename bin/{Data/Scripts/Editor => EditorData/Editor/Scripts}/EditorHierarchyWindow.as (96%) rename bin/{Data/Scripts/Editor => EditorData/Editor/Scripts}/EditorImport.as (97%) rename bin/{Data/Scripts/Editor => EditorData/Editor/Scripts}/EditorInspectorWindow.as (96%) rename bin/{Data/Scripts/Editor => EditorData/Editor/Scripts}/EditorLayers.as (80%) rename bin/{Data/Scripts/Editor => EditorData/Editor/Scripts}/EditorMaterial.as (94%) rename bin/{Data/Scripts/Editor => EditorData/Editor/Scripts}/EditorParticleEffect.as (99%) rename bin/{Data/Scripts/Editor => EditorData/Editor/Scripts}/EditorPreferences.as (97%) rename bin/{Data/Scripts/Editor => EditorData/Editor/Scripts}/EditorResourceBrowser.as (96%) rename bin/{Data/Scripts/Editor => EditorData/Editor/Scripts}/EditorScene.as (96%) rename bin/{Data/Scripts/Editor => EditorData/Editor/Scripts}/EditorSecondaryToolbar.as (100%) rename bin/{Data/Scripts/Editor => EditorData/Editor/Scripts}/EditorSettings.as (96%) rename bin/{Data/Scripts/Editor => EditorData/Editor/Scripts}/EditorSoundType.as (94%) rename bin/{Data/Scripts/Editor => EditorData/Editor/Scripts}/EditorSpawn.as (99%) rename bin/{Data/Scripts/Editor => EditorData/Editor/Scripts}/EditorTerrain.as (99%) rename bin/{Data/Scripts/Editor => EditorData/Editor/Scripts}/EditorToolBar.as (100%) rename bin/{Data/Scripts/Editor => EditorData/Editor/Scripts}/EditorUI.as (96%) rename bin/{Data/Scripts/Editor => EditorData/Editor/Scripts}/EditorUIElement.as (96%) rename bin/{Data/Scripts/Editor => EditorData/Editor/Scripts}/EditorView.as (96%) rename bin/{Data/Scripts/Editor => EditorData/Editor/Scripts}/EditorViewDebugIcons.as (98%) rename bin/{Data/Scripts/Editor => EditorData/Editor/Scripts}/EditorViewPaintSelection.as (98%) rename bin/{Data/Scripts/Editor => EditorData/Editor/Scripts}/EditorViewSelectableOrigins.as (97%) rename bin/{Data => EditorData/Editor/Strings}/EditorStrings.json (100%) rename bin/{Data/Textures/Editor => EditorData/Editor/Textures}/BW.png (100%) rename bin/{Data/Textures/Editor => EditorData/Editor/Textures}/EditorIcons.png (100%) rename bin/{Data/Textures/Editor => EditorData/Editor/Textures}/EditorIcons.xml (100%) rename bin/{Data/Textures/Editor => EditorData/Editor/Textures}/HSV20.png (100%) rename bin/{Data/Textures/Editor => EditorData/Editor/Textures}/IconCamera.png (100%) rename bin/{Data/Textures/Editor => EditorData/Editor/Textures}/IconCollisionTrigger.png (100%) rename bin/{Data/Textures/Editor => EditorData/Editor/Textures}/IconCustomGeometry.png (100%) rename bin/{Data/Textures/Editor => EditorData/Editor/Textures}/IconLight.png (100%) rename bin/{Data/Textures/Editor => EditorData/Editor/Textures}/IconParticleEmitter.png (100%) rename bin/{Data/Textures/Editor => EditorData/Editor/Textures}/IconPointLight.png (100%) rename bin/{Data/Textures/Editor => EditorData/Editor/Textures}/IconSoundListener.png (100%) rename bin/{Data/Textures/Editor => EditorData/Editor/Textures}/IconSoundSource.png (100%) rename bin/{Data/Textures/Editor => EditorData/Editor/Textures}/IconSplinePathPoint.png (100%) rename bin/{Data/Textures/Editor => EditorData/Editor/Textures}/IconSpotLight.png (100%) rename bin/{Data/Textures/Editor => EditorData/Editor/Textures}/IconZone.png (100%) create mode 100644 bin/EditorData/Editor/Textures/LogoLarge.png create mode 100644 bin/EditorData/Editor/Textures/LogoLarge.xml rename bin/{Data/Textures/Editor => EditorData/Editor/Textures}/NoPreviewAvailable.png (100%) rename bin/{Data/Textures/Editor => EditorData/Editor/Textures}/SelectionCircle.png (100%) rename bin/{Data/Textures/Editor => EditorData/Editor/Textures}/TerrainBrushes/large_dots.png (100%) rename bin/{Data/Textures/Editor => EditorData/Editor/Textures}/TerrainBrushes/large_hard.png (100%) rename bin/{Data/Textures/Editor => EditorData/Editor/Textures}/TerrainBrushes/large_med.png (100%) rename bin/{Data/Textures/Editor => EditorData/Editor/Textures}/TerrainBrushes/large_soft.png (100%) rename bin/{Data/Textures/Editor => EditorData/Editor/Textures}/TerrainBrushes/tiny_hard.png (100%) rename bin/{Data/Textures/Editor => EditorData/Editor/Textures}/TerrainBrushes/tiny_med.png (100%) rename bin/{Data/Textures/Editor => EditorData/Editor/Textures}/TerrainBrushes/tiny_soft.png (100%) create mode 100644 bin/EditorData/Editor/Textures/UI.png create mode 100644 bin/EditorData/Editor/Textures/UI.xml create mode 100644 bin/EditorData/Editor/Textures/UrhoIcon.png rename bin/{Data => EditorData/Editor}/UI/EditorColorWheel.xml (99%) rename bin/{Data => EditorData/Editor}/UI/EditorContextMenu.xml (100%) rename bin/{Data => EditorData/Editor}/UI/EditorDuplicator.xml (100%) rename bin/{Data => EditorData/Editor}/UI/EditorHierarchyWindow.xml (100%) rename bin/{Data => EditorData/Editor}/UI/EditorIcons.xml (54%) rename bin/{Data => EditorData/Editor}/UI/EditorInspectorWindow.xml (95%) rename bin/{Data => EditorData/Editor}/UI/EditorInspector_Attribute.xml (97%) rename bin/{Data => EditorData/Editor}/UI/EditorInspector_Style.xml (100%) rename bin/{Data => EditorData/Editor}/UI/EditorInspector_Tags.xml (100%) rename bin/{Data => EditorData/Editor}/UI/EditorInspector_Variable.xml (100%) rename bin/{Data => EditorData/Editor}/UI/EditorLayersWindow.xml (100%) rename bin/{Data => EditorData/Editor}/UI/EditorMaterialWindow.xml (98%) rename bin/{Data => EditorData/Editor}/UI/EditorParticleEffectWindow.xml (100%) rename bin/{Data => EditorData/Editor}/UI/EditorPreferencesDialog.xml (98%) rename bin/{Data => EditorData/Editor}/UI/EditorQuickMenu.xml (97%) rename bin/{Data => EditorData/Editor}/UI/EditorResourceBrowser.xml (99%) rename bin/{Data => EditorData/Editor}/UI/EditorResourceFilterWindow.xml (100%) rename bin/{Data => EditorData/Editor}/UI/EditorSettingsDialog.xml (98%) rename bin/{Data => EditorData/Editor}/UI/EditorSoundTypeWindow.xml (100%) rename bin/{Data => EditorData/Editor}/UI/EditorSpawnWindow.xml (100%) create mode 100755 bin/EditorData/Editor/UI/EditorStyle.xml rename bin/{Data => EditorData/Editor}/UI/EditorTerrainWindow.xml (94%) rename bin/{Data => EditorData/Editor}/UI/EditorViewport.xml (100%) diff --git a/bin/Data/Materials/Editor/DebugIconLight.xml b/bin/Data/Materials/Editor/DebugIconLight.xml deleted file mode 100644 index 1409963f312..00000000000 --- a/bin/Data/Materials/Editor/DebugIconLight.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/bin/Editor.sh b/bin/Editor.sh index b558940dd00..35d81604df5 100755 --- a/bin/Editor.sh +++ b/bin/Editor.sh @@ -1,3 +1,3 @@ #!/usr/bin/env bash if [[ $# -eq 0 ]]; then OPT1="-w -s"; fi -$(dirname $0)/Urho3DPlayer Scripts/Editor.as $OPT1 $@ +$(dirname $0)/Urho3DPlayer Editor/Scripts/Editor.as -p "EditorData;Data;CoreData" $OPT1 $@ diff --git a/bin/EditorData/Editor/Fonts/Hack-Regular.ttf b/bin/EditorData/Editor/Fonts/Hack-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..92a90cb06e0535afa79f6cba26ceece15a7f7959 GIT binary patch literal 309408 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

lipX z89%t)L1Zn7w#;-}USgg#E*&nk6g|(G1Qmus3tNsjIhevgp16@5CCac&oFQ%eMM}nZ z{mnz>s34v3aMz*q^t7z0vk!G1GW!Jt2ASxcw2bWTp8MFWsimbQCDW&tl$Dk-0g1n8B}ozJZoF0r$e^J`a*sp<@okRTsO%H2(}g3TTkA_r zY_P>jVzWUR3KUay?+f%NC1?ezWXd19?3;y;D2) z=Jd@SxaBCD7@yRdoI1@I*B<-wy!p$P-g4A5y{O1h(7URyq%^-^5%4=S<)}GLXGT&? zP*KLzTkhzZ9`LuniN0E|5dVh^Rxh7GQJxUnGxL_)=k$BaC)IDL^7E%p&b9}+_DF?A zGiGGlx&2!bzLjrRQ-8rQ!6d{9w;c=hLS+CVTojp^4u~Lz7RS90h8bLbuiryo}eM%KoVvP^fjleHU^+=q5A;2uIP{IMn8hjx?CUO%x=LY*K$e zUzje{LKq99L~=Fia#OW;19`##VP1~Mrtvh>B0Gh6=r^B#@tfaImET5%z7r=71y6sR z1)+KIKbbhmHDqK(Opkw#A(lJ1ckkf2b32FbV2>ybjO9me1|^F}-I933OjHUpo!ybB za2SP5ev0ZSLtRNH0zA}Iy~GRPK;d0)CZZjOFGZkHpcnj=aBX`konu{0C@R1fXWJ6u zt>zd5de9(qgvu{hvPyqiP=X*?9QK49o6V7vmy2IXc%isb1#ifGsWofO;PWB)4-5)1 z;7{NZNdd%*kTmtr&*fqJ!&%FTzlevj)`WLeZu_9MsiDqXpU^aM&*aJB?87X%2GSC~ z2D0Q+hRKs}Y;8)eG1oRU8MQ2k#V^zZ`IMHcL&f>K|5&>xBqaImm<(OqoH=jI_YU>b z>SAO4B%e^+h{dN=?PS~(gl+~lg! zO4x6Syg#q!XWP@GGQ%@7XD^FdKFsu40i{{Jdo!~$as#PyNTj_fx=|f«q(At`*`NbkV(;WAZRz)Jn7&p-cM>U1p@ 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 D9ti__ literal 0 HcmV?d00001 diff --git a/bin/EditorData/Editor/Fonts/Hack-fonts-LICENSE.txt b/bin/EditorData/Editor/Fonts/Hack-fonts-LICENSE.txt new file mode 100644 index 00000000000..efab9fb8f92 --- /dev/null +++ b/bin/EditorData/Editor/Fonts/Hack-fonts-LICENSE.txt @@ -0,0 +1,20 @@ +Copyright (c) 2018 Source Foundry Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/bin/Data/Materials/Editor/BlueUnlit.xml b/bin/EditorData/Editor/Materials/BlueUnlit.xml similarity index 100% rename from bin/Data/Materials/Editor/BlueUnlit.xml rename to bin/EditorData/Editor/Materials/BlueUnlit.xml diff --git a/bin/Data/Materials/Editor/BrightBlueUnlit.xml b/bin/EditorData/Editor/Materials/BrightBlueUnlit.xml similarity index 100% rename from bin/Data/Materials/Editor/BrightBlueUnlit.xml rename to bin/EditorData/Editor/Materials/BrightBlueUnlit.xml diff --git a/bin/Data/Materials/Editor/BrightGreenUnlit.xml b/bin/EditorData/Editor/Materials/BrightGreenUnlit.xml similarity index 100% rename from bin/Data/Materials/Editor/BrightGreenUnlit.xml rename to bin/EditorData/Editor/Materials/BrightGreenUnlit.xml diff --git a/bin/Data/Materials/Editor/BrightRedUnlit.xml b/bin/EditorData/Editor/Materials/BrightRedUnlit.xml similarity index 100% rename from bin/Data/Materials/Editor/BrightRedUnlit.xml rename to bin/EditorData/Editor/Materials/BrightRedUnlit.xml diff --git a/bin/Data/Materials/Editor/DebugIconCamera.xml b/bin/EditorData/Editor/Materials/DebugIconCamera.xml similarity index 50% rename from bin/Data/Materials/Editor/DebugIconCamera.xml rename to bin/EditorData/Editor/Materials/DebugIconCamera.xml index c1bad2782e9..2d6e07ef06c 100644 --- a/bin/Data/Materials/Editor/DebugIconCamera.xml +++ b/bin/EditorData/Editor/Materials/DebugIconCamera.xml @@ -1,4 +1,4 @@ - - \ No newline at end of file + + diff --git a/bin/Data/Materials/Editor/DebugIconCollisionTrigger.xml b/bin/EditorData/Editor/Materials/DebugIconCollisionTrigger.xml similarity index 64% rename from bin/Data/Materials/Editor/DebugIconCollisionTrigger.xml rename to bin/EditorData/Editor/Materials/DebugIconCollisionTrigger.xml index c3da6ddb6a5..453fad99257 100644 --- a/bin/Data/Materials/Editor/DebugIconCollisionTrigger.xml +++ b/bin/EditorData/Editor/Materials/DebugIconCollisionTrigger.xml @@ -1,5 +1,5 @@ - + diff --git a/bin/Data/Materials/Editor/DebugIconCustomGeometry.xml b/bin/EditorData/Editor/Materials/DebugIconCustomGeometry.xml similarity index 52% rename from bin/Data/Materials/Editor/DebugIconCustomGeometry.xml rename to bin/EditorData/Editor/Materials/DebugIconCustomGeometry.xml index 042cc6e748f..34e2bc76d81 100644 --- a/bin/Data/Materials/Editor/DebugIconCustomGeometry.xml +++ b/bin/EditorData/Editor/Materials/DebugIconCustomGeometry.xml @@ -1,4 +1,4 @@ - - \ No newline at end of file + + diff --git a/bin/EditorData/Editor/Materials/DebugIconLight.xml b/bin/EditorData/Editor/Materials/DebugIconLight.xml new file mode 100644 index 00000000000..da1af6f0007 --- /dev/null +++ b/bin/EditorData/Editor/Materials/DebugIconLight.xml @@ -0,0 +1,4 @@ + + + + diff --git a/bin/Data/Materials/Editor/DebugIconParticleEmitter.xml b/bin/EditorData/Editor/Materials/DebugIconParticleEmitter.xml similarity index 52% rename from bin/Data/Materials/Editor/DebugIconParticleEmitter.xml rename to bin/EditorData/Editor/Materials/DebugIconParticleEmitter.xml index a1764f49025..7a6cbf4122b 100644 --- a/bin/Data/Materials/Editor/DebugIconParticleEmitter.xml +++ b/bin/EditorData/Editor/Materials/DebugIconParticleEmitter.xml @@ -1,4 +1,4 @@ - - \ No newline at end of file + + diff --git a/bin/Data/Materials/Editor/DebugIconPointLight.xml b/bin/EditorData/Editor/Materials/DebugIconPointLight.xml similarity index 51% rename from bin/Data/Materials/Editor/DebugIconPointLight.xml rename to bin/EditorData/Editor/Materials/DebugIconPointLight.xml index ac9c1c4bb71..e0eabf1727c 100644 --- a/bin/Data/Materials/Editor/DebugIconPointLight.xml +++ b/bin/EditorData/Editor/Materials/DebugIconPointLight.xml @@ -1,4 +1,4 @@ - - \ No newline at end of file + + diff --git a/bin/Data/Materials/Editor/DebugIconSoundListener.xml b/bin/EditorData/Editor/Materials/DebugIconSoundListener.xml similarity index 52% rename from bin/Data/Materials/Editor/DebugIconSoundListener.xml rename to bin/EditorData/Editor/Materials/DebugIconSoundListener.xml index 12e61558fbb..0dc7c1f2f58 100644 --- a/bin/Data/Materials/Editor/DebugIconSoundListener.xml +++ b/bin/EditorData/Editor/Materials/DebugIconSoundListener.xml @@ -1,4 +1,4 @@ - - \ No newline at end of file + + diff --git a/bin/Data/Materials/Editor/DebugIconSoundSource.xml b/bin/EditorData/Editor/Materials/DebugIconSoundSource.xml similarity index 51% rename from bin/Data/Materials/Editor/DebugIconSoundSource.xml rename to bin/EditorData/Editor/Materials/DebugIconSoundSource.xml index fcfd1f6a741..eacca6eb3b1 100644 --- a/bin/Data/Materials/Editor/DebugIconSoundSource.xml +++ b/bin/EditorData/Editor/Materials/DebugIconSoundSource.xml @@ -1,4 +1,4 @@ - - \ No newline at end of file + + diff --git a/bin/Data/Materials/Editor/DebugIconSplinePathPoint.xml b/bin/EditorData/Editor/Materials/DebugIconSplinePathPoint.xml similarity index 68% rename from bin/Data/Materials/Editor/DebugIconSplinePathPoint.xml rename to bin/EditorData/Editor/Materials/DebugIconSplinePathPoint.xml index 7794253493e..0db192be91e 100644 --- a/bin/Data/Materials/Editor/DebugIconSplinePathPoint.xml +++ b/bin/EditorData/Editor/Materials/DebugIconSplinePathPoint.xml @@ -1,5 +1,5 @@ - + diff --git a/bin/Data/Materials/Editor/DebugIconSpotLight.xml b/bin/EditorData/Editor/Materials/DebugIconSpotLight.xml similarity index 50% rename from bin/Data/Materials/Editor/DebugIconSpotLight.xml rename to bin/EditorData/Editor/Materials/DebugIconSpotLight.xml index 6c22cd27ce5..72e30e6391e 100644 --- a/bin/Data/Materials/Editor/DebugIconSpotLight.xml +++ b/bin/EditorData/Editor/Materials/DebugIconSpotLight.xml @@ -1,4 +1,4 @@ - - \ No newline at end of file + + diff --git a/bin/Data/Materials/Editor/DebugIconZone.xml b/bin/EditorData/Editor/Materials/DebugIconZone.xml similarity index 67% rename from bin/Data/Materials/Editor/DebugIconZone.xml rename to bin/EditorData/Editor/Materials/DebugIconZone.xml index 06a3e711a3d..392a03fc183 100644 --- a/bin/Data/Materials/Editor/DebugIconZone.xml +++ b/bin/EditorData/Editor/Materials/DebugIconZone.xml @@ -1,5 +1,5 @@ - + diff --git a/bin/Data/Materials/Editor/GreenUnlit.xml b/bin/EditorData/Editor/Materials/GreenUnlit.xml similarity index 100% rename from bin/Data/Materials/Editor/GreenUnlit.xml rename to bin/EditorData/Editor/Materials/GreenUnlit.xml diff --git a/bin/Data/Materials/Editor/RedUnlit.xml b/bin/EditorData/Editor/Materials/RedUnlit.xml similarity index 100% rename from bin/Data/Materials/Editor/RedUnlit.xml rename to bin/EditorData/Editor/Materials/RedUnlit.xml diff --git a/bin/Data/Materials/Editor/TexturedUnlit.xml b/bin/EditorData/Editor/Materials/TexturedUnlit.xml similarity index 65% rename from bin/Data/Materials/Editor/TexturedUnlit.xml rename to bin/EditorData/Editor/Materials/TexturedUnlit.xml index 1bbacbb79fd..dc6382ed3d8 100644 --- a/bin/Data/Materials/Editor/TexturedUnlit.xml +++ b/bin/EditorData/Editor/Materials/TexturedUnlit.xml @@ -1,5 +1,5 @@ - + diff --git a/bin/Data/Models/Editor/Axes.mdl b/bin/EditorData/Editor/Models/Axes.mdl similarity index 100% rename from bin/Data/Models/Editor/Axes.mdl rename to bin/EditorData/Editor/Models/Axes.mdl diff --git a/bin/EditorData/Editor/Models/Box.mdl b/bin/EditorData/Editor/Models/Box.mdl new file mode 100644 index 0000000000000000000000000000000000000000..27a7f2cbc36f9e0d513b74712ee7caf5c0df7946 GIT binary patch literal 1436 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 literal 0 HcmV?d00001 diff --git a/bin/Data/Models/Editor/ImagePlane.mdl b/bin/EditorData/Editor/Models/ImagePlane.mdl similarity index 100% rename from bin/Data/Models/Editor/ImagePlane.mdl rename to bin/EditorData/Editor/Models/ImagePlane.mdl diff --git a/bin/EditorData/Editor/Models/Plane.mdl b/bin/EditorData/Editor/Models/Plane.mdl new file mode 100644 index 0000000000000000000000000000000000000000..f5454a68b790d7ec3b6d40285aebfed7da8bc288 GIT binary patch literal 336 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

  • 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

    o@6w)(p|^!74TVx0}sXXMG;A7^l#sU zqK4@#H2~)W+EhKe_|{#|!-+ z_3!d<^nauE$a8uces0{(LTdL<4UFOXcX{Ga$7KUQm*3`wqJIO4qsLnA{#1sadx!l= z``01~{ZvWc9^t!IyD&~ALzajBT`P{BWF!NrJ93=Q+c%pQH>+IM(MmwXsCg5Hu!UxHqHw4Rt}{`?L6-1ngtYURja<5~SXL!D5cSODL-HF??48&B!8 z&;yS!BY%(OtKqXcJ6sTs#ZDz3*1zd=1K-?|n2A2IivGQ05eL3{x0LHWjo+4z^WO&!+E`adz97hLy{n38{+vB4{2J-qX2 zy7MxWzE8^nG~Xz{Pn+u-M8}px9#It<@9kl%UW_|5~3lhDPPr^9C@ z+XbrYLbn<=>fd=dpnbc8ebs!H>|vbiv*b9^XMKeX1}$`PI;W3w0XgyGuvL!v{TRN# zAp6;Ie1Cz)k$LCIushQ&Ca4$3L?w^Zzo{X#@0YNT@LBR5SYDqc$B{nE7VEjo4x@*$ z#?@pYUw;cWWL)zwhHpd2KAIfghTu{U6wk&y3-$othEQu>N;2NozeV!b94$1sbQvriMHjXU_Jjzq+{L z3*&74dn{8?vw-jau(4&6|6fyQ0an%0h2fd85F1-WEG)oALOjP#>~2&H#1<0~6ct59 z#ROaIx+aQ+9sgCYUc0V(?YMU5|IVC)kM3u_wZ3o7?6c3VGkXU2GJmt6f3wK{W7;@G zcv$;zzC|tWkrEYoE_s_h|Bn+nDD?43!|@>xht5Iq>-aR<+`t^4%^aUsu8GO+Znhj0 zm4kzKG~u^o2k#ap-$zB>cWWiz$FFne9KR*{+-d2M*K1JhEOYKSGOwwRPjQY9IZw

    S1 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

    Go=1^Z1KD2ne)YOz;(c)3 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�Jp(P?%2^YZ-@A{&c7fB8qy@Xv-F>0o#&kTiUw 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 h_#Url3#n<8)-b1K5m-KB%E^FJ~CKH+mR2 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 S-~99NSKe-l{(IV%lz#(|2`)bX literal 0 HcmV?d00001 diff --git a/bin/Data/Scripts/Editor/AttributeEditor.as b/bin/EditorData/Editor/Scripts/AttributeEditor.as similarity index 97% rename from bin/Data/Scripts/Editor/AttributeEditor.as rename to bin/EditorData/Editor/Scripts/AttributeEditor.as index b6ecff37c0a..7bb2a5dcccc 100644 --- a/bin/Data/Scripts/Editor/AttributeEditor.as +++ b/bin/EditorData/Editor/Scripts/AttributeEditor.as @@ -1,1560 +1,1560 @@ -// Attribute editor -// -// Functions that caller must implement: -// - void SetAttributeEditorID(UIElement@ attrEdit, Array@ serializables); -// - bool PreEditAttribute(Array@ serializables, uint index); -// - void PostEditAttribute(Array@ serializables, uint index, const Array& oldValues); -// - Array@ GetAttributeEditorTargets(UIElement@ attrEdit); -// - String GetVariableName(StringHash hash); - -const uint MIN_NODE_ATTRIBUTES = 4; -const uint MAX_NODE_ATTRIBUTES = 8; -const int ATTRNAME_WIDTH = 150; -const int ATTR_HEIGHT = 19; -const StringHash TEXT_CHANGED_EVENT_TYPE("TextChanged"); - -bool inLoadAttributeEditor = false; -bool inEditAttribute = false; -bool inUpdateBitSelection = false; -bool showNonEditableAttribute = false; - -Color normalTextColor(1.0f, 1.0f, 1.0f); -Color modifiedTextColor(1.0f, 0.8f, 0.5f); -Color nonEditableTextColor(0.7f, 0.7f, 0.7f); - -String sceneResourcePath = AddTrailingSlash(fileSystem.programDir + "Data"); -bool rememberResourcePath = true; - -// Exceptions for string attributes that should not be continuously edited -Array noTextChangedAttrs = {"Script File", "Class Name", "Script Object Type", "Script File Name"}; - -// List of attributes that should be created with a bit selection editor -const Array bitSelectionAttrs = {"Collision Mask", "Collision Layer", "Light Mask", "Zone Mask", "View Mask", "Shadow Mask"}; - -// Number of editable bits for bit selection editor -const int MAX_BITMASK_BITS = 8; -const int MAX_BITMASK_VALUE = (1 << MAX_BITMASK_BITS) - 1; -Color nonEditableBitSelectorColor(0.5f, 0.5f, 0.5f); -Color editableBitSelectorColor(1.0f, 1.0f, 1.0f); - -WeakHandle testAnimState; - -bool dragEditAttribute = false; - -UIElement@ SetEditable(UIElement@ element, bool editable) -{ - if (element is null) - return element; - - element.editable = editable; - element.colors[C_TOPLEFT] = editable ? element.colors[C_BOTTOMRIGHT] : nonEditableTextColor; - element.colors[C_BOTTOMLEFT] = element.colors[C_TOPLEFT]; - element.colors[C_TOPRIGHT] = element.colors[C_TOPLEFT]; - return element; -} - -UIElement@ SetValue(LineEdit@ element, const String&in value, bool sameValue) -{ - element.text = sameValue ? value : STRIKED_OUT; - element.cursorPosition = 0; - return element; -} - -UIElement@ SetValue(CheckBox@ element, bool value, bool sameValue) -{ - element.checked = sameValue ? value : false; - return element; -} - -UIElement@ SetValue(DropDownList@ element, int value, bool sameValue) -{ - element.selection = sameValue ? uint(value) : M_MAX_UNSIGNED; - return element; -} - -UIElement@ CreateAttributeEditorParentWithSeparatedLabel(ListView@ list, const String&in name, uint index, uint subIndex, bool suppressedSeparatedLabel = false) -{ - UIElement@ editorParent = UIElement("Edit" + String(index) + "_" + String(subIndex)); - editorParent.vars["Index"] = index; - editorParent.vars["SubIndex"] = subIndex; - editorParent.SetLayout(LM_VERTICAL, 2); - list.AddItem(editorParent); - - if (suppressedSeparatedLabel) - { - UIElement@ placeHolder = UIElement(name); - editorParent.AddChild(placeHolder); - } - else - { - Text@ attrNameText = Text(); - editorParent.AddChild(attrNameText); - attrNameText.style = "EditorAttributeText"; - attrNameText.text = name; - } - - return editorParent; -} - -UIElement@ CreateAttributeEditorParentAsListChild(ListView@ list, const String&in name, uint index, uint subIndex) -{ - UIElement@ editorParent = UIElement("Edit" + String(index) + "_" + String(subIndex)); - editorParent.vars["Index"] = index; - editorParent.vars["SubIndex"] = subIndex; - editorParent.SetLayout(LM_HORIZONTAL); - list.AddChild(editorParent); - - UIElement@ placeHolder = UIElement(name); - editorParent.AddChild(placeHolder); - - return editorParent; -} - -UIElement@ CreateAttributeEditorParent(ListView@ list, const String&in name, uint index, uint subIndex) -{ - UIElement@ editorParent = UIElement("Edit" + String(index) + "_" + String(subIndex)); - editorParent.vars["Index"] = index; - editorParent.vars["SubIndex"] = subIndex; - editorParent.SetLayout(LM_HORIZONTAL); - editorParent.SetFixedHeight(ATTR_HEIGHT); - list.AddItem(editorParent); - - Text@ attrNameText = Text(); - editorParent.AddChild(attrNameText); - attrNameText.style = "EditorAttributeText"; - attrNameText.text = name; - attrNameText.SetFixedWidth(ATTRNAME_WIDTH); - - return editorParent; -} - -LineEdit@ CreateAttributeLineEdit(UIElement@ parent, Array@ serializables, uint index, uint subIndex) -{ - LineEdit@ attrEdit = LineEdit(); - parent.AddChild(attrEdit); - attrEdit.dragDropMode = DD_TARGET; - attrEdit.style = "EditorAttributeEdit"; - attrEdit.SetFixedHeight(ATTR_HEIGHT - 2); - attrEdit.vars["Index"] = index; - attrEdit.vars["SubIndex"] = subIndex; - SetAttributeEditorID(attrEdit, serializables); - - return attrEdit; -} - -LineEdit@ CreateAttributeBitSelector(UIElement@ parent, Array@ serializables, uint index, uint subIndex) -{ - UIElement@ container = UIElement(); - parent.AddChild(container); - parent.SetFixedHeight(38); - container.SetFixedWidth(16 * 4 + 4); - - for (int i = 0; i < 2; i++) - { - for (int j = 0; j < 4; j++) - { - CheckBox@ bitBox = CheckBox(); - bitBox.name = "BitSelect_" + String(i * 4 + j); - container.AddChild(bitBox); - bitBox.position = IntVector2(16 * j, 16 * i); - bitBox.style = "CheckBox"; - bitBox.SetFixedHeight(16); - - SubscribeToEvent(bitBox,"Toggled", "HandleBitSelectionToggled"); - } - } - - LineEdit@ attrEdit = CreateAttributeLineEdit(parent, serializables, index, subIndex); - attrEdit.name = "LineEdit"; - SubscribeToEvent(attrEdit, "TextChanged", "HandleBitSelectionEdit"); - SubscribeToEvent(attrEdit, "TextFinished", "HandleBitSelectionEdit"); - return attrEdit; -} - -void UpdateBitSelection(UIElement@ parent) -{ - int mask = 0; - for (int i = 0; i < MAX_BITMASK_BITS; i++) - { - CheckBox@ bitBox = parent.GetChild("BitSelect_" + String(i), true); - mask = mask | (bitBox.checked ? 1 << i : 0); - } - - if (mask == MAX_BITMASK_VALUE) - mask = -1; - - inUpdateBitSelection = true; - LineEdit@ attrEdit = parent.parent.GetChild("LineEdit", true); - attrEdit.text = String(mask); - inUpdateBitSelection = false; -} - -void SetBitSelection(UIElement@ parent, int value) -{ - int mask = value; - bool enabled = true; - - if (mask == -1) - mask = MAX_BITMASK_VALUE; - else if (mask > MAX_BITMASK_VALUE) - enabled = false; - - for (int i = 0; i < MAX_BITMASK_BITS; i++) - { - CheckBox@ bitBox = parent.GetChild("BitSelect_" + String(i), true); - bitBox.enabled = enabled; - if (!enabled) - bitBox.color = nonEditableBitSelectorColor; - else - bitBox.color = editableBitSelectorColor; - - if ((1 << i) & mask != 0) - bitBox.checked = true; - else - bitBox.checked = false; - } -} - -void HandleBitSelectionToggled(StringHash eventType, VariantMap& eventData) -{ - if (inUpdateBitSelection) - return; - - CheckBox@ bitBox = eventData["Element"].GetPtr(); - - UpdateBitSelection(bitBox.parent); -} - -void HandleBitSelectionEdit(StringHash eventType, VariantMap& eventData) -{ - if (!inUpdateBitSelection) - { - LineEdit@ attrEdit = eventData["Element"].GetPtr(); - - inUpdateBitSelection = true; - SetBitSelection(attrEdit.parent, attrEdit.text.ToI32()); - inUpdateBitSelection = false; - } - - EditAttribute(eventType, eventData); -} - -UIElement@ CreateStringAttributeEditor(ListView@ list, Array@ serializables, const AttributeInfo&in info, uint index, uint subIndex) -{ - UIElement@ parent = CreateAttributeEditorParent(list, info.name, index, subIndex); - LineEdit@ attrEdit = CreateAttributeLineEdit(parent, serializables, index, subIndex); - attrEdit.dragDropMode = DD_TARGET; - // Do not subscribe to continuous edits of certain attributes (script class names) to prevent unnecessary errors getting printed - if (noTextChangedAttrs.Find(info.name) == -1) - SubscribeToEvent(attrEdit, "TextChanged", "EditAttribute"); - SubscribeToEvent(attrEdit, "TextFinished", "EditAttribute"); - - return parent; -} - -UIElement@ CreateBoolAttributeEditor(ListView@ list, Array@ serializables, const AttributeInfo&in info, uint index, uint subIndex) -{ - bool isUIElement = cast(serializables[0]) !is null; - UIElement@ parent; - if (info.name == (isUIElement ? "Is Visible" : "Is Enabled")) - parent = CreateAttributeEditorParentAsListChild(list, info.name, index, subIndex); - else - parent = CreateAttributeEditorParent(list, info.name, index, subIndex); - - CheckBox@ attrEdit = CheckBox(); - parent.AddChild(attrEdit); - attrEdit.style = AUTO_STYLE; - attrEdit.vars["Index"] = index; - attrEdit.vars["SubIndex"] = subIndex; - SetAttributeEditorID(attrEdit, serializables); - SubscribeToEvent(attrEdit, "Toggled", "EditAttribute"); - - return parent; -} - -UIElement@ CreateNumAttributeEditor(ListView@ list, Array@ serializables, const AttributeInfo&in info, uint index, uint subIndex) -{ - UIElement@ parent = CreateAttributeEditorParent(list, info.name, index, subIndex); - VariantType type = info.type; - uint numCoords = 1; - if (type == VAR_VECTOR2 || type == VAR_INTVECTOR2) - numCoords = 2; - if (type == VAR_VECTOR3 || type == VAR_INTVECTOR3 || type == VAR_QUATERNION) - numCoords = 3; - else if (type == VAR_VECTOR4 || type == VAR_COLOR || type == VAR_INTRECT || type == VAR_RECT) - numCoords = 4; - - for (uint i = 0; i < numCoords; ++i) - { - LineEdit@ attrEdit = CreateAttributeLineEdit(parent, serializables, index, subIndex); - attrEdit.vars["Coordinate"] = i; - - CreateDragSlider(attrEdit); - - SubscribeToEvent(attrEdit, "TextChanged", "EditAttribute"); - SubscribeToEvent(attrEdit, "TextFinished", "EditAttribute"); - } - - return parent; -} - -UIElement@ CreateIntAttributeEditor(ListView@ list, Array@ serializables, const AttributeInfo&in info, uint index, uint subIndex) -{ - UIElement@ parent = CreateAttributeEditorParent(list, info.name, index, subIndex); - - // Check for masks and layers - if (bitSelectionAttrs.Find(info.name) > -1) - { - LineEdit@ attrEdit = CreateAttributeBitSelector(parent, serializables, index, subIndex); - } - // Check for enums - else if (info.enumNames is null || info.enumNames.empty) - { - // No enums, create a numeric editor - LineEdit@ attrEdit = CreateAttributeLineEdit(parent, serializables, index, subIndex); - CreateDragSlider(attrEdit); - // If the attribute is a counter for things like billboards or animation states, disable apply at each change - if (info.name.Find(" Count", 0, false) == String::NPOS) - SubscribeToEvent(attrEdit, "TextChanged", "EditAttribute"); - SubscribeToEvent(attrEdit, "TextFinished", "EditAttribute"); - // If the attribute is a node ID, make it a drag/drop target - if (info.name.Contains("NodeID", false) || info.name.Contains("Node ID", false) || (info.mode & AM_NODEID) != 0) - attrEdit.dragDropMode = DD_TARGET; - } - else - { - DropDownList@ attrEdit = DropDownList(); - parent.AddChild(attrEdit); - attrEdit.style = AUTO_STYLE; - attrEdit.SetFixedHeight(ATTR_HEIGHT - 2); - attrEdit.resizePopup = true; - attrEdit.placeholderText = STRIKED_OUT; - attrEdit.vars["Index"] = index; - attrEdit.vars["SubIndex"] = subIndex; - attrEdit.SetLayout(LM_HORIZONTAL, 0, IntRect(4, 1, 4, 1)); - SetAttributeEditorID(attrEdit, serializables); - - for (uint i = 0; i < info.enumNames.length; ++i) - { - Text@ choice = Text(); - attrEdit.AddItem(choice); - choice.style = "EditorEnumAttributeText"; - choice.text = info.enumNames[i]; - } - SubscribeToEvent(attrEdit, "ItemSelected", "EditAttribute"); - } - - return parent; -} - -UIElement@ CreateResourceRefAttributeEditor(ListView@ list, Array@ serializables, const AttributeInfo&in info, uint index, uint subIndex, bool suppressedSeparatedLabel = false) -{ - UIElement@ parent; - StringHash resourceType; - - // Get the real attribute info from the serializable for the correct resource type - AttributeInfo attrInfo = serializables[0].attributeInfos[index]; - if (attrInfo.type == VAR_RESOURCEREF) - resourceType = serializables[0].attributes[index].GetResourceRef().type; - else if (attrInfo.type == VAR_RESOURCEREFLIST) - resourceType = serializables[0].attributes[index].GetResourceRefList().type; - else if (attrInfo.type == VAR_VARIANTVECTOR) - resourceType = serializables[0].attributes[index].GetVariantVector()[subIndex].GetResourceRef().type; - - ResourcePicker@ picker = GetResourcePicker(resourceType); - - // Create the attribute name on a separate non-interactive line to allow for more space - parent = CreateAttributeEditorParentWithSeparatedLabel(list, info.name, index, subIndex, suppressedSeparatedLabel); - - UIElement@ container = UIElement(); - container.SetLayout(LM_HORIZONTAL, 4, IntRect(info.name.StartsWith(" ") ? 20 : 10, 0, 4, 0)); // Left margin is indented more when the name is so - container.SetFixedHeight(ATTR_HEIGHT); - parent.AddChild(container); - - LineEdit@ attrEdit = CreateAttributeLineEdit(container, serializables, index, subIndex); - attrEdit.vars[TYPE_VAR] = resourceType.value; - SubscribeToEvent(attrEdit, "TextFinished", "EditAttribute"); - - if (picker !is null) - { - if ((picker.actions & ACTION_PICK) != 0) - { - Button@ pickButton = CreateResourcePickerButton(container, serializables, index, subIndex, "smallButtonPick"); - SubscribeToEvent(pickButton, "Released", "PickResource"); - } - if ((picker.actions & ACTION_OPEN) != 0) - { - Button@ openButton = CreateResourcePickerButton(container, serializables, index, subIndex, "smallButtonOpen"); - SubscribeToEvent(openButton, "Released", "OpenResource"); - } - if ((picker.actions & ACTION_EDIT) != 0) - { - Button@ editButton = CreateResourcePickerButton(container, serializables, index, subIndex, "smallButtonEdit"); - SubscribeToEvent(editButton, "Released", "EditResource"); - } - if ((picker.actions & ACTION_TEST) != 0) - { - Button@ testButton = CreateResourcePickerButton(container, serializables, index, subIndex, "smallButtonTest"); - SubscribeToEvent(testButton, "Released", "TestResource"); - } - } - - return parent; -} - -Button@ CreateResourcePickerButton(UIElement@ container, Array@ serializables, uint index, uint subIndex, const String&in text) -{ - Button@ button = Button(); - container.AddChild(button); - button.style = AUTO_STYLE; - button.SetFixedSize(36, ATTR_HEIGHT - 2); - button.vars["Index"] = index; - button.vars["SubIndex"] = subIndex; - SetAttributeEditorID(button, serializables); - - Text@ buttonText = Text(); - button.AddChild(buttonText); - buttonText.style = "EditorAttributeText"; - buttonText.SetAlignment(HA_CENTER, VA_CENTER); - buttonText.text = text; - buttonText.autoLocalizable = true; - - return button; -} - -// Use internally for nested variant vector -uint nestedSubIndex; -Array@ nestedVector; - -UIElement@ CreateAttributeEditor(ListView@ list, Array@ serializables, const AttributeInfo&in info, uint index, uint subIndex, bool suppressedSeparatedLabel = false) -{ - UIElement@ parent; - - VariantType type = info.type; - if (type == VAR_STRING || type == VAR_BUFFER) - parent = CreateStringAttributeEditor(list, serializables, info, index, subIndex); - else if (type == VAR_BOOL) - parent = CreateBoolAttributeEditor(list, serializables, info, index, subIndex); - else if ((type >= VAR_FLOAT && type <= VAR_VECTOR4) || type == VAR_QUATERNION || type == VAR_COLOR || type == VAR_INTVECTOR2 || type == VAR_INTVECTOR3 || type == VAR_INTRECT || type == VAR_DOUBLE || type == VAR_RECT) - parent = CreateNumAttributeEditor(list, serializables, info, index, subIndex); - else if (type == VAR_INT) - parent = CreateIntAttributeEditor(list, serializables, info, index, subIndex); - else if (type == VAR_RESOURCEREF) - parent = CreateResourceRefAttributeEditor(list, serializables, info, index, subIndex, suppressedSeparatedLabel); - else if (type == VAR_RESOURCEREFLIST) - { - uint numRefs = serializables[0].attributes[index].GetResourceRefList().length; - - // Straightly speaking the individual resource reference in the list is not an attribute of the serializable - // However, the AttributeInfo structure is used here to reduce the number of parameters being passed in the function - AttributeInfo refInfo; - refInfo.name = info.name; - refInfo.type = VAR_RESOURCEREF; - for (uint i = 0; i < numRefs; ++i) - CreateAttributeEditor(list, serializables, refInfo, index, i, i > 0); - } - else if (type == VAR_VARIANTVECTOR) - { - uint nameIndex = 0; - uint repeat = M_MAX_UNSIGNED; - - VectorStruct@ vectorStruct; - Array@ vector; - bool emptyNestedVector = false; - if (info.name.Contains('>')) - { - @vector = @nestedVector; - vectorStruct = GetNestedVectorStruct(serializables, info.name); - repeat = vector[subIndex].GetU32(); // Nested VariantVector must have a predefined repeat count at the start of the vector - emptyNestedVector = repeat == 0; - } - else - { - @vector = serializables[0].attributes[index].GetVariantVector(); - vectorStruct = GetVectorStruct(serializables, index); - subIndex = 0; - } - if (vectorStruct is null) - return null; - - for (uint i = subIndex; i < vector.length; ++i) - { - // The individual variant in the vector is not an attribute of the serializable, the structure is reused for convenience - AttributeInfo vectorInfo; - vectorInfo.name = vectorStruct.variableNames[nameIndex++]; - bool nested = vectorInfo.name.Contains('>'); - if (nested) - { - vectorInfo.type = VAR_VARIANTVECTOR; - @nestedVector = @vector; - } - else - vectorInfo.type = vector[i].type; - CreateAttributeEditor(list, serializables, vectorInfo, index, i); - if (nested) - { - i = nestedSubIndex; - @nestedVector = null; - } - if (emptyNestedVector) - { - nestedSubIndex = i; - break; - } - if (nameIndex >= vectorStruct.variableNames.length) - { - if (--repeat == 0) - { - nestedSubIndex = i; - break; - } - nameIndex = vectorStruct.restartIndex; - - // Create small divider for repeated instances - UIElement@ divider = UIElement(); - divider.SetFixedHeight(8); - list.AddItem(divider); - } - } - } - else if (type == VAR_VARIANTMAP) - { - VariantMap map = serializables[0].attributes[index].GetVariantMap(); - Array@ keys = map.keys; - for (uint i = 0; i < keys.length; ++i) - { - String varName = GetVarName(keys[i]); - bool shouldHide = false; - - if (varName.empty) - { - // UIElements will contain internal vars, which do not have known mappings. Hide these - if (cast(serializables[0]) !is null) - shouldHide = true; - // Else, for scene nodes, show as hexadecimal hashes if nothing else is available - varName = keys[i].ToString(); - } - Variant value = map[keys[i]]; - - // The individual variant in the map is not an attribute of the serializable, the structure is reused for convenience - AttributeInfo mapInfo; - mapInfo.name = varName + " (Var)"; - mapInfo.type = value.type; - parent = CreateAttributeEditor(list, serializables, mapInfo, index, i); - // Add the variant key to the parent. We may fail to add the editor in case it is unsupported - if (parent !is null) - { - parent.vars["Key"] = keys[i].value; - // If variable name is not registered (i.e. it is an editor internal variable) then hide it - if (varName.empty || shouldHide) - parent.visible = false; - } - } - } - - return parent; -} - -uint GetAttributeEditorCount(Array@ serializables) -{ - uint count = 0; - - if (!serializables.empty) - { - /// \todo When multi-editing, this only counts the editor count of the first serializable - bool isUIElement = cast(serializables[0]) !is null; - for (uint i = 0; i < serializables[0].numAttributes; ++i) - { - AttributeInfo info = serializables[0].attributeInfos[i]; - if (!showNonEditableAttribute && info.mode & AM_NOEDIT != 0) - continue; - // "Is Enabled" is not inserted into the main attribute list, so do not count - // Similarly, for UIElement, "Is Visible" is not inserted - if (info.name == (isUIElement ? "Is Visible" : "Is Enabled")) - continue; - // Tags are also handled separately - if (info.name == "Tags") - continue; - if (info.type == VAR_RESOURCEREFLIST) - count += serializables[0].attributes[i].GetResourceRefList().length; - else if (info.type == VAR_VARIANTVECTOR && GetVectorStruct(serializables, i) !is null) - count += serializables[0].attributes[i].GetVariantVector().length; - else if (info.type == VAR_VARIANTMAP) - count += serializables[0].attributes[i].GetVariantMap().length; - else - ++count; - } - } - - return count; -} - -UIElement@ GetAttributeEditorParent(UIElement@ parent, uint index, uint subIndex) -{ - return parent.GetChild("Edit" + String(index) + "_" + String(subIndex), true); -} - -void LoadAttributeEditor(ListView@ list, Array@ serializables, const AttributeInfo&in info, uint index) -{ - bool editable = info.mode & AM_NOEDIT == 0; - - UIElement@ parent = GetAttributeEditorParent(list, index, 0); - if (parent is null) - return; - - inLoadAttributeEditor = true; - - bool sameName = true; - bool sameValue = true; - Variant value = serializables[0].attributes[index]; - Array values; - for (uint i = 0; i < serializables.length; ++i) - { - if (index >= serializables[i].numAttributes || serializables[i].attributeInfos[index].name != info.name) - { - sameName = false; - break; - } - - Variant val = serializables[i].attributes[index]; - if (val != value) - sameValue = false; - values.Push(val); - } - - // Attribute with different values from multiple-select is loaded with default/empty value and non-editable - if (sameName) - LoadAttributeEditor(parent, value, info, editable, sameValue, values); - else - parent.visible = false; - - inLoadAttributeEditor = false; -} - -void LoadAttributeEditor(UIElement@ parent, const Variant&in value, const AttributeInfo&in info, bool editable, bool sameValue, const Array&in values) -{ - uint index = parent.vars["Index"].GetU32(); - - // Assume the first child is always a text label element or a container that containing a text label element - UIElement@ label = parent.children[0]; - if (label.type == UI_ELEMENT_TYPE && label.numChildren > 0) - label = label.children[0]; - if (label.type == TEXT_TYPE) - { - bool modified; - if (info.defaultValue.type == VAR_NONE || info.defaultValue.type == VAR_RESOURCEREFLIST) - modified = !value.zero; - else - modified = value != info.defaultValue; - cast(label).color = (editable ? (modified ? modifiedTextColor : normalTextColor) : nonEditableTextColor); - } - - VariantType type = info.type; - if (type == VAR_FLOAT || type == VAR_DOUBLE || type == VAR_STRING || type == VAR_BUFFER) - SetEditable(SetValue(parent.children[1], value.ToString(), sameValue), editable && sameValue); - else if (type == VAR_BOOL) - SetEditable(SetValue(parent.children[1], value.GetBool(), sameValue), editable && sameValue); - else if (type == VAR_INT) - { - if (bitSelectionAttrs.Find(info.name) > -1) - SetEditable(SetValue(parent.GetChild("LineEdit", true), value.ToString(), sameValue), editable && sameValue); - else if (info.enumNames is null || info.enumNames.empty) - SetEditable(SetValue(parent.children[1], value.ToString(), sameValue), editable && sameValue); - else - SetEditable(SetValue(parent.children[1], value.GetI32(), sameValue), editable && sameValue); - } - else if (type == VAR_RESOURCEREF) - { - SetEditable(SetValue(parent.children[1].children[0], value.GetResourceRef().name, sameValue), editable && sameValue); - SetEditable(parent.children[1].children[1], editable && sameValue); // If editable then can pick - for (uint i = 2; i < parent.children[1].numChildren; ++i) - SetEditable(parent.children[1].children[i], sameValue); // If same value then can open/edit/test - } - else if (type == VAR_RESOURCEREFLIST) - { - UIElement@ list = parent.parent; - ResourceRefList refList = value.GetResourceRefList(); - for (uint subIndex = 0; subIndex < refList.length; ++subIndex) - { - parent = GetAttributeEditorParent(list, index, subIndex); - if (parent is null) - break; - - String firstName = refList.names[subIndex]; - bool nameSameValue = true; - if (!sameValue) - { - // Reevaluate each name in the list - for (uint i = 0; i < values.length; ++i) - { - ResourceRefList rList = values[i].GetResourceRefList(); - if (subIndex >= rList.length || rList.names[subIndex] != firstName) - { - nameSameValue = false; - break; - } - } - } - SetEditable(SetValue(parent.children[1].children[0], firstName, nameSameValue), editable && nameSameValue); - } - } - else if (type == VAR_VARIANTVECTOR) - { - UIElement@ list = parent.parent; - Array@ vector = value.GetVariantVector(); - for (uint subIndex = 0; subIndex < vector.length; ++subIndex) - { - parent = GetAttributeEditorParent(list, index, subIndex); - if (parent is null) - break; - - Variant firstValue = vector[subIndex]; - bool sameVal = true; - Array varValues; - - // Reevaluate each variant in the vector - for (uint i = 0; i < values.length; ++i) - { - Array@ vec = values[i].GetVariantVector(); - if (subIndex < vec.length) - { - Variant v = vec[subIndex]; - varValues.Push(v); - if (v != firstValue) - sameVal = false; - } - else - sameVal = false; - } - - // The individual variant in the list is not an attribute of the serializable, the structure is reused for convenience - AttributeInfo ai; - ai.type = firstValue.type; - LoadAttributeEditor(parent, firstValue, ai, editable, sameVal, varValues); - } - } - else if (type == VAR_VARIANTMAP) - { - UIElement@ list = parent.parent; - VariantMap map = value.GetVariantMap(); - Array@ keys = map.keys; - for (uint subIndex = 0; subIndex < keys.length; ++subIndex) - { - parent = GetAttributeEditorParent(list, index, subIndex); - if (parent is null) - break; - - String varName = GetVarName(keys[subIndex]); - if (varName.empty) - varName = keys[subIndex].ToString(); // Use hexadecimal if nothing else is available - - Variant firstValue = map[keys[subIndex]]; - bool sameVal = true; - Array varValues; - - // Reevaluate each variant in the map - for (uint i = 0; i < values.length; ++i) - { - VariantMap m = values[i].GetVariantMap(); - if (m.Contains(keys[subIndex])) - { - Variant v = m[keys[subIndex]]; - varValues.Push(v); - if (v != firstValue) - sameVal = false; - } - else - sameVal = false; - } - - // The individual variant in the map is not an attribute of the serializable, the structure is reused for convenience - AttributeInfo ai; - ai.type = firstValue.type; - LoadAttributeEditor(parent, firstValue, ai, editable, sameVal, varValues); - } - } - else - { - Array > coordinates; - for (uint i = 0; i < values.length; ++i) - { - Variant v = values[i]; - - // Convert Quaternion value to Vector3 value first - if (type == VAR_QUATERNION) - v = v.GetQuaternion().eulerAngles; - - coordinates.Push(v.ToString().Split(' ')); - } - for (uint i = 0; i < coordinates[0].length; ++i) - { - String str = coordinates[0][i]; - bool coordinateSameValue = true; - if (!sameValue) - { - // Reevaluate each coordinate - for (uint j = 1; j < coordinates.length; ++j) - { - if (coordinates[j][i] != str) - { - coordinateSameValue = false; - break; - } - } - } - SetEditable(SetValue(parent.children[i + 1], str, coordinateSameValue), editable && coordinateSameValue); - } - } -} - -void StoreAttributeEditor(UIElement@ parent, Array@ serializables, uint index, uint subIndex, uint coordinate) -{ - AttributeInfo info = serializables[0].attributeInfos[index]; - - if (info.type == VAR_RESOURCEREFLIST) - { - for (uint i = 0; i < serializables.length; ++i) - { - ResourceRefList refList = serializables[i].attributes[index].GetResourceRefList(); - Variant[] values(1); - GetEditorValue(parent, VAR_RESOURCEREF, null, coordinate, values); - ResourceRef ref = values[0].GetResourceRef(); - refList.names[subIndex] = ref.name; - serializables[i].attributes[index] = Variant(refList); - } - } - else if (info.type == VAR_VARIANTVECTOR) - { - for (uint i = 0; i < serializables.length; ++i) - { - Array@ vector = serializables[i].attributes[index].GetVariantVector(); - Variant[] values; - values.Push(vector[subIndex]); // Each individual variant may have multiple coordinates itself - GetEditorValue(parent, vector[subIndex].type, null, coordinate, values); - vector[subIndex] = values[0]; - serializables[i].attributes[index] = Variant(vector); - } - } - else if (info.type == VAR_VARIANTMAP) - { - StringHash key(parent.vars["Key"].GetU32()); - for (uint i = 0; i < serializables.length; ++i) - { - VariantMap map = serializables[i].attributes[index].GetVariantMap(); - Variant[] values; - values.Push(map[key]); // Each individual variant may have multiple coordinates itself - GetEditorValue(parent, map[key].type, null, coordinate, values); - map[key] = values[0]; - serializables[i].attributes[index] = Variant(map); - } - } - else - { - Array values; - for (uint i = 0; i < serializables.length; ++i) - values.Push(serializables[i].attributes[index]); - GetEditorValue(parent, info.type, info.enumNames, coordinate, values); - for (uint i = 0; i < serializables.length; ++i) - serializables[i].attributes[index] = values[i]; - } -} - -void FillValue(Array& values, const Variant&in value) -{ - for (uint i = 0; i < values.length; ++i) - values[i] = value; -} - -void SanitizeNumericalValue(VariantType type, String& value) -{ - if ((type >= VAR_FLOAT && type <= VAR_COLOR) || type == VAR_RECT) - value = String(value.ToFloat()); - else if (type == VAR_INT || type == VAR_INTRECT || type == VAR_INTVECTOR2 || type == VAR_INTVECTOR3) - value = String(value.ToI32()); - else if (type == VAR_DOUBLE) - value = String(value.ToDouble()); -} - -void GetEditorValue(UIElement@ parent, VariantType type, Array@ enumNames, uint coordinate, Array& values) -{ - LineEdit@ attrEdit = parent.children[coordinate + 1]; - - if (attrEdit is null) - attrEdit = parent.GetChild("LineEdit", true); - - if (type == VAR_STRING) - FillValue(values, Variant(attrEdit.text.Trimmed())); - else if (type == VAR_BOOL) - { - CheckBox@ cb = parent.children[1]; - FillValue(values, Variant(cb.checked)); - } - else if (type == VAR_FLOAT) - FillValue(values, Variant(attrEdit.text.ToFloat())); - else if (type == VAR_DOUBLE) - FillValue(values, Variant(attrEdit.text.ToDouble())); - else if (type == VAR_QUATERNION) - { - float value = attrEdit.text.ToFloat(); - for (uint i = 0; i < values.length; ++i) - { - float[] data = values[i].GetQuaternion().eulerAngles.data; - data[coordinate] = value; - values[i] = Quaternion(Vector3(data)); - } - } - else if (type == VAR_INT) - { - if (enumNames is null || enumNames.empty) - FillValue(values, Variant(attrEdit.text.ToI32())); - else - { - DropDownList@ ddl = parent.children[1]; - FillValue(values, Variant(ddl.selection)); - } - } - else if (type == VAR_RESOURCEREF) - { - LineEdit@ le = parent.children[0]; - ResourceRef ref; - ref.name = le.text.Trimmed(); - ref.type = StringHash(le.vars[TYPE_VAR].GetU32()); - FillValue(values, Variant(ref)); - } - else - { - String value = attrEdit.text; - SanitizeNumericalValue(type, value); - for (uint i = 0; i < values.length; ++i) - { - String[] data = values[i].ToString().Split(' '); - data[coordinate] = value; - values[i] = Variant(type, Join(data, " ")); - } - } -} - -void UpdateAttributes(Array@ serializables, ListView@ list, bool& fullUpdate) -{ - // If attributes have changed structurally, do a full update - uint count = GetAttributeEditorCount(serializables); - if (fullUpdate == false) - { - if (list.contentElement.numChildren != count) - fullUpdate = true; - } - - // Remember the old scroll position so that a full update does not feel as jarring - IntVector2 oldViewPos = list.viewPosition; - - if (fullUpdate) - { - list.RemoveAllItems(); - Array children = list.GetChildren(); - for (uint i = 0; i < children.length; ++i) - { - if (!children[i].internal) - children[i].Remove(); - } - } - - if (serializables.empty) - return; - - // If there are many serializables, they must share same attribute structure (up to certain number if not all) - for (uint i = 0; i < serializables[0].numAttributes; ++i) - { - AttributeInfo info = serializables[0].attributeInfos[i]; - if (!showNonEditableAttribute && info.mode & AM_NOEDIT != 0) - continue; - - // Use the default value (could be instance's default value) of the first serializable as the default for all - info.defaultValue = serializables[0].attributeDefaults[i]; - - if (fullUpdate) - CreateAttributeEditor(list, serializables, info, i, 0); - - LoadAttributeEditor(list, serializables, info, i); - } - - if (fullUpdate) - list.viewPosition = oldViewPos; -} - -void CreateDragSlider(LineEdit@ parent) -{ - Button@ dragSld = Button(); - dragSld.style = "EditorDragSlider"; - dragSld.SetFixedHeight(ATTR_HEIGHT - 3); - dragSld.SetFixedWidth(dragSld.height); - dragSld.SetAlignment(HA_RIGHT, VA_TOP); - dragSld.focusMode = FM_NOTFOCUSABLE; - parent.AddChild(dragSld); - - SubscribeToEvent(dragSld, "DragBegin", "LineDragBegin"); - SubscribeToEvent(dragSld, "DragMove", "LineDragMove"); - SubscribeToEvent(dragSld, "DragEnd", "LineDragEnd"); - SubscribeToEvent(dragSld, "DragCancel", "LineDragCancel"); -} - -void EditAttribute(StringHash eventType, VariantMap& eventData) -{ - // Changing elements programmatically may cause events to be sent. Stop possible infinite loop in that case. - if (inLoadAttributeEditor) - return; - - UIElement@ attrEdit = eventData["Element"].GetPtr(); - UIElement@ parent = attrEdit.parent; - Array@ serializables = GetAttributeEditorTargets(attrEdit); - if (serializables.empty) - return; - - uint index = attrEdit.vars["Index"].GetU32(); - uint subIndex = attrEdit.vars["SubIndex"].GetU32(); - uint coordinate = attrEdit.vars["Coordinate"].GetU32(); - bool intermediateEdit = eventType == TEXT_CHANGED_EVENT_TYPE; - - // Do the editor pre logic before attribute is being modified - if (!PreEditAttribute(serializables, index)) - return; - - inEditAttribute = true; - - Array oldValues; - - if (!dragEditAttribute) - { - // Store old values so that PostEditAttribute can create undo actions - for (uint i = 0; i < serializables.length; ++i) - oldValues.Push(serializables[i].attributes[index]); - } - - StoreAttributeEditor(parent, serializables, index, subIndex, coordinate); - for (uint i = 0; i < serializables.length; ++i) - serializables[i].ApplyAttributes(); - - if (!dragEditAttribute) - { - // Do the editor post logic after attribute has been modified. - PostEditAttribute(serializables, index, oldValues); - } - - inEditAttribute = false; - - // If not an intermediate edit, reload the editor fields with validated values - // (attributes may have interactions; therefore we load everything, not just the value being edited) - if (!intermediateEdit) - attributesDirty = true; -} - -void LineDragBegin(StringHash eventType, VariantMap& eventData) -{ - UIElement@ label = eventData["Element"].GetPtr(); - int x = eventData["X"].GetI32(); - label.vars["posX"] = x; - - // Store the old value before dragging - dragEditAttribute = false; - LineEdit@ selectedNumEditor = label.parent; - - selectedNumEditor.vars["DragBeginValue"] = selectedNumEditor.text; - selectedNumEditor.cursorPosition = 0; - - // Set mouse mode to user preference - SetMouseMode(true); -} - -void LineDragMove(StringHash eventTypem, VariantMap& eventData) -{ - UIElement@ label = eventData["Element"].GetPtr(); - LineEdit@ selectedNumEditor = label.parent; - - // Prevent undo - dragEditAttribute = true; - - int x = eventData["X"].GetI32(); - int posx = label.vars["posX"].GetI32(); - float val = input.mouseMoveX; - - float fieldVal = selectedNumEditor.text.ToFloat(); - fieldVal += val/100; - label.vars["posX"] = x; - selectedNumEditor.text = fieldVal; - selectedNumEditor.cursorPosition = 0; -} - -void LineDragEnd(StringHash eventType, VariantMap& eventData) -{ - UIElement@ label = eventData["Element"].GetPtr(); - LineEdit@ selectedNumEditor = label.parent; - - // Prepare the attributes to store an undo with: - // - old value = drag begin value - // - new value = final value - - String finalValue = selectedNumEditor.text; - // Reset attribute to begin value, and prevent undo - dragEditAttribute = true; - selectedNumEditor.text = selectedNumEditor.vars["DragBeginValue"].GetString(); - - // Store final value, allow undo - dragEditAttribute = false; - selectedNumEditor.text = finalValue; - selectedNumEditor.cursorPosition = 0; - - // Revert mouse to normal behaviour - SetMouseMode(false); -} - -void LineDragCancel(StringHash eventType, VariantMap& eventData) -{ - UIElement@ label = eventData["Element"].GetPtr(); - - // Reset value to what it was when drag edit began, preventing undo. - dragEditAttribute = true; - LineEdit@ selectedNumEditor = label.parent; - selectedNumEditor.text = selectedNumEditor.vars["DragBeginValue"].GetString(); - selectedNumEditor.cursorPosition = 0; - - // Revert mouse to normal behaviour - SetMouseMode(false); -} - -// Resource picker functionality -const uint ACTION_PICK = 1; -const uint ACTION_OPEN = 2; -const uint ACTION_EDIT = 4; -const uint ACTION_TEST = 8; - -class ResourcePicker -{ - String typeName; - StringHash type; - String lastPath; - uint lastFilter; - Array filters; - uint actions; - - ResourcePicker(const String&in typeName_, const String&in filter_, uint actions_ = ACTION_PICK | ACTION_OPEN) - { - typeName = typeName_; - type = StringHash(typeName_); - actions = actions_; - filters.Push(filter_); - filters.Push("*.*"); - lastFilter = 0; - } - - ResourcePicker(const String&in typeName_, const Array@ filters_, uint actions_ = ACTION_PICK | ACTION_OPEN) - { - typeName = typeName_; - type = StringHash(typeName_); - filters = filters_; - actions = actions_; - filters.Push("*.*"); - lastFilter = 0; - } -}; - -Array resourcePickers; -Array resourceTargets; -uint resourcePickIndex = 0; -uint resourcePickSubIndex = 0; -ResourcePicker@ resourcePicker = null; - -void InitResourcePicker() -{ - // Fill resource picker data - Array fontFilters = {"*.ttf", "*.otf", "*.fnt", "*.xml", "*.sdf"}; - Array imageFilters = {"*.png", "*.jpg", "*.bmp", "*.tga", "*.hdr"}; - Array luaFileFilters = {"*.lua", "*.luc"}; - Array scriptFilters = {"*.as", "*.asc"}; - Array soundFilters = {"*.wav","*.ogg"}; - Array textureFilters = {"*.dds", "*.png", "*.jpg", "*.bmp", "*.tga", "*.ktx", "*.pvr", "*.hdr"}; - Array materialFilters = {"*.xml", "*.material", "*.json"}; - Array anmSetFilters = {"*.scml"}; - Array pexFilters = {"*.pex"}; - Array tmxFilters = {"*.tmx"}; - resourcePickers.Push(ResourcePicker("Animation", "*.ani", ACTION_PICK | ACTION_TEST)); - resourcePickers.Push(ResourcePicker("Font", fontFilters)); - resourcePickers.Push(ResourcePicker("Image", imageFilters)); - resourcePickers.Push(ResourcePicker("LuaFile", luaFileFilters)); - resourcePickers.Push(ResourcePicker("Material", materialFilters, ACTION_PICK | ACTION_OPEN | ACTION_EDIT)); - resourcePickers.Push(ResourcePicker("Model", "*.mdl", ACTION_PICK)); - resourcePickers.Push(ResourcePicker("ParticleEffect", "*.xml", ACTION_PICK | ACTION_OPEN | ACTION_EDIT)); - resourcePickers.Push(ResourcePicker("ScriptFile", scriptFilters)); - resourcePickers.Push(ResourcePicker("Sound", soundFilters)); - resourcePickers.Push(ResourcePicker("Technique", "*.xml")); - resourcePickers.Push(ResourcePicker("Texture2D", textureFilters)); - resourcePickers.Push(ResourcePicker("TextureCube", "*.xml")); - resourcePickers.Push(ResourcePicker("Texture3D", "*.xml")); - resourcePickers.Push(ResourcePicker("XMLFile", "*.xml")); - resourcePickers.Push(ResourcePicker("JSONFile", "*.json")); - resourcePickers.Push(ResourcePicker("Sprite2D", textureFilters, ACTION_PICK | ACTION_OPEN)); - resourcePickers.Push(ResourcePicker("AnimationSet2D", anmSetFilters, ACTION_PICK | ACTION_OPEN)); - resourcePickers.Push(ResourcePicker("ParticleEffect2D", pexFilters, ACTION_PICK | ACTION_OPEN)); - resourcePickers.Push(ResourcePicker("TmxFile2D", tmxFilters, ACTION_PICK | ACTION_OPEN)); -} - -ResourcePicker@ GetResourcePicker(StringHash resourceType) -{ - for (uint i = 0; i < resourcePickers.length; ++i) - { - // TODO: refactor to use dictionary instead - if (resourceType == resourcePickers[i].type) - return resourcePickers[i]; - } - return null; -} - -void PickResource(StringHash eventType, VariantMap& eventData) -{ - UIElement@ button = eventData["Element"].GetPtr(); - LineEdit@ attrEdit = button.parent.children[0]; - - Array@ targets = GetAttributeEditorTargets(attrEdit); - if (targets.empty) - return; - - resourcePickIndex = attrEdit.vars["Index"].GetU32(); - resourcePickSubIndex = attrEdit.vars["SubIndex"].GetU32(); - AttributeInfo info = targets[0].attributeInfos[resourcePickIndex]; - - StringHash resourceType; - if (info.type == VAR_RESOURCEREF) - resourceType = targets[0].attributes[resourcePickIndex].GetResourceRef().type; - else if (info.type == VAR_RESOURCEREFLIST) - resourceType = targets[0].attributes[resourcePickIndex].GetResourceRefList().type; - else if (info.type == VAR_VARIANTVECTOR) - resourceType = targets[0].attributes[resourcePickIndex].GetVariantVector()[resourcePickSubIndex].GetResourceRef().type; - - @resourcePicker = GetResourcePicker(resourceType); - if (resourcePicker is null) - return; - - resourceTargets.Clear(); - for (uint i = 0; i < targets.length; ++i) - resourceTargets.Push(targets[i]); - - String lastPath = resourcePicker.lastPath; - if (lastPath.empty) - lastPath = sceneResourcePath; - CreateFileSelector(localization.Get("Pick ") + resourcePicker.typeName, "OK", "Cancel", lastPath, resourcePicker.filters, resourcePicker.lastFilter, false); - SubscribeToEvent(uiFileSelector, "FileSelected", "PickResourceDone"); -} - -void PickResourceDone(StringHash eventType, VariantMap& eventData) -{ - StoreResourcePickerPath(); - CloseFileSelector(); - - if (!eventData["OK"].GetBool()) - { - resourceTargets.Clear(); - @resourcePicker = null; - return; - } - - if (resourcePicker is null) - return; - - // Validate the resource. It must come from within a registered resource directory, and be loaded successfully - String resourceName = eventData["FileName"].GetString(); - Resource@ res = GetPickedResource(resourceName); - if (res is null) - { - @resourcePicker = null; - return; - } - - // Store old values so that PostEditAttribute can create undo actions - Array oldValues; - for (uint i = 0; i < resourceTargets.length; ++i) - oldValues.Push(resourceTargets[i].attributes[resourcePickIndex]); - - for (uint i = 0; i < resourceTargets.length; ++i) - { - Serializable@ target = resourceTargets[i]; - - AttributeInfo info = target.attributeInfos[resourcePickIndex]; - if (info.type == VAR_RESOURCEREF) - { - ResourceRef ref = target.attributes[resourcePickIndex].GetResourceRef(); - ref.type = res.type; - ref.name = res.name; - target.attributes[resourcePickIndex] = Variant(ref); - target.ApplyAttributes(); - } - else if (info.type == VAR_RESOURCEREFLIST) - { - ResourceRefList refList = target.attributes[resourcePickIndex].GetResourceRefList(); - if (resourcePickSubIndex < refList.length) - { - refList.names[resourcePickSubIndex] = res.name; - target.attributes[resourcePickIndex] = Variant(refList); - target.ApplyAttributes(); - } - } - else if (info.type == VAR_VARIANTVECTOR) - { - Array@ attrs = target.attributes[resourcePickIndex].GetVariantVector(); - ResourceRef ref = attrs[resourcePickSubIndex].GetResourceRef(); - ref.type = res.type; - ref.name = res.name; - attrs[resourcePickSubIndex] = ref; - target.attributes[resourcePickIndex] = Variant(attrs); - target.ApplyAttributes(); - } - } - - PostEditAttribute(resourceTargets, resourcePickIndex, oldValues); - UpdateAttributeInspector(false); - - resourceTargets.Clear(); - @resourcePicker = null; -} - -void StoreResourcePickerPath() -{ - // Store filter and directory for next time - if (resourcePicker !is null && uiFileSelector !is null) - { - resourcePicker.lastPath = uiFileSelector.path; - resourcePicker.lastFilter = uiFileSelector.filterIndex; - } -} - -Resource@ GetPickedResource(String resourceName) -{ - resourceName = GetResourceNameFromFullName(resourceName); - String type = resourcePicker.typeName; - // Cube and 3D textures both use .xml extension. In that case interrogate the proper resource type - // from the file itself - if (type == "Texture3D" || type == "TextureCube") - { - XMLFile@ xmlRes = cache.GetResource("XMLFile", resourceName); - if (xmlRes !is null) - { - if (xmlRes.root.name.Compare("cubemap", false) == 0 || xmlRes.root.name.Compare("texturecube", false) == 0) - type = "TextureCube"; - else if (xmlRes.root.name.Compare("texture3d", false) == 0) - type = "Texture3D"; - } - } - - Resource@ res = cache.GetResource(type, resourceName); - - if (res is null) - log.Warning("Cannot find resource type: " + type + " Name:" + resourceName); - - return res; -} - -String GetResourceNameFromFullName(const String&in resourceName) -{ - Array@ resourceDirs = cache.resourceDirs; - - for (uint i = 0; i < resourceDirs.length; ++i) - { - if (!resourceName.ToLower().StartsWith(resourceDirs[i].ToLower())) - continue; - return resourceName.Substring(resourceDirs[i].length); - } - - return ""; // Not found -} - -void OpenResource(StringHash eventType, VariantMap& eventData) -{ - UIElement@ button = eventData["Element"].GetPtr(); - LineEdit@ attrEdit = button.parent.children[0]; - - String fileName = attrEdit.text.Trimmed(); - if (fileName.empty) - return; - - OpenResource(fileName); -} - -void OpenResource(String fileName) -{ - Array@ resourceDirs = cache.resourceDirs; - for (uint i = 0; i < resourceDirs.length; ++i) - { - String fullPath = resourceDirs[i] + fileName; - if (fileSystem.FileExists(fullPath)) - { - fileSystem.SystemOpen(fullPath, ""); - return; - } - } -} - -void EditResource(StringHash eventType, VariantMap& eventData) -{ - UIElement@ button = eventData["Element"].GetPtr(); - LineEdit@ attrEdit = button.parent.children[0]; - - String fileName = attrEdit.text.Trimmed(); - if (fileName.empty) - return; - - StringHash resourceType(attrEdit.vars[TYPE_VAR].GetU32()); - Resource@ resource = cache.GetResource(resourceType, fileName); - - if (resource !is null) - { - // For now only Materials can be edited - if (resource.typeName == "Material") - EditMaterial(cast(resource)); - else if (resource.typeName == "ParticleEffect") - EditParticleEffect(cast(resource)); - } -} - -void TestResource(StringHash eventType, VariantMap& eventData) -{ - UIElement@ button = eventData["Element"].GetPtr(); - LineEdit@ attrEdit = button.parent.children[0]; - - StringHash resourceType(attrEdit.vars[TYPE_VAR].GetU32()); - - // For now only Animations can be tested - StringHash animType("Animation"); - if (resourceType == animType) - TestAnimation(attrEdit); -} - -void TestAnimation(UIElement@ attrEdit) -{ - // Note: only supports the AnimationState array in AnimatedModel, and if only 1 model selected - Array@ targets = GetAttributeEditorTargets(attrEdit); - if (targets.length != 1) - return; - AnimatedModel@ model = cast(targets[0]); - if (model is null) - return; - - uint animStateIndex = (attrEdit.vars["SubIndex"].GetU32() - 1) / 6; - if (testAnimState.Get() is null) - { - testAnimState = model.GetAnimationState(animStateIndex); - AnimationState@ animState = testAnimState.Get(); - if (animState !is null) - animState.time = 0; // Start from beginning - } - else - testAnimState = null; -} - -void UpdateTestAnimation(float timeStep) -{ - AnimationState@ animState = testAnimState.Get(); - if (animState !is null) - { - // If has also an AnimationController, and scene update is enabled, check if it is also driving the animation - // and skip in that case (avoid double speed animation) - if (runUpdate) - { - AnimatedModel@ model = animState.model; - if (model !is null) - { - Node@ node = model.node; - if (node !is null) - { - AnimationController@ ctrl = node.GetComponent("AnimationController"); - Animation@ anim = animState.animation; - if (ctrl !is null && anim !is null) - { - if (ctrl.IsPlaying(anim.name)) - return; - } - } - } - } - - animState.AddTime(timeStep); - } -} - -// VariantVector decoding & editing for certain components - -class VectorStruct -{ - String componentTypeName; - String attributeName; - Array variableNames; - uint restartIndex; - - VectorStruct(const String&in componentTypeName_, const String&in attributeName_, const Array@ variableNames_, uint restartIndex_) - { - componentTypeName = componentTypeName_; - attributeName = attributeName_; - variableNames = variableNames_; - restartIndex = restartIndex_; - } -}; - -Array vectorStructs; - -void InitVectorStructs() -{ - Array crowdManagerAreaCostVariables = { - " Area Count", - " Cost" - }; - vectorStructs.Push(VectorStruct("CrowdManager", " >AreaCost", crowdManagerAreaCostVariables, 1)); - - Array categories = GetObjectCategories(); - for (uint categoryIndex = 0; categoryIndex < categories.length; categoryIndex++) - { - Array objectsNames = GetObjectsByCategory(categories[categoryIndex]); - for (uint objectIndex = 0; objectIndex < objectsNames.length; objectIndex++) - { - String objectName = objectsNames[objectIndex]; - Array attributes = GetObjectAttributeInfos(objectName); - - for (uint attributeIndex = 0; attributeIndex < attributes.length; attributeIndex++) - { - AttributeInfo attribute = attributes[attributeIndex]; - if (attribute.type == VAR_VARIANTVECTOR and attribute.metadata.Contains("VectorStructElements")) - { - Array@ elementsNames = attribute.metadata["VectorStructElements"].GetStringVector(); - vectorStructs.Push(VectorStruct(objectName, attribute.name, elementsNames, 1)); - } - } - } - } -} - -VectorStruct@ GetVectorStruct(Array@ serializables, uint index) -{ - AttributeInfo info = serializables[0].attributeInfos[index]; - for (uint i = 0; i < vectorStructs.length; ++i) - { - if (vectorStructs[i].componentTypeName == serializables[0].typeName && vectorStructs[i].attributeName == info.name) - return vectorStructs[i]; - } - return null; -} - -VectorStruct@ GetNestedVectorStruct(Array@ serializables, const String&in name) -{ - for (uint i = 0; i < vectorStructs.length; ++i) - { - if (vectorStructs[i].componentTypeName == serializables[0].typeName && vectorStructs[i].attributeName == name) - return vectorStructs[i]; - } - return null; -} - -int GetAttributeIndex(Serializable@ serializable, const String&in attrName) -{ - for (uint i = 0; i < serializable.numAttributes; ++i) - { - if (serializable.attributeInfos[i].name.Compare(attrName, false) == 0) - return i; - } - - return -1; -} +// Attribute editor +// +// Functions that caller must implement: +// - void SetAttributeEditorID(UIElement@ attrEdit, Array@ serializables); +// - bool PreEditAttribute(Array@ serializables, uint index); +// - void PostEditAttribute(Array@ serializables, uint index, const Array& oldValues); +// - Array@ GetAttributeEditorTargets(UIElement@ attrEdit); +// - String GetVariableName(StringHash hash); + +const uint MIN_NODE_ATTRIBUTES = 4; +const uint MAX_NODE_ATTRIBUTES = 8; +const int ATTRNAME_WIDTH = 150; +const int ATTR_HEIGHT = 19; +const StringHash TEXT_CHANGED_EVENT_TYPE("TextChanged"); + +bool inLoadAttributeEditor = false; +bool inEditAttribute = false; +bool inUpdateBitSelection = false; +bool showNonEditableAttribute = false; + +Color normalTextColor(1.0f, 1.0f, 1.0f); +Color modifiedTextColor(1.0f, 0.8f, 0.5f); +Color nonEditableTextColor(0.7f, 0.7f, 0.7f); + +String sceneResourcePath = AddTrailingSlash(fileSystem.programDir + "Data"); +bool rememberResourcePath = true; + +// Exceptions for string attributes that should not be continuously edited +Array noTextChangedAttrs = {"Script File", "Class Name", "Script Object Type", "Script File Name"}; + +// List of attributes that should be created with a bit selection editor +const Array bitSelectionAttrs = {"Collision Mask", "Collision Layer", "Light Mask", "Zone Mask", "View Mask", "Shadow Mask"}; + +// Number of editable bits for bit selection editor +const int MAX_BITMASK_BITS = 8; +const int MAX_BITMASK_VALUE = (1 << MAX_BITMASK_BITS) - 1; +Color nonEditableBitSelectorColor(0.5f, 0.5f, 0.5f); +Color editableBitSelectorColor(1.0f, 1.0f, 1.0f); + +WeakHandle testAnimState; + +bool dragEditAttribute = false; + +UIElement@ SetEditable(UIElement@ element, bool editable) +{ + if (element is null) + return element; + + element.editable = editable; + element.colors[C_TOPLEFT] = editable ? element.colors[C_BOTTOMRIGHT] : nonEditableTextColor; + element.colors[C_BOTTOMLEFT] = element.colors[C_TOPLEFT]; + element.colors[C_TOPRIGHT] = element.colors[C_TOPLEFT]; + return element; +} + +UIElement@ SetValue(LineEdit@ element, const String&in value, bool sameValue) +{ + element.text = sameValue ? value : STRIKED_OUT; + element.cursorPosition = 0; + return element; +} + +UIElement@ SetValue(CheckBox@ element, bool value, bool sameValue) +{ + element.checked = sameValue ? value : false; + return element; +} + +UIElement@ SetValue(DropDownList@ element, int value, bool sameValue) +{ + element.selection = sameValue ? uint(value) : M_MAX_UNSIGNED; + return element; +} + +UIElement@ CreateAttributeEditorParentWithSeparatedLabel(ListView@ list, const String&in name, uint index, uint subIndex, bool suppressedSeparatedLabel = false) +{ + UIElement@ editorParent = UIElement("Edit" + String(index) + "_" + String(subIndex)); + editorParent.vars["Index"] = index; + editorParent.vars["SubIndex"] = subIndex; + editorParent.SetLayout(LM_VERTICAL, 2); + list.AddItem(editorParent); + + if (suppressedSeparatedLabel) + { + UIElement@ placeHolder = UIElement(name); + editorParent.AddChild(placeHolder); + } + else + { + Text@ attrNameText = Text(); + editorParent.AddChild(attrNameText); + attrNameText.style = "EditorAttributeText"; + attrNameText.text = name; + } + + return editorParent; +} + +UIElement@ CreateAttributeEditorParentAsListChild(ListView@ list, const String&in name, uint index, uint subIndex) +{ + UIElement@ editorParent = UIElement("Edit" + String(index) + "_" + String(subIndex)); + editorParent.vars["Index"] = index; + editorParent.vars["SubIndex"] = subIndex; + editorParent.SetLayout(LM_HORIZONTAL); + list.AddChild(editorParent); + + UIElement@ placeHolder = UIElement(name); + editorParent.AddChild(placeHolder); + + return editorParent; +} + +UIElement@ CreateAttributeEditorParent(ListView@ list, const String&in name, uint index, uint subIndex) +{ + UIElement@ editorParent = UIElement("Edit" + String(index) + "_" + String(subIndex)); + editorParent.vars["Index"] = index; + editorParent.vars["SubIndex"] = subIndex; + editorParent.SetLayout(LM_HORIZONTAL); + editorParent.SetFixedHeight(ATTR_HEIGHT); + list.AddItem(editorParent); + + Text@ attrNameText = Text(); + editorParent.AddChild(attrNameText); + attrNameText.style = "EditorAttributeText"; + attrNameText.text = name; + attrNameText.SetFixedWidth(ATTRNAME_WIDTH); + + return editorParent; +} + +LineEdit@ CreateAttributeLineEdit(UIElement@ parent, Array@ serializables, uint index, uint subIndex) +{ + LineEdit@ attrEdit = LineEdit(); + parent.AddChild(attrEdit); + attrEdit.dragDropMode = DD_TARGET; + attrEdit.style = "EditorAttributeEdit"; + attrEdit.SetFixedHeight(ATTR_HEIGHT - 2); + attrEdit.vars["Index"] = index; + attrEdit.vars["SubIndex"] = subIndex; + SetAttributeEditorID(attrEdit, serializables); + + return attrEdit; +} + +LineEdit@ CreateAttributeBitSelector(UIElement@ parent, Array@ serializables, uint index, uint subIndex) +{ + UIElement@ container = UIElement(); + parent.AddChild(container); + parent.SetFixedHeight(38); + container.SetFixedWidth(16 * 4 + 4); + + for (int i = 0; i < 2; i++) + { + for (int j = 0; j < 4; j++) + { + CheckBox@ bitBox = CheckBox(); + bitBox.name = "BitSelect_" + String(i * 4 + j); + container.AddChild(bitBox); + bitBox.position = IntVector2(16 * j, 16 * i); + bitBox.style = "CheckBox"; + bitBox.SetFixedHeight(16); + + SubscribeToEvent(bitBox,"Toggled", "HandleBitSelectionToggled"); + } + } + + LineEdit@ attrEdit = CreateAttributeLineEdit(parent, serializables, index, subIndex); + attrEdit.name = "LineEdit"; + SubscribeToEvent(attrEdit, "TextChanged", "HandleBitSelectionEdit"); + SubscribeToEvent(attrEdit, "TextFinished", "HandleBitSelectionEdit"); + return attrEdit; +} + +void UpdateBitSelection(UIElement@ parent) +{ + int mask = 0; + for (int i = 0; i < MAX_BITMASK_BITS; i++) + { + CheckBox@ bitBox = parent.GetChild("BitSelect_" + String(i), true); + mask = mask | (bitBox.checked ? 1 << i : 0); + } + + if (mask == MAX_BITMASK_VALUE) + mask = -1; + + inUpdateBitSelection = true; + LineEdit@ attrEdit = parent.parent.GetChild("LineEdit", true); + attrEdit.text = String(mask); + inUpdateBitSelection = false; +} + +void SetBitSelection(UIElement@ parent, int value) +{ + int mask = value; + bool enabled = true; + + if (mask == -1) + mask = MAX_BITMASK_VALUE; + else if (mask > MAX_BITMASK_VALUE) + enabled = false; + + for (int i = 0; i < MAX_BITMASK_BITS; i++) + { + CheckBox@ bitBox = parent.GetChild("BitSelect_" + String(i), true); + bitBox.enabled = enabled; + if (!enabled) + bitBox.color = nonEditableBitSelectorColor; + else + bitBox.color = editableBitSelectorColor; + + if ((1 << i) & mask != 0) + bitBox.checked = true; + else + bitBox.checked = false; + } +} + +void HandleBitSelectionToggled(StringHash eventType, VariantMap& eventData) +{ + if (inUpdateBitSelection) + return; + + CheckBox@ bitBox = eventData["Element"].GetPtr(); + + UpdateBitSelection(bitBox.parent); +} + +void HandleBitSelectionEdit(StringHash eventType, VariantMap& eventData) +{ + if (!inUpdateBitSelection) + { + LineEdit@ attrEdit = eventData["Element"].GetPtr(); + + inUpdateBitSelection = true; + SetBitSelection(attrEdit.parent, attrEdit.text.ToI32()); + inUpdateBitSelection = false; + } + + EditAttribute(eventType, eventData); +} + +UIElement@ CreateStringAttributeEditor(ListView@ list, Array@ serializables, const AttributeInfo&in info, uint index, uint subIndex) +{ + UIElement@ parent = CreateAttributeEditorParent(list, info.name, index, subIndex); + LineEdit@ attrEdit = CreateAttributeLineEdit(parent, serializables, index, subIndex); + attrEdit.dragDropMode = DD_TARGET; + // Do not subscribe to continuous edits of certain attributes (script class names) to prevent unnecessary errors getting printed + if (noTextChangedAttrs.Find(info.name) == -1) + SubscribeToEvent(attrEdit, "TextChanged", "EditAttribute"); + SubscribeToEvent(attrEdit, "TextFinished", "EditAttribute"); + + return parent; +} + +UIElement@ CreateBoolAttributeEditor(ListView@ list, Array@ serializables, const AttributeInfo&in info, uint index, uint subIndex) +{ + bool isUIElement = cast(serializables[0]) !is null; + UIElement@ parent; + if (info.name == (isUIElement ? "Is Visible" : "Is Enabled")) + parent = CreateAttributeEditorParentAsListChild(list, info.name, index, subIndex); + else + parent = CreateAttributeEditorParent(list, info.name, index, subIndex); + + CheckBox@ attrEdit = CheckBox(); + parent.AddChild(attrEdit); + attrEdit.style = AUTO_STYLE; + attrEdit.vars["Index"] = index; + attrEdit.vars["SubIndex"] = subIndex; + SetAttributeEditorID(attrEdit, serializables); + SubscribeToEvent(attrEdit, "Toggled", "EditAttribute"); + + return parent; +} + +UIElement@ CreateNumAttributeEditor(ListView@ list, Array@ serializables, const AttributeInfo&in info, uint index, uint subIndex) +{ + UIElement@ parent = CreateAttributeEditorParent(list, info.name, index, subIndex); + VariantType type = info.type; + uint numCoords = 1; + if (type == VAR_VECTOR2 || type == VAR_INTVECTOR2) + numCoords = 2; + if (type == VAR_VECTOR3 || type == VAR_INTVECTOR3 || type == VAR_QUATERNION) + numCoords = 3; + else if (type == VAR_VECTOR4 || type == VAR_COLOR || type == VAR_INTRECT || type == VAR_RECT) + numCoords = 4; + + for (uint i = 0; i < numCoords; ++i) + { + LineEdit@ attrEdit = CreateAttributeLineEdit(parent, serializables, index, subIndex); + attrEdit.vars["Coordinate"] = i; + + CreateDragSlider(attrEdit); + + SubscribeToEvent(attrEdit, "TextChanged", "EditAttribute"); + SubscribeToEvent(attrEdit, "TextFinished", "EditAttribute"); + } + + return parent; +} + +UIElement@ CreateIntAttributeEditor(ListView@ list, Array@ serializables, const AttributeInfo&in info, uint index, uint subIndex) +{ + UIElement@ parent = CreateAttributeEditorParent(list, info.name, index, subIndex); + + // Check for masks and layers + if (bitSelectionAttrs.Find(info.name) > -1) + { + LineEdit@ attrEdit = CreateAttributeBitSelector(parent, serializables, index, subIndex); + } + // Check for enums + else if (info.enumNames is null || info.enumNames.empty) + { + // No enums, create a numeric editor + LineEdit@ attrEdit = CreateAttributeLineEdit(parent, serializables, index, subIndex); + CreateDragSlider(attrEdit); + // If the attribute is a counter for things like billboards or animation states, disable apply at each change + if (info.name.Find(" Count", 0, false) == String::NPOS) + SubscribeToEvent(attrEdit, "TextChanged", "EditAttribute"); + SubscribeToEvent(attrEdit, "TextFinished", "EditAttribute"); + // If the attribute is a node ID, make it a drag/drop target + if (info.name.Contains("NodeID", false) || info.name.Contains("Node ID", false) || (info.mode & AM_NODEID) != 0) + attrEdit.dragDropMode = DD_TARGET; + } + else + { + DropDownList@ attrEdit = DropDownList(); + parent.AddChild(attrEdit); + attrEdit.style = AUTO_STYLE; + attrEdit.SetFixedHeight(ATTR_HEIGHT - 2); + attrEdit.resizePopup = true; + attrEdit.placeholderText = STRIKED_OUT; + attrEdit.vars["Index"] = index; + attrEdit.vars["SubIndex"] = subIndex; + attrEdit.SetLayout(LM_HORIZONTAL, 0, IntRect(4, 1, 4, 1)); + SetAttributeEditorID(attrEdit, serializables); + + for (uint i = 0; i < info.enumNames.length; ++i) + { + Text@ choice = Text(); + attrEdit.AddItem(choice); + choice.style = "EditorEnumAttributeText"; + choice.text = info.enumNames[i]; + } + SubscribeToEvent(attrEdit, "ItemSelected", "EditAttribute"); + } + + return parent; +} + +UIElement@ CreateResourceRefAttributeEditor(ListView@ list, Array@ serializables, const AttributeInfo&in info, uint index, uint subIndex, bool suppressedSeparatedLabel = false) +{ + UIElement@ parent; + StringHash resourceType; + + // Get the real attribute info from the serializable for the correct resource type + AttributeInfo attrInfo = serializables[0].attributeInfos[index]; + if (attrInfo.type == VAR_RESOURCEREF) + resourceType = serializables[0].attributes[index].GetResourceRef().type; + else if (attrInfo.type == VAR_RESOURCEREFLIST) + resourceType = serializables[0].attributes[index].GetResourceRefList().type; + else if (attrInfo.type == VAR_VARIANTVECTOR) + resourceType = serializables[0].attributes[index].GetVariantVector()[subIndex].GetResourceRef().type; + + ResourcePicker@ picker = GetResourcePicker(resourceType); + + // Create the attribute name on a separate non-interactive line to allow for more space + parent = CreateAttributeEditorParentWithSeparatedLabel(list, info.name, index, subIndex, suppressedSeparatedLabel); + + UIElement@ container = UIElement(); + container.SetLayout(LM_HORIZONTAL, 4, IntRect(info.name.StartsWith(" ") ? 20 : 10, 0, 4, 0)); // Left margin is indented more when the name is so + container.SetFixedHeight(ATTR_HEIGHT); + parent.AddChild(container); + + LineEdit@ attrEdit = CreateAttributeLineEdit(container, serializables, index, subIndex); + attrEdit.vars[TYPE_VAR] = resourceType.value; + SubscribeToEvent(attrEdit, "TextFinished", "EditAttribute"); + + if (picker !is null) + { + if ((picker.actions & ACTION_PICK) != 0) + { + Button@ pickButton = CreateResourcePickerButton(container, serializables, index, subIndex, "smallButtonPick"); + SubscribeToEvent(pickButton, "Released", "PickResource"); + } + if ((picker.actions & ACTION_OPEN) != 0) + { + Button@ openButton = CreateResourcePickerButton(container, serializables, index, subIndex, "smallButtonOpen"); + SubscribeToEvent(openButton, "Released", "OpenResource"); + } + if ((picker.actions & ACTION_EDIT) != 0) + { + Button@ editButton = CreateResourcePickerButton(container, serializables, index, subIndex, "smallButtonEdit"); + SubscribeToEvent(editButton, "Released", "EditResource"); + } + if ((picker.actions & ACTION_TEST) != 0) + { + Button@ testButton = CreateResourcePickerButton(container, serializables, index, subIndex, "smallButtonTest"); + SubscribeToEvent(testButton, "Released", "TestResource"); + } + } + + return parent; +} + +Button@ CreateResourcePickerButton(UIElement@ container, Array@ serializables, uint index, uint subIndex, const String&in text) +{ + Button@ button = Button(); + container.AddChild(button); + button.style = AUTO_STYLE; + button.SetFixedSize(36, ATTR_HEIGHT - 2); + button.vars["Index"] = index; + button.vars["SubIndex"] = subIndex; + SetAttributeEditorID(button, serializables); + + Text@ buttonText = Text(); + button.AddChild(buttonText); + buttonText.style = "EditorAttributeText"; + buttonText.SetAlignment(HA_CENTER, VA_CENTER); + buttonText.text = text; + buttonText.autoLocalizable = true; + + return button; +} + +// Use internally for nested variant vector +uint nestedSubIndex; +Array@ nestedVector; + +UIElement@ CreateAttributeEditor(ListView@ list, Array@ serializables, const AttributeInfo&in info, uint index, uint subIndex, bool suppressedSeparatedLabel = false) +{ + UIElement@ parent; + + VariantType type = info.type; + if (type == VAR_STRING || type == VAR_BUFFER) + parent = CreateStringAttributeEditor(list, serializables, info, index, subIndex); + else if (type == VAR_BOOL) + parent = CreateBoolAttributeEditor(list, serializables, info, index, subIndex); + else if ((type >= VAR_FLOAT && type <= VAR_VECTOR4) || type == VAR_QUATERNION || type == VAR_COLOR || type == VAR_INTVECTOR2 || type == VAR_INTVECTOR3 || type == VAR_INTRECT || type == VAR_DOUBLE || type == VAR_RECT) + parent = CreateNumAttributeEditor(list, serializables, info, index, subIndex); + else if (type == VAR_INT) + parent = CreateIntAttributeEditor(list, serializables, info, index, subIndex); + else if (type == VAR_RESOURCEREF) + parent = CreateResourceRefAttributeEditor(list, serializables, info, index, subIndex, suppressedSeparatedLabel); + else if (type == VAR_RESOURCEREFLIST) + { + uint numRefs = serializables[0].attributes[index].GetResourceRefList().length; + + // Straightly speaking the individual resource reference in the list is not an attribute of the serializable + // However, the AttributeInfo structure is used here to reduce the number of parameters being passed in the function + AttributeInfo refInfo; + refInfo.name = info.name; + refInfo.type = VAR_RESOURCEREF; + for (uint i = 0; i < numRefs; ++i) + CreateAttributeEditor(list, serializables, refInfo, index, i, i > 0); + } + else if (type == VAR_VARIANTVECTOR) + { + uint nameIndex = 0; + uint repeat = M_MAX_UNSIGNED; + + VectorStruct@ vectorStruct; + Array@ vector; + bool emptyNestedVector = false; + if (info.name.Contains('>')) + { + @vector = @nestedVector; + vectorStruct = GetNestedVectorStruct(serializables, info.name); + repeat = vector[subIndex].GetU32(); // Nested VariantVector must have a predefined repeat count at the start of the vector + emptyNestedVector = repeat == 0; + } + else + { + @vector = serializables[0].attributes[index].GetVariantVector(); + vectorStruct = GetVectorStruct(serializables, index); + subIndex = 0; + } + if (vectorStruct is null) + return null; + + for (uint i = subIndex; i < vector.length; ++i) + { + // The individual variant in the vector is not an attribute of the serializable, the structure is reused for convenience + AttributeInfo vectorInfo; + vectorInfo.name = vectorStruct.variableNames[nameIndex++]; + bool nested = vectorInfo.name.Contains('>'); + if (nested) + { + vectorInfo.type = VAR_VARIANTVECTOR; + @nestedVector = @vector; + } + else + vectorInfo.type = vector[i].type; + CreateAttributeEditor(list, serializables, vectorInfo, index, i); + if (nested) + { + i = nestedSubIndex; + @nestedVector = null; + } + if (emptyNestedVector) + { + nestedSubIndex = i; + break; + } + if (nameIndex >= vectorStruct.variableNames.length) + { + if (--repeat == 0) + { + nestedSubIndex = i; + break; + } + nameIndex = vectorStruct.restartIndex; + + // Create small divider for repeated instances + UIElement@ divider = UIElement(); + divider.SetFixedHeight(8); + list.AddItem(divider); + } + } + } + else if (type == VAR_VARIANTMAP) + { + VariantMap map = serializables[0].attributes[index].GetVariantMap(); + Array@ keys = map.keys; + for (uint i = 0; i < keys.length; ++i) + { + String varName = GetVarName(keys[i]); + bool shouldHide = false; + + if (varName.empty) + { + // UIElements will contain internal vars, which do not have known mappings. Hide these + if (cast(serializables[0]) !is null) + shouldHide = true; + // Else, for scene nodes, show as hexadecimal hashes if nothing else is available + varName = keys[i].ToString(); + } + Variant value = map[keys[i]]; + + // The individual variant in the map is not an attribute of the serializable, the structure is reused for convenience + AttributeInfo mapInfo; + mapInfo.name = varName + " (Var)"; + mapInfo.type = value.type; + parent = CreateAttributeEditor(list, serializables, mapInfo, index, i); + // Add the variant key to the parent. We may fail to add the editor in case it is unsupported + if (parent !is null) + { + parent.vars["Key"] = keys[i].value; + // If variable name is not registered (i.e. it is an editor internal variable) then hide it + if (varName.empty || shouldHide) + parent.visible = false; + } + } + } + + return parent; +} + +uint GetAttributeEditorCount(Array@ serializables) +{ + uint count = 0; + + if (!serializables.empty) + { + /// \todo When multi-editing, this only counts the editor count of the first serializable + bool isUIElement = cast(serializables[0]) !is null; + for (uint i = 0; i < serializables[0].numAttributes; ++i) + { + AttributeInfo info = serializables[0].attributeInfos[i]; + if (!showNonEditableAttribute && info.mode & AM_NOEDIT != 0) + continue; + // "Is Enabled" is not inserted into the main attribute list, so do not count + // Similarly, for UIElement, "Is Visible" is not inserted + if (info.name == (isUIElement ? "Is Visible" : "Is Enabled")) + continue; + // Tags are also handled separately + if (info.name == "Tags") + continue; + if (info.type == VAR_RESOURCEREFLIST) + count += serializables[0].attributes[i].GetResourceRefList().length; + else if (info.type == VAR_VARIANTVECTOR && GetVectorStruct(serializables, i) !is null) + count += serializables[0].attributes[i].GetVariantVector().length; + else if (info.type == VAR_VARIANTMAP) + count += serializables[0].attributes[i].GetVariantMap().length; + else + ++count; + } + } + + return count; +} + +UIElement@ GetAttributeEditorParent(UIElement@ parent, uint index, uint subIndex) +{ + return parent.GetChild("Edit" + String(index) + "_" + String(subIndex), true); +} + +void LoadAttributeEditor(ListView@ list, Array@ serializables, const AttributeInfo&in info, uint index) +{ + bool editable = info.mode & AM_NOEDIT == 0; + + UIElement@ parent = GetAttributeEditorParent(list, index, 0); + if (parent is null) + return; + + inLoadAttributeEditor = true; + + bool sameName = true; + bool sameValue = true; + Variant value = serializables[0].attributes[index]; + Array values; + for (uint i = 0; i < serializables.length; ++i) + { + if (index >= serializables[i].numAttributes || serializables[i].attributeInfos[index].name != info.name) + { + sameName = false; + break; + } + + Variant val = serializables[i].attributes[index]; + if (val != value) + sameValue = false; + values.Push(val); + } + + // Attribute with different values from multiple-select is loaded with default/empty value and non-editable + if (sameName) + LoadAttributeEditor(parent, value, info, editable, sameValue, values); + else + parent.visible = false; + + inLoadAttributeEditor = false; +} + +void LoadAttributeEditor(UIElement@ parent, const Variant&in value, const AttributeInfo&in info, bool editable, bool sameValue, const Array&in values) +{ + uint index = parent.vars["Index"].GetU32(); + + // Assume the first child is always a text label element or a container that containing a text label element + UIElement@ label = parent.children[0]; + if (label.type == UI_ELEMENT_TYPE && label.numChildren > 0) + label = label.children[0]; + if (label.type == TEXT_TYPE) + { + bool modified; + if (info.defaultValue.type == VAR_NONE || info.defaultValue.type == VAR_RESOURCEREFLIST) + modified = !value.zero; + else + modified = value != info.defaultValue; + cast(label).color = (editable ? (modified ? modifiedTextColor : normalTextColor) : nonEditableTextColor); + } + + VariantType type = info.type; + if (type == VAR_FLOAT || type == VAR_DOUBLE || type == VAR_STRING || type == VAR_BUFFER) + SetEditable(SetValue(parent.children[1], value.ToString(), sameValue), editable && sameValue); + else if (type == VAR_BOOL) + SetEditable(SetValue(parent.children[1], value.GetBool(), sameValue), editable && sameValue); + else if (type == VAR_INT) + { + if (bitSelectionAttrs.Find(info.name) > -1) + SetEditable(SetValue(parent.GetChild("LineEdit", true), value.ToString(), sameValue), editable && sameValue); + else if (info.enumNames is null || info.enumNames.empty) + SetEditable(SetValue(parent.children[1], value.ToString(), sameValue), editable && sameValue); + else + SetEditable(SetValue(parent.children[1], value.GetI32(), sameValue), editable && sameValue); + } + else if (type == VAR_RESOURCEREF) + { + SetEditable(SetValue(parent.children[1].children[0], value.GetResourceRef().name, sameValue), editable && sameValue); + SetEditable(parent.children[1].children[1], editable && sameValue); // If editable then can pick + for (uint i = 2; i < parent.children[1].numChildren; ++i) + SetEditable(parent.children[1].children[i], sameValue); // If same value then can open/edit/test + } + else if (type == VAR_RESOURCEREFLIST) + { + UIElement@ list = parent.parent; + ResourceRefList refList = value.GetResourceRefList(); + for (uint subIndex = 0; subIndex < refList.length; ++subIndex) + { + parent = GetAttributeEditorParent(list, index, subIndex); + if (parent is null) + break; + + String firstName = refList.names[subIndex]; + bool nameSameValue = true; + if (!sameValue) + { + // Reevaluate each name in the list + for (uint i = 0; i < values.length; ++i) + { + ResourceRefList rList = values[i].GetResourceRefList(); + if (subIndex >= rList.length || rList.names[subIndex] != firstName) + { + nameSameValue = false; + break; + } + } + } + SetEditable(SetValue(parent.children[1].children[0], firstName, nameSameValue), editable && nameSameValue); + } + } + else if (type == VAR_VARIANTVECTOR) + { + UIElement@ list = parent.parent; + Array@ vector = value.GetVariantVector(); + for (uint subIndex = 0; subIndex < vector.length; ++subIndex) + { + parent = GetAttributeEditorParent(list, index, subIndex); + if (parent is null) + break; + + Variant firstValue = vector[subIndex]; + bool sameVal = true; + Array varValues; + + // Reevaluate each variant in the vector + for (uint i = 0; i < values.length; ++i) + { + Array@ vec = values[i].GetVariantVector(); + if (subIndex < vec.length) + { + Variant v = vec[subIndex]; + varValues.Push(v); + if (v != firstValue) + sameVal = false; + } + else + sameVal = false; + } + + // The individual variant in the list is not an attribute of the serializable, the structure is reused for convenience + AttributeInfo ai; + ai.type = firstValue.type; + LoadAttributeEditor(parent, firstValue, ai, editable, sameVal, varValues); + } + } + else if (type == VAR_VARIANTMAP) + { + UIElement@ list = parent.parent; + VariantMap map = value.GetVariantMap(); + Array@ keys = map.keys; + for (uint subIndex = 0; subIndex < keys.length; ++subIndex) + { + parent = GetAttributeEditorParent(list, index, subIndex); + if (parent is null) + break; + + String varName = GetVarName(keys[subIndex]); + if (varName.empty) + varName = keys[subIndex].ToString(); // Use hexadecimal if nothing else is available + + Variant firstValue = map[keys[subIndex]]; + bool sameVal = true; + Array varValues; + + // Reevaluate each variant in the map + for (uint i = 0; i < values.length; ++i) + { + VariantMap m = values[i].GetVariantMap(); + if (m.Contains(keys[subIndex])) + { + Variant v = m[keys[subIndex]]; + varValues.Push(v); + if (v != firstValue) + sameVal = false; + } + else + sameVal = false; + } + + // The individual variant in the map is not an attribute of the serializable, the structure is reused for convenience + AttributeInfo ai; + ai.type = firstValue.type; + LoadAttributeEditor(parent, firstValue, ai, editable, sameVal, varValues); + } + } + else + { + Array > coordinates; + for (uint i = 0; i < values.length; ++i) + { + Variant v = values[i]; + + // Convert Quaternion value to Vector3 value first + if (type == VAR_QUATERNION) + v = v.GetQuaternion().eulerAngles; + + coordinates.Push(v.ToString().Split(' ')); + } + for (uint i = 0; i < coordinates[0].length; ++i) + { + String str = coordinates[0][i]; + bool coordinateSameValue = true; + if (!sameValue) + { + // Reevaluate each coordinate + for (uint j = 1; j < coordinates.length; ++j) + { + if (coordinates[j][i] != str) + { + coordinateSameValue = false; + break; + } + } + } + SetEditable(SetValue(parent.children[i + 1], str, coordinateSameValue), editable && coordinateSameValue); + } + } +} + +void StoreAttributeEditor(UIElement@ parent, Array@ serializables, uint index, uint subIndex, uint coordinate) +{ + AttributeInfo info = serializables[0].attributeInfos[index]; + + if (info.type == VAR_RESOURCEREFLIST) + { + for (uint i = 0; i < serializables.length; ++i) + { + ResourceRefList refList = serializables[i].attributes[index].GetResourceRefList(); + Variant[] values(1); + GetEditorValue(parent, VAR_RESOURCEREF, null, coordinate, values); + ResourceRef ref = values[0].GetResourceRef(); + refList.names[subIndex] = ref.name; + serializables[i].attributes[index] = Variant(refList); + } + } + else if (info.type == VAR_VARIANTVECTOR) + { + for (uint i = 0; i < serializables.length; ++i) + { + Array@ vector = serializables[i].attributes[index].GetVariantVector(); + Variant[] values; + values.Push(vector[subIndex]); // Each individual variant may have multiple coordinates itself + GetEditorValue(parent, vector[subIndex].type, null, coordinate, values); + vector[subIndex] = values[0]; + serializables[i].attributes[index] = Variant(vector); + } + } + else if (info.type == VAR_VARIANTMAP) + { + StringHash key(parent.vars["Key"].GetU32()); + for (uint i = 0; i < serializables.length; ++i) + { + VariantMap map = serializables[i].attributes[index].GetVariantMap(); + Variant[] values; + values.Push(map[key]); // Each individual variant may have multiple coordinates itself + GetEditorValue(parent, map[key].type, null, coordinate, values); + map[key] = values[0]; + serializables[i].attributes[index] = Variant(map); + } + } + else + { + Array values; + for (uint i = 0; i < serializables.length; ++i) + values.Push(serializables[i].attributes[index]); + GetEditorValue(parent, info.type, info.enumNames, coordinate, values); + for (uint i = 0; i < serializables.length; ++i) + serializables[i].attributes[index] = values[i]; + } +} + +void FillValue(Array& values, const Variant&in value) +{ + for (uint i = 0; i < values.length; ++i) + values[i] = value; +} + +void SanitizeNumericalValue(VariantType type, String& value) +{ + if ((type >= VAR_FLOAT && type <= VAR_COLOR) || type == VAR_RECT) + value = String(value.ToFloat()); + else if (type == VAR_INT || type == VAR_INTRECT || type == VAR_INTVECTOR2 || type == VAR_INTVECTOR3) + value = String(value.ToI32()); + else if (type == VAR_DOUBLE) + value = String(value.ToDouble()); +} + +void GetEditorValue(UIElement@ parent, VariantType type, Array@ enumNames, uint coordinate, Array& values) +{ + LineEdit@ attrEdit = parent.children[coordinate + 1]; + + if (attrEdit is null) + attrEdit = parent.GetChild("LineEdit", true); + + if (type == VAR_STRING) + FillValue(values, Variant(attrEdit.text.Trimmed())); + else if (type == VAR_BOOL) + { + CheckBox@ cb = parent.children[1]; + FillValue(values, Variant(cb.checked)); + } + else if (type == VAR_FLOAT) + FillValue(values, Variant(attrEdit.text.ToFloat())); + else if (type == VAR_DOUBLE) + FillValue(values, Variant(attrEdit.text.ToDouble())); + else if (type == VAR_QUATERNION) + { + float value = attrEdit.text.ToFloat(); + for (uint i = 0; i < values.length; ++i) + { + float[] data = values[i].GetQuaternion().eulerAngles.data; + data[coordinate] = value; + values[i] = Quaternion(Vector3(data)); + } + } + else if (type == VAR_INT) + { + if (enumNames is null || enumNames.empty) + FillValue(values, Variant(attrEdit.text.ToI32())); + else + { + DropDownList@ ddl = parent.children[1]; + FillValue(values, Variant(ddl.selection)); + } + } + else if (type == VAR_RESOURCEREF) + { + LineEdit@ le = parent.children[0]; + ResourceRef ref; + ref.name = le.text.Trimmed(); + ref.type = StringHash(le.vars[TYPE_VAR].GetU32()); + FillValue(values, Variant(ref)); + } + else + { + String value = attrEdit.text; + SanitizeNumericalValue(type, value); + for (uint i = 0; i < values.length; ++i) + { + String[] data = values[i].ToString().Split(' '); + data[coordinate] = value; + values[i] = Variant(type, Join(data, " ")); + } + } +} + +void UpdateAttributes(Array@ serializables, ListView@ list, bool& fullUpdate) +{ + // If attributes have changed structurally, do a full update + uint count = GetAttributeEditorCount(serializables); + if (fullUpdate == false) + { + if (list.contentElement.numChildren != count) + fullUpdate = true; + } + + // Remember the old scroll position so that a full update does not feel as jarring + IntVector2 oldViewPos = list.viewPosition; + + if (fullUpdate) + { + list.RemoveAllItems(); + Array children = list.GetChildren(); + for (uint i = 0; i < children.length; ++i) + { + if (!children[i].internal) + children[i].Remove(); + } + } + + if (serializables.empty) + return; + + // If there are many serializables, they must share same attribute structure (up to certain number if not all) + for (uint i = 0; i < serializables[0].numAttributes; ++i) + { + AttributeInfo info = serializables[0].attributeInfos[i]; + if (!showNonEditableAttribute && info.mode & AM_NOEDIT != 0) + continue; + + // Use the default value (could be instance's default value) of the first serializable as the default for all + info.defaultValue = serializables[0].attributeDefaults[i]; + + if (fullUpdate) + CreateAttributeEditor(list, serializables, info, i, 0); + + LoadAttributeEditor(list, serializables, info, i); + } + + if (fullUpdate) + list.viewPosition = oldViewPos; +} + +void CreateDragSlider(LineEdit@ parent) +{ + Button@ dragSld = Button(); + dragSld.style = "EditorDragSlider"; + dragSld.SetFixedHeight(ATTR_HEIGHT - 3); + dragSld.SetFixedWidth(dragSld.height); + dragSld.SetAlignment(HA_RIGHT, VA_TOP); + dragSld.focusMode = FM_NOTFOCUSABLE; + parent.AddChild(dragSld); + + SubscribeToEvent(dragSld, "DragBegin", "LineDragBegin"); + SubscribeToEvent(dragSld, "DragMove", "LineDragMove"); + SubscribeToEvent(dragSld, "DragEnd", "LineDragEnd"); + SubscribeToEvent(dragSld, "DragCancel", "LineDragCancel"); +} + +void EditAttribute(StringHash eventType, VariantMap& eventData) +{ + // Changing elements programmatically may cause events to be sent. Stop possible infinite loop in that case. + if (inLoadAttributeEditor) + return; + + UIElement@ attrEdit = eventData["Element"].GetPtr(); + UIElement@ parent = attrEdit.parent; + Array@ serializables = GetAttributeEditorTargets(attrEdit); + if (serializables.empty) + return; + + uint index = attrEdit.vars["Index"].GetU32(); + uint subIndex = attrEdit.vars["SubIndex"].GetU32(); + uint coordinate = attrEdit.vars["Coordinate"].GetU32(); + bool intermediateEdit = eventType == TEXT_CHANGED_EVENT_TYPE; + + // Do the editor pre logic before attribute is being modified + if (!PreEditAttribute(serializables, index)) + return; + + inEditAttribute = true; + + Array oldValues; + + if (!dragEditAttribute) + { + // Store old values so that PostEditAttribute can create undo actions + for (uint i = 0; i < serializables.length; ++i) + oldValues.Push(serializables[i].attributes[index]); + } + + StoreAttributeEditor(parent, serializables, index, subIndex, coordinate); + for (uint i = 0; i < serializables.length; ++i) + serializables[i].ApplyAttributes(); + + if (!dragEditAttribute) + { + // Do the editor post logic after attribute has been modified. + PostEditAttribute(serializables, index, oldValues); + } + + inEditAttribute = false; + + // If not an intermediate edit, reload the editor fields with validated values + // (attributes may have interactions; therefore we load everything, not just the value being edited) + if (!intermediateEdit) + attributesDirty = true; +} + +void LineDragBegin(StringHash eventType, VariantMap& eventData) +{ + UIElement@ label = eventData["Element"].GetPtr(); + int x = eventData["X"].GetI32(); + label.vars["posX"] = x; + + // Store the old value before dragging + dragEditAttribute = false; + LineEdit@ selectedNumEditor = label.parent; + + selectedNumEditor.vars["DragBeginValue"] = selectedNumEditor.text; + selectedNumEditor.cursorPosition = 0; + + // Set mouse mode to user preference + SetMouseMode(true); +} + +void LineDragMove(StringHash eventTypem, VariantMap& eventData) +{ + UIElement@ label = eventData["Element"].GetPtr(); + LineEdit@ selectedNumEditor = label.parent; + + // Prevent undo + dragEditAttribute = true; + + int x = eventData["X"].GetI32(); + int posx = label.vars["posX"].GetI32(); + float val = input.mouseMoveX; + + float fieldVal = selectedNumEditor.text.ToFloat(); + fieldVal += val/100; + label.vars["posX"] = x; + selectedNumEditor.text = fieldVal; + selectedNumEditor.cursorPosition = 0; +} + +void LineDragEnd(StringHash eventType, VariantMap& eventData) +{ + UIElement@ label = eventData["Element"].GetPtr(); + LineEdit@ selectedNumEditor = label.parent; + + // Prepare the attributes to store an undo with: + // - old value = drag begin value + // - new value = final value + + String finalValue = selectedNumEditor.text; + // Reset attribute to begin value, and prevent undo + dragEditAttribute = true; + selectedNumEditor.text = selectedNumEditor.vars["DragBeginValue"].GetString(); + + // Store final value, allow undo + dragEditAttribute = false; + selectedNumEditor.text = finalValue; + selectedNumEditor.cursorPosition = 0; + + // Revert mouse to normal behaviour + SetMouseMode(false); +} + +void LineDragCancel(StringHash eventType, VariantMap& eventData) +{ + UIElement@ label = eventData["Element"].GetPtr(); + + // Reset value to what it was when drag edit began, preventing undo. + dragEditAttribute = true; + LineEdit@ selectedNumEditor = label.parent; + selectedNumEditor.text = selectedNumEditor.vars["DragBeginValue"].GetString(); + selectedNumEditor.cursorPosition = 0; + + // Revert mouse to normal behaviour + SetMouseMode(false); +} + +// Resource picker functionality +const uint ACTION_PICK = 1; +const uint ACTION_OPEN = 2; +const uint ACTION_EDIT = 4; +const uint ACTION_TEST = 8; + +class ResourcePicker +{ + String typeName; + StringHash type; + String lastPath; + uint lastFilter; + Array filters; + uint actions; + + ResourcePicker(const String&in typeName_, const String&in filter_, uint actions_ = ACTION_PICK | ACTION_OPEN) + { + typeName = typeName_; + type = StringHash(typeName_); + actions = actions_; + filters.Push(filter_); + filters.Push("*.*"); + lastFilter = 0; + } + + ResourcePicker(const String&in typeName_, const Array@ filters_, uint actions_ = ACTION_PICK | ACTION_OPEN) + { + typeName = typeName_; + type = StringHash(typeName_); + filters = filters_; + actions = actions_; + filters.Push("*.*"); + lastFilter = 0; + } +}; + +Array resourcePickers; +Array resourceTargets; +uint resourcePickIndex = 0; +uint resourcePickSubIndex = 0; +ResourcePicker@ resourcePicker = null; + +void InitResourcePicker() +{ + // Fill resource picker data + Array fontFilters = {"*.ttf", "*.otf", "*.fnt", "*.xml", "*.sdf"}; + Array imageFilters = {"*.png", "*.jpg", "*.bmp", "*.tga", "*.hdr"}; + Array luaFileFilters = {"*.lua", "*.luc"}; + Array scriptFilters = {"*.as", "*.asc"}; + Array soundFilters = {"*.wav","*.ogg"}; + Array textureFilters = {"*.dds", "*.png", "*.jpg", "*.bmp", "*.tga", "*.ktx", "*.pvr", "*.hdr"}; + Array materialFilters = {"*.xml", "*.material", "*.json"}; + Array anmSetFilters = {"*.scml"}; + Array pexFilters = {"*.pex"}; + Array tmxFilters = {"*.tmx"}; + resourcePickers.Push(ResourcePicker("Animation", "*.ani", ACTION_PICK | ACTION_TEST)); + resourcePickers.Push(ResourcePicker("Font", fontFilters)); + resourcePickers.Push(ResourcePicker("Image", imageFilters)); + resourcePickers.Push(ResourcePicker("LuaFile", luaFileFilters)); + resourcePickers.Push(ResourcePicker("Material", materialFilters, ACTION_PICK | ACTION_OPEN | ACTION_EDIT)); + resourcePickers.Push(ResourcePicker("Model", "*.mdl", ACTION_PICK)); + resourcePickers.Push(ResourcePicker("ParticleEffect", "*.xml", ACTION_PICK | ACTION_OPEN | ACTION_EDIT)); + resourcePickers.Push(ResourcePicker("ScriptFile", scriptFilters)); + resourcePickers.Push(ResourcePicker("Sound", soundFilters)); + resourcePickers.Push(ResourcePicker("Technique", "*.xml")); + resourcePickers.Push(ResourcePicker("Texture2D", textureFilters)); + resourcePickers.Push(ResourcePicker("TextureCube", "*.xml")); + resourcePickers.Push(ResourcePicker("Texture3D", "*.xml")); + resourcePickers.Push(ResourcePicker("XMLFile", "*.xml")); + resourcePickers.Push(ResourcePicker("JSONFile", "*.json")); + resourcePickers.Push(ResourcePicker("Sprite2D", textureFilters, ACTION_PICK | ACTION_OPEN)); + resourcePickers.Push(ResourcePicker("AnimationSet2D", anmSetFilters, ACTION_PICK | ACTION_OPEN)); + resourcePickers.Push(ResourcePicker("ParticleEffect2D", pexFilters, ACTION_PICK | ACTION_OPEN)); + resourcePickers.Push(ResourcePicker("TmxFile2D", tmxFilters, ACTION_PICK | ACTION_OPEN)); +} + +ResourcePicker@ GetResourcePicker(StringHash resourceType) +{ + for (uint i = 0; i < resourcePickers.length; ++i) + { + // TODO: refactor to use dictionary instead + if (resourceType == resourcePickers[i].type) + return resourcePickers[i]; + } + return null; +} + +void PickResource(StringHash eventType, VariantMap& eventData) +{ + UIElement@ button = eventData["Element"].GetPtr(); + LineEdit@ attrEdit = button.parent.children[0]; + + Array@ targets = GetAttributeEditorTargets(attrEdit); + if (targets.empty) + return; + + resourcePickIndex = attrEdit.vars["Index"].GetU32(); + resourcePickSubIndex = attrEdit.vars["SubIndex"].GetU32(); + AttributeInfo info = targets[0].attributeInfos[resourcePickIndex]; + + StringHash resourceType; + if (info.type == VAR_RESOURCEREF) + resourceType = targets[0].attributes[resourcePickIndex].GetResourceRef().type; + else if (info.type == VAR_RESOURCEREFLIST) + resourceType = targets[0].attributes[resourcePickIndex].GetResourceRefList().type; + else if (info.type == VAR_VARIANTVECTOR) + resourceType = targets[0].attributes[resourcePickIndex].GetVariantVector()[resourcePickSubIndex].GetResourceRef().type; + + @resourcePicker = GetResourcePicker(resourceType); + if (resourcePicker is null) + return; + + resourceTargets.Clear(); + for (uint i = 0; i < targets.length; ++i) + resourceTargets.Push(targets[i]); + + String lastPath = resourcePicker.lastPath; + if (lastPath.empty) + lastPath = sceneResourcePath; + CreateFileSelector(localization.Get("Pick ") + resourcePicker.typeName, "OK", "Cancel", lastPath, resourcePicker.filters, resourcePicker.lastFilter, false); + SubscribeToEvent(uiFileSelector, "FileSelected", "PickResourceDone"); +} + +void PickResourceDone(StringHash eventType, VariantMap& eventData) +{ + StoreResourcePickerPath(); + CloseFileSelector(); + + if (!eventData["OK"].GetBool()) + { + resourceTargets.Clear(); + @resourcePicker = null; + return; + } + + if (resourcePicker is null) + return; + + // Validate the resource. It must come from within a registered resource directory, and be loaded successfully + String resourceName = eventData["FileName"].GetString(); + Resource@ res = GetPickedResource(resourceName); + if (res is null) + { + @resourcePicker = null; + return; + } + + // Store old values so that PostEditAttribute can create undo actions + Array oldValues; + for (uint i = 0; i < resourceTargets.length; ++i) + oldValues.Push(resourceTargets[i].attributes[resourcePickIndex]); + + for (uint i = 0; i < resourceTargets.length; ++i) + { + Serializable@ target = resourceTargets[i]; + + AttributeInfo info = target.attributeInfos[resourcePickIndex]; + if (info.type == VAR_RESOURCEREF) + { + ResourceRef ref = target.attributes[resourcePickIndex].GetResourceRef(); + ref.type = res.type; + ref.name = res.name; + target.attributes[resourcePickIndex] = Variant(ref); + target.ApplyAttributes(); + } + else if (info.type == VAR_RESOURCEREFLIST) + { + ResourceRefList refList = target.attributes[resourcePickIndex].GetResourceRefList(); + if (resourcePickSubIndex < refList.length) + { + refList.names[resourcePickSubIndex] = res.name; + target.attributes[resourcePickIndex] = Variant(refList); + target.ApplyAttributes(); + } + } + else if (info.type == VAR_VARIANTVECTOR) + { + Array@ attrs = target.attributes[resourcePickIndex].GetVariantVector(); + ResourceRef ref = attrs[resourcePickSubIndex].GetResourceRef(); + ref.type = res.type; + ref.name = res.name; + attrs[resourcePickSubIndex] = ref; + target.attributes[resourcePickIndex] = Variant(attrs); + target.ApplyAttributes(); + } + } + + PostEditAttribute(resourceTargets, resourcePickIndex, oldValues); + UpdateAttributeInspector(false); + + resourceTargets.Clear(); + @resourcePicker = null; +} + +void StoreResourcePickerPath() +{ + // Store filter and directory for next time + if (resourcePicker !is null && uiFileSelector !is null) + { + resourcePicker.lastPath = uiFileSelector.path; + resourcePicker.lastFilter = uiFileSelector.filterIndex; + } +} + +Resource@ GetPickedResource(String resourceName) +{ + resourceName = GetResourceNameFromFullName(resourceName); + String type = resourcePicker.typeName; + // Cube and 3D textures both use .xml extension. In that case interrogate the proper resource type + // from the file itself + if (type == "Texture3D" || type == "TextureCube") + { + XMLFile@ xmlRes = cache.GetResource("XMLFile", resourceName); + if (xmlRes !is null) + { + if (xmlRes.root.name.Compare("cubemap", false) == 0 || xmlRes.root.name.Compare("texturecube", false) == 0) + type = "TextureCube"; + else if (xmlRes.root.name.Compare("texture3d", false) == 0) + type = "Texture3D"; + } + } + + Resource@ res = cache.GetResource(type, resourceName); + + if (res is null) + log.Warning("Cannot find resource type: " + type + " Name:" + resourceName); + + return res; +} + +String GetResourceNameFromFullName(const String&in resourceName) +{ + Array@ resourceDirs = cache.resourceDirs; + + for (uint i = 0; i < resourceDirs.length; ++i) + { + if (!resourceName.ToLower().StartsWith(resourceDirs[i].ToLower())) + continue; + return resourceName.Substring(resourceDirs[i].length); + } + + return ""; // Not found +} + +void OpenResource(StringHash eventType, VariantMap& eventData) +{ + UIElement@ button = eventData["Element"].GetPtr(); + LineEdit@ attrEdit = button.parent.children[0]; + + String fileName = attrEdit.text.Trimmed(); + if (fileName.empty) + return; + + OpenResource(fileName); +} + +void OpenResource(String fileName) +{ + Array@ resourceDirs = cache.resourceDirs; + for (uint i = 0; i < resourceDirs.length; ++i) + { + String fullPath = resourceDirs[i] + fileName; + if (fileSystem.FileExists(fullPath)) + { + fileSystem.SystemOpen(fullPath, ""); + return; + } + } +} + +void EditResource(StringHash eventType, VariantMap& eventData) +{ + UIElement@ button = eventData["Element"].GetPtr(); + LineEdit@ attrEdit = button.parent.children[0]; + + String fileName = attrEdit.text.Trimmed(); + if (fileName.empty) + return; + + StringHash resourceType(attrEdit.vars[TYPE_VAR].GetU32()); + Resource@ resource = cache.GetResource(resourceType, fileName); + + if (resource !is null) + { + // For now only Materials can be edited + if (resource.typeName == "Material") + EditMaterial(cast(resource)); + else if (resource.typeName == "ParticleEffect") + EditParticleEffect(cast(resource)); + } +} + +void TestResource(StringHash eventType, VariantMap& eventData) +{ + UIElement@ button = eventData["Element"].GetPtr(); + LineEdit@ attrEdit = button.parent.children[0]; + + StringHash resourceType(attrEdit.vars[TYPE_VAR].GetU32()); + + // For now only Animations can be tested + StringHash animType("Animation"); + if (resourceType == animType) + TestAnimation(attrEdit); +} + +void TestAnimation(UIElement@ attrEdit) +{ + // Note: only supports the AnimationState array in AnimatedModel, and if only 1 model selected + Array@ targets = GetAttributeEditorTargets(attrEdit); + if (targets.length != 1) + return; + AnimatedModel@ model = cast(targets[0]); + if (model is null) + return; + + uint animStateIndex = (attrEdit.vars["SubIndex"].GetU32() - 1) / 6; + if (testAnimState.Get() is null) + { + testAnimState = model.GetAnimationState(animStateIndex); + AnimationState@ animState = testAnimState.Get(); + if (animState !is null) + animState.time = 0; // Start from beginning + } + else + testAnimState = null; +} + +void UpdateTestAnimation(float timeStep) +{ + AnimationState@ animState = testAnimState.Get(); + if (animState !is null) + { + // If has also an AnimationController, and scene update is enabled, check if it is also driving the animation + // and skip in that case (avoid double speed animation) + if (runUpdate) + { + AnimatedModel@ model = animState.model; + if (model !is null) + { + Node@ node = model.node; + if (node !is null) + { + AnimationController@ ctrl = node.GetComponent("AnimationController"); + Animation@ anim = animState.animation; + if (ctrl !is null && anim !is null) + { + if (ctrl.IsPlaying(anim.name)) + return; + } + } + } + } + + animState.AddTime(timeStep); + } +} + +// VariantVector decoding & editing for certain components + +class VectorStruct +{ + String componentTypeName; + String attributeName; + Array variableNames; + uint restartIndex; + + VectorStruct(const String&in componentTypeName_, const String&in attributeName_, const Array@ variableNames_, uint restartIndex_) + { + componentTypeName = componentTypeName_; + attributeName = attributeName_; + variableNames = variableNames_; + restartIndex = restartIndex_; + } +}; + +Array vectorStructs; + +void InitVectorStructs() +{ + Array crowdManagerAreaCostVariables = { + " Area Count", + " Cost" + }; + vectorStructs.Push(VectorStruct("CrowdManager", " >AreaCost", crowdManagerAreaCostVariables, 1)); + + Array categories = GetObjectCategories(); + for (uint categoryIndex = 0; categoryIndex < categories.length; categoryIndex++) + { + Array objectsNames = GetObjectsByCategory(categories[categoryIndex]); + for (uint objectIndex = 0; objectIndex < objectsNames.length; objectIndex++) + { + String objectName = objectsNames[objectIndex]; + Array attributes = GetObjectAttributeInfos(objectName); + + for (uint attributeIndex = 0; attributeIndex < attributes.length; attributeIndex++) + { + AttributeInfo attribute = attributes[attributeIndex]; + if (attribute.type == VAR_VARIANTVECTOR and attribute.metadata.Contains("VectorStructElements")) + { + Array@ elementsNames = attribute.metadata["VectorStructElements"].GetStringVector(); + vectorStructs.Push(VectorStruct(objectName, attribute.name, elementsNames, 1)); + } + } + } + } +} + +VectorStruct@ GetVectorStruct(Array@ serializables, uint index) +{ + AttributeInfo info = serializables[0].attributeInfos[index]; + for (uint i = 0; i < vectorStructs.length; ++i) + { + if (vectorStructs[i].componentTypeName == serializables[0].typeName && vectorStructs[i].attributeName == info.name) + return vectorStructs[i]; + } + return null; +} + +VectorStruct@ GetNestedVectorStruct(Array@ serializables, const String&in name) +{ + for (uint i = 0; i < vectorStructs.length; ++i) + { + if (vectorStructs[i].componentTypeName == serializables[0].typeName && vectorStructs[i].attributeName == name) + return vectorStructs[i]; + } + return null; +} + +int GetAttributeIndex(Serializable@ serializable, const String&in attrName) +{ + for (uint i = 0; i < serializable.numAttributes; ++i) + { + if (serializable.attributeInfos[i].name.Compare(attrName, false) == 0) + return i; + } + + return -1; +} diff --git a/bin/Data/Scripts/Editor.as b/bin/EditorData/Editor/Scripts/Editor.as similarity index 91% rename from bin/Data/Scripts/Editor.as rename to bin/EditorData/Editor/Scripts/Editor.as index 0a613d4301c..049436c1e2b 100644 --- a/bin/Data/Scripts/Editor.as +++ b/bin/EditorData/Editor/Scripts/Editor.as @@ -1,456 +1,456 @@ -// Urho3D editor - -#include "Scripts/Editor/EditorHierarchyWindow.as" -#include "Scripts/Editor/EditorView.as" -#include "Scripts/Editor/EditorScene.as" -#include "Scripts/Editor/EditorActions.as" -#include "Scripts/Editor/EditorUIElement.as" -#include "Scripts/Editor/EditorGizmo.as" -#include "Scripts/Editor/EditorMaterial.as" -#include "Scripts/Editor/EditorParticleEffect.as" -#include "Scripts/Editor/EditorSettings.as" -#include "Scripts/Editor/EditorPreferences.as" -#include "Scripts/Editor/EditorToolBar.as" -#include "Scripts/Editor/EditorSecondaryToolbar.as" -#include "Scripts/Editor/EditorUI.as" -#include "Scripts/Editor/EditorImport.as" -#include "Scripts/Editor/EditorExport.as" -#include "Scripts/Editor/EditorResourceBrowser.as" -#include "Scripts/Editor/EditorSpawn.as" -#include "Scripts/Editor/EditorSoundType.as" -#include "Scripts/Editor/EditorTerrain.as" -#include "Scripts/Editor/EditorLayers.as" -#include "Scripts/Editor/EditorColorWheel.as" -#include "Scripts/Editor/EditorEventsHandlers.as" -#include "Scripts/Editor/EditorViewDebugIcons.as" -#include "Scripts/Editor/EditorViewSelectableOrigins.as" -#include "Scripts/Editor/EditorViewPaintSelection.as" -#include "Scripts/Editor/EditorDuplicator.as" - -String configFileName; - -void Start() -{ - // Assign the value ASAP because configFileName is needed on exit, including exit on error - configFileName = fileSystem.GetAppPreferencesDir("urho3d", "Editor") + "Config.xml"; - localization.LoadJSONFile("EditorStrings.json"); - - if (engine.headless) - { - ErrorDialog("Urho3D Editor", "Headless mode is not supported. The program will now exit."); - engine.Exit(); - return; - } - - // Use the first frame to setup when the resolution is initialized - SubscribeToEvent("Update", "FirstFrame"); - - SubscribeToEvent(input, "ExitRequested", "HandleExitRequested"); - - // Disable Editor auto exit, check first if it is OK to exit - engine.autoExit = false; - // Pause completely when minimized to save OS resources, reduce defocused framerate - engine.pauseMinimized = true; - engine.maxInactiveFps = 10; - // Enable console commands from the editor script - script.defaultScriptFile = scriptFile; - // Enable automatic resource reloading - cache.autoReloadResources = true; - // Return resources which exist but failed to load due to error, so that we will not lose resource refs - cache.returnFailedResources = true; - // Use OS mouse without grabbing it - input.mouseVisible = true; - // If input is scaled the double the UI size (High DPI display) - if (input.inputScale != Vector2::ONE) - { - // Should we use the inputScale itself to scale UI? - ui.scale = 2; - // When UI scale is increased, also set the UI atlas to nearest filtering to avoid artifacts - // (there is no padding) and to have a sharper look - Texture2D@ uiTex = cache.GetResource("Texture2D", "Textures/UI.png"); - if (uiTex !is null) - uiTex.filterMode = FILTER_NEAREST; - } - // Use system clipboard to allow transport of text in & out from the editor - ui.useSystemClipboard = true; -} - -void FirstFrame() -{ - // Create root scene node - CreateScene(); - // Load editor settings and preferences - LoadConfig(); - // Create user interface for the editor - CreateUI(); - // Create root UI element where all 'editable' UI elements would be parented to - CreateRootUIElement(); - // Load the initial scene if provided - ParseArguments(); - // Switch to real frame handler after initialization - SubscribeToEvent("Update", "HandleUpdate"); - SubscribeToEvent("ReloadFinished", "HandleReloadFinishOrFail"); - SubscribeToEvent("ReloadFailed", "HandleReloadFinishOrFail"); - EditorSubscribeToEvents(); -} - -void Stop() -{ - SaveConfig(); -} - -void ParseArguments() -{ - Array arguments = GetArguments(); - bool loaded = false; - - for (uint i = 1; i < arguments.length; ++i) - { - if (arguments[i].ToLower() == "-scene") - { - if (++i == arguments.length) - break; - - loaded = LoadScene(arguments[i]); - continue; - } - - if (arguments[i].ToLower() == "-language") - { - if (++i == arguments.length) - break; - - localization.SetLanguage(arguments[i]); - continue; - } - } - - if (!loaded) - ResetScene(); -} - -void HandleUpdate(StringHash eventType, VariantMap& eventData) -{ - float timeStep = eventData["TimeStep"].GetFloat(); - - DoResourceBrowserWork(); - UpdateView(timeStep); - UpdateViewports(timeStep); - UpdateStats(timeStep); - UpdateScene(timeStep); - UpdateTestAnimation(timeStep); - UpdateGizmo(); - UpdateDirtyUI(); - UpdateViewDebugIcons(); - UpdateOrigins(); - UpdatePaintSelection(); - - // Handle Particle Editor looping. - if (particleEffectWindow !is null and particleEffectWindow.visible) - { - if (!particleEffectEmitter.emitting) - { - if (particleResetTimer == 0.0f) - particleResetTimer = editParticleEffect.maxTimeToLive + 0.2f; - else - { - particleResetTimer = Max(particleResetTimer - timeStep, 0.0f); - if (particleResetTimer <= 0.0001f) - { - particleEffectEmitter.Reset(); - particleResetTimer = 0.0f; - } - } - } - } -} - -void HandleReloadFinishOrFail(StringHash eventType, VariantMap& eventData) -{ - Resource@ res = cast(GetEventSender()); - // Only refresh inspector when reloading scripts (script attributes may change) - if (res !is null && (res.typeName == "ScriptFile" || res.typeName == "LuaFile")) - attributesFullDirty = true; -} - -void LoadConfig() -{ - if (!fileSystem.FileExists(configFileName)) - return; - - XMLFile config; - config.Load(File(configFileName, FILE_READ)); - - XMLElement configElem = config.root; - if (configElem.isNull) - return; - - XMLElement cameraElem = configElem.GetChild("camera"); - XMLElement objectElem = configElem.GetChild("object"); - XMLElement renderingElem = configElem.GetChild("rendering"); - XMLElement uiElem = configElem.GetChild("ui"); - XMLElement hierarchyElem = configElem.GetChild("hierarchy"); - XMLElement inspectorElem = configElem.GetChild("attributeinspector"); - XMLElement viewElem = configElem.GetChild("view"); - XMLElement resourcesElem = configElem.GetChild("resources"); - XMLElement consoleElem = configElem.GetChild("console"); - XMLElement varNamesElem = configElem.GetChild("varnames"); - XMLElement soundTypesElem = configElem.GetChild("soundtypes"); - XMLElement cubeMapElem = configElem.GetChild("cubegen"); - XMLElement defaultTagsElem = configElem.GetChild("tags"); - - if (!cameraElem.isNull) - { - if (cameraElem.HasAttribute("nearclip")) viewNearClip = cameraElem.GetFloat("nearclip"); - if (cameraElem.HasAttribute("farclip")) viewFarClip = cameraElem.GetFloat("farclip"); - if (cameraElem.HasAttribute("fov")) viewFov = cameraElem.GetFloat("fov"); - if (cameraElem.HasAttribute("speed")) cameraBaseSpeed = cameraElem.GetFloat("speed"); - if (cameraElem.HasAttribute("limitrotation")) limitRotation = cameraElem.GetBool("limitrotation"); - if (cameraElem.HasAttribute("viewportmode")) viewportMode = cameraElem.GetU32("viewportmode"); - if (cameraElem.HasAttribute("mouseorbitmode")) mouseOrbitMode = cameraElem.GetI32("mouseorbitmode"); - if (cameraElem.HasAttribute("mmbpan")) mmbPanMode = cameraElem.GetBool("mmbpan"); - if (cameraElem.HasAttribute("rotatearoundselect")) rotateAroundSelect = cameraElem.GetBool("rotatearoundselect"); - - UpdateViewParameters(); - } - - if (!objectElem.isNull) - { - if (objectElem.HasAttribute("cameraflymode")) cameraFlyMode = objectElem.GetBool("cameraflymode"); - if (objectElem.HasAttribute("hotkeymode")) hotKeyMode = objectElem.GetI32("hotkeymode"); - if (objectElem.HasAttribute("newnodemode")) newNodeMode = objectElem.GetI32("newnodemode"); - if (objectElem.HasAttribute("movestep")) moveStep = objectElem.GetFloat("movestep"); - if (objectElem.HasAttribute("rotatestep")) rotateStep = objectElem.GetFloat("rotatestep"); - if (objectElem.HasAttribute("scalestep")) scaleStep = objectElem.GetFloat("scalestep"); - if (objectElem.HasAttribute("movesnap")) moveSnap = objectElem.GetBool("movesnap"); - if (objectElem.HasAttribute("rotatesnap")) rotateSnap = objectElem.GetBool("rotatesnap"); - if (objectElem.HasAttribute("scalesnap")) scaleSnap = objectElem.GetBool("scalesnap"); - if (objectElem.HasAttribute("applymateriallist")) applyMaterialList = objectElem.GetBool("applymateriallist"); - if (objectElem.HasAttribute("importoptions")) importOptions = objectElem.GetAttribute("importoptions"); - if (objectElem.HasAttribute("pickmode")) pickMode = objectElem.GetI32("pickmode"); - if (objectElem.HasAttribute("axismode")) axisMode = AxisMode(objectElem.GetI32("axismode")); - if (objectElem.HasAttribute("revertonpause")) revertOnPause = objectElem.GetBool("revertonpause"); - } - - if (!resourcesElem.isNull) - { - if (resourcesElem.HasAttribute("rememberresourcepath")) rememberResourcePath = resourcesElem.GetBool("rememberresourcepath"); - if (rememberResourcePath && resourcesElem.HasAttribute("resourcepath")) - { - String newResourcePath = resourcesElem.GetAttribute("resourcepath"); - if (fileSystem.DirExists(newResourcePath)) - SetResourcePath(resourcesElem.GetAttribute("resourcepath"), false); - } - if (resourcesElem.HasAttribute("importpath")) - { - String newImportPath = resourcesElem.GetAttribute("importpath"); - if (fileSystem.DirExists(newImportPath)) - uiImportPath = newImportPath; - } - if (resourcesElem.HasAttribute("recentscenes")) - { - uiRecentScenes = resourcesElem.GetAttribute("recentscenes").Split(';'); - } - } - - if (!renderingElem.isNull) - { - if (renderingElem.HasAttribute("renderpath")) renderPathName = renderingElem.GetAttribute("renderpath"); - if (renderingElem.HasAttribute("texturequality")) renderer.textureQuality = renderingElem.GetI32("texturequality"); - if (renderingElem.HasAttribute("materialquality")) renderer.materialQuality = renderingElem.GetI32("materialquality"); - if (renderingElem.HasAttribute("shadowresolution")) SetShadowResolution(renderingElem.GetI32("shadowresolution")); - if (renderingElem.HasAttribute("shadowquality")) renderer.shadowQuality = ShadowQuality(renderingElem.GetI32("shadowquality")); - if (renderingElem.HasAttribute("maxoccludertriangles")) renderer.maxOccluderTriangles = renderingElem.GetI32("maxoccludertriangles"); - if (renderingElem.HasAttribute("specularlighting")) renderer.specularLighting = renderingElem.GetBool("specularlighting"); - if (renderingElem.HasAttribute("dynamicinstancing")) renderer.dynamicInstancing = renderingElem.GetBool("dynamicinstancing"); - if (renderingElem.HasAttribute("framelimiter")) engine.maxFps = renderingElem.GetBool("framelimiter") ? 200 : 0; - if (renderingElem.HasAttribute("gammacorrection")) gammaCorrection = renderingElem.GetBool("gammacorrection"); - if (renderingElem.HasAttribute("hdr")) HDR = renderingElem.GetBool("hdr"); - if (renderingElem.HasAttribute("srgb")) graphics.sRGB = renderingElem.GetBool("srgb"); - } - - if (!uiElem.isNull) - { - if (uiElem.HasAttribute("minopacity")) uiMinOpacity = uiElem.GetFloat("minopacity"); - if (uiElem.HasAttribute("maxopacity")) uiMaxOpacity = uiElem.GetFloat("maxopacity"); - if (uiElem.HasAttribute("languageindex")) localization.SetLanguage(uiElem.GetI32("languageindex")); - } - - if (!hierarchyElem.isNull) - { - if (hierarchyElem.HasAttribute("showinternaluielement")) showInternalUIElement = hierarchyElem.GetBool("showinternaluielement"); - if (hierarchyElem.HasAttribute("showtemporaryobject")) showTemporaryObject = hierarchyElem.GetBool("showtemporaryobject"); - if (inspectorElem.HasAttribute("nodecolor")) nodeTextColor = inspectorElem.GetColor("nodecolor"); - if (inspectorElem.HasAttribute("componentcolor")) componentTextColor = inspectorElem.GetColor("componentcolor"); - } - - if (!inspectorElem.isNull) - { - if (inspectorElem.HasAttribute("originalcolor")) normalTextColor = inspectorElem.GetColor("originalcolor"); - if (inspectorElem.HasAttribute("modifiedcolor")) modifiedTextColor = inspectorElem.GetColor("modifiedcolor"); - if (inspectorElem.HasAttribute("noneditablecolor")) nonEditableTextColor = inspectorElem.GetColor("noneditablecolor"); - if (inspectorElem.HasAttribute("shownoneditable")) showNonEditableAttribute = inspectorElem.GetBool("shownoneditable"); - } - - if (!viewElem.isNull) - { - if (viewElem.HasAttribute("defaultzoneambientcolor")) renderer.defaultZone.ambientColor = viewElem.GetColor("defaultzoneambientcolor"); - if (viewElem.HasAttribute("defaultzonefogcolor")) renderer.defaultZone.fogColor = viewElem.GetColor("defaultzonefogcolor"); - if (viewElem.HasAttribute("defaultzonefogstart")) renderer.defaultZone.fogStart = viewElem.GetI32("defaultzonefogstart"); - if (viewElem.HasAttribute("defaultzonefogend")) renderer.defaultZone.fogEnd = viewElem.GetI32("defaultzonefogend"); - if (viewElem.HasAttribute("showgrid")) showGrid = viewElem.GetBool("showgrid"); - if (viewElem.HasAttribute("grid2dmode")) grid2DMode = viewElem.GetBool("grid2dmode"); - if (viewElem.HasAttribute("gridsize")) gridSize = viewElem.GetI32("gridsize"); - if (viewElem.HasAttribute("gridsubdivisions")) gridSubdivisions = viewElem.GetI32("gridsubdivisions"); - if (viewElem.HasAttribute("gridscale")) gridScale = viewElem.GetFloat("gridscale"); - if (viewElem.HasAttribute("gridcolor")) gridColor = viewElem.GetColor("gridcolor"); - if (viewElem.HasAttribute("gridsubdivisioncolor")) gridSubdivisionColor = viewElem.GetColor("gridsubdivisioncolor"); - } - - if (!consoleElem.isNull) - { - // Console does not exist yet at this point, so store the string in a global variable - if (consoleElem.HasAttribute("commandinterpreter")) consoleCommandInterpreter = consoleElem.GetAttribute("commandinterpreter"); - } - - if (!varNamesElem.isNull) - globalVarNames = varNamesElem.GetVariantMap(); - - if (!soundTypesElem.isNull) - LoadSoundTypes(soundTypesElem); - - if (!cubeMapElem.isNull) - { - cubeMapGen_Name = cubeMapElem.HasAttribute("name") ? cubeMapElem.GetAttribute("name") : ""; - cubeMapGen_Path = cubeMapElem.HasAttribute("path") ? cubeMapElem.GetAttribute("path") : cubemapDefaultOutputPath; - cubeMapGen_Size = cubeMapElem.HasAttribute("size") ? cubeMapElem.GetI32("size") : 128; - } - else - { - cubeMapGen_Name = ""; - cubeMapGen_Path = cubemapDefaultOutputPath; - cubeMapGen_Size = 128; - } - - if (!defaultTagsElem.isNull) - { - if (defaultTagsElem.HasAttribute("tags")) defaultTags = defaultTagsElem.GetAttribute("tags"); - } -} - -void SaveConfig() -{ - XMLFile config; - XMLElement configElem = config.CreateRoot("configuration"); - XMLElement cameraElem = configElem.CreateChild("camera"); - XMLElement objectElem = configElem.CreateChild("object"); - XMLElement renderingElem = configElem.CreateChild("rendering"); - XMLElement uiElem = configElem.CreateChild("ui"); - XMLElement hierarchyElem = configElem.CreateChild("hierarchy"); - XMLElement inspectorElem = configElem.CreateChild("attributeinspector"); - XMLElement viewElem = configElem.CreateChild("view"); - XMLElement resourcesElem = configElem.CreateChild("resources"); - XMLElement consoleElem = configElem.CreateChild("console"); - XMLElement varNamesElem = configElem.CreateChild("varnames"); - XMLElement soundTypesElem = configElem.CreateChild("soundtypes"); - XMLElement cubeGenElem = configElem.CreateChild("cubegen"); - XMLElement defaultTagsElem = configElem.CreateChild("tags"); - - cameraElem.SetFloat("nearclip", viewNearClip); - cameraElem.SetFloat("farclip", viewFarClip); - cameraElem.SetFloat("fov", viewFov); - cameraElem.SetFloat("speed", cameraBaseSpeed); - cameraElem.SetBool("limitrotation", limitRotation); - cameraElem.SetU32("viewportmode", viewportMode); - cameraElem.SetI32("mouseorbitmode", mouseOrbitMode); - cameraElem.SetBool("mmbpan", mmbPanMode); - cameraElem.SetBool("rotatearoundselect", rotateAroundSelect); - - objectElem.SetBool("cameraflymode", cameraFlyMode); - objectElem.SetI32("hotkeymode", hotKeyMode); - objectElem.SetI32("newnodemode", newNodeMode); - objectElem.SetFloat("movestep", moveStep); - objectElem.SetFloat("rotatestep", rotateStep); - objectElem.SetFloat("scalestep", scaleStep); - objectElem.SetBool("movesnap", moveSnap); - objectElem.SetBool("rotatesnap", rotateSnap); - objectElem.SetBool("scalesnap", scaleSnap); - objectElem.SetBool("applymateriallist", applyMaterialList); - objectElem.SetAttribute("importoptions", importOptions); - objectElem.SetI32("pickmode", pickMode); - objectElem.SetI32("axismode", axisMode); - objectElem.SetBool("revertonpause", revertOnPause); - - resourcesElem.SetBool("rememberresourcepath", rememberResourcePath); - resourcesElem.SetAttribute("resourcepath", sceneResourcePath); - resourcesElem.SetAttribute("importpath", uiImportPath); - resourcesElem.SetAttribute("recentscenes", Join(uiRecentScenes, ";")); - - if (renderer !is null && graphics !is null) - { - renderingElem.SetAttribute("renderpath", renderPathName); - renderingElem.SetI32("texturequality", renderer.textureQuality); - renderingElem.SetI32("materialquality", renderer.materialQuality); - renderingElem.SetI32("shadowresolution", GetShadowResolution()); - renderingElem.SetI32("maxoccludertriangles", renderer.maxOccluderTriangles); - renderingElem.SetBool("specularlighting", renderer.specularLighting); - renderingElem.SetI32("shadowquality", int(renderer.shadowQuality)); - renderingElem.SetBool("dynamicinstancing", renderer.dynamicInstancing); - } - - renderingElem.SetBool("framelimiter", engine.maxFps > 0); - renderingElem.SetBool("gammacorrection", gammaCorrection); - renderingElem.SetBool("hdr", HDR); - renderingElem.SetBool("srgb", graphics.sRGB); - - uiElem.SetFloat("minopacity", uiMinOpacity); - uiElem.SetFloat("maxopacity", uiMaxOpacity); - uiElem.SetI32("languageindex", localization.languageIndex); - - hierarchyElem.SetBool("showinternaluielement", showInternalUIElement); - hierarchyElem.SetBool("showtemporaryobject", showTemporaryObject); - inspectorElem.SetColor("nodecolor", nodeTextColor); - inspectorElem.SetColor("componentcolor", componentTextColor); - - inspectorElem.SetColor("originalcolor", normalTextColor); - inspectorElem.SetColor("modifiedcolor", modifiedTextColor); - inspectorElem.SetColor("noneditablecolor", nonEditableTextColor); - inspectorElem.SetBool("shownoneditable", showNonEditableAttribute); - - viewElem.SetBool("showgrid", showGrid); - viewElem.SetBool("grid2dmode", grid2DMode); - viewElem.SetColor("defaultzoneambientcolor", renderer.defaultZone.ambientColor); - viewElem.SetColor("defaultzonefogcolor", renderer.defaultZone.fogColor); - viewElem.SetFloat("defaultzonefogstart", renderer.defaultZone.fogStart); - viewElem.SetFloat("defaultzonefogend", renderer.defaultZone.fogEnd); - viewElem.SetI32("gridsize", gridSize); - viewElem.SetI32("gridsubdivisions", gridSubdivisions); - viewElem.SetFloat("gridscale", gridScale); - viewElem.SetColor("gridcolor", gridColor); - viewElem.SetColor("gridsubdivisioncolor", gridSubdivisionColor); - - consoleElem.SetAttribute("commandinterpreter", console.commandInterpreter); - - varNamesElem.SetVariantMap(globalVarNames); - - cubeGenElem.SetAttribute("name", cubeMapGen_Name); - cubeGenElem.SetAttribute("path", cubeMapGen_Path); - cubeGenElem.SetAttribute("size", cubeMapGen_Size); - - defaultTagsElem.SetAttribute("tags", defaultTags); - - SaveSoundTypes(soundTypesElem); - - config.Save(File(configFileName, FILE_WRITE)); -} - -void MakeBackup(const String&in fileName) -{ - fileSystem.Rename(fileName, fileName + ".old"); -} - -void RemoveBackup(bool success, const String&in fileName) -{ - if (success) - fileSystem.Delete(fileName + ".old"); -} +// Urho3D editor + +#include "Editor/Scripts/EditorHierarchyWindow.as" +#include "Editor/Scripts/EditorView.as" +#include "Editor/Scripts/EditorScene.as" +#include "Editor/Scripts/EditorActions.as" +#include "Editor/Scripts/EditorUIElement.as" +#include "Editor/Scripts/EditorGizmo.as" +#include "Editor/Scripts/EditorMaterial.as" +#include "Editor/Scripts/EditorParticleEffect.as" +#include "Editor/Scripts/EditorSettings.as" +#include "Editor/Scripts/EditorPreferences.as" +#include "Editor/Scripts/EditorToolBar.as" +#include "Editor/Scripts/EditorSecondaryToolbar.as" +#include "Editor/Scripts/EditorUI.as" +#include "Editor/Scripts/EditorImport.as" +#include "Editor/Scripts/EditorExport.as" +#include "Editor/Scripts/EditorResourceBrowser.as" +#include "Editor/Scripts/EditorSpawn.as" +#include "Editor/Scripts/EditorSoundType.as" +#include "Editor/Scripts/EditorTerrain.as" +#include "Editor/Scripts/EditorLayers.as" +#include "Editor/Scripts/EditorColorWheel.as" +#include "Editor/Scripts/EditorEventsHandlers.as" +#include "Editor/Scripts/EditorViewDebugIcons.as" +#include "Editor/Scripts/EditorViewSelectableOrigins.as" +#include "Editor/Scripts/EditorViewPaintSelection.as" +#include "Editor/Scripts/EditorDuplicator.as" + +String configFileName; + +void Start() +{ + // Assign the value ASAP because configFileName is needed on exit, including exit on error + configFileName = fileSystem.GetAppPreferencesDir("urho3d", "Editor") + "Config.xml"; + localization.LoadJSONFile("Editor/Strings/EditorStrings.json"); + + if (engine.headless) + { + ErrorDialog("Urho3D Editor", "Headless mode is not supported. The program will now exit."); + engine.Exit(); + return; + } + + // Use the first frame to setup when the resolution is initialized + SubscribeToEvent("Update", "FirstFrame"); + + SubscribeToEvent(input, "ExitRequested", "HandleExitRequested"); + + // Disable Editor auto exit, check first if it is OK to exit + engine.autoExit = false; + // Pause completely when minimized to save OS resources, reduce defocused framerate + engine.pauseMinimized = true; + engine.maxInactiveFps = 10; + // Enable console commands from the editor script + script.defaultScriptFile = scriptFile; + // Enable automatic resource reloading + cache.autoReloadResources = true; + // Return resources which exist but failed to load due to error, so that we will not lose resource refs + cache.returnFailedResources = true; + // Use OS mouse without grabbing it + input.mouseVisible = true; + // If input is scaled the double the UI size (High DPI display) + if (input.inputScale != Vector2::ONE) + { + // Should we use the inputScale itself to scale UI? + ui.scale = 2; + // When UI scale is increased, also set the UI atlas to nearest filtering to avoid artifacts + // (there is no padding) and to have a sharper look + Texture2D@ uiTex = cache.GetResource("Texture2D", "Textures/UI.png"); + if (uiTex !is null) + uiTex.filterMode = FILTER_NEAREST; + } + // Use system clipboard to allow transport of text in & out from the editor + ui.useSystemClipboard = true; +} + +void FirstFrame() +{ + // Create root scene node + CreateScene(); + // Load editor settings and preferences + LoadConfig(); + // Create user interface for the editor + CreateUI(); + // Create root UI element where all 'editable' UI elements would be parented to + CreateRootUIElement(); + // Load the initial scene if provided + ParseArguments(); + // Switch to real frame handler after initialization + SubscribeToEvent("Update", "HandleUpdate"); + SubscribeToEvent("ReloadFinished", "HandleReloadFinishOrFail"); + SubscribeToEvent("ReloadFailed", "HandleReloadFinishOrFail"); + EditorSubscribeToEvents(); +} + +void Stop() +{ + SaveConfig(); +} + +void ParseArguments() +{ + Array arguments = GetArguments(); + bool loaded = false; + + for (uint i = 1; i < arguments.length; ++i) + { + if (arguments[i].ToLower() == "-scene") + { + if (++i == arguments.length) + break; + + loaded = LoadScene(arguments[i]); + continue; + } + + if (arguments[i].ToLower() == "-language") + { + if (++i == arguments.length) + break; + + localization.SetLanguage(arguments[i]); + continue; + } + } + + if (!loaded) + ResetScene(); +} + +void HandleUpdate(StringHash eventType, VariantMap& eventData) +{ + float timeStep = eventData["TimeStep"].GetFloat(); + + DoResourceBrowserWork(); + UpdateView(timeStep); + UpdateViewports(timeStep); + UpdateStats(timeStep); + UpdateScene(timeStep); + UpdateTestAnimation(timeStep); + UpdateGizmo(); + UpdateDirtyUI(); + UpdateViewDebugIcons(); + UpdateOrigins(); + UpdatePaintSelection(); + + // Handle Particle Editor looping. + if (particleEffectWindow !is null and particleEffectWindow.visible) + { + if (!particleEffectEmitter.emitting) + { + if (particleResetTimer == 0.0f) + particleResetTimer = editParticleEffect.maxTimeToLive + 0.2f; + else + { + particleResetTimer = Max(particleResetTimer - timeStep, 0.0f); + if (particleResetTimer <= 0.0001f) + { + particleEffectEmitter.Reset(); + particleResetTimer = 0.0f; + } + } + } + } +} + +void HandleReloadFinishOrFail(StringHash eventType, VariantMap& eventData) +{ + Resource@ res = cast(GetEventSender()); + // Only refresh inspector when reloading scripts (script attributes may change) + if (res !is null && (res.typeName == "ScriptFile" || res.typeName == "LuaFile")) + attributesFullDirty = true; +} + +void LoadConfig() +{ + if (!fileSystem.FileExists(configFileName)) + return; + + XMLFile config; + config.Load(File(configFileName, FILE_READ)); + + XMLElement configElem = config.root; + if (configElem.isNull) + return; + + XMLElement cameraElem = configElem.GetChild("camera"); + XMLElement objectElem = configElem.GetChild("object"); + XMLElement renderingElem = configElem.GetChild("rendering"); + XMLElement uiElem = configElem.GetChild("ui"); + XMLElement hierarchyElem = configElem.GetChild("hierarchy"); + XMLElement inspectorElem = configElem.GetChild("attributeinspector"); + XMLElement viewElem = configElem.GetChild("view"); + XMLElement resourcesElem = configElem.GetChild("resources"); + XMLElement consoleElem = configElem.GetChild("console"); + XMLElement varNamesElem = configElem.GetChild("varnames"); + XMLElement soundTypesElem = configElem.GetChild("soundtypes"); + XMLElement cubeMapElem = configElem.GetChild("cubegen"); + XMLElement defaultTagsElem = configElem.GetChild("tags"); + + if (!cameraElem.isNull) + { + if (cameraElem.HasAttribute("nearclip")) viewNearClip = cameraElem.GetFloat("nearclip"); + if (cameraElem.HasAttribute("farclip")) viewFarClip = cameraElem.GetFloat("farclip"); + if (cameraElem.HasAttribute("fov")) viewFov = cameraElem.GetFloat("fov"); + if (cameraElem.HasAttribute("speed")) cameraBaseSpeed = cameraElem.GetFloat("speed"); + if (cameraElem.HasAttribute("limitrotation")) limitRotation = cameraElem.GetBool("limitrotation"); + if (cameraElem.HasAttribute("viewportmode")) viewportMode = cameraElem.GetU32("viewportmode"); + if (cameraElem.HasAttribute("mouseorbitmode")) mouseOrbitMode = cameraElem.GetI32("mouseorbitmode"); + if (cameraElem.HasAttribute("mmbpan")) mmbPanMode = cameraElem.GetBool("mmbpan"); + if (cameraElem.HasAttribute("rotatearoundselect")) rotateAroundSelect = cameraElem.GetBool("rotatearoundselect"); + + UpdateViewParameters(); + } + + if (!objectElem.isNull) + { + if (objectElem.HasAttribute("cameraflymode")) cameraFlyMode = objectElem.GetBool("cameraflymode"); + if (objectElem.HasAttribute("hotkeymode")) hotKeyMode = objectElem.GetI32("hotkeymode"); + if (objectElem.HasAttribute("newnodemode")) newNodeMode = objectElem.GetI32("newnodemode"); + if (objectElem.HasAttribute("movestep")) moveStep = objectElem.GetFloat("movestep"); + if (objectElem.HasAttribute("rotatestep")) rotateStep = objectElem.GetFloat("rotatestep"); + if (objectElem.HasAttribute("scalestep")) scaleStep = objectElem.GetFloat("scalestep"); + if (objectElem.HasAttribute("movesnap")) moveSnap = objectElem.GetBool("movesnap"); + if (objectElem.HasAttribute("rotatesnap")) rotateSnap = objectElem.GetBool("rotatesnap"); + if (objectElem.HasAttribute("scalesnap")) scaleSnap = objectElem.GetBool("scalesnap"); + if (objectElem.HasAttribute("applymateriallist")) applyMaterialList = objectElem.GetBool("applymateriallist"); + if (objectElem.HasAttribute("importoptions")) importOptions = objectElem.GetAttribute("importoptions"); + if (objectElem.HasAttribute("pickmode")) pickMode = objectElem.GetI32("pickmode"); + if (objectElem.HasAttribute("axismode")) axisMode = AxisMode(objectElem.GetI32("axismode")); + if (objectElem.HasAttribute("revertonpause")) revertOnPause = objectElem.GetBool("revertonpause"); + } + + if (!resourcesElem.isNull) + { + if (resourcesElem.HasAttribute("rememberresourcepath")) rememberResourcePath = resourcesElem.GetBool("rememberresourcepath"); + if (rememberResourcePath && resourcesElem.HasAttribute("resourcepath")) + { + String newResourcePath = resourcesElem.GetAttribute("resourcepath"); + if (fileSystem.DirExists(newResourcePath)) + SetResourcePath(resourcesElem.GetAttribute("resourcepath"), false); + } + if (resourcesElem.HasAttribute("importpath")) + { + String newImportPath = resourcesElem.GetAttribute("importpath"); + if (fileSystem.DirExists(newImportPath)) + uiImportPath = newImportPath; + } + if (resourcesElem.HasAttribute("recentscenes")) + { + uiRecentScenes = resourcesElem.GetAttribute("recentscenes").Split(';'); + } + } + + if (!renderingElem.isNull) + { + if (renderingElem.HasAttribute("renderpath")) renderPathName = renderingElem.GetAttribute("renderpath"); + if (renderingElem.HasAttribute("texturequality")) renderer.textureQuality = renderingElem.GetI32("texturequality"); + if (renderingElem.HasAttribute("materialquality")) renderer.materialQuality = renderingElem.GetI32("materialquality"); + if (renderingElem.HasAttribute("shadowresolution")) SetShadowResolution(renderingElem.GetI32("shadowresolution")); + if (renderingElem.HasAttribute("shadowquality")) renderer.shadowQuality = ShadowQuality(renderingElem.GetI32("shadowquality")); + if (renderingElem.HasAttribute("maxoccludertriangles")) renderer.maxOccluderTriangles = renderingElem.GetI32("maxoccludertriangles"); + if (renderingElem.HasAttribute("specularlighting")) renderer.specularLighting = renderingElem.GetBool("specularlighting"); + if (renderingElem.HasAttribute("dynamicinstancing")) renderer.dynamicInstancing = renderingElem.GetBool("dynamicinstancing"); + if (renderingElem.HasAttribute("framelimiter")) engine.maxFps = renderingElem.GetBool("framelimiter") ? 200 : 0; + if (renderingElem.HasAttribute("gammacorrection")) gammaCorrection = renderingElem.GetBool("gammacorrection"); + if (renderingElem.HasAttribute("hdr")) HDR = renderingElem.GetBool("hdr"); + if (renderingElem.HasAttribute("srgb")) graphics.sRGB = renderingElem.GetBool("srgb"); + } + + if (!uiElem.isNull) + { + if (uiElem.HasAttribute("minopacity")) uiMinOpacity = uiElem.GetFloat("minopacity"); + if (uiElem.HasAttribute("maxopacity")) uiMaxOpacity = uiElem.GetFloat("maxopacity"); + if (uiElem.HasAttribute("languageindex")) localization.SetLanguage(uiElem.GetI32("languageindex")); + } + + if (!hierarchyElem.isNull) + { + if (hierarchyElem.HasAttribute("showinternaluielement")) showInternalUIElement = hierarchyElem.GetBool("showinternaluielement"); + if (hierarchyElem.HasAttribute("showtemporaryobject")) showTemporaryObject = hierarchyElem.GetBool("showtemporaryobject"); + if (inspectorElem.HasAttribute("nodecolor")) nodeTextColor = inspectorElem.GetColor("nodecolor"); + if (inspectorElem.HasAttribute("componentcolor")) componentTextColor = inspectorElem.GetColor("componentcolor"); + } + + if (!inspectorElem.isNull) + { + if (inspectorElem.HasAttribute("originalcolor")) normalTextColor = inspectorElem.GetColor("originalcolor"); + if (inspectorElem.HasAttribute("modifiedcolor")) modifiedTextColor = inspectorElem.GetColor("modifiedcolor"); + if (inspectorElem.HasAttribute("noneditablecolor")) nonEditableTextColor = inspectorElem.GetColor("noneditablecolor"); + if (inspectorElem.HasAttribute("shownoneditable")) showNonEditableAttribute = inspectorElem.GetBool("shownoneditable"); + } + + if (!viewElem.isNull) + { + if (viewElem.HasAttribute("defaultzoneambientcolor")) renderer.defaultZone.ambientColor = viewElem.GetColor("defaultzoneambientcolor"); + if (viewElem.HasAttribute("defaultzonefogcolor")) renderer.defaultZone.fogColor = viewElem.GetColor("defaultzonefogcolor"); + if (viewElem.HasAttribute("defaultzonefogstart")) renderer.defaultZone.fogStart = viewElem.GetI32("defaultzonefogstart"); + if (viewElem.HasAttribute("defaultzonefogend")) renderer.defaultZone.fogEnd = viewElem.GetI32("defaultzonefogend"); + if (viewElem.HasAttribute("showgrid")) showGrid = viewElem.GetBool("showgrid"); + if (viewElem.HasAttribute("grid2dmode")) grid2DMode = viewElem.GetBool("grid2dmode"); + if (viewElem.HasAttribute("gridsize")) gridSize = viewElem.GetI32("gridsize"); + if (viewElem.HasAttribute("gridsubdivisions")) gridSubdivisions = viewElem.GetI32("gridsubdivisions"); + if (viewElem.HasAttribute("gridscale")) gridScale = viewElem.GetFloat("gridscale"); + if (viewElem.HasAttribute("gridcolor")) gridColor = viewElem.GetColor("gridcolor"); + if (viewElem.HasAttribute("gridsubdivisioncolor")) gridSubdivisionColor = viewElem.GetColor("gridsubdivisioncolor"); + } + + if (!consoleElem.isNull) + { + // Console does not exist yet at this point, so store the string in a global variable + if (consoleElem.HasAttribute("commandinterpreter")) consoleCommandInterpreter = consoleElem.GetAttribute("commandinterpreter"); + } + + if (!varNamesElem.isNull) + globalVarNames = varNamesElem.GetVariantMap(); + + if (!soundTypesElem.isNull) + LoadSoundTypes(soundTypesElem); + + if (!cubeMapElem.isNull) + { + cubeMapGen_Name = cubeMapElem.HasAttribute("name") ? cubeMapElem.GetAttribute("name") : ""; + cubeMapGen_Path = cubeMapElem.HasAttribute("path") ? cubeMapElem.GetAttribute("path") : cubemapDefaultOutputPath; + cubeMapGen_Size = cubeMapElem.HasAttribute("size") ? cubeMapElem.GetI32("size") : 128; + } + else + { + cubeMapGen_Name = ""; + cubeMapGen_Path = cubemapDefaultOutputPath; + cubeMapGen_Size = 128; + } + + if (!defaultTagsElem.isNull) + { + if (defaultTagsElem.HasAttribute("tags")) defaultTags = defaultTagsElem.GetAttribute("tags"); + } +} + +void SaveConfig() +{ + XMLFile config; + XMLElement configElem = config.CreateRoot("configuration"); + XMLElement cameraElem = configElem.CreateChild("camera"); + XMLElement objectElem = configElem.CreateChild("object"); + XMLElement renderingElem = configElem.CreateChild("rendering"); + XMLElement uiElem = configElem.CreateChild("ui"); + XMLElement hierarchyElem = configElem.CreateChild("hierarchy"); + XMLElement inspectorElem = configElem.CreateChild("attributeinspector"); + XMLElement viewElem = configElem.CreateChild("view"); + XMLElement resourcesElem = configElem.CreateChild("resources"); + XMLElement consoleElem = configElem.CreateChild("console"); + XMLElement varNamesElem = configElem.CreateChild("varnames"); + XMLElement soundTypesElem = configElem.CreateChild("soundtypes"); + XMLElement cubeGenElem = configElem.CreateChild("cubegen"); + XMLElement defaultTagsElem = configElem.CreateChild("tags"); + + cameraElem.SetFloat("nearclip", viewNearClip); + cameraElem.SetFloat("farclip", viewFarClip); + cameraElem.SetFloat("fov", viewFov); + cameraElem.SetFloat("speed", cameraBaseSpeed); + cameraElem.SetBool("limitrotation", limitRotation); + cameraElem.SetU32("viewportmode", viewportMode); + cameraElem.SetI32("mouseorbitmode", mouseOrbitMode); + cameraElem.SetBool("mmbpan", mmbPanMode); + cameraElem.SetBool("rotatearoundselect", rotateAroundSelect); + + objectElem.SetBool("cameraflymode", cameraFlyMode); + objectElem.SetI32("hotkeymode", hotKeyMode); + objectElem.SetI32("newnodemode", newNodeMode); + objectElem.SetFloat("movestep", moveStep); + objectElem.SetFloat("rotatestep", rotateStep); + objectElem.SetFloat("scalestep", scaleStep); + objectElem.SetBool("movesnap", moveSnap); + objectElem.SetBool("rotatesnap", rotateSnap); + objectElem.SetBool("scalesnap", scaleSnap); + objectElem.SetBool("applymateriallist", applyMaterialList); + objectElem.SetAttribute("importoptions", importOptions); + objectElem.SetI32("pickmode", pickMode); + objectElem.SetI32("axismode", axisMode); + objectElem.SetBool("revertonpause", revertOnPause); + + resourcesElem.SetBool("rememberresourcepath", rememberResourcePath); + resourcesElem.SetAttribute("resourcepath", sceneResourcePath); + resourcesElem.SetAttribute("importpath", uiImportPath); + resourcesElem.SetAttribute("recentscenes", Join(uiRecentScenes, ";")); + + if (renderer !is null && graphics !is null) + { + renderingElem.SetAttribute("renderpath", renderPathName); + renderingElem.SetI32("texturequality", renderer.textureQuality); + renderingElem.SetI32("materialquality", renderer.materialQuality); + renderingElem.SetI32("shadowresolution", GetShadowResolution()); + renderingElem.SetI32("maxoccludertriangles", renderer.maxOccluderTriangles); + renderingElem.SetBool("specularlighting", renderer.specularLighting); + renderingElem.SetI32("shadowquality", int(renderer.shadowQuality)); + renderingElem.SetBool("dynamicinstancing", renderer.dynamicInstancing); + } + + renderingElem.SetBool("framelimiter", engine.maxFps > 0); + renderingElem.SetBool("gammacorrection", gammaCorrection); + renderingElem.SetBool("hdr", HDR); + renderingElem.SetBool("srgb", graphics.sRGB); + + uiElem.SetFloat("minopacity", uiMinOpacity); + uiElem.SetFloat("maxopacity", uiMaxOpacity); + uiElem.SetI32("languageindex", localization.languageIndex); + + hierarchyElem.SetBool("showinternaluielement", showInternalUIElement); + hierarchyElem.SetBool("showtemporaryobject", showTemporaryObject); + inspectorElem.SetColor("nodecolor", nodeTextColor); + inspectorElem.SetColor("componentcolor", componentTextColor); + + inspectorElem.SetColor("originalcolor", normalTextColor); + inspectorElem.SetColor("modifiedcolor", modifiedTextColor); + inspectorElem.SetColor("noneditablecolor", nonEditableTextColor); + inspectorElem.SetBool("shownoneditable", showNonEditableAttribute); + + viewElem.SetBool("showgrid", showGrid); + viewElem.SetBool("grid2dmode", grid2DMode); + viewElem.SetColor("defaultzoneambientcolor", renderer.defaultZone.ambientColor); + viewElem.SetColor("defaultzonefogcolor", renderer.defaultZone.fogColor); + viewElem.SetFloat("defaultzonefogstart", renderer.defaultZone.fogStart); + viewElem.SetFloat("defaultzonefogend", renderer.defaultZone.fogEnd); + viewElem.SetI32("gridsize", gridSize); + viewElem.SetI32("gridsubdivisions", gridSubdivisions); + viewElem.SetFloat("gridscale", gridScale); + viewElem.SetColor("gridcolor", gridColor); + viewElem.SetColor("gridsubdivisioncolor", gridSubdivisionColor); + + consoleElem.SetAttribute("commandinterpreter", console.commandInterpreter); + + varNamesElem.SetVariantMap(globalVarNames); + + cubeGenElem.SetAttribute("name", cubeMapGen_Name); + cubeGenElem.SetAttribute("path", cubeMapGen_Path); + cubeGenElem.SetAttribute("size", cubeMapGen_Size); + + defaultTagsElem.SetAttribute("tags", defaultTags); + + SaveSoundTypes(soundTypesElem); + + config.Save(File(configFileName, FILE_WRITE)); +} + +void MakeBackup(const String&in fileName) +{ + fileSystem.Rename(fileName, fileName + ".old"); +} + +void RemoveBackup(bool success, const String&in fileName) +{ + if (success) + fileSystem.Delete(fileName + ".old"); +} diff --git a/bin/Data/Scripts/Editor/EditorActions.as b/bin/EditorData/Editor/Scripts/EditorActions.as similarity index 96% rename from bin/Data/Scripts/Editor/EditorActions.as rename to bin/EditorData/Editor/Scripts/EditorActions.as index f7d8d590d12..f5e470c356e 100644 --- a/bin/Data/Scripts/Editor/EditorActions.as +++ b/bin/EditorData/Editor/Scripts/EditorActions.as @@ -1,1152 +1,1152 @@ -class EditAction -{ - void Undo() - { - } - - void Redo() - { - } -} - -class EditActionGroup -{ - Array actions; -} - -class CreateDrawableMaskAction : EditAction -{ - uint nodeID; - uint drawableID; - int oldMask; - int redoMask; - int typeMask; - - void Define(Drawable@ drawable, int editMaskType) - { - drawableID = drawable.id; - nodeID = drawable.node.id; - - switch (editMaskType) - { - case EDIT_VIEW_MASK: - oldMask = drawable.viewMask; - break; - case EDIT_LIGHT_MASK: - oldMask = drawable.lightMask; - break; - case EDIT_SHADOW_MASK: - oldMask = drawable.shadowMask; - break; - case EDIT_ZONE_MASK: - oldMask = drawable.zoneMask; - break; - } - - typeMask = editMaskType; - redoMask = oldMask; - } - - void Undo() - { - Node@ node = editorScene.GetNode(nodeID); - Drawable@ drawable = editorScene.GetComponent(drawableID); - if (node !is null && drawable !is null) - { - switch (typeMask) - { - case EDIT_VIEW_MASK: - redoMask = drawable.viewMask; - drawable.viewMask = oldMask; - break; - case EDIT_LIGHT_MASK: - redoMask = drawable.lightMask; - drawable.lightMask = oldMask; - break; - case EDIT_SHADOW_MASK: - redoMask = drawable.shadowMask; - drawable.shadowMask = oldMask; - break; - case EDIT_ZONE_MASK: - redoMask = drawable.zoneMask; - drawable.zoneMask = oldMask; - break; - } - } - } - - void Redo() - { - Node@ node = editorScene.GetNode(nodeID); - Drawable@ drawable = editorScene.GetComponent(drawableID); - if (node !is null && drawable !is null) - { - switch (typeMask) - { - case EDIT_VIEW_MASK: - oldMask = drawable.viewMask; - drawable.viewMask = redoMask; - break; - case EDIT_LIGHT_MASK: - oldMask = drawable.lightMask; - drawable.lightMask = redoMask; - break; - case EDIT_SHADOW_MASK: - oldMask = drawable.shadowMask; - drawable.shadowMask = redoMask; - break; - case EDIT_ZONE_MASK: - oldMask = drawable.zoneMask; - drawable.zoneMask = redoMask; - break; - } - } - } -} - -class CreateNodeAction : EditAction -{ - uint nodeID; - uint parentID; - XMLFile@ nodeData; - - void Define(Node@ node) - { - nodeID = node.id; - parentID = node.parent.id; - nodeData = XMLFile(); - XMLElement rootElem = nodeData.CreateRoot("node"); - node.SaveXML(rootElem); - } - - void Undo() - { - Node@ parent = editorScene.GetNode(parentID); - Node@ node = editorScene.GetNode(nodeID); - if (parent !is null && node !is null) - { - parent.RemoveChild(node); - hierarchyList.ClearSelection(); - } - } - - void Redo() - { - Node@ parent = editorScene.GetNode(parentID); - if (parent !is null) - { - Node@ node = parent.CreateChild("", Scene::IsReplicatedID(nodeID) ? REPLICATED : LOCAL, nodeID); - node.LoadXML(nodeData.root); - FocusNode(node); - } - } -} - -class DeleteNodeAction : EditAction -{ - uint nodeID; - uint parentID; - XMLFile@ nodeData; - - void Define(Node@ node) - { - nodeID = node.id; - parentID = node.parent.id; - nodeData = XMLFile(); - XMLElement rootElem = nodeData.CreateRoot("node"); - node.SaveXML(rootElem); - rootElem.SetU32("listItemIndex", GetListIndex(node)); - } - - void Undo() - { - Node@ parent = editorScene.GetNode(parentID); - if (parent !is null) - { - // Handle update manually so that the node can be reinserted back into its previous list index - suppressSceneChanges = true; - - Node@ node = parent.CreateChild("", Scene::IsReplicatedID(nodeID) ? REPLICATED : LOCAL, nodeID); - if (node.LoadXML(nodeData.root)) - { - uint listItemIndex = nodeData.root.GetU32("listItemIndex"); - UIElement@ parentItem = hierarchyList.items[GetListIndex(parent)]; - UpdateHierarchyItem(listItemIndex, node, parentItem); - FocusNode(node); - } - - suppressSceneChanges = false; - } - } - - void Redo() - { - Node@ parent = editorScene.GetNode(parentID); - Node@ node = editorScene.GetNode(nodeID); - if (parent !is null && node !is null) - { - parent.RemoveChild(node); - hierarchyList.ClearSelection(); - } - } -} - -class ReparentNodeAction : EditAction -{ - uint nodeID; - uint oldParentID; - uint newParentID; - Array nodeList; // 2 uints get inserted per node (node, node.parent) - bool multiple; - - void Define(Node@ node, Node@ newParent) - { - multiple = false; - nodeID = node.id; - oldParentID = node.parent.id; - newParentID = newParent.id; - } - - void Define(Array nodes, Node@ newParent) - { - multiple = true; - newParentID = newParent.id; - for(uint i = 0; i < nodes.length; ++i) - { - Node@ node = nodes[i]; - nodeList.Push(node.id); - nodeList.Push(node.parent.id); - } - } - - void Undo() - { - if (multiple) - { - for (uint i = 0; i < nodeList.length; i+=2) - { - uint nodeID_ = nodeList[i]; - uint oldParentID_ = nodeList[i+1]; - Node@ parent = editorScene.GetNode(oldParentID_); - Node@ node = editorScene.GetNode(nodeID_); - if (parent !is null && node !is null) - node.parent = parent; - } - } - else - { - Node@ parent = editorScene.GetNode(oldParentID); - Node@ node = editorScene.GetNode(nodeID); - if (parent !is null && node !is null) - node.parent = parent; - } - } - - void Redo() - { - if (multiple) - { - Node@ parent = editorScene.GetNode(newParentID); - if (parent is null) - return; - - for (uint i = 0; i < nodeList.length; i+=2) - { - uint nodeID_ = nodeList[i]; - Node@ node = editorScene.GetNode(nodeID_); - if (node !is null) - node.parent = parent; - } - } - else - { - Node@ parent = editorScene.GetNode(newParentID); - Node@ node = editorScene.GetNode(nodeID); - if (parent !is null && node !is null) - node.parent = parent; - } - } -} - -class ReorderNodeAction : EditAction -{ - uint nodeID; - uint parentID; - uint oldChildIndex; - uint newChildIndex; - - void Define(Node@ node, uint newIndex) - { - nodeID = node.id; - parentID = node.parent.id; - oldChildIndex = SceneFindChildIndex(node.parent, node); - newChildIndex = newIndex; - } - - void Undo() - { - Node@ parent = editorScene.GetNode(parentID); - Node@ node = editorScene.GetNode(nodeID); - if (parent !is null && node !is null) - PerformReorder(parent, node, oldChildIndex); - } - - void Redo() - { - Node@ parent = editorScene.GetNode(parentID); - Node@ node = editorScene.GetNode(nodeID); - if (parent !is null && node !is null) - PerformReorder(parent, node, newChildIndex); - } -} - -class ReorderComponentAction : EditAction -{ - uint componentID; - uint nodeID; - uint oldComponentIndex; - uint newComponentIndex; - - void Define(Component@ component, uint newIndex) - { - componentID = component.id; - nodeID = component.node.id; - oldComponentIndex = SceneFindComponentIndex(component.node, component); - newComponentIndex = newIndex; - } - - void Undo() - { - Node@ node = editorScene.GetNode(nodeID); - Component@ component = editorScene.GetComponent(componentID); - if (node !is null && component !is null) - PerformReorder(node, component, oldComponentIndex); - } - - void Redo() - { - Node@ node = editorScene.GetNode(nodeID); - Component@ component = editorScene.GetComponent(componentID); - if (node !is null && component !is null) - PerformReorder(node, component, newComponentIndex); - } -} - -class CreateComponentAction : EditAction -{ - uint nodeID; - uint componentID; - XMLFile@ componentData; - - void Define(Component@ component) - { - componentID = component.id; - nodeID = component.node.id; - componentData = XMLFile(); - XMLElement rootElem = componentData.CreateRoot("component"); - component.SaveXML(rootElem); - } - - void Undo() - { - Node@ node = editorScene.GetNode(nodeID); - Component@ component = editorScene.GetComponent(componentID); - if (node !is null && component !is null) - { - node.RemoveComponent(component); - hierarchyList.ClearSelection(); - } - } - - void Redo() - { - Node@ node = editorScene.GetNode(nodeID); - if (node !is null) - { - Component@ component = node.CreateComponent(componentData.root.GetAttribute("type"), Scene::IsReplicatedID(componentID) ? - REPLICATED : LOCAL, componentID); - component.LoadXML(componentData.root); - component.ApplyAttributes(); - FocusComponent(component); - } - } - -} - -class DeleteComponentAction : EditAction -{ - uint nodeID; - uint componentID; - XMLFile@ componentData; - - void Define(Component@ component) - { - componentID = component.id; - nodeID = component.node.id; - componentData = XMLFile(); - XMLElement rootElem = componentData.CreateRoot("component"); - component.SaveXML(rootElem); - rootElem.SetU32("listItemIndex", GetComponentListIndex(component)); - } - - void Undo() - { - Node@ node = editorScene.GetNode(nodeID); - if (node !is null) - { - // Handle update manually so that the component can be reinserted back into its previous list index - suppressSceneChanges = true; - - Component@ component = node.CreateComponent(componentData.root.GetAttribute("type"), Scene::IsReplicatedID(componentID) ? - REPLICATED : LOCAL, componentID); - if (component.LoadXML(componentData.root)) - { - component.ApplyAttributes(); - - uint listItemIndex = componentData.root.GetU32("listItemIndex"); - UIElement@ parentItem = hierarchyList.items[GetListIndex(node)]; - UpdateHierarchyItem(listItemIndex, component, parentItem); - FocusComponent(component); - } - - suppressSceneChanges = false; - } - } - - void Redo() - { - Node@ node = editorScene.GetNode(nodeID); - Component@ component = editorScene.GetComponent(componentID); - if (node !is null && component !is null) - { - node.RemoveComponent(component); - hierarchyList.ClearSelection(); - } - } -} - -class EditAttributeAction : EditAction -{ - int targetType; - uint targetID; - uint attrIndex; - Variant undoValue; - Variant redoValue; - - void Define(Serializable@ target, uint index, const Variant&in oldValue) - { - attrIndex = index; - undoValue = oldValue; - redoValue = target.attributes[index]; - - targetType = GetType(target); - targetID = GetID(target, targetType); - } - - Serializable@ GetTarget() - { - switch (targetType) - { - case ITEM_NODE: - return editorScene.GetNode(targetID); - case ITEM_COMPONENT: - return editorScene.GetComponent(targetID); - case ITEM_UI_ELEMENT: - return GetUIElementByID(targetID); - } - - return null; - } - - void Undo() - { - Serializable@ target = GetTarget(); - if (target !is null) - { - target.attributes[attrIndex] = undoValue; - target.ApplyAttributes(); - // Can't know if need a full update, so assume true - attributesFullDirty = true; - // Apply side effects - PostEditAttribute(target, attrIndex); - - if (targetType == ITEM_UI_ELEMENT) - SetUIElementModified(target); - else - SetSceneModified(); - } - } - - void Redo() - { - Serializable@ target = GetTarget(); - if (target !is null) - { - target.attributes[attrIndex] = redoValue; - target.ApplyAttributes(); - // Can't know if need a full update, so assume true - attributesFullDirty = true; - // Apply side effects - PostEditAttribute(target, attrIndex); - - if (targetType == ITEM_UI_ELEMENT) - SetUIElementModified(target); - else - SetSceneModified(); - } - } -} - -class ResetAttributesAction : EditAction -{ - int targetType; - uint targetID; - Array undoValues; - VariantMap internalVars; // UIElement specific - - void Define(Serializable@ target) - { - for (uint i = 0; i < target.numAttributes; ++i) - undoValues.Push(target.attributes[i]); - - targetType = GetType(target); - targetID = GetID(target, targetType); - - if (targetType == ITEM_UI_ELEMENT) - { - // Special handling for UIElement to preserve the internal variables containing the element's generated ID among others - UIElement@ element = target; - Array keys = element.vars.keys; - for (uint i = 0; i < keys.length; ++i) - { - // If variable name is empty (or unregistered) then it is an internal variable and should be preserved - String name = GetVarName(keys[i]); - if (name.empty) - internalVars[keys[i]] = element.vars[keys[i]]; - } - } - } - - Serializable@ GetTarget() - { - switch (targetType) - { - case ITEM_NODE: - return editorScene.GetNode(targetID); - case ITEM_COMPONENT: - return editorScene.GetComponent(targetID); - case ITEM_UI_ELEMENT: - return GetUIElementByID(targetID); - } - - return null; - } - - void SetInternalVars(UIElement@ element) - { - // Revert back internal variables - Array keys = internalVars.keys; - for (uint i = 0; i < keys.length; ++i) - element.vars[keys[i]] = internalVars[keys[i]]; - - if (element.vars.Contains(FILENAME_VAR)) - CenterDialog(element); - } - - void Undo() - { - ui.cursor.shape = CS_BUSY; - - Serializable@ target = GetTarget(); - if (target !is null) - { - for (uint i = 0; i < target.numAttributes; ++i) - { - AttributeInfo info = target.attributeInfos[i]; - if (info.mode & AM_NOEDIT != 0 || info.mode & AM_NODEID != 0 || info.mode & AM_COMPONENTID != 0) - continue; - - target.attributes[i] = undoValues[i]; - } - target.ApplyAttributes(); - - // Apply side effects - for (uint i = 0; i < target.numAttributes; ++i) - PostEditAttribute(target, i); - - if (targetType == ITEM_UI_ELEMENT) - SetUIElementModified(target); - else - SetSceneModified(); - - attributesFullDirty = true; - } - } - - void Redo() - { - ui.cursor.shape = CS_BUSY; - - Serializable@ target = GetTarget(); - if (target !is null) - { - for (uint i = 0; i < target.numAttributes; ++i) - { - AttributeInfo info = target.attributeInfos[i]; - if (info.mode & AM_NOEDIT != 0 || info.mode & AM_NODEID != 0 || info.mode & AM_COMPONENTID != 0) - continue; - - target.attributes[i] = target.attributeDefaults[i]; - } - if (targetType == ITEM_UI_ELEMENT) - SetInternalVars(target); - target.ApplyAttributes(); - - // Apply side effects - for (uint i = 0; i < target.numAttributes; ++i) - PostEditAttribute(target, i); - - if (targetType == ITEM_UI_ELEMENT) - SetUIElementModified(target); - else - SetSceneModified(); - - attributesFullDirty = true; - } - } -} - -class ToggleNodeEnabledAction : EditAction -{ - uint nodeID; - bool undoValue; - - void Define(Node@ node, bool oldEnabled) - { - nodeID = node.id; - undoValue = oldEnabled; - } - - void Undo() - { - Node@ node = editorScene.GetNode(nodeID); - if (node !is null) - node.SetEnabledRecursive(undoValue); - } - - void Redo() - { - Node@ node = editorScene.GetNode(nodeID); - if (node !is null) - node.SetEnabledRecursive(!undoValue); - } -} - -class Transform -{ - Vector3 position; - Quaternion rotation; - Vector3 scale; - - void Define(Node@ node) - { - position = node.position; - rotation = node.rotation; - scale = node.scale; - } - - void Apply(Node@ node) - { - node.SetTransform(position, rotation, scale); - } -} - -class EditNodeTransformAction : EditAction -{ - uint nodeID; - Transform undoTransform; - Transform redoTransform; - - void Define(Node@ node, const Transform&in oldTransform) - { - nodeID = node.id; - undoTransform = oldTransform; - redoTransform.Define(node); - } - - void Undo() - { - Node@ node = editorScene.GetNode(nodeID); - if (node !is null) - { - undoTransform.Apply(node); - UpdateNodeAttributes(); - } - } - - void Redo() - { - Node@ node = editorScene.GetNode(nodeID); - if (node !is null) - { - redoTransform.Apply(node); - UpdateNodeAttributes(); - } - } -} - -class CreateUIElementAction : EditAction -{ - Variant elementID; - Variant parentID; - XMLFile@ elementData; - XMLFile@ styleFile; - - void Define(UIElement@ element) - { - elementID = GetUIElementID(element); - parentID = GetUIElementID(element.parent); - elementData = XMLFile(); - XMLElement rootElem = elementData.CreateRoot("element"); - element.SaveXML(rootElem); - styleFile = element.defaultStyle; - } - - void Undo() - { - UIElement@ parent = GetUIElementByID(parentID); - UIElement@ element = GetUIElementByID(elementID); - if (parent !is null && element !is null) - { - parent.RemoveChild(element); - hierarchyList.ClearSelection(); - SetUIElementModified(parent); - } - } - - void Redo() - { - UIElement@ parent = GetUIElementByID(parentID); - if (parent !is null) - { - // Have to update manually because the element ID var is not set yet when the E_ELEMENTADDED event is sent - suppressUIElementChanges = true; - - if (parent.LoadChildXML(elementData.root, styleFile) !is null) - { - UIElement@ element = parent.children[parent.numChildren - 1]; - UpdateHierarchyItem(element); - FocusUIElement(element); - SetUIElementModified(parent); - } - - suppressUIElementChanges = false; - - } - } -} - -class DeleteUIElementAction : EditAction -{ - Variant elementID; - Variant parentID; - XMLFile@ elementData; - XMLFile@ styleFile; - - void Define(UIElement@ element) - { - elementID = GetUIElementID(element); - parentID = GetUIElementID(element.parent); - elementData = XMLFile(); - XMLElement rootElem = elementData.CreateRoot("element"); - element.SaveXML(rootElem); - rootElem.SetU32("index", element.parent.FindChild(element)); - rootElem.SetU32("listItemIndex", GetListIndex(element)); - styleFile = element.defaultStyle; - } - - void Undo() - { - UIElement@ parent = GetUIElementByID(parentID); - if (parent !is null) - { - // Have to update manually because the element ID var is not set yet when the E_ELEMENTADDED event is sent - suppressUIElementChanges = true; - - if (parent.LoadChildXML(elementData.root, styleFile) !is null) - { - XMLElement rootElem = elementData.root; - uint index = rootElem.GetU32("index"); - uint listItemIndex = rootElem.GetU32("listItemIndex"); - UIElement@ element = parent.children[index]; - UIElement@ parentItem = hierarchyList.items[GetListIndex(parent)]; - UpdateHierarchyItem(listItemIndex, element, parentItem); - FocusUIElement(element); - SetUIElementModified(parent); - } - - suppressUIElementChanges = false; - } - } - - void Redo() - { - UIElement@ parent = GetUIElementByID(parentID); - UIElement@ element = GetUIElementByID(elementID); - if (parent !is null && element !is null) - { - parent.RemoveChild(element); - hierarchyList.ClearSelection(); - SetUIElementModified(parent); - } - } -} - -class ReparentUIElementAction : EditAction -{ - Variant elementID; - Variant oldParentID; - uint oldChildIndex; - Variant newParentID; - - void Define(UIElement@ element, UIElement@ newParent) - { - elementID = GetUIElementID(element); - oldParentID = GetUIElementID(element.parent); - oldChildIndex = element.parent.FindChild(element); - newParentID = GetUIElementID(newParent); - } - - void Undo() - { - UIElement@ parent = GetUIElementByID(oldParentID); - UIElement@ element = GetUIElementByID(elementID); - if (parent !is null && element !is null) - { - element.SetParent(parent, oldChildIndex); - SetUIElementModified(parent); - } - } - - void Redo() - { - UIElement@ parent = GetUIElementByID(newParentID); - UIElement@ element = GetUIElementByID(elementID); - if (parent !is null && element !is null) - { - element.parent = parent; - SetUIElementModified(parent); - } - } -} - -class ReorderUIElementAction : EditAction -{ - Variant elementID; - Variant parentID; - uint oldChildIndex; - uint newChildIndex; - - void Define(UIElement@ element, uint newIndex) - { - elementID = GetUIElementID(element); - parentID = GetUIElementID(element.parent); - oldChildIndex = element.parent.FindChild(element); - newChildIndex = newIndex; - } - - void Undo() - { - UIElement@ parent = GetUIElementByID(parentID); - UIElement@ element = GetUIElementByID(elementID); - if (parent !is null && element !is null) - PerformReorder(parent, element, oldChildIndex); - } - - void Redo() - { - UIElement@ parent = GetUIElementByID(parentID); - UIElement@ element = GetUIElementByID(elementID); - if (parent !is null && element !is null) - PerformReorder(parent, element, newChildIndex); - } -} - -class ApplyUIElementStyleAction : EditAction -{ - Variant elementID; - Variant parentID; - XMLFile@ elementData; - XMLFile@ styleFile; - String elementOldStyle; - String elementNewStyle; - - void Define(UIElement@ element, const String&in newStyle) - { - elementID = GetUIElementID(element); - parentID = GetUIElementID(element.parent); - elementData = XMLFile(); - XMLElement rootElem = elementData.CreateRoot("element"); - element.SaveXML(rootElem); - rootElem.SetU32("index", element.parent.FindChild(element)); - rootElem.SetU32("listItemIndex", GetListIndex(element)); - styleFile = element.defaultStyle; - elementOldStyle = element.style; - elementNewStyle = newStyle; - } - - void ApplyStyle(const String&in style) - { - UIElement@ parent = GetUIElementByID(parentID); - UIElement@ element = GetUIElementByID(elementID); - if (parent !is null && element !is null) - { - // Apply the style in the XML data - elementData.root.SetAttribute("style", style); - - // Have to update manually because the element ID var is not set yet when the E_ELEMENTADDED event is sent - suppressUIElementChanges = true; - - parent.RemoveChild(element); - if (parent.LoadChildXML(elementData.root, styleFile) !is null) - { - XMLElement rootElem = elementData.root; - uint index = rootElem.GetU32("index"); - uint listItemIndex = rootElem.GetU32("listItemIndex"); - UIElement@ elem = parent.children[index]; - UIElement@ parentItem = hierarchyList.items[GetListIndex(parent)]; - UpdateHierarchyItem(listItemIndex, elem, parentItem); - SetUIElementModified(elem); - hierarchyUpdateSelections.Push(listItemIndex); - } - - suppressUIElementChanges = false; - } - } - - void Undo() - { - ApplyStyle(elementOldStyle); - } - - void Redo() - { - ApplyStyle(elementNewStyle); - } -} - -class EditMaterialAction : EditAction -{ - XMLFile@ oldState; - XMLFile@ newState; - WeakHandle material; - - void Define(Material@ material_, XMLFile@ oldState_) - { - material = material_; - oldState = oldState_; - newState = XMLFile(); - - XMLElement materialElem = newState.CreateRoot("material"); - material_.Save(materialElem); - } - - void Undo() - { - Material@ mat = material.Get(); - if (mat !is null) - { - mat.Load(oldState.root); - RefreshMaterialEditor(); - } - } - - void Redo() - { - Material@ mat = material.Get(); - if (mat !is null) - { - mat.Load(newState.root); - RefreshMaterialEditor(); - } - } -} - -class EditParticleEffectAction : EditAction -{ - XMLFile@ oldState; - XMLFile@ newState; - WeakHandle particleEffect; - ParticleEmitter@ particleEmitter; - - void Define(ParticleEmitter@ particleEmitter_, ParticleEffect@ particleEffect_, XMLFile@ oldState_) - { - particleEmitter = particleEmitter_; - particleEffect = particleEffect_; - oldState = oldState_; - newState = XMLFile(); - - XMLElement particleElem = newState.CreateRoot("particleeffect"); - particleEffect_.Save(particleElem); - } - - void Undo() - { - ParticleEffect@ effect = particleEffect.Get(); - if (effect !is null) - { - effect.Load(oldState.root); - particleEmitter.ApplyEffect(); - RefreshParticleEffectEditor(); - } - } - - void Redo() - { - ParticleEffect@ effect = particleEffect.Get(); - if (effect !is null) - { - effect.Load(newState.root); - particleEmitter.ApplyEffect(); - RefreshParticleEffectEditor(); - } - } -} - -class AssignMaterialAction : EditAction -{ - WeakHandle model; - Array oldMaterials; - String newMaterialName; - - void Define(StaticModel@ model_, Array oldMaterials_, Material@ newMaterial_) - { - model = model_; - oldMaterials = oldMaterials_; - newMaterialName = newMaterial_.name; - } - - void Undo() - { - StaticModel@ staticModel = model.Get(); - if (staticModel is null) - return; - - for(uint i=0; i actions; +} + +class CreateDrawableMaskAction : EditAction +{ + uint nodeID; + uint drawableID; + int oldMask; + int redoMask; + int typeMask; + + void Define(Drawable@ drawable, int editMaskType) + { + drawableID = drawable.id; + nodeID = drawable.node.id; + + switch (editMaskType) + { + case EDIT_VIEW_MASK: + oldMask = drawable.viewMask; + break; + case EDIT_LIGHT_MASK: + oldMask = drawable.lightMask; + break; + case EDIT_SHADOW_MASK: + oldMask = drawable.shadowMask; + break; + case EDIT_ZONE_MASK: + oldMask = drawable.zoneMask; + break; + } + + typeMask = editMaskType; + redoMask = oldMask; + } + + void Undo() + { + Node@ node = editorScene.GetNode(nodeID); + Drawable@ drawable = editorScene.GetComponent(drawableID); + if (node !is null && drawable !is null) + { + switch (typeMask) + { + case EDIT_VIEW_MASK: + redoMask = drawable.viewMask; + drawable.viewMask = oldMask; + break; + case EDIT_LIGHT_MASK: + redoMask = drawable.lightMask; + drawable.lightMask = oldMask; + break; + case EDIT_SHADOW_MASK: + redoMask = drawable.shadowMask; + drawable.shadowMask = oldMask; + break; + case EDIT_ZONE_MASK: + redoMask = drawable.zoneMask; + drawable.zoneMask = oldMask; + break; + } + } + } + + void Redo() + { + Node@ node = editorScene.GetNode(nodeID); + Drawable@ drawable = editorScene.GetComponent(drawableID); + if (node !is null && drawable !is null) + { + switch (typeMask) + { + case EDIT_VIEW_MASK: + oldMask = drawable.viewMask; + drawable.viewMask = redoMask; + break; + case EDIT_LIGHT_MASK: + oldMask = drawable.lightMask; + drawable.lightMask = redoMask; + break; + case EDIT_SHADOW_MASK: + oldMask = drawable.shadowMask; + drawable.shadowMask = redoMask; + break; + case EDIT_ZONE_MASK: + oldMask = drawable.zoneMask; + drawable.zoneMask = redoMask; + break; + } + } + } +} + +class CreateNodeAction : EditAction +{ + uint nodeID; + uint parentID; + XMLFile@ nodeData; + + void Define(Node@ node) + { + nodeID = node.id; + parentID = node.parent.id; + nodeData = XMLFile(); + XMLElement rootElem = nodeData.CreateRoot("node"); + node.SaveXML(rootElem); + } + + void Undo() + { + Node@ parent = editorScene.GetNode(parentID); + Node@ node = editorScene.GetNode(nodeID); + if (parent !is null && node !is null) + { + parent.RemoveChild(node); + hierarchyList.ClearSelection(); + } + } + + void Redo() + { + Node@ parent = editorScene.GetNode(parentID); + if (parent !is null) + { + Node@ node = parent.CreateChild("", Scene::IsReplicatedID(nodeID) ? REPLICATED : LOCAL, nodeID); + node.LoadXML(nodeData.root); + FocusNode(node); + } + } +} + +class DeleteNodeAction : EditAction +{ + uint nodeID; + uint parentID; + XMLFile@ nodeData; + + void Define(Node@ node) + { + nodeID = node.id; + parentID = node.parent.id; + nodeData = XMLFile(); + XMLElement rootElem = nodeData.CreateRoot("node"); + node.SaveXML(rootElem); + rootElem.SetU32("listItemIndex", GetListIndex(node)); + } + + void Undo() + { + Node@ parent = editorScene.GetNode(parentID); + if (parent !is null) + { + // Handle update manually so that the node can be reinserted back into its previous list index + suppressSceneChanges = true; + + Node@ node = parent.CreateChild("", Scene::IsReplicatedID(nodeID) ? REPLICATED : LOCAL, nodeID); + if (node.LoadXML(nodeData.root)) + { + uint listItemIndex = nodeData.root.GetU32("listItemIndex"); + UIElement@ parentItem = hierarchyList.items[GetListIndex(parent)]; + UpdateHierarchyItem(listItemIndex, node, parentItem); + FocusNode(node); + } + + suppressSceneChanges = false; + } + } + + void Redo() + { + Node@ parent = editorScene.GetNode(parentID); + Node@ node = editorScene.GetNode(nodeID); + if (parent !is null && node !is null) + { + parent.RemoveChild(node); + hierarchyList.ClearSelection(); + } + } +} + +class ReparentNodeAction : EditAction +{ + uint nodeID; + uint oldParentID; + uint newParentID; + Array nodeList; // 2 uints get inserted per node (node, node.parent) + bool multiple; + + void Define(Node@ node, Node@ newParent) + { + multiple = false; + nodeID = node.id; + oldParentID = node.parent.id; + newParentID = newParent.id; + } + + void Define(Array nodes, Node@ newParent) + { + multiple = true; + newParentID = newParent.id; + for(uint i = 0; i < nodes.length; ++i) + { + Node@ node = nodes[i]; + nodeList.Push(node.id); + nodeList.Push(node.parent.id); + } + } + + void Undo() + { + if (multiple) + { + for (uint i = 0; i < nodeList.length; i+=2) + { + uint nodeID_ = nodeList[i]; + uint oldParentID_ = nodeList[i+1]; + Node@ parent = editorScene.GetNode(oldParentID_); + Node@ node = editorScene.GetNode(nodeID_); + if (parent !is null && node !is null) + node.parent = parent; + } + } + else + { + Node@ parent = editorScene.GetNode(oldParentID); + Node@ node = editorScene.GetNode(nodeID); + if (parent !is null && node !is null) + node.parent = parent; + } + } + + void Redo() + { + if (multiple) + { + Node@ parent = editorScene.GetNode(newParentID); + if (parent is null) + return; + + for (uint i = 0; i < nodeList.length; i+=2) + { + uint nodeID_ = nodeList[i]; + Node@ node = editorScene.GetNode(nodeID_); + if (node !is null) + node.parent = parent; + } + } + else + { + Node@ parent = editorScene.GetNode(newParentID); + Node@ node = editorScene.GetNode(nodeID); + if (parent !is null && node !is null) + node.parent = parent; + } + } +} + +class ReorderNodeAction : EditAction +{ + uint nodeID; + uint parentID; + uint oldChildIndex; + uint newChildIndex; + + void Define(Node@ node, uint newIndex) + { + nodeID = node.id; + parentID = node.parent.id; + oldChildIndex = SceneFindChildIndex(node.parent, node); + newChildIndex = newIndex; + } + + void Undo() + { + Node@ parent = editorScene.GetNode(parentID); + Node@ node = editorScene.GetNode(nodeID); + if (parent !is null && node !is null) + PerformReorder(parent, node, oldChildIndex); + } + + void Redo() + { + Node@ parent = editorScene.GetNode(parentID); + Node@ node = editorScene.GetNode(nodeID); + if (parent !is null && node !is null) + PerformReorder(parent, node, newChildIndex); + } +} + +class ReorderComponentAction : EditAction +{ + uint componentID; + uint nodeID; + uint oldComponentIndex; + uint newComponentIndex; + + void Define(Component@ component, uint newIndex) + { + componentID = component.id; + nodeID = component.node.id; + oldComponentIndex = SceneFindComponentIndex(component.node, component); + newComponentIndex = newIndex; + } + + void Undo() + { + Node@ node = editorScene.GetNode(nodeID); + Component@ component = editorScene.GetComponent(componentID); + if (node !is null && component !is null) + PerformReorder(node, component, oldComponentIndex); + } + + void Redo() + { + Node@ node = editorScene.GetNode(nodeID); + Component@ component = editorScene.GetComponent(componentID); + if (node !is null && component !is null) + PerformReorder(node, component, newComponentIndex); + } +} + +class CreateComponentAction : EditAction +{ + uint nodeID; + uint componentID; + XMLFile@ componentData; + + void Define(Component@ component) + { + componentID = component.id; + nodeID = component.node.id; + componentData = XMLFile(); + XMLElement rootElem = componentData.CreateRoot("component"); + component.SaveXML(rootElem); + } + + void Undo() + { + Node@ node = editorScene.GetNode(nodeID); + Component@ component = editorScene.GetComponent(componentID); + if (node !is null && component !is null) + { + node.RemoveComponent(component); + hierarchyList.ClearSelection(); + } + } + + void Redo() + { + Node@ node = editorScene.GetNode(nodeID); + if (node !is null) + { + Component@ component = node.CreateComponent(componentData.root.GetAttribute("type"), Scene::IsReplicatedID(componentID) ? + REPLICATED : LOCAL, componentID); + component.LoadXML(componentData.root); + component.ApplyAttributes(); + FocusComponent(component); + } + } + +} + +class DeleteComponentAction : EditAction +{ + uint nodeID; + uint componentID; + XMLFile@ componentData; + + void Define(Component@ component) + { + componentID = component.id; + nodeID = component.node.id; + componentData = XMLFile(); + XMLElement rootElem = componentData.CreateRoot("component"); + component.SaveXML(rootElem); + rootElem.SetU32("listItemIndex", GetComponentListIndex(component)); + } + + void Undo() + { + Node@ node = editorScene.GetNode(nodeID); + if (node !is null) + { + // Handle update manually so that the component can be reinserted back into its previous list index + suppressSceneChanges = true; + + Component@ component = node.CreateComponent(componentData.root.GetAttribute("type"), Scene::IsReplicatedID(componentID) ? + REPLICATED : LOCAL, componentID); + if (component.LoadXML(componentData.root)) + { + component.ApplyAttributes(); + + uint listItemIndex = componentData.root.GetU32("listItemIndex"); + UIElement@ parentItem = hierarchyList.items[GetListIndex(node)]; + UpdateHierarchyItem(listItemIndex, component, parentItem); + FocusComponent(component); + } + + suppressSceneChanges = false; + } + } + + void Redo() + { + Node@ node = editorScene.GetNode(nodeID); + Component@ component = editorScene.GetComponent(componentID); + if (node !is null && component !is null) + { + node.RemoveComponent(component); + hierarchyList.ClearSelection(); + } + } +} + +class EditAttributeAction : EditAction +{ + int targetType; + uint targetID; + uint attrIndex; + Variant undoValue; + Variant redoValue; + + void Define(Serializable@ target, uint index, const Variant&in oldValue) + { + attrIndex = index; + undoValue = oldValue; + redoValue = target.attributes[index]; + + targetType = GetType(target); + targetID = GetID(target, targetType); + } + + Serializable@ GetTarget() + { + switch (targetType) + { + case ITEM_NODE: + return editorScene.GetNode(targetID); + case ITEM_COMPONENT: + return editorScene.GetComponent(targetID); + case ITEM_UI_ELEMENT: + return GetUIElementByID(targetID); + } + + return null; + } + + void Undo() + { + Serializable@ target = GetTarget(); + if (target !is null) + { + target.attributes[attrIndex] = undoValue; + target.ApplyAttributes(); + // Can't know if need a full update, so assume true + attributesFullDirty = true; + // Apply side effects + PostEditAttribute(target, attrIndex); + + if (targetType == ITEM_UI_ELEMENT) + SetUIElementModified(target); + else + SetSceneModified(); + } + } + + void Redo() + { + Serializable@ target = GetTarget(); + if (target !is null) + { + target.attributes[attrIndex] = redoValue; + target.ApplyAttributes(); + // Can't know if need a full update, so assume true + attributesFullDirty = true; + // Apply side effects + PostEditAttribute(target, attrIndex); + + if (targetType == ITEM_UI_ELEMENT) + SetUIElementModified(target); + else + SetSceneModified(); + } + } +} + +class ResetAttributesAction : EditAction +{ + int targetType; + uint targetID; + Array undoValues; + VariantMap internalVars; // UIElement specific + + void Define(Serializable@ target) + { + for (uint i = 0; i < target.numAttributes; ++i) + undoValues.Push(target.attributes[i]); + + targetType = GetType(target); + targetID = GetID(target, targetType); + + if (targetType == ITEM_UI_ELEMENT) + { + // Special handling for UIElement to preserve the internal variables containing the element's generated ID among others + UIElement@ element = target; + Array keys = element.vars.keys; + for (uint i = 0; i < keys.length; ++i) + { + // If variable name is empty (or unregistered) then it is an internal variable and should be preserved + String name = GetVarName(keys[i]); + if (name.empty) + internalVars[keys[i]] = element.vars[keys[i]]; + } + } + } + + Serializable@ GetTarget() + { + switch (targetType) + { + case ITEM_NODE: + return editorScene.GetNode(targetID); + case ITEM_COMPONENT: + return editorScene.GetComponent(targetID); + case ITEM_UI_ELEMENT: + return GetUIElementByID(targetID); + } + + return null; + } + + void SetInternalVars(UIElement@ element) + { + // Revert back internal variables + Array keys = internalVars.keys; + for (uint i = 0; i < keys.length; ++i) + element.vars[keys[i]] = internalVars[keys[i]]; + + if (element.vars.Contains(FILENAME_VAR)) + CenterDialog(element); + } + + void Undo() + { + ui.cursor.shape = CS_BUSY; + + Serializable@ target = GetTarget(); + if (target !is null) + { + for (uint i = 0; i < target.numAttributes; ++i) + { + AttributeInfo info = target.attributeInfos[i]; + if (info.mode & AM_NOEDIT != 0 || info.mode & AM_NODEID != 0 || info.mode & AM_COMPONENTID != 0) + continue; + + target.attributes[i] = undoValues[i]; + } + target.ApplyAttributes(); + + // Apply side effects + for (uint i = 0; i < target.numAttributes; ++i) + PostEditAttribute(target, i); + + if (targetType == ITEM_UI_ELEMENT) + SetUIElementModified(target); + else + SetSceneModified(); + + attributesFullDirty = true; + } + } + + void Redo() + { + ui.cursor.shape = CS_BUSY; + + Serializable@ target = GetTarget(); + if (target !is null) + { + for (uint i = 0; i < target.numAttributes; ++i) + { + AttributeInfo info = target.attributeInfos[i]; + if (info.mode & AM_NOEDIT != 0 || info.mode & AM_NODEID != 0 || info.mode & AM_COMPONENTID != 0) + continue; + + target.attributes[i] = target.attributeDefaults[i]; + } + if (targetType == ITEM_UI_ELEMENT) + SetInternalVars(target); + target.ApplyAttributes(); + + // Apply side effects + for (uint i = 0; i < target.numAttributes; ++i) + PostEditAttribute(target, i); + + if (targetType == ITEM_UI_ELEMENT) + SetUIElementModified(target); + else + SetSceneModified(); + + attributesFullDirty = true; + } + } +} + +class ToggleNodeEnabledAction : EditAction +{ + uint nodeID; + bool undoValue; + + void Define(Node@ node, bool oldEnabled) + { + nodeID = node.id; + undoValue = oldEnabled; + } + + void Undo() + { + Node@ node = editorScene.GetNode(nodeID); + if (node !is null) + node.SetEnabledRecursive(undoValue); + } + + void Redo() + { + Node@ node = editorScene.GetNode(nodeID); + if (node !is null) + node.SetEnabledRecursive(!undoValue); + } +} + +class Transform +{ + Vector3 position; + Quaternion rotation; + Vector3 scale; + + void Define(Node@ node) + { + position = node.position; + rotation = node.rotation; + scale = node.scale; + } + + void Apply(Node@ node) + { + node.SetTransform(position, rotation, scale); + } +} + +class EditNodeTransformAction : EditAction +{ + uint nodeID; + Transform undoTransform; + Transform redoTransform; + + void Define(Node@ node, const Transform&in oldTransform) + { + nodeID = node.id; + undoTransform = oldTransform; + redoTransform.Define(node); + } + + void Undo() + { + Node@ node = editorScene.GetNode(nodeID); + if (node !is null) + { + undoTransform.Apply(node); + UpdateNodeAttributes(); + } + } + + void Redo() + { + Node@ node = editorScene.GetNode(nodeID); + if (node !is null) + { + redoTransform.Apply(node); + UpdateNodeAttributes(); + } + } +} + +class CreateUIElementAction : EditAction +{ + Variant elementID; + Variant parentID; + XMLFile@ elementData; + XMLFile@ styleFile; + + void Define(UIElement@ element) + { + elementID = GetUIElementID(element); + parentID = GetUIElementID(element.parent); + elementData = XMLFile(); + XMLElement rootElem = elementData.CreateRoot("element"); + element.SaveXML(rootElem); + styleFile = element.defaultStyle; + } + + void Undo() + { + UIElement@ parent = GetUIElementByID(parentID); + UIElement@ element = GetUIElementByID(elementID); + if (parent !is null && element !is null) + { + parent.RemoveChild(element); + hierarchyList.ClearSelection(); + SetUIElementModified(parent); + } + } + + void Redo() + { + UIElement@ parent = GetUIElementByID(parentID); + if (parent !is null) + { + // Have to update manually because the element ID var is not set yet when the E_ELEMENTADDED event is sent + suppressUIElementChanges = true; + + if (parent.LoadChildXML(elementData.root, styleFile) !is null) + { + UIElement@ element = parent.children[parent.numChildren - 1]; + UpdateHierarchyItem(element); + FocusUIElement(element); + SetUIElementModified(parent); + } + + suppressUIElementChanges = false; + + } + } +} + +class DeleteUIElementAction : EditAction +{ + Variant elementID; + Variant parentID; + XMLFile@ elementData; + XMLFile@ styleFile; + + void Define(UIElement@ element) + { + elementID = GetUIElementID(element); + parentID = GetUIElementID(element.parent); + elementData = XMLFile(); + XMLElement rootElem = elementData.CreateRoot("element"); + element.SaveXML(rootElem); + rootElem.SetU32("index", element.parent.FindChild(element)); + rootElem.SetU32("listItemIndex", GetListIndex(element)); + styleFile = element.defaultStyle; + } + + void Undo() + { + UIElement@ parent = GetUIElementByID(parentID); + if (parent !is null) + { + // Have to update manually because the element ID var is not set yet when the E_ELEMENTADDED event is sent + suppressUIElementChanges = true; + + if (parent.LoadChildXML(elementData.root, styleFile) !is null) + { + XMLElement rootElem = elementData.root; + uint index = rootElem.GetU32("index"); + uint listItemIndex = rootElem.GetU32("listItemIndex"); + UIElement@ element = parent.children[index]; + UIElement@ parentItem = hierarchyList.items[GetListIndex(parent)]; + UpdateHierarchyItem(listItemIndex, element, parentItem); + FocusUIElement(element); + SetUIElementModified(parent); + } + + suppressUIElementChanges = false; + } + } + + void Redo() + { + UIElement@ parent = GetUIElementByID(parentID); + UIElement@ element = GetUIElementByID(elementID); + if (parent !is null && element !is null) + { + parent.RemoveChild(element); + hierarchyList.ClearSelection(); + SetUIElementModified(parent); + } + } +} + +class ReparentUIElementAction : EditAction +{ + Variant elementID; + Variant oldParentID; + uint oldChildIndex; + Variant newParentID; + + void Define(UIElement@ element, UIElement@ newParent) + { + elementID = GetUIElementID(element); + oldParentID = GetUIElementID(element.parent); + oldChildIndex = element.parent.FindChild(element); + newParentID = GetUIElementID(newParent); + } + + void Undo() + { + UIElement@ parent = GetUIElementByID(oldParentID); + UIElement@ element = GetUIElementByID(elementID); + if (parent !is null && element !is null) + { + element.SetParent(parent, oldChildIndex); + SetUIElementModified(parent); + } + } + + void Redo() + { + UIElement@ parent = GetUIElementByID(newParentID); + UIElement@ element = GetUIElementByID(elementID); + if (parent !is null && element !is null) + { + element.parent = parent; + SetUIElementModified(parent); + } + } +} + +class ReorderUIElementAction : EditAction +{ + Variant elementID; + Variant parentID; + uint oldChildIndex; + uint newChildIndex; + + void Define(UIElement@ element, uint newIndex) + { + elementID = GetUIElementID(element); + parentID = GetUIElementID(element.parent); + oldChildIndex = element.parent.FindChild(element); + newChildIndex = newIndex; + } + + void Undo() + { + UIElement@ parent = GetUIElementByID(parentID); + UIElement@ element = GetUIElementByID(elementID); + if (parent !is null && element !is null) + PerformReorder(parent, element, oldChildIndex); + } + + void Redo() + { + UIElement@ parent = GetUIElementByID(parentID); + UIElement@ element = GetUIElementByID(elementID); + if (parent !is null && element !is null) + PerformReorder(parent, element, newChildIndex); + } +} + +class ApplyUIElementStyleAction : EditAction +{ + Variant elementID; + Variant parentID; + XMLFile@ elementData; + XMLFile@ styleFile; + String elementOldStyle; + String elementNewStyle; + + void Define(UIElement@ element, const String&in newStyle) + { + elementID = GetUIElementID(element); + parentID = GetUIElementID(element.parent); + elementData = XMLFile(); + XMLElement rootElem = elementData.CreateRoot("element"); + element.SaveXML(rootElem); + rootElem.SetU32("index", element.parent.FindChild(element)); + rootElem.SetU32("listItemIndex", GetListIndex(element)); + styleFile = element.defaultStyle; + elementOldStyle = element.style; + elementNewStyle = newStyle; + } + + void ApplyStyle(const String&in style) + { + UIElement@ parent = GetUIElementByID(parentID); + UIElement@ element = GetUIElementByID(elementID); + if (parent !is null && element !is null) + { + // Apply the style in the XML data + elementData.root.SetAttribute("style", style); + + // Have to update manually because the element ID var is not set yet when the E_ELEMENTADDED event is sent + suppressUIElementChanges = true; + + parent.RemoveChild(element); + if (parent.LoadChildXML(elementData.root, styleFile) !is null) + { + XMLElement rootElem = elementData.root; + uint index = rootElem.GetU32("index"); + uint listItemIndex = rootElem.GetU32("listItemIndex"); + UIElement@ elem = parent.children[index]; + UIElement@ parentItem = hierarchyList.items[GetListIndex(parent)]; + UpdateHierarchyItem(listItemIndex, elem, parentItem); + SetUIElementModified(elem); + hierarchyUpdateSelections.Push(listItemIndex); + } + + suppressUIElementChanges = false; + } + } + + void Undo() + { + ApplyStyle(elementOldStyle); + } + + void Redo() + { + ApplyStyle(elementNewStyle); + } +} + +class EditMaterialAction : EditAction +{ + XMLFile@ oldState; + XMLFile@ newState; + WeakHandle material; + + void Define(Material@ material_, XMLFile@ oldState_) + { + material = material_; + oldState = oldState_; + newState = XMLFile(); + + XMLElement materialElem = newState.CreateRoot("material"); + material_.Save(materialElem); + } + + void Undo() + { + Material@ mat = material.Get(); + if (mat !is null) + { + mat.Load(oldState.root); + RefreshMaterialEditor(); + } + } + + void Redo() + { + Material@ mat = material.Get(); + if (mat !is null) + { + mat.Load(newState.root); + RefreshMaterialEditor(); + } + } +} + +class EditParticleEffectAction : EditAction +{ + XMLFile@ oldState; + XMLFile@ newState; + WeakHandle particleEffect; + ParticleEmitter@ particleEmitter; + + void Define(ParticleEmitter@ particleEmitter_, ParticleEffect@ particleEffect_, XMLFile@ oldState_) + { + particleEmitter = particleEmitter_; + particleEffect = particleEffect_; + oldState = oldState_; + newState = XMLFile(); + + XMLElement particleElem = newState.CreateRoot("particleeffect"); + particleEffect_.Save(particleElem); + } + + void Undo() + { + ParticleEffect@ effect = particleEffect.Get(); + if (effect !is null) + { + effect.Load(oldState.root); + particleEmitter.ApplyEffect(); + RefreshParticleEffectEditor(); + } + } + + void Redo() + { + ParticleEffect@ effect = particleEffect.Get(); + if (effect !is null) + { + effect.Load(newState.root); + particleEmitter.ApplyEffect(); + RefreshParticleEffectEditor(); + } + } +} + +class AssignMaterialAction : EditAction +{ + WeakHandle model; + Array oldMaterials; + String newMaterialName; + + void Define(StaticModel@ model_, Array oldMaterials_, Material@ newMaterial_) + { + model = model_; + oldMaterials = oldMaterials_; + newMaterialName = newMaterial_.name; + } + + void Undo() + { + StaticModel@ staticModel = model.Get(); + if (staticModel is null) + return; + + for(uint i=0; i 0) + if (wheelValue > 0) { high = high + wheelValue; - high = Min(high, IMAGE_SIZE); // limit BWGradietn by high + high = Min(high, IMAGE_SIZE); // limit BWGradietn by high } else if (wheelValue < 0) { @@ -237,7 +237,7 @@ void HandleColorWheelMouseWheel(StringHash eventType, VariantMap& eventData) wheelColor.FromHSV(colorHValue, colorSValue, colorVValue, colorAValue); SendEventChangeColor(); - UpdateColorInformation(); + UpdateColorInformation(); } } @@ -262,8 +262,8 @@ void HandleColorWheelMouseMove(StringHash eventType, VariantMap& eventData) isBWGradientHovering = false; isColorWheelHovering = false; - // if mouse cursor move on wheel rectangle - if (colorWheel.IsInside(IntVector2(x,y), true)) + // if mouse cursor move on wheel rectangle + if (colorWheel.IsInside(IntVector2(x,y), true)) { isColorWheelHovering = true; // get element pos win screen @@ -273,7 +273,7 @@ void HandleColorWheelMouseMove(StringHash eventType, VariantMap& eventData) cwx = x - ep.x; cwy = y - ep.y; - // shift mouse pos to center of wheel + // shift mouse pos to center of wheel cx = cwx - HALF_IMAGE_SIZE; cy = cwy - HALF_IMAGE_SIZE; @@ -281,7 +281,7 @@ void HandleColorWheelMouseMove(StringHash eventType, VariantMap& eventData) Vector2 d = Vector2(cx, cy); // if out of circle place colorCurcor back to circle - if (d.length > HALF_IMAGE_SIZE) + if (d.length > HALF_IMAGE_SIZE) { d.Normalize(); d = d * HALF_IMAGE_SIZE; @@ -290,7 +290,7 @@ void HandleColorWheelMouseMove(StringHash eventType, VariantMap& eventData) inWheel = false; } - else + else { inWheel = true; } @@ -319,10 +319,10 @@ void HandleColorWheelMouseMove(StringHash eventType, VariantMap& eventData) } } // if mouse cursor move on bwGradient rectangle - else if (bwGradient.IsInside(IntVector2(x,y), true)) + else if (bwGradient.IsInside(IntVector2(x,y), true)) { isBWGradientHovering = true; - IntVector2 ep = bwGradient.screenPosition; + IntVector2 ep = bwGradient.screenPosition; int high = y - ep.y; if (input.mouseButtonDown[MOUSEB_LEFT] || input.mouseButtonDown[MOUSEB_RIGHT]) @@ -339,10 +339,10 @@ void HandleColorWheelMouseMove(StringHash eventType, VariantMap& eventData) UpdateColorInformation(); } - // if mouse cursor move on AlphaGradient rectangle + // if mouse cursor move on AlphaGradient rectangle else if (AGradient.IsInside(IntVector2(x,y), true)) { - IntVector2 ep = AGradient.screenPosition; + IntVector2 ep = AGradient.screenPosition; int aValue = x - ep.x; if (input.mouseButtonDown[MOUSEB_LEFT] || input.mouseButtonDown[MOUSEB_RIGHT]) @@ -369,7 +369,7 @@ void HandleColorWheelMouseMove(StringHash eventType, VariantMap& eventData) } } -void UpdateColorInformation() +void UpdateColorInformation() { // fill UI from current color hLineEdit.text = String(colorHValue).Substring(0,5); @@ -380,17 +380,17 @@ void UpdateColorInformation() gLineEdit.text = String(wheelColor.g).Substring(0,5); bLineEdit.text = String(wheelColor.b).Substring(0,5); - aLineEdit.text = String(colorAValue).Substring(0,5); + aLineEdit.text = String(colorAValue).Substring(0,5); colorCheck.color = wheelColor; colorWheel.color = Color(colorVValue,colorVValue,colorVValue); AGradient.color = Color(wheelColor.r, wheelColor.g, wheelColor.b); // update selected fast-colors - if (colorFastSelectedIndex != -1) + if (colorFastSelectedIndex != -1) { colorFastItem[colorFastSelectedIndex].color = wheelColor; - colorFast[colorFastSelectedIndex] = wheelColor; + colorFast[colorFastSelectedIndex] = wheelColor; } } @@ -401,7 +401,7 @@ void SendEventChangeColor() SendEvent("WheelChangeColor", eventData); } -void EstablishColorWheelUIFromColor(Color c) +void EstablishColorWheelUIFromColor(Color c) { wheelColor = c; colorHValue = c.Hue(); @@ -439,7 +439,7 @@ float GetAngle(Vector2 point) { float angle = Atan2( point.y, point.x ); - if (angle < 0) + if (angle < 0) angle += MAX_ANGLE; return angle; diff --git a/bin/Data/Scripts/Editor/EditorCubeCapture.as b/bin/EditorData/Editor/Scripts/EditorCubeCapture.as similarity index 100% rename from bin/Data/Scripts/Editor/EditorCubeCapture.as rename to bin/EditorData/Editor/Scripts/EditorCubeCapture.as diff --git a/bin/Data/Scripts/Editor/EditorDuplicator.as b/bin/EditorData/Editor/Scripts/EditorDuplicator.as similarity index 93% rename from bin/Data/Scripts/Editor/EditorDuplicator.as rename to bin/EditorData/Editor/Scripts/EditorDuplicator.as index 8d76b6669fa..5adaddf5711 100644 --- a/bin/Data/Scripts/Editor/EditorDuplicator.as +++ b/bin/EditorData/Editor/Scripts/EditorDuplicator.as @@ -15,15 +15,15 @@ void CreateDuplicatorEditor() { if (duplicatorWindow !is null) return; - - duplicatorWindow = ui.LoadLayout(cache.GetResource("XMLFile", "UI/EditorDuplicator.xml")); + + duplicatorWindow = ui.LoadLayout(cache.GetResource("XMLFile", "Editor/UI/EditorDuplicator.xml")); ui.root.AddChild(duplicatorWindow); duplicatorWindow.opacity = uiMaxOpacity; InitDuplicatorWindow(); RefreshDuplicatorWindow(); - int height = Min(ui.root.height - 60, 750); + int height = Min(ui.root.height - 60, 750); duplicatorWindow.SetSize(400, 0); CenterDialog(duplicatorWindow); @@ -64,7 +64,7 @@ void InitDuplicatorWindow() CreateDragSlider(cast(duplicatorWindow.GetChild("RotZ", true))); CreateDragSlider(cast(duplicatorWindow.GetChild("ScaleX", true))); CreateDragSlider(cast(duplicatorWindow.GetChild("ScaleY", true))); - CreateDragSlider(cast(duplicatorWindow.GetChild("ScaleZ", true))); + CreateDragSlider(cast(duplicatorWindow.GetChild("ScaleZ", true))); } void RefreshDuplicatorWindow() @@ -121,7 +121,7 @@ void EditDuplicatorCount(StringHash eventType, VariantMap& eventData) if (inDuplicatorRefresh) return; - LineEdit@ element = eventData["Element"].GetPtr(); + LineEdit@ element = eventData["Element"].GetPtr(); if (element.name == "Count") DuplicatorCount = element.text.ToFloat(); @@ -187,7 +187,7 @@ void EditDuplicatorScale(StringHash eventType, VariantMap& eventData) void OnButtonPreview(StringHash eventType, VariantMap& eventData) -{ +{ ProcessDuplicator(); } @@ -242,7 +242,7 @@ void ProcessDuplicator() DuplicatorClearAll(); if(isClonesInChild) - DuplicatorInNode(); + DuplicatorInNode(); else DuplicatorInScene(); } @@ -252,12 +252,12 @@ void DuplicatorInScene() if(selectedNodes.length > 0) { // Apply multiplication only on first selected node - Node@ currentNode = selectedNodes[0]; - + Node@ currentNode = selectedNodes[0]; + Node@ baseClone = currentNode.Clone(); baseClone.AddTag(tagName); - Matrix3x4 mulMat(Position, Quaternion(Rotation), Scale); + Matrix3x4 mulMat(Position, Quaternion(Rotation), Scale); Matrix3x4 accu = baseClone.transform; for(uint i=1; i oldGizmoTransforms; - -class GizmoAxis -{ - Ray axisRay; - bool selected; - bool lastSelected; - float t; - float d; - float lastT; - float lastD; - - GizmoAxis() - { - selected = false; - lastSelected = false; - t = 0.0; - d = 0.0; - lastT = 0.0; - lastD = 0.0; - } - - void Update(Ray cameraRay, float scale, bool drag) - { - // Do not select when UI has modal element - if (ui.HasModalElement()) - { - selected = false; - return; - } - - Vector3 closest = cameraRay.ClosestPoint(axisRay); - Vector3 projected = axisRay.Project(closest); - d = axisRay.Distance(closest); - t = (projected - axisRay.origin).DotProduct(axisRay.direction); - - // Determine the sign of d from a plane that goes through the camera position to the axis - Plane axisPlane(cameraNode.position, axisRay.origin, axisRay.origin + axisRay.direction); - if (axisPlane.Distance(closest) < 0.0) - d = -d; - - // Update selected status only when not dragging - if (!drag) - { - selected = Abs(d) < axisMaxD * scale && t >= -axisMaxD * scale && t <= axisMaxT * scale; - lastT = t; - lastD = d; - } - } - - void Moved() - { - lastT = t; - lastD = d; - } -} - -GizmoAxis gizmoAxisX; -GizmoAxis gizmoAxisY; -GizmoAxis gizmoAxisZ; - -void CreateGizmo() -{ - gizmoNode = Node(); - gizmo = gizmoNode.CreateComponent("StaticModel"); - gizmo.model = cache.GetResource("Model", "Models/Editor/Axes.mdl"); - gizmo.materials[0] = cache.GetResource("Material", "Materials/Editor/RedUnlit.xml"); - gizmo.materials[1] = cache.GetResource("Material", "Materials/Editor/GreenUnlit.xml"); - gizmo.materials[2] = cache.GetResource("Material", "Materials/Editor/BlueUnlit.xml"); - gizmo.enabled = false; - gizmo.viewMask = 0x80000000; // Editor raycasts use viewmask 0x7fffffff - gizmo.occludee = false; - gizmoNode.name = "EditorGizmo"; - - gizmoAxisX.lastSelected = false; - gizmoAxisY.lastSelected = false; - gizmoAxisZ.lastSelected = false; - lastGizmoMode = EDIT_MOVE; -} - -void HideGizmo() -{ - if (gizmo !is null) - gizmo.enabled = false; -} - -void ShowGizmo() -{ - if (gizmo !is null) - { - gizmo.enabled = true; - - // Because setting enabled = false detaches the gizmo from octree, - // and it is a manually added drawable, must readd to octree when showing - if (editorScene.octree !is null) - editorScene.octree.AddManualDrawable(gizmo); - } -} - -void UpdateGizmo() -{ - UseGizmo(); - PositionGizmo(); - ResizeGizmo(); -} - -void PositionGizmo() -{ - if (gizmo is null) - return; - - Vector3 center(0, 0, 0); - bool containsScene = false; - - for (uint i = 0; i < editNodes.length; ++i) - { - // Scene's transform should not be edited, so hide gizmo if it is included - if (editNodes[i] is editorScene) - { - containsScene = true; - break; - } - center += editNodes[i].worldPosition; - } - - if (editNodes.empty || containsScene) - { - HideGizmo(); - return; - } - - center /= editNodes.length; - gizmoNode.position = center; - - if (axisMode == AXIS_WORLD || editNodes.length > 1) - gizmoNode.rotation = Quaternion(); - else - gizmoNode.rotation = editNodes[0].worldRotation; - - if (editMode != lastGizmoMode) - { - switch (editMode) - { - case EDIT_MOVE: - gizmo.model = cache.GetResource("Model", "Models/Editor/Axes.mdl"); - break; - - case EDIT_ROTATE: - gizmo.model = cache.GetResource("Model", "Models/Editor/RotateAxes.mdl"); - break; - - case EDIT_SCALE: - gizmo.model = cache.GetResource("Model", "Models/Editor/ScaleAxes.mdl"); - break; - } - - lastGizmoMode = editMode; - } - - if ((editMode != EDIT_SELECT && !orbiting) && !gizmo.enabled) - ShowGizmo(); - else if ((editMode == EDIT_SELECT || orbiting) && gizmo.enabled) - HideGizmo(); -} - -void ResizeGizmo() -{ - if (gizmo is null || !gizmo.enabled) - return; - - float scale = 0.1 / camera.zoom; - - if (camera.orthographic) - scale *= camera.orthoSize; - else - scale *= (camera.view * gizmoNode.position).z; - - gizmoNode.scale = Vector3(scale, scale, scale); -} - -void CalculateGizmoAxes() -{ - gizmoAxisX.axisRay = Ray(gizmoNode.position, gizmoNode.rotation * Vector3(1, 0, 0)); - gizmoAxisY.axisRay = Ray(gizmoNode.position, gizmoNode.rotation * Vector3(0, 1, 0)); - gizmoAxisZ.axisRay = Ray(gizmoNode.position, gizmoNode.rotation * Vector3(0, 0, 1)); -} - -void GizmoMoved() -{ - gizmoAxisX.Moved(); - gizmoAxisY.Moved(); - gizmoAxisZ.Moved(); -} - -void UseGizmo() -{ - if (gizmo is null || !gizmo.enabled || editMode == EDIT_SELECT) - { - StoreGizmoEditActions(); - previousGizmoDrag = false; - return; - } - - IntVector2 pos = ui.cursorPosition; - if (ui.GetElementAt(pos) !is null) - return; - - Ray cameraRay = GetActiveViewportCameraRay(); - float scale = gizmoNode.scale.x; - - // Recalculate axes only when not left-dragging - bool drag = input.mouseButtonDown[MOUSEB_LEFT]; - if (!drag) - CalculateGizmoAxes(); - - gizmoAxisX.Update(cameraRay, scale, drag); - gizmoAxisY.Update(cameraRay, scale, drag); - gizmoAxisZ.Update(cameraRay, scale, drag); - - if (gizmoAxisX.selected != gizmoAxisX.lastSelected) - { - gizmo.materials[0] = cache.GetResource("Material", gizmoAxisX.selected ? "Materials/Editor/BrightRedUnlit.xml" : - "Materials/Editor/RedUnlit.xml"); - gizmoAxisX.lastSelected = gizmoAxisX.selected; - } - if (gizmoAxisY.selected != gizmoAxisY.lastSelected) - { - gizmo.materials[1] = cache.GetResource("Material", gizmoAxisY.selected ? "Materials/Editor/BrightGreenUnlit.xml" : - "Materials/Editor/GreenUnlit.xml"); - gizmoAxisY.lastSelected = gizmoAxisY.selected; - } - if (gizmoAxisZ.selected != gizmoAxisZ.lastSelected) - { - gizmo.materials[2] = cache.GetResource("Material", gizmoAxisZ.selected ? "Materials/Editor/BrightBlueUnlit.xml" : - "Materials/Editor/BlueUnlit.xml"); - gizmoAxisZ.lastSelected = gizmoAxisZ.selected; - }; - - if (drag) - { - // Store initial transforms for undo when gizmo drag started - if (!previousGizmoDrag) - { - oldGizmoTransforms.Resize(editNodes.length); - for (uint i = 0; i < editNodes.length; ++i) - oldGizmoTransforms[i].Define(editNodes[i]); - } - - bool moved = false; - - if (editMode == EDIT_MOVE) - { - Vector3 adjust(0, 0, 0); - if (gizmoAxisX.selected) - adjust += Vector3(1, 0, 0) * (gizmoAxisX.t - gizmoAxisX.lastT); - if (gizmoAxisY.selected) - adjust += Vector3(0, 1, 0) * (gizmoAxisY.t - gizmoAxisY.lastT); - if (gizmoAxisZ.selected) - adjust += Vector3(0, 0, 1) * (gizmoAxisZ.t - gizmoAxisZ.lastT); - - moved = MoveNodes(adjust); - } - else if (editMode == EDIT_ROTATE) - { - Vector3 adjust(0, 0, 0); - if (gizmoAxisX.selected) - adjust.x = (gizmoAxisX.d - gizmoAxisX.lastD) * rotSensitivity / scale; - if (gizmoAxisY.selected) - adjust.y = -(gizmoAxisY.d - gizmoAxisY.lastD) * rotSensitivity / scale; - if (gizmoAxisZ.selected) - adjust.z = (gizmoAxisZ.d - gizmoAxisZ.lastD) * rotSensitivity / scale; - - moved = RotateNodes(adjust); - } - else if (editMode == EDIT_SCALE) - { - Vector3 adjust(0, 0, 0); - if (gizmoAxisX.selected) - adjust += Vector3(1, 0, 0) * (gizmoAxisX.t - gizmoAxisX.lastT); - if (gizmoAxisY.selected) - adjust += Vector3(0, 1, 0) * (gizmoAxisY.t - gizmoAxisY.lastT); - if (gizmoAxisZ.selected) - adjust += Vector3(0, 0, 1) * (gizmoAxisZ.t - gizmoAxisZ.lastT); - - // Special handling for uniform scale: use the unmodified X-axis movement only - if (editMode == EDIT_SCALE && gizmoAxisX.selected && gizmoAxisY.selected && gizmoAxisZ.selected) - { - float x = gizmoAxisX.t - gizmoAxisX.lastT; - adjust = Vector3(x, x, x); - } - - moved = ScaleNodes(adjust); - } - - if (moved) - { - GizmoMoved(); - UpdateNodeAttributes(); - needGizmoUndo = true; - } - } - else - { - if (previousGizmoDrag) - StoreGizmoEditActions(); - } - - previousGizmoDrag = drag; -} - -bool IsGizmoSelected() -{ - return gizmo !is null && gizmo.enabled && (gizmoAxisX.selected || gizmoAxisY.selected || gizmoAxisZ.selected); -} - -bool MoveNodes(Vector3 adjust) -{ - bool moved = false; - - if (adjust.length > M_EPSILON) - { - for (uint i = 0; i < editNodes.length; ++i) - { - if (moveSnap) - { - float moveStepScaled = moveStep * snapScale; - adjust.x = Floor(adjust.x / moveStepScaled + 0.5) * moveStepScaled; - adjust.y = Floor(adjust.y / moveStepScaled + 0.5) * moveStepScaled; - adjust.z = Floor(adjust.z / moveStepScaled + 0.5) * moveStepScaled; - } - - Node@ node = editNodes[i]; - Vector3 nodeAdjust = adjust; - if (axisMode == AXIS_LOCAL && editNodes.length == 1) - nodeAdjust = node.worldRotation * nodeAdjust; - - Vector3 worldPos = node.worldPosition; - Vector3 oldPos = node.position; - - worldPos += nodeAdjust; - - if (node.parent is null) - node.position = worldPos; - else - node.position = node.parent.WorldToLocal(worldPos); - - if (node.position != oldPos) - moved = true; - } - } - - return moved; -} - -bool RotateNodes(Vector3 adjust) -{ - bool moved = false; - - if (rotateSnap) - { - float rotateStepScaled = rotateStep * snapScale; - adjust.x = Floor(adjust.x / rotateStepScaled + 0.5) * rotateStepScaled; - adjust.y = Floor(adjust.y / rotateStepScaled + 0.5) * rotateStepScaled; - adjust.z = Floor(adjust.z / rotateStepScaled + 0.5) * rotateStepScaled; - } - - if (adjust.length > M_EPSILON) - { - moved = true; - - for (uint i = 0; i < editNodes.length; ++i) - { - Node@ node = editNodes[i]; - Quaternion rotQuat(adjust); - if (axisMode == AXIS_LOCAL && editNodes.length == 1) - node.rotation = node.rotation * rotQuat; - else - { - Vector3 offset = node.worldPosition - gizmoAxisX.axisRay.origin; - if (node.parent !is null && node.parent.worldRotation != Quaternion(1, 0, 0, 0)) - rotQuat = node.parent.worldRotation.Inverse() * rotQuat * node.parent.worldRotation; - node.rotation = rotQuat * node.rotation; - Vector3 newPosition = gizmoAxisX.axisRay.origin + rotQuat * offset; - if (node.parent !is null) - newPosition = node.parent.WorldToLocal(newPosition); - node.position = newPosition; - } - } - } - - return moved; -} - -bool ScaleNodes(Vector3 adjust) -{ - bool moved = false; - - if (adjust.length > M_EPSILON) - { - for (uint i = 0; i < editNodes.length; ++i) - { - Node@ node = editNodes[i]; - - Vector3 scale = node.scale; - Vector3 oldScale = scale; - - if (!scaleSnap) - scale += adjust; - else - { - float scaleStepScaled = scaleStep * snapScale; - if (adjust.x != 0) - { - scale.x += adjust.x * scaleStepScaled; - scale.x = Floor(scale.x / scaleStepScaled + 0.5) * scaleStepScaled; - } - if (adjust.y != 0) - { - scale.y += adjust.y * scaleStepScaled; - scale.y = Floor(scale.y / scaleStepScaled + 0.5) * scaleStepScaled; - } - if (adjust.z != 0) - { - scale.z += adjust.z * scaleStepScaled; - scale.z = Floor(scale.z / scaleStepScaled + 0.5) * scaleStepScaled; - } - } - - if (scale != oldScale) - moved = true; - - node.scale = scale; - } - } - - return moved; -} - -void StoreGizmoEditActions() -{ - if (needGizmoUndo && editNodes.length > 0 && oldGizmoTransforms.length == editNodes.length) - { - EditActionGroup group; - - for (uint i = 0; i < editNodes.length; ++i) - { - EditNodeTransformAction action; - action.Define(editNodes[i], oldGizmoTransforms[i]); - group.actions.Push(action); - } - - SaveEditActionGroup(group); - SetSceneModified(); - } - - needGizmoUndo = false; -} +// Urho3D editor node transform gizmo handling + +Node@ gizmoNode; +StaticModel@ gizmo; + +const float axisMaxD = 0.1; +const float axisMaxT = 1.0; +const float rotSensitivity = 50.0; + +EditMode lastGizmoMode; + +// For undo +bool previousGizmoDrag; +bool needGizmoUndo; +Array oldGizmoTransforms; + +class GizmoAxis +{ + Ray axisRay; + bool selected; + bool lastSelected; + float t; + float d; + float lastT; + float lastD; + + GizmoAxis() + { + selected = false; + lastSelected = false; + t = 0.0; + d = 0.0; + lastT = 0.0; + lastD = 0.0; + } + + void Update(Ray cameraRay, float scale, bool drag) + { + // Do not select when UI has modal element + if (ui.HasModalElement()) + { + selected = false; + return; + } + + Vector3 closest = cameraRay.ClosestPoint(axisRay); + Vector3 projected = axisRay.Project(closest); + d = axisRay.Distance(closest); + t = (projected - axisRay.origin).DotProduct(axisRay.direction); + + // Determine the sign of d from a plane that goes through the camera position to the axis + Plane axisPlane(cameraNode.position, axisRay.origin, axisRay.origin + axisRay.direction); + if (axisPlane.Distance(closest) < 0.0) + d = -d; + + // Update selected status only when not dragging + if (!drag) + { + selected = Abs(d) < axisMaxD * scale && t >= -axisMaxD * scale && t <= axisMaxT * scale; + lastT = t; + lastD = d; + } + } + + void Moved() + { + lastT = t; + lastD = d; + } +} + +GizmoAxis gizmoAxisX; +GizmoAxis gizmoAxisY; +GizmoAxis gizmoAxisZ; + +void CreateGizmo() +{ + gizmoNode = Node(); + gizmo = gizmoNode.CreateComponent("StaticModel"); + gizmo.model = cache.GetResource("Model", "Editor/Models/Axes.mdl"); + gizmo.materials[0] = cache.GetResource("Material", "Editor/Materials/RedUnlit.xml"); + gizmo.materials[1] = cache.GetResource("Material", "Editor/Materials/GreenUnlit.xml"); + gizmo.materials[2] = cache.GetResource("Material", "Editor/Materials/BlueUnlit.xml"); + gizmo.enabled = false; + gizmo.viewMask = 0x80000000; // Editor raycasts use viewmask 0x7fffffff + gizmo.occludee = false; + gizmoNode.name = "EditorGizmo"; + + gizmoAxisX.lastSelected = false; + gizmoAxisY.lastSelected = false; + gizmoAxisZ.lastSelected = false; + lastGizmoMode = EDIT_MOVE; +} + +void HideGizmo() +{ + if (gizmo !is null) + gizmo.enabled = false; +} + +void ShowGizmo() +{ + if (gizmo !is null) + { + gizmo.enabled = true; + + // Because setting enabled = false detaches the gizmo from octree, + // and it is a manually added drawable, must readd to octree when showing + if (editorScene.octree !is null) + editorScene.octree.AddManualDrawable(gizmo); + } +} + +void UpdateGizmo() +{ + UseGizmo(); + PositionGizmo(); + ResizeGizmo(); +} + +void PositionGizmo() +{ + if (gizmo is null) + return; + + Vector3 center(0, 0, 0); + bool containsScene = false; + + for (uint i = 0; i < editNodes.length; ++i) + { + // Scene's transform should not be edited, so hide gizmo if it is included + if (editNodes[i] is editorScene) + { + containsScene = true; + break; + } + center += editNodes[i].worldPosition; + } + + if (editNodes.empty || containsScene) + { + HideGizmo(); + return; + } + + center /= editNodes.length; + gizmoNode.position = center; + + if (axisMode == AXIS_WORLD || editNodes.length > 1) + gizmoNode.rotation = Quaternion(); + else + gizmoNode.rotation = editNodes[0].worldRotation; + + if (editMode != lastGizmoMode) + { + switch (editMode) + { + case EDIT_MOVE: + gizmo.model = cache.GetResource("Model", "Editor/Models/Axes.mdl"); + break; + + case EDIT_ROTATE: + gizmo.model = cache.GetResource("Model", "Editor/Models/RotateAxes.mdl"); + break; + + case EDIT_SCALE: + gizmo.model = cache.GetResource("Model", "Editor/Models/ScaleAxes.mdl"); + break; + } + + lastGizmoMode = editMode; + } + + if ((editMode != EDIT_SELECT && !orbiting) && !gizmo.enabled) + ShowGizmo(); + else if ((editMode == EDIT_SELECT || orbiting) && gizmo.enabled) + HideGizmo(); +} + +void ResizeGizmo() +{ + if (gizmo is null || !gizmo.enabled) + return; + + float scale = 0.1 / camera.zoom; + + if (camera.orthographic) + scale *= camera.orthoSize; + else + scale *= (camera.view * gizmoNode.position).z; + + gizmoNode.scale = Vector3(scale, scale, scale); +} + +void CalculateGizmoAxes() +{ + gizmoAxisX.axisRay = Ray(gizmoNode.position, gizmoNode.rotation * Vector3(1, 0, 0)); + gizmoAxisY.axisRay = Ray(gizmoNode.position, gizmoNode.rotation * Vector3(0, 1, 0)); + gizmoAxisZ.axisRay = Ray(gizmoNode.position, gizmoNode.rotation * Vector3(0, 0, 1)); +} + +void GizmoMoved() +{ + gizmoAxisX.Moved(); + gizmoAxisY.Moved(); + gizmoAxisZ.Moved(); +} + +void UseGizmo() +{ + if (gizmo is null || !gizmo.enabled || editMode == EDIT_SELECT) + { + StoreGizmoEditActions(); + previousGizmoDrag = false; + return; + } + + IntVector2 pos = ui.cursorPosition; + if (ui.GetElementAt(pos) !is null) + return; + + Ray cameraRay = GetActiveViewportCameraRay(); + float scale = gizmoNode.scale.x; + + // Recalculate axes only when not left-dragging + bool drag = input.mouseButtonDown[MOUSEB_LEFT]; + if (!drag) + CalculateGizmoAxes(); + + gizmoAxisX.Update(cameraRay, scale, drag); + gizmoAxisY.Update(cameraRay, scale, drag); + gizmoAxisZ.Update(cameraRay, scale, drag); + + if (gizmoAxisX.selected != gizmoAxisX.lastSelected) + { + gizmo.materials[0] = cache.GetResource("Material", gizmoAxisX.selected ? "Editor/Materials/BrightRedUnlit.xml" : + "Editor/Materials/RedUnlit.xml"); + gizmoAxisX.lastSelected = gizmoAxisX.selected; + } + if (gizmoAxisY.selected != gizmoAxisY.lastSelected) + { + gizmo.materials[1] = cache.GetResource("Material", gizmoAxisY.selected ? "Editor/Materials/BrightGreenUnlit.xml" : + "Editor/Materials/GreenUnlit.xml"); + gizmoAxisY.lastSelected = gizmoAxisY.selected; + } + if (gizmoAxisZ.selected != gizmoAxisZ.lastSelected) + { + gizmo.materials[2] = cache.GetResource("Material", gizmoAxisZ.selected ? "Editor/Materials/BrightBlueUnlit.xml" : + "Editor/Materials/BlueUnlit.xml"); + gizmoAxisZ.lastSelected = gizmoAxisZ.selected; + }; + + if (drag) + { + // Store initial transforms for undo when gizmo drag started + if (!previousGizmoDrag) + { + oldGizmoTransforms.Resize(editNodes.length); + for (uint i = 0; i < editNodes.length; ++i) + oldGizmoTransforms[i].Define(editNodes[i]); + } + + bool moved = false; + + if (editMode == EDIT_MOVE) + { + Vector3 adjust(0, 0, 0); + if (gizmoAxisX.selected) + adjust += Vector3(1, 0, 0) * (gizmoAxisX.t - gizmoAxisX.lastT); + if (gizmoAxisY.selected) + adjust += Vector3(0, 1, 0) * (gizmoAxisY.t - gizmoAxisY.lastT); + if (gizmoAxisZ.selected) + adjust += Vector3(0, 0, 1) * (gizmoAxisZ.t - gizmoAxisZ.lastT); + + moved = MoveNodes(adjust); + } + else if (editMode == EDIT_ROTATE) + { + Vector3 adjust(0, 0, 0); + if (gizmoAxisX.selected) + adjust.x = (gizmoAxisX.d - gizmoAxisX.lastD) * rotSensitivity / scale; + if (gizmoAxisY.selected) + adjust.y = -(gizmoAxisY.d - gizmoAxisY.lastD) * rotSensitivity / scale; + if (gizmoAxisZ.selected) + adjust.z = (gizmoAxisZ.d - gizmoAxisZ.lastD) * rotSensitivity / scale; + + moved = RotateNodes(adjust); + } + else if (editMode == EDIT_SCALE) + { + Vector3 adjust(0, 0, 0); + if (gizmoAxisX.selected) + adjust += Vector3(1, 0, 0) * (gizmoAxisX.t - gizmoAxisX.lastT); + if (gizmoAxisY.selected) + adjust += Vector3(0, 1, 0) * (gizmoAxisY.t - gizmoAxisY.lastT); + if (gizmoAxisZ.selected) + adjust += Vector3(0, 0, 1) * (gizmoAxisZ.t - gizmoAxisZ.lastT); + + // Special handling for uniform scale: use the unmodified X-axis movement only + if (editMode == EDIT_SCALE && gizmoAxisX.selected && gizmoAxisY.selected && gizmoAxisZ.selected) + { + float x = gizmoAxisX.t - gizmoAxisX.lastT; + adjust = Vector3(x, x, x); + } + + moved = ScaleNodes(adjust); + } + + if (moved) + { + GizmoMoved(); + UpdateNodeAttributes(); + needGizmoUndo = true; + } + } + else + { + if (previousGizmoDrag) + StoreGizmoEditActions(); + } + + previousGizmoDrag = drag; +} + +bool IsGizmoSelected() +{ + return gizmo !is null && gizmo.enabled && (gizmoAxisX.selected || gizmoAxisY.selected || gizmoAxisZ.selected); +} + +bool MoveNodes(Vector3 adjust) +{ + bool moved = false; + + if (adjust.length > M_EPSILON) + { + for (uint i = 0; i < editNodes.length; ++i) + { + if (moveSnap) + { + float moveStepScaled = moveStep * snapScale; + adjust.x = Floor(adjust.x / moveStepScaled + 0.5) * moveStepScaled; + adjust.y = Floor(adjust.y / moveStepScaled + 0.5) * moveStepScaled; + adjust.z = Floor(adjust.z / moveStepScaled + 0.5) * moveStepScaled; + } + + Node@ node = editNodes[i]; + Vector3 nodeAdjust = adjust; + if (axisMode == AXIS_LOCAL && editNodes.length == 1) + nodeAdjust = node.worldRotation * nodeAdjust; + + Vector3 worldPos = node.worldPosition; + Vector3 oldPos = node.position; + + worldPos += nodeAdjust; + + if (node.parent is null) + node.position = worldPos; + else + node.position = node.parent.WorldToLocal(worldPos); + + if (node.position != oldPos) + moved = true; + } + } + + return moved; +} + +bool RotateNodes(Vector3 adjust) +{ + bool moved = false; + + if (rotateSnap) + { + float rotateStepScaled = rotateStep * snapScale; + adjust.x = Floor(adjust.x / rotateStepScaled + 0.5) * rotateStepScaled; + adjust.y = Floor(adjust.y / rotateStepScaled + 0.5) * rotateStepScaled; + adjust.z = Floor(adjust.z / rotateStepScaled + 0.5) * rotateStepScaled; + } + + if (adjust.length > M_EPSILON) + { + moved = true; + + for (uint i = 0; i < editNodes.length; ++i) + { + Node@ node = editNodes[i]; + Quaternion rotQuat(adjust); + if (axisMode == AXIS_LOCAL && editNodes.length == 1) + node.rotation = node.rotation * rotQuat; + else + { + Vector3 offset = node.worldPosition - gizmoAxisX.axisRay.origin; + if (node.parent !is null && node.parent.worldRotation != Quaternion(1, 0, 0, 0)) + rotQuat = node.parent.worldRotation.Inverse() * rotQuat * node.parent.worldRotation; + node.rotation = rotQuat * node.rotation; + Vector3 newPosition = gizmoAxisX.axisRay.origin + rotQuat * offset; + if (node.parent !is null) + newPosition = node.parent.WorldToLocal(newPosition); + node.position = newPosition; + } + } + } + + return moved; +} + +bool ScaleNodes(Vector3 adjust) +{ + bool moved = false; + + if (adjust.length > M_EPSILON) + { + for (uint i = 0; i < editNodes.length; ++i) + { + Node@ node = editNodes[i]; + + Vector3 scale = node.scale; + Vector3 oldScale = scale; + + if (!scaleSnap) + scale += adjust; + else + { + float scaleStepScaled = scaleStep * snapScale; + if (adjust.x != 0) + { + scale.x += adjust.x * scaleStepScaled; + scale.x = Floor(scale.x / scaleStepScaled + 0.5) * scaleStepScaled; + } + if (adjust.y != 0) + { + scale.y += adjust.y * scaleStepScaled; + scale.y = Floor(scale.y / scaleStepScaled + 0.5) * scaleStepScaled; + } + if (adjust.z != 0) + { + scale.z += adjust.z * scaleStepScaled; + scale.z = Floor(scale.z / scaleStepScaled + 0.5) * scaleStepScaled; + } + } + + if (scale != oldScale) + moved = true; + + node.scale = scale; + } + } + + return moved; +} + +void StoreGizmoEditActions() +{ + if (needGizmoUndo && editNodes.length > 0 && oldGizmoTransforms.length == editNodes.length) + { + EditActionGroup group; + + for (uint i = 0; i < editNodes.length; ++i) + { + EditNodeTransformAction action; + action.Define(editNodes[i], oldGizmoTransforms[i]); + group.actions.Push(action); + } + + SaveEditActionGroup(group); + SetSceneModified(); + } + + needGizmoUndo = false; +} diff --git a/bin/Data/Scripts/Editor/EditorHierarchyWindow.as b/bin/EditorData/Editor/Scripts/EditorHierarchyWindow.as similarity index 96% rename from bin/Data/Scripts/Editor/EditorHierarchyWindow.as rename to bin/EditorData/Editor/Scripts/EditorHierarchyWindow.as index 45581da95d5..39d7c838757 100644 --- a/bin/Data/Scripts/Editor/EditorHierarchyWindow.as +++ b/bin/EditorData/Editor/Scripts/EditorHierarchyWindow.as @@ -1,1945 +1,1945 @@ -// Urho3D editor hierarchy window handling - -const int ITEM_NONE = 0; -const int ITEM_NODE = 1; -const int ITEM_COMPONENT = 2; -const int ITEM_UI_ELEMENT = 3; -const uint NO_ITEM = M_MAX_UNSIGNED; -const StringHash SCENE_TYPE("Scene"); -const StringHash NODE_TYPE("Node"); -const StringHash STATICMODEL_TYPE("StaticModel"); -const StringHash ANIMATEDMODEL_TYPE("AnimatedModel"); -const StringHash STATICMODELGROUP_TYPE("StaticModelGroup"); -const StringHash SPLINEPATH_TYPE("SplinePath"); -const StringHash CONSTRAINT_TYPE("Constraint"); -const String NO_CHANGE(0); -const StringHash TYPE_VAR("Type"); -const StringHash NODE_ID_VAR("NodeID"); -const StringHash COMPONENT_ID_VAR("ComponentID"); -const StringHash UI_ELEMENT_ID_VAR("UIElementID"); -const StringHash DRAGDROPCONTENT_VAR("DragDropContent"); -const StringHash[] ID_VARS = { StringHash(""), NODE_ID_VAR, COMPONENT_ID_VAR, UI_ELEMENT_ID_VAR }; -Color nodeTextColor(1.0f, 1.0f, 1.0f); -Color componentTextColor(0.7f, 1.0f, 0.7f); - -Window@ hierarchyWindow; -ListView@ hierarchyList; -bool showID = true; - -// UIElement does not have unique ID, so use a running number to generate a new ID each time an item is inserted into hierarchy list -const uint UI_ELEMENT_BASE_ID = 1; -uint uiElementNextID = UI_ELEMENT_BASE_ID; -bool showInternalUIElement = false; -bool showTemporaryObject = false; -Array hierarchyUpdateSelections; - -Variant GetUIElementID(UIElement@ element) -{ - Variant elementID = element.GetVar(UI_ELEMENT_ID_VAR); - if (elementID.empty) - { - // Generate new ID - elementID = uiElementNextID++; - // Store the generated ID - element.vars[UI_ELEMENT_ID_VAR] = elementID; - } - - return elementID; -} - -UIElement@ GetUIElementByID(const Variant&in id) -{ - return id == UI_ELEMENT_BASE_ID ? editorUIElement : editorUIElement.GetChild(UI_ELEMENT_ID_VAR, id, true); -} - -void CreateHierarchyWindow() -{ - if (hierarchyWindow !is null) - return; - - hierarchyWindow = LoadEditorUI("UI/EditorHierarchyWindow.xml"); - hierarchyList = hierarchyWindow.GetChild("HierarchyList"); - ui.root.AddChild(hierarchyWindow); - int height = Min(ui.root.height - 60, 500); - hierarchyWindow.SetSize(300, height); - hierarchyWindow.SetPosition(35, 100); - hierarchyWindow.opacity = uiMaxOpacity; - hierarchyWindow.BringToFront(); - - UpdateHierarchyItem(editorScene); - - // Set selection to happen on click end, so that we can drag nodes to the inspector without resetting the inspector view - hierarchyList.selectOnClickEnd = true; - - // Set drag & drop target mode on the node list background, which is used to parent nodes back to the root node - hierarchyList.contentElement.dragDropMode = DD_TARGET; - hierarchyList.scrollPanel.dragDropMode = DD_TARGET; - - SubscribeToEvent(hierarchyWindow.GetChild("CloseButton", true), "Released", "HideHierarchyWindow"); - SubscribeToEvent(hierarchyWindow.GetChild("ExpandButton", true), "Released", "ExpandCollapseHierarchy"); - SubscribeToEvent(hierarchyWindow.GetChild("CollapseButton", true), "Released", "ExpandCollapseHierarchy"); - SubscribeToEvent(hierarchyWindow.GetChild("ResetButton", true), "Released", "CollapseHierarchy"); - SubscribeToEvent(hierarchyWindow.GetChild("ShowID", true), "Toggled", "HandleShowID"); - SubscribeToEvent(hierarchyList, "SelectionChanged", "HandleHierarchyListSelectionChange"); - SubscribeToEvent(hierarchyList, "ItemDoubleClicked", "HandleHierarchyListDoubleClick"); - SubscribeToEvent(hierarchyList, "ItemClicked", "HandleHierarchyItemClick"); - SubscribeToEvent("DragDropTest", "HandleDragDropTest"); - SubscribeToEvent("DragDropFinish", "HandleDragDropFinish"); - SubscribeToEvent(editorScene, "ComponentAdded", "HandleComponentAdded"); - SubscribeToEvent(editorScene, "ComponentRemoved", "HandleComponentRemoved"); - SubscribeToEvent(editorScene, "NodeEnabledChanged", "HandleNodeEnabledChanged"); - SubscribeToEvent(editorScene, "ComponentEnabledChanged", "HandleComponentEnabledChanged"); - SubscribeToEvent("TemporaryChanged", "HandleTemporaryChanged"); -} - -bool ToggleHierarchyWindow() -{ - if (hierarchyWindow.visible == false) - ShowHierarchyWindow(); - else - HideHierarchyWindow(); - return true; -} - -void ShowHierarchyWindow() -{ - hierarchyWindow.visible = true; - hierarchyWindow.BringToFront(); -} - -void HideHierarchyWindow() -{ - if(viewportMode == VIEWPORT_COMPACT) - return; - hierarchyWindow.visible = false; -} - -void ExpandCollapseHierarchy(StringHash eventType, VariantMap& eventData) -{ - Button@ button = eventData["Element"].GetPtr(); - bool enable = button.name == "ExpandButton"; - CheckBox@ checkBox = hierarchyWindow.GetChild("AllCheckBox", true); - bool all = checkBox.checked; - checkBox.checked = false; // Auto-reset - - Array selections = hierarchyList.selections; - for (uint i = 0; i < selections.length; ++i) - hierarchyList.Expand(selections[i], enable, all); -} - -void EnableExpandCollapseButtons(bool enable) -{ - String[] buttons = { "ExpandButton", "CollapseButton", "AllCheckBox" }; - for (uint i = 0; i < buttons.length; ++i) - { - UIElement@ element = hierarchyWindow.GetChild(buttons[i], true); - element.enabled = enable; - element.children[0].color = enable ? normalTextColor : nonEditableTextColor; - } -} - -void UpdateHierarchyItem(Serializable@ serializable, bool clear = false) -{ - if (clear) - { - // Remove the current selection before updating the list item (in turn trigger an update on the attribute editor) - hierarchyList.ClearSelection(); - - // Clear copybuffer when whole window refreshed - sceneCopyBuffer.Clear(); - uiElementCopyBuffer.Clear(); - } - - // In case of item's parent is not found in the hierarchy list then the item will be inserted at the list root level - Serializable@ parent; - switch (GetType(serializable)) - { - case ITEM_NODE: - parent = cast(serializable).parent; - break; - - case ITEM_COMPONENT: - parent = cast(serializable).node; - break; - - case ITEM_UI_ELEMENT: - parent = cast(serializable).parent; - break; - - default: - break; - } - UIElement@ parentItem = hierarchyList.items[GetListIndex(parent)]; - UpdateHierarchyItem(GetListIndex(serializable), serializable, parentItem); -} - -uint UpdateHierarchyItem(uint itemIndex, Serializable@ serializable, UIElement@ parentItem) -{ - // Whenever we're updating, disable layout update to optimize speed - hierarchyList.contentElement.DisableLayoutUpdate(); - - if (serializable is null) - { - hierarchyList.RemoveItem(itemIndex); - hierarchyList.contentElement.EnableLayoutUpdate(); - hierarchyList.contentElement.UpdateLayout(); - return itemIndex; - } - - int itemType = GetType(serializable); - Variant id = GetID(serializable, itemType); - - // Remove old item if exists - if (itemIndex < hierarchyList.numItems && itemIndex != NINDEX && MatchID(hierarchyList.items[itemIndex], id, itemType)) - hierarchyList.RemoveItem(itemIndex); - - Text@ text = Text(); - hierarchyList.InsertItem(itemIndex, text, parentItem); - text.style = "FileSelectorListText"; - - if (serializable.type == SCENE_TYPE || serializable is editorUIElement) - // The root node (scene) and editor's root UIElement cannot be moved by drag and drop - text.dragDropMode = DD_TARGET; - else - // Internal UIElement is not able to participate in drag and drop action - text.dragDropMode = itemType == ITEM_UI_ELEMENT && cast(serializable).internal ? DD_DISABLED : DD_SOURCE_AND_TARGET; - - // Advance the index for the child items - if (itemIndex == M_MAX_UNSIGNED) - itemIndex = hierarchyList.numItems; - else - ++itemIndex; - - String iconType = serializable.typeName; - if (serializable is editorUIElement) - iconType = "Root" + iconType; - IconizeUIElement(text, iconType); - - SetID(text, serializable, itemType); - switch (itemType) - { - case ITEM_NODE: - { - Node@ node = cast(serializable); - - text.text = GetNodeTitle(node); - text.color = nodeTextColor; - SetIconEnabledColor(text, node.enabled); - - // Update components first - for (uint i = 0; i < node.numComponents; ++i) - { - Component@ component = node.components[i]; - if (showTemporaryObject || !component.temporary) - AddComponentItem(itemIndex++, component, text); - } - - // Then update child nodes recursively - for (uint i = 0; i < node.numChildren; ++i) - { - Node@ childNode = node.children[i]; - if (showTemporaryObject || !childNode.temporary) - itemIndex = UpdateHierarchyItem(itemIndex, childNode, text); - } - - break; - } - - case ITEM_COMPONENT: - { - Component@ component = cast(serializable); - text.text = GetComponentTitle(component); - text.color = componentTextColor; - SetIconEnabledColor(text, component.enabledEffective); - break; - } - - case ITEM_UI_ELEMENT: - { - UIElement@ element = cast(serializable); - - text.text = GetUIElementTitle(element); - SetIconEnabledColor(text, element.visible); - - // Update child elements recursively - for (uint i = 0; i < element.numChildren; ++i) - { - UIElement@ childElement = element.children[i]; - if ((showInternalUIElement || !childElement.internal) && (showTemporaryObject || !childElement.temporary)) - itemIndex = UpdateHierarchyItem(itemIndex, childElement, text); - } - - break; - } - - default: - break; - } - - // Re-enable layout update (and do manual layout) now - hierarchyList.contentElement.EnableLayoutUpdate(); - hierarchyList.contentElement.UpdateLayout(); - - return itemIndex; -} - -void UpdateHierarchyItemText(uint itemIndex, bool iconEnabled, const String&in textTitle = NO_CHANGE) -{ - Text@ text = hierarchyList.items[itemIndex]; - if (text is null) - return; - - SetIconEnabledColor(text, iconEnabled); - - if (textTitle != NO_CHANGE) - text.text = textTitle; -} - -void AddComponentItem(uint compItemIndex, Component@ component, UIElement@ parentItem) -{ - Text@ text = Text(); - hierarchyList.InsertItem(compItemIndex, text, parentItem); - text.style = "FileSelectorListText"; - text.vars[TYPE_VAR] = ITEM_COMPONENT; - text.vars[NODE_ID_VAR] = component.node.id; - text.vars[COMPONENT_ID_VAR] = component.id; - text.text = GetComponentTitle(component); - text.color = componentTextColor; - text.dragDropMode = DD_SOURCE_AND_TARGET; - - IconizeUIElement(text, component.typeName); - SetIconEnabledColor(text, component.enabledEffective); -} - -int GetType(Serializable@ serializable) -{ - if (cast(serializable) !is null) - return ITEM_NODE; - else if (cast(serializable) !is null) - return ITEM_COMPONENT; - else if (cast(serializable) !is null) - return ITEM_UI_ELEMENT; - else - return ITEM_NONE; -} - -void SetID(Text@ text, Serializable@ serializable, int itemType = ITEM_NONE) -{ - // If item type is not provided, auto detect it - if (itemType == ITEM_NONE) - itemType = GetType(serializable); - - text.vars[TYPE_VAR] = itemType; - text.vars[ID_VARS[itemType]] = GetID(serializable, itemType); - - // Set node ID as drag and drop content for node ID editing - if (itemType == ITEM_NODE) - text.vars[DRAGDROPCONTENT_VAR] = String(text.vars[NODE_ID_VAR].GetU32()); - - switch (itemType) - { - case ITEM_COMPONENT: - text.vars[NODE_ID_VAR] = cast(serializable).node.id; - break; - - case ITEM_UI_ELEMENT: - // Subscribe to UI-element events - SubscribeToEvent(serializable, "NameChanged", "HandleElementNameChanged"); - SubscribeToEvent(serializable, "VisibleChanged", "HandleElementVisibilityChanged"); - SubscribeToEvent(serializable, "Resized", "HandleElementAttributeChanged"); - SubscribeToEvent(serializable, "Positioned", "HandleElementAttributeChanged"); - break; - - default: - break; - } -} - -uint GetID(Serializable@ serializable, int itemType = ITEM_NONE) -{ - // If item type is not provided, auto detect it - if (itemType == ITEM_NONE) - itemType = GetType(serializable); - - switch (itemType) - { - case ITEM_NODE: - return cast(serializable).id; - - case ITEM_COMPONENT: - return cast(serializable).id; - - case ITEM_UI_ELEMENT: - return GetUIElementID(cast(serializable)).GetU32(); - } - - return M_MAX_UNSIGNED; -} - -bool MatchID(UIElement@ element, const Variant&in id, int itemType) -{ - return element.GetVar(TYPE_VAR).GetI32() == itemType && element.GetVar(ID_VARS[itemType]) == id; -} - -uint GetListIndex(Serializable@ serializable) -{ - if (serializable is null) - return NO_ITEM; - - int itemType = GetType(serializable); - Variant id = GetID(serializable, itemType); - - uint numItems = hierarchyList.numItems; - for (uint i = 0; i < numItems; ++i) - { - if (MatchID(hierarchyList.items[i], id, itemType)) - return i; - } - - return NO_ITEM; -} - -UIElement@ GetListUIElement(uint index) -{ - UIElement@ item = hierarchyList.items[index]; - if (item is null) - return null; - - // Get the text item's ID and use it to retrieve the actual UIElement the text item is associated to - return GetUIElementByID(GetUIElementID(item)); -} - -Node@ GetListNode(uint index) -{ - UIElement@ item = hierarchyList.items[index]; - if (item is null) - return null; - - return editorScene.GetNode(item.vars[NODE_ID_VAR].GetU32()); -} - -Component@ GetListComponent(uint index) -{ - UIElement@ item = hierarchyList.items[index]; - return GetListComponent(item); -} - -Component@ GetListComponent(UIElement@ item) -{ - if (item is null) - return null; - - if (item.vars[TYPE_VAR].GetI32() != ITEM_COMPONENT) - return null; - - return editorScene.GetComponent(item.vars[COMPONENT_ID_VAR].GetU32()); -} - -uint GetComponentListIndex(Component@ component) -{ - if (component is null) - return NO_ITEM; - - uint numItems = hierarchyList.numItems; - for (uint i = 0; i < numItems; ++i) - { - UIElement@ item = hierarchyList.items[i]; - if (item.vars[TYPE_VAR].GetI32() == ITEM_COMPONENT && item.vars[COMPONENT_ID_VAR].GetU32() == component.id) - return i; - } - - return NO_ITEM; -} - -String GetUIElementTitle(UIElement@ element) -{ - String ret; - - // Only top level UI-element has this variable - String modifiedStr = element.GetVar(MODIFIED_VAR).GetBool() ? "*" : ""; - ret = (element.name.empty ? element.typeName : element.name) + modifiedStr + " [" + GetUIElementID(element).ToString() + "]"; - - if (element.temporary) - ret += " (Temp)"; - - return ret; -} - -String GetNodeTitle(Node@ node) -{ - String ret; - - if (node.name.empty) - ret = node.typeName; - else - ret = node.name; - - if (showID) - { - if (node.replicated) - ret += " (" + String(node.id) + ")"; - else - ret += " (Local " + String(node.id) + ")"; - - if (node.temporary) - ret += " (Temp)"; - } - - return ret; -} - -String GetComponentTitle(Component@ component) -{ - String ret = component.typeName; - - if (showID) - { - if (!component.replicated) - ret += " (Local)"; - - if (component.temporary) - ret += " (Temp)"; - } - return ret; -} - -void SelectNode(Node@ node, bool multiselect) -{ - if (node is null && !multiselect) - { - hierarchyList.ClearSelection(); - return; - } - - lastSelectedNode = node; - uint index = GetListIndex(node); - uint numItems = hierarchyList.numItems; - - if (index < numItems) - { - // Expand the node chain now - if (!multiselect || !hierarchyList.IsSelected(index)) - { - // Go in the parent chain up to make sure the chain is expanded - Node@ current = node; - do - { - hierarchyList.Expand(GetListIndex(current), true); - current = current.parent; - } - while (current !is null); - } - - // This causes an event to be sent, in response we set the node/component selections, and refresh editors - if (!multiselect) - hierarchyList.selection = index; - else - hierarchyList.ToggleSelection(index); - } - else if (!multiselect) - hierarchyList.ClearSelection(); -} - -void DeselectNode(Node@ node) -{ - if (node is null) - { - hierarchyList.ClearSelection(); - return; - } - - uint index = GetListIndex(node); - uint numItems = hierarchyList.numItems; - - if (index < numItems) - { - hierarchyList.ToggleSelection(index); - } -} - -void SelectComponent(Component@ component, bool multiselect) -{ - if (component is null && !multiselect) - { - hierarchyList.ClearSelection(); - return; - } - - Node@ node = component.node; - if (node is null && !multiselect) - { - hierarchyList.ClearSelection(); - return; - } - - uint nodeIndex = GetListIndex(node); - uint componentIndex = GetComponentListIndex(component); - uint numItems = hierarchyList.numItems; - - if (nodeIndex < numItems && componentIndex < numItems) - { - // Expand the node chain now - if (!multiselect || !hierarchyList.IsSelected(componentIndex)) - { - // Go in the parent chain up to make sure the chain is expanded - Node@ current = node; - do - { - hierarchyList.Expand(GetListIndex(current), true); - current = current.parent; - } - while (current !is null); - } - - // This causes an event to be sent, in response we set the node/component selections, and refresh editors - if (!multiselect) - hierarchyList.selection = componentIndex; - else - hierarchyList.ToggleSelection(componentIndex); - } - else if (!multiselect) - hierarchyList.ClearSelection(); -} - -void SelectUIElement(UIElement@ element, bool multiselect) -{ - uint index = GetListIndex(element); - uint numItems = hierarchyList.numItems; - - if (index < numItems) - { - // Expand the node chain now - if (!multiselect || !hierarchyList.IsSelected(index)) - { - // Go in the parent chain up to make sure the chain is expanded - UIElement@ current = element; - do - { - hierarchyList.Expand(GetListIndex(current), true); - current = current.parent; - } - while (current !is null); - } - - if (!multiselect) - hierarchyList.selection = index; - else - hierarchyList.ToggleSelection(index); - } - else if (!multiselect) - hierarchyList.ClearSelection(); -} - -// Find the first selected StaticModel/AtimatedModel component or node with it -Model@ FindFirstSelectedModel() -{ - for (uint i = 0; i < selectedComponents.length; ++i) - { - // Get SM, but also works well for AnimatedModel - StaticModel@ sm = cast(selectedComponents[i]); - if (sm !is null && sm.model !is null) - return sm.model; - } - - for (uint i = 0; i < selectedNodes.length; ++i) - { - for (uint j = 0; j < selectedNodes[i].numComponents; ++j) - { - StaticModel@ sm = cast(selectedNodes[i].components[j]); - if (sm !is null && sm.model !is null) - return sm.model; - } - } - - return null; -} - -void UpdateModelInfo(Model@ model) -{ - if (model is null) - { - modelInfoText.text = ""; - return; - } - - String infoStr = "Model: " + model.name; - - infoStr += "\n Morphs: " + model.numMorphs; - - for (uint g = 0; g < model.numGeometries; ++g) - { - uint numLods = model.numGeometryLodLevels[g]; - infoStr += "\n Geometry " + g + "\n Lods: " + numLods; - for (uint l = 0; l < numLods; l++) - { - Geometry@ geom = model.GetGeometry(g, l); - infoStr += "\n Vertex Count: " + geom.vertexCount; - infoStr += "\n Index Count: " + geom.indexCount; - } - } - - modelInfoText.text = infoStr; -} - -void HandleHierarchyListSelectionChange() -{ - if (inSelectionModify) - return; - - ClearSceneSelection(); - ClearUIElementSelection(); - - Array indices = hierarchyList.selections; - - // Enable Expand/Collapse button when there is selection - EnableExpandCollapseButtons(indices.length > 0); - - for (uint i = 0; i < indices.length; ++i) - { - uint index = indices[i]; - UIElement@ item = hierarchyList.items[index]; - int type = item.vars[TYPE_VAR].GetI32(); - if (type == ITEM_COMPONENT) - { - Component@ comp = GetListComponent(index); - if (comp !is null) - selectedComponents.Push(comp); - } - else if (type == ITEM_NODE) - { - Node@ node = GetListNode(index); - if (node !is null) - selectedNodes.Push(node); - } - else if (type == ITEM_UI_ELEMENT) - { - UIElement@ element = GetListUIElement(index); - if (element !is null && element !is editorUIElement) - selectedUIElements.Push(element); - } - } - - // If only one node/UIElement selected, use it for editing - if (selectedNodes.length == 1) - editNode = selectedNodes[0]; - if (selectedUIElements.length == 1) - editUIElement = selectedUIElements[0]; - - // If selection contains only components, and they have a common node, use it for editing - if (selectedNodes.empty && !selectedComponents.empty) - { - Node@ commonNode; - for (uint i = 0; i < selectedComponents.length; ++i) - { - if (i == 0) - commonNode = selectedComponents[i].node; - else - { - if (selectedComponents[i].node !is commonNode) - commonNode = null; - } - } - editNode = commonNode; - } - - UpdateModelInfo(FindFirstSelectedModel()); - - // Now check if the component(s) can be edited. If many selected, must have same type or have same edit node - if (!selectedComponents.empty) - { - if (editNode is null) - { - StringHash compType = selectedComponents[0].type; - bool sameType = true; - for (uint i = 1; i < selectedComponents.length; ++i) - { - if (selectedComponents[i].type != compType) - { - sameType = false; - break; - } - } - if (sameType) - editComponents = selectedComponents; - } - else - { - editComponents = selectedComponents; - numEditableComponentsPerNode = selectedComponents.length; - } - } - - // If just nodes selected, and no components, show as many matching components for editing as possible - if (!selectedNodes.empty && selectedComponents.empty && selectedNodes[0].numComponents > 0) - { - uint count = 0; - for (uint j = 0; j < selectedNodes[0].numComponents; ++j) - { - StringHash compType = selectedNodes[0].components[j].type; - bool sameType = true; - for (uint i = 1; i < selectedNodes.length; ++i) - { - if (selectedNodes[i].numComponents <= j || selectedNodes[i].components[j].type != compType) - { - sameType = false; - break; - } - } - - if (sameType) - { - ++count; - for (uint i = 0; i < selectedNodes.length; ++i) - editComponents.Push(selectedNodes[i].components[j]); - } - } - if (count > 1) - numEditableComponentsPerNode = count; - } - - if (selectedNodes.empty && editNode !is null) - editNodes.Push(editNode); - else - { - editNodes = selectedNodes; - - // Cannot multi-edit on scene and node(s) together as scene and node do not share identical attributes, - // editing via gizmo does not make too much sense either - if (editNodes.length > 1 && editNodes[0] is editorScene) - editNodes.Erase(0); - } - - if (selectedUIElements.empty && editUIElement !is null) - editUIElements.Push(editUIElement); - else - editUIElements = selectedUIElements; - - PositionGizmo(); - UpdateAttributeInspector(); - UpdateCameraPreview(); -} - -void HandleHierarchyListDoubleClick(StringHash eventType, VariantMap& eventData) -{ - UIElement@ item = eventData["Item"].GetPtr(); - int type = item.vars[TYPE_VAR].GetI32(); - // Locate nodes from the scene by double-clicking - if (type == ITEM_NODE) - { - Node@ node = editorScene.GetNode(item.vars[NODE_ID_VAR].GetU32()); - Array nodes; - nodes.Push(node); - LocateNodes(nodes); - } - else if (type == ITEM_COMPONENT) - { - Component@ component = editorScene.GetComponent(item.vars[COMPONENT_ID_VAR].GetU32()); - Array components; - components.Push(component); - LocateComponents(components); - } - - bool isExpanded = hierarchyList.IsExpanded(hierarchyList.selection); - - if (!isExpanded && eventData["Button"].GetI32() == MOUSEB_LEFT) - { - isExpanded = !isExpanded; - hierarchyList.Expand(hierarchyList.selection, isExpanded, false); - } -} - -void HandleHierarchyItemClick(StringHash eventType, VariantMap& eventData) -{ - if (eventData["Button"].GetI32() != MOUSEB_RIGHT) - return; - - UIElement@ uiElement = eventData["Item"].GetPtr(); - int selectionIndex = eventData["Selection"].GetI32(); - - Array actions; - int type = uiElement.vars[TYPE_VAR].GetI32(); - - // Adds left clicked items to selection which is not normal listview behavior - if (type == ITEM_COMPONENT || type == ITEM_NODE) - { - if (input.keyDown[KEY_LSHIFT]) - hierarchyList.AddSelection(selectionIndex); - else - { - hierarchyList.ClearSelection(); - hierarchyList.AddSelection(selectionIndex); - } - } - - if (type == ITEM_COMPONENT) - { - Component@ targetComponent = editorScene.GetComponent(uiElement.vars[COMPONENT_ID_VAR].GetU32()); - if (targetComponent is null) - return; - - actions.Push(CreateContextMenuItem("Copy", "HandleHierarchyContextCopy")); - actions.Push(CreateContextMenuItem("Cut", "HandleHierarchyContextCut")); - actions.Push(CreateContextMenuItem("Delete", "HandleHierarchyContextDelete")); - actions.Push(CreateContextMenuItem("Paste", "HandleHierarchyContextPaste")); - actions.Push(CreateContextMenuItem("Enable/disable", "HandleHierarchyContextEnableDisable")); - - /* actions.Push(CreateBrowserFileActionMenu("Edit", "HandleBrowserEditResource", file)); */ - } - else if (type == ITEM_NODE) - { - actions.Push(CreateContextMenuItem("Create Replicated Node", "HandleHierarchyContextCreateReplicatedNode")); - actions.Push(CreateContextMenuItem("Create Local Node", "HandleHierarchyContextCreateLocalNode")); - actions.Push(CreateContextMenuItem("Duplicate", "HandleHierarchyContextDuplicate")); - actions.Push(CreateContextMenuItem("Copy", "HandleHierarchyContextCopy")); - actions.Push(CreateContextMenuItem("Cut", "HandleHierarchyContextCut")); - actions.Push(CreateContextMenuItem("Delete", "HandleHierarchyContextDelete")); - actions.Push(CreateContextMenuItem("Paste", "HandleHierarchyContextPaste")); - actions.Push(CreateContextMenuItem("Reset to default", "HandleHierarchyContextResetToDefault")); - actions.Push(CreateContextMenuItem("Reset position", "HandleHierarchyContextResetPosition")); - actions.Push(CreateContextMenuItem("Reset rotation", "HandleHierarchyContextResetRotation")); - actions.Push(CreateContextMenuItem("Reset scale", "HandleHierarchyContextResetScale")); - actions.Push(CreateContextMenuItem("Enable/disable", "HandleHierarchyContextEnableDisable")); - actions.Push(CreateContextMenuItem("Unparent", "HandleHierarchyContextUnparent")); - } - else if (type == ITEM_UI_ELEMENT) - { - // close ui element - actions.Push(CreateContextMenuItem("Close UI-Layout", "HandleHierarchyContextUIElementCloseUILayout")); - actions.Push(CreateContextMenuItem("Close all UI-layouts", "HandleHierarchyContextUIElementCloseAllUILayouts")); - } - - if (actions.length > 0) - ActivateContextMenu(actions); -} - -void HandleDragDropTest(StringHash eventType, VariantMap& eventData) -{ - UIElement@ source = eventData["Source"].GetPtr(); - UIElement@ target = eventData["Target"].GetPtr(); - int itemType; - eventData["Accept"] = TestDragDrop(source, target, itemType); -} - -void HandleDragDropFinish(StringHash eventType, VariantMap& eventData) -{ - UIElement@ source = eventData["Source"].GetPtr(); - UIElement@ target = eventData["Target"].GetPtr(); - int itemType = ITEM_NONE; - bool accept = TestDragDrop(source, target, itemType); - eventData["Accept"] = accept; - if (!accept) - return; - - // Resource browser - if (source !is null && source.GetVar(TEXT_VAR_RESOURCE_TYPE).GetI32() > 0) - { - int type = source.GetVar(TEXT_VAR_RESOURCE_TYPE).GetI32(); - - BrowserFile@ browserFile = GetBrowserFileFromId(source.vars[TEXT_VAR_FILE_ID].GetU32()); - if (browserFile is null) - return; - - Component@ createdComponent; - if (itemType == ITEM_NODE) - { - Node@ targetNode = editorScene.GetNode(target.vars[NODE_ID_VAR].GetU32()); - if (targetNode is null) - return; - - if (type == RESOURCE_TYPE_PREFAB) - { - LoadNode(browserFile.GetFullPath(), targetNode); - } - else if(type == RESOURCE_TYPE_SCRIPTFILE) - { - // TODO: not sure what to do here. lots of choices. - } - else if(type == RESOURCE_TYPE_MODEL) - { - CreateModelWithStaticModel(browserFile.resourceKey, targetNode); - return; - } - else if (type == RESOURCE_TYPE_PARTICLEEFFECT) - { - if (browserFile.extension == "xml") - { - ParticleEffect@ effect = cache.GetResource("ParticleEffect", browserFile.resourceKey); - if (effect is null) - return; - - ParticleEmitter@ emitter = targetNode.CreateComponent("ParticleEmitter"); - emitter.effect = effect; - createdComponent = emitter; - } - } - else if (type == RESOURCE_TYPE_2D_PARTICLE_EFFECT) - { - if (browserFile.extension == "xml") - { - Resource@ effect = cache.GetResource("ParticleEffect2D", browserFile.resourceKey); - if (effect is null) - return; - - ResourceRef effectRef; - effectRef.type = effect.type; - effectRef.name = effect.name; - - Component@ emitter = targetNode.CreateComponent("ParticleEmitter2D"); - emitter.SetAttribute("Particle Effect", Variant(effectRef)); - createdComponent = emitter; - } - } - } - else if (itemType == ITEM_COMPONENT) - { - Component@ targetComponent = editorScene.GetComponent(target.vars[COMPONENT_ID_VAR].GetU32()); - - if (targetComponent is null) - return; - - if (type == RESOURCE_TYPE_MATERIAL) - { - StaticModel@ model = cast(targetComponent); - if (model is null) - return; - - AssignMaterial(model, browserFile.resourceKey); - } - else if (type == RESOURCE_TYPE_MODEL) - { - StaticModel@ staticModel = cast(targetComponent); - if (staticModel is null) - return; - - AssignModel(staticModel, browserFile.resourceKey); - } - } - else - { - LineEdit@ text = cast(target); - if (text is null) - return; - text.text = browserFile.resourceKey; - VariantMap data(); - data["Element"] = text; - data["Text"] = text.text; - text.SendEvent("TextFinished", data); - } - - if (createdComponent !is null) - { - CreateLoadedComponent(createdComponent); - } - return; - } - - if (itemType == ITEM_NODE) - { - Node@ targetNode = editorScene.GetNode(target.vars[NODE_ID_VAR].GetU32()); - Array sourceNodes = GetMultipleSourceNodes(source); - - if (sourceNodes.length > 0) - { - if (input.qualifierDown[QUAL_CTRL] && sourceNodes.length == 1) - SceneReorder(sourceNodes[0], targetNode); - else - { - // If target is null, parent to scene - if (targetNode is null) - targetNode = editorScene; - - if (sourceNodes.length > 1) - SceneChangeParent(sourceNodes[0], sourceNodes, targetNode); - else - SceneChangeParent(sourceNodes[0], targetNode); - } - - // Focus the node at its new position in the list which in turn should trigger a refresh in attribute inspector - FocusNode(sourceNodes[0]); - } - } - else if (itemType == ITEM_UI_ELEMENT) - { - UIElement@ sourceElement = GetUIElementByID(source.vars[UI_ELEMENT_ID_VAR].GetU32()); - UIElement@ targetElement = GetUIElementByID(target.vars[UI_ELEMENT_ID_VAR].GetU32()); - - // If target is null, cannot proceed - if (targetElement is null) - return; - - if (input.qualifierDown[QUAL_CTRL]) - { - if (!UIElementReorder(sourceElement, targetElement)) - return; - } - else - { - if (!UIElementChangeParent(sourceElement, targetElement)) - return; - } - - // Focus the element at its new position in the list which in turn should trigger a refresh in attribute inspector - FocusUIElement(sourceElement); - } - else if (itemType == ITEM_COMPONENT) - { - Component@ targetComponent = editorScene.GetComponent(target.vars[COMPONENT_ID_VAR].GetU32()); - Array sourceNodes = GetMultipleSourceNodes(source); - - if (input.qualifierDown[QUAL_CTRL] && sourceNodes.length == 1) - { - // Reorder components within node - Component@ sourceComponent = editorScene.GetComponent(source.vars[COMPONENT_ID_VAR].GetU32()); - SceneReorder(sourceComponent, targetComponent); - } - else - { - if (targetComponent !is null && sourceNodes.length > 0) - { - // Drag node to StaticModelGroup to make it an instance - StaticModelGroup@ smg = cast(targetComponent); - if (smg !is null) - { - // Save undo action - EditAttributeAction action; - uint attrIndex = GetAttributeIndex(smg, "Instance Nodes"); - Variant oldIDs = smg.attributes[attrIndex]; - - for (uint i = 0; i < sourceNodes.length; ++i) - smg.AddInstanceNode(sourceNodes[i]); - - action.Define(smg, attrIndex, oldIDs); - SaveEditAction(action); - SetSceneModified(); - UpdateAttributeInspector(false); - } - - // Drag node to SplinePath to make it a control point - SplinePath@ spline = cast(targetComponent); - if (spline !is null) - { - // Save undo action - EditAttributeAction action; - uint attrIndex = GetAttributeIndex(spline, "Control Points"); - Variant oldIDs = spline.attributes[attrIndex]; - - for (uint i = 0; i < sourceNodes.length; ++i) - spline.AddControlPoint(sourceNodes[i]); - - action.Define(spline, attrIndex, oldIDs); - SaveEditAction(action); - SetSceneModified(); - UpdateAttributeInspector(false); - } - - // Drag a node to Constraint to make it the remote end of the constraint - Constraint@ constraint = cast(targetComponent); - RigidBody@ rigidBody = sourceNodes[0].GetComponent("RigidBody"); - if (constraint !is null && rigidBody !is null) - { - // Save undo action - EditAttributeAction action; - uint attrIndex = GetAttributeIndex(constraint, "Other Body NodeID"); - Variant oldID = constraint.attributes[attrIndex]; - - constraint.otherBody = rigidBody; - - action.Define(constraint, attrIndex, oldID); - SaveEditAction(action); - SetSceneModified(); - UpdateAttributeInspector(false); - } - } - } - } -} - -Array GetMultipleSourceNodes(UIElement@ source) -{ - Array nodeList; - - Node@ node = editorScene.GetNode(source.vars[NODE_ID_VAR].GetU32()); - if (node !is null) - nodeList.Push(node); - - // Handle additional selected children from a ListView - if (source.parent !is null && source.parent.typeName == "HierarchyContainer") - { - ListView@ listView_ = cast(source.parent.parent.parent); - if (listView_ is null) - return nodeList; - - bool sourceIsSelected = false; - for (uint i = 0; i < listView_.selectedItems.length; ++i) - { - if (listView_.selectedItems[i] is source) - { - sourceIsSelected = true; - break; - } - } - - if (sourceIsSelected) - { - for (uint i = 0; i < listView_.selectedItems.length; ++i) - { - UIElement@ item_ = listView_.selectedItems[i]; - // The source item is already added - if (item_ is source) - continue; - - if (item_.vars[TYPE_VAR] == ITEM_NODE) - { - Node@ n = editorScene.GetNode(item_.vars[NODE_ID_VAR].GetU32()); - if (n !is null) - nodeList.Push(n); - } - } - } - } - - return nodeList; -} - -bool TestDragDrop(UIElement@ source, UIElement@ target, int& itemType) -{ - int targetItemType = target.GetVar(TYPE_VAR).GetI32(); - - if (targetItemType == ITEM_NODE) - { - Node@ sourceNode; - Node@ targetNode; - Variant variant = source.GetVar(NODE_ID_VAR); - if (!variant.empty) - sourceNode = editorScene.GetNode(variant.GetU32()); - variant = target.GetVar(NODE_ID_VAR); - if (!variant.empty) - targetNode = editorScene.GetNode(variant.GetU32()); - Array sourceNodes = GetMultipleSourceNodes(source); - - if (sourceNode !is null && targetNode !is null) - { - itemType = ITEM_NODE; - - // Ctrl pressed: reorder - if (input.qualifierDown[QUAL_CTRL] && sourceNodes.length == 1) - { - // Must be within the same parent - if (sourceNode.parent is null || sourceNode.parent !is targetNode.parent) - return false; - } - // No ctrl: Reparent - else - { - if (sourceNode.parent is targetNode) - return false; - if (targetNode.parent is sourceNode) - return false; - } - } - - // Resource browser - if (sourceNode is null && targetNode !is null) - { - itemType = ITEM_NODE; - int type = source.GetVar(TEXT_VAR_RESOURCE_TYPE).GetI32(); - return type == RESOURCE_TYPE_PREFAB || - type == RESOURCE_TYPE_SCRIPTFILE || - type == RESOURCE_TYPE_MODEL || - type == RESOURCE_TYPE_PARTICLEEFFECT || - type == RESOURCE_TYPE_2D_PARTICLE_EFFECT; - } - - return true; - } - else if (targetItemType == ITEM_UI_ELEMENT) - { - UIElement@ sourceElement; - UIElement@ targetElement; - Variant variant = source.GetVar(UI_ELEMENT_ID_VAR); - if (!variant.empty) - sourceElement = GetUIElementByID(variant.GetU32()); - variant = target.GetVar(UI_ELEMENT_ID_VAR); - if (!variant.empty) - targetElement = GetUIElementByID(variant.GetU32()); - - if (sourceElement !is null && targetElement !is null) - { - itemType = ITEM_UI_ELEMENT; - - // Ctrl pressed: reorder - if (input.qualifierDown[QUAL_CTRL]) - { - // Must be within the same parent - if (sourceElement.parent is null || sourceElement.parent !is targetElement.parent) - return false; - } - // No ctrl: reparent - else - { - if (sourceElement.parent is targetElement) - return false; - if (targetElement.parent is sourceElement) - return false; - } - } - - return true; - } - else if (targetItemType == ITEM_COMPONENT) - { - Node@ sourceNode; - Component@ sourceComponent; - Component@ targetComponent; - Variant variant = source.GetVar(NODE_ID_VAR); - if (!variant.empty) - sourceNode = editorScene.GetNode(variant.GetU32()); - variant = target.GetVar(COMPONENT_ID_VAR); - if (!variant.empty) - targetComponent = editorScene.GetComponent(variant.GetU32()); - variant = source.GetVar(COMPONENT_ID_VAR); - if (!variant.empty) - sourceComponent = editorScene.GetComponent(variant.GetU32()); - Array sourceNodes = GetMultipleSourceNodes(source); - - itemType = ITEM_COMPONENT; - - if (input.qualifierDown[QUAL_CTRL] && sourceNodes.length == 1) - { - // Reorder components within node - if (sourceComponent !is null && targetComponent !is null && sourceComponent.node is targetComponent.node) - return true; - } - else - { - // Dragging of nodes to StaticModelGroup, SplinePath or Constraint - if (sourceNode !is null && targetComponent !is null && (targetComponent.type == STATICMODELGROUP_TYPE || - targetComponent.type == CONSTRAINT_TYPE || targetComponent.type == SPLINEPATH_TYPE)) - return true; - - // Resource browser - int type = source.GetVar(TEXT_VAR_RESOURCE_TYPE).GetI32(); - if (targetComponent.type == STATICMODEL_TYPE || targetComponent.type == ANIMATEDMODEL_TYPE) - return type == RESOURCE_TYPE_MATERIAL || type == RESOURCE_TYPE_MODEL; - } - - return false; - } - else if (source.vars.Contains(TEXT_VAR_RESOURCE_TYPE)) // only testing resource browser ui elements - { - int type = source.GetVar(TEXT_VAR_RESOURCE_TYPE).GetI32(); - - // Test against resource pickers - LineEdit@ lineEdit = cast(target); - if (lineEdit !is null) - { - StringHash resourceType = GetResourceTypeFromPickerLineEdit(lineEdit); - if (resourceType == StringHash("Material") && type == RESOURCE_TYPE_MATERIAL) - return true; - else if (resourceType == StringHash("Model") && type == RESOURCE_TYPE_MODEL) - return true; - else if (resourceType == StringHash("Animation") && type == RESOURCE_TYPE_ANIMATION) - return true; - } - } - return true; -} - -StringHash GetResourceTypeFromPickerLineEdit(UIElement@ lineEdit) -{ - Array@ targets = GetAttributeEditorTargets(lineEdit); - if (!targets.empty) - { - resourcePickIndex = lineEdit.vars["Index"].GetU32(); - resourcePickSubIndex = lineEdit.vars["SubIndex"].GetU32(); - AttributeInfo info = targets[0].attributeInfos[resourcePickIndex]; - StringHash resourceType; - if (info.type == VAR_RESOURCEREF) - return targets[0].attributes[resourcePickIndex].GetResourceRef().type; - else if (info.type == VAR_RESOURCEREFLIST) - return targets[0].attributes[resourcePickIndex].GetResourceRefList().type; - else if (info.type == VAR_VARIANTVECTOR) - return targets[0].attributes[resourcePickIndex].GetVariantVector()[resourcePickSubIndex].GetResourceRef().type; - } - return StringHash(); -} - -void FocusNode(Node@ node) -{ - uint index = GetListIndex(node); - hierarchyList.selection = index; -} - -void FocusComponent(Component@ component) -{ - uint index = GetComponentListIndex(component); - hierarchyList.selection = index; -} - -void FocusUIElement(UIElement@ element) -{ - uint index = GetListIndex(element); - hierarchyList.selection = index; -} - -void CreateBuiltinObject(const String& name) -{ - Node@ newNode = editorScene.CreateChild(name, REPLICATED); - // Set the new node a certain distance from the camera - newNode.position = GetNewNodePosition(); - - StaticModel@ object = newNode.CreateComponent("StaticModel"); - object.model = cache.GetResource("Model", "Models/" + name + ".mdl"); - - // Create an undo action for the create - CreateNodeAction action; - action.Define(newNode); - SaveEditAction(action); - SetSceneModified(); - - FocusNode(newNode); -} - -bool CheckHierarchyWindowFocus() -{ - // When we do edit operations based on key shortcuts, make sure the hierarchy list is focused - return ui.focusElement is hierarchyList || ui.focusElement is null; -} - -bool CheckForExistingGlobalComponent(Node@ node, const String&in typeName) -{ - if (typeName != "Octree" && typeName != "PhysicsWorld" && typeName != "DebugRenderer") - return false; - else - return node.HasComponent(typeName); -} - -void HandleNodeAdded(StringHash eventType, VariantMap& eventData) -{ - if (suppressSceneChanges) - return; - - Node@ node = eventData["Node"].GetPtr(); - if (showTemporaryObject || !node.temporary) - UpdateHierarchyItem(node); -} - -void HandleNodeRemoved(StringHash eventType, VariantMap& eventData) -{ - if (suppressSceneChanges) - return; - - Node@ node = eventData["Node"].GetPtr(); - if (showTemporaryObject || !node.temporary) - UpdateHierarchyItem(GetListIndex(node), null, null); -} - -void HandleComponentAdded(StringHash eventType, VariantMap& eventData) -{ - if (suppressSceneChanges) - return; - - // Insert the newly added component at last component position but before the first child node position of the parent node - Node@ node = eventData["Node"].GetPtr(); - Component@ component = eventData["Component"].GetPtr(); - if (showTemporaryObject || (!node.temporary && !component.temporary)) - { - uint nodeIndex = GetListIndex(node); - if (nodeIndex != NO_ITEM) - { - uint index = node.numChildren > 0 ? GetListIndex(node.children[0]) : M_MAX_UNSIGNED; - UpdateHierarchyItem(index, component, hierarchyList.items[nodeIndex]); - } - } -} - -void HandleComponentRemoved(StringHash eventType, VariantMap& eventData) -{ - if (suppressSceneChanges) - return; - - Node@ node = eventData["Node"].GetPtr(); - Component@ component = eventData["Component"].GetPtr(); - if (showTemporaryObject || (!node.temporary && !component.temporary)) - { - uint index = GetComponentListIndex(component); - if (index != NO_ITEM) - hierarchyList.RemoveItem(index); - } -} - -void HandleNodeNameChanged(StringHash eventType, VariantMap& eventData) -{ - if (suppressSceneChanges) - return; - - Node@ node = eventData["Node"].GetPtr(); - if (showTemporaryObject || !node.temporary) - UpdateHierarchyItemText(GetListIndex(node), node.enabled, GetNodeTitle(node)); -} - -void HandleNodeEnabledChanged(StringHash eventType, VariantMap& eventData) -{ - if (suppressSceneChanges) - return; - - Node@ node = eventData["Node"].GetPtr(); - if (showTemporaryObject || !node.temporary) - { - UpdateHierarchyItemText(GetListIndex(node), node.enabled); - attributesDirty = true; - } -} - -void HandleComponentEnabledChanged(StringHash eventType, VariantMap& eventData) -{ - if (suppressSceneChanges) - return; - - Node@ node = eventData["Node"].GetPtr(); - Component@ component = eventData["Component"].GetPtr(); - if (showTemporaryObject || (!node.temporary && !component.temporary)) - { - UpdateHierarchyItemText(GetComponentListIndex(component), component.enabledEffective); - attributesDirty = true; - } -} - -void HandleUIElementAdded(StringHash eventType, VariantMap& eventData) -{ - if (suppressUIElementChanges) - return; - - UIElement@ element = eventData["Element"].GetPtr(); - if ((showInternalUIElement || !element.internal) && (showTemporaryObject || !element.temporary)) - UpdateHierarchyItem(element); -} - -void HandleUIElementRemoved(StringHash eventType, VariantMap& eventData) -{ - if (suppressUIElementChanges) - return; - - UIElement@ element = eventData["Element"].GetPtr(); - UpdateHierarchyItem(GetListIndex(element), null, null); -} - -void HandleElementNameChanged(StringHash eventType, VariantMap& eventData) -{ - if (suppressUIElementChanges) - return; - - UIElement@ element = eventData["Element"].GetPtr(); - UpdateHierarchyItemText(GetListIndex(element), element.visible, GetUIElementTitle(element)); -} - -void HandleElementVisibilityChanged(StringHash eventType, VariantMap& eventData) -{ - if (suppressUIElementChanges) - return; - - UIElement@ element = eventData["Element"].GetPtr(); - UpdateHierarchyItemText(GetListIndex(element), element.visible); -} - -void HandleElementAttributeChanged(StringHash eventType, VariantMap& eventData) -{ - // Do not refresh the attribute inspector while the attribute is being edited via the attribute-editors - if (suppressUIElementChanges || inEditAttribute) - return; - - UIElement@ element = eventData["Element"].GetPtr(); - for (uint i = 0; i < editUIElements.length; ++i) - { - if (editUIElements[i] is element) - attributesDirty = true; - } -} - -void HandleTemporaryChanged(StringHash eventType, VariantMap& eventData) -{ - if (suppressSceneChanges || suppressUIElementChanges) - return; - - Serializable@ serializable = cast(GetEventSender()); - - Node@ node = cast(serializable); - if (node !is null && node.scene is editorScene) - { - if (showTemporaryObject) - UpdateHierarchyItemText(GetListIndex(node), node.enabled); - else if (!node.temporary && GetListIndex(node) == NO_ITEM) - UpdateHierarchyItem(node); - else if (node.temporary) - UpdateHierarchyItem(GetListIndex(node), null, null); - - return; - } - - Component@ component = cast(serializable); - if (component !is null && component.node !is null && component.node.scene is editorScene) - { - node = component.node; - if (showTemporaryObject) - UpdateHierarchyItemText(GetComponentListIndex(component), node.enabled); - else if (!component.temporary && GetComponentListIndex(component) == NO_ITEM) - { - uint nodeIndex = GetListIndex(node); - if (nodeIndex != NO_ITEM) - { - uint index = node.numChildren > 0 ? GetListIndex(node.children[0]) : M_MAX_UNSIGNED; - UpdateHierarchyItem(index, component, hierarchyList.items[nodeIndex]); - } - } - else if (component.temporary) - { - uint index = GetComponentListIndex(component); - if (index != NO_ITEM) - hierarchyList.RemoveItem(index); - } - - return; - } - - UIElement@ element = cast(serializable); - if (element !is null) - { - if (showTemporaryObject) - UpdateHierarchyItemText(GetListIndex(element), element.visible); - else if (!element.temporary && GetListIndex(element) == NO_ITEM) - UpdateHierarchyItem(element); - else if (element.temporary) - UpdateHierarchyItem(GetListIndex(element), null, null); - - return; - } -} - -// Hierarchy window edit functions -bool Undo() -{ - if (undoStackPos > 0) - { - --undoStackPos; - // Undo commands in reverse order - for (int i = int(undoStack[undoStackPos].actions.length - 1); i >= 0; --i) - undoStack[undoStackPos].actions[i].Undo(); - } - - return true; -} - -bool Redo() -{ - if (undoStackPos < undoStack.length) - { - // Redo commands in same order as stored - for (uint i = 0; i < undoStack[undoStackPos].actions.length; ++i) - undoStack[undoStackPos].actions[i].Redo(); - ++undoStackPos; - } - - return true; -} - -bool Cut() -{ - if (CheckHierarchyWindowFocus()) - { - bool ret = true; - if (!selectedNodes.empty || !selectedComponents.empty) - ret = ret && SceneCut(); - // Not mutually exclusive - if (!selectedUIElements.empty) - ret = ret && UIElementCut(); - return ret; - } - - return false; -} - -bool Duplicate() -{ - if (CheckHierarchyWindowFocus()) - { - bool ret = true; - if (!selectedNodes.empty || !selectedComponents.empty) - ret = ret && (selectedNodes.empty || selectedComponents.empty ? SceneDuplicate() : false); // Node and component is mutually exclusive for copy action - // Not mutually exclusive - if (!selectedUIElements.empty) - ret = ret && UIElementDuplicate(); - return ret; - } - - return false; -} - -bool Copy() -{ - if (CheckHierarchyWindowFocus()) - { - bool ret = true; - if (!selectedNodes.empty || !selectedComponents.empty) - ret = ret && (selectedNodes.empty || selectedComponents.empty ? SceneCopy() : false); // Node and component is mutually exclusive for copy action - // Not mutually exclusive - if (!selectedUIElements.empty) - ret = ret && UIElementCopy(); - return ret; - } - - return false; -} - -bool Paste() -{ - if (CheckHierarchyWindowFocus()) - { - bool ret = true; - if (editNode !is null && !sceneCopyBuffer.empty) - ret = ret && ScenePaste(); - // Not mutually exclusive - if (editUIElement !is null && !uiElementCopyBuffer.empty) - ret = ret && UIElementPaste(); - return ret; - } - - return false; -} - -bool BlenderModeDelete() -{ - if (ui.focusElement is null) - { - Array actions; - actions.Push(CreateContextMenuItem("Delete?", "HandleBlenderModeDelete")); - actions.Push(CreateContextMenuItem("Cancel", "HandleEmpty")); - - if (actions.length > 0) - { - ActivateContextMenu(actions); - return true; - } - } - return false; -} - -bool Delete() -{ - if (CheckHierarchyWindowFocus()) - { - bool ret = true; - if (!selectedNodes.empty || !selectedComponents.empty) - ret = ret && SceneDelete(); - // Not mutually exclusive - if (!selectedUIElements.empty) - ret = ret && UIElementDelete(); - return ret; - } - - return false; -} - -bool SelectAll() -{ - if (CheckHierarchyWindowFocus()) - { - if (!selectedNodes.empty || !selectedComponents.empty) - return SceneSelectAll(); - else if (!selectedUIElements.empty || hierarchyList.items[GetListIndex(editorUIElement)].selected) - return UIElementSelectAll(); - else - return SceneSelectAll(); // If nothing is selected yet, fall back to scene select all - } - - return false; -} - -bool DeselectAll() -{ - if (CheckHierarchyWindowFocus()) - { - BeginSelectionModify(); - hierarchyList.ClearSelection(); - EndSelectionModify(); - return true; - } - return false; -} - -bool ResetToDefault() -{ - if (CheckHierarchyWindowFocus()) - { - bool ret = true; - if (!selectedNodes.empty || !selectedComponents.empty) - ret = ret && (selectedNodes.empty || selectedComponents.empty ? SceneResetToDefault() : false); // Node and component is mutually exclusive for reset-to-default action - // Not mutually exclusive - if (!selectedUIElements.empty) - ret = ret && UIElementResetToDefault(); - return ret; - } - - return false; -} - -void ClearEditActions() -{ - undoStack.Clear(); - undoStackPos = 0; -} - -void SaveEditAction(EditAction@ action) -{ - // Create a group with 1 action - EditActionGroup group; - group.actions.Push(action); - SaveEditActionGroup(group); -} - -void SaveEditActionGroup(EditActionGroup@ group) -{ - if (group.actions.empty) - return; - - // Truncate the stack first to current pos - undoStack.Resize(undoStackPos); - undoStack.Push(group); - ++undoStackPos; - - // Limit maximum undo steps - if (undoStack.length > MAX_UNDOSTACK_SIZE) - { - undoStack.Erase(0); - --undoStackPos; - } -} - -void BeginSelectionModify() -{ - // A large operation on selected nodes is about to begin. Disable intermediate selection updates - inSelectionModify = true; - - // Cursor shape reverts back to normal automatically after the large operation is completed - ui.cursor.shape = CS_BUSY; -} - -void EndSelectionModify() -{ - // The large operation on selected nodes has ended. Update node/component selection now - inSelectionModify = false; - HandleHierarchyListSelectionChange(); -} - -void HandleHierarchyContextCreateReplicatedNode() -{ - CreateNode(REPLICATED); -} - -void HandleHierarchyContextCreateLocalNode() -{ - CreateNode(LOCAL); -} - -void HandleHierarchyContextDuplicate() -{ - Duplicate(); -} - -void HandleHierarchyContextCopy() -{ - Copy(); -} - -void HandleHierarchyContextCut() -{ - Cut(); -} - -void HandleHierarchyContextDelete() -{ - Delete(); -} - -void HandleBlenderModeDelete() -{ - Delete(); -} - -void HandleEmpty() -{ - //just doing nothing -} - -void HandleHierarchyContextPaste() -{ - Paste(); -} - -void HandleHierarchyContextResetToDefault() -{ - ResetToDefault(); -} - -void HandleHierarchyContextResetPosition() -{ - SceneResetPosition(); -} - -void HandleHierarchyContextResetRotation() -{ - SceneResetRotation(); -} - -void HandleHierarchyContextResetScale() -{ - SceneResetScale(); -} - -void HandleHierarchyContextEnableDisable() -{ - SceneToggleEnable(); -} - -void HandleHierarchyContextUnparent() -{ - SceneUnparent(); -} - -void HandleHierarchyContextUIElementCloseUILayout() -{ - CloseUILayout(); -} - -void HandleHierarchyContextUIElementCloseAllUILayouts() -{ - CloseAllUILayouts(); -} - -void CollapseHierarchy() -{ - Array oldSelections = hierarchyList.selections; - Array selections = {0}; - - hierarchyList.SetSelections(selections); - - for (uint i = 0; i < selections.length; ++i) - hierarchyList.Expand(selections[i], false, true); - - // only scene's scope expand by default - hierarchyList.Expand(0, true, false); - - hierarchyList.SetSelections(oldSelections); -} - -void CollapseHierarchy(StringHash eventType, VariantMap& eventData) -{ - CollapseHierarchy(); -} - -void HandleShowID(StringHash eventType, VariantMap& eventData) -{ - CheckBox@ checkBox = eventData["Element"].GetPtr(); - showID = checkBox.checked; - UpdateHierarchyItem(editorScene, false); - CollapseHierarchy(); -} +// Urho3D editor hierarchy window handling + +const int ITEM_NONE = 0; +const int ITEM_NODE = 1; +const int ITEM_COMPONENT = 2; +const int ITEM_UI_ELEMENT = 3; +const uint NO_ITEM = M_MAX_UNSIGNED; +const StringHash SCENE_TYPE("Scene"); +const StringHash NODE_TYPE("Node"); +const StringHash STATICMODEL_TYPE("StaticModel"); +const StringHash ANIMATEDMODEL_TYPE("AnimatedModel"); +const StringHash STATICMODELGROUP_TYPE("StaticModelGroup"); +const StringHash SPLINEPATH_TYPE("SplinePath"); +const StringHash CONSTRAINT_TYPE("Constraint"); +const String NO_CHANGE(0); +const StringHash TYPE_VAR("Type"); +const StringHash NODE_ID_VAR("NodeID"); +const StringHash COMPONENT_ID_VAR("ComponentID"); +const StringHash UI_ELEMENT_ID_VAR("UIElementID"); +const StringHash DRAGDROPCONTENT_VAR("DragDropContent"); +const StringHash[] ID_VARS = { StringHash(""), NODE_ID_VAR, COMPONENT_ID_VAR, UI_ELEMENT_ID_VAR }; +Color nodeTextColor(1.0f, 1.0f, 1.0f); +Color componentTextColor(0.7f, 1.0f, 0.7f); + +Window@ hierarchyWindow; +ListView@ hierarchyList; +bool showID = true; + +// UIElement does not have unique ID, so use a running number to generate a new ID each time an item is inserted into hierarchy list +const uint UI_ELEMENT_BASE_ID = 1; +uint uiElementNextID = UI_ELEMENT_BASE_ID; +bool showInternalUIElement = false; +bool showTemporaryObject = false; +Array hierarchyUpdateSelections; + +Variant GetUIElementID(UIElement@ element) +{ + Variant elementID = element.GetVar(UI_ELEMENT_ID_VAR); + if (elementID.empty) + { + // Generate new ID + elementID = uiElementNextID++; + // Store the generated ID + element.vars[UI_ELEMENT_ID_VAR] = elementID; + } + + return elementID; +} + +UIElement@ GetUIElementByID(const Variant&in id) +{ + return id == UI_ELEMENT_BASE_ID ? editorUIElement : editorUIElement.GetChild(UI_ELEMENT_ID_VAR, id, true); +} + +void CreateHierarchyWindow() +{ + if (hierarchyWindow !is null) + return; + + hierarchyWindow = LoadEditorUI("Editor/UI/EditorHierarchyWindow.xml"); + hierarchyList = hierarchyWindow.GetChild("HierarchyList"); + ui.root.AddChild(hierarchyWindow); + int height = Min(ui.root.height - 60, 500); + hierarchyWindow.SetSize(300, height); + hierarchyWindow.SetPosition(35, 100); + hierarchyWindow.opacity = uiMaxOpacity; + hierarchyWindow.BringToFront(); + + UpdateHierarchyItem(editorScene); + + // Set selection to happen on click end, so that we can drag nodes to the inspector without resetting the inspector view + hierarchyList.selectOnClickEnd = true; + + // Set drag & drop target mode on the node list background, which is used to parent nodes back to the root node + hierarchyList.contentElement.dragDropMode = DD_TARGET; + hierarchyList.scrollPanel.dragDropMode = DD_TARGET; + + SubscribeToEvent(hierarchyWindow.GetChild("CloseButton", true), "Released", "HideHierarchyWindow"); + SubscribeToEvent(hierarchyWindow.GetChild("ExpandButton", true), "Released", "ExpandCollapseHierarchy"); + SubscribeToEvent(hierarchyWindow.GetChild("CollapseButton", true), "Released", "ExpandCollapseHierarchy"); + SubscribeToEvent(hierarchyWindow.GetChild("ResetButton", true), "Released", "CollapseHierarchy"); + SubscribeToEvent(hierarchyWindow.GetChild("ShowID", true), "Toggled", "HandleShowID"); + SubscribeToEvent(hierarchyList, "SelectionChanged", "HandleHierarchyListSelectionChange"); + SubscribeToEvent(hierarchyList, "ItemDoubleClicked", "HandleHierarchyListDoubleClick"); + SubscribeToEvent(hierarchyList, "ItemClicked", "HandleHierarchyItemClick"); + SubscribeToEvent("DragDropTest", "HandleDragDropTest"); + SubscribeToEvent("DragDropFinish", "HandleDragDropFinish"); + SubscribeToEvent(editorScene, "ComponentAdded", "HandleComponentAdded"); + SubscribeToEvent(editorScene, "ComponentRemoved", "HandleComponentRemoved"); + SubscribeToEvent(editorScene, "NodeEnabledChanged", "HandleNodeEnabledChanged"); + SubscribeToEvent(editorScene, "ComponentEnabledChanged", "HandleComponentEnabledChanged"); + SubscribeToEvent("TemporaryChanged", "HandleTemporaryChanged"); +} + +bool ToggleHierarchyWindow() +{ + if (hierarchyWindow.visible == false) + ShowHierarchyWindow(); + else + HideHierarchyWindow(); + return true; +} + +void ShowHierarchyWindow() +{ + hierarchyWindow.visible = true; + hierarchyWindow.BringToFront(); +} + +void HideHierarchyWindow() +{ + if(viewportMode == VIEWPORT_COMPACT) + return; + hierarchyWindow.visible = false; +} + +void ExpandCollapseHierarchy(StringHash eventType, VariantMap& eventData) +{ + Button@ button = eventData["Element"].GetPtr(); + bool enable = button.name == "ExpandButton"; + CheckBox@ checkBox = hierarchyWindow.GetChild("AllCheckBox", true); + bool all = checkBox.checked; + checkBox.checked = false; // Auto-reset + + Array selections = hierarchyList.selections; + for (uint i = 0; i < selections.length; ++i) + hierarchyList.Expand(selections[i], enable, all); +} + +void EnableExpandCollapseButtons(bool enable) +{ + String[] buttons = { "ExpandButton", "CollapseButton", "AllCheckBox" }; + for (uint i = 0; i < buttons.length; ++i) + { + UIElement@ element = hierarchyWindow.GetChild(buttons[i], true); + element.enabled = enable; + element.children[0].color = enable ? normalTextColor : nonEditableTextColor; + } +} + +void UpdateHierarchyItem(Serializable@ serializable, bool clear = false) +{ + if (clear) + { + // Remove the current selection before updating the list item (in turn trigger an update on the attribute editor) + hierarchyList.ClearSelection(); + + // Clear copybuffer when whole window refreshed + sceneCopyBuffer.Clear(); + uiElementCopyBuffer.Clear(); + } + + // In case of item's parent is not found in the hierarchy list then the item will be inserted at the list root level + Serializable@ parent; + switch (GetType(serializable)) + { + case ITEM_NODE: + parent = cast(serializable).parent; + break; + + case ITEM_COMPONENT: + parent = cast(serializable).node; + break; + + case ITEM_UI_ELEMENT: + parent = cast(serializable).parent; + break; + + default: + break; + } + UIElement@ parentItem = hierarchyList.items[GetListIndex(parent)]; + UpdateHierarchyItem(GetListIndex(serializable), serializable, parentItem); +} + +uint UpdateHierarchyItem(uint itemIndex, Serializable@ serializable, UIElement@ parentItem) +{ + // Whenever we're updating, disable layout update to optimize speed + hierarchyList.contentElement.DisableLayoutUpdate(); + + if (serializable is null) + { + hierarchyList.RemoveItem(itemIndex); + hierarchyList.contentElement.EnableLayoutUpdate(); + hierarchyList.contentElement.UpdateLayout(); + return itemIndex; + } + + int itemType = GetType(serializable); + Variant id = GetID(serializable, itemType); + + // Remove old item if exists + if (itemIndex < hierarchyList.numItems && itemIndex != NINDEX && MatchID(hierarchyList.items[itemIndex], id, itemType)) + hierarchyList.RemoveItem(itemIndex); + + Text@ text = Text(); + hierarchyList.InsertItem(itemIndex, text, parentItem); + text.style = "FileSelectorListText"; + + if (serializable.type == SCENE_TYPE || serializable is editorUIElement) + // The root node (scene) and editor's root UIElement cannot be moved by drag and drop + text.dragDropMode = DD_TARGET; + else + // Internal UIElement is not able to participate in drag and drop action + text.dragDropMode = itemType == ITEM_UI_ELEMENT && cast(serializable).internal ? DD_DISABLED : DD_SOURCE_AND_TARGET; + + // Advance the index for the child items + if (itemIndex == M_MAX_UNSIGNED) + itemIndex = hierarchyList.numItems; + else + ++itemIndex; + + String iconType = serializable.typeName; + if (serializable is editorUIElement) + iconType = "Root" + iconType; + IconizeUIElement(text, iconType); + + SetID(text, serializable, itemType); + switch (itemType) + { + case ITEM_NODE: + { + Node@ node = cast(serializable); + + text.text = GetNodeTitle(node); + text.color = nodeTextColor; + SetIconEnabledColor(text, node.enabled); + + // Update components first + for (uint i = 0; i < node.numComponents; ++i) + { + Component@ component = node.components[i]; + if (showTemporaryObject || !component.temporary) + AddComponentItem(itemIndex++, component, text); + } + + // Then update child nodes recursively + for (uint i = 0; i < node.numChildren; ++i) + { + Node@ childNode = node.children[i]; + if (showTemporaryObject || !childNode.temporary) + itemIndex = UpdateHierarchyItem(itemIndex, childNode, text); + } + + break; + } + + case ITEM_COMPONENT: + { + Component@ component = cast(serializable); + text.text = GetComponentTitle(component); + text.color = componentTextColor; + SetIconEnabledColor(text, component.enabledEffective); + break; + } + + case ITEM_UI_ELEMENT: + { + UIElement@ element = cast(serializable); + + text.text = GetUIElementTitle(element); + SetIconEnabledColor(text, element.visible); + + // Update child elements recursively + for (uint i = 0; i < element.numChildren; ++i) + { + UIElement@ childElement = element.children[i]; + if ((showInternalUIElement || !childElement.internal) && (showTemporaryObject || !childElement.temporary)) + itemIndex = UpdateHierarchyItem(itemIndex, childElement, text); + } + + break; + } + + default: + break; + } + + // Re-enable layout update (and do manual layout) now + hierarchyList.contentElement.EnableLayoutUpdate(); + hierarchyList.contentElement.UpdateLayout(); + + return itemIndex; +} + +void UpdateHierarchyItemText(uint itemIndex, bool iconEnabled, const String&in textTitle = NO_CHANGE) +{ + Text@ text = hierarchyList.items[itemIndex]; + if (text is null) + return; + + SetIconEnabledColor(text, iconEnabled); + + if (textTitle != NO_CHANGE) + text.text = textTitle; +} + +void AddComponentItem(uint compItemIndex, Component@ component, UIElement@ parentItem) +{ + Text@ text = Text(); + hierarchyList.InsertItem(compItemIndex, text, parentItem); + text.style = "FileSelectorListText"; + text.vars[TYPE_VAR] = ITEM_COMPONENT; + text.vars[NODE_ID_VAR] = component.node.id; + text.vars[COMPONENT_ID_VAR] = component.id; + text.text = GetComponentTitle(component); + text.color = componentTextColor; + text.dragDropMode = DD_SOURCE_AND_TARGET; + + IconizeUIElement(text, component.typeName); + SetIconEnabledColor(text, component.enabledEffective); +} + +int GetType(Serializable@ serializable) +{ + if (cast(serializable) !is null) + return ITEM_NODE; + else if (cast(serializable) !is null) + return ITEM_COMPONENT; + else if (cast(serializable) !is null) + return ITEM_UI_ELEMENT; + else + return ITEM_NONE; +} + +void SetID(Text@ text, Serializable@ serializable, int itemType = ITEM_NONE) +{ + // If item type is not provided, auto detect it + if (itemType == ITEM_NONE) + itemType = GetType(serializable); + + text.vars[TYPE_VAR] = itemType; + text.vars[ID_VARS[itemType]] = GetID(serializable, itemType); + + // Set node ID as drag and drop content for node ID editing + if (itemType == ITEM_NODE) + text.vars[DRAGDROPCONTENT_VAR] = String(text.vars[NODE_ID_VAR].GetU32()); + + switch (itemType) + { + case ITEM_COMPONENT: + text.vars[NODE_ID_VAR] = cast(serializable).node.id; + break; + + case ITEM_UI_ELEMENT: + // Subscribe to UI-element events + SubscribeToEvent(serializable, "NameChanged", "HandleElementNameChanged"); + SubscribeToEvent(serializable, "VisibleChanged", "HandleElementVisibilityChanged"); + SubscribeToEvent(serializable, "Resized", "HandleElementAttributeChanged"); + SubscribeToEvent(serializable, "Positioned", "HandleElementAttributeChanged"); + break; + + default: + break; + } +} + +uint GetID(Serializable@ serializable, int itemType = ITEM_NONE) +{ + // If item type is not provided, auto detect it + if (itemType == ITEM_NONE) + itemType = GetType(serializable); + + switch (itemType) + { + case ITEM_NODE: + return cast(serializable).id; + + case ITEM_COMPONENT: + return cast(serializable).id; + + case ITEM_UI_ELEMENT: + return GetUIElementID(cast(serializable)).GetU32(); + } + + return M_MAX_UNSIGNED; +} + +bool MatchID(UIElement@ element, const Variant&in id, int itemType) +{ + return element.GetVar(TYPE_VAR).GetI32() == itemType && element.GetVar(ID_VARS[itemType]) == id; +} + +uint GetListIndex(Serializable@ serializable) +{ + if (serializable is null) + return NO_ITEM; + + int itemType = GetType(serializable); + Variant id = GetID(serializable, itemType); + + uint numItems = hierarchyList.numItems; + for (uint i = 0; i < numItems; ++i) + { + if (MatchID(hierarchyList.items[i], id, itemType)) + return i; + } + + return NO_ITEM; +} + +UIElement@ GetListUIElement(uint index) +{ + UIElement@ item = hierarchyList.items[index]; + if (item is null) + return null; + + // Get the text item's ID and use it to retrieve the actual UIElement the text item is associated to + return GetUIElementByID(GetUIElementID(item)); +} + +Node@ GetListNode(uint index) +{ + UIElement@ item = hierarchyList.items[index]; + if (item is null) + return null; + + return editorScene.GetNode(item.vars[NODE_ID_VAR].GetU32()); +} + +Component@ GetListComponent(uint index) +{ + UIElement@ item = hierarchyList.items[index]; + return GetListComponent(item); +} + +Component@ GetListComponent(UIElement@ item) +{ + if (item is null) + return null; + + if (item.vars[TYPE_VAR].GetI32() != ITEM_COMPONENT) + return null; + + return editorScene.GetComponent(item.vars[COMPONENT_ID_VAR].GetU32()); +} + +uint GetComponentListIndex(Component@ component) +{ + if (component is null) + return NO_ITEM; + + uint numItems = hierarchyList.numItems; + for (uint i = 0; i < numItems; ++i) + { + UIElement@ item = hierarchyList.items[i]; + if (item.vars[TYPE_VAR].GetI32() == ITEM_COMPONENT && item.vars[COMPONENT_ID_VAR].GetU32() == component.id) + return i; + } + + return NO_ITEM; +} + +String GetUIElementTitle(UIElement@ element) +{ + String ret; + + // Only top level UI-element has this variable + String modifiedStr = element.GetVar(MODIFIED_VAR).GetBool() ? "*" : ""; + ret = (element.name.empty ? element.typeName : element.name) + modifiedStr + " [" + GetUIElementID(element).ToString() + "]"; + + if (element.temporary) + ret += " (Temp)"; + + return ret; +} + +String GetNodeTitle(Node@ node) +{ + String ret; + + if (node.name.empty) + ret = node.typeName; + else + ret = node.name; + + if (showID) + { + if (node.replicated) + ret += " (" + String(node.id) + ")"; + else + ret += " (Local " + String(node.id) + ")"; + + if (node.temporary) + ret += " (Temp)"; + } + + return ret; +} + +String GetComponentTitle(Component@ component) +{ + String ret = component.typeName; + + if (showID) + { + if (!component.replicated) + ret += " (Local)"; + + if (component.temporary) + ret += " (Temp)"; + } + return ret; +} + +void SelectNode(Node@ node, bool multiselect) +{ + if (node is null && !multiselect) + { + hierarchyList.ClearSelection(); + return; + } + + lastSelectedNode = node; + uint index = GetListIndex(node); + uint numItems = hierarchyList.numItems; + + if (index < numItems) + { + // Expand the node chain now + if (!multiselect || !hierarchyList.IsSelected(index)) + { + // Go in the parent chain up to make sure the chain is expanded + Node@ current = node; + do + { + hierarchyList.Expand(GetListIndex(current), true); + current = current.parent; + } + while (current !is null); + } + + // This causes an event to be sent, in response we set the node/component selections, and refresh editors + if (!multiselect) + hierarchyList.selection = index; + else + hierarchyList.ToggleSelection(index); + } + else if (!multiselect) + hierarchyList.ClearSelection(); +} + +void DeselectNode(Node@ node) +{ + if (node is null) + { + hierarchyList.ClearSelection(); + return; + } + + uint index = GetListIndex(node); + uint numItems = hierarchyList.numItems; + + if (index < numItems) + { + hierarchyList.ToggleSelection(index); + } +} + +void SelectComponent(Component@ component, bool multiselect) +{ + if (component is null && !multiselect) + { + hierarchyList.ClearSelection(); + return; + } + + Node@ node = component.node; + if (node is null && !multiselect) + { + hierarchyList.ClearSelection(); + return; + } + + uint nodeIndex = GetListIndex(node); + uint componentIndex = GetComponentListIndex(component); + uint numItems = hierarchyList.numItems; + + if (nodeIndex < numItems && componentIndex < numItems) + { + // Expand the node chain now + if (!multiselect || !hierarchyList.IsSelected(componentIndex)) + { + // Go in the parent chain up to make sure the chain is expanded + Node@ current = node; + do + { + hierarchyList.Expand(GetListIndex(current), true); + current = current.parent; + } + while (current !is null); + } + + // This causes an event to be sent, in response we set the node/component selections, and refresh editors + if (!multiselect) + hierarchyList.selection = componentIndex; + else + hierarchyList.ToggleSelection(componentIndex); + } + else if (!multiselect) + hierarchyList.ClearSelection(); +} + +void SelectUIElement(UIElement@ element, bool multiselect) +{ + uint index = GetListIndex(element); + uint numItems = hierarchyList.numItems; + + if (index < numItems) + { + // Expand the node chain now + if (!multiselect || !hierarchyList.IsSelected(index)) + { + // Go in the parent chain up to make sure the chain is expanded + UIElement@ current = element; + do + { + hierarchyList.Expand(GetListIndex(current), true); + current = current.parent; + } + while (current !is null); + } + + if (!multiselect) + hierarchyList.selection = index; + else + hierarchyList.ToggleSelection(index); + } + else if (!multiselect) + hierarchyList.ClearSelection(); +} + +// Find the first selected StaticModel/AtimatedModel component or node with it +Model@ FindFirstSelectedModel() +{ + for (uint i = 0; i < selectedComponents.length; ++i) + { + // Get SM, but also works well for AnimatedModel + StaticModel@ sm = cast(selectedComponents[i]); + if (sm !is null && sm.model !is null) + return sm.model; + } + + for (uint i = 0; i < selectedNodes.length; ++i) + { + for (uint j = 0; j < selectedNodes[i].numComponents; ++j) + { + StaticModel@ sm = cast(selectedNodes[i].components[j]); + if (sm !is null && sm.model !is null) + return sm.model; + } + } + + return null; +} + +void UpdateModelInfo(Model@ model) +{ + if (model is null) + { + modelInfoText.text = ""; + return; + } + + String infoStr = "Model: " + model.name; + + infoStr += "\n Morphs: " + model.numMorphs; + + for (uint g = 0; g < model.numGeometries; ++g) + { + uint numLods = model.numGeometryLodLevels[g]; + infoStr += "\n Geometry " + g + "\n Lods: " + numLods; + for (uint l = 0; l < numLods; l++) + { + Geometry@ geom = model.GetGeometry(g, l); + infoStr += "\n Vertex Count: " + geom.vertexCount; + infoStr += "\n Index Count: " + geom.indexCount; + } + } + + modelInfoText.text = infoStr; +} + +void HandleHierarchyListSelectionChange() +{ + if (inSelectionModify) + return; + + ClearSceneSelection(); + ClearUIElementSelection(); + + Array indices = hierarchyList.selections; + + // Enable Expand/Collapse button when there is selection + EnableExpandCollapseButtons(indices.length > 0); + + for (uint i = 0; i < indices.length; ++i) + { + uint index = indices[i]; + UIElement@ item = hierarchyList.items[index]; + int type = item.vars[TYPE_VAR].GetI32(); + if (type == ITEM_COMPONENT) + { + Component@ comp = GetListComponent(index); + if (comp !is null) + selectedComponents.Push(comp); + } + else if (type == ITEM_NODE) + { + Node@ node = GetListNode(index); + if (node !is null) + selectedNodes.Push(node); + } + else if (type == ITEM_UI_ELEMENT) + { + UIElement@ element = GetListUIElement(index); + if (element !is null && element !is editorUIElement) + selectedUIElements.Push(element); + } + } + + // If only one node/UIElement selected, use it for editing + if (selectedNodes.length == 1) + editNode = selectedNodes[0]; + if (selectedUIElements.length == 1) + editUIElement = selectedUIElements[0]; + + // If selection contains only components, and they have a common node, use it for editing + if (selectedNodes.empty && !selectedComponents.empty) + { + Node@ commonNode; + for (uint i = 0; i < selectedComponents.length; ++i) + { + if (i == 0) + commonNode = selectedComponents[i].node; + else + { + if (selectedComponents[i].node !is commonNode) + commonNode = null; + } + } + editNode = commonNode; + } + + UpdateModelInfo(FindFirstSelectedModel()); + + // Now check if the component(s) can be edited. If many selected, must have same type or have same edit node + if (!selectedComponents.empty) + { + if (editNode is null) + { + StringHash compType = selectedComponents[0].type; + bool sameType = true; + for (uint i = 1; i < selectedComponents.length; ++i) + { + if (selectedComponents[i].type != compType) + { + sameType = false; + break; + } + } + if (sameType) + editComponents = selectedComponents; + } + else + { + editComponents = selectedComponents; + numEditableComponentsPerNode = selectedComponents.length; + } + } + + // If just nodes selected, and no components, show as many matching components for editing as possible + if (!selectedNodes.empty && selectedComponents.empty && selectedNodes[0].numComponents > 0) + { + uint count = 0; + for (uint j = 0; j < selectedNodes[0].numComponents; ++j) + { + StringHash compType = selectedNodes[0].components[j].type; + bool sameType = true; + for (uint i = 1; i < selectedNodes.length; ++i) + { + if (selectedNodes[i].numComponents <= j || selectedNodes[i].components[j].type != compType) + { + sameType = false; + break; + } + } + + if (sameType) + { + ++count; + for (uint i = 0; i < selectedNodes.length; ++i) + editComponents.Push(selectedNodes[i].components[j]); + } + } + if (count > 1) + numEditableComponentsPerNode = count; + } + + if (selectedNodes.empty && editNode !is null) + editNodes.Push(editNode); + else + { + editNodes = selectedNodes; + + // Cannot multi-edit on scene and node(s) together as scene and node do not share identical attributes, + // editing via gizmo does not make too much sense either + if (editNodes.length > 1 && editNodes[0] is editorScene) + editNodes.Erase(0); + } + + if (selectedUIElements.empty && editUIElement !is null) + editUIElements.Push(editUIElement); + else + editUIElements = selectedUIElements; + + PositionGizmo(); + UpdateAttributeInspector(); + UpdateCameraPreview(); +} + +void HandleHierarchyListDoubleClick(StringHash eventType, VariantMap& eventData) +{ + UIElement@ item = eventData["Item"].GetPtr(); + int type = item.vars[TYPE_VAR].GetI32(); + // Locate nodes from the scene by double-clicking + if (type == ITEM_NODE) + { + Node@ node = editorScene.GetNode(item.vars[NODE_ID_VAR].GetU32()); + Array nodes; + nodes.Push(node); + LocateNodes(nodes); + } + else if (type == ITEM_COMPONENT) + { + Component@ component = editorScene.GetComponent(item.vars[COMPONENT_ID_VAR].GetU32()); + Array components; + components.Push(component); + LocateComponents(components); + } + + bool isExpanded = hierarchyList.IsExpanded(hierarchyList.selection); + + if (!isExpanded && eventData["Button"].GetI32() == MOUSEB_LEFT) + { + isExpanded = !isExpanded; + hierarchyList.Expand(hierarchyList.selection, isExpanded, false); + } +} + +void HandleHierarchyItemClick(StringHash eventType, VariantMap& eventData) +{ + if (eventData["Button"].GetI32() != MOUSEB_RIGHT) + return; + + UIElement@ uiElement = eventData["Item"].GetPtr(); + int selectionIndex = eventData["Selection"].GetI32(); + + Array actions; + int type = uiElement.vars[TYPE_VAR].GetI32(); + + // Adds left clicked items to selection which is not normal listview behavior + if (type == ITEM_COMPONENT || type == ITEM_NODE) + { + if (input.keyDown[KEY_LSHIFT]) + hierarchyList.AddSelection(selectionIndex); + else + { + hierarchyList.ClearSelection(); + hierarchyList.AddSelection(selectionIndex); + } + } + + if (type == ITEM_COMPONENT) + { + Component@ targetComponent = editorScene.GetComponent(uiElement.vars[COMPONENT_ID_VAR].GetU32()); + if (targetComponent is null) + return; + + actions.Push(CreateContextMenuItem("Copy", "HandleHierarchyContextCopy")); + actions.Push(CreateContextMenuItem("Cut", "HandleHierarchyContextCut")); + actions.Push(CreateContextMenuItem("Delete", "HandleHierarchyContextDelete")); + actions.Push(CreateContextMenuItem("Paste", "HandleHierarchyContextPaste")); + actions.Push(CreateContextMenuItem("Enable/disable", "HandleHierarchyContextEnableDisable")); + + /* actions.Push(CreateBrowserFileActionMenu("Edit", "HandleBrowserEditResource", file)); */ + } + else if (type == ITEM_NODE) + { + actions.Push(CreateContextMenuItem("Create Replicated Node", "HandleHierarchyContextCreateReplicatedNode")); + actions.Push(CreateContextMenuItem("Create Local Node", "HandleHierarchyContextCreateLocalNode")); + actions.Push(CreateContextMenuItem("Duplicate", "HandleHierarchyContextDuplicate")); + actions.Push(CreateContextMenuItem("Copy", "HandleHierarchyContextCopy")); + actions.Push(CreateContextMenuItem("Cut", "HandleHierarchyContextCut")); + actions.Push(CreateContextMenuItem("Delete", "HandleHierarchyContextDelete")); + actions.Push(CreateContextMenuItem("Paste", "HandleHierarchyContextPaste")); + actions.Push(CreateContextMenuItem("Reset to default", "HandleHierarchyContextResetToDefault")); + actions.Push(CreateContextMenuItem("Reset position", "HandleHierarchyContextResetPosition")); + actions.Push(CreateContextMenuItem("Reset rotation", "HandleHierarchyContextResetRotation")); + actions.Push(CreateContextMenuItem("Reset scale", "HandleHierarchyContextResetScale")); + actions.Push(CreateContextMenuItem("Enable/disable", "HandleHierarchyContextEnableDisable")); + actions.Push(CreateContextMenuItem("Unparent", "HandleHierarchyContextUnparent")); + } + else if (type == ITEM_UI_ELEMENT) + { + // close ui element + actions.Push(CreateContextMenuItem("Close UI-Layout", "HandleHierarchyContextUIElementCloseUILayout")); + actions.Push(CreateContextMenuItem("Close all UI-layouts", "HandleHierarchyContextUIElementCloseAllUILayouts")); + } + + if (actions.length > 0) + ActivateContextMenu(actions); +} + +void HandleDragDropTest(StringHash eventType, VariantMap& eventData) +{ + UIElement@ source = eventData["Source"].GetPtr(); + UIElement@ target = eventData["Target"].GetPtr(); + int itemType; + eventData["Accept"] = TestDragDrop(source, target, itemType); +} + +void HandleDragDropFinish(StringHash eventType, VariantMap& eventData) +{ + UIElement@ source = eventData["Source"].GetPtr(); + UIElement@ target = eventData["Target"].GetPtr(); + int itemType = ITEM_NONE; + bool accept = TestDragDrop(source, target, itemType); + eventData["Accept"] = accept; + if (!accept) + return; + + // Resource browser + if (source !is null && source.GetVar(TEXT_VAR_RESOURCE_TYPE).GetI32() > 0) + { + int type = source.GetVar(TEXT_VAR_RESOURCE_TYPE).GetI32(); + + BrowserFile@ browserFile = GetBrowserFileFromId(source.vars[TEXT_VAR_FILE_ID].GetU32()); + if (browserFile is null) + return; + + Component@ createdComponent; + if (itemType == ITEM_NODE) + { + Node@ targetNode = editorScene.GetNode(target.vars[NODE_ID_VAR].GetU32()); + if (targetNode is null) + return; + + if (type == RESOURCE_TYPE_PREFAB) + { + LoadNode(browserFile.GetFullPath(), targetNode); + } + else if(type == RESOURCE_TYPE_SCRIPTFILE) + { + // TODO: not sure what to do here. lots of choices. + } + else if(type == RESOURCE_TYPE_MODEL) + { + CreateModelWithStaticModel(browserFile.resourceKey, targetNode); + return; + } + else if (type == RESOURCE_TYPE_PARTICLEEFFECT) + { + if (browserFile.extension == "xml") + { + ParticleEffect@ effect = cache.GetResource("ParticleEffect", browserFile.resourceKey); + if (effect is null) + return; + + ParticleEmitter@ emitter = targetNode.CreateComponent("ParticleEmitter"); + emitter.effect = effect; + createdComponent = emitter; + } + } + else if (type == RESOURCE_TYPE_2D_PARTICLE_EFFECT) + { + if (browserFile.extension == "xml") + { + Resource@ effect = cache.GetResource("ParticleEffect2D", browserFile.resourceKey); + if (effect is null) + return; + + ResourceRef effectRef; + effectRef.type = effect.type; + effectRef.name = effect.name; + + Component@ emitter = targetNode.CreateComponent("ParticleEmitter2D"); + emitter.SetAttribute("Particle Effect", Variant(effectRef)); + createdComponent = emitter; + } + } + } + else if (itemType == ITEM_COMPONENT) + { + Component@ targetComponent = editorScene.GetComponent(target.vars[COMPONENT_ID_VAR].GetU32()); + + if (targetComponent is null) + return; + + if (type == RESOURCE_TYPE_MATERIAL) + { + StaticModel@ model = cast(targetComponent); + if (model is null) + return; + + AssignMaterial(model, browserFile.resourceKey); + } + else if (type == RESOURCE_TYPE_MODEL) + { + StaticModel@ staticModel = cast(targetComponent); + if (staticModel is null) + return; + + AssignModel(staticModel, browserFile.resourceKey); + } + } + else + { + LineEdit@ text = cast(target); + if (text is null) + return; + text.text = browserFile.resourceKey; + VariantMap data(); + data["Element"] = text; + data["Text"] = text.text; + text.SendEvent("TextFinished", data); + } + + if (createdComponent !is null) + { + CreateLoadedComponent(createdComponent); + } + return; + } + + if (itemType == ITEM_NODE) + { + Node@ targetNode = editorScene.GetNode(target.vars[NODE_ID_VAR].GetU32()); + Array sourceNodes = GetMultipleSourceNodes(source); + + if (sourceNodes.length > 0) + { + if (input.qualifierDown[QUAL_CTRL] && sourceNodes.length == 1) + SceneReorder(sourceNodes[0], targetNode); + else + { + // If target is null, parent to scene + if (targetNode is null) + targetNode = editorScene; + + if (sourceNodes.length > 1) + SceneChangeParent(sourceNodes[0], sourceNodes, targetNode); + else + SceneChangeParent(sourceNodes[0], targetNode); + } + + // Focus the node at its new position in the list which in turn should trigger a refresh in attribute inspector + FocusNode(sourceNodes[0]); + } + } + else if (itemType == ITEM_UI_ELEMENT) + { + UIElement@ sourceElement = GetUIElementByID(source.vars[UI_ELEMENT_ID_VAR].GetU32()); + UIElement@ targetElement = GetUIElementByID(target.vars[UI_ELEMENT_ID_VAR].GetU32()); + + // If target is null, cannot proceed + if (targetElement is null) + return; + + if (input.qualifierDown[QUAL_CTRL]) + { + if (!UIElementReorder(sourceElement, targetElement)) + return; + } + else + { + if (!UIElementChangeParent(sourceElement, targetElement)) + return; + } + + // Focus the element at its new position in the list which in turn should trigger a refresh in attribute inspector + FocusUIElement(sourceElement); + } + else if (itemType == ITEM_COMPONENT) + { + Component@ targetComponent = editorScene.GetComponent(target.vars[COMPONENT_ID_VAR].GetU32()); + Array sourceNodes = GetMultipleSourceNodes(source); + + if (input.qualifierDown[QUAL_CTRL] && sourceNodes.length == 1) + { + // Reorder components within node + Component@ sourceComponent = editorScene.GetComponent(source.vars[COMPONENT_ID_VAR].GetU32()); + SceneReorder(sourceComponent, targetComponent); + } + else + { + if (targetComponent !is null && sourceNodes.length > 0) + { + // Drag node to StaticModelGroup to make it an instance + StaticModelGroup@ smg = cast(targetComponent); + if (smg !is null) + { + // Save undo action + EditAttributeAction action; + uint attrIndex = GetAttributeIndex(smg, "Instance Nodes"); + Variant oldIDs = smg.attributes[attrIndex]; + + for (uint i = 0; i < sourceNodes.length; ++i) + smg.AddInstanceNode(sourceNodes[i]); + + action.Define(smg, attrIndex, oldIDs); + SaveEditAction(action); + SetSceneModified(); + UpdateAttributeInspector(false); + } + + // Drag node to SplinePath to make it a control point + SplinePath@ spline = cast(targetComponent); + if (spline !is null) + { + // Save undo action + EditAttributeAction action; + uint attrIndex = GetAttributeIndex(spline, "Control Points"); + Variant oldIDs = spline.attributes[attrIndex]; + + for (uint i = 0; i < sourceNodes.length; ++i) + spline.AddControlPoint(sourceNodes[i]); + + action.Define(spline, attrIndex, oldIDs); + SaveEditAction(action); + SetSceneModified(); + UpdateAttributeInspector(false); + } + + // Drag a node to Constraint to make it the remote end of the constraint + Constraint@ constraint = cast(targetComponent); + RigidBody@ rigidBody = sourceNodes[0].GetComponent("RigidBody"); + if (constraint !is null && rigidBody !is null) + { + // Save undo action + EditAttributeAction action; + uint attrIndex = GetAttributeIndex(constraint, "Other Body NodeID"); + Variant oldID = constraint.attributes[attrIndex]; + + constraint.otherBody = rigidBody; + + action.Define(constraint, attrIndex, oldID); + SaveEditAction(action); + SetSceneModified(); + UpdateAttributeInspector(false); + } + } + } + } +} + +Array GetMultipleSourceNodes(UIElement@ source) +{ + Array nodeList; + + Node@ node = editorScene.GetNode(source.vars[NODE_ID_VAR].GetU32()); + if (node !is null) + nodeList.Push(node); + + // Handle additional selected children from a ListView + if (source.parent !is null && source.parent.typeName == "HierarchyContainer") + { + ListView@ listView_ = cast(source.parent.parent.parent); + if (listView_ is null) + return nodeList; + + bool sourceIsSelected = false; + for (uint i = 0; i < listView_.selectedItems.length; ++i) + { + if (listView_.selectedItems[i] is source) + { + sourceIsSelected = true; + break; + } + } + + if (sourceIsSelected) + { + for (uint i = 0; i < listView_.selectedItems.length; ++i) + { + UIElement@ item_ = listView_.selectedItems[i]; + // The source item is already added + if (item_ is source) + continue; + + if (item_.vars[TYPE_VAR] == ITEM_NODE) + { + Node@ n = editorScene.GetNode(item_.vars[NODE_ID_VAR].GetU32()); + if (n !is null) + nodeList.Push(n); + } + } + } + } + + return nodeList; +} + +bool TestDragDrop(UIElement@ source, UIElement@ target, int& itemType) +{ + int targetItemType = target.GetVar(TYPE_VAR).GetI32(); + + if (targetItemType == ITEM_NODE) + { + Node@ sourceNode; + Node@ targetNode; + Variant variant = source.GetVar(NODE_ID_VAR); + if (!variant.empty) + sourceNode = editorScene.GetNode(variant.GetU32()); + variant = target.GetVar(NODE_ID_VAR); + if (!variant.empty) + targetNode = editorScene.GetNode(variant.GetU32()); + Array sourceNodes = GetMultipleSourceNodes(source); + + if (sourceNode !is null && targetNode !is null) + { + itemType = ITEM_NODE; + + // Ctrl pressed: reorder + if (input.qualifierDown[QUAL_CTRL] && sourceNodes.length == 1) + { + // Must be within the same parent + if (sourceNode.parent is null || sourceNode.parent !is targetNode.parent) + return false; + } + // No ctrl: Reparent + else + { + if (sourceNode.parent is targetNode) + return false; + if (targetNode.parent is sourceNode) + return false; + } + } + + // Resource browser + if (sourceNode is null && targetNode !is null) + { + itemType = ITEM_NODE; + int type = source.GetVar(TEXT_VAR_RESOURCE_TYPE).GetI32(); + return type == RESOURCE_TYPE_PREFAB || + type == RESOURCE_TYPE_SCRIPTFILE || + type == RESOURCE_TYPE_MODEL || + type == RESOURCE_TYPE_PARTICLEEFFECT || + type == RESOURCE_TYPE_2D_PARTICLE_EFFECT; + } + + return true; + } + else if (targetItemType == ITEM_UI_ELEMENT) + { + UIElement@ sourceElement; + UIElement@ targetElement; + Variant variant = source.GetVar(UI_ELEMENT_ID_VAR); + if (!variant.empty) + sourceElement = GetUIElementByID(variant.GetU32()); + variant = target.GetVar(UI_ELEMENT_ID_VAR); + if (!variant.empty) + targetElement = GetUIElementByID(variant.GetU32()); + + if (sourceElement !is null && targetElement !is null) + { + itemType = ITEM_UI_ELEMENT; + + // Ctrl pressed: reorder + if (input.qualifierDown[QUAL_CTRL]) + { + // Must be within the same parent + if (sourceElement.parent is null || sourceElement.parent !is targetElement.parent) + return false; + } + // No ctrl: reparent + else + { + if (sourceElement.parent is targetElement) + return false; + if (targetElement.parent is sourceElement) + return false; + } + } + + return true; + } + else if (targetItemType == ITEM_COMPONENT) + { + Node@ sourceNode; + Component@ sourceComponent; + Component@ targetComponent; + Variant variant = source.GetVar(NODE_ID_VAR); + if (!variant.empty) + sourceNode = editorScene.GetNode(variant.GetU32()); + variant = target.GetVar(COMPONENT_ID_VAR); + if (!variant.empty) + targetComponent = editorScene.GetComponent(variant.GetU32()); + variant = source.GetVar(COMPONENT_ID_VAR); + if (!variant.empty) + sourceComponent = editorScene.GetComponent(variant.GetU32()); + Array sourceNodes = GetMultipleSourceNodes(source); + + itemType = ITEM_COMPONENT; + + if (input.qualifierDown[QUAL_CTRL] && sourceNodes.length == 1) + { + // Reorder components within node + if (sourceComponent !is null && targetComponent !is null && sourceComponent.node is targetComponent.node) + return true; + } + else + { + // Dragging of nodes to StaticModelGroup, SplinePath or Constraint + if (sourceNode !is null && targetComponent !is null && (targetComponent.type == STATICMODELGROUP_TYPE || + targetComponent.type == CONSTRAINT_TYPE || targetComponent.type == SPLINEPATH_TYPE)) + return true; + + // Resource browser + int type = source.GetVar(TEXT_VAR_RESOURCE_TYPE).GetI32(); + if (targetComponent.type == STATICMODEL_TYPE || targetComponent.type == ANIMATEDMODEL_TYPE) + return type == RESOURCE_TYPE_MATERIAL || type == RESOURCE_TYPE_MODEL; + } + + return false; + } + else if (source.vars.Contains(TEXT_VAR_RESOURCE_TYPE)) // only testing resource browser ui elements + { + int type = source.GetVar(TEXT_VAR_RESOURCE_TYPE).GetI32(); + + // Test against resource pickers + LineEdit@ lineEdit = cast(target); + if (lineEdit !is null) + { + StringHash resourceType = GetResourceTypeFromPickerLineEdit(lineEdit); + if (resourceType == StringHash("Material") && type == RESOURCE_TYPE_MATERIAL) + return true; + else if (resourceType == StringHash("Model") && type == RESOURCE_TYPE_MODEL) + return true; + else if (resourceType == StringHash("Animation") && type == RESOURCE_TYPE_ANIMATION) + return true; + } + } + return true; +} + +StringHash GetResourceTypeFromPickerLineEdit(UIElement@ lineEdit) +{ + Array@ targets = GetAttributeEditorTargets(lineEdit); + if (!targets.empty) + { + resourcePickIndex = lineEdit.vars["Index"].GetU32(); + resourcePickSubIndex = lineEdit.vars["SubIndex"].GetU32(); + AttributeInfo info = targets[0].attributeInfos[resourcePickIndex]; + StringHash resourceType; + if (info.type == VAR_RESOURCEREF) + return targets[0].attributes[resourcePickIndex].GetResourceRef().type; + else if (info.type == VAR_RESOURCEREFLIST) + return targets[0].attributes[resourcePickIndex].GetResourceRefList().type; + else if (info.type == VAR_VARIANTVECTOR) + return targets[0].attributes[resourcePickIndex].GetVariantVector()[resourcePickSubIndex].GetResourceRef().type; + } + return StringHash(); +} + +void FocusNode(Node@ node) +{ + uint index = GetListIndex(node); + hierarchyList.selection = index; +} + +void FocusComponent(Component@ component) +{ + uint index = GetComponentListIndex(component); + hierarchyList.selection = index; +} + +void FocusUIElement(UIElement@ element) +{ + uint index = GetListIndex(element); + hierarchyList.selection = index; +} + +void CreateBuiltinObject(const String& name) +{ + Node@ newNode = editorScene.CreateChild(name, REPLICATED); + // Set the new node a certain distance from the camera + newNode.position = GetNewNodePosition(); + + StaticModel@ object = newNode.CreateComponent("StaticModel"); + object.model = cache.GetResource("Model", "Models/" + name + ".mdl"); + + // Create an undo action for the create + CreateNodeAction action; + action.Define(newNode); + SaveEditAction(action); + SetSceneModified(); + + FocusNode(newNode); +} + +bool CheckHierarchyWindowFocus() +{ + // When we do edit operations based on key shortcuts, make sure the hierarchy list is focused + return ui.focusElement is hierarchyList || ui.focusElement is null; +} + +bool CheckForExistingGlobalComponent(Node@ node, const String&in typeName) +{ + if (typeName != "Octree" && typeName != "PhysicsWorld" && typeName != "DebugRenderer") + return false; + else + return node.HasComponent(typeName); +} + +void HandleNodeAdded(StringHash eventType, VariantMap& eventData) +{ + if (suppressSceneChanges) + return; + + Node@ node = eventData["Node"].GetPtr(); + if (showTemporaryObject || !node.temporary) + UpdateHierarchyItem(node); +} + +void HandleNodeRemoved(StringHash eventType, VariantMap& eventData) +{ + if (suppressSceneChanges) + return; + + Node@ node = eventData["Node"].GetPtr(); + if (showTemporaryObject || !node.temporary) + UpdateHierarchyItem(GetListIndex(node), null, null); +} + +void HandleComponentAdded(StringHash eventType, VariantMap& eventData) +{ + if (suppressSceneChanges) + return; + + // Insert the newly added component at last component position but before the first child node position of the parent node + Node@ node = eventData["Node"].GetPtr(); + Component@ component = eventData["Component"].GetPtr(); + if (showTemporaryObject || (!node.temporary && !component.temporary)) + { + uint nodeIndex = GetListIndex(node); + if (nodeIndex != NO_ITEM) + { + uint index = node.numChildren > 0 ? GetListIndex(node.children[0]) : M_MAX_UNSIGNED; + UpdateHierarchyItem(index, component, hierarchyList.items[nodeIndex]); + } + } +} + +void HandleComponentRemoved(StringHash eventType, VariantMap& eventData) +{ + if (suppressSceneChanges) + return; + + Node@ node = eventData["Node"].GetPtr(); + Component@ component = eventData["Component"].GetPtr(); + if (showTemporaryObject || (!node.temporary && !component.temporary)) + { + uint index = GetComponentListIndex(component); + if (index != NO_ITEM) + hierarchyList.RemoveItem(index); + } +} + +void HandleNodeNameChanged(StringHash eventType, VariantMap& eventData) +{ + if (suppressSceneChanges) + return; + + Node@ node = eventData["Node"].GetPtr(); + if (showTemporaryObject || !node.temporary) + UpdateHierarchyItemText(GetListIndex(node), node.enabled, GetNodeTitle(node)); +} + +void HandleNodeEnabledChanged(StringHash eventType, VariantMap& eventData) +{ + if (suppressSceneChanges) + return; + + Node@ node = eventData["Node"].GetPtr(); + if (showTemporaryObject || !node.temporary) + { + UpdateHierarchyItemText(GetListIndex(node), node.enabled); + attributesDirty = true; + } +} + +void HandleComponentEnabledChanged(StringHash eventType, VariantMap& eventData) +{ + if (suppressSceneChanges) + return; + + Node@ node = eventData["Node"].GetPtr(); + Component@ component = eventData["Component"].GetPtr(); + if (showTemporaryObject || (!node.temporary && !component.temporary)) + { + UpdateHierarchyItemText(GetComponentListIndex(component), component.enabledEffective); + attributesDirty = true; + } +} + +void HandleUIElementAdded(StringHash eventType, VariantMap& eventData) +{ + if (suppressUIElementChanges) + return; + + UIElement@ element = eventData["Element"].GetPtr(); + if ((showInternalUIElement || !element.internal) && (showTemporaryObject || !element.temporary)) + UpdateHierarchyItem(element); +} + +void HandleUIElementRemoved(StringHash eventType, VariantMap& eventData) +{ + if (suppressUIElementChanges) + return; + + UIElement@ element = eventData["Element"].GetPtr(); + UpdateHierarchyItem(GetListIndex(element), null, null); +} + +void HandleElementNameChanged(StringHash eventType, VariantMap& eventData) +{ + if (suppressUIElementChanges) + return; + + UIElement@ element = eventData["Element"].GetPtr(); + UpdateHierarchyItemText(GetListIndex(element), element.visible, GetUIElementTitle(element)); +} + +void HandleElementVisibilityChanged(StringHash eventType, VariantMap& eventData) +{ + if (suppressUIElementChanges) + return; + + UIElement@ element = eventData["Element"].GetPtr(); + UpdateHierarchyItemText(GetListIndex(element), element.visible); +} + +void HandleElementAttributeChanged(StringHash eventType, VariantMap& eventData) +{ + // Do not refresh the attribute inspector while the attribute is being edited via the attribute-editors + if (suppressUIElementChanges || inEditAttribute) + return; + + UIElement@ element = eventData["Element"].GetPtr(); + for (uint i = 0; i < editUIElements.length; ++i) + { + if (editUIElements[i] is element) + attributesDirty = true; + } +} + +void HandleTemporaryChanged(StringHash eventType, VariantMap& eventData) +{ + if (suppressSceneChanges || suppressUIElementChanges) + return; + + Serializable@ serializable = cast(GetEventSender()); + + Node@ node = cast(serializable); + if (node !is null && node.scene is editorScene) + { + if (showTemporaryObject) + UpdateHierarchyItemText(GetListIndex(node), node.enabled); + else if (!node.temporary && GetListIndex(node) == NO_ITEM) + UpdateHierarchyItem(node); + else if (node.temporary) + UpdateHierarchyItem(GetListIndex(node), null, null); + + return; + } + + Component@ component = cast(serializable); + if (component !is null && component.node !is null && component.node.scene is editorScene) + { + node = component.node; + if (showTemporaryObject) + UpdateHierarchyItemText(GetComponentListIndex(component), node.enabled); + else if (!component.temporary && GetComponentListIndex(component) == NO_ITEM) + { + uint nodeIndex = GetListIndex(node); + if (nodeIndex != NO_ITEM) + { + uint index = node.numChildren > 0 ? GetListIndex(node.children[0]) : M_MAX_UNSIGNED; + UpdateHierarchyItem(index, component, hierarchyList.items[nodeIndex]); + } + } + else if (component.temporary) + { + uint index = GetComponentListIndex(component); + if (index != NO_ITEM) + hierarchyList.RemoveItem(index); + } + + return; + } + + UIElement@ element = cast(serializable); + if (element !is null) + { + if (showTemporaryObject) + UpdateHierarchyItemText(GetListIndex(element), element.visible); + else if (!element.temporary && GetListIndex(element) == NO_ITEM) + UpdateHierarchyItem(element); + else if (element.temporary) + UpdateHierarchyItem(GetListIndex(element), null, null); + + return; + } +} + +// Hierarchy window edit functions +bool Undo() +{ + if (undoStackPos > 0) + { + --undoStackPos; + // Undo commands in reverse order + for (int i = int(undoStack[undoStackPos].actions.length - 1); i >= 0; --i) + undoStack[undoStackPos].actions[i].Undo(); + } + + return true; +} + +bool Redo() +{ + if (undoStackPos < undoStack.length) + { + // Redo commands in same order as stored + for (uint i = 0; i < undoStack[undoStackPos].actions.length; ++i) + undoStack[undoStackPos].actions[i].Redo(); + ++undoStackPos; + } + + return true; +} + +bool Cut() +{ + if (CheckHierarchyWindowFocus()) + { + bool ret = true; + if (!selectedNodes.empty || !selectedComponents.empty) + ret = ret && SceneCut(); + // Not mutually exclusive + if (!selectedUIElements.empty) + ret = ret && UIElementCut(); + return ret; + } + + return false; +} + +bool Duplicate() +{ + if (CheckHierarchyWindowFocus()) + { + bool ret = true; + if (!selectedNodes.empty || !selectedComponents.empty) + ret = ret && (selectedNodes.empty || selectedComponents.empty ? SceneDuplicate() : false); // Node and component is mutually exclusive for copy action + // Not mutually exclusive + if (!selectedUIElements.empty) + ret = ret && UIElementDuplicate(); + return ret; + } + + return false; +} + +bool Copy() +{ + if (CheckHierarchyWindowFocus()) + { + bool ret = true; + if (!selectedNodes.empty || !selectedComponents.empty) + ret = ret && (selectedNodes.empty || selectedComponents.empty ? SceneCopy() : false); // Node and component is mutually exclusive for copy action + // Not mutually exclusive + if (!selectedUIElements.empty) + ret = ret && UIElementCopy(); + return ret; + } + + return false; +} + +bool Paste() +{ + if (CheckHierarchyWindowFocus()) + { + bool ret = true; + if (editNode !is null && !sceneCopyBuffer.empty) + ret = ret && ScenePaste(); + // Not mutually exclusive + if (editUIElement !is null && !uiElementCopyBuffer.empty) + ret = ret && UIElementPaste(); + return ret; + } + + return false; +} + +bool BlenderModeDelete() +{ + if (ui.focusElement is null) + { + Array actions; + actions.Push(CreateContextMenuItem("Delete?", "HandleBlenderModeDelete")); + actions.Push(CreateContextMenuItem("Cancel", "HandleEmpty")); + + if (actions.length > 0) + { + ActivateContextMenu(actions); + return true; + } + } + return false; +} + +bool Delete() +{ + if (CheckHierarchyWindowFocus()) + { + bool ret = true; + if (!selectedNodes.empty || !selectedComponents.empty) + ret = ret && SceneDelete(); + // Not mutually exclusive + if (!selectedUIElements.empty) + ret = ret && UIElementDelete(); + return ret; + } + + return false; +} + +bool SelectAll() +{ + if (CheckHierarchyWindowFocus()) + { + if (!selectedNodes.empty || !selectedComponents.empty) + return SceneSelectAll(); + else if (!selectedUIElements.empty || hierarchyList.items[GetListIndex(editorUIElement)].selected) + return UIElementSelectAll(); + else + return SceneSelectAll(); // If nothing is selected yet, fall back to scene select all + } + + return false; +} + +bool DeselectAll() +{ + if (CheckHierarchyWindowFocus()) + { + BeginSelectionModify(); + hierarchyList.ClearSelection(); + EndSelectionModify(); + return true; + } + return false; +} + +bool ResetToDefault() +{ + if (CheckHierarchyWindowFocus()) + { + bool ret = true; + if (!selectedNodes.empty || !selectedComponents.empty) + ret = ret && (selectedNodes.empty || selectedComponents.empty ? SceneResetToDefault() : false); // Node and component is mutually exclusive for reset-to-default action + // Not mutually exclusive + if (!selectedUIElements.empty) + ret = ret && UIElementResetToDefault(); + return ret; + } + + return false; +} + +void ClearEditActions() +{ + undoStack.Clear(); + undoStackPos = 0; +} + +void SaveEditAction(EditAction@ action) +{ + // Create a group with 1 action + EditActionGroup group; + group.actions.Push(action); + SaveEditActionGroup(group); +} + +void SaveEditActionGroup(EditActionGroup@ group) +{ + if (group.actions.empty) + return; + + // Truncate the stack first to current pos + undoStack.Resize(undoStackPos); + undoStack.Push(group); + ++undoStackPos; + + // Limit maximum undo steps + if (undoStack.length > MAX_UNDOSTACK_SIZE) + { + undoStack.Erase(0); + --undoStackPos; + } +} + +void BeginSelectionModify() +{ + // A large operation on selected nodes is about to begin. Disable intermediate selection updates + inSelectionModify = true; + + // Cursor shape reverts back to normal automatically after the large operation is completed + ui.cursor.shape = CS_BUSY; +} + +void EndSelectionModify() +{ + // The large operation on selected nodes has ended. Update node/component selection now + inSelectionModify = false; + HandleHierarchyListSelectionChange(); +} + +void HandleHierarchyContextCreateReplicatedNode() +{ + CreateNode(REPLICATED); +} + +void HandleHierarchyContextCreateLocalNode() +{ + CreateNode(LOCAL); +} + +void HandleHierarchyContextDuplicate() +{ + Duplicate(); +} + +void HandleHierarchyContextCopy() +{ + Copy(); +} + +void HandleHierarchyContextCut() +{ + Cut(); +} + +void HandleHierarchyContextDelete() +{ + Delete(); +} + +void HandleBlenderModeDelete() +{ + Delete(); +} + +void HandleEmpty() +{ + //just doing nothing +} + +void HandleHierarchyContextPaste() +{ + Paste(); +} + +void HandleHierarchyContextResetToDefault() +{ + ResetToDefault(); +} + +void HandleHierarchyContextResetPosition() +{ + SceneResetPosition(); +} + +void HandleHierarchyContextResetRotation() +{ + SceneResetRotation(); +} + +void HandleHierarchyContextResetScale() +{ + SceneResetScale(); +} + +void HandleHierarchyContextEnableDisable() +{ + SceneToggleEnable(); +} + +void HandleHierarchyContextUnparent() +{ + SceneUnparent(); +} + +void HandleHierarchyContextUIElementCloseUILayout() +{ + CloseUILayout(); +} + +void HandleHierarchyContextUIElementCloseAllUILayouts() +{ + CloseAllUILayouts(); +} + +void CollapseHierarchy() +{ + Array oldSelections = hierarchyList.selections; + Array selections = {0}; + + hierarchyList.SetSelections(selections); + + for (uint i = 0; i < selections.length; ++i) + hierarchyList.Expand(selections[i], false, true); + + // only scene's scope expand by default + hierarchyList.Expand(0, true, false); + + hierarchyList.SetSelections(oldSelections); +} + +void CollapseHierarchy(StringHash eventType, VariantMap& eventData) +{ + CollapseHierarchy(); +} + +void HandleShowID(StringHash eventType, VariantMap& eventData) +{ + CheckBox@ checkBox = eventData["Element"].GetPtr(); + showID = checkBox.checked; + UpdateHierarchyItem(editorScene, false); + CollapseHierarchy(); +} diff --git a/bin/Data/Scripts/Editor/EditorImport.as b/bin/EditorData/Editor/Scripts/EditorImport.as similarity index 97% rename from bin/Data/Scripts/Editor/EditorImport.as rename to bin/EditorData/Editor/Scripts/EditorImport.as index fa752502e1e..2d9f0cd61b5 100644 --- a/bin/Data/Scripts/Editor/EditorImport.as +++ b/bin/EditorData/Editor/Scripts/EditorImport.as @@ -1,576 +1,576 @@ -// Urho3D editor import functions - -String importOptions = "-t"; - -class ParentAssignment -{ - uint childID; - String parentName; -} - -class AssetMapping -{ - String assetName; - String fullAssetName; -} - -Array assetMappings; - -String assetImporterPath; - -int ExecuteAssetImporter(Array@ args) -{ - if (assetImporterPath.empty) - { - String exeSuffix = ""; - if (GetPlatform() == "Windows") - exeSuffix = ".exe"; - // Try both with and without the tool directory; a packaged build may not have the tool directory - assetImporterPath = fileSystem.programDir + "tool/AssetImporter" + exeSuffix; - if (!fileSystem.FileExists(assetImporterPath)) - assetImporterPath = fileSystem.programDir + "AssetImporter" + exeSuffix; - } - - return fileSystem.SystemRun(assetImporterPath, args); -} - -void ImportAnimation(const String&in fileName) -{ - if (fileName.empty) - return; - - ui.cursor.shape = CS_BUSY; - - String modelName = "Models/" + GetFileName(fileName) + ".ani"; - String outFileName = sceneResourcePath + modelName; - fileSystem.CreateDir(sceneResourcePath + "Models"); - - Array args; - args.Push("anim"); - args.Push("\"" + fileName + "\""); - args.Push("\"" + outFileName + "\""); - args.Push("-p \"" + sceneResourcePath + "\""); - Array options = importOptions.Trimmed().Split(' '); - for (uint i = 0; i < options.length; ++i) - args.Push(options[i]); - - if (ExecuteAssetImporter(args) == 0) - { - - } - else - log.Error("Failed to execute AssetImporter to import model"); -} - -void ImportModel(const String&in fileName) -{ - if (fileName.empty) - return; - - ui.cursor.shape = CS_BUSY; - - String modelName = "Models/" + GetFileName(fileName) + ".mdl"; - String outFileName = sceneResourcePath + modelName; - fileSystem.CreateDir(sceneResourcePath + "Models"); - - Array args; - args.Push("model"); - args.Push("\"" + fileName + "\""); - args.Push("\"" + outFileName + "\""); - args.Push("-p \"" + sceneResourcePath + "\""); - Array options = importOptions.Trimmed().Split(' '); - for (uint i = 0; i < options.length; ++i) - args.Push(options[i]); - // If material lists are to be applied, make sure the option to create them exists - if (applyMaterialList) - args.Push("-l"); - - if (ExecuteAssetImporter(args) == 0) - { - Node@ newNode = editorScene.CreateChild(GetFileName(fileName)); - StaticModel@ newModel = newNode.CreateComponent("StaticModel"); - newNode.position = GetNewNodePosition(); - newModel.model = cache.GetResource("Model", modelName); - newModel.ApplyMaterialList(); // Setup default materials if possible - - // Create an undo action for the create - CreateNodeAction action; - action.Define(newNode); - SaveEditAction(action); - SetSceneModified(); - - FocusNode(newNode); - } - else - log.Error("Failed to execute AssetImporter to import model"); -} - -void ImportScene(const String&in fileName) -{ - if (fileName.empty) - return; - - ui.cursor.shape = CS_BUSY; - - // Handle Tundra scene files here in code, otherwise via AssetImporter - if (GetExtension(fileName) == ".txml") - ImportTundraScene(fileName); - else - { - // Export scene to a temp file, then load and delete it if successful - Array options = importOptions.Trimmed().Split(' '); - bool isBinary = false; - for (uint i = 0; i < options.length; ++i) - if (options[i] == "-b") - isBinary = true; - String tempSceneName = sceneResourcePath + (isBinary ? TEMP_BINARY_SCENE_NAME : TEMP_SCENE_NAME); - - Array args; - args.Push("scene"); - args.Push("\"" + fileName + "\""); - args.Push("\"" + tempSceneName + "\""); - args.Push("-p \"" + sceneResourcePath + "\""); - for (uint i = 0; i < options.length; ++i) - args.Push(options[i]); - if (applyMaterialList) - args.Push("-l"); - - if (ExecuteAssetImporter(args) == 0) - { - skipMruScene = true; // set to avoid adding tempscene to mru - LoadScene(tempSceneName); - fileSystem.Delete(tempSceneName); - UpdateWindowTitle(); - } - else - log.Error("Failed to execute AssetImporter to import scene"); - } -} - -void ImportTundraScene(const String&in fileName) -{ - fileSystem.CreateDir(sceneResourcePath + "Materials"); - fileSystem.CreateDir(sceneResourcePath + "Models"); - fileSystem.CreateDir(sceneResourcePath + "Textures"); - - XMLFile source; - source.Load(File(fileName, FILE_READ)); - String filePath = GetPath(fileName); - - XMLElement sceneElem = source.root; - XMLElement entityElem = sceneElem.GetChild("entity"); - - Array convertedMaterials; - Array convertedMeshes; - Array parentAssignments; - - // Read the scene directory structure recursively to get assetname to full assetname mappings - Array fileNames = fileSystem.ScanDir(filePath, "*.*", SCAN_FILES, true); - for (uint i = 0; i < fileNames.length; ++i) - { - AssetMapping mapping; - mapping.assetName = GetFileNameAndExtension(fileNames[i]); - mapping.fullAssetName = fileNames[i]; - assetMappings.Push(mapping); - } - - // Clear old scene, then create a zone and a directional light first - ResetScene(); - - // Set standard gravity - editorScene.CreateComponent("PhysicsWorld"); - editorScene.physicsWorld.gravity = Vector3(0, -9.81, 0); - - // Create zone & global light - Node@ zoneNode = editorScene.CreateChild("Zone"); - Zone@ zone = zoneNode.CreateComponent("Zone"); - zone.boundingBox = BoundingBox(-1000, 1000); - zone.ambientColor = Color(0.364, 0.364, 0.364); - zone.fogColor = Color(0.707792, 0.770537, 0.831373); - zone.fogStart = 100.0; - zone.fogEnd = 500.0; - - Node@ lightNode = editorScene.CreateChild("GlobalLight"); - Light@ light = lightNode.CreateComponent("Light"); - lightNode.rotation = Quaternion(60, 30, 0); - light.lightType = LIGHT_DIRECTIONAL; - light.color = Color(0.639, 0.639, 0.639); - light.castShadows = true; - light.shadowCascade = CascadeParameters(5, 15.0, 50.0, 0.0, 0.9); - - // Loop through scene entities - while (!entityElem.isNull) - { - String nodeName; - String meshName; - String parentName; - Vector3 meshPos; - Vector3 meshRot; - Vector3 meshScale(1, 1, 1); - Vector3 pos; - Vector3 rot; - Vector3 scale(1, 1, 1); - bool castShadows = false; - float drawDistance = 0; - Array materialNames; - - int shapeType = -1; - float mass = 0.0f; - Vector3 bodySize; - bool trigger = false; - bool kinematic = false; - uint collisionLayer; - uint collisionMask; - String collisionMeshName; - - XMLElement compElem = entityElem.GetChild("component"); - while (!compElem.isNull) - { - String compType = compElem.GetAttribute("type"); - - if (compType == "EC_Mesh" || compType == "Mesh") - { - Array coords = GetComponentAttribute(compElem, "Transform").Split(','); - meshPos = GetVector3FromStrings(coords, 0); - meshPos.z = -meshPos.z; // Convert to lefthanded - meshRot = GetVector3FromStrings(coords, 3); - meshScale = GetVector3FromStrings(coords, 6); - meshName = GetComponentAttribute(compElem, "Mesh ref"); - castShadows = GetComponentAttribute(compElem, "Cast shadows").ToBool(); - drawDistance = GetComponentAttribute(compElem, "Draw distance").ToFloat(); - materialNames = GetComponentAttribute(compElem, "Mesh materials").Split(';'); - ProcessRef(meshName); - for (uint i = 0; i < materialNames.length; ++i) - ProcessRef(materialNames[i]); - } - if (compType == "EC_Name" || compType == "Name") - nodeName = GetComponentAttribute(compElem, "name"); - if (compType == "EC_Placeable" || compType == "Placeable") - { - Array coords = GetComponentAttribute(compElem, "Transform").Split(','); - pos = GetVector3FromStrings(coords, 0); - pos.z = -pos.z; // Convert to lefthanded - rot = GetVector3FromStrings(coords, 3); - scale = GetVector3FromStrings(coords, 6); - parentName = GetComponentAttribute(compElem, "Parent entity ref"); - } - if (compType == "EC_RigidBody" || compType == "RigidBody") - { - shapeType = GetComponentAttribute(compElem, "Shape type").ToI32(); - mass = GetComponentAttribute(compElem, "Mass").ToFloat(); - bodySize = GetComponentAttribute(compElem, "Size").ToVector3(); - collisionMeshName = GetComponentAttribute(compElem, "Collision mesh ref"); - trigger = GetComponentAttribute(compElem, "Phantom").ToBool(); - kinematic = GetComponentAttribute(compElem, "Kinematic").ToBool(); - collisionLayer = GetComponentAttribute(compElem, "Collision Layer").ToI32(); - collisionMask = GetComponentAttribute(compElem, "Collision Mask").ToI32(); - ProcessRef(collisionMeshName); - } - - compElem = compElem.GetNext("component"); - } - - // If collision mesh not specified for the rigid body, assume same as the visible mesh - if ((shapeType == 4 || shapeType == 6) && collisionMeshName.Trimmed().empty) - collisionMeshName = meshName; - - if (!meshName.empty || shapeType >= 0) - { - for (uint i = 0; i < materialNames.length; ++i) - ConvertMaterial(materialNames[i], filePath, convertedMaterials); - - ConvertModel(meshName, filePath, convertedMeshes); - ConvertModel(collisionMeshName, filePath, convertedMeshes); - - Node@ newNode = editorScene.CreateChild(nodeName); - - // Calculate final transform in an Ogre-like fashion - Quaternion quat = GetTransformQuaternion(rot); - Quaternion meshQuat = GetTransformQuaternion(meshRot); - Quaternion finalQuat = quat * meshQuat; - Vector3 finalScale = scale * meshScale; - Vector3 finalPos = pos + quat * (scale * meshPos); - - newNode.SetTransform(finalPos, finalQuat, finalScale); - - // Create model - if (!meshName.empty) - { - StaticModel@ model = newNode.CreateComponent("StaticModel"); - model.model = cache.GetResource("Model", GetOutModelName(meshName)); - model.drawDistance = drawDistance; - model.castShadows = castShadows; - // Set default grey material to match Tundra defaults - model.material = cache.GetResource("Material", "Materials/DefaultGrey.xml"); - // Then try to assign the actual materials - for (uint i = 0; i < materialNames.length; ++i) - { - Material@ mat = cache.GetResource("Material", GetOutMaterialName(materialNames[i])); - if (mat !is null) - model.materials[i] = mat; - } - } - - // Create rigidbody & collision shape - if (shapeType >= 0) - { - RigidBody@ body = newNode.CreateComponent("RigidBody"); - - // If mesh has scaling, undo it for the collision shape - bodySize.x /= meshScale.x; - bodySize.y /= meshScale.y; - bodySize.z /= meshScale.z; - - CollisionShape@ shape = newNode.CreateComponent("CollisionShape"); - switch (shapeType) - { - case 0: - shape.SetBox(bodySize); - break; - - case 1: - shape.SetSphere(bodySize.x); - break; - - case 2: - shape.SetCylinder(bodySize.x, bodySize.y); - break; - - case 3: - shape.SetCapsule(bodySize.x, bodySize.y); - break; - - case 4: - shape.SetTriangleMesh(cache.GetResource("Model", GetOutModelName(collisionMeshName)), 0, bodySize); - break; - - case 6: - shape.SetConvexHull(cache.GetResource("Model", GetOutModelName(collisionMeshName)), 0, bodySize); - break; - } - - body.collisionLayer = collisionLayer; - body.collisionMask = collisionMask; - body.trigger = trigger; - body.mass = mass; - } - - // Store pending parent assignment if necessary - if (!parentName.empty) - { - ParentAssignment assignment; - assignment.childID = newNode.id; - assignment.parentName = parentName; - parentAssignments.Push(assignment); - } - } - - entityElem = entityElem.GetNext("entity"); - } - - // Process any parent assignments now - for (uint i = 0; i < parentAssignments.length; ++i) - { - Node@ childNode = editorScene.GetNode(parentAssignments[i].childID); - Node@ parentNode = editorScene.GetChild(parentAssignments[i].parentName, true); - if (childNode !is null && parentNode !is null) - childNode.parent = parentNode; - } - - UpdateHierarchyItem(editorScene, true); - UpdateWindowTitle(); - assetMappings.Clear(); -} - -String GetFullAssetName(const String& assetName) -{ - for (uint i = 0; i < assetMappings.length; ++i) - { - if (assetMappings[i].assetName == assetName) - return assetMappings[i].fullAssetName; - } - - return assetName; -} - -Quaternion GetTransformQuaternion(Vector3 rotEuler) -{ - // Convert rotation to lefthanded - Quaternion rotateX(-rotEuler.x, Vector3(1, 0, 0)); - Quaternion rotateY(-rotEuler.y, Vector3(0, 1, 0)); - Quaternion rotateZ(-rotEuler.z, Vector3(0, 0, -1)); - return rotateZ * rotateY * rotateX; -} - -String GetComponentAttribute(XMLElement compElem, const String&in name) -{ - XMLElement attrElem = compElem.GetChild("attribute"); - while (!attrElem.isNull) - { - if (attrElem.GetAttribute("name") == name) - return attrElem.GetAttribute("value"); - - attrElem = attrElem.GetNext("attribute"); - } - - return ""; -} - -Vector3 GetVector3FromStrings(Array@ coords, uint startIndex) -{ - return Vector3(coords[startIndex].ToFloat(), coords[startIndex + 1].ToFloat(), coords[startIndex + 2].ToFloat()); -} - -void ProcessRef(String& ref) -{ - if (ref.StartsWith("local://")) - ref = ref.Substring(8); - if (ref.StartsWith("file://")) - ref = ref.Substring(7); -} - -String GetOutModelName(const String&in ref) -{ - return "Models/" + GetFullAssetName(ref).Replaced('/', '_').Replaced(".mesh", ".mdl"); -} - -String GetOutMaterialName(const String&in ref) -{ - return "Materials/" + GetFullAssetName(ref).Replaced('/', '_').Replaced(".material", ".xml"); -} - -String GetOutTextureName(const String&in ref) -{ - return "Textures/" + GetFullAssetName(ref).Replaced('/', '_'); -} - -void ConvertModel(const String&in modelName, const String&in filePath, Array@ convertedModels) -{ - if (modelName.Trimmed().empty) - return; - - for (uint i = 0; i < convertedModels.length; ++i) - { - if (convertedModels[i] == modelName) - return; - } - - String meshFileName = filePath + GetFullAssetName(modelName); - String xmlFileName = filePath + GetFullAssetName(modelName) + ".xml"; - String outFileName = sceneResourcePath + GetOutModelName(modelName); - - // Convert .mesh to .mesh.xml - String cmdLine = "ogrexmlconverter \"" + meshFileName + "\" \"" + xmlFileName + "\""; - if (!fileSystem.FileExists(xmlFileName)) - fileSystem.SystemCommand(cmdLine.Replaced('/', '\\')); - - if (!fileSystem.FileExists(outFileName)) - { - // Convert .mesh.xml to .mdl - Array args; - args.Push("\"" + xmlFileName + "\""); - args.Push("\"" + outFileName + "\""); - args.Push("-a"); - fileSystem.SystemRun(fileSystem.programDir + "tool/OgreImporter", args); - } - - convertedModels.Push(modelName); -} - -void ConvertMaterial(const String&in materialName, const String&in filePath, Array@ convertedMaterials) -{ - if (materialName.Trimmed().empty) - return; - - for (uint i = 0; i < convertedMaterials.length; ++i) - { - if (convertedMaterials[i] == materialName) - return; - } - - String fileName = filePath + GetFullAssetName(materialName); - String outFileName = sceneResourcePath + GetOutMaterialName(materialName); - - if (!fileSystem.FileExists(fileName)) - return; - - bool mask = false; - bool twoSided = false; - bool uvScaleSet = false; - String textureName; - Vector2 uvScale(1, 1); - Color diffuse(1, 1, 1, 1); - - File file(fileName, FILE_READ); - while (!file.eof) - { - String line = file.ReadLine().Trimmed(); - if (line.StartsWith("alpha_rejection") || line.StartsWith("scene_blend alpha_blend")) - mask = true; - if (line.StartsWith("cull_hardware none")) - twoSided = true; - // Todo: handle multiple textures per material - if (textureName.empty && line.StartsWith("texture ")) - { - textureName = line.Substring(8); - ProcessRef(textureName); - } - if (!uvScaleSet && line.StartsWith("scale ")) - { - uvScale = line.Substring(6).ToVector2(); - uvScaleSet = true; - } - if (line.StartsWith("diffuse ")) - diffuse = line.Substring(8).ToColor(); - } - - XMLFile outMat; - XMLElement rootElem = outMat.CreateRoot("material"); - XMLElement techniqueElem = rootElem.CreateChild("technique"); - - if (twoSided) - { - XMLElement cullElem = rootElem.CreateChild("cull"); - cullElem.SetAttribute("value", "none"); - XMLElement shadowCullElem = rootElem.CreateChild("shadowcull"); - shadowCullElem.SetAttribute("value", "none"); - } - - if (!textureName.empty) - { - techniqueElem.SetAttribute("name", mask ? "Techniques/DiffAlphaMask.xml" : "Techniques/Diff.xml"); - - String outTextureName = GetOutTextureName(textureName); - XMLElement textureElem = rootElem.CreateChild("texture"); - textureElem.SetAttribute("unit", "diffuse"); - textureElem.SetAttribute("name", outTextureName); - - fileSystem.Copy(filePath + GetFullAssetName(textureName), sceneResourcePath + outTextureName); - } - else - techniqueElem.SetAttribute("name", "NoTexture.xml"); - - if (uvScale != Vector2(1, 1)) - { - XMLElement uScaleElem = rootElem.CreateChild("parameter"); - uScaleElem.SetAttribute("name", "UOffset"); - uScaleElem.SetVector3("value", Vector3(1 / uvScale.x, 0, 0)); - - XMLElement vScaleElem = rootElem.CreateChild("parameter"); - vScaleElem.SetAttribute("name", "VOffset"); - vScaleElem.SetVector3("value", Vector3(0, 1 / uvScale.y, 0)); - } - - if (diffuse != Color(1, 1, 1, 1)) - { - XMLElement diffuseElem = rootElem.CreateChild("parameter"); - diffuseElem.SetAttribute("name", "MatDiffColor"); - diffuseElem.SetColor("value", diffuse); - } - - File outFile(outFileName, FILE_WRITE); - outMat.Save(outFile); - outFile.Close(); - - convertedMaterials.Push(materialName); -} +// Urho3D editor import functions + +String importOptions = "-t"; + +class ParentAssignment +{ + uint childID; + String parentName; +} + +class AssetMapping +{ + String assetName; + String fullAssetName; +} + +Array assetMappings; + +String assetImporterPath; + +int ExecuteAssetImporter(Array@ args) +{ + if (assetImporterPath.empty) + { + String exeSuffix = ""; + if (GetPlatform() == "Windows") + exeSuffix = ".exe"; + // Try both with and without the tool directory; a packaged build may not have the tool directory + assetImporterPath = fileSystem.programDir + "tool/AssetImporter" + exeSuffix; + if (!fileSystem.FileExists(assetImporterPath)) + assetImporterPath = fileSystem.programDir + "AssetImporter" + exeSuffix; + } + + return fileSystem.SystemRun(assetImporterPath, args); +} + +void ImportAnimation(const String&in fileName) +{ + if (fileName.empty) + return; + + ui.cursor.shape = CS_BUSY; + + String modelName = "Models/" + GetFileName(fileName) + ".ani"; + String outFileName = sceneResourcePath + modelName; + fileSystem.CreateDir(sceneResourcePath + "Models"); + + Array args; + args.Push("anim"); + args.Push("\"" + fileName + "\""); + args.Push("\"" + outFileName + "\""); + args.Push("-p \"" + sceneResourcePath + "\""); + Array options = importOptions.Trimmed().Split(' '); + for (uint i = 0; i < options.length; ++i) + args.Push(options[i]); + + if (ExecuteAssetImporter(args) == 0) + { + + } + else + log.Error("Failed to execute AssetImporter to import model"); +} + +void ImportModel(const String&in fileName) +{ + if (fileName.empty) + return; + + ui.cursor.shape = CS_BUSY; + + String modelName = "Models/" + GetFileName(fileName) + ".mdl"; + String outFileName = sceneResourcePath + modelName; + fileSystem.CreateDir(sceneResourcePath + "Models"); + + Array args; + args.Push("model"); + args.Push("\"" + fileName + "\""); + args.Push("\"" + outFileName + "\""); + args.Push("-p \"" + sceneResourcePath + "\""); + Array options = importOptions.Trimmed().Split(' '); + for (uint i = 0; i < options.length; ++i) + args.Push(options[i]); + // If material lists are to be applied, make sure the option to create them exists + if (applyMaterialList) + args.Push("-l"); + + if (ExecuteAssetImporter(args) == 0) + { + Node@ newNode = editorScene.CreateChild(GetFileName(fileName)); + StaticModel@ newModel = newNode.CreateComponent("StaticModel"); + newNode.position = GetNewNodePosition(); + newModel.model = cache.GetResource("Model", modelName); + newModel.ApplyMaterialList(); // Setup default materials if possible + + // Create an undo action for the create + CreateNodeAction action; + action.Define(newNode); + SaveEditAction(action); + SetSceneModified(); + + FocusNode(newNode); + } + else + log.Error("Failed to execute AssetImporter to import model"); +} + +void ImportScene(const String&in fileName) +{ + if (fileName.empty) + return; + + ui.cursor.shape = CS_BUSY; + + // Handle Tundra scene files here in code, otherwise via AssetImporter + if (GetExtension(fileName) == ".txml") + ImportTundraScene(fileName); + else + { + // Export scene to a temp file, then load and delete it if successful + Array options = importOptions.Trimmed().Split(' '); + bool isBinary = false; + for (uint i = 0; i < options.length; ++i) + if (options[i] == "-b") + isBinary = true; + String tempSceneName = sceneResourcePath + (isBinary ? TEMP_BINARY_SCENE_NAME : TEMP_SCENE_NAME); + + Array args; + args.Push("scene"); + args.Push("\"" + fileName + "\""); + args.Push("\"" + tempSceneName + "\""); + args.Push("-p \"" + sceneResourcePath + "\""); + for (uint i = 0; i < options.length; ++i) + args.Push(options[i]); + if (applyMaterialList) + args.Push("-l"); + + if (ExecuteAssetImporter(args) == 0) + { + skipMruScene = true; // set to avoid adding tempscene to mru + LoadScene(tempSceneName); + fileSystem.Delete(tempSceneName); + UpdateWindowTitle(); + } + else + log.Error("Failed to execute AssetImporter to import scene"); + } +} + +void ImportTundraScene(const String&in fileName) +{ + fileSystem.CreateDir(sceneResourcePath + "Materials"); + fileSystem.CreateDir(sceneResourcePath + "Models"); + fileSystem.CreateDir(sceneResourcePath + "Textures"); + + XMLFile source; + source.Load(File(fileName, FILE_READ)); + String filePath = GetPath(fileName); + + XMLElement sceneElem = source.root; + XMLElement entityElem = sceneElem.GetChild("entity"); + + Array convertedMaterials; + Array convertedMeshes; + Array parentAssignments; + + // Read the scene directory structure recursively to get assetname to full assetname mappings + Array fileNames = fileSystem.ScanDir(filePath, "*.*", SCAN_FILES, true); + for (uint i = 0; i < fileNames.length; ++i) + { + AssetMapping mapping; + mapping.assetName = GetFileNameAndExtension(fileNames[i]); + mapping.fullAssetName = fileNames[i]; + assetMappings.Push(mapping); + } + + // Clear old scene, then create a zone and a directional light first + ResetScene(); + + // Set standard gravity + editorScene.CreateComponent("PhysicsWorld"); + editorScene.physicsWorld.gravity = Vector3(0, -9.81, 0); + + // Create zone & global light + Node@ zoneNode = editorScene.CreateChild("Zone"); + Zone@ zone = zoneNode.CreateComponent("Zone"); + zone.boundingBox = BoundingBox(-1000, 1000); + zone.ambientColor = Color(0.364, 0.364, 0.364); + zone.fogColor = Color(0.707792, 0.770537, 0.831373); + zone.fogStart = 100.0; + zone.fogEnd = 500.0; + + Node@ lightNode = editorScene.CreateChild("GlobalLight"); + Light@ light = lightNode.CreateComponent("Light"); + lightNode.rotation = Quaternion(60, 30, 0); + light.lightType = LIGHT_DIRECTIONAL; + light.color = Color(0.639, 0.639, 0.639); + light.castShadows = true; + light.shadowCascade = CascadeParameters(5, 15.0, 50.0, 0.0, 0.9); + + // Loop through scene entities + while (!entityElem.isNull) + { + String nodeName; + String meshName; + String parentName; + Vector3 meshPos; + Vector3 meshRot; + Vector3 meshScale(1, 1, 1); + Vector3 pos; + Vector3 rot; + Vector3 scale(1, 1, 1); + bool castShadows = false; + float drawDistance = 0; + Array materialNames; + + int shapeType = -1; + float mass = 0.0f; + Vector3 bodySize; + bool trigger = false; + bool kinematic = false; + uint collisionLayer; + uint collisionMask; + String collisionMeshName; + + XMLElement compElem = entityElem.GetChild("component"); + while (!compElem.isNull) + { + String compType = compElem.GetAttribute("type"); + + if (compType == "EC_Mesh" || compType == "Mesh") + { + Array coords = GetComponentAttribute(compElem, "Transform").Split(','); + meshPos = GetVector3FromStrings(coords, 0); + meshPos.z = -meshPos.z; // Convert to lefthanded + meshRot = GetVector3FromStrings(coords, 3); + meshScale = GetVector3FromStrings(coords, 6); + meshName = GetComponentAttribute(compElem, "Mesh ref"); + castShadows = GetComponentAttribute(compElem, "Cast shadows").ToBool(); + drawDistance = GetComponentAttribute(compElem, "Draw distance").ToFloat(); + materialNames = GetComponentAttribute(compElem, "Mesh materials").Split(';'); + ProcessRef(meshName); + for (uint i = 0; i < materialNames.length; ++i) + ProcessRef(materialNames[i]); + } + if (compType == "EC_Name" || compType == "Name") + nodeName = GetComponentAttribute(compElem, "name"); + if (compType == "EC_Placeable" || compType == "Placeable") + { + Array coords = GetComponentAttribute(compElem, "Transform").Split(','); + pos = GetVector3FromStrings(coords, 0); + pos.z = -pos.z; // Convert to lefthanded + rot = GetVector3FromStrings(coords, 3); + scale = GetVector3FromStrings(coords, 6); + parentName = GetComponentAttribute(compElem, "Parent entity ref"); + } + if (compType == "EC_RigidBody" || compType == "RigidBody") + { + shapeType = GetComponentAttribute(compElem, "Shape type").ToI32(); + mass = GetComponentAttribute(compElem, "Mass").ToFloat(); + bodySize = GetComponentAttribute(compElem, "Size").ToVector3(); + collisionMeshName = GetComponentAttribute(compElem, "Collision mesh ref"); + trigger = GetComponentAttribute(compElem, "Phantom").ToBool(); + kinematic = GetComponentAttribute(compElem, "Kinematic").ToBool(); + collisionLayer = GetComponentAttribute(compElem, "Collision Layer").ToI32(); + collisionMask = GetComponentAttribute(compElem, "Collision Mask").ToI32(); + ProcessRef(collisionMeshName); + } + + compElem = compElem.GetNext("component"); + } + + // If collision mesh not specified for the rigid body, assume same as the visible mesh + if ((shapeType == 4 || shapeType == 6) && collisionMeshName.Trimmed().empty) + collisionMeshName = meshName; + + if (!meshName.empty || shapeType >= 0) + { + for (uint i = 0; i < materialNames.length; ++i) + ConvertMaterial(materialNames[i], filePath, convertedMaterials); + + ConvertModel(meshName, filePath, convertedMeshes); + ConvertModel(collisionMeshName, filePath, convertedMeshes); + + Node@ newNode = editorScene.CreateChild(nodeName); + + // Calculate final transform in an Ogre-like fashion + Quaternion quat = GetTransformQuaternion(rot); + Quaternion meshQuat = GetTransformQuaternion(meshRot); + Quaternion finalQuat = quat * meshQuat; + Vector3 finalScale = scale * meshScale; + Vector3 finalPos = pos + quat * (scale * meshPos); + + newNode.SetTransform(finalPos, finalQuat, finalScale); + + // Create model + if (!meshName.empty) + { + StaticModel@ model = newNode.CreateComponent("StaticModel"); + model.model = cache.GetResource("Model", GetOutModelName(meshName)); + model.drawDistance = drawDistance; + model.castShadows = castShadows; + // Set default grey material to match Tundra defaults + model.material = cache.GetResource("Material", "Materials/DefaultGrey.xml"); + // Then try to assign the actual materials + for (uint i = 0; i < materialNames.length; ++i) + { + Material@ mat = cache.GetResource("Material", GetOutMaterialName(materialNames[i])); + if (mat !is null) + model.materials[i] = mat; + } + } + + // Create rigidbody & collision shape + if (shapeType >= 0) + { + RigidBody@ body = newNode.CreateComponent("RigidBody"); + + // If mesh has scaling, undo it for the collision shape + bodySize.x /= meshScale.x; + bodySize.y /= meshScale.y; + bodySize.z /= meshScale.z; + + CollisionShape@ shape = newNode.CreateComponent("CollisionShape"); + switch (shapeType) + { + case 0: + shape.SetBox(bodySize); + break; + + case 1: + shape.SetSphere(bodySize.x); + break; + + case 2: + shape.SetCylinder(bodySize.x, bodySize.y); + break; + + case 3: + shape.SetCapsule(bodySize.x, bodySize.y); + break; + + case 4: + shape.SetTriangleMesh(cache.GetResource("Model", GetOutModelName(collisionMeshName)), 0, bodySize); + break; + + case 6: + shape.SetConvexHull(cache.GetResource("Model", GetOutModelName(collisionMeshName)), 0, bodySize); + break; + } + + body.collisionLayer = collisionLayer; + body.collisionMask = collisionMask; + body.trigger = trigger; + body.mass = mass; + } + + // Store pending parent assignment if necessary + if (!parentName.empty) + { + ParentAssignment assignment; + assignment.childID = newNode.id; + assignment.parentName = parentName; + parentAssignments.Push(assignment); + } + } + + entityElem = entityElem.GetNext("entity"); + } + + // Process any parent assignments now + for (uint i = 0; i < parentAssignments.length; ++i) + { + Node@ childNode = editorScene.GetNode(parentAssignments[i].childID); + Node@ parentNode = editorScene.GetChild(parentAssignments[i].parentName, true); + if (childNode !is null && parentNode !is null) + childNode.parent = parentNode; + } + + UpdateHierarchyItem(editorScene, true); + UpdateWindowTitle(); + assetMappings.Clear(); +} + +String GetFullAssetName(const String& assetName) +{ + for (uint i = 0; i < assetMappings.length; ++i) + { + if (assetMappings[i].assetName == assetName) + return assetMappings[i].fullAssetName; + } + + return assetName; +} + +Quaternion GetTransformQuaternion(Vector3 rotEuler) +{ + // Convert rotation to lefthanded + Quaternion rotateX(-rotEuler.x, Vector3(1, 0, 0)); + Quaternion rotateY(-rotEuler.y, Vector3(0, 1, 0)); + Quaternion rotateZ(-rotEuler.z, Vector3(0, 0, -1)); + return rotateZ * rotateY * rotateX; +} + +String GetComponentAttribute(XMLElement compElem, const String&in name) +{ + XMLElement attrElem = compElem.GetChild("attribute"); + while (!attrElem.isNull) + { + if (attrElem.GetAttribute("name") == name) + return attrElem.GetAttribute("value"); + + attrElem = attrElem.GetNext("attribute"); + } + + return ""; +} + +Vector3 GetVector3FromStrings(Array@ coords, uint startIndex) +{ + return Vector3(coords[startIndex].ToFloat(), coords[startIndex + 1].ToFloat(), coords[startIndex + 2].ToFloat()); +} + +void ProcessRef(String& ref) +{ + if (ref.StartsWith("local://")) + ref = ref.Substring(8); + if (ref.StartsWith("file://")) + ref = ref.Substring(7); +} + +String GetOutModelName(const String&in ref) +{ + return "Models/" + GetFullAssetName(ref).Replaced('/', '_').Replaced(".mesh", ".mdl"); +} + +String GetOutMaterialName(const String&in ref) +{ + return "Materials/" + GetFullAssetName(ref).Replaced('/', '_').Replaced(".material", ".xml"); +} + +String GetOutTextureName(const String&in ref) +{ + return "Textures/" + GetFullAssetName(ref).Replaced('/', '_'); +} + +void ConvertModel(const String&in modelName, const String&in filePath, Array@ convertedModels) +{ + if (modelName.Trimmed().empty) + return; + + for (uint i = 0; i < convertedModels.length; ++i) + { + if (convertedModels[i] == modelName) + return; + } + + String meshFileName = filePath + GetFullAssetName(modelName); + String xmlFileName = filePath + GetFullAssetName(modelName) + ".xml"; + String outFileName = sceneResourcePath + GetOutModelName(modelName); + + // Convert .mesh to .mesh.xml + String cmdLine = "ogrexmlconverter \"" + meshFileName + "\" \"" + xmlFileName + "\""; + if (!fileSystem.FileExists(xmlFileName)) + fileSystem.SystemCommand(cmdLine.Replaced('/', '\\')); + + if (!fileSystem.FileExists(outFileName)) + { + // Convert .mesh.xml to .mdl + Array args; + args.Push("\"" + xmlFileName + "\""); + args.Push("\"" + outFileName + "\""); + args.Push("-a"); + fileSystem.SystemRun(fileSystem.programDir + "tool/OgreImporter", args); + } + + convertedModels.Push(modelName); +} + +void ConvertMaterial(const String&in materialName, const String&in filePath, Array@ convertedMaterials) +{ + if (materialName.Trimmed().empty) + return; + + for (uint i = 0; i < convertedMaterials.length; ++i) + { + if (convertedMaterials[i] == materialName) + return; + } + + String fileName = filePath + GetFullAssetName(materialName); + String outFileName = sceneResourcePath + GetOutMaterialName(materialName); + + if (!fileSystem.FileExists(fileName)) + return; + + bool mask = false; + bool twoSided = false; + bool uvScaleSet = false; + String textureName; + Vector2 uvScale(1, 1); + Color diffuse(1, 1, 1, 1); + + File file(fileName, FILE_READ); + while (!file.eof) + { + String line = file.ReadLine().Trimmed(); + if (line.StartsWith("alpha_rejection") || line.StartsWith("scene_blend alpha_blend")) + mask = true; + if (line.StartsWith("cull_hardware none")) + twoSided = true; + // Todo: handle multiple textures per material + if (textureName.empty && line.StartsWith("texture ")) + { + textureName = line.Substring(8); + ProcessRef(textureName); + } + if (!uvScaleSet && line.StartsWith("scale ")) + { + uvScale = line.Substring(6).ToVector2(); + uvScaleSet = true; + } + if (line.StartsWith("diffuse ")) + diffuse = line.Substring(8).ToColor(); + } + + XMLFile outMat; + XMLElement rootElem = outMat.CreateRoot("material"); + XMLElement techniqueElem = rootElem.CreateChild("technique"); + + if (twoSided) + { + XMLElement cullElem = rootElem.CreateChild("cull"); + cullElem.SetAttribute("value", "none"); + XMLElement shadowCullElem = rootElem.CreateChild("shadowcull"); + shadowCullElem.SetAttribute("value", "none"); + } + + if (!textureName.empty) + { + techniqueElem.SetAttribute("name", mask ? "Techniques/DiffAlphaMask.xml" : "Techniques/Diff.xml"); + + String outTextureName = GetOutTextureName(textureName); + XMLElement textureElem = rootElem.CreateChild("texture"); + textureElem.SetAttribute("unit", "diffuse"); + textureElem.SetAttribute("name", outTextureName); + + fileSystem.Copy(filePath + GetFullAssetName(textureName), sceneResourcePath + outTextureName); + } + else + techniqueElem.SetAttribute("name", "NoTexture.xml"); + + if (uvScale != Vector2(1, 1)) + { + XMLElement uScaleElem = rootElem.CreateChild("parameter"); + uScaleElem.SetAttribute("name", "UOffset"); + uScaleElem.SetVector3("value", Vector3(1 / uvScale.x, 0, 0)); + + XMLElement vScaleElem = rootElem.CreateChild("parameter"); + vScaleElem.SetAttribute("name", "VOffset"); + vScaleElem.SetVector3("value", Vector3(0, 1 / uvScale.y, 0)); + } + + if (diffuse != Color(1, 1, 1, 1)) + { + XMLElement diffuseElem = rootElem.CreateChild("parameter"); + diffuseElem.SetAttribute("name", "MatDiffColor"); + diffuseElem.SetColor("value", diffuse); + } + + File outFile(outFileName, FILE_WRITE); + outMat.Save(outFile); + outFile.Close(); + + convertedMaterials.Push(materialName); +} diff --git a/bin/Data/Scripts/Editor/EditorInspectorWindow.as b/bin/EditorData/Editor/Scripts/EditorInspectorWindow.as similarity index 96% rename from bin/Data/Scripts/Editor/EditorInspectorWindow.as rename to bin/EditorData/Editor/Scripts/EditorInspectorWindow.as index 7586fc319a2..89d6a82d075 100644 --- a/bin/Data/Scripts/Editor/EditorInspectorWindow.as +++ b/bin/EditorData/Editor/Scripts/EditorInspectorWindow.as @@ -1,1032 +1,1032 @@ -// Urho3D editor attribute inspector window handling -#include "Scripts/Editor/AttributeEditor.as" - -Window@ attributeInspectorWindow; -UIElement@ parentContainer; -UIElement@ inspectorLockButton; - -bool applyMaterialList = true; -bool attributesDirty = false; -bool attributesFullDirty = false; - -const String STRIKED_OUT = "——"; // Two unicode EM DASH (U+2014) -const StringHash NODE_IDS_VAR("NodeIDs"); -const StringHash COMPONENT_IDS_VAR("ComponentIDs"); -const StringHash UI_ELEMENT_IDS_VAR("UIElementIDs"); -const int LABEL_WIDTH = 30; - -// Constants for accessing xmlResources -Array xmlResources; -const uint ATTRIBUTE_RES = 0; -const uint VARIABLE_RES = 1; -const uint STYLE_RES = 2; -const uint TAGS_RES = 3; - -uint nodeContainerIndex = M_MAX_UNSIGNED; -uint componentContainerStartIndex = 0; -uint elementContainerIndex = M_MAX_UNSIGNED; - -// Node or UIElement hash-to-varname reverse mapping -VariantMap globalVarNames; -bool inspectorLocked = false; - -void InitXMLResources() -{ - String[] resources = { "UI/EditorInspector_Attribute.xml", "UI/EditorInspector_Variable.xml", - "UI/EditorInspector_Style.xml", "UI/EditorInspector_Tags.xml" }; - for (uint i = 0; i < resources.length; ++i) - xmlResources.Push(cache.GetResource("XMLFile", resources[i])); -} - -/// Delete all child containers in the inspector list. -void DeleteAllContainers() -{ - parentContainer.RemoveAllChildren(); - nodeContainerIndex = M_MAX_UNSIGNED; - componentContainerStartIndex = 0; - elementContainerIndex = M_MAX_UNSIGNED; -} - -/// Get container at the specified index in the inspector list, the container must be created before. -UIElement@ GetContainer(uint index) -{ - return parentContainer.children[index]; -} - -/// Get node container in the inspector list, create the container if it is not yet available. -UIElement@ GetNodeContainer() -{ - if (nodeContainerIndex != M_MAX_UNSIGNED) - return GetContainer(nodeContainerIndex); - - nodeContainerIndex = parentContainer.numChildren; - parentContainer.LoadChildXML(xmlResources[ATTRIBUTE_RES], uiStyle); - UIElement@ container = GetContainer(nodeContainerIndex); - container.LoadChildXML(xmlResources[VARIABLE_RES], uiStyle); - SubscribeToEvent(container.GetChild("ResetToDefault", true), "Released", "HandleResetToDefault"); - SubscribeToEvent(container.GetChild("NewVarDropDown", true), "ItemSelected", "CreateNodeVariable"); - SubscribeToEvent(container.GetChild("DeleteVarButton", true), "Released", "DeleteNodeVariable"); - ++componentContainerStartIndex; - - parentContainer.LoadChildXML(xmlResources[TAGS_RES], uiStyle); - parentContainer.GetChild("TagsLabel", true).SetFixedWidth(LABEL_WIDTH); - LineEdit@ tagEdit = parentContainer.GetChild("TagsEdit", true); - SubscribeToEvent(tagEdit, "TextChanged", "HandleTagsEdit"); - UIElement@ tagSelect = parentContainer.GetChild("TagsSelect", true); - SubscribeToEvent(tagSelect, "Released", "HandleTagsSelect"); - ++componentContainerStartIndex; - - return container; -} - -/// Get component container at the specified index, create the container if it is not yet available at the specified index. -UIElement@ GetComponentContainer(uint index) -{ - if (componentContainerStartIndex + index < parentContainer.numChildren) - return GetContainer(componentContainerStartIndex + index); - - UIElement@ container; - for (uint i = parentContainer.numChildren; i <= componentContainerStartIndex + index; ++i) - { - parentContainer.LoadChildXML(xmlResources[ATTRIBUTE_RES], uiStyle); - container = GetContainer(i); - SubscribeToEvent(container.GetChild("ResetToDefault", true), "Released", "HandleResetToDefault"); - } - return container; -} - -/// Get UI-element container, create the container if it is not yet available. -UIElement@ GetUIElementContainer() -{ - if (elementContainerIndex != M_MAX_UNSIGNED) - return GetContainer(elementContainerIndex); - - elementContainerIndex = parentContainer.numChildren; - parentContainer.LoadChildXML(xmlResources[ATTRIBUTE_RES], uiStyle); - parentContainer.LoadChildXML(xmlResources[TAGS_RES], uiStyle); - parentContainer.GetChild("TagsLabel", true).SetFixedWidth(LABEL_WIDTH); - UIElement@ container = GetContainer(elementContainerIndex); - container.LoadChildXML(xmlResources[VARIABLE_RES], uiStyle); - container.LoadChildXML(xmlResources[STYLE_RES], uiStyle); - DropDownList@ styleList = container.GetChild("StyleDropDown", true); - styleList.placeholderText = STRIKED_OUT; - styleList.parent.GetChild("StyleDropDownLabel").SetFixedWidth(LABEL_WIDTH); - PopulateStyleList(styleList); - SubscribeToEvent(container.GetChild("ResetToDefault", true), "Released", "HandleResetToDefault"); - SubscribeToEvent(container.GetChild("NewVarDropDown", true), "ItemSelected", "CreateUIElementVariable"); - SubscribeToEvent(container.GetChild("DeleteVarButton", true), "Released", "DeleteUIElementVariable"); - SubscribeToEvent(styleList, "ItemSelected", "HandleStyleItemSelected"); - LineEdit@ tagEdit = parentContainer.GetChild("TagsEdit", true); - SubscribeToEvent(tagEdit, "TextChanged", "HandleTagsEdit"); - UIElement@ tagSelect = parentContainer.GetChild("TagsSelect", true); - SubscribeToEvent(tagSelect, "Released", "HandleTagsSelect"); - return container; -} - -void CreateAttributeInspectorWindow() -{ - if (attributeInspectorWindow !is null) - return; - - InitResourcePicker(); - InitVectorStructs(); - InitXMLResources(); - - attributeInspectorWindow = LoadEditorUI("UI/EditorInspectorWindow.xml"); - parentContainer = attributeInspectorWindow.GetChild("ParentContainer"); - ui.root.AddChild(attributeInspectorWindow); - int height = Min(ui.root.height - 60, 500); - attributeInspectorWindow.SetSize(344, height); - attributeInspectorWindow.SetPosition(ui.root.width - 10 - attributeInspectorWindow.width, 100); - attributeInspectorWindow.opacity = uiMaxOpacity; - attributeInspectorWindow.BringToFront(); - inspectorLockButton = attributeInspectorWindow.GetChild("LockButton", true); - - UpdateAttributeInspector(); - - SubscribeToEvent(inspectorLockButton, "Pressed", "ToggleInspectorLock"); - SubscribeToEvent(attributeInspectorWindow.GetChild("CloseButton", true), "Pressed", "HideAttributeInspectorWindow"); - SubscribeToEvent(attributeInspectorWindow, "LayoutUpdated", "HandleWindowLayoutUpdated"); -} - -void DisableInspectorLock() -{ - inspectorLocked = false; - if (inspectorLockButton !is null) - inspectorLockButton.style = "Button"; - UpdateAttributeInspector(true); -} - -void EnableInspectorLock() -{ - inspectorLocked = true; - if (inspectorLockButton !is null) - inspectorLockButton.style = "ToggledButton"; -} - -void ToggleInspectorLock() -{ - if (inspectorLocked) - DisableInspectorLock(); - else - EnableInspectorLock(); -} - -bool ToggleAttributeInspectorWindow() -{ - if (attributeInspectorWindow.visible == false) - ShowAttributeInspectorWindow(); - else - HideAttributeInspectorWindow(); - return true; -} - -void ShowAttributeInspectorWindow() -{ - attributeInspectorWindow.visible = true; - attributeInspectorWindow.BringToFront(); -} - -void HideAttributeInspectorWindow() -{ - if(viewportMode == VIEWPORT_COMPACT) - return; - attributeInspectorWindow.visible = false; -} - - -/// 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). -void HandleWindowLayoutUpdated() -{ - // 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 - for (uint i = 0; i < parentContainer.numChildren; ++i) - { - UIElement@ container = GetContainer(i); - ListView@ list = container.GetChild("AttributeList"); - if (list is null) - continue; - - int width = list.width; - - // Adjust the icon panel's width - UIElement@ panel = container.GetChild("IconsPanel", true); - if (panel !is null) - panel.width = width; - - // 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 - for (uint j = 0; j < list.numChildren; ++j) - { - UIElement@ element = list.children[j]; - if (!element.internal) - { - element.SetFixedWidth(width); - UIElement@ title = container.GetChild("TitleText"); - element.position = IntVector2(0, (title.screenPosition - list.screenPosition).y); - - // Adjust icon panel's width one more time to cater for the space occupied by 'Is Enabled' check box - if (panel !is null) - panel.width = width - element.children[1].width - panel.layoutSpacing; - - break; - } - } - } -} - -Array ToSerializableArray(Array nodes) -{ - Array serializables; - for (uint i = 0; i < nodes.length; ++i) - serializables.Push(nodes[i]); - return serializables; -} - -/// 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. -/// The fullUpdate flag is usually set to true when the structure of the attributes are different than the existing attributes in the list. -void UpdateAttributeInspector(bool fullUpdate = true) -{ - if (inspectorLocked) - return; - - attributesDirty = false; - if (fullUpdate) - attributesFullDirty = false; - - // If full update delete all containers and add them back as necessary - if (fullUpdate) - DeleteAllContainers(); - - if (!editNodes.empty) - { - UIElement@ container = GetNodeContainer(); - - Text@ nodeTitle = container.GetChild("TitleText"); - String nodeType; - - if (editNode !is null) - { - String idStr; - if (editNode.replicated) - idStr = " (ID " + String(editNode.id) + ")"; - else - idStr = " (Local ID " + String(editNode.id) + ")"; - nodeType = editNode.typeName; - nodeTitle.text = nodeType + idStr; - LineEdit@ tagEdit = parentContainer.GetChild("TagsEdit", true); - tagEdit.text = Join(editNode.tags, ";"); - } - else - { - nodeType = editNodes[0].typeName; - nodeTitle.text = nodeType + " (ID " + STRIKED_OUT + " : " + editNodes.length + "x)"; - } - IconizeUIElement(nodeTitle, nodeType); - - ListView@ list = container.GetChild("AttributeList"); - Array nodes = ToSerializableArray(editNodes); - UpdateAttributes(nodes, list, fullUpdate); - - if (fullUpdate) - { - //\todo Avoid hardcoding - // Resize the node editor according to the number of variables, up to a certain maximum - uint maxAttrs = Clamp(list.contentElement.numChildren, MIN_NODE_ATTRIBUTES, MAX_NODE_ATTRIBUTES); - list.SetFixedHeight(maxAttrs * ATTR_HEIGHT + 2); - container.SetFixedHeight(maxAttrs * ATTR_HEIGHT + 58); - } - - // Set icon's target in the icon panel - SetAttributeEditorID(container.GetChild("ResetToDefault", true), nodes); - } - - if (!editComponents.empty) - { - uint numEditableComponents = editComponents.length / numEditableComponentsPerNode; - String multiplierText; - if (numEditableComponents > 1) - multiplierText = " (" + numEditableComponents + "x)"; - - for (uint j = 0; j < numEditableComponentsPerNode; ++j) - { - UIElement@ container = GetComponentContainer(j); - Text@ componentTitle = container.GetChild("TitleText"); - componentTitle.text = GetComponentTitle(editComponents[j * numEditableComponents]) + multiplierText; - IconizeUIElement(componentTitle, editComponents[j * numEditableComponents].typeName); - SetIconEnabledColor(componentTitle, editComponents[j * numEditableComponents].enabledEffective); - - Array components; - for (uint i = 0; i < numEditableComponents; ++i) - { - Component@ component = editComponents[j * numEditableComponents + i]; - components.Push(component); - } - - UpdateAttributes(components, container.GetChild("AttributeList"), fullUpdate); - SetAttributeEditorID(container.GetChild("ResetToDefault", true), components); - } - } - - if (!editUIElements.empty) - { - UIElement@ container = GetUIElementContainer(); - - Text@ titleText = container.GetChild("TitleText"); - DropDownList@ styleList = container.GetChild("StyleDropDown", true); - String elementType; - - if (editUIElement !is null) - { - elementType = editUIElement.typeName; - titleText.text = elementType + " [ID " + GetUIElementID(editUIElement).ToString() + "]"; - SetStyleListSelection(styleList, editUIElement.style); - LineEdit@ tagEdit = parentContainer.GetChild("TagsEdit", true); - tagEdit.text = Join(editUIElement.tags, ";"); - } - else - { - elementType = editUIElements[0].typeName; - String appliedStyle = cast(editUIElements[0]).style; - - bool sameType = true; - bool sameStyle = true; - for (uint i = 1; i < editUIElements.length; ++i) - { - if (editUIElements[i].typeName != elementType) - { - sameType = false; - sameStyle = false; - break; - } - - if (sameStyle && cast(editUIElements[i]).style != appliedStyle) - sameStyle = false; - } - titleText.text = (sameType ? elementType : "Mixed type") + " [ID " + STRIKED_OUT + " : " + editUIElements.length + "x]"; - SetStyleListSelection(SetEditable(styleList, sameStyle), sameStyle ? appliedStyle : STRIKED_OUT); - if (!sameType) - elementType.Clear(); // No icon - } - IconizeUIElement(titleText, elementType); - - UpdateAttributes(editUIElements, container.GetChild("AttributeList"), fullUpdate); - SetAttributeEditorID(container.GetChild("ResetToDefault", true), editUIElements); - } - - if (parentContainer.numChildren > 0) - UpdateAttributeInspectorIcons(); - else - { - // No editables, insert a dummy component container to show the information - Text@ titleText = GetComponentContainer(0).GetChild("TitleText"); - titleText.text = "Select editable objects"; - titleText.autoLocalizable = true; - UIElement@ panel = titleText.GetChild("IconsPanel"); - panel.visible = false; - } - - // Adjust size and position of manual-layout UI-elements, e.g. icons panel - if (fullUpdate) - HandleWindowLayoutUpdated(); -} - -/// Update the attribute list of the node container. -void UpdateNodeAttributes() -{ - bool fullUpdate = false; - UpdateAttributes(ToSerializableArray(editNodes), GetNodeContainer().GetChild("AttributeList"), fullUpdate); - if (fullUpdate) - HandleWindowLayoutUpdated(); -} - -/// Update the icons enabled color based on the internal state of the objects. -/// For node and component, based on "enabled" property. -/// For ui-element, based on "visible" property. -void UpdateAttributeInspectorIcons() -{ - if (!editNodes.empty) - { - Text@ nodeTitle = GetNodeContainer().GetChild("TitleText"); - if (editNode !is null) - SetIconEnabledColor(nodeTitle, editNode.enabled); - else if (editNodes.length > 0) - { - bool hasSameEnabledState = true; - - for (uint i = 1; i < editNodes.length; ++i) - { - if (editNodes[i].enabled != editNodes[0].enabled) - { - hasSameEnabledState = false; - break; - } - } - - SetIconEnabledColor(nodeTitle, editNodes[0].enabled, !hasSameEnabledState); - } - } - - if (!editComponents.empty) - { - uint numEditableComponents = editComponents.length / numEditableComponentsPerNode; - - for (uint j = 0; j < numEditableComponentsPerNode; ++j) - { - Text@ componentTitle = GetComponentContainer(j).GetChild("TitleText"); - - bool enabledEffective = editComponents[j * numEditableComponents].enabledEffective; - bool hasSameEnabledState = true; - for (uint i = 1; i < numEditableComponents; ++i) - { - if (editComponents[j * numEditableComponents + i].enabledEffective != enabledEffective) - { - hasSameEnabledState = false; - break; - } - } - - SetIconEnabledColor(componentTitle, enabledEffective, !hasSameEnabledState); - } - } - - if (!editUIElements.empty) - { - Text@ elementTitle = GetUIElementContainer().GetChild("TitleText"); - if (editUIElement !is null) - SetIconEnabledColor(elementTitle, editUIElement.visible); - else if (editUIElements.length > 0) - { - bool hasSameVisibleState = true; - bool visible = cast(editUIElements[0]).visible; - - for (uint i = 1; i < editUIElements.length; ++i) - { - if (cast(editUIElements[i]).visible != visible) - { - hasSameVisibleState = false; - break; - } - } - - SetIconEnabledColor(elementTitle, visible, !hasSameVisibleState); - } - } -} - -/// Return true if the edit attribute action should continue. -bool PreEditAttribute(Array@ serializables, uint index) -{ - return true; -} - -/// Call after the attribute values in the target serializables have been edited. -void PostEditAttribute(Array@ serializables, uint index, const Array& oldValues) -{ - // Create undo actions for the edits - EditActionGroup group; - for (uint i = 0; i < serializables.length; ++i) - { - EditAttributeAction action; - action.Define(serializables[i], index, oldValues[i]); - group.actions.Push(action); - } - SaveEditActionGroup(group); - - // If a UI-element changing its 'Is Modal' attribute, clear the hierarchy list selection - int itemType = GetType(serializables[0]); - if (itemType == ITEM_UI_ELEMENT && serializables[0].attributeInfos[index].name == "Is Modal") - hierarchyList.ClearSelection(); - - for (uint i = 0; i < serializables.length; ++i) - { - PostEditAttribute(serializables[i], index); - if (itemType == ITEM_UI_ELEMENT) - SetUIElementModified(serializables[i]); - } - - if (itemType != ITEM_UI_ELEMENT) - SetSceneModified(); -} - -/// Call after the attribute values in the target serializables have been edited. -void PostEditAttribute(Serializable@ serializable, uint index) -{ - // If a StaticModel/AnimatedModel/Skybox model was changed, apply a possibly different material list - if (applyMaterialList && serializable.attributeInfos[index].name == "Model") - { - StaticModel@ staticModel = cast(serializable); - if (staticModel !is null) - staticModel.ApplyMaterialList(); - } - - // If a CollisionShape changed the shape type to trimesh or convex, and a collision model is not set, - // try to get it from a StaticModel in the same node - if (serializable.typeName == "CollisionShape" && serializable.attributeInfos[index].name == "Shape Type") - { - int shapeType = serializable.GetAttribute("Shape Type").GetI32(); - if ((shapeType == 6 || shapeType == 7) && serializable.GetAttribute("CustomGeometry ComponentID").GetI32() == 0 && - serializable.GetAttribute("Model").GetResourceRef().name.Trimmed().length == 0) - { - Node@ ownerNode = cast(serializable).node; - if (ownerNode !is null) - { - StaticModel@ staticModel = ownerNode.GetComponent("StaticModel"); - if (staticModel !is null) - { - serializable.SetAttribute("Model", staticModel.GetAttribute("Model")); - serializable.ApplyAttributes(); - } - } - } - } - -} - -/// Store the IDs of the actual serializable objects into user-defined variable of the 'attribute editor' (e.g. line-edit, drop-down-list, etc). -void SetAttributeEditorID(UIElement@ attrEdit, Array@ serializables) -{ - if (serializables is null || serializables.length == 0) - return; - - // All target serializables must be either nodes, ui-elements, or components - Array ids; - switch (GetType(serializables[0])) - { - case ITEM_NODE: - for (uint i = 0; i < serializables.length; ++i) - ids.Push(cast(serializables[i]).id); - attrEdit.vars[NODE_IDS_VAR] = ids; - break; - - case ITEM_COMPONENT: - for (uint i = 0; i < serializables.length; ++i) - ids.Push(cast(serializables[i]).id); - attrEdit.vars[COMPONENT_IDS_VAR] = ids; - break; - - case ITEM_UI_ELEMENT: - for (uint i = 0; i < serializables.length; ++i) - ids.Push(GetUIElementID(cast(serializables[i]))); - attrEdit.vars[UI_ELEMENT_IDS_VAR] = ids; - break; - - default: - break; - } -} - -/// Return the actual serializable objects based on the IDs stored in the user-defined variable of the 'attribute editor'. -Array@ GetAttributeEditorTargets(UIElement@ attrEdit) -{ - Array ret; - Variant variant = attrEdit.GetVar(NODE_IDS_VAR); - if (!variant.empty) - { - Array@ ids = variant.GetVariantVector(); - for (uint i = 0; i < ids.length; ++i) - { - Node@ node = editorScene.GetNode(ids[i].GetU32()); - if (node !is null) - ret.Push(node); - } - } - else - { - variant = attrEdit.GetVar(COMPONENT_IDS_VAR); - if (!variant.empty) - { - Array@ ids = variant.GetVariantVector(); - for (uint i = 0; i < ids.length; ++i) - { - Component@ component = editorScene.GetComponent(ids[i].GetU32()); - if (component !is null) - ret.Push(component); - } - } - else - { - variant = attrEdit.GetVar(UI_ELEMENT_IDS_VAR); - if (!variant.empty) - { - Array@ ids = variant.GetVariantVector(); - for (uint i = 0; i < ids.length; ++i) - { - UIElement@ element = editorUIElement.GetChild(UI_ELEMENT_ID_VAR, ids[i], true); - if (element !is null) - ret.Push(element); - } - } - } - } - - return ret; -} - -void HandleTagsEdit(StringHash eventType, VariantMap& eventData) -{ - LineEdit@ lineEdit = eventData["Element"].GetPtr(); - Array tags = lineEdit.text.Split(';'); - - if (editUIElement !is null) - { - editUIElement.RemoveAllTags(); - for (uint i = 0; i < tags.length; i++) - editUIElement.AddTag(tags[i].Trimmed()); - } - else if (editNode !is null) - { - editNode.RemoveAllTags(); - for (uint i = 0; i < tags.length; i++) - editNode.AddTag(tags[i].Trimmed()); - } -} - -void HandleTagsSelect(StringHash eventType, VariantMap& eventData) -{ - UIElement@ tagSelect = eventData["Element"].GetPtr(); - Array actions; - String Indicator = "* "; - // In first priority changes to UIElement - if (editUIElement !is null) - { - // 1. Add established tags from current editable UIElement to menu - Array elementTags = editUIElement.tags; - for (uint i = 0; i < elementTags.length; i++) - { - bool isHasTag = editUIElement.HasTag(elementTags[i]); - String taggedIndicator = (isHasTag ? Indicator : ""); - actions.Push(CreateContextMenuItem(taggedIndicator + elementTags[i], "HandleTagsMenuSelection", elementTags[i])); - } - - // 2. Add default tags - Array stdTags = defaultTags.Split(';'); - for (uint i= 0; i < stdTags.length; i++) - { - bool isHasTag = editUIElement.HasTag(stdTags[i]); - // Add this tag into menu if only Node not tadded with it yet, otherwise it showed on step 1. - if (!isHasTag) - { - String taggedIndicator = (isHasTag ? Indicator : ""); - actions.Push(CreateContextMenuItem(taggedIndicator + stdTags[i], "HandleTagsMenuSelection", stdTags[i])); - } - } - } - else if (editNode !is null) - { - // 1. Add established tags from Node to menu - Array nodeTags = editNode.tags; - for (uint i = 0; i < nodeTags.length; i++) - { - bool isHasTag = editNode.HasTag(nodeTags[i]); - String taggedIndicator = (isHasTag ? Indicator : ""); - actions.Push(CreateContextMenuItem(taggedIndicator + nodeTags[i], "HandleTagsMenuSelection", nodeTags[i])); - } - - Array sceneTags = editorScene.tags; - // 2. Add tags from Scene.tags (In this scenario Scene.tags used as storage for frequently used tags in current Scene only) - for (uint i = 0; i < sceneTags.length; i++) - { - bool isHasTag = editNode.HasTag(sceneTags[i]); - // Add this tag into menu if only Node not tadded with it yet, otherwise it showed on step 1. - if (!isHasTag) - { - String taggedIndicator = (isHasTag ? Indicator : ""); - actions.Push(CreateContextMenuItem(taggedIndicator + sceneTags[i], "HandleTagsMenuSelection", sceneTags[i])); - } - } - - // 3. Add default tags - Array stdTags = defaultTags.Split(';'); - for (uint i = 0; i < stdTags.length; i++) - { - bool isHasTag = editNode.HasTag(stdTags[i]); - // Add this tag into menu if only Node not tadded with it yet, otherwise it showed on step 1. - if (!isHasTag) - { - String taggedIndicator = (isHasTag ? Indicator : ""); - actions.Push(CreateContextMenuItem(taggedIndicator + stdTags[i], "HandleTagsMenuSelection", stdTags[i])); - } - } - } - - // if any action has been added, add also Reset and Cancel and show menu - if (actions.length > 0) - { - actions.Push(CreateContextMenuItem("Reset", "HandleTagsMenuSelection", "Reset")); - actions.Push(CreateContextMenuItem("Cancel", "HandleTagsMenuSelectionDivisor")); - ActivateContextMenu(actions); - } - -} -void HandleTagsMenuSelectionDivisor() -{ - //do nothing -} -void HandleTagsMenuSelection() -{ - Menu@ menu = GetEventSender(); - if (menu is null) - return; - - String menuSelectedTag = menu.name; - - // In first priority changes to UIElement - if (editUIElement !is null) - { - if (menuSelectedTag == "Reset") - { - editUIElement.RemoveAllTags(); - UpdateAttributeInspector(); - return; - } - - if (!editUIElement.HasTag(menuSelectedTag)) - { - editUIElement.AddTag(menuSelectedTag.Trimmed()); - } - else - { - editUIElement.RemoveTag(menuSelectedTag.Trimmed()); - } - } - else if (editNode !is null) - { - if (menuSelectedTag == "Reset") - { - editNode.RemoveAllTags(); - UpdateAttributeInspector(); - return; - } - - if (!editNode.HasTag(menuSelectedTag)) - { - editNode.AddTag(menuSelectedTag.Trimmed()); - } - else - { - editNode.RemoveTag(menuSelectedTag.Trimmed()); - } - } - - UpdateAttributeInspector(); -} - -/// Handle reset to default event, sent when reset icon in the icon-panel is clicked. -void HandleResetToDefault(StringHash eventType, VariantMap& eventData) -{ - ui.cursor.shape = CS_BUSY; - - UIElement@ button = eventData["Element"].GetPtr(); - Array@ serializables = GetAttributeEditorTargets(button); - if (serializables.empty) - return; - - // Group for storing undo actions - EditActionGroup group; - - // Reset target serializables to their default values - for (uint i = 0; i < serializables.length; ++i) - { - Serializable@ target = serializables[i]; - - ResetAttributesAction action; - action.Define(target); - group.actions.Push(action); - - target.ResetToDefault(); - if (action.targetType == ITEM_UI_ELEMENT) - { - action.SetInternalVars(target); - SetUIElementModified(target); - } - target.ApplyAttributes(); - for (uint j = 0; j < target.numAttributes; ++j) - PostEditAttribute(target, j); - } - - SaveEditActionGroup(group); - if (GetType(serializables[0]) != ITEM_UI_ELEMENT) - SetSceneModified(); - attributesFullDirty = true; -} - -/// Handle create new user-defined variable event for node target. -void CreateNodeVariable(StringHash eventType, VariantMap& eventData) -{ - if (editNodes.empty) - return; - - String newName = ExtractVariableName(eventData); - if (newName.empty) - return; - - // Create scene variable - editorScene.RegisterVar(newName); - globalVarNames[newName] = newName; - - Variant newValue = ExtractVariantType(eventData); - - // If we overwrite an existing variable, must recreate the attribute-editor(s) for the correct type - bool overwrite = false; - for (uint i = 0; i < editNodes.length; ++i) - { - overwrite = overwrite || editNodes[i].vars.Contains(newName); - editNodes[i].vars[newName] = newValue; - } - if (overwrite) - attributesFullDirty = true; - else - attributesDirty = true; -} - -/// Handle delete existing user-defined variable event for node target. -void DeleteNodeVariable(StringHash eventType, VariantMap& eventData) -{ - if (editNodes.empty) - return; - - String delName = ExtractVariableName(eventData); - if (delName.empty) - return; - - bool erased = false; - for (uint i = 0; i < editNodes.length; ++i) - { - // \todo Should first check whether var in question is editable - erased = editNodes[i].vars.Erase(delName) || erased; - } - if (erased) - { - attributesDirty = true; - // If the attribute is not defined in any other node, unregister from the scene - // to prevent it from being unnecessarily saved; the global var list will still hold it - // to keep the hash-name mapping known in case it's in use in other scenes - Array@ allChildren = editorScene.GetChildren(true); - StringHash delNameHash(delName); - bool inUse = false; - - for (uint i = 0; i < allChildren.length; ++i) - { - if (allChildren[i].vars.Contains(delNameHash)) - { - inUse = true; - break; - } - } - - if (!inUse) - editorScene.UnregisterVar(delName); - } -} - -/// Handle create new user-defined variable event for ui-element target. -void CreateUIElementVariable(StringHash eventType, VariantMap& eventData) -{ - if (editUIElements.empty) - return; - - String newName = ExtractVariableName(eventData); - if (newName.empty) - return; - - // Create UIElement variable - globalVarNames[newName] = newName; - - Variant newValue = ExtractVariantType(eventData); - - // If we overwrite an existing variable, must recreate the attribute-editor(s) for the correct type - bool overwrite = false; - for (uint i = 0; i < editUIElements.length; ++i) - { - UIElement@ element = cast(editUIElements[i]); - overwrite = overwrite || element.vars.Contains(newName); - element.vars[newName] = newValue; - } - if (overwrite) - attributesFullDirty = true; - else - attributesDirty = true; -} - -/// Handle delete existing user-defined variable event for ui-element target. -void DeleteUIElementVariable(StringHash eventType, VariantMap& eventData) -{ - if (editUIElements.empty) - return; - - String delName = ExtractVariableName(eventData); - if (delName.empty) - return; - - // Note: intentionally do not unregister the variable name here as the same variable name may still be used by other attribute list - - bool erased = false; - for (uint i = 0; i < editUIElements.length; ++i) - { - // \todo Should first check whether var in question is editable - erased = cast(editUIElements[i]).vars.Erase(delName) || erased; - } - if (erased) - attributesDirty = true; -} - -String ExtractVariableName(VariantMap& eventData) -{ - UIElement@ element = eventData["Element"].GetPtr(); - LineEdit@ nameEdit = element.parent.GetChild("VarNameEdit"); - return nameEdit.text.Trimmed(); -} - -Variant ExtractVariantType(VariantMap& eventData) -{ - DropDownList@ dropDown = eventData["Element"].GetPtr(); - switch (dropDown.selection) - { - case 0: - return int(0); - case 1: - return false; - case 2: - return float(0.0); - case 3: - return Variant(String()); - case 4: - return Variant(Vector3()); - case 5: - return Variant(Color()); - } - - return Variant(); // This should not happen -} - -/// Get back the human-readable variable name from the StringHash. -String GetVarName(StringHash hash) -{ - // First try to get it from scene - String name = editorScene.GetVarName(hash); - // Then from the global variable reverse mappings - if (name.empty && globalVarNames.Contains(hash)) - name = globalVarNames[hash].ToString(); - return name; -} - -bool inSetStyleListSelection = false; - -/// Select/highlight the matching style in the style drop-down-list based on specified style. -void SetStyleListSelection(DropDownList@ styleList, const String&in style) -{ - // Prevent infinite loop upon initial style selection - inSetStyleListSelection = true; - - uint selection = M_MAX_UNSIGNED; - String styleName = style.empty ? "auto" : style; - Array items = styleList.GetItems(); - for (uint i = 0; i < items.length; ++i) - { - Text@ element = cast(items[i]); - if (element is null) - continue; // It may be a divider - if (element.text == styleName) - { - selection = i; - break; - } - } - styleList.selection = selection; - - inSetStyleListSelection = false; -} - -/// Handle the style change of the target ui-elements event when a new style is picked from the style drop-down-list. -void HandleStyleItemSelected(StringHash eventType, VariantMap& eventData) -{ - if (inSetStyleListSelection || editUIElements.empty) - return; - - ui.cursor.shape = CS_BUSY; - - DropDownList@ styleList = eventData["Element"].GetPtr(); - Text@ text = cast(styleList.selectedItem); - if (text is null) - return; - String newStyle = text.text; - if (newStyle == "auto") - newStyle.Clear(); - - // Group for storing undo actions - EditActionGroup group; - - // Apply new style to selected UI-elements - for (uint i = 0; i < editUIElements.length; ++i) - { - UIElement@ element = editUIElements[i]; - - ApplyUIElementStyleAction action; - action.Define(element, newStyle); - group.actions.Push(action); - - // Use the Redo() to actually do the action - action.Redo(); - } - - SaveEditActionGroup(group); -} +// Urho3D editor attribute inspector window handling +#include "Editor/Scripts/AttributeEditor.as" + +Window@ attributeInspectorWindow; +UIElement@ parentContainer; +UIElement@ inspectorLockButton; + +bool applyMaterialList = true; +bool attributesDirty = false; +bool attributesFullDirty = false; + +const String STRIKED_OUT = "——"; // Two unicode EM DASH (U+2014) +const StringHash NODE_IDS_VAR("NodeIDs"); +const StringHash COMPONENT_IDS_VAR("ComponentIDs"); +const StringHash UI_ELEMENT_IDS_VAR("UIElementIDs"); +const int LABEL_WIDTH = 30; + +// Constants for accessing xmlResources +Array xmlResources; +const uint ATTRIBUTE_RES = 0; +const uint VARIABLE_RES = 1; +const uint STYLE_RES = 2; +const uint TAGS_RES = 3; + +uint nodeContainerIndex = M_MAX_UNSIGNED; +uint componentContainerStartIndex = 0; +uint elementContainerIndex = M_MAX_UNSIGNED; + +// Node or UIElement hash-to-varname reverse mapping +VariantMap globalVarNames; +bool inspectorLocked = false; + +void InitXMLResources() +{ + String[] resources = { "Editor/UI/EditorInspector_Attribute.xml", "Editor/UI/EditorInspector_Variable.xml", + "Editor/UI/EditorInspector_Style.xml", "Editor/UI/EditorInspector_Tags.xml" }; + for (uint i = 0; i < resources.length; ++i) + xmlResources.Push(cache.GetResource("XMLFile", resources[i])); +} + +/// Delete all child containers in the inspector list. +void DeleteAllContainers() +{ + parentContainer.RemoveAllChildren(); + nodeContainerIndex = M_MAX_UNSIGNED; + componentContainerStartIndex = 0; + elementContainerIndex = M_MAX_UNSIGNED; +} + +/// Get container at the specified index in the inspector list, the container must be created before. +UIElement@ GetContainer(uint index) +{ + return parentContainer.children[index]; +} + +/// Get node container in the inspector list, create the container if it is not yet available. +UIElement@ GetNodeContainer() +{ + if (nodeContainerIndex != M_MAX_UNSIGNED) + return GetContainer(nodeContainerIndex); + + nodeContainerIndex = parentContainer.numChildren; + parentContainer.LoadChildXML(xmlResources[ATTRIBUTE_RES], uiStyle); + UIElement@ container = GetContainer(nodeContainerIndex); + container.LoadChildXML(xmlResources[VARIABLE_RES], uiStyle); + SubscribeToEvent(container.GetChild("ResetToDefault", true), "Released", "HandleResetToDefault"); + SubscribeToEvent(container.GetChild("NewVarDropDown", true), "ItemSelected", "CreateNodeVariable"); + SubscribeToEvent(container.GetChild("DeleteVarButton", true), "Released", "DeleteNodeVariable"); + ++componentContainerStartIndex; + + parentContainer.LoadChildXML(xmlResources[TAGS_RES], uiStyle); + parentContainer.GetChild("TagsLabel", true).SetFixedWidth(LABEL_WIDTH); + LineEdit@ tagEdit = parentContainer.GetChild("TagsEdit", true); + SubscribeToEvent(tagEdit, "TextChanged", "HandleTagsEdit"); + UIElement@ tagSelect = parentContainer.GetChild("TagsSelect", true); + SubscribeToEvent(tagSelect, "Released", "HandleTagsSelect"); + ++componentContainerStartIndex; + + return container; +} + +/// Get component container at the specified index, create the container if it is not yet available at the specified index. +UIElement@ GetComponentContainer(uint index) +{ + if (componentContainerStartIndex + index < parentContainer.numChildren) + return GetContainer(componentContainerStartIndex + index); + + UIElement@ container; + for (uint i = parentContainer.numChildren; i <= componentContainerStartIndex + index; ++i) + { + parentContainer.LoadChildXML(xmlResources[ATTRIBUTE_RES], uiStyle); + container = GetContainer(i); + SubscribeToEvent(container.GetChild("ResetToDefault", true), "Released", "HandleResetToDefault"); + } + return container; +} + +/// Get UI-element container, create the container if it is not yet available. +UIElement@ GetUIElementContainer() +{ + if (elementContainerIndex != M_MAX_UNSIGNED) + return GetContainer(elementContainerIndex); + + elementContainerIndex = parentContainer.numChildren; + parentContainer.LoadChildXML(xmlResources[ATTRIBUTE_RES], uiStyle); + parentContainer.LoadChildXML(xmlResources[TAGS_RES], uiStyle); + parentContainer.GetChild("TagsLabel", true).SetFixedWidth(LABEL_WIDTH); + UIElement@ container = GetContainer(elementContainerIndex); + container.LoadChildXML(xmlResources[VARIABLE_RES], uiStyle); + container.LoadChildXML(xmlResources[STYLE_RES], uiStyle); + DropDownList@ styleList = container.GetChild("StyleDropDown", true); + styleList.placeholderText = STRIKED_OUT; + styleList.parent.GetChild("StyleDropDownLabel").SetFixedWidth(LABEL_WIDTH); + PopulateStyleList(styleList); + SubscribeToEvent(container.GetChild("ResetToDefault", true), "Released", "HandleResetToDefault"); + SubscribeToEvent(container.GetChild("NewVarDropDown", true), "ItemSelected", "CreateUIElementVariable"); + SubscribeToEvent(container.GetChild("DeleteVarButton", true), "Released", "DeleteUIElementVariable"); + SubscribeToEvent(styleList, "ItemSelected", "HandleStyleItemSelected"); + LineEdit@ tagEdit = parentContainer.GetChild("TagsEdit", true); + SubscribeToEvent(tagEdit, "TextChanged", "HandleTagsEdit"); + UIElement@ tagSelect = parentContainer.GetChild("TagsSelect", true); + SubscribeToEvent(tagSelect, "Released", "HandleTagsSelect"); + return container; +} + +void CreateAttributeInspectorWindow() +{ + if (attributeInspectorWindow !is null) + return; + + InitResourcePicker(); + InitVectorStructs(); + InitXMLResources(); + + attributeInspectorWindow = LoadEditorUI("Editor/UI/EditorInspectorWindow.xml"); + parentContainer = attributeInspectorWindow.GetChild("ParentContainer"); + ui.root.AddChild(attributeInspectorWindow); + int height = Min(ui.root.height - 60, 500); + attributeInspectorWindow.SetSize(344, height); + attributeInspectorWindow.SetPosition(ui.root.width - 10 - attributeInspectorWindow.width, 100); + attributeInspectorWindow.opacity = uiMaxOpacity; + attributeInspectorWindow.BringToFront(); + inspectorLockButton = attributeInspectorWindow.GetChild("LockButton", true); + + UpdateAttributeInspector(); + + SubscribeToEvent(inspectorLockButton, "Pressed", "ToggleInspectorLock"); + SubscribeToEvent(attributeInspectorWindow.GetChild("CloseButton", true), "Pressed", "HideAttributeInspectorWindow"); + SubscribeToEvent(attributeInspectorWindow, "LayoutUpdated", "HandleWindowLayoutUpdated"); +} + +void DisableInspectorLock() +{ + inspectorLocked = false; + if (inspectorLockButton !is null) + inspectorLockButton.style = "Button"; + UpdateAttributeInspector(true); +} + +void EnableInspectorLock() +{ + inspectorLocked = true; + if (inspectorLockButton !is null) + inspectorLockButton.style = "ToggledButton"; +} + +void ToggleInspectorLock() +{ + if (inspectorLocked) + DisableInspectorLock(); + else + EnableInspectorLock(); +} + +bool ToggleAttributeInspectorWindow() +{ + if (attributeInspectorWindow.visible == false) + ShowAttributeInspectorWindow(); + else + HideAttributeInspectorWindow(); + return true; +} + +void ShowAttributeInspectorWindow() +{ + attributeInspectorWindow.visible = true; + attributeInspectorWindow.BringToFront(); +} + +void HideAttributeInspectorWindow() +{ + if(viewportMode == VIEWPORT_COMPACT) + return; + attributeInspectorWindow.visible = false; +} + + +/// 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). +void HandleWindowLayoutUpdated() +{ + // 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 + for (uint i = 0; i < parentContainer.numChildren; ++i) + { + UIElement@ container = GetContainer(i); + ListView@ list = container.GetChild("AttributeList"); + if (list is null) + continue; + + int width = list.width; + + // Adjust the icon panel's width + UIElement@ panel = container.GetChild("IconsPanel", true); + if (panel !is null) + panel.width = width; + + // 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 + for (uint j = 0; j < list.numChildren; ++j) + { + UIElement@ element = list.children[j]; + if (!element.internal) + { + element.SetFixedWidth(width); + UIElement@ title = container.GetChild("TitleText"); + element.position = IntVector2(0, (title.screenPosition - list.screenPosition).y); + + // Adjust icon panel's width one more time to cater for the space occupied by 'Is Enabled' check box + if (panel !is null) + panel.width = width - element.children[1].width - panel.layoutSpacing; + + break; + } + } + } +} + +Array ToSerializableArray(Array nodes) +{ + Array serializables; + for (uint i = 0; i < nodes.length; ++i) + serializables.Push(nodes[i]); + return serializables; +} + +/// 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. +/// The fullUpdate flag is usually set to true when the structure of the attributes are different than the existing attributes in the list. +void UpdateAttributeInspector(bool fullUpdate = true) +{ + if (inspectorLocked) + return; + + attributesDirty = false; + if (fullUpdate) + attributesFullDirty = false; + + // If full update delete all containers and add them back as necessary + if (fullUpdate) + DeleteAllContainers(); + + if (!editNodes.empty) + { + UIElement@ container = GetNodeContainer(); + + Text@ nodeTitle = container.GetChild("TitleText"); + String nodeType; + + if (editNode !is null) + { + String idStr; + if (editNode.replicated) + idStr = " (ID " + String(editNode.id) + ")"; + else + idStr = " (Local ID " + String(editNode.id) + ")"; + nodeType = editNode.typeName; + nodeTitle.text = nodeType + idStr; + LineEdit@ tagEdit = parentContainer.GetChild("TagsEdit", true); + tagEdit.text = Join(editNode.tags, ";"); + } + else + { + nodeType = editNodes[0].typeName; + nodeTitle.text = nodeType + " (ID " + STRIKED_OUT + " : " + editNodes.length + "x)"; + } + IconizeUIElement(nodeTitle, nodeType); + + ListView@ list = container.GetChild("AttributeList"); + Array nodes = ToSerializableArray(editNodes); + UpdateAttributes(nodes, list, fullUpdate); + + if (fullUpdate) + { + //\todo Avoid hardcoding + // Resize the node editor according to the number of variables, up to a certain maximum + uint maxAttrs = Clamp(list.contentElement.numChildren, MIN_NODE_ATTRIBUTES, MAX_NODE_ATTRIBUTES); + list.SetFixedHeight(maxAttrs * ATTR_HEIGHT + 2); + container.SetFixedHeight(maxAttrs * ATTR_HEIGHT + 58); + } + + // Set icon's target in the icon panel + SetAttributeEditorID(container.GetChild("ResetToDefault", true), nodes); + } + + if (!editComponents.empty) + { + uint numEditableComponents = editComponents.length / numEditableComponentsPerNode; + String multiplierText; + if (numEditableComponents > 1) + multiplierText = " (" + numEditableComponents + "x)"; + + for (uint j = 0; j < numEditableComponentsPerNode; ++j) + { + UIElement@ container = GetComponentContainer(j); + Text@ componentTitle = container.GetChild("TitleText"); + componentTitle.text = GetComponentTitle(editComponents[j * numEditableComponents]) + multiplierText; + IconizeUIElement(componentTitle, editComponents[j * numEditableComponents].typeName); + SetIconEnabledColor(componentTitle, editComponents[j * numEditableComponents].enabledEffective); + + Array components; + for (uint i = 0; i < numEditableComponents; ++i) + { + Component@ component = editComponents[j * numEditableComponents + i]; + components.Push(component); + } + + UpdateAttributes(components, container.GetChild("AttributeList"), fullUpdate); + SetAttributeEditorID(container.GetChild("ResetToDefault", true), components); + } + } + + if (!editUIElements.empty) + { + UIElement@ container = GetUIElementContainer(); + + Text@ titleText = container.GetChild("TitleText"); + DropDownList@ styleList = container.GetChild("StyleDropDown", true); + String elementType; + + if (editUIElement !is null) + { + elementType = editUIElement.typeName; + titleText.text = elementType + " [ID " + GetUIElementID(editUIElement).ToString() + "]"; + SetStyleListSelection(styleList, editUIElement.style); + LineEdit@ tagEdit = parentContainer.GetChild("TagsEdit", true); + tagEdit.text = Join(editUIElement.tags, ";"); + } + else + { + elementType = editUIElements[0].typeName; + String appliedStyle = cast(editUIElements[0]).style; + + bool sameType = true; + bool sameStyle = true; + for (uint i = 1; i < editUIElements.length; ++i) + { + if (editUIElements[i].typeName != elementType) + { + sameType = false; + sameStyle = false; + break; + } + + if (sameStyle && cast(editUIElements[i]).style != appliedStyle) + sameStyle = false; + } + titleText.text = (sameType ? elementType : "Mixed type") + " [ID " + STRIKED_OUT + " : " + editUIElements.length + "x]"; + SetStyleListSelection(SetEditable(styleList, sameStyle), sameStyle ? appliedStyle : STRIKED_OUT); + if (!sameType) + elementType.Clear(); // No icon + } + IconizeUIElement(titleText, elementType); + + UpdateAttributes(editUIElements, container.GetChild("AttributeList"), fullUpdate); + SetAttributeEditorID(container.GetChild("ResetToDefault", true), editUIElements); + } + + if (parentContainer.numChildren > 0) + UpdateAttributeInspectorIcons(); + else + { + // No editables, insert a dummy component container to show the information + Text@ titleText = GetComponentContainer(0).GetChild("TitleText"); + titleText.text = "Select editable objects"; + titleText.autoLocalizable = true; + UIElement@ panel = titleText.GetChild("IconsPanel"); + panel.visible = false; + } + + // Adjust size and position of manual-layout UI-elements, e.g. icons panel + if (fullUpdate) + HandleWindowLayoutUpdated(); +} + +/// Update the attribute list of the node container. +void UpdateNodeAttributes() +{ + bool fullUpdate = false; + UpdateAttributes(ToSerializableArray(editNodes), GetNodeContainer().GetChild("AttributeList"), fullUpdate); + if (fullUpdate) + HandleWindowLayoutUpdated(); +} + +/// Update the icons enabled color based on the internal state of the objects. +/// For node and component, based on "enabled" property. +/// For ui-element, based on "visible" property. +void UpdateAttributeInspectorIcons() +{ + if (!editNodes.empty) + { + Text@ nodeTitle = GetNodeContainer().GetChild("TitleText"); + if (editNode !is null) + SetIconEnabledColor(nodeTitle, editNode.enabled); + else if (editNodes.length > 0) + { + bool hasSameEnabledState = true; + + for (uint i = 1; i < editNodes.length; ++i) + { + if (editNodes[i].enabled != editNodes[0].enabled) + { + hasSameEnabledState = false; + break; + } + } + + SetIconEnabledColor(nodeTitle, editNodes[0].enabled, !hasSameEnabledState); + } + } + + if (!editComponents.empty) + { + uint numEditableComponents = editComponents.length / numEditableComponentsPerNode; + + for (uint j = 0; j < numEditableComponentsPerNode; ++j) + { + Text@ componentTitle = GetComponentContainer(j).GetChild("TitleText"); + + bool enabledEffective = editComponents[j * numEditableComponents].enabledEffective; + bool hasSameEnabledState = true; + for (uint i = 1; i < numEditableComponents; ++i) + { + if (editComponents[j * numEditableComponents + i].enabledEffective != enabledEffective) + { + hasSameEnabledState = false; + break; + } + } + + SetIconEnabledColor(componentTitle, enabledEffective, !hasSameEnabledState); + } + } + + if (!editUIElements.empty) + { + Text@ elementTitle = GetUIElementContainer().GetChild("TitleText"); + if (editUIElement !is null) + SetIconEnabledColor(elementTitle, editUIElement.visible); + else if (editUIElements.length > 0) + { + bool hasSameVisibleState = true; + bool visible = cast(editUIElements[0]).visible; + + for (uint i = 1; i < editUIElements.length; ++i) + { + if (cast(editUIElements[i]).visible != visible) + { + hasSameVisibleState = false; + break; + } + } + + SetIconEnabledColor(elementTitle, visible, !hasSameVisibleState); + } + } +} + +/// Return true if the edit attribute action should continue. +bool PreEditAttribute(Array@ serializables, uint index) +{ + return true; +} + +/// Call after the attribute values in the target serializables have been edited. +void PostEditAttribute(Array@ serializables, uint index, const Array& oldValues) +{ + // Create undo actions for the edits + EditActionGroup group; + for (uint i = 0; i < serializables.length; ++i) + { + EditAttributeAction action; + action.Define(serializables[i], index, oldValues[i]); + group.actions.Push(action); + } + SaveEditActionGroup(group); + + // If a UI-element changing its 'Is Modal' attribute, clear the hierarchy list selection + int itemType = GetType(serializables[0]); + if (itemType == ITEM_UI_ELEMENT && serializables[0].attributeInfos[index].name == "Is Modal") + hierarchyList.ClearSelection(); + + for (uint i = 0; i < serializables.length; ++i) + { + PostEditAttribute(serializables[i], index); + if (itemType == ITEM_UI_ELEMENT) + SetUIElementModified(serializables[i]); + } + + if (itemType != ITEM_UI_ELEMENT) + SetSceneModified(); +} + +/// Call after the attribute values in the target serializables have been edited. +void PostEditAttribute(Serializable@ serializable, uint index) +{ + // If a StaticModel/AnimatedModel/Skybox model was changed, apply a possibly different material list + if (applyMaterialList && serializable.attributeInfos[index].name == "Model") + { + StaticModel@ staticModel = cast(serializable); + if (staticModel !is null) + staticModel.ApplyMaterialList(); + } + + // If a CollisionShape changed the shape type to trimesh or convex, and a collision model is not set, + // try to get it from a StaticModel in the same node + if (serializable.typeName == "CollisionShape" && serializable.attributeInfos[index].name == "Shape Type") + { + int shapeType = serializable.GetAttribute("Shape Type").GetI32(); + if ((shapeType == 6 || shapeType == 7) && serializable.GetAttribute("CustomGeometry ComponentID").GetI32() == 0 && + serializable.GetAttribute("Model").GetResourceRef().name.Trimmed().length == 0) + { + Node@ ownerNode = cast(serializable).node; + if (ownerNode !is null) + { + StaticModel@ staticModel = ownerNode.GetComponent("StaticModel"); + if (staticModel !is null) + { + serializable.SetAttribute("Model", staticModel.GetAttribute("Model")); + serializable.ApplyAttributes(); + } + } + } + } + +} + +/// Store the IDs of the actual serializable objects into user-defined variable of the 'attribute editor' (e.g. line-edit, drop-down-list, etc). +void SetAttributeEditorID(UIElement@ attrEdit, Array@ serializables) +{ + if (serializables is null || serializables.length == 0) + return; + + // All target serializables must be either nodes, ui-elements, or components + Array ids; + switch (GetType(serializables[0])) + { + case ITEM_NODE: + for (uint i = 0; i < serializables.length; ++i) + ids.Push(cast(serializables[i]).id); + attrEdit.vars[NODE_IDS_VAR] = ids; + break; + + case ITEM_COMPONENT: + for (uint i = 0; i < serializables.length; ++i) + ids.Push(cast(serializables[i]).id); + attrEdit.vars[COMPONENT_IDS_VAR] = ids; + break; + + case ITEM_UI_ELEMENT: + for (uint i = 0; i < serializables.length; ++i) + ids.Push(GetUIElementID(cast(serializables[i]))); + attrEdit.vars[UI_ELEMENT_IDS_VAR] = ids; + break; + + default: + break; + } +} + +/// Return the actual serializable objects based on the IDs stored in the user-defined variable of the 'attribute editor'. +Array@ GetAttributeEditorTargets(UIElement@ attrEdit) +{ + Array ret; + Variant variant = attrEdit.GetVar(NODE_IDS_VAR); + if (!variant.empty) + { + Array@ ids = variant.GetVariantVector(); + for (uint i = 0; i < ids.length; ++i) + { + Node@ node = editorScene.GetNode(ids[i].GetU32()); + if (node !is null) + ret.Push(node); + } + } + else + { + variant = attrEdit.GetVar(COMPONENT_IDS_VAR); + if (!variant.empty) + { + Array@ ids = variant.GetVariantVector(); + for (uint i = 0; i < ids.length; ++i) + { + Component@ component = editorScene.GetComponent(ids[i].GetU32()); + if (component !is null) + ret.Push(component); + } + } + else + { + variant = attrEdit.GetVar(UI_ELEMENT_IDS_VAR); + if (!variant.empty) + { + Array@ ids = variant.GetVariantVector(); + for (uint i = 0; i < ids.length; ++i) + { + UIElement@ element = editorUIElement.GetChild(UI_ELEMENT_ID_VAR, ids[i], true); + if (element !is null) + ret.Push(element); + } + } + } + } + + return ret; +} + +void HandleTagsEdit(StringHash eventType, VariantMap& eventData) +{ + LineEdit@ lineEdit = eventData["Element"].GetPtr(); + Array tags = lineEdit.text.Split(';'); + + if (editUIElement !is null) + { + editUIElement.RemoveAllTags(); + for (uint i = 0; i < tags.length; i++) + editUIElement.AddTag(tags[i].Trimmed()); + } + else if (editNode !is null) + { + editNode.RemoveAllTags(); + for (uint i = 0; i < tags.length; i++) + editNode.AddTag(tags[i].Trimmed()); + } +} + +void HandleTagsSelect(StringHash eventType, VariantMap& eventData) +{ + UIElement@ tagSelect = eventData["Element"].GetPtr(); + Array actions; + String Indicator = "* "; + // In first priority changes to UIElement + if (editUIElement !is null) + { + // 1. Add established tags from current editable UIElement to menu + Array elementTags = editUIElement.tags; + for (uint i = 0; i < elementTags.length; i++) + { + bool isHasTag = editUIElement.HasTag(elementTags[i]); + String taggedIndicator = (isHasTag ? Indicator : ""); + actions.Push(CreateContextMenuItem(taggedIndicator + elementTags[i], "HandleTagsMenuSelection", elementTags[i])); + } + + // 2. Add default tags + Array stdTags = defaultTags.Split(';'); + for (uint i= 0; i < stdTags.length; i++) + { + bool isHasTag = editUIElement.HasTag(stdTags[i]); + // Add this tag into menu if only Node not tadded with it yet, otherwise it showed on step 1. + if (!isHasTag) + { + String taggedIndicator = (isHasTag ? Indicator : ""); + actions.Push(CreateContextMenuItem(taggedIndicator + stdTags[i], "HandleTagsMenuSelection", stdTags[i])); + } + } + } + else if (editNode !is null) + { + // 1. Add established tags from Node to menu + Array nodeTags = editNode.tags; + for (uint i = 0; i < nodeTags.length; i++) + { + bool isHasTag = editNode.HasTag(nodeTags[i]); + String taggedIndicator = (isHasTag ? Indicator : ""); + actions.Push(CreateContextMenuItem(taggedIndicator + nodeTags[i], "HandleTagsMenuSelection", nodeTags[i])); + } + + Array sceneTags = editorScene.tags; + // 2. Add tags from Scene.tags (In this scenario Scene.tags used as storage for frequently used tags in current Scene only) + for (uint i = 0; i < sceneTags.length; i++) + { + bool isHasTag = editNode.HasTag(sceneTags[i]); + // Add this tag into menu if only Node not tadded with it yet, otherwise it showed on step 1. + if (!isHasTag) + { + String taggedIndicator = (isHasTag ? Indicator : ""); + actions.Push(CreateContextMenuItem(taggedIndicator + sceneTags[i], "HandleTagsMenuSelection", sceneTags[i])); + } + } + + // 3. Add default tags + Array stdTags = defaultTags.Split(';'); + for (uint i = 0; i < stdTags.length; i++) + { + bool isHasTag = editNode.HasTag(stdTags[i]); + // Add this tag into menu if only Node not tadded with it yet, otherwise it showed on step 1. + if (!isHasTag) + { + String taggedIndicator = (isHasTag ? Indicator : ""); + actions.Push(CreateContextMenuItem(taggedIndicator + stdTags[i], "HandleTagsMenuSelection", stdTags[i])); + } + } + } + + // if any action has been added, add also Reset and Cancel and show menu + if (actions.length > 0) + { + actions.Push(CreateContextMenuItem("Reset", "HandleTagsMenuSelection", "Reset")); + actions.Push(CreateContextMenuItem("Cancel", "HandleTagsMenuSelectionDivisor")); + ActivateContextMenu(actions); + } + +} +void HandleTagsMenuSelectionDivisor() +{ + //do nothing +} +void HandleTagsMenuSelection() +{ + Menu@ menu = GetEventSender(); + if (menu is null) + return; + + String menuSelectedTag = menu.name; + + // In first priority changes to UIElement + if (editUIElement !is null) + { + if (menuSelectedTag == "Reset") + { + editUIElement.RemoveAllTags(); + UpdateAttributeInspector(); + return; + } + + if (!editUIElement.HasTag(menuSelectedTag)) + { + editUIElement.AddTag(menuSelectedTag.Trimmed()); + } + else + { + editUIElement.RemoveTag(menuSelectedTag.Trimmed()); + } + } + else if (editNode !is null) + { + if (menuSelectedTag == "Reset") + { + editNode.RemoveAllTags(); + UpdateAttributeInspector(); + return; + } + + if (!editNode.HasTag(menuSelectedTag)) + { + editNode.AddTag(menuSelectedTag.Trimmed()); + } + else + { + editNode.RemoveTag(menuSelectedTag.Trimmed()); + } + } + + UpdateAttributeInspector(); +} + +/// Handle reset to default event, sent when reset icon in the icon-panel is clicked. +void HandleResetToDefault(StringHash eventType, VariantMap& eventData) +{ + ui.cursor.shape = CS_BUSY; + + UIElement@ button = eventData["Element"].GetPtr(); + Array@ serializables = GetAttributeEditorTargets(button); + if (serializables.empty) + return; + + // Group for storing undo actions + EditActionGroup group; + + // Reset target serializables to their default values + for (uint i = 0; i < serializables.length; ++i) + { + Serializable@ target = serializables[i]; + + ResetAttributesAction action; + action.Define(target); + group.actions.Push(action); + + target.ResetToDefault(); + if (action.targetType == ITEM_UI_ELEMENT) + { + action.SetInternalVars(target); + SetUIElementModified(target); + } + target.ApplyAttributes(); + for (uint j = 0; j < target.numAttributes; ++j) + PostEditAttribute(target, j); + } + + SaveEditActionGroup(group); + if (GetType(serializables[0]) != ITEM_UI_ELEMENT) + SetSceneModified(); + attributesFullDirty = true; +} + +/// Handle create new user-defined variable event for node target. +void CreateNodeVariable(StringHash eventType, VariantMap& eventData) +{ + if (editNodes.empty) + return; + + String newName = ExtractVariableName(eventData); + if (newName.empty) + return; + + // Create scene variable + editorScene.RegisterVar(newName); + globalVarNames[newName] = newName; + + Variant newValue = ExtractVariantType(eventData); + + // If we overwrite an existing variable, must recreate the attribute-editor(s) for the correct type + bool overwrite = false; + for (uint i = 0; i < editNodes.length; ++i) + { + overwrite = overwrite || editNodes[i].vars.Contains(newName); + editNodes[i].vars[newName] = newValue; + } + if (overwrite) + attributesFullDirty = true; + else + attributesDirty = true; +} + +/// Handle delete existing user-defined variable event for node target. +void DeleteNodeVariable(StringHash eventType, VariantMap& eventData) +{ + if (editNodes.empty) + return; + + String delName = ExtractVariableName(eventData); + if (delName.empty) + return; + + bool erased = false; + for (uint i = 0; i < editNodes.length; ++i) + { + // \todo Should first check whether var in question is editable + erased = editNodes[i].vars.Erase(delName) || erased; + } + if (erased) + { + attributesDirty = true; + // If the attribute is not defined in any other node, unregister from the scene + // to prevent it from being unnecessarily saved; the global var list will still hold it + // to keep the hash-name mapping known in case it's in use in other scenes + Array@ allChildren = editorScene.GetChildren(true); + StringHash delNameHash(delName); + bool inUse = false; + + for (uint i = 0; i < allChildren.length; ++i) + { + if (allChildren[i].vars.Contains(delNameHash)) + { + inUse = true; + break; + } + } + + if (!inUse) + editorScene.UnregisterVar(delName); + } +} + +/// Handle create new user-defined variable event for ui-element target. +void CreateUIElementVariable(StringHash eventType, VariantMap& eventData) +{ + if (editUIElements.empty) + return; + + String newName = ExtractVariableName(eventData); + if (newName.empty) + return; + + // Create UIElement variable + globalVarNames[newName] = newName; + + Variant newValue = ExtractVariantType(eventData); + + // If we overwrite an existing variable, must recreate the attribute-editor(s) for the correct type + bool overwrite = false; + for (uint i = 0; i < editUIElements.length; ++i) + { + UIElement@ element = cast(editUIElements[i]); + overwrite = overwrite || element.vars.Contains(newName); + element.vars[newName] = newValue; + } + if (overwrite) + attributesFullDirty = true; + else + attributesDirty = true; +} + +/// Handle delete existing user-defined variable event for ui-element target. +void DeleteUIElementVariable(StringHash eventType, VariantMap& eventData) +{ + if (editUIElements.empty) + return; + + String delName = ExtractVariableName(eventData); + if (delName.empty) + return; + + // Note: intentionally do not unregister the variable name here as the same variable name may still be used by other attribute list + + bool erased = false; + for (uint i = 0; i < editUIElements.length; ++i) + { + // \todo Should first check whether var in question is editable + erased = cast(editUIElements[i]).vars.Erase(delName) || erased; + } + if (erased) + attributesDirty = true; +} + +String ExtractVariableName(VariantMap& eventData) +{ + UIElement@ element = eventData["Element"].GetPtr(); + LineEdit@ nameEdit = element.parent.GetChild("VarNameEdit"); + return nameEdit.text.Trimmed(); +} + +Variant ExtractVariantType(VariantMap& eventData) +{ + DropDownList@ dropDown = eventData["Element"].GetPtr(); + switch (dropDown.selection) + { + case 0: + return int(0); + case 1: + return false; + case 2: + return float(0.0); + case 3: + return Variant(String()); + case 4: + return Variant(Vector3()); + case 5: + return Variant(Color()); + } + + return Variant(); // This should not happen +} + +/// Get back the human-readable variable name from the StringHash. +String GetVarName(StringHash hash) +{ + // First try to get it from scene + String name = editorScene.GetVarName(hash); + // Then from the global variable reverse mappings + if (name.empty && globalVarNames.Contains(hash)) + name = globalVarNames[hash].ToString(); + return name; +} + +bool inSetStyleListSelection = false; + +/// Select/highlight the matching style in the style drop-down-list based on specified style. +void SetStyleListSelection(DropDownList@ styleList, const String&in style) +{ + // Prevent infinite loop upon initial style selection + inSetStyleListSelection = true; + + uint selection = M_MAX_UNSIGNED; + String styleName = style.empty ? "auto" : style; + Array items = styleList.GetItems(); + for (uint i = 0; i < items.length; ++i) + { + Text@ element = cast(items[i]); + if (element is null) + continue; // It may be a divider + if (element.text == styleName) + { + selection = i; + break; + } + } + styleList.selection = selection; + + inSetStyleListSelection = false; +} + +/// Handle the style change of the target ui-elements event when a new style is picked from the style drop-down-list. +void HandleStyleItemSelected(StringHash eventType, VariantMap& eventData) +{ + if (inSetStyleListSelection || editUIElements.empty) + return; + + ui.cursor.shape = CS_BUSY; + + DropDownList@ styleList = eventData["Element"].GetPtr(); + Text@ text = cast(styleList.selectedItem); + if (text is null) + return; + String newStyle = text.text; + if (newStyle == "auto") + newStyle.Clear(); + + // Group for storing undo actions + EditActionGroup group; + + // Apply new style to selected UI-elements + for (uint i = 0; i < editUIElements.length; ++i) + { + UIElement@ element = editUIElements[i]; + + ApplyUIElementStyleAction action; + action.Define(element, newStyle); + group.actions.Push(action); + + // Use the Redo() to actually do the action + action.Redo(); + } + + SaveEditActionGroup(group); +} diff --git a/bin/Data/Scripts/Editor/EditorLayers.as b/bin/EditorData/Editor/Scripts/EditorLayers.as similarity index 80% rename from bin/Data/Scripts/Editor/EditorLayers.as rename to bin/EditorData/Editor/Scripts/EditorLayers.as index ba53b9e4708..e674af57eb2 100644 --- a/bin/Data/Scripts/Editor/EditorLayers.as +++ b/bin/EditorData/Editor/Scripts/EditorLayers.as @@ -25,18 +25,18 @@ void CreateLayerEditor() if (layerWindow !is null) return; - layerWindow = LoadEditorUI("UI/EditorLayersWindow.xml"); + layerWindow = LoadEditorUI("Editor/UI/EditorLayersWindow.xml"); ui.root.AddChild(layerWindow); layerWindow.opacity = uiMaxOpacity; - + HideLayerEditor(); - + bits.Resize(MAX_BITMASK_BITS); - + DropDownList@ EditMaskModeList = layerWindow.GetChild("LayerModeEdit", true); SubscribeToEvent(EditMaskModeList, "ItemSelected", "HandleLayerModeEdit"); - - for (int i=0; i < MAX_BITMASK_BITS; i++) + + for (int i=0; i < MAX_BITMASK_BITS; i++) { bits[i] = layerWindow.GetChild("Bit" + String(i), true); bits[i].vars["index"] = i; @@ -47,13 +47,13 @@ void CreateLayerEditor() bool ShowLayerEditor() { // avoid to show layer window when we type text in LineEdit - if (ui.focusElement !is null && ui.focusElement.type == lineEditType && lastSelectedNode.Get() is null) + if (ui.focusElement !is null && ui.focusElement.type == lineEditType && lastSelectedNode.Get() is null) return false; - + // to avoid when we close dialog with selected other node - Node@ node = lastSelectedNode.Get(); + Node@ node = lastSelectedNode.Get(); patternMaskNode = node; - + // just change position if already opened if (layerWindow.visible == true) { @@ -66,14 +66,14 @@ bool ShowLayerEditor() */ return true; } - + // to prevent manipulation until we change mask for one or group nodes previousEdit = editMode; editMode = EDIT_SELECT; - + // get mask type from pattern node EstablishSelectedNodeBitMaskToPanel(); - + layerWindowPosition = ui.cursorPosition; layerWindow.position = layerWindowPosition; layerWindowPosition.x += layerWindow.width / 2; @@ -85,32 +85,32 @@ bool ShowLayerEditor() void HideLayerEditor() { - layerWindow.visible = false; + layerWindow.visible = false; editMode = previousEdit; } -void EstablishSelectedNodeBitMaskToPanel() +void EstablishSelectedNodeBitMaskToPanel() { if (selectedNodes.length < 1) return; Node@ node; node = patternMaskNode; - - if (node !is null) + + if (node !is null) { // find first drawable to get mask Array components = node.GetComponents(); - Drawable@ firstDrawableInNode; - if (components.length > 0 ) + Drawable@ firstDrawableInNode; + if (components.length > 0 ) { firstDrawableInNode = cast(components[0]); } - + if (firstDrawableInNode !is null) { int showMask = 0; - - switch (editMaskType) + + switch (editMaskType) { case EDIT_VIEW_MASK: showMask = firstDrawableInNode.viewMask; @@ -125,17 +125,17 @@ void EstablishSelectedNodeBitMaskToPanel() showMask = firstDrawableInNode.zoneMask; break; } - + SetupBitsPanel(showMask); } } } -void SetupBitsPanel(int mask) +void SetupBitsPanel(int mask) { - for (int i = 0; i < 8; i++) + for (int i = 0; i < 8; i++) { - if ((1 << i) & mask != 0) + if ((1 << i) & mask != 0) { bits[i].checked = true; } @@ -146,23 +146,23 @@ void SetupBitsPanel(int mask) } } -void ChangeNodeViewMask(Node@ node, EditActionGroup@ group, int mask) +void ChangeNodeViewMask(Node@ node, EditActionGroup@ group, int mask) { Array components = node.GetComponents(); - if (components.length > 0) + if (components.length > 0) { - for (uint componentIndex = 0; componentIndex < components.length; componentIndex++) + for (uint componentIndex = 0; componentIndex < components.length; componentIndex++) { Component@ component = components[componentIndex]; Drawable@ drawable = cast(component); - if (drawable !is null) - { + if (drawable !is null) + { // Save before modification CreateDrawableMaskAction action; action.Define(drawable, editMaskType); group.actions.Push(action); - - switch (editMaskType) + + switch (editMaskType) { case EDIT_VIEW_MASK: drawable.viewMask = mask; @@ -176,47 +176,47 @@ void ChangeNodeViewMask(Node@ node, EditActionGroup@ group, int mask) case EDIT_ZONE_MASK: drawable.zoneMask = mask; break; - } - } + } + } } } } -void EstablishBitMaskToSelectedNodes() +void EstablishBitMaskToSelectedNodes() { if (selectedNodes.length < 1) return; - + int maskTypeSelected = 0; - + // Group for storing undo actions EditActionGroup group; - - for (uint indexNode = 0; indexNode < selectedNodes.length; indexNode++) + + for (uint indexNode = 0; indexNode < selectedNodes.length; indexNode++) { Node@ node = selectedNodes[indexNode]; - if (node !is null) + if (node !is null) { int mask = 0; - for (int i = 0; i < MAX_BITMASK_BITS; i++) + for (int i = 0; i < MAX_BITMASK_BITS; i++) { mask = mask | (bits[i].checked ? 1 << i : 0); } - + if (mask == MAX_BITMASK_VALUE) mask = -1; - + ChangeNodeViewMask(node, group, mask); Array children = node.GetChildren(true); - if (children.length > 0) + if (children.length > 0) { - for (uint i = 0; i < children.length; i++) + for (uint i = 0; i < children.length; i++) { ChangeNodeViewMask(children[i], group, mask); } - } + } } } - + SaveEditActionGroup(group); SetSceneModified(); } @@ -231,21 +231,21 @@ void HandleLayerModeEdit(StringHash eventType, VariantMap& eventData) void HandleMaskTypeScroll(StringHash eventType, VariantMap& eventData) { if (!layerWindow.IsInside(ui.cursorPosition, true)) return; - + DropDownList@ listView = layerWindow.GetChild("LayerModeEdit", true); editMaskType = listView.selection; - + int wheel = eventData["Wheel"].GetI32(); - - if (wheel > 0) + + if (wheel > 0) { if (editMaskType > 0) editMaskType--; } - else if (wheel < 0) + else if (wheel < 0) { if (editMaskType < 3) editMaskType++; } - + listView.selection = editMaskType; EstablishSelectedNodeBitMaskToPanel(); } @@ -253,29 +253,29 @@ void HandleMaskTypeScroll(StringHash eventType, VariantMap& eventData) void HandleHideLayerEditor(StringHash eventType, VariantMap& eventData) { if (layerWindow.visible == false) return; - + // if layer window not in focus and mouse folow away - close layer window - if ( eventType == eventTypeMouseMove) + if ( eventType == eventTypeMouseMove) { IntVector2 mousePos; mousePos.x = eventData["X"].GetI32(); mousePos.y = eventData["Y"].GetI32(); - + Vector2 a = Vector2(layerWindowPosition.x, layerWindowPosition.y); Vector2 b = Vector2(mousePos.x, mousePos.y); Vector2 dir = a - b; float distance = dir.length; - + if (distance > layerWindow.width) HideLayerEditor(); - } + } // if user click on scene - close layser window - else if (eventType == eventTypeMouseButtonDown) + else if (eventType == eventTypeMouseButtonDown) { - if (ui.focusElement is null) + if (ui.focusElement is null) { HideLayerEditor(); - } + } } } @@ -284,43 +284,43 @@ void ToggleBits(StringHash eventType, VariantMap& eventData) { if (toggleBusy) return; toggleBusy = true; - + CheckBox@ cb = cast(eventData["Element"].GetPtr()); int bitIndex = cb.vars["index"].GetI32(); - - - if (bitIndex < MAX_BITMASK_BITS) + + + if (bitIndex < MAX_BITMASK_BITS) { // batch bits invert if pressed ctrl or alt if (input.keyDown[KEY_CTRL]) { bool bit = true; bits[bitIndex].checked = bit; - - for (int i = 0; i < MAX_BITMASK_BITS; i++) + + for (int i = 0; i < MAX_BITMASK_BITS; i++) { - if (i != bitIndex) + if (i != bitIndex) { bits[i].checked = !bit; - }; + }; } } - else if (input.keyDown[KEY_ALT]) + else if (input.keyDown[KEY_ALT]) { bool bit = false; bits[bitIndex].checked = bit; - - for (int i = 0; i < MAX_BITMASK_BITS; i++) + + for (int i = 0; i < MAX_BITMASK_BITS; i++) { - if (i != bitIndex) + if (i != bitIndex) { bits[i].checked = !bit; - }; + }; } } - + EstablishBitMaskToSelectedNodes(); } - + toggleBusy = false; } diff --git a/bin/Data/Scripts/Editor/EditorMaterial.as b/bin/EditorData/Editor/Scripts/EditorMaterial.as similarity index 94% rename from bin/Data/Scripts/Editor/EditorMaterial.as rename to bin/EditorData/Editor/Scripts/EditorMaterial.as index 0020754d4b5..bc180321b23 100644 --- a/bin/Data/Scripts/Editor/EditorMaterial.as +++ b/bin/EditorData/Editor/Scripts/EditorMaterial.as @@ -1,1027 +1,1027 @@ -// Urho3D material editor - -Window@ materialWindow; -Material@ editMaterial; -XMLFile@ oldMaterialState; -bool inMaterialRefresh = true; -View3D@ materialPreview; -Scene@ previewScene; -Node@ previewCameraNode; -Node@ previewLightNode; -Light@ previewLight; -Node@ previewModelNode; -StaticModel@ previewModel; - -void CreateMaterialEditor() -{ - if (materialWindow !is null) - return; - - materialWindow = LoadEditorUI("UI/EditorMaterialWindow.xml"); - ui.root.AddChild(materialWindow); - materialWindow.opacity = uiMaxOpacity; - - InitMaterialPreview(); - InitModelPreviewList(); - RefreshMaterialEditor(); - - int height = Min(ui.root.height - 60, 600); - materialWindow.SetSize(400, height); - CenterDialog(materialWindow); - - HideMaterialEditor(); - - SubscribeToEvent(materialWindow.GetChild("NewButton", true), "Released", "NewMaterial"); - SubscribeToEvent(materialWindow.GetChild("RevertButton", true), "Released", "RevertMaterial"); - SubscribeToEvent(materialWindow.GetChild("SaveButton", true), "Released", "SaveMaterial"); - SubscribeToEvent(materialWindow.GetChild("SaveAsButton", true), "Released", "SaveMaterialAs"); - SubscribeToEvent(materialWindow.GetChild("CloseButton", true), "Released", "HideMaterialEditor"); - SubscribeToEvent(materialWindow.GetChild("NewParameterDropDown", true), "ItemSelected", "CreateShaderParameter"); - SubscribeToEvent(materialWindow.GetChild("DeleteParameterButton", true), "Released", "DeleteShaderParameter"); - SubscribeToEvent(materialWindow.GetChild("NewTechniqueButton", true), "Released", "NewTechnique"); - SubscribeToEvent(materialWindow.GetChild("DeleteTechniqueButton", true), "Released", "DeleteTechnique"); - SubscribeToEvent(materialWindow.GetChild("SortTechniquesButton", true), "Released", "SortTechniques"); - SubscribeToEvent(materialWindow.GetChild("VSDefinesEdit", true), "TextFinished", "EditVSDefines"); - SubscribeToEvent(materialWindow.GetChild("PSDefinesEdit", true), "TextFinished", "EditPSDefines"); - SubscribeToEvent(materialWindow.GetChild("ConstantBiasEdit", true), "TextChanged", "EditConstantBias"); - SubscribeToEvent(materialWindow.GetChild("ConstantBiasEdit", true), "TextFinished", "EditConstantBias"); - SubscribeToEvent(materialWindow.GetChild("SlopeBiasEdit", true), "TextChanged", "EditSlopeBias"); - SubscribeToEvent(materialWindow.GetChild("SlopeBiasEdit", true), "TextFinished", "EditSlopeBias"); - SubscribeToEvent(materialWindow.GetChild("RenderOrderEdit", true), "TextChanged", "EditRenderOrder"); - SubscribeToEvent(materialWindow.GetChild("RenderOrderEdit", true), "TextFinished", "EditRenderOrder"); - SubscribeToEvent(materialWindow.GetChild("CullModeEdit", true), "ItemSelected", "EditCullMode"); - SubscribeToEvent(materialWindow.GetChild("ShadowCullModeEdit", true), "ItemSelected", "EditShadowCullMode"); - SubscribeToEvent(materialWindow.GetChild("FillModeEdit", true), "ItemSelected", "EditFillMode"); - SubscribeToEvent(materialWindow.GetChild("OcclusionEdit", true), "Toggled", "EditOcclusion"); - SubscribeToEvent(materialWindow.GetChild("AlphaToCoverageEdit", true), "Toggled", "EditAlphaToCoverage"); - SubscribeToEvent(materialWindow.GetChild("LineAntiAliasEdit", true), "Toggled", "EditLineAntiAlias"); -} - -bool ToggleMaterialEditor() -{ - if (materialWindow.visible == false) - ShowMaterialEditor(); - else - HideMaterialEditor(); - return true; -} - -void ShowMaterialEditor() -{ - RefreshMaterialEditor(); - materialWindow.visible = true; - materialWindow.BringToFront(); -} - -void HideMaterialEditor() -{ - materialWindow.visible = false; -} - -void InitMaterialPreview() -{ - previewScene = Scene("PreviewScene"); - previewScene.CreateComponent("Octree"); - - Node@ zoneNode = previewScene.CreateChild("Zone"); - Zone@ zone = zoneNode.CreateComponent("Zone"); - zone.boundingBox = BoundingBox(-1000, 1000); - zone.ambientColor = Color(0.15, 0.15, 0.15); - zone.fogColor = Color(0, 0, 0); - zone.fogStart = 10.0; - zone.fogEnd = 100.0; - - previewCameraNode = previewScene.CreateChild("PreviewCamera"); - previewCameraNode.position = Vector3(0, 0, -1.5); - Camera@ camera = previewCameraNode.CreateComponent("Camera"); - camera.nearClip = 0.1f; - camera.farClip = 100.0f; - - previewLightNode = previewScene.CreateChild("PreviewLight"); - previewLightNode.direction = Vector3(0.5, -0.5, 0.5); - previewLight = previewLightNode.CreateComponent("Light"); - previewLight.lightType = LIGHT_DIRECTIONAL; - previewLight.specularIntensity = 0.5; - - previewModelNode = previewScene.CreateChild("PreviewModel"); - previewModelNode.rotation = Quaternion(0, 0, 0); - previewModel = previewModelNode.CreateComponent("StaticModel"); - previewModel.model = cache.GetResource("Model", "Models/Sphere.mdl"); - - materialPreview = materialWindow.GetChild("MaterialPreview", true); - materialPreview.SetFixedHeight(100); - materialPreview.SetView(previewScene, camera); - materialPreview.viewport.renderPath = renderPath; - materialPreview.autoUpdate = false; - - SubscribeToEvent(materialPreview, "DragMove", "RotateMaterialPreview"); -} - -void InitModelPreviewList() -{ - DropDownList@ modelPreview = materialWindow.GetChild("ModelPreview", true); - modelPreview.selection = 1; - SubscribeToEvent(materialWindow.GetChild("ModelPreview", true), "ItemSelected", "EditModelPreviewChange"); -} - -void EditMaterial(Material@ mat) -{ - if (editMaterial !is null) - UnsubscribeFromEvent(editMaterial, "ReloadFinished"); - - editMaterial = mat; - - if (editMaterial !is null) - SubscribeToEvent(editMaterial, "ReloadFinished", "RefreshMaterialEditor"); - - ShowMaterialEditor(); -} - -void RefreshMaterialEditor() -{ - RefreshMaterialPreview(); - RefreshMaterialName(); - RefreshMaterialTechniques(); - RefreshMaterialTextures(); - RefreshMaterialShaderParameters(); - RefreshMaterialMiscParameters(); -} - -void RefreshMaterialPreview() -{ - previewModel.material = editMaterial; - materialPreview.QueueUpdate(); -} - -void RefreshMaterialName() -{ - UIElement@ container = materialWindow.GetChild("NameContainer", true); - container.RemoveAllChildren(); - - LineEdit@ nameEdit = CreateAttributeLineEdit(container, null, 0, 0); - if (editMaterial !is null) - nameEdit.text = editMaterial.name; - SubscribeToEvent(nameEdit, "TextFinished", "EditMaterialName"); - - Button@ pickButton = CreateResourcePickerButton(container, null, 0, 0, "smallButtonPick"); - SubscribeToEvent(pickButton, "Released", "PickEditMaterial"); -} - -void RefreshMaterialTechniques(bool fullUpdate = true) -{ - ListView@ list = materialWindow.GetChild("TechniqueList", true); - - if (editMaterial is null) - return; - - if (fullUpdate == true) - { - list.RemoveAllItems(); - - for (uint i = 0; i < editMaterial.numTechniques; ++i) - { - TechniqueEntry entry = editMaterial.techniqueEntries[i]; - - UIElement@ container = UIElement(); - container.SetLayout(LM_HORIZONTAL, 4); - container.SetFixedHeight(ATTR_HEIGHT); - list.AddItem(container); - - LineEdit@ nameEdit = CreateAttributeLineEdit(container, null, i, 0); - nameEdit.name = "TechniqueNameEdit" + String(i); - - Button@ pickButton = CreateResourcePickerButton(container, null, i, 0, "smallButtonPick"); - SubscribeToEvent(pickButton, "Released", "PickMaterialTechnique"); - Button@ openButton = CreateResourcePickerButton(container, null, i, 0, "smallButtonOpen"); - SubscribeToEvent(openButton, "Released", "OpenResource"); - - if (entry.technique !is null) - nameEdit.text = entry.technique.name; - - SubscribeToEvent(nameEdit, "TextFinished", "EditMaterialTechnique"); - - UIElement@ container2 = UIElement(); - container2.SetLayout(LM_HORIZONTAL, 4); - container2.SetFixedHeight(ATTR_HEIGHT); - list.AddItem(container2); - - Text@ text = container2.CreateChild("Text"); - text.style = "EditorAttributeText"; - text.text = "Quality"; - LineEdit@ attrEdit = CreateAttributeLineEdit(container2, null, i, 0); - attrEdit.text = String(entry.qualityLevel); - SubscribeToEvent(attrEdit, "TextChanged", "EditTechniqueQuality"); - SubscribeToEvent(attrEdit, "TextFinished", "EditTechniqueQuality"); - - text = container2.CreateChild("Text"); - text.style = "EditorAttributeText"; - text.text = "LOD Distance"; - attrEdit = CreateAttributeLineEdit(container2, null, i, 0); - attrEdit.text = String(entry.lodDistance); - SubscribeToEvent(attrEdit, "TextChanged", "EditTechniqueLodDistance"); - SubscribeToEvent(attrEdit, "TextFinished", "EditTechniqueLodDistance"); - } - } - else - { - for (uint i = 0; i < editMaterial.numTechniques; ++i) - { - TechniqueEntry entry = editMaterial.techniqueEntries[i]; - - LineEdit@ nameEdit = materialWindow.GetChild("TechniqueNameEdit" + String(i), true); - if (nameEdit is null) - continue; - - nameEdit.text = entry.technique !is null ? entry.technique.name : ""; - } - } -} - -void RefreshMaterialTextures(bool fullUpdate = true) -{ - if (fullUpdate) - { - ListView@ list = materialWindow.GetChild("TextureList", true); - list.RemoveAllItems(); - - for (uint i = 0; i < MAX_MATERIAL_TEXTURE_UNITS; ++i) - { - String tuName = Material::GetTextureUnitName(TextureUnit(i)); - tuName[0] = ToUpper(tuName[0]); - - UIElement@ parent = CreateAttributeEditorParentWithSeparatedLabel(list, "Unit " + i + " " + tuName, i, 0, false); - - UIElement@ container = UIElement(); - container.SetLayout(LM_HORIZONTAL, 4, IntRect(10, 0, 4, 0)); - container.SetFixedHeight(ATTR_HEIGHT); - parent.AddChild(container); - - LineEdit@ nameEdit = CreateAttributeLineEdit(container, null, i, 0); - nameEdit.name = "TextureNameEdit" + String(i); - - Button@ pickButton = CreateResourcePickerButton(container, null, i, 0, "smallButtonPick"); - SubscribeToEvent(pickButton, "Released", "PickMaterialTexture"); - Button@ openButton = CreateResourcePickerButton(container, null, i, 0, "smallButtonOpen"); - SubscribeToEvent(openButton, "Released", "OpenResource"); - - if (editMaterial !is null) - { - Texture@ texture = editMaterial.textures[TextureUnit(i)]; - if (texture !is null) - nameEdit.text = texture.name; - } - - SubscribeToEvent(nameEdit, "TextFinished", "EditMaterialTexture"); - } - } - else - { - for (uint i = 0; i < MAX_MATERIAL_TEXTURE_UNITS; ++i) - { - LineEdit@ nameEdit = materialWindow.GetChild("TextureNameEdit" + String(i), true); - if (nameEdit is null) - continue; - - String textureName; - if (editMaterial !is null) - { - Texture@ texture = editMaterial.textures[TextureUnit(i)]; - if (texture !is null) - textureName = texture.name; - } - - nameEdit.text = textureName; - } - } -} - -void RefreshMaterialShaderParameters() -{ - ListView@ list = materialWindow.GetChild("ShaderParameterList", true); - list.RemoveAllItems(); - if (editMaterial is null) - return; - - Array@ parameterNames = editMaterial.shaderParameterNames; - - for (uint i = 0; i < parameterNames.length; ++i) - { - VariantType type = editMaterial.shaderParameters[parameterNames[i]].type; - Variant value = editMaterial.shaderParameters[parameterNames[i]]; - UIElement@ parent = CreateAttributeEditorParent(list, parameterNames[i], 0, 0); - uint numCoords = 1; - if (type >= VAR_VECTOR2 && type <= VAR_VECTOR4) - numCoords = type - VAR_FLOAT + 1; - - Array coordValues = value.ToString().Split(' '); - - for (uint j = 0; j < numCoords; ++j) - { - LineEdit@ attrEdit = CreateAttributeLineEdit(parent, null, 0, 0); - attrEdit.vars["Coordinate"] = j; - attrEdit.vars["Name"] = parameterNames[i]; - attrEdit.text = coordValues[j]; - - CreateDragSlider(attrEdit); - - SubscribeToEvent(attrEdit, "TextChanged", "EditShaderParameter"); - SubscribeToEvent(attrEdit, "TextFinished", "EditShaderParameter"); - } - } -} - -void RefreshMaterialMiscParameters() -{ - if (editMaterial is null) - return; - - inMaterialRefresh = true; - - BiasParameters bias = editMaterial.depthBias; - LineEdit@ attrEdit = materialWindow.GetChild("ConstantBiasEdit", true); - attrEdit.text = String(bias.constantBias); - attrEdit = materialWindow.GetChild("SlopeBiasEdit", true); - attrEdit.text = String(bias.slopeScaledBias); - attrEdit = materialWindow.GetChild("RenderOrderEdit", true); - attrEdit.text = String(int(editMaterial.renderOrder)); - attrEdit = materialWindow.GetChild("VSDefinesEdit", true); - attrEdit.text = editMaterial.vertexShaderDefines; - attrEdit = materialWindow.GetChild("PSDefinesEdit", true); - attrEdit.text = editMaterial.pixelShaderDefines; - - DropDownList@ attrList = materialWindow.GetChild("CullModeEdit", true); - attrList.selection = editMaterial.cullMode; - attrList = materialWindow.GetChild("ShadowCullModeEdit", true); - attrList.selection = editMaterial.shadowCullMode; - attrList = materialWindow.GetChild("FillModeEdit", true); - attrList.selection = editMaterial.fillMode; - - CheckBox@ attrCheckBox = materialWindow.GetChild("OcclusionEdit", true); - attrCheckBox.checked = editMaterial.occlusion; - attrCheckBox = materialWindow.GetChild("AlphaToCoverageEdit", true); - attrCheckBox.checked = editMaterial.alphaToCoverage; - attrCheckBox = materialWindow.GetChild("LineAntiAliasEdit", true); - attrCheckBox.checked = editMaterial.lineAntiAlias; - - inMaterialRefresh = false; -} - -void RotateMaterialPreview(StringHash eventType, VariantMap& eventData) -{ - int elemX = eventData["ElementX"].GetI32(); - int elemY = eventData["ElementY"].GetI32(); - - if (materialPreview.height > 0 && materialPreview.width > 0) - { - float yaw = ((materialPreview.height / 2) - elemY) * (90.0 / materialPreview.height); - float pitch = ((materialPreview.width / 2) - elemX) * (90.0 / materialPreview.width); - - previewModelNode.rotation = previewModelNode.rotation.Slerp(Quaternion(yaw, pitch, 0), 0.1); - materialPreview.QueueUpdate(); - } -} - -void EditMaterialName(StringHash eventType, VariantMap& eventData) -{ - LineEdit@ nameEdit = eventData["Element"].GetPtr(); - String newMaterialName = nameEdit.text.Trimmed(); - if (!newMaterialName.empty) - { - Material@ newMaterial = cache.GetResource("Material", newMaterialName); - if (newMaterial !is null) - EditMaterial(newMaterial); - } -} - -void PickEditMaterial() -{ - @resourcePicker = GetResourcePicker(StringHash("Material")); - if (resourcePicker is null) - return; - - String lastPath = resourcePicker.lastPath; - if (lastPath.empty) - lastPath = sceneResourcePath; - CreateFileSelector(localization.Get("Pick ") + resourcePicker.typeName, "OK", "Cancel", lastPath, resourcePicker.filters, resourcePicker.lastFilter, false); - SubscribeToEvent(uiFileSelector, "FileSelected", "PickEditMaterialDone"); -} - -void PickEditMaterialDone(StringHash eventType, VariantMap& eventData) -{ - StoreResourcePickerPath(); - CloseFileSelector(); - - if (!eventData["OK"].GetBool()) - { - @resourcePicker = null; - return; - } - - String resourceName = eventData["FileName"].GetString(); - Resource@ res = GetPickedResource(resourceName); - - if (res !is null) - EditMaterial(cast(res)); - - @resourcePicker = null; -} - -void NewMaterial() -{ - EditMaterial(Material()); -} - -void RevertMaterial() -{ - if (editMaterial is null) - return; - - BeginMaterialEdit(); - cache.ReloadResource(editMaterial); - EndMaterialEdit(); - - RefreshMaterialEditor(); -} - -void SaveMaterial() -{ - if (editMaterial is null || editMaterial.name.empty) - return; - - String fullName = cache.GetResourceFileName(editMaterial.name); - if (fullName.empty) - return; - - MakeBackup(fullName); - File saveFile(fullName, FILE_WRITE); - bool success; - if (GetExtension(fullName) == ".json") - { - JSONFile json; - editMaterial.Save(json.root); - success = json.Save(saveFile); - } - else - success = editMaterial.Save(saveFile); - RemoveBackup(success, fullName); -} - -void SaveMaterialAs() -{ - if (editMaterial is null) - return; - - @resourcePicker = GetResourcePicker(StringHash("Material")); - if (resourcePicker is null) - return; - - String lastPath = resourcePicker.lastPath; - if (lastPath.empty) - lastPath = sceneResourcePath; - CreateFileSelector("Save material as", "Save", "Cancel", lastPath, resourcePicker.filters, resourcePicker.lastFilter); - SubscribeToEvent(uiFileSelector, "FileSelected", "SaveMaterialAsDone"); -} - -void SaveMaterialAsDone(StringHash eventType, VariantMap& eventData) -{ - StoreResourcePickerPath(); - CloseFileSelector(); - @resourcePicker = null; - - if (editMaterial is null) - return; - - if (!eventData["OK"].GetBool()) - { - @resourcePicker = null; - return; - } - - String fullName = eventData["FileName"].GetString(); - - // Add default extension for saving if not specified - String filter = eventData["Filter"].GetString(); - if (GetExtension(fullName).empty && filter != "*.*") - fullName = fullName + filter.Substring(1); - - MakeBackup(fullName); - File saveFile(fullName, FILE_WRITE); - bool success; - if (GetExtension(fullName) == ".json") - { - JSONFile json; - editMaterial.Save(json.root); - success = json.Save(saveFile); - } - else - success = editMaterial.Save(saveFile); - - if (success) - { - saveFile.Close(); - RemoveBackup(true, fullName); - - // Load the new resource to update the name in the editor - Material@ newMat = cache.GetResource("Material", GetResourceNameFromFullName(fullName)); - if (newMat !is null) - EditMaterial(newMat); - } -} - -void EditModelPreviewChange(StringHash eventType, VariantMap& eventData) -{ - if (materialPreview is null) - return; - - previewModelNode.scale = Vector3(1.0, 1.0, 1.0); - - DropDownList@ element = eventData["Element"].GetPtr(); - - switch (element.selection) - { - case 0: - previewModel.model = cache.GetResource("Model", "Models/Box.mdl"); - break; - case 1: - previewModel.model = cache.GetResource("Model", "Models/Sphere.mdl"); - break; - case 2: - previewModel.model = cache.GetResource("Model", "Models/Plane.mdl"); - break; - case 3: - previewModel.model = cache.GetResource("Model", "Models/Cylinder.mdl"); - previewModelNode.scale = Vector3(0.8, 0.8, 0.8); - break; - case 4: - previewModel.model = cache.GetResource("Model", "Models/Cone.mdl"); - break; - case 5: - previewModel.model = cache.GetResource("Model", "Models/TeaPot.mdl"); - break; - } - - materialPreview.QueueUpdate(); - -} - -void EditShaderParameter(StringHash eventType, VariantMap& eventData) -{ - if (editMaterial is null) - return; - - LineEdit@ attrEdit = eventData["Element"].GetPtr(); - uint coordinate = attrEdit.vars["Coordinate"].GetU32(); - - String name = attrEdit.vars["Name"].GetString(); - - Variant oldValue = editMaterial.shaderParameters[name]; - Array coordValues = oldValue.ToString().Split(' '); - if (oldValue.type != VAR_BOOL) - coordValues[coordinate] = String(attrEdit.text.ToFloat()); - else - coordValues[coordinate] = attrEdit.text; - - String valueString; - for (uint i = 0; i < coordValues.length; ++i) - { - valueString += coordValues[i]; - valueString += " "; - } - - Variant newValue; - newValue.FromString(oldValue.type, valueString); - - BeginMaterialEdit(); - editMaterial.shaderParameters[name] = newValue; - EndMaterialEdit(); -} - -void CreateShaderParameter(StringHash eventType, VariantMap& eventData) -{ - if (editMaterial is null) - return; - - LineEdit@ nameEdit = materialWindow.GetChild("ParameterNameEdit", true); - String newName = nameEdit.text.Trimmed(); - if (newName.empty) - return; - - DropDownList@ dropDown = eventData["Element"].GetPtr(); - Variant newValue; - - switch (dropDown.selection) - { - case 0: - newValue = float(0); - break; - case 1: - newValue = Vector2(0, 0); - break; - case 2: - newValue = Vector3(0, 0, 0); - break; - case 3: - newValue = Vector4(0, 0, 0, 0); - break; - case 4: - newValue = int(0); - break; - case 5: - newValue = false; - break; - } - - BeginMaterialEdit(); - editMaterial.shaderParameters[newName] = newValue; - EndMaterialEdit(); - - RefreshMaterialShaderParameters(); -} - -void DeleteShaderParameter() -{ - if (editMaterial is null) - return; - - LineEdit@ nameEdit = materialWindow.GetChild("ParameterNameEdit", true); - String name = nameEdit.text.Trimmed(); - if (name.empty) - return; - - BeginMaterialEdit(); - editMaterial.RemoveShaderParameter(name); - EndMaterialEdit(); - - RefreshMaterialShaderParameters(); -} - -void PickMaterialTexture(StringHash eventType, VariantMap& eventData) -{ - if (editMaterial is null) - return; - - UIElement@ button = eventData["Element"].GetPtr(); - resourcePickIndex = button.vars["Index"].GetU32(); - - @resourcePicker = GetResourcePicker(StringHash("Texture2D")); - if (resourcePicker is null) - return; - - String lastPath = resourcePicker.lastPath; - if (lastPath.empty) - lastPath = sceneResourcePath; - CreateFileSelector(localization.Get("Pick ") + resourcePicker.typeName, "OK", "Cancel", lastPath, resourcePicker.filters, resourcePicker.lastFilter, false); - SubscribeToEvent(uiFileSelector, "FileSelected", "PickMaterialTextureDone"); -} - -void PickMaterialTextureDone(StringHash eventType, VariantMap& eventData) -{ - StoreResourcePickerPath(); - CloseFileSelector(); - - if (!eventData["OK"].GetBool()) - { - @resourcePicker = null; - return; - } - - String resourceName = eventData["FileName"].GetString(); - Resource@ res = GetPickedResource(resourceName); - - if (res !is null && editMaterial !is null) - { - BeginMaterialEdit(); - editMaterial.textures[TextureUnit(resourcePickIndex)] = res; - EndMaterialEdit(); - - RefreshMaterialTextures(false); - } - - @resourcePicker = null; -} - -void EditMaterialTexture(StringHash eventType, VariantMap& eventData) -{ - if (editMaterial is null) - return; - - LineEdit@ attrEdit = eventData["Element"].GetPtr(); - String textureName = attrEdit.text.Trimmed(); - uint index = attrEdit.vars["Index"].GetU32(); - - BeginMaterialEdit(); - - if (!textureName.empty) - { - Texture@ texture = cache.GetResource(GetExtension(textureName) == ".xml" ? "TextureCube" : "Texture2D", textureName); - editMaterial.textures[TextureUnit(index)] = texture; - } - else - editMaterial.textures[TextureUnit(index)] = null; - - EndMaterialEdit(); -} - -void NewTechnique() -{ - if (editMaterial is null) - return; - - BeginMaterialEdit(); - editMaterial.numTechniques = editMaterial.numTechniques + 1; - EndMaterialEdit(); - - RefreshMaterialTechniques(); -} - -void DeleteTechnique() -{ - if (editMaterial is null || editMaterial.numTechniques < 2) - return; - - BeginMaterialEdit(); - editMaterial.numTechniques = editMaterial.numTechniques - 1; - EndMaterialEdit(); - - RefreshMaterialTechniques(); -} - -void PickMaterialTechnique(StringHash eventType, VariantMap& eventData) -{ - if (editMaterial is null) - return; - - UIElement@ button = eventData["Element"].GetPtr(); - resourcePickIndex = button.vars["Index"].GetU32(); - - @resourcePicker = GetResourcePicker(StringHash("Technique")); - if (resourcePicker is null) - return; - - String lastPath = resourcePicker.lastPath; - if (lastPath.empty) - lastPath = sceneResourcePath; - CreateFileSelector(localization.Get("Pick ") + resourcePicker.typeName, "OK", "Cancel", lastPath, resourcePicker.filters, resourcePicker.lastFilter, false); - SubscribeToEvent(uiFileSelector, "FileSelected", "PickMaterialTechniqueDone"); -} - -void PickMaterialTechniqueDone(StringHash eventType, VariantMap& eventData) -{ - StoreResourcePickerPath(); - CloseFileSelector(); - - if (!eventData["OK"].GetBool()) - { - @resourcePicker = null; - return; - } - - String resourceName = eventData["FileName"].GetString(); - Resource@ res = GetPickedResource(resourceName); - - if (res !is null && editMaterial !is null) - { - BeginMaterialEdit(); - TechniqueEntry entry = editMaterial.techniqueEntries[resourcePickIndex]; - editMaterial.SetTechnique(resourcePickIndex, res, entry.qualityLevel, entry.lodDistance); - EndMaterialEdit(); - - RefreshMaterialTechniques(false); - } - - @resourcePicker = null; -} - -void EditMaterialTechnique(StringHash eventType, VariantMap& eventData) -{ - if (editMaterial is null) - return; - - LineEdit@ attrEdit = eventData["Element"].GetPtr(); - String techniqueName = attrEdit.text.Trimmed(); - uint index = attrEdit.vars["Index"].GetU32(); - - BeginMaterialEdit(); - - Technique@ newTech; - if (!techniqueName.empty) - newTech = cache.GetResource("Technique", techniqueName); - - TechniqueEntry entry = editMaterial.techniqueEntries[index]; - editMaterial.SetTechnique(index, newTech, entry.qualityLevel, entry.lodDistance); - - EndMaterialEdit(); -} - -void EditTechniqueQuality(StringHash eventType, VariantMap& eventData) -{ - if (editMaterial is null) - return; - - LineEdit@ attrEdit = eventData["Element"].GetPtr(); - uint newQualityLevel = attrEdit.text.ToU32(); - uint index = attrEdit.vars["Index"].GetU32(); - - BeginMaterialEdit(); - TechniqueEntry entry = editMaterial.techniqueEntries[index]; - editMaterial.SetTechnique(index, entry.technique, newQualityLevel, entry.lodDistance); - EndMaterialEdit(); -} - -void EditTechniqueLodDistance(StringHash eventType, VariantMap& eventData) -{ - if (editMaterial is null) - return; - - LineEdit@ attrEdit = eventData["Element"].GetPtr(); - float newLodDistance = attrEdit.text.ToFloat(); - uint index = attrEdit.vars["Index"].GetU32(); - - BeginMaterialEdit(); - TechniqueEntry entry = editMaterial.techniqueEntries[index]; - editMaterial.SetTechnique(index, entry.technique, entry.qualityLevel, newLodDistance); - EndMaterialEdit(); -} - -void SortTechniques() -{ - if (editMaterial is null) - return; - - BeginMaterialEdit(); - editMaterial.SortTechniques(); - EndMaterialEdit(); - - RefreshMaterialTechniques(); -} - -void EditConstantBias(StringHash eventType, VariantMap& eventData) -{ - if (editMaterial is null || inMaterialRefresh) - return; - - BeginMaterialEdit(); - - LineEdit@ attrEdit = eventData["Element"].GetPtr(); - BiasParameters bias = editMaterial.depthBias; - bias.constantBias = attrEdit.text.ToFloat(); - editMaterial.depthBias = bias; - - EndMaterialEdit(); -} - -void EditSlopeBias(StringHash eventType, VariantMap& eventData) -{ - if (editMaterial is null || inMaterialRefresh) - return; - - BeginMaterialEdit(); - - LineEdit@ attrEdit = eventData["Element"].GetPtr(); - BiasParameters bias = editMaterial.depthBias; - bias.slopeScaledBias = attrEdit.text.ToFloat(); - editMaterial.depthBias = bias; - - EndMaterialEdit(); -} - -void EditRenderOrder(StringHash eventType, VariantMap& eventData) -{ - if (editMaterial is null || inMaterialRefresh) - return; - - BeginMaterialEdit(); - - LineEdit@ attrEdit = eventData["Element"].GetPtr(); - editMaterial.renderOrder = attrEdit.text.ToI32(); - - EndMaterialEdit(); -} - -void EditCullMode(StringHash eventType, VariantMap& eventData) -{ - if (editMaterial is null || inMaterialRefresh) - return; - - BeginMaterialEdit(); - - DropDownList@ attrEdit = eventData["Element"].GetPtr(); - editMaterial.cullMode = CullMode(attrEdit.selection); - - EndMaterialEdit(); -} - -void EditShadowCullMode(StringHash eventType, VariantMap& eventData) -{ - if (editMaterial is null || inMaterialRefresh) - return; - - BeginMaterialEdit(); - - DropDownList@ attrEdit = eventData["Element"].GetPtr(); - editMaterial.shadowCullMode = CullMode(attrEdit.selection); - - EndMaterialEdit(); -} - -void EditFillMode(StringHash eventType, VariantMap& eventData) -{ - if (editMaterial is null || inMaterialRefresh) - return; - - BeginMaterialEdit(); - - DropDownList@ attrEdit = eventData["Element"].GetPtr(); - editMaterial.fillMode = FillMode(attrEdit.selection); - - EndMaterialEdit(); -} - -void EditOcclusion(StringHash eventType, VariantMap& eventData) -{ - if (editMaterial is null || inMaterialRefresh) - return; - - BeginMaterialEdit(); - - CheckBox@ attrEdit = eventData["Element"].GetPtr(); - editMaterial.occlusion = attrEdit.checked; - - EndMaterialEdit(); -} - -void EditAlphaToCoverage(StringHash eventType, VariantMap& eventData) -{ - if (editMaterial is null || inMaterialRefresh) - return; - - BeginMaterialEdit(); - - CheckBox@ attrEdit = eventData["Element"].GetPtr(); - editMaterial.alphaToCoverage = attrEdit.checked; - - EndMaterialEdit(); -} - -void EditLineAntiAlias(StringHash eventType, VariantMap& eventData) -{ - if (editMaterial is null || inMaterialRefresh) - return; - - BeginMaterialEdit(); - - CheckBox@ attrEdit = eventData["Element"].GetPtr(); - editMaterial.lineAntiAlias = attrEdit.checked; - - EndMaterialEdit(); -} - -void EditVSDefines(StringHash eventType, VariantMap& eventData) -{ - if (editMaterial is null || inMaterialRefresh) - return; - - BeginMaterialEdit(); - - LineEdit@ attrEdit = eventData["Element"].GetPtr(); - editMaterial.vertexShaderDefines = attrEdit.text.Trimmed(); - - EndMaterialEdit(); -} - -void EditPSDefines(StringHash eventType, VariantMap& eventData) -{ - if (editMaterial is null || inMaterialRefresh) - return; - - BeginMaterialEdit(); - - LineEdit@ attrEdit = eventData["Element"].GetPtr(); - editMaterial.pixelShaderDefines = attrEdit.text.Trimmed(); - - EndMaterialEdit(); -} - -void BeginMaterialEdit() -{ - if (editMaterial is null) - return; - - oldMaterialState = XMLFile(); - XMLElement materialElem = oldMaterialState.CreateRoot("material"); - editMaterial.Save(materialElem); -} - -void EndMaterialEdit() -{ - if (editMaterial is null) - return; - if (!dragEditAttribute) - { - EditMaterialAction@ action = EditMaterialAction(); - action.Define(editMaterial, oldMaterialState); - SaveEditAction(action); - } - - materialPreview.QueueUpdate(); -} +// Urho3D material editor + +Window@ materialWindow; +Material@ editMaterial; +XMLFile@ oldMaterialState; +bool inMaterialRefresh = true; +View3D@ materialPreview; +Scene@ previewScene; +Node@ previewCameraNode; +Node@ previewLightNode; +Light@ previewLight; +Node@ previewModelNode; +StaticModel@ previewModel; + +void CreateMaterialEditor() +{ + if (materialWindow !is null) + return; + + materialWindow = LoadEditorUI("Editor/UI/EditorMaterialWindow.xml"); + ui.root.AddChild(materialWindow); + materialWindow.opacity = uiMaxOpacity; + + InitMaterialPreview(); + InitModelPreviewList(); + RefreshMaterialEditor(); + + int height = Min(ui.root.height - 60, 600); + materialWindow.SetSize(400, height); + CenterDialog(materialWindow); + + HideMaterialEditor(); + + SubscribeToEvent(materialWindow.GetChild("NewButton", true), "Released", "NewMaterial"); + SubscribeToEvent(materialWindow.GetChild("RevertButton", true), "Released", "RevertMaterial"); + SubscribeToEvent(materialWindow.GetChild("SaveButton", true), "Released", "SaveMaterial"); + SubscribeToEvent(materialWindow.GetChild("SaveAsButton", true), "Released", "SaveMaterialAs"); + SubscribeToEvent(materialWindow.GetChild("CloseButton", true), "Released", "HideMaterialEditor"); + SubscribeToEvent(materialWindow.GetChild("NewParameterDropDown", true), "ItemSelected", "CreateShaderParameter"); + SubscribeToEvent(materialWindow.GetChild("DeleteParameterButton", true), "Released", "DeleteShaderParameter"); + SubscribeToEvent(materialWindow.GetChild("NewTechniqueButton", true), "Released", "NewTechnique"); + SubscribeToEvent(materialWindow.GetChild("DeleteTechniqueButton", true), "Released", "DeleteTechnique"); + SubscribeToEvent(materialWindow.GetChild("SortTechniquesButton", true), "Released", "SortTechniques"); + SubscribeToEvent(materialWindow.GetChild("VSDefinesEdit", true), "TextFinished", "EditVSDefines"); + SubscribeToEvent(materialWindow.GetChild("PSDefinesEdit", true), "TextFinished", "EditPSDefines"); + SubscribeToEvent(materialWindow.GetChild("ConstantBiasEdit", true), "TextChanged", "EditConstantBias"); + SubscribeToEvent(materialWindow.GetChild("ConstantBiasEdit", true), "TextFinished", "EditConstantBias"); + SubscribeToEvent(materialWindow.GetChild("SlopeBiasEdit", true), "TextChanged", "EditSlopeBias"); + SubscribeToEvent(materialWindow.GetChild("SlopeBiasEdit", true), "TextFinished", "EditSlopeBias"); + SubscribeToEvent(materialWindow.GetChild("RenderOrderEdit", true), "TextChanged", "EditRenderOrder"); + SubscribeToEvent(materialWindow.GetChild("RenderOrderEdit", true), "TextFinished", "EditRenderOrder"); + SubscribeToEvent(materialWindow.GetChild("CullModeEdit", true), "ItemSelected", "EditCullMode"); + SubscribeToEvent(materialWindow.GetChild("ShadowCullModeEdit", true), "ItemSelected", "EditShadowCullMode"); + SubscribeToEvent(materialWindow.GetChild("FillModeEdit", true), "ItemSelected", "EditFillMode"); + SubscribeToEvent(materialWindow.GetChild("OcclusionEdit", true), "Toggled", "EditOcclusion"); + SubscribeToEvent(materialWindow.GetChild("AlphaToCoverageEdit", true), "Toggled", "EditAlphaToCoverage"); + SubscribeToEvent(materialWindow.GetChild("LineAntiAliasEdit", true), "Toggled", "EditLineAntiAlias"); +} + +bool ToggleMaterialEditor() +{ + if (materialWindow.visible == false) + ShowMaterialEditor(); + else + HideMaterialEditor(); + return true; +} + +void ShowMaterialEditor() +{ + RefreshMaterialEditor(); + materialWindow.visible = true; + materialWindow.BringToFront(); +} + +void HideMaterialEditor() +{ + materialWindow.visible = false; +} + +void InitMaterialPreview() +{ + previewScene = Scene("PreviewScene"); + previewScene.CreateComponent("Octree"); + + Node@ zoneNode = previewScene.CreateChild("Zone"); + Zone@ zone = zoneNode.CreateComponent("Zone"); + zone.boundingBox = BoundingBox(-1000, 1000); + zone.ambientColor = Color(0.15, 0.15, 0.15); + zone.fogColor = Color(0, 0, 0); + zone.fogStart = 10.0; + zone.fogEnd = 100.0; + + previewCameraNode = previewScene.CreateChild("PreviewCamera"); + previewCameraNode.position = Vector3(0, 0, -1.5); + Camera@ camera = previewCameraNode.CreateComponent("Camera"); + camera.nearClip = 0.1f; + camera.farClip = 100.0f; + + previewLightNode = previewScene.CreateChild("PreviewLight"); + previewLightNode.direction = Vector3(0.5, -0.5, 0.5); + previewLight = previewLightNode.CreateComponent("Light"); + previewLight.lightType = LIGHT_DIRECTIONAL; + previewLight.specularIntensity = 0.5; + + previewModelNode = previewScene.CreateChild("PreviewModel"); + previewModelNode.rotation = Quaternion(0, 0, 0); + previewModel = previewModelNode.CreateComponent("StaticModel"); + previewModel.model = cache.GetResource("Model", "Editor/Models/Sphere.mdl"); + + materialPreview = materialWindow.GetChild("MaterialPreview", true); + materialPreview.SetFixedHeight(100); + materialPreview.SetView(previewScene, camera); + materialPreview.viewport.renderPath = renderPath; + materialPreview.autoUpdate = false; + + SubscribeToEvent(materialPreview, "DragMove", "RotateMaterialPreview"); +} + +void InitModelPreviewList() +{ + DropDownList@ modelPreview = materialWindow.GetChild("ModelPreview", true); + modelPreview.selection = 1; + SubscribeToEvent(materialWindow.GetChild("ModelPreview", true), "ItemSelected", "EditModelPreviewChange"); +} + +void EditMaterial(Material@ mat) +{ + if (editMaterial !is null) + UnsubscribeFromEvent(editMaterial, "ReloadFinished"); + + editMaterial = mat; + + if (editMaterial !is null) + SubscribeToEvent(editMaterial, "ReloadFinished", "RefreshMaterialEditor"); + + ShowMaterialEditor(); +} + +void RefreshMaterialEditor() +{ + RefreshMaterialPreview(); + RefreshMaterialName(); + RefreshMaterialTechniques(); + RefreshMaterialTextures(); + RefreshMaterialShaderParameters(); + RefreshMaterialMiscParameters(); +} + +void RefreshMaterialPreview() +{ + previewModel.material = editMaterial; + materialPreview.QueueUpdate(); +} + +void RefreshMaterialName() +{ + UIElement@ container = materialWindow.GetChild("NameContainer", true); + container.RemoveAllChildren(); + + LineEdit@ nameEdit = CreateAttributeLineEdit(container, null, 0, 0); + if (editMaterial !is null) + nameEdit.text = editMaterial.name; + SubscribeToEvent(nameEdit, "TextFinished", "EditMaterialName"); + + Button@ pickButton = CreateResourcePickerButton(container, null, 0, 0, "smallButtonPick"); + SubscribeToEvent(pickButton, "Released", "PickEditMaterial"); +} + +void RefreshMaterialTechniques(bool fullUpdate = true) +{ + ListView@ list = materialWindow.GetChild("TechniqueList", true); + + if (editMaterial is null) + return; + + if (fullUpdate == true) + { + list.RemoveAllItems(); + + for (uint i = 0; i < editMaterial.numTechniques; ++i) + { + TechniqueEntry entry = editMaterial.techniqueEntries[i]; + + UIElement@ container = UIElement(); + container.SetLayout(LM_HORIZONTAL, 4); + container.SetFixedHeight(ATTR_HEIGHT); + list.AddItem(container); + + LineEdit@ nameEdit = CreateAttributeLineEdit(container, null, i, 0); + nameEdit.name = "TechniqueNameEdit" + String(i); + + Button@ pickButton = CreateResourcePickerButton(container, null, i, 0, "smallButtonPick"); + SubscribeToEvent(pickButton, "Released", "PickMaterialTechnique"); + Button@ openButton = CreateResourcePickerButton(container, null, i, 0, "smallButtonOpen"); + SubscribeToEvent(openButton, "Released", "OpenResource"); + + if (entry.technique !is null) + nameEdit.text = entry.technique.name; + + SubscribeToEvent(nameEdit, "TextFinished", "EditMaterialTechnique"); + + UIElement@ container2 = UIElement(); + container2.SetLayout(LM_HORIZONTAL, 4); + container2.SetFixedHeight(ATTR_HEIGHT); + list.AddItem(container2); + + Text@ text = container2.CreateChild("Text"); + text.style = "EditorAttributeText"; + text.text = "Quality"; + LineEdit@ attrEdit = CreateAttributeLineEdit(container2, null, i, 0); + attrEdit.text = String(entry.qualityLevel); + SubscribeToEvent(attrEdit, "TextChanged", "EditTechniqueQuality"); + SubscribeToEvent(attrEdit, "TextFinished", "EditTechniqueQuality"); + + text = container2.CreateChild("Text"); + text.style = "EditorAttributeText"; + text.text = "LOD Distance"; + attrEdit = CreateAttributeLineEdit(container2, null, i, 0); + attrEdit.text = String(entry.lodDistance); + SubscribeToEvent(attrEdit, "TextChanged", "EditTechniqueLodDistance"); + SubscribeToEvent(attrEdit, "TextFinished", "EditTechniqueLodDistance"); + } + } + else + { + for (uint i = 0; i < editMaterial.numTechniques; ++i) + { + TechniqueEntry entry = editMaterial.techniqueEntries[i]; + + LineEdit@ nameEdit = materialWindow.GetChild("TechniqueNameEdit" + String(i), true); + if (nameEdit is null) + continue; + + nameEdit.text = entry.technique !is null ? entry.technique.name : ""; + } + } +} + +void RefreshMaterialTextures(bool fullUpdate = true) +{ + if (fullUpdate) + { + ListView@ list = materialWindow.GetChild("TextureList", true); + list.RemoveAllItems(); + + for (uint i = 0; i < MAX_MATERIAL_TEXTURE_UNITS; ++i) + { + String tuName = Material::GetTextureUnitName(TextureUnit(i)); + tuName[0] = ToUpper(tuName[0]); + + UIElement@ parent = CreateAttributeEditorParentWithSeparatedLabel(list, "Unit " + i + " " + tuName, i, 0, false); + + UIElement@ container = UIElement(); + container.SetLayout(LM_HORIZONTAL, 4, IntRect(10, 0, 4, 0)); + container.SetFixedHeight(ATTR_HEIGHT); + parent.AddChild(container); + + LineEdit@ nameEdit = CreateAttributeLineEdit(container, null, i, 0); + nameEdit.name = "TextureNameEdit" + String(i); + + Button@ pickButton = CreateResourcePickerButton(container, null, i, 0, "smallButtonPick"); + SubscribeToEvent(pickButton, "Released", "PickMaterialTexture"); + Button@ openButton = CreateResourcePickerButton(container, null, i, 0, "smallButtonOpen"); + SubscribeToEvent(openButton, "Released", "OpenResource"); + + if (editMaterial !is null) + { + Texture@ texture = editMaterial.textures[TextureUnit(i)]; + if (texture !is null) + nameEdit.text = texture.name; + } + + SubscribeToEvent(nameEdit, "TextFinished", "EditMaterialTexture"); + } + } + else + { + for (uint i = 0; i < MAX_MATERIAL_TEXTURE_UNITS; ++i) + { + LineEdit@ nameEdit = materialWindow.GetChild("TextureNameEdit" + String(i), true); + if (nameEdit is null) + continue; + + String textureName; + if (editMaterial !is null) + { + Texture@ texture = editMaterial.textures[TextureUnit(i)]; + if (texture !is null) + textureName = texture.name; + } + + nameEdit.text = textureName; + } + } +} + +void RefreshMaterialShaderParameters() +{ + ListView@ list = materialWindow.GetChild("ShaderParameterList", true); + list.RemoveAllItems(); + if (editMaterial is null) + return; + + Array@ parameterNames = editMaterial.shaderParameterNames; + + for (uint i = 0; i < parameterNames.length; ++i) + { + VariantType type = editMaterial.shaderParameters[parameterNames[i]].type; + Variant value = editMaterial.shaderParameters[parameterNames[i]]; + UIElement@ parent = CreateAttributeEditorParent(list, parameterNames[i], 0, 0); + uint numCoords = 1; + if (type >= VAR_VECTOR2 && type <= VAR_VECTOR4) + numCoords = type - VAR_FLOAT + 1; + + Array coordValues = value.ToString().Split(' '); + + for (uint j = 0; j < numCoords; ++j) + { + LineEdit@ attrEdit = CreateAttributeLineEdit(parent, null, 0, 0); + attrEdit.vars["Coordinate"] = j; + attrEdit.vars["Name"] = parameterNames[i]; + attrEdit.text = coordValues[j]; + + CreateDragSlider(attrEdit); + + SubscribeToEvent(attrEdit, "TextChanged", "EditShaderParameter"); + SubscribeToEvent(attrEdit, "TextFinished", "EditShaderParameter"); + } + } +} + +void RefreshMaterialMiscParameters() +{ + if (editMaterial is null) + return; + + inMaterialRefresh = true; + + BiasParameters bias = editMaterial.depthBias; + LineEdit@ attrEdit = materialWindow.GetChild("ConstantBiasEdit", true); + attrEdit.text = String(bias.constantBias); + attrEdit = materialWindow.GetChild("SlopeBiasEdit", true); + attrEdit.text = String(bias.slopeScaledBias); + attrEdit = materialWindow.GetChild("RenderOrderEdit", true); + attrEdit.text = String(int(editMaterial.renderOrder)); + attrEdit = materialWindow.GetChild("VSDefinesEdit", true); + attrEdit.text = editMaterial.vertexShaderDefines; + attrEdit = materialWindow.GetChild("PSDefinesEdit", true); + attrEdit.text = editMaterial.pixelShaderDefines; + + DropDownList@ attrList = materialWindow.GetChild("CullModeEdit", true); + attrList.selection = editMaterial.cullMode; + attrList = materialWindow.GetChild("ShadowCullModeEdit", true); + attrList.selection = editMaterial.shadowCullMode; + attrList = materialWindow.GetChild("FillModeEdit", true); + attrList.selection = editMaterial.fillMode; + + CheckBox@ attrCheckBox = materialWindow.GetChild("OcclusionEdit", true); + attrCheckBox.checked = editMaterial.occlusion; + attrCheckBox = materialWindow.GetChild("AlphaToCoverageEdit", true); + attrCheckBox.checked = editMaterial.alphaToCoverage; + attrCheckBox = materialWindow.GetChild("LineAntiAliasEdit", true); + attrCheckBox.checked = editMaterial.lineAntiAlias; + + inMaterialRefresh = false; +} + +void RotateMaterialPreview(StringHash eventType, VariantMap& eventData) +{ + int elemX = eventData["ElementX"].GetI32(); + int elemY = eventData["ElementY"].GetI32(); + + if (materialPreview.height > 0 && materialPreview.width > 0) + { + float yaw = ((materialPreview.height / 2) - elemY) * (90.0 / materialPreview.height); + float pitch = ((materialPreview.width / 2) - elemX) * (90.0 / materialPreview.width); + + previewModelNode.rotation = previewModelNode.rotation.Slerp(Quaternion(yaw, pitch, 0), 0.1); + materialPreview.QueueUpdate(); + } +} + +void EditMaterialName(StringHash eventType, VariantMap& eventData) +{ + LineEdit@ nameEdit = eventData["Element"].GetPtr(); + String newMaterialName = nameEdit.text.Trimmed(); + if (!newMaterialName.empty) + { + Material@ newMaterial = cache.GetResource("Material", newMaterialName); + if (newMaterial !is null) + EditMaterial(newMaterial); + } +} + +void PickEditMaterial() +{ + @resourcePicker = GetResourcePicker(StringHash("Material")); + if (resourcePicker is null) + return; + + String lastPath = resourcePicker.lastPath; + if (lastPath.empty) + lastPath = sceneResourcePath; + CreateFileSelector(localization.Get("Pick ") + resourcePicker.typeName, "OK", "Cancel", lastPath, resourcePicker.filters, resourcePicker.lastFilter, false); + SubscribeToEvent(uiFileSelector, "FileSelected", "PickEditMaterialDone"); +} + +void PickEditMaterialDone(StringHash eventType, VariantMap& eventData) +{ + StoreResourcePickerPath(); + CloseFileSelector(); + + if (!eventData["OK"].GetBool()) + { + @resourcePicker = null; + return; + } + + String resourceName = eventData["FileName"].GetString(); + Resource@ res = GetPickedResource(resourceName); + + if (res !is null) + EditMaterial(cast(res)); + + @resourcePicker = null; +} + +void NewMaterial() +{ + EditMaterial(Material()); +} + +void RevertMaterial() +{ + if (editMaterial is null) + return; + + BeginMaterialEdit(); + cache.ReloadResource(editMaterial); + EndMaterialEdit(); + + RefreshMaterialEditor(); +} + +void SaveMaterial() +{ + if (editMaterial is null || editMaterial.name.empty) + return; + + String fullName = cache.GetResourceFileName(editMaterial.name); + if (fullName.empty) + return; + + MakeBackup(fullName); + File saveFile(fullName, FILE_WRITE); + bool success; + if (GetExtension(fullName) == ".json") + { + JSONFile json; + editMaterial.Save(json.root); + success = json.Save(saveFile); + } + else + success = editMaterial.Save(saveFile); + RemoveBackup(success, fullName); +} + +void SaveMaterialAs() +{ + if (editMaterial is null) + return; + + @resourcePicker = GetResourcePicker(StringHash("Material")); + if (resourcePicker is null) + return; + + String lastPath = resourcePicker.lastPath; + if (lastPath.empty) + lastPath = sceneResourcePath; + CreateFileSelector("Save material as", "Save", "Cancel", lastPath, resourcePicker.filters, resourcePicker.lastFilter); + SubscribeToEvent(uiFileSelector, "FileSelected", "SaveMaterialAsDone"); +} + +void SaveMaterialAsDone(StringHash eventType, VariantMap& eventData) +{ + StoreResourcePickerPath(); + CloseFileSelector(); + @resourcePicker = null; + + if (editMaterial is null) + return; + + if (!eventData["OK"].GetBool()) + { + @resourcePicker = null; + return; + } + + String fullName = eventData["FileName"].GetString(); + + // Add default extension for saving if not specified + String filter = eventData["Filter"].GetString(); + if (GetExtension(fullName).empty && filter != "*.*") + fullName = fullName + filter.Substring(1); + + MakeBackup(fullName); + File saveFile(fullName, FILE_WRITE); + bool success; + if (GetExtension(fullName) == ".json") + { + JSONFile json; + editMaterial.Save(json.root); + success = json.Save(saveFile); + } + else + success = editMaterial.Save(saveFile); + + if (success) + { + saveFile.Close(); + RemoveBackup(true, fullName); + + // Load the new resource to update the name in the editor + Material@ newMat = cache.GetResource("Material", GetResourceNameFromFullName(fullName)); + if (newMat !is null) + EditMaterial(newMat); + } +} + +void EditModelPreviewChange(StringHash eventType, VariantMap& eventData) +{ + if (materialPreview is null) + return; + + previewModelNode.scale = Vector3(1.0, 1.0, 1.0); + + DropDownList@ element = eventData["Element"].GetPtr(); + + switch (element.selection) + { + case 0: + previewModel.model = cache.GetResource("Model", "Editor/Models/Box.mdl"); + break; + case 1: + previewModel.model = cache.GetResource("Model", "Editor/Models/Sphere.mdl"); + break; + case 2: + previewModel.model = cache.GetResource("Model", "Editor/Models/Plane.mdl"); + break; + case 3: + previewModel.model = cache.GetResource("Model", "Editor/Models/Cylinder.mdl"); + previewModelNode.scale = Vector3(0.8, 0.8, 0.8); + break; + case 4: + previewModel.model = cache.GetResource("Model", "Editor/Models/Cone.mdl"); + break; + case 5: + previewModel.model = cache.GetResource("Model", "Editor/Models/TeaPot.mdl"); + break; + } + + materialPreview.QueueUpdate(); + +} + +void EditShaderParameter(StringHash eventType, VariantMap& eventData) +{ + if (editMaterial is null) + return; + + LineEdit@ attrEdit = eventData["Element"].GetPtr(); + uint coordinate = attrEdit.vars["Coordinate"].GetU32(); + + String name = attrEdit.vars["Name"].GetString(); + + Variant oldValue = editMaterial.shaderParameters[name]; + Array coordValues = oldValue.ToString().Split(' '); + if (oldValue.type != VAR_BOOL) + coordValues[coordinate] = String(attrEdit.text.ToFloat()); + else + coordValues[coordinate] = attrEdit.text; + + String valueString; + for (uint i = 0; i < coordValues.length; ++i) + { + valueString += coordValues[i]; + valueString += " "; + } + + Variant newValue; + newValue.FromString(oldValue.type, valueString); + + BeginMaterialEdit(); + editMaterial.shaderParameters[name] = newValue; + EndMaterialEdit(); +} + +void CreateShaderParameter(StringHash eventType, VariantMap& eventData) +{ + if (editMaterial is null) + return; + + LineEdit@ nameEdit = materialWindow.GetChild("ParameterNameEdit", true); + String newName = nameEdit.text.Trimmed(); + if (newName.empty) + return; + + DropDownList@ dropDown = eventData["Element"].GetPtr(); + Variant newValue; + + switch (dropDown.selection) + { + case 0: + newValue = float(0); + break; + case 1: + newValue = Vector2(0, 0); + break; + case 2: + newValue = Vector3(0, 0, 0); + break; + case 3: + newValue = Vector4(0, 0, 0, 0); + break; + case 4: + newValue = int(0); + break; + case 5: + newValue = false; + break; + } + + BeginMaterialEdit(); + editMaterial.shaderParameters[newName] = newValue; + EndMaterialEdit(); + + RefreshMaterialShaderParameters(); +} + +void DeleteShaderParameter() +{ + if (editMaterial is null) + return; + + LineEdit@ nameEdit = materialWindow.GetChild("ParameterNameEdit", true); + String name = nameEdit.text.Trimmed(); + if (name.empty) + return; + + BeginMaterialEdit(); + editMaterial.RemoveShaderParameter(name); + EndMaterialEdit(); + + RefreshMaterialShaderParameters(); +} + +void PickMaterialTexture(StringHash eventType, VariantMap& eventData) +{ + if (editMaterial is null) + return; + + UIElement@ button = eventData["Element"].GetPtr(); + resourcePickIndex = button.vars["Index"].GetU32(); + + @resourcePicker = GetResourcePicker(StringHash("Texture2D")); + if (resourcePicker is null) + return; + + String lastPath = resourcePicker.lastPath; + if (lastPath.empty) + lastPath = sceneResourcePath; + CreateFileSelector(localization.Get("Pick ") + resourcePicker.typeName, "OK", "Cancel", lastPath, resourcePicker.filters, resourcePicker.lastFilter, false); + SubscribeToEvent(uiFileSelector, "FileSelected", "PickMaterialTextureDone"); +} + +void PickMaterialTextureDone(StringHash eventType, VariantMap& eventData) +{ + StoreResourcePickerPath(); + CloseFileSelector(); + + if (!eventData["OK"].GetBool()) + { + @resourcePicker = null; + return; + } + + String resourceName = eventData["FileName"].GetString(); + Resource@ res = GetPickedResource(resourceName); + + if (res !is null && editMaterial !is null) + { + BeginMaterialEdit(); + editMaterial.textures[TextureUnit(resourcePickIndex)] = res; + EndMaterialEdit(); + + RefreshMaterialTextures(false); + } + + @resourcePicker = null; +} + +void EditMaterialTexture(StringHash eventType, VariantMap& eventData) +{ + if (editMaterial is null) + return; + + LineEdit@ attrEdit = eventData["Element"].GetPtr(); + String textureName = attrEdit.text.Trimmed(); + uint index = attrEdit.vars["Index"].GetU32(); + + BeginMaterialEdit(); + + if (!textureName.empty) + { + Texture@ texture = cache.GetResource(GetExtension(textureName) == ".xml" ? "TextureCube" : "Texture2D", textureName); + editMaterial.textures[TextureUnit(index)] = texture; + } + else + editMaterial.textures[TextureUnit(index)] = null; + + EndMaterialEdit(); +} + +void NewTechnique() +{ + if (editMaterial is null) + return; + + BeginMaterialEdit(); + editMaterial.numTechniques = editMaterial.numTechniques + 1; + EndMaterialEdit(); + + RefreshMaterialTechniques(); +} + +void DeleteTechnique() +{ + if (editMaterial is null || editMaterial.numTechniques < 2) + return; + + BeginMaterialEdit(); + editMaterial.numTechniques = editMaterial.numTechniques - 1; + EndMaterialEdit(); + + RefreshMaterialTechniques(); +} + +void PickMaterialTechnique(StringHash eventType, VariantMap& eventData) +{ + if (editMaterial is null) + return; + + UIElement@ button = eventData["Element"].GetPtr(); + resourcePickIndex = button.vars["Index"].GetU32(); + + @resourcePicker = GetResourcePicker(StringHash("Technique")); + if (resourcePicker is null) + return; + + String lastPath = resourcePicker.lastPath; + if (lastPath.empty) + lastPath = sceneResourcePath; + CreateFileSelector(localization.Get("Pick ") + resourcePicker.typeName, "OK", "Cancel", lastPath, resourcePicker.filters, resourcePicker.lastFilter, false); + SubscribeToEvent(uiFileSelector, "FileSelected", "PickMaterialTechniqueDone"); +} + +void PickMaterialTechniqueDone(StringHash eventType, VariantMap& eventData) +{ + StoreResourcePickerPath(); + CloseFileSelector(); + + if (!eventData["OK"].GetBool()) + { + @resourcePicker = null; + return; + } + + String resourceName = eventData["FileName"].GetString(); + Resource@ res = GetPickedResource(resourceName); + + if (res !is null && editMaterial !is null) + { + BeginMaterialEdit(); + TechniqueEntry entry = editMaterial.techniqueEntries[resourcePickIndex]; + editMaterial.SetTechnique(resourcePickIndex, res, entry.qualityLevel, entry.lodDistance); + EndMaterialEdit(); + + RefreshMaterialTechniques(false); + } + + @resourcePicker = null; +} + +void EditMaterialTechnique(StringHash eventType, VariantMap& eventData) +{ + if (editMaterial is null) + return; + + LineEdit@ attrEdit = eventData["Element"].GetPtr(); + String techniqueName = attrEdit.text.Trimmed(); + uint index = attrEdit.vars["Index"].GetU32(); + + BeginMaterialEdit(); + + Technique@ newTech; + if (!techniqueName.empty) + newTech = cache.GetResource("Technique", techniqueName); + + TechniqueEntry entry = editMaterial.techniqueEntries[index]; + editMaterial.SetTechnique(index, newTech, entry.qualityLevel, entry.lodDistance); + + EndMaterialEdit(); +} + +void EditTechniqueQuality(StringHash eventType, VariantMap& eventData) +{ + if (editMaterial is null) + return; + + LineEdit@ attrEdit = eventData["Element"].GetPtr(); + uint newQualityLevel = attrEdit.text.ToU32(); + uint index = attrEdit.vars["Index"].GetU32(); + + BeginMaterialEdit(); + TechniqueEntry entry = editMaterial.techniqueEntries[index]; + editMaterial.SetTechnique(index, entry.technique, newQualityLevel, entry.lodDistance); + EndMaterialEdit(); +} + +void EditTechniqueLodDistance(StringHash eventType, VariantMap& eventData) +{ + if (editMaterial is null) + return; + + LineEdit@ attrEdit = eventData["Element"].GetPtr(); + float newLodDistance = attrEdit.text.ToFloat(); + uint index = attrEdit.vars["Index"].GetU32(); + + BeginMaterialEdit(); + TechniqueEntry entry = editMaterial.techniqueEntries[index]; + editMaterial.SetTechnique(index, entry.technique, entry.qualityLevel, newLodDistance); + EndMaterialEdit(); +} + +void SortTechniques() +{ + if (editMaterial is null) + return; + + BeginMaterialEdit(); + editMaterial.SortTechniques(); + EndMaterialEdit(); + + RefreshMaterialTechniques(); +} + +void EditConstantBias(StringHash eventType, VariantMap& eventData) +{ + if (editMaterial is null || inMaterialRefresh) + return; + + BeginMaterialEdit(); + + LineEdit@ attrEdit = eventData["Element"].GetPtr(); + BiasParameters bias = editMaterial.depthBias; + bias.constantBias = attrEdit.text.ToFloat(); + editMaterial.depthBias = bias; + + EndMaterialEdit(); +} + +void EditSlopeBias(StringHash eventType, VariantMap& eventData) +{ + if (editMaterial is null || inMaterialRefresh) + return; + + BeginMaterialEdit(); + + LineEdit@ attrEdit = eventData["Element"].GetPtr(); + BiasParameters bias = editMaterial.depthBias; + bias.slopeScaledBias = attrEdit.text.ToFloat(); + editMaterial.depthBias = bias; + + EndMaterialEdit(); +} + +void EditRenderOrder(StringHash eventType, VariantMap& eventData) +{ + if (editMaterial is null || inMaterialRefresh) + return; + + BeginMaterialEdit(); + + LineEdit@ attrEdit = eventData["Element"].GetPtr(); + editMaterial.renderOrder = attrEdit.text.ToI32(); + + EndMaterialEdit(); +} + +void EditCullMode(StringHash eventType, VariantMap& eventData) +{ + if (editMaterial is null || inMaterialRefresh) + return; + + BeginMaterialEdit(); + + DropDownList@ attrEdit = eventData["Element"].GetPtr(); + editMaterial.cullMode = CullMode(attrEdit.selection); + + EndMaterialEdit(); +} + +void EditShadowCullMode(StringHash eventType, VariantMap& eventData) +{ + if (editMaterial is null || inMaterialRefresh) + return; + + BeginMaterialEdit(); + + DropDownList@ attrEdit = eventData["Element"].GetPtr(); + editMaterial.shadowCullMode = CullMode(attrEdit.selection); + + EndMaterialEdit(); +} + +void EditFillMode(StringHash eventType, VariantMap& eventData) +{ + if (editMaterial is null || inMaterialRefresh) + return; + + BeginMaterialEdit(); + + DropDownList@ attrEdit = eventData["Element"].GetPtr(); + editMaterial.fillMode = FillMode(attrEdit.selection); + + EndMaterialEdit(); +} + +void EditOcclusion(StringHash eventType, VariantMap& eventData) +{ + if (editMaterial is null || inMaterialRefresh) + return; + + BeginMaterialEdit(); + + CheckBox@ attrEdit = eventData["Element"].GetPtr(); + editMaterial.occlusion = attrEdit.checked; + + EndMaterialEdit(); +} + +void EditAlphaToCoverage(StringHash eventType, VariantMap& eventData) +{ + if (editMaterial is null || inMaterialRefresh) + return; + + BeginMaterialEdit(); + + CheckBox@ attrEdit = eventData["Element"].GetPtr(); + editMaterial.alphaToCoverage = attrEdit.checked; + + EndMaterialEdit(); +} + +void EditLineAntiAlias(StringHash eventType, VariantMap& eventData) +{ + if (editMaterial is null || inMaterialRefresh) + return; + + BeginMaterialEdit(); + + CheckBox@ attrEdit = eventData["Element"].GetPtr(); + editMaterial.lineAntiAlias = attrEdit.checked; + + EndMaterialEdit(); +} + +void EditVSDefines(StringHash eventType, VariantMap& eventData) +{ + if (editMaterial is null || inMaterialRefresh) + return; + + BeginMaterialEdit(); + + LineEdit@ attrEdit = eventData["Element"].GetPtr(); + editMaterial.vertexShaderDefines = attrEdit.text.Trimmed(); + + EndMaterialEdit(); +} + +void EditPSDefines(StringHash eventType, VariantMap& eventData) +{ + if (editMaterial is null || inMaterialRefresh) + return; + + BeginMaterialEdit(); + + LineEdit@ attrEdit = eventData["Element"].GetPtr(); + editMaterial.pixelShaderDefines = attrEdit.text.Trimmed(); + + EndMaterialEdit(); +} + +void BeginMaterialEdit() +{ + if (editMaterial is null) + return; + + oldMaterialState = XMLFile(); + XMLElement materialElem = oldMaterialState.CreateRoot("material"); + editMaterial.Save(materialElem); +} + +void EndMaterialEdit() +{ + if (editMaterial is null) + return; + if (!dragEditAttribute) + { + EditMaterialAction@ action = EditMaterialAction(); + action.Define(editMaterial, oldMaterialState); + SaveEditAction(action); + } + + materialPreview.QueueUpdate(); +} diff --git a/bin/Data/Scripts/Editor/EditorParticleEffect.as b/bin/EditorData/Editor/Scripts/EditorParticleEffect.as similarity index 99% rename from bin/Data/Scripts/Editor/EditorParticleEffect.as rename to bin/EditorData/Editor/Scripts/EditorParticleEffect.as index 8250ca2c0e3..e0dc57fd3e3 100644 --- a/bin/Data/Scripts/Editor/EditorParticleEffect.as +++ b/bin/EditorData/Editor/Scripts/EditorParticleEffect.as @@ -30,7 +30,7 @@ void CreateParticleEffectEditor() if (particleEffectWindow !is null) return; - particleEffectWindow = LoadEditorUI("UI/EditorParticleEffectWindow.xml"); + particleEffectWindow = LoadEditorUI("Editor/UI/EditorParticleEffectWindow.xml"); ui.root.AddChild(particleEffectWindow); particleEffectWindow.opacity = uiMaxOpacity; @@ -147,7 +147,7 @@ void ResetCameraTransformation() particlePreviewCameraNode.position = Vector3(0, 0, -5); particlePreviewCameraNode.LookAt(Vector3(0, 0, 0)); particleViewCamDir = -particlePreviewCameraNode.position; - + // Manually set initial rotation because eulerAngle always return 0 on first frame particleViewCamRot = Vector3(0.0, 180.0, 0.0); @@ -216,7 +216,7 @@ void EditParticleEffectColorFrameRemove(StringHash eventType, VariantMap& eventD if (lv !is null && lv.selection != M_MAX_UNSIGNED) { BeginParticleEffectEdit(); - + editParticleEffect.RemoveColorFrame(lv.selection); RefreshParticleEffectColorFrames(); @@ -237,7 +237,7 @@ void EditParticleEffectTextureFrameRemove(StringHash eventType, VariantMap& even if (lv !is null && lv.selection != M_MAX_UNSIGNED) { BeginParticleEffectEdit(); - + editParticleEffect.RemoveTextureFrame(lv.selection); RefreshParticleEffectTextureFrames(); @@ -774,7 +774,7 @@ void EditParticleEffectMaterial(StringHash eventType, VariantMap& eventData) editParticleEffect.material = res; particleEffectEmitter.ApplyEffect(); - + EndParticleEffectEdit(); } } @@ -976,7 +976,7 @@ void UpdateParticleEffectPreviewGrid() lineOffset += scale; } particleEffectPreviewGrid.Commit(); - + } void InitParticleEffectPreview() @@ -1006,15 +1006,15 @@ void InitParticleEffectPreview() particleEffectPreviewNode = particlePreviewScene.CreateChild("PreviewEmitter"); particleEffectPreviewNode.rotation = Quaternion(0, 0, 0); - + ResetCameraTransformation(); particleEffectPreviewGizmoNode = particlePreviewScene.CreateChild("Gizmo"); StaticModel@ gizmo = particleEffectPreviewGizmoNode.CreateComponent("StaticModel"); - gizmo.model = cache.GetResource("Model", "Models/Editor/Axes.mdl"); - gizmo.materials[0] = cache.GetResource("Material", "Materials/Editor/RedUnlit.xml"); - gizmo.materials[1] = cache.GetResource("Material", "Materials/Editor/GreenUnlit.xml"); - gizmo.materials[2] = cache.GetResource("Material", "Materials/Editor/BlueUnlit.xml"); + gizmo.model = cache.GetResource("Model", "Editor/Models/Axes.mdl"); + gizmo.materials[0] = cache.GetResource("Material", "Editor/Materials/RedUnlit.xml"); + gizmo.materials[1] = cache.GetResource("Material", "Editor/Materials/GreenUnlit.xml"); + gizmo.materials[2] = cache.GetResource("Material", "Editor/Materials/BlueUnlit.xml"); gizmo.occludee = false; particleEffectPreviewGridNode = particlePreviewScene.CreateChild("Grid"); @@ -1059,7 +1059,7 @@ void EditParticleEffect(ParticleEffect@ effect) if (!effect.name.empty) cache.ReloadResource(effect); - + editParticleEffect = effect; particleEffectEmitter.effect = editParticleEffect; @@ -1093,7 +1093,7 @@ void RefreshParticleEffectColorFrames() ListView@ lv = particleEffectWindow.GetChild("ColorFrameListView", true); if (lv is null) return; - + lv.RemoveAllItems(); for (uint i = 0; i < editParticleEffect.numColorFrames; i++) @@ -1415,7 +1415,7 @@ void RefreshParticleEffectName() UIElement@ container = particleEffectWindow.GetChild("NameContainer", true); if (container is null) return; - + container.RemoveAllChildren(); LineEdit@ nameEdit = CreateAttributeLineEdit(container, null, 0, 0); @@ -1531,7 +1531,7 @@ void RefreshParticleEffectMaterial() UIElement@ container = particleEffectWindow.GetChild("ParticleMaterialContainer", true); if (container is null) return; - + container.RemoveAllChildren(); LineEdit@ nameEdit = CreateAttributeLineEdit(container, null, 0, 0); @@ -1573,7 +1573,7 @@ void NavigateParticleEffectPreview(StringHash eventType, VariantMap& eventData) particleViewCamDist -= dx * 1.5 * time.timeStep; particleViewCamDist = Max(particleViewCamDist, 0.2); } - particlePreviewCameraNode.position = particleEffectPreviewNode.position + + particlePreviewCameraNode.position = particleEffectPreviewNode.position + Quaternion(particleViewCamRot.x, particleViewCamRot.y, 0) * particleViewCamDir * particleViewCamDist; particlePreviewCameraNode.LookAt(particleEffectPreviewNode.position); @@ -1585,10 +1585,10 @@ void NavigateParticleEffectPreview(StringHash eventType, VariantMap& eventData) void ResizeParticleEffectPreview(StringHash eventType, VariantMap& eventData) { - + float width = float(particleEffectPreview.width); float height = float(particleEffectPreview.height); - + // Manually set aspect ratio because first frame is always returning aspect ratio of 1 float aspectRatio = width / height; particlePreviewCamera.aspectRatio = aspectRatio; @@ -1657,7 +1657,7 @@ void NewParticleEffect() BeginParticleEffectEdit(); EditParticleEffect(CreateNewParticleEffect()); - + EndParticleEffectEdit(); } @@ -1676,11 +1676,11 @@ void RevertParticleEffect() } BeginParticleEffectEdit(); - + cache.ReloadResource(editParticleEffect); EndParticleEffectEdit(); - + RefreshParticleEffectEditor(); } @@ -1772,6 +1772,6 @@ void EndParticleEffectEdit() } inParticleEffectRefresh = false; - + particleEffectPreview.QueueUpdate(); } diff --git a/bin/Data/Scripts/Editor/EditorPreferences.as b/bin/EditorData/Editor/Scripts/EditorPreferences.as similarity index 97% rename from bin/Data/Scripts/Editor/EditorPreferences.as rename to bin/EditorData/Editor/Scripts/EditorPreferences.as index e3fc6ca6147..38ce7917bb6 100644 --- a/bin/Data/Scripts/Editor/EditorPreferences.as +++ b/bin/EditorData/Editor/Scripts/EditorPreferences.as @@ -1,444 +1,444 @@ -// Urho3D editor preferences dialog - -bool subscribedToEditorPreferences = false; -Window@ preferencesDialog; - -LineEdit@ nodeItemTextColorEditR; -LineEdit@ nodeItemTextColorEditG; -LineEdit@ nodeItemTextColorEditB; -LineEdit@ componentItemTextColorEditR; -LineEdit@ componentItemTextColorEditG; -LineEdit@ componentItemTextColorEditB; - -LineEdit@ originalAttributeTextColorEditR; -LineEdit@ originalAttributeTextColorEditG; -LineEdit@ originalAttributeTextColorEditB; -LineEdit@ modifiedAttributeTextColorEditR; -LineEdit@ modifiedAttributeTextColorEditG; -LineEdit@ modifiedAttributeTextColorEditB; -LineEdit@ nonEditableAttributeTextColorEditR; -LineEdit@ nonEditableAttributeTextColorEditG; -LineEdit@ nonEditableAttributeTextColorEditB; - -LineEdit@ defaultZoneAmbientColorEditR; -LineEdit@ defaultZoneAmbientColorEditG; -LineEdit@ defaultZoneAmbientColorEditB; -LineEdit@ defaultZoneFogColorEditR; -LineEdit@ defaultZoneFogColorEditG; -LineEdit@ defaultZoneFogColorEditB; - -LineEdit@ gridColorEditR; -LineEdit@ gridColorEditG; -LineEdit@ gridColorEditB; -LineEdit@ gridSubdivisionColorEditR; -LineEdit@ gridSubdivisionColorEditG; -LineEdit@ gridSubdivisionColorEditB; - -void CreateEditorPreferencesDialog() -{ - if (preferencesDialog !is null) - return; - - preferencesDialog = LoadEditorUI("UI/EditorPreferencesDialog.xml"); - ui.root.AddChild(preferencesDialog); - preferencesDialog.opacity = uiMaxOpacity; - preferencesDialog.height = 440; - CenterDialog(preferencesDialog); - - DropDownList@ languageSelector = preferencesDialog.GetChild("LanguageSelector", true); - for (int i = 0; i < localization.numLanguages; i++) - { - Text@ choice = Text(); - languageSelector.AddItem(choice); - choice.style = "FileSelectorFilterText"; - choice.text = localization.GetLanguage(i); - } - - nodeItemTextColorEditR = preferencesDialog.GetChild("NodeItemTextColor.r", true); - nodeItemTextColorEditG = preferencesDialog.GetChild("NodeItemTextColor.g", true); - nodeItemTextColorEditB = preferencesDialog.GetChild("NodeItemTextColor.b", true); - componentItemTextColorEditR = preferencesDialog.GetChild("ComponentItemTextColor.r", true); - componentItemTextColorEditG = preferencesDialog.GetChild("ComponentItemTextColor.g", true); - componentItemTextColorEditB = preferencesDialog.GetChild("ComponentItemTextColor.b", true); - - originalAttributeTextColorEditR = preferencesDialog.GetChild("OriginalAttributeTextColor.r", true); - originalAttributeTextColorEditG = preferencesDialog.GetChild("OriginalAttributeTextColor.g", true); - originalAttributeTextColorEditB = preferencesDialog.GetChild("OriginalAttributeTextColor.b", true); - modifiedAttributeTextColorEditR = preferencesDialog.GetChild("ModifiedAttributeTextColor.r", true); - modifiedAttributeTextColorEditG = preferencesDialog.GetChild("ModifiedAttributeTextColor.g", true); - modifiedAttributeTextColorEditB = preferencesDialog.GetChild("ModifiedAttributeTextColor.b", true); - nonEditableAttributeTextColorEditR = preferencesDialog.GetChild("NonEditableAttributeTextColor.r", true); - nonEditableAttributeTextColorEditG = preferencesDialog.GetChild("NonEditableAttributeTextColor.g", true); - nonEditableAttributeTextColorEditB = preferencesDialog.GetChild("NonEditableAttributeTextColor.b", true); - - defaultZoneAmbientColorEditR = preferencesDialog.GetChild("DefaultZoneAmbientColor.r", true); - defaultZoneAmbientColorEditG = preferencesDialog.GetChild("DefaultZoneAmbientColor.g", true); - defaultZoneAmbientColorEditB = preferencesDialog.GetChild("DefaultZoneAmbientColor.b", true); - defaultZoneFogColorEditR = preferencesDialog.GetChild("DefaultZoneFogColor.r", true); - defaultZoneFogColorEditG = preferencesDialog.GetChild("DefaultZoneFogColor.g", true); - defaultZoneFogColorEditB = preferencesDialog.GetChild("DefaultZoneFogColor.b", true); - - gridColorEditR = preferencesDialog.GetChild("GridColor.r", true); - gridColorEditG = preferencesDialog.GetChild("GridColor.g", true); - gridColorEditB = preferencesDialog.GetChild("GridColor.b", true); - gridSubdivisionColorEditR = preferencesDialog.GetChild("GridSubdivisionColor.r", true); - gridSubdivisionColorEditG = preferencesDialog.GetChild("GridSubdivisionColor.g", true); - gridSubdivisionColorEditB = preferencesDialog.GetChild("GridSubdivisionColor.b", true); - - UpdateEditorPreferencesDialog(); - HideEditorPreferencesDialog(); -} - -void UpdateEditorPreferencesDialog() -{ - if (preferencesDialog is null) - return; - - DropDownList@ languageSelector = preferencesDialog.GetChild("LanguageSelector", true); - languageSelector.selection = localization.languageIndex; - - LineEdit@ uiMinOpacityEdit = preferencesDialog.GetChild("UIMinOpacity", true); - uiMinOpacityEdit.text = String(uiMinOpacity); - - LineEdit@ uiMaxOpacityEdit = preferencesDialog.GetChild("UIMaxOpacity", true); - uiMaxOpacityEdit.text = String(uiMaxOpacity); - - CheckBox@ showInternalUIElementToggle = preferencesDialog.GetChild("ShowInternalUIElement", true); - showInternalUIElementToggle.checked = showInternalUIElement; - - CheckBox@ showTemporaryObjectToggle = preferencesDialog.GetChild("ShowTemporaryObject", true); - showTemporaryObjectToggle.checked = showTemporaryObject; - - nodeItemTextColorEditR.text = String(nodeTextColor.r); - nodeItemTextColorEditG.text = String(nodeTextColor.g); - nodeItemTextColorEditB.text = String(nodeTextColor.b); - - componentItemTextColorEditR.text = String(componentTextColor.r); - componentItemTextColorEditG.text = String(componentTextColor.g); - componentItemTextColorEditB.text = String(componentTextColor.b); - - CheckBox@ showNonEditableAttributeToggle = preferencesDialog.GetChild("ShowNonEditableAttribute", true); - showNonEditableAttributeToggle.checked = showNonEditableAttribute; - - originalAttributeTextColorEditR.text = String(normalTextColor.r); - originalAttributeTextColorEditG.text = String(normalTextColor.g); - originalAttributeTextColorEditB.text = String(normalTextColor.b); - - modifiedAttributeTextColorEditR.text = String(modifiedTextColor.r); - modifiedAttributeTextColorEditG.text = String(modifiedTextColor.g); - modifiedAttributeTextColorEditB.text = String(modifiedTextColor.b); - - nonEditableAttributeTextColorEditR.text = String(nonEditableTextColor.r); - nonEditableAttributeTextColorEditG.text = String(nonEditableTextColor.g); - nonEditableAttributeTextColorEditB.text = String(nonEditableTextColor.b); - - defaultZoneAmbientColorEditR.text = String(renderer.defaultZone.ambientColor.r); - defaultZoneAmbientColorEditG.text = String(renderer.defaultZone.ambientColor.g); - defaultZoneAmbientColorEditB.text = String(renderer.defaultZone.ambientColor.b); - - defaultZoneFogColorEditR.text = String(renderer.defaultZone.fogColor.r); - defaultZoneFogColorEditG.text = String(renderer.defaultZone.fogColor.g); - defaultZoneFogColorEditB.text = String(renderer.defaultZone.fogColor.b); - - LineEdit@ defaultZoneFogStartEdit = preferencesDialog.GetChild("DefaultZoneFogStart", true); - defaultZoneFogStartEdit.text = String(renderer.defaultZone.fogStart); - LineEdit@ defaultZoneFogEndEdit = preferencesDialog.GetChild("DefaultZoneFogEnd", true); - defaultZoneFogEndEdit.text = String(renderer.defaultZone.fogEnd); - - CheckBox@ showGridToggle = preferencesDialog.GetChild("ShowGrid", true); - showGridToggle.checked = showGrid; - - CheckBox@ grid2DModeToggle = preferencesDialog.GetChild("Grid2DMode", true); - grid2DModeToggle.checked = grid2DMode; - - LineEdit@ gridSizeEdit = preferencesDialog.GetChild("GridSize", true); - gridSizeEdit.text = String(gridSize); - - LineEdit@ gridSubdivisionsEdit = preferencesDialog.GetChild("GridSubdivisions", true); - gridSubdivisionsEdit.text = String(gridSubdivisions); - - LineEdit@ gridScaleEdit = preferencesDialog.GetChild("GridScale", true); - gridScaleEdit.text = String(gridScale); - - gridColorEditR.text = String(gridColor.r); - gridColorEditG.text = String(gridColor.g); - gridColorEditB.text = String(gridColor.b); - gridSubdivisionColorEditR.text = String(gridSubdivisionColor.r); - gridSubdivisionColorEditG.text = String(gridSubdivisionColor.g); - gridSubdivisionColorEditB.text = String(gridSubdivisionColor.b); - - if (!subscribedToEditorPreferences) - { - SubscribeToEvent(uiMinOpacityEdit, "TextFinished", "EditUIMinOpacity"); - SubscribeToEvent(uiMaxOpacityEdit, "TextFinished", "EditUIMaxOpacity"); - SubscribeToEvent(showInternalUIElementToggle, "Toggled", "ToggleShowInternalUIElement"); - SubscribeToEvent(showTemporaryObjectToggle, "Toggled", "ToggleShowTemporaryObject"); - SubscribeToEvent(nodeItemTextColorEditR, "TextFinished", "EditNodeTextColor"); - SubscribeToEvent(nodeItemTextColorEditG, "TextFinished", "EditNodeTextColor"); - SubscribeToEvent(nodeItemTextColorEditB, "TextFinished", "EditNodeTextColor"); - SubscribeToEvent(componentItemTextColorEditR, "TextFinished", "EditComponentTextColor"); - SubscribeToEvent(componentItemTextColorEditG, "TextFinished", "EditComponentTextColor"); - SubscribeToEvent(componentItemTextColorEditB, "TextFinished", "EditComponentTextColor"); - SubscribeToEvent(showNonEditableAttributeToggle, "Toggled", "ToggleShowNonEditableAttribute"); - SubscribeToEvent(originalAttributeTextColorEditR, "TextFinished", "EditOriginalAttributeTextColor"); - SubscribeToEvent(originalAttributeTextColorEditG, "TextFinished", "EditOriginalAttributeTextColor"); - SubscribeToEvent(originalAttributeTextColorEditB, "TextFinished", "EditOriginalAttributeTextColor"); - SubscribeToEvent(modifiedAttributeTextColorEditR, "TextFinished", "EditModifiedAttributeTextColor"); - SubscribeToEvent(modifiedAttributeTextColorEditG, "TextFinished", "EditModifiedAttributeTextColor"); - SubscribeToEvent(modifiedAttributeTextColorEditB, "TextFinished", "EditModifiedAttributeTextColor"); - SubscribeToEvent(nonEditableAttributeTextColorEditR, "TextFinished", "EditNonEditableAttributeTextColor"); - SubscribeToEvent(nonEditableAttributeTextColorEditG, "TextFinished", "EditNonEditableAttributeTextColor"); - SubscribeToEvent(nonEditableAttributeTextColorEditB, "TextFinished", "EditNonEditableAttributeTextColor"); - SubscribeToEvent(defaultZoneAmbientColorEditR, "TextFinished", "EditDefaultZoneAmbientColor"); - SubscribeToEvent(defaultZoneAmbientColorEditG, "TextFinished", "EditDefaultZoneAmbientColor"); - SubscribeToEvent(defaultZoneAmbientColorEditB, "TextFinished", "EditDefaultZoneAmbientColor"); - SubscribeToEvent(defaultZoneFogColorEditR, "TextFinished", "EditDefaultZoneFogColor"); - SubscribeToEvent(defaultZoneFogColorEditG, "TextFinished", "EditDefaultZoneFogColor"); - SubscribeToEvent(defaultZoneFogColorEditB, "TextFinished", "EditDefaultZoneFogColor"); - SubscribeToEvent(defaultZoneFogStartEdit, "TextFinished", "EditDefaultZoneFogStart"); - SubscribeToEvent(defaultZoneFogEndEdit, "TextFinished", "EditDefaultZoneFogEnd"); - SubscribeToEvent(showGridToggle, "Toggled", "ToggleShowGrid"); - SubscribeToEvent(grid2DModeToggle, "Toggled", "ToggleGrid2DMode"); - SubscribeToEvent(gridSizeEdit, "TextFinished", "EditGridSize"); - SubscribeToEvent(gridSubdivisionsEdit, "TextFinished", "EditGridSubdivisions"); - SubscribeToEvent(gridScaleEdit, "TextFinished", "EditGridScale"); - SubscribeToEvent(gridColorEditR, "TextFinished", "EditGridColor"); - SubscribeToEvent(gridColorEditG, "TextFinished", "EditGridColor"); - SubscribeToEvent(gridColorEditB, "TextFinished", "EditGridColor"); - SubscribeToEvent(languageSelector, "ItemSelected", "EditLanguageSelector"); - SubscribeToEvent(gridSubdivisionColorEditR, "TextFinished", "EditGridSubdivisionColor"); - SubscribeToEvent(gridSubdivisionColorEditG, "TextFinished", "EditGridSubdivisionColor"); - SubscribeToEvent(gridSubdivisionColorEditB, "TextFinished", "EditGridSubdivisionColor"); - SubscribeToEvent(preferencesDialog.GetChild("CloseButton", true), "Released", "HideEditorPreferencesDialog"); - subscribedToEditorPreferences = true; - } -} - -void EditLanguageSelector(StringHash eventType, VariantMap& eventData) -{ - DropDownList@ edit = eventData["Element"].GetPtr(); - localization.SetLanguage(edit.selection); -} - -bool ToggleEditorPreferencesDialog() -{ - if (preferencesDialog.visible == false) - ShowEditorPreferencesDialog(); - else - HideEditorPreferencesDialog(); - return true; -} - -void ShowEditorPreferencesDialog() -{ - UpdateEditorPreferencesDialog(); - preferencesDialog.visible = true; - preferencesDialog.BringToFront(); -} - -void HideEditorPreferencesDialog() -{ - preferencesDialog.visible = false; -} - -void EditUIMinOpacity(StringHash eventType, VariantMap& eventData) -{ - LineEdit@ edit = eventData["Element"].GetPtr(); - uiMinOpacity = edit.text.ToFloat(); - edit.text = String(uiMinOpacity); - FadeUI(); - UnfadeUI(); -} - -void EditUIMaxOpacity(StringHash eventType, VariantMap& eventData) -{ - LineEdit@ edit = eventData["Element"].GetPtr(); - uiMaxOpacity = edit.text.ToFloat(); - edit.text = String(uiMaxOpacity); - FadeUI(); - UnfadeUI(); -} - -void ToggleShowInternalUIElement(StringHash eventType, VariantMap& eventData) -{ - showInternalUIElement = cast(eventData["Element"].GetPtr()).checked; - UpdateHierarchyItem(editorUIElement, true); -} - -void ToggleShowTemporaryObject(StringHash eventType, VariantMap& eventData) -{ - showTemporaryObject = cast(eventData["Element"].GetPtr()).checked; - UpdateHierarchyItem(editorScene, true); - UpdateHierarchyItem(editorUIElement, true); -} - -void EditNodeTextColor(StringHash eventType, VariantMap& eventData) -{ - LineEdit@ edit = eventData["Element"].GetPtr(); - nodeTextColor = Color(nodeItemTextColorEditR.text.ToFloat(), nodeItemTextColorEditG.text.ToFloat(), nodeItemTextColorEditB.text.ToFloat()); - if (edit.name == "NodeItemTextColor.r") - edit.text = String(normalTextColor.r); - else if (edit.name == "NodeItemTextColor.g") - edit.text = String(normalTextColor.g); - else if (edit.name == "NodeItemTextColor.b") - edit.text = String(normalTextColor.b); - UpdateHierarchyItem(editorScene); -} - -void EditComponentTextColor(StringHash eventType, VariantMap& eventData) -{ - LineEdit@ edit = eventData["Element"].GetPtr(); - componentTextColor = Color(componentItemTextColorEditR.text.ToFloat(), componentItemTextColorEditG.text.ToFloat(), componentItemTextColorEditB.text.ToFloat()); - if (edit.name == "ComponentItemTextColor.r") - edit.text = String(normalTextColor.r); - else if (edit.name == "ComponentItemTextColor.g") - edit.text = String(normalTextColor.g); - else if (edit.name == "ComponentItemTextColor.b") - edit.text = String(normalTextColor.b); - UpdateHierarchyItem(editorScene); -} - -void ToggleShowNonEditableAttribute(StringHash eventType, VariantMap& eventData) -{ - showNonEditableAttribute = cast(eventData["Element"].GetPtr()).checked; - UpdateAttributeInspector(true); -} - -void EditOriginalAttributeTextColor(StringHash eventType, VariantMap& eventData) -{ - LineEdit@ edit = eventData["Element"].GetPtr(); - normalTextColor = Color(originalAttributeTextColorEditR.text.ToFloat(), originalAttributeTextColorEditG.text.ToFloat(), originalAttributeTextColorEditB.text.ToFloat()); - if (edit.name == "OriginalAttributeTextColor.r") - edit.text = String(normalTextColor.r); - else if (edit.name == "OriginalAttributeTextColor.g") - edit.text = String(normalTextColor.g); - else if (edit.name == "OriginalAttributeTextColor.b") - edit.text = String(normalTextColor.b); - UpdateAttributeInspector(false); -} - -void EditModifiedAttributeTextColor(StringHash eventType, VariantMap& eventData) -{ - LineEdit@ edit = eventData["Element"].GetPtr(); - modifiedTextColor = Color(modifiedAttributeTextColorEditR.text.ToFloat(), modifiedAttributeTextColorEditG.text.ToFloat(), modifiedAttributeTextColorEditB.text.ToFloat()); - if (edit.name == "ModifiedAttributeTextColor.r") - edit.text = String(modifiedTextColor.r); - else if (edit.name == "ModifiedAttributeTextColor.g") - edit.text = String(modifiedTextColor.g); - else if (edit.name == "ModifiedAttributeTextColor.b") - edit.text = String(modifiedTextColor.b); - UpdateAttributeInspector(false); -} - -void EditNonEditableAttributeTextColor(StringHash eventType, VariantMap& eventData) -{ - LineEdit@ edit = eventData["Element"].GetPtr(); - nonEditableTextColor = Color(nonEditableAttributeTextColorEditR.text.ToFloat(), nonEditableAttributeTextColorEditG.text.ToFloat(), nonEditableAttributeTextColorEditB.text.ToFloat()); - if (edit.name == "NonEditableAttributeTextColor.r") - edit.text = String(nonEditableTextColor.r); - else if (edit.name == "NonEditableAttributeTextColor.g") - edit.text = String(nonEditableTextColor.g); - else if (edit.name == "NonEditableAttributeTextColor.b") - edit.text = String(nonEditableTextColor.b); - UpdateAttributeInspector(false); -} - -void EditDefaultZoneAmbientColor(StringHash eventType, VariantMap& eventData) -{ - LineEdit@ edit = eventData["Element"].GetPtr(); - renderer.defaultZone.ambientColor = Color(defaultZoneAmbientColorEditR.text.ToFloat(), defaultZoneAmbientColorEditG.text.ToFloat(), defaultZoneAmbientColorEditB.text.ToFloat()); - if (edit.name == "DefaultZoneAmbientColor.r") - edit.text = String(renderer.defaultZone.ambientColor.r); - else if (edit.name == "DefaultZoneAmbientColor.g") - edit.text = String(renderer.defaultZone.ambientColor.g); - else if (edit.name == "DefaultZoneAmbientColor.b") - edit.text = String(renderer.defaultZone.ambientColor.b); -} - -void EditDefaultZoneFogColor(StringHash eventType, VariantMap& eventData) -{ - LineEdit@ edit = eventData["Element"].GetPtr(); - renderer.defaultZone.fogColor = Color(defaultZoneFogColorEditR.text.ToFloat(), defaultZoneFogColorEditG.text.ToFloat(), defaultZoneFogColorEditB.text.ToFloat()); - if (edit.name == "DefaultZoneFogColor.r") - edit.text = String(renderer.defaultZone.fogColor.r); - else if (edit.name == "DefaultZoneFogColor.g") - edit.text = String(renderer.defaultZone.fogColor.g); - else if (edit.name == "DefaultZoneFogColor.b") - edit.text = String(renderer.defaultZone.fogColor.b); -} - -void EditDefaultZoneFogStart(StringHash eventType, VariantMap& eventData) -{ - LineEdit@ edit = eventData["Element"].GetPtr(); - renderer.defaultZone.fogStart = edit.text.ToFloat(); - edit.text = String(renderer.defaultZone.fogStart); -} - -void EditDefaultZoneFogEnd(StringHash eventType, VariantMap& eventData) -{ - LineEdit@ edit = eventData["Element"].GetPtr(); - renderer.defaultZone.fogEnd = edit.text.ToFloat(); - edit.text = String(renderer.defaultZone.fogEnd); -} - -void ToggleShowGrid(StringHash eventType, VariantMap& eventData) -{ - showGrid = cast(eventData["Element"].GetPtr()).checked; - UpdateGrid(false); -} - -void ToggleGrid2DMode(StringHash eventType, VariantMap& eventData) -{ - grid2DMode = cast(eventData["Element"].GetPtr()).checked; - UpdateGrid(); -} - -void EditGridSize(StringHash eventType, VariantMap& eventData) -{ - LineEdit@ edit = eventData["Element"].GetPtr(); - gridSize = edit.text.ToI32(); - edit.text = String(gridSize); - UpdateGrid(); -} - -void EditGridSubdivisions(StringHash eventType, VariantMap& eventData) -{ - LineEdit@ edit = eventData["Element"].GetPtr(); - gridSubdivisions = edit.text.ToI32(); - edit.text = String(gridSubdivisions); - UpdateGrid(); -} - -void EditGridScale(StringHash eventType, VariantMap& eventData) -{ - LineEdit@ edit = eventData["Element"].GetPtr(); - gridScale = edit.text.ToFloat(); - edit.text = String(gridScale); - UpdateGrid(false); -} - -void EditGridColor(StringHash eventType, VariantMap& eventData) -{ - LineEdit@ edit = eventData["Element"].GetPtr(); - gridColor = Color(gridColorEditR.text.ToFloat(), gridColorEditG.text.ToFloat(), gridColorEditB.text.ToFloat()); - if (edit.name == "GridColor.r") - edit.text = String(gridColor.r); - else if (edit.name == "GridColor.g") - edit.text = String(gridColor.g); - else if (edit.name == "GridColor.b") - edit.text = String(gridColor.b); - UpdateGrid(); -} - -void EditGridSubdivisionColor(StringHash eventType, VariantMap& eventData) -{ - LineEdit@ edit = eventData["Element"].GetPtr(); - gridSubdivisionColor = Color(gridSubdivisionColorEditR.text.ToFloat(), gridSubdivisionColorEditG.text.ToFloat(), gridSubdivisionColorEditB.text.ToFloat()); - if (edit.name == "GridSubdivisionColor.r") - edit.text = String(gridSubdivisionColor.r); - else if (edit.name == "GridSubdivisionColor.g") - edit.text = String(gridSubdivisionColor.g); - else if (edit.name == "GridSubdivisionColor.b") - edit.text = String(gridSubdivisionColor.b); - UpdateGrid(); -} +// Urho3D editor preferences dialog + +bool subscribedToEditorPreferences = false; +Window@ preferencesDialog; + +LineEdit@ nodeItemTextColorEditR; +LineEdit@ nodeItemTextColorEditG; +LineEdit@ nodeItemTextColorEditB; +LineEdit@ componentItemTextColorEditR; +LineEdit@ componentItemTextColorEditG; +LineEdit@ componentItemTextColorEditB; + +LineEdit@ originalAttributeTextColorEditR; +LineEdit@ originalAttributeTextColorEditG; +LineEdit@ originalAttributeTextColorEditB; +LineEdit@ modifiedAttributeTextColorEditR; +LineEdit@ modifiedAttributeTextColorEditG; +LineEdit@ modifiedAttributeTextColorEditB; +LineEdit@ nonEditableAttributeTextColorEditR; +LineEdit@ nonEditableAttributeTextColorEditG; +LineEdit@ nonEditableAttributeTextColorEditB; + +LineEdit@ defaultZoneAmbientColorEditR; +LineEdit@ defaultZoneAmbientColorEditG; +LineEdit@ defaultZoneAmbientColorEditB; +LineEdit@ defaultZoneFogColorEditR; +LineEdit@ defaultZoneFogColorEditG; +LineEdit@ defaultZoneFogColorEditB; + +LineEdit@ gridColorEditR; +LineEdit@ gridColorEditG; +LineEdit@ gridColorEditB; +LineEdit@ gridSubdivisionColorEditR; +LineEdit@ gridSubdivisionColorEditG; +LineEdit@ gridSubdivisionColorEditB; + +void CreateEditorPreferencesDialog() +{ + if (preferencesDialog !is null) + return; + + preferencesDialog = LoadEditorUI("Editor/UI/EditorPreferencesDialog.xml"); + ui.root.AddChild(preferencesDialog); + preferencesDialog.opacity = uiMaxOpacity; + preferencesDialog.height = 440; + CenterDialog(preferencesDialog); + + DropDownList@ languageSelector = preferencesDialog.GetChild("LanguageSelector", true); + for (int i = 0; i < localization.numLanguages; i++) + { + Text@ choice = Text(); + languageSelector.AddItem(choice); + choice.style = "FileSelectorFilterText"; + choice.text = localization.GetLanguage(i); + } + + nodeItemTextColorEditR = preferencesDialog.GetChild("NodeItemTextColor.r", true); + nodeItemTextColorEditG = preferencesDialog.GetChild("NodeItemTextColor.g", true); + nodeItemTextColorEditB = preferencesDialog.GetChild("NodeItemTextColor.b", true); + componentItemTextColorEditR = preferencesDialog.GetChild("ComponentItemTextColor.r", true); + componentItemTextColorEditG = preferencesDialog.GetChild("ComponentItemTextColor.g", true); + componentItemTextColorEditB = preferencesDialog.GetChild("ComponentItemTextColor.b", true); + + originalAttributeTextColorEditR = preferencesDialog.GetChild("OriginalAttributeTextColor.r", true); + originalAttributeTextColorEditG = preferencesDialog.GetChild("OriginalAttributeTextColor.g", true); + originalAttributeTextColorEditB = preferencesDialog.GetChild("OriginalAttributeTextColor.b", true); + modifiedAttributeTextColorEditR = preferencesDialog.GetChild("ModifiedAttributeTextColor.r", true); + modifiedAttributeTextColorEditG = preferencesDialog.GetChild("ModifiedAttributeTextColor.g", true); + modifiedAttributeTextColorEditB = preferencesDialog.GetChild("ModifiedAttributeTextColor.b", true); + nonEditableAttributeTextColorEditR = preferencesDialog.GetChild("NonEditableAttributeTextColor.r", true); + nonEditableAttributeTextColorEditG = preferencesDialog.GetChild("NonEditableAttributeTextColor.g", true); + nonEditableAttributeTextColorEditB = preferencesDialog.GetChild("NonEditableAttributeTextColor.b", true); + + defaultZoneAmbientColorEditR = preferencesDialog.GetChild("DefaultZoneAmbientColor.r", true); + defaultZoneAmbientColorEditG = preferencesDialog.GetChild("DefaultZoneAmbientColor.g", true); + defaultZoneAmbientColorEditB = preferencesDialog.GetChild("DefaultZoneAmbientColor.b", true); + defaultZoneFogColorEditR = preferencesDialog.GetChild("DefaultZoneFogColor.r", true); + defaultZoneFogColorEditG = preferencesDialog.GetChild("DefaultZoneFogColor.g", true); + defaultZoneFogColorEditB = preferencesDialog.GetChild("DefaultZoneFogColor.b", true); + + gridColorEditR = preferencesDialog.GetChild("GridColor.r", true); + gridColorEditG = preferencesDialog.GetChild("GridColor.g", true); + gridColorEditB = preferencesDialog.GetChild("GridColor.b", true); + gridSubdivisionColorEditR = preferencesDialog.GetChild("GridSubdivisionColor.r", true); + gridSubdivisionColorEditG = preferencesDialog.GetChild("GridSubdivisionColor.g", true); + gridSubdivisionColorEditB = preferencesDialog.GetChild("GridSubdivisionColor.b", true); + + UpdateEditorPreferencesDialog(); + HideEditorPreferencesDialog(); +} + +void UpdateEditorPreferencesDialog() +{ + if (preferencesDialog is null) + return; + + DropDownList@ languageSelector = preferencesDialog.GetChild("LanguageSelector", true); + languageSelector.selection = localization.languageIndex; + + LineEdit@ uiMinOpacityEdit = preferencesDialog.GetChild("UIMinOpacity", true); + uiMinOpacityEdit.text = String(uiMinOpacity); + + LineEdit@ uiMaxOpacityEdit = preferencesDialog.GetChild("UIMaxOpacity", true); + uiMaxOpacityEdit.text = String(uiMaxOpacity); + + CheckBox@ showInternalUIElementToggle = preferencesDialog.GetChild("ShowInternalUIElement", true); + showInternalUIElementToggle.checked = showInternalUIElement; + + CheckBox@ showTemporaryObjectToggle = preferencesDialog.GetChild("ShowTemporaryObject", true); + showTemporaryObjectToggle.checked = showTemporaryObject; + + nodeItemTextColorEditR.text = String(nodeTextColor.r); + nodeItemTextColorEditG.text = String(nodeTextColor.g); + nodeItemTextColorEditB.text = String(nodeTextColor.b); + + componentItemTextColorEditR.text = String(componentTextColor.r); + componentItemTextColorEditG.text = String(componentTextColor.g); + componentItemTextColorEditB.text = String(componentTextColor.b); + + CheckBox@ showNonEditableAttributeToggle = preferencesDialog.GetChild("ShowNonEditableAttribute", true); + showNonEditableAttributeToggle.checked = showNonEditableAttribute; + + originalAttributeTextColorEditR.text = String(normalTextColor.r); + originalAttributeTextColorEditG.text = String(normalTextColor.g); + originalAttributeTextColorEditB.text = String(normalTextColor.b); + + modifiedAttributeTextColorEditR.text = String(modifiedTextColor.r); + modifiedAttributeTextColorEditG.text = String(modifiedTextColor.g); + modifiedAttributeTextColorEditB.text = String(modifiedTextColor.b); + + nonEditableAttributeTextColorEditR.text = String(nonEditableTextColor.r); + nonEditableAttributeTextColorEditG.text = String(nonEditableTextColor.g); + nonEditableAttributeTextColorEditB.text = String(nonEditableTextColor.b); + + defaultZoneAmbientColorEditR.text = String(renderer.defaultZone.ambientColor.r); + defaultZoneAmbientColorEditG.text = String(renderer.defaultZone.ambientColor.g); + defaultZoneAmbientColorEditB.text = String(renderer.defaultZone.ambientColor.b); + + defaultZoneFogColorEditR.text = String(renderer.defaultZone.fogColor.r); + defaultZoneFogColorEditG.text = String(renderer.defaultZone.fogColor.g); + defaultZoneFogColorEditB.text = String(renderer.defaultZone.fogColor.b); + + LineEdit@ defaultZoneFogStartEdit = preferencesDialog.GetChild("DefaultZoneFogStart", true); + defaultZoneFogStartEdit.text = String(renderer.defaultZone.fogStart); + LineEdit@ defaultZoneFogEndEdit = preferencesDialog.GetChild("DefaultZoneFogEnd", true); + defaultZoneFogEndEdit.text = String(renderer.defaultZone.fogEnd); + + CheckBox@ showGridToggle = preferencesDialog.GetChild("ShowGrid", true); + showGridToggle.checked = showGrid; + + CheckBox@ grid2DModeToggle = preferencesDialog.GetChild("Grid2DMode", true); + grid2DModeToggle.checked = grid2DMode; + + LineEdit@ gridSizeEdit = preferencesDialog.GetChild("GridSize", true); + gridSizeEdit.text = String(gridSize); + + LineEdit@ gridSubdivisionsEdit = preferencesDialog.GetChild("GridSubdivisions", true); + gridSubdivisionsEdit.text = String(gridSubdivisions); + + LineEdit@ gridScaleEdit = preferencesDialog.GetChild("GridScale", true); + gridScaleEdit.text = String(gridScale); + + gridColorEditR.text = String(gridColor.r); + gridColorEditG.text = String(gridColor.g); + gridColorEditB.text = String(gridColor.b); + gridSubdivisionColorEditR.text = String(gridSubdivisionColor.r); + gridSubdivisionColorEditG.text = String(gridSubdivisionColor.g); + gridSubdivisionColorEditB.text = String(gridSubdivisionColor.b); + + if (!subscribedToEditorPreferences) + { + SubscribeToEvent(uiMinOpacityEdit, "TextFinished", "EditUIMinOpacity"); + SubscribeToEvent(uiMaxOpacityEdit, "TextFinished", "EditUIMaxOpacity"); + SubscribeToEvent(showInternalUIElementToggle, "Toggled", "ToggleShowInternalUIElement"); + SubscribeToEvent(showTemporaryObjectToggle, "Toggled", "ToggleShowTemporaryObject"); + SubscribeToEvent(nodeItemTextColorEditR, "TextFinished", "EditNodeTextColor"); + SubscribeToEvent(nodeItemTextColorEditG, "TextFinished", "EditNodeTextColor"); + SubscribeToEvent(nodeItemTextColorEditB, "TextFinished", "EditNodeTextColor"); + SubscribeToEvent(componentItemTextColorEditR, "TextFinished", "EditComponentTextColor"); + SubscribeToEvent(componentItemTextColorEditG, "TextFinished", "EditComponentTextColor"); + SubscribeToEvent(componentItemTextColorEditB, "TextFinished", "EditComponentTextColor"); + SubscribeToEvent(showNonEditableAttributeToggle, "Toggled", "ToggleShowNonEditableAttribute"); + SubscribeToEvent(originalAttributeTextColorEditR, "TextFinished", "EditOriginalAttributeTextColor"); + SubscribeToEvent(originalAttributeTextColorEditG, "TextFinished", "EditOriginalAttributeTextColor"); + SubscribeToEvent(originalAttributeTextColorEditB, "TextFinished", "EditOriginalAttributeTextColor"); + SubscribeToEvent(modifiedAttributeTextColorEditR, "TextFinished", "EditModifiedAttributeTextColor"); + SubscribeToEvent(modifiedAttributeTextColorEditG, "TextFinished", "EditModifiedAttributeTextColor"); + SubscribeToEvent(modifiedAttributeTextColorEditB, "TextFinished", "EditModifiedAttributeTextColor"); + SubscribeToEvent(nonEditableAttributeTextColorEditR, "TextFinished", "EditNonEditableAttributeTextColor"); + SubscribeToEvent(nonEditableAttributeTextColorEditG, "TextFinished", "EditNonEditableAttributeTextColor"); + SubscribeToEvent(nonEditableAttributeTextColorEditB, "TextFinished", "EditNonEditableAttributeTextColor"); + SubscribeToEvent(defaultZoneAmbientColorEditR, "TextFinished", "EditDefaultZoneAmbientColor"); + SubscribeToEvent(defaultZoneAmbientColorEditG, "TextFinished", "EditDefaultZoneAmbientColor"); + SubscribeToEvent(defaultZoneAmbientColorEditB, "TextFinished", "EditDefaultZoneAmbientColor"); + SubscribeToEvent(defaultZoneFogColorEditR, "TextFinished", "EditDefaultZoneFogColor"); + SubscribeToEvent(defaultZoneFogColorEditG, "TextFinished", "EditDefaultZoneFogColor"); + SubscribeToEvent(defaultZoneFogColorEditB, "TextFinished", "EditDefaultZoneFogColor"); + SubscribeToEvent(defaultZoneFogStartEdit, "TextFinished", "EditDefaultZoneFogStart"); + SubscribeToEvent(defaultZoneFogEndEdit, "TextFinished", "EditDefaultZoneFogEnd"); + SubscribeToEvent(showGridToggle, "Toggled", "ToggleShowGrid"); + SubscribeToEvent(grid2DModeToggle, "Toggled", "ToggleGrid2DMode"); + SubscribeToEvent(gridSizeEdit, "TextFinished", "EditGridSize"); + SubscribeToEvent(gridSubdivisionsEdit, "TextFinished", "EditGridSubdivisions"); + SubscribeToEvent(gridScaleEdit, "TextFinished", "EditGridScale"); + SubscribeToEvent(gridColorEditR, "TextFinished", "EditGridColor"); + SubscribeToEvent(gridColorEditG, "TextFinished", "EditGridColor"); + SubscribeToEvent(gridColorEditB, "TextFinished", "EditGridColor"); + SubscribeToEvent(languageSelector, "ItemSelected", "EditLanguageSelector"); + SubscribeToEvent(gridSubdivisionColorEditR, "TextFinished", "EditGridSubdivisionColor"); + SubscribeToEvent(gridSubdivisionColorEditG, "TextFinished", "EditGridSubdivisionColor"); + SubscribeToEvent(gridSubdivisionColorEditB, "TextFinished", "EditGridSubdivisionColor"); + SubscribeToEvent(preferencesDialog.GetChild("CloseButton", true), "Released", "HideEditorPreferencesDialog"); + subscribedToEditorPreferences = true; + } +} + +void EditLanguageSelector(StringHash eventType, VariantMap& eventData) +{ + DropDownList@ edit = eventData["Element"].GetPtr(); + localization.SetLanguage(edit.selection); +} + +bool ToggleEditorPreferencesDialog() +{ + if (preferencesDialog.visible == false) + ShowEditorPreferencesDialog(); + else + HideEditorPreferencesDialog(); + return true; +} + +void ShowEditorPreferencesDialog() +{ + UpdateEditorPreferencesDialog(); + preferencesDialog.visible = true; + preferencesDialog.BringToFront(); +} + +void HideEditorPreferencesDialog() +{ + preferencesDialog.visible = false; +} + +void EditUIMinOpacity(StringHash eventType, VariantMap& eventData) +{ + LineEdit@ edit = eventData["Element"].GetPtr(); + uiMinOpacity = edit.text.ToFloat(); + edit.text = String(uiMinOpacity); + FadeUI(); + UnfadeUI(); +} + +void EditUIMaxOpacity(StringHash eventType, VariantMap& eventData) +{ + LineEdit@ edit = eventData["Element"].GetPtr(); + uiMaxOpacity = edit.text.ToFloat(); + edit.text = String(uiMaxOpacity); + FadeUI(); + UnfadeUI(); +} + +void ToggleShowInternalUIElement(StringHash eventType, VariantMap& eventData) +{ + showInternalUIElement = cast(eventData["Element"].GetPtr()).checked; + UpdateHierarchyItem(editorUIElement, true); +} + +void ToggleShowTemporaryObject(StringHash eventType, VariantMap& eventData) +{ + showTemporaryObject = cast(eventData["Element"].GetPtr()).checked; + UpdateHierarchyItem(editorScene, true); + UpdateHierarchyItem(editorUIElement, true); +} + +void EditNodeTextColor(StringHash eventType, VariantMap& eventData) +{ + LineEdit@ edit = eventData["Element"].GetPtr(); + nodeTextColor = Color(nodeItemTextColorEditR.text.ToFloat(), nodeItemTextColorEditG.text.ToFloat(), nodeItemTextColorEditB.text.ToFloat()); + if (edit.name == "NodeItemTextColor.r") + edit.text = String(normalTextColor.r); + else if (edit.name == "NodeItemTextColor.g") + edit.text = String(normalTextColor.g); + else if (edit.name == "NodeItemTextColor.b") + edit.text = String(normalTextColor.b); + UpdateHierarchyItem(editorScene); +} + +void EditComponentTextColor(StringHash eventType, VariantMap& eventData) +{ + LineEdit@ edit = eventData["Element"].GetPtr(); + componentTextColor = Color(componentItemTextColorEditR.text.ToFloat(), componentItemTextColorEditG.text.ToFloat(), componentItemTextColorEditB.text.ToFloat()); + if (edit.name == "ComponentItemTextColor.r") + edit.text = String(normalTextColor.r); + else if (edit.name == "ComponentItemTextColor.g") + edit.text = String(normalTextColor.g); + else if (edit.name == "ComponentItemTextColor.b") + edit.text = String(normalTextColor.b); + UpdateHierarchyItem(editorScene); +} + +void ToggleShowNonEditableAttribute(StringHash eventType, VariantMap& eventData) +{ + showNonEditableAttribute = cast(eventData["Element"].GetPtr()).checked; + UpdateAttributeInspector(true); +} + +void EditOriginalAttributeTextColor(StringHash eventType, VariantMap& eventData) +{ + LineEdit@ edit = eventData["Element"].GetPtr(); + normalTextColor = Color(originalAttributeTextColorEditR.text.ToFloat(), originalAttributeTextColorEditG.text.ToFloat(), originalAttributeTextColorEditB.text.ToFloat()); + if (edit.name == "OriginalAttributeTextColor.r") + edit.text = String(normalTextColor.r); + else if (edit.name == "OriginalAttributeTextColor.g") + edit.text = String(normalTextColor.g); + else if (edit.name == "OriginalAttributeTextColor.b") + edit.text = String(normalTextColor.b); + UpdateAttributeInspector(false); +} + +void EditModifiedAttributeTextColor(StringHash eventType, VariantMap& eventData) +{ + LineEdit@ edit = eventData["Element"].GetPtr(); + modifiedTextColor = Color(modifiedAttributeTextColorEditR.text.ToFloat(), modifiedAttributeTextColorEditG.text.ToFloat(), modifiedAttributeTextColorEditB.text.ToFloat()); + if (edit.name == "ModifiedAttributeTextColor.r") + edit.text = String(modifiedTextColor.r); + else if (edit.name == "ModifiedAttributeTextColor.g") + edit.text = String(modifiedTextColor.g); + else if (edit.name == "ModifiedAttributeTextColor.b") + edit.text = String(modifiedTextColor.b); + UpdateAttributeInspector(false); +} + +void EditNonEditableAttributeTextColor(StringHash eventType, VariantMap& eventData) +{ + LineEdit@ edit = eventData["Element"].GetPtr(); + nonEditableTextColor = Color(nonEditableAttributeTextColorEditR.text.ToFloat(), nonEditableAttributeTextColorEditG.text.ToFloat(), nonEditableAttributeTextColorEditB.text.ToFloat()); + if (edit.name == "NonEditableAttributeTextColor.r") + edit.text = String(nonEditableTextColor.r); + else if (edit.name == "NonEditableAttributeTextColor.g") + edit.text = String(nonEditableTextColor.g); + else if (edit.name == "NonEditableAttributeTextColor.b") + edit.text = String(nonEditableTextColor.b); + UpdateAttributeInspector(false); +} + +void EditDefaultZoneAmbientColor(StringHash eventType, VariantMap& eventData) +{ + LineEdit@ edit = eventData["Element"].GetPtr(); + renderer.defaultZone.ambientColor = Color(defaultZoneAmbientColorEditR.text.ToFloat(), defaultZoneAmbientColorEditG.text.ToFloat(), defaultZoneAmbientColorEditB.text.ToFloat()); + if (edit.name == "DefaultZoneAmbientColor.r") + edit.text = String(renderer.defaultZone.ambientColor.r); + else if (edit.name == "DefaultZoneAmbientColor.g") + edit.text = String(renderer.defaultZone.ambientColor.g); + else if (edit.name == "DefaultZoneAmbientColor.b") + edit.text = String(renderer.defaultZone.ambientColor.b); +} + +void EditDefaultZoneFogColor(StringHash eventType, VariantMap& eventData) +{ + LineEdit@ edit = eventData["Element"].GetPtr(); + renderer.defaultZone.fogColor = Color(defaultZoneFogColorEditR.text.ToFloat(), defaultZoneFogColorEditG.text.ToFloat(), defaultZoneFogColorEditB.text.ToFloat()); + if (edit.name == "DefaultZoneFogColor.r") + edit.text = String(renderer.defaultZone.fogColor.r); + else if (edit.name == "DefaultZoneFogColor.g") + edit.text = String(renderer.defaultZone.fogColor.g); + else if (edit.name == "DefaultZoneFogColor.b") + edit.text = String(renderer.defaultZone.fogColor.b); +} + +void EditDefaultZoneFogStart(StringHash eventType, VariantMap& eventData) +{ + LineEdit@ edit = eventData["Element"].GetPtr(); + renderer.defaultZone.fogStart = edit.text.ToFloat(); + edit.text = String(renderer.defaultZone.fogStart); +} + +void EditDefaultZoneFogEnd(StringHash eventType, VariantMap& eventData) +{ + LineEdit@ edit = eventData["Element"].GetPtr(); + renderer.defaultZone.fogEnd = edit.text.ToFloat(); + edit.text = String(renderer.defaultZone.fogEnd); +} + +void ToggleShowGrid(StringHash eventType, VariantMap& eventData) +{ + showGrid = cast(eventData["Element"].GetPtr()).checked; + UpdateGrid(false); +} + +void ToggleGrid2DMode(StringHash eventType, VariantMap& eventData) +{ + grid2DMode = cast(eventData["Element"].GetPtr()).checked; + UpdateGrid(); +} + +void EditGridSize(StringHash eventType, VariantMap& eventData) +{ + LineEdit@ edit = eventData["Element"].GetPtr(); + gridSize = edit.text.ToI32(); + edit.text = String(gridSize); + UpdateGrid(); +} + +void EditGridSubdivisions(StringHash eventType, VariantMap& eventData) +{ + LineEdit@ edit = eventData["Element"].GetPtr(); + gridSubdivisions = edit.text.ToI32(); + edit.text = String(gridSubdivisions); + UpdateGrid(); +} + +void EditGridScale(StringHash eventType, VariantMap& eventData) +{ + LineEdit@ edit = eventData["Element"].GetPtr(); + gridScale = edit.text.ToFloat(); + edit.text = String(gridScale); + UpdateGrid(false); +} + +void EditGridColor(StringHash eventType, VariantMap& eventData) +{ + LineEdit@ edit = eventData["Element"].GetPtr(); + gridColor = Color(gridColorEditR.text.ToFloat(), gridColorEditG.text.ToFloat(), gridColorEditB.text.ToFloat()); + if (edit.name == "GridColor.r") + edit.text = String(gridColor.r); + else if (edit.name == "GridColor.g") + edit.text = String(gridColor.g); + else if (edit.name == "GridColor.b") + edit.text = String(gridColor.b); + UpdateGrid(); +} + +void EditGridSubdivisionColor(StringHash eventType, VariantMap& eventData) +{ + LineEdit@ edit = eventData["Element"].GetPtr(); + gridSubdivisionColor = Color(gridSubdivisionColorEditR.text.ToFloat(), gridSubdivisionColorEditG.text.ToFloat(), gridSubdivisionColorEditB.text.ToFloat()); + if (edit.name == "GridSubdivisionColor.r") + edit.text = String(gridSubdivisionColor.r); + else if (edit.name == "GridSubdivisionColor.g") + edit.text = String(gridSubdivisionColor.g); + else if (edit.name == "GridSubdivisionColor.b") + edit.text = String(gridSubdivisionColor.b); + UpdateGrid(); +} diff --git a/bin/Data/Scripts/Editor/EditorResourceBrowser.as b/bin/EditorData/Editor/Scripts/EditorResourceBrowser.as similarity index 96% rename from bin/Data/Scripts/Editor/EditorResourceBrowser.as rename to bin/EditorData/Editor/Scripts/EditorResourceBrowser.as index eab9763535f..6887d50f663 100644 --- a/bin/Data/Scripts/Editor/EditorResourceBrowser.as +++ b/bin/EditorData/Editor/Scripts/EditorResourceBrowser.as @@ -1,1694 +1,1694 @@ -UIElement@ browserWindow; -Window@ browserFilterWindow; -ListView@ browserDirList; -ListView@ browserFileList; -LineEdit@ browserSearch; -BrowserFile@ browserDragFile; -Node@ browserDragNode; -Component@ browserDragComponent; -View3D@ resourceBrowserPreview; -Scene@ resourcePreviewScene; -Node@ resourcePreviewNode; -Node@ resourcePreviewCameraNode; -Node@ resourcePreviewLightNode; -Light@ resourcePreviewLight; -int browserSearchSortMode = 0; - -BrowserDir@ rootDir; -Array browserFiles; -Dictionary browserDirs; -Array activeResourceTypeFilters; -Array activeResourceDirFilters; - -Array browserFilesToScan; -const uint BROWSER_WORKER_ITEMS_PER_TICK = 10; -const uint BROWSER_SEARCH_LIMIT = 50; -const int BROWSER_SORT_MODE_ALPHA = 1; -const int BROWSER_SORT_MODE_SEARCH = 2; - -const int RESOURCE_TYPE_UNUSABLE = -2; -const int RESOURCE_TYPE_UNKNOWN = -1; -const int RESOURCE_TYPE_NOTSET = 0; -const int RESOURCE_TYPE_SCENE = 1; -const int RESOURCE_TYPE_SCRIPTFILE = 2; -const int RESOURCE_TYPE_MODEL = 3; -const int RESOURCE_TYPE_MATERIAL = 4; -const int RESOURCE_TYPE_ANIMATION = 5; -const int RESOURCE_TYPE_IMAGE = 6; -const int RESOURCE_TYPE_SOUND = 7; -const int RESOURCE_TYPE_TEXTURE = 8; -const int RESOURCE_TYPE_FONT = 9; -const int RESOURCE_TYPE_PREFAB = 10; -const int RESOURCE_TYPE_TECHNIQUE = 11; -const int RESOURCE_TYPE_PARTICLEEFFECT = 12; -const int RESOURCE_TYPE_UIELEMENT = 13; -const int RESOURCE_TYPE_UIELEMENTS = 14; -const int RESOURCE_TYPE_ANIMATION_SETTINGS = 15; -const int RESOURCE_TYPE_RENDERPATH = 16; -const int RESOURCE_TYPE_TEXTURE_ATLAS = 17; -const int RESOURCE_TYPE_2D_PARTICLE_EFFECT = 18; -const int RESOURCE_TYPE_TEXTURE_3D = 19; -const int RESOURCE_TYPE_CUBEMAP = 20; -const int RESOURCE_TYPE_PARTICLEEMITTER = 21; -const int RESOURCE_TYPE_2D_ANIMATION_SET = 22; -const int RESOURCE_TYPE_GENERIC_XML = 23; -const int RESOURCE_TYPE_GENERIC_JSON = 24; - -// any resource type > 0 is valid -const int NUMBER_OF_VALID_RESOURCE_TYPES = 24; - -const StringHash XML_TYPE_SCENE("scene"); -const StringHash XML_TYPE_NODE("node"); -const StringHash XML_TYPE_MATERIAL("material"); -const StringHash XML_TYPE_TECHNIQUE("technique"); -const StringHash XML_TYPE_PARTICLEEFFECT("particleeffect"); -const StringHash XML_TYPE_PARTICLEEMITTER("particleemitter"); -const StringHash XML_TYPE_TEXTURE("texture"); -const StringHash XML_TYPE_ELEMENT("element"); -const StringHash XML_TYPE_ELEMENTS("elements"); -const StringHash XML_TYPE_ANIMATION_SETTINGS("animation"); -const StringHash XML_TYPE_RENDERPATH("renderpath"); -const StringHash XML_TYPE_TEXTURE_ATLAS("TextureAtlas"); -const StringHash XML_TYPE_2D_PARTICLE_EFFECT("particleEmitterConfig"); -const StringHash XML_TYPE_TEXTURE_3D("texture3d"); -const StringHash XML_TYPE_CUBEMAP("cubemap"); -const StringHash XML_TYPE_SPRITER_DATA("spriter_data"); -const StringHash XML_TYPE_GENERIC("xml"); - -const StringHash JSON_TYPE_SCENE("scene"); -const StringHash JSON_TYPE_NODE("node"); -const StringHash JSON_TYPE_MATERIAL("material"); -const StringHash JSON_TYPE_TECHNIQUE("technique"); -const StringHash JSON_TYPE_PARTICLEEFFECT("particleeffect"); -const StringHash JSON_TYPE_PARTICLEEMITTER("particleemitter"); -const StringHash JSON_TYPE_TEXTURE("texture"); -const StringHash JSON_TYPE_ELEMENT("element"); -const StringHash JSON_TYPE_ELEMENTS("elements"); -const StringHash JSON_TYPE_ANIMATION_SETTINGS("animation"); -const StringHash JSON_TYPE_RENDERPATH("renderpath"); -const StringHash JSON_TYPE_TEXTURE_ATLAS("TextureAtlas"); -const StringHash JSON_TYPE_2D_PARTICLE_EFFECT("particleEmitterConfig"); -const StringHash JSON_TYPE_TEXTURE_3D("texture3d"); -const StringHash JSON_TYPE_CUBEMAP("cubemap"); -const StringHash JSON_TYPE_SPRITER_DATA("spriter_data"); -const StringHash JSON_TYPE_GENERIC("json"); - -const StringHash BINARY_TYPE_SCENE("USCN"); -const StringHash BINARY_TYPE_PACKAGE("UPAK"); -const StringHash BINARY_TYPE_COMPRESSED_PACKAGE("ULZ4"); -const StringHash BINARY_TYPE_ANGELSCRIPT("ASBC"); -const StringHash BINARY_TYPE_MODEL("UMDL"); -const StringHash BINARY_TYPE_MODEL2("UMD2"); -const StringHash BINARY_TYPE_SHADER("USHD"); -const StringHash BINARY_TYPE_ANIMATION("UANI"); - -const StringHash EXTENSION_TYPE_TTF(".ttf"); -const StringHash EXTENSION_TYPE_OTF(".otf"); -const StringHash EXTENSION_TYPE_OGG(".ogg"); -const StringHash EXTENSION_TYPE_WAV(".wav"); -const StringHash EXTENSION_TYPE_DDS(".dds"); -const StringHash EXTENSION_TYPE_PNG(".png"); -const StringHash EXTENSION_TYPE_JPG(".jpg"); -const StringHash EXTENSION_TYPE_JPEG(".jpeg"); -const StringHash EXTENSION_TYPE_HDR(".hdr"); -const StringHash EXTENSION_TYPE_BMP(".bmp"); -const StringHash EXTENSION_TYPE_TGA(".tga"); -const StringHash EXTENSION_TYPE_KTX(".ktx"); -const StringHash EXTENSION_TYPE_PVR(".pvr"); -const StringHash EXTENSION_TYPE_OBJ(".obj"); -const StringHash EXTENSION_TYPE_FBX(".fbx"); -const StringHash EXTENSION_TYPE_COLLADA(".dae"); -const StringHash EXTENSION_TYPE_BLEND(".blend"); -const StringHash EXTENSION_TYPE_ANGELSCRIPT(".as"); -const StringHash EXTENSION_TYPE_LUASCRIPT(".lua"); -const StringHash EXTENSION_TYPE_HLSL(".hlsl"); -const StringHash EXTENSION_TYPE_GLSL(".glsl"); -const StringHash EXTENSION_TYPE_FRAGMENTSHADER(".frag"); -const StringHash EXTENSION_TYPE_VERTEXSHADER(".vert"); -const StringHash EXTENSION_TYPE_HTML(".html"); - -const StringHash TEXT_VAR_FILE_ID("browser_file_id"); -const StringHash TEXT_VAR_DIR_ID("browser_dir_id"); -const StringHash TEXT_VAR_RESOURCE_TYPE("resource_type"); -const StringHash TEXT_VAR_RESOURCE_DIR_ID("resource_dir_id"); - -const int BROWSER_FILE_SOURCE_RESOURCE_DIR = 1; - -uint browserDirIndex = 1; -uint browserFileIndex = 1; -BrowserDir@ selectedBrowserDirectory; -BrowserFile@ selectedBrowserFile; -Text@ browserStatusMessage; -Text@ browserResultsMessage; -bool ignoreRefreshBrowserResults = false; -String resourceDirsCache; - -void CreateResourceBrowser() -{ - if (browserWindow !is null) return; - - CreateResourceBrowserUI(); - InitResourceBrowserPreview(); - RebuildResourceDatabase(); -} - -void RebuildResourceDatabase() -{ - if (browserWindow is null) - return; - - String newResourceDirsCache = Join(cache.resourceDirs, ';'); - ScanResourceDirectories(); - if (newResourceDirsCache != resourceDirsCache) - { - resourceDirsCache = newResourceDirsCache; - PopulateResourceDirFilters(); - } - PopulateBrowserDirectories(); - PopulateResourceBrowserFilesByDirectory(rootDir); -} - -void ScanResourceDirectories() -{ - browserDirs.Clear(); - browserFiles.Clear(); - browserFilesToScan.Clear(); - - rootDir = BrowserDir(""); - browserDirs.Set("", @rootDir); - - // collect all of the items and sort them afterwards - for(uint i=0; i < cache.resourceDirs.length; ++i) - { - if (activeResourceDirFilters.Find(i) > -1) - continue; - - ScanResourceDir(i); - } -} - -// used to stop ui from blocking while determining file types -void DoResourceBrowserWork() -{ - if (browserFilesToScan.length == 0) - return; - - int counter = 0; - bool updateBrowserUI = false; - BrowserFile@ scanItem = browserFilesToScan[0]; - while(counter < BROWSER_WORKER_ITEMS_PER_TICK) - { - scanItem.DetermainResourceType(); - - // next - browserFilesToScan.Erase(0); - if (browserFilesToScan.length > 0) - @scanItem = browserFilesToScan[0]; - else - break; - counter++; - } - - if (browserFilesToScan.length > 0) - browserStatusMessage.text = localization.Get("Files left to scan: " )+ browserFilesToScan.length; - else - browserStatusMessage.text = localization.Get("Scan complete"); - -} - -void CreateResourceBrowserUI() -{ - browserWindow = LoadEditorUI("UI/EditorResourceBrowser.xml"); - browserDirList = browserWindow.GetChild("DirectoryList", true); - browserFileList = browserWindow.GetChild("FileList", true); - browserSearch = browserWindow.GetChild("Search", true); - browserStatusMessage = browserWindow.GetChild("StatusMessage", true); - browserResultsMessage = browserWindow.GetChild("ResultsMessage", true); - // browserWindow.visible = false; - browserWindow.opacity = uiMaxOpacity; - - browserFilterWindow = LoadEditorUI("UI/EditorResourceFilterWindow.xml"); - CreateResourceFilterUI(); - HideResourceFilterWindow(); - - int height = Min(ui.root.height / 4, 300); - browserWindow.SetSize(900, height); - browserWindow.SetPosition(35, ui.root.height - height - 25); - - CloseContextMenu(); - ui.root.AddChild(browserWindow); - ui.root.AddChild(browserFilterWindow); - - SubscribeToEvent(browserWindow.GetChild("CloseButton", true), "Released", "HideResourceBrowserWindow"); - SubscribeToEvent(browserWindow.GetChild("RescanButton", true), "Released", "HandleRescanResourceBrowserClick"); - SubscribeToEvent(browserWindow.GetChild("FilterButton", true), "Released", "ToggleResourceFilterWindow"); - SubscribeToEvent(browserDirList, "SelectionChanged", "HandleResourceBrowserDirListSelectionChange"); - SubscribeToEvent(browserSearch, "TextChanged", "HandleResourceBrowserSearchTextChange"); - SubscribeToEvent(browserFileList, "ItemClicked", "HandleBrowserFileClick"); - SubscribeToEvent(browserFileList, "SelectionChanged", "HandleResourceBrowserFileListSelectionChange"); - SubscribeToEvent(cache, "FileChanged", "HandleFileChanged"); -} - -void CreateResourceFilterUI() -{ - UIElement@ options = browserFilterWindow.GetChild("TypeOptions", true); - CheckBox@ toggleAllTypes = browserFilterWindow.GetChild("ToggleAllTypes", true); - CheckBox@ toggleAllResourceDirs = browserFilterWindow.GetChild("ToggleAllResourceDirs", true); - SubscribeToEvent(toggleAllTypes, "Toggled", "HandleResourceTypeFilterToggleAllTypesToggled"); - SubscribeToEvent(toggleAllResourceDirs, "Toggled", "HandleResourceDirFilterToggleAllTypesToggled"); - SubscribeToEvent(browserFilterWindow.GetChild("CloseButton", true), "Released", "HideResourceFilterWindow"); - - int columns = 2; - UIElement@ col1 = browserFilterWindow.GetChild("TypeFilterColumn1", true); - UIElement@ col2 = browserFilterWindow.GetChild("TypeFilterColumn2", true); - - // use array to get sort of items - Array sorted; - for (int i=1; i <= NUMBER_OF_VALID_RESOURCE_TYPES; ++i) - sorted.Push(ResourceType(i, ResourceTypeName(i))); - - // 2 unknown types are reserved for the top, the rest are alphabetized - sorted.Sort(); - sorted.Insert(0, ResourceType(RESOURCE_TYPE_UNKNOWN, ResourceTypeName(RESOURCE_TYPE_UNKNOWN)) ); - sorted.Insert(0, ResourceType(RESOURCE_TYPE_UNUSABLE, ResourceTypeName(RESOURCE_TYPE_UNUSABLE)) ); - uint halfColumns = uint( Ceil( float(sorted.length) / float(columns) ) ); - - for (uint i = 0; i < sorted.length; ++i) - { - ResourceType@ type = sorted[i]; - UIElement@ resourceTypeHolder = UIElement(); - if (i < halfColumns) - col1.AddChild(resourceTypeHolder); - else - col2.AddChild(resourceTypeHolder); - - resourceTypeHolder.layoutMode = LM_HORIZONTAL; - resourceTypeHolder.layoutSpacing = 4; - - Text@ label = Text(); - label.style = "EditorAttributeText"; - label.text = type.name; - CheckBox@ checkbox = CheckBox(); - checkbox.name = type.id; - checkbox.SetStyleAuto(); - checkbox.vars[TEXT_VAR_RESOURCE_TYPE] = i; - checkbox.checked = true; - SubscribeToEvent(checkbox, "Toggled", "HandleResourceTypeFilterToggled"); - - resourceTypeHolder.AddChild(checkbox); - resourceTypeHolder.AddChild(label); - } -} - -void CreateDirList(BrowserDir@ dir, UIElement@ parentUI = null) -{ - Text@ dirText = Text(); - browserDirList.InsertItem(browserDirList.numItems, dirText, parentUI); - dirText.style = "FileSelectorListText"; - dirText.text = dir.resourceKey.empty ? localization.Get("Root") : dir.name; - dirText.name = dir.resourceKey; - dirText.vars[TEXT_VAR_DIR_ID] = dir.resourceKey; - - // Sort directories alphetically - browserSearchSortMode = BROWSER_SORT_MODE_ALPHA; - dir.children.Sort(); - - for(uint i=0; i 0) - fileText.dragDropMode = DD_SOURCE; - - { - Text@ text = Text(); - fileText.AddChild(text); - text.style = "FileSelectorListText"; - text.text = file.fullname; - text.name = file.resourceKey; - } - - { - Text@ text = Text(); - fileText.AddChild(text); - text.style = "FileSelectorListText"; - text.text = file.ResourceTypeName(); - } - - if (file.resourceType == RESOURCE_TYPE_MATERIAL || - file.resourceType == RESOURCE_TYPE_MODEL || - file.resourceType == RESOURCE_TYPE_PARTICLEEFFECT || - file.resourceType == RESOURCE_TYPE_PREFAB - ) - { - SubscribeToEvent(fileText, "DragBegin", "HandleBrowserFileDragBegin"); - SubscribeToEvent(fileText, "DragEnd", "HandleBrowserFileDragEnd"); - } - -} - -void InitResourceBrowserPreview() -{ - resourcePreviewScene = Scene("PreviewScene"); - resourcePreviewScene.CreateComponent("Octree"); - PhysicsWorld@ physicsWorld = resourcePreviewScene.CreateComponent("PhysicsWorld"); - physicsWorld.enabled = false; - physicsWorld.gravity = Vector3(0.0, 0.0, 0.0); - - Node@ zoneNode = resourcePreviewScene.CreateChild("Zone"); - Zone@ zone = zoneNode.CreateComponent("Zone"); - zone.boundingBox = BoundingBox(-1000, 1000); - zone.ambientColor = Color(0.15, 0.15, 0.15); - zone.fogColor = Color(0, 0, 0); - zone.fogStart = 10.0; - zone.fogEnd = 100.0; - - resourcePreviewCameraNode = resourcePreviewScene.CreateChild("PreviewCamera"); - resourcePreviewCameraNode.position = Vector3(0, 0, -1.5); - Camera@ camera = resourcePreviewCameraNode.CreateComponent("Camera"); - camera.nearClip = 0.1f; - camera.farClip = 100.0f; - - resourcePreviewLightNode = resourcePreviewScene.CreateChild("PreviewLight"); - resourcePreviewLightNode.direction = Vector3(0.5, -0.5, 0.5); - resourcePreviewLight = resourcePreviewLightNode.CreateComponent("Light"); - resourcePreviewLight.lightType = LIGHT_DIRECTIONAL; - resourcePreviewLight.specularIntensity = 0.5; - - resourceBrowserPreview = browserWindow.GetChild("ResourceBrowserPreview", true); - resourceBrowserPreview.SetFixedHeight(200); - resourceBrowserPreview.SetFixedWidth(266); - resourceBrowserPreview.SetView(resourcePreviewScene, camera); - resourceBrowserPreview.autoUpdate = false; - - resourcePreviewNode = resourcePreviewScene.CreateChild("PreviewNodeContainer"); - - SubscribeToEvent(resourceBrowserPreview, "DragMove", "RotateResourceBrowserPreview"); - - RefreshBrowserPreview(); -} - -// Opens a contextual menu based on what resource item was actioned -void HandleBrowserFileClick(StringHash eventType, VariantMap& eventData) -{ - if (eventData["Button"].GetI32() != MOUSEB_RIGHT) - return; - - UIElement@ uiElement = eventData["Item"].GetPtr(); - BrowserFile@ file = GetBrowserFileFromUIElement(uiElement); - - if (file is null) - return; - - Array actions; - if (file.resourceType == RESOURCE_TYPE_MATERIAL) - { - actions.Push(CreateBrowserFileActionMenu("Edit", "HandleBrowserEditResource", file)); - } - else if (file.resourceType == RESOURCE_TYPE_MODEL) - { - actions.Push(CreateBrowserFileActionMenu("Instance Animated Model", "HandleBrowserInstantiateAnimatedModel", file)); - actions.Push(CreateBrowserFileActionMenu("Instance Static Model", "HandleBrowserInstantiateStaticModel", file)); - } - else if (file.resourceType == RESOURCE_TYPE_PREFAB) - { - actions.Push(CreateBrowserFileActionMenu("Instance Prefab", "HandleBrowserInstantiatePrefab", file)); - actions.Push(CreateBrowserFileActionMenu("Instance in Spawner", "HandleBrowserInstantiateInSpawnEditor", file)); - } - else if (file.fileType == EXTENSION_TYPE_OBJ || - file.fileType == EXTENSION_TYPE_COLLADA || - file.fileType == EXTENSION_TYPE_FBX || - file.fileType == EXTENSION_TYPE_BLEND) - { - actions.Push(CreateBrowserFileActionMenu("Import Model", "HandleBrowserImportModel", file)); - actions.Push(CreateBrowserFileActionMenu("Import Scene", "HandleBrowserImportScene", file)); - } - else if (file.resourceType == RESOURCE_TYPE_UIELEMENT) - { - actions.Push(CreateBrowserFileActionMenu("Open UI Layout", "HandleBrowserOpenUILayout", file)); - } - else if (file.resourceType == RESOURCE_TYPE_SCENE) - { - actions.Push(CreateBrowserFileActionMenu("Load Scene", "HandleBrowserLoadScene", file)); - } - else if (file.resourceType == RESOURCE_TYPE_SCRIPTFILE) - { - actions.Push(CreateBrowserFileActionMenu("Execute Script", "HandleBrowserRunScript", file)); - } - else if (file.resourceType == RESOURCE_TYPE_PARTICLEEFFECT) - { - actions.Push(CreateBrowserFileActionMenu("Edit", "HandleBrowserEditResource", file)); - } - - actions.Push(CreateBrowserFileActionMenu("Open", "HandleBrowserOpenResource", file)); - - ActivateContextMenu(actions); -} - -BrowserDir@ GetBrowserDir(String path) -{ - BrowserDir@ browserDir; - browserDirs.Get(path, @browserDir); - return browserDir; -} - -// Makes sure the entire directory tree exists and new dir is linked to parent -BrowserDir@ InitBrowserDir(String path) -{ - BrowserDir@ browserDir; - if (browserDirs.Get(path, @browserDir)) - return browserDir; - - Array parts = path.Split('/'); - Array finishedParts; - if (parts.length > 0) - { - BrowserDir@ parent = rootDir; - for( uint i = 0; i < parts.length; ++i ) - { - finishedParts.Push(parts[i]); - String currentPath = Join(finishedParts, "/"); - if (!browserDirs.Get(currentPath, @browserDir)) - { - browserDir = BrowserDir(currentPath); - browserDirs.Set(currentPath, @browserDir); - parent.children.Push(browserDir); - } - @parent = browserDir; - } - return browserDir; - } - return null; -} - -void ScanResourceDir(uint resourceDirIndex) -{ - String resourceDir = cache.resourceDirs[resourceDirIndex]; - ScanResourceDirFiles("", resourceDirIndex); - Array dirs = fileSystem.ScanDir(resourceDir, "*", SCAN_DIRS, true); - for (uint i=0; i < dirs.length; ++i) - { - String path = dirs[i]; - if (path.EndsWith(".")) - continue; - - InitBrowserDir(path); - ScanResourceDirFiles(path, resourceDirIndex); - } -} - -void ScanResourceDirFiles(String path, uint resourceDirIndex) -{ - String fullPath = cache.resourceDirs[resourceDirIndex] + path; - if (!fileSystem.DirExists(fullPath)) - return; - - BrowserDir@ dir = GetBrowserDir(path); - - if (dir is null) - return; - - // get files in directory - Array dirFiles = fileSystem.ScanDir(fullPath, "*.*", SCAN_FILES, false); - - // add new files - for (uint x=0; x < dirFiles.length; x++) - { - String filename = dirFiles[x]; - BrowserFile@ browserFile = dir.AddFile(filename, resourceDirIndex, BROWSER_FILE_SOURCE_RESOURCE_DIR); - browserFiles.Push(browserFile); - browserFilesToScan.Push(browserFile); - } -} - -bool ToggleResourceBrowserWindow() -{ - if (browserWindow.visible == false) - ShowResourceBrowserWindow(); - else - HideResourceBrowserWindow(); - return true; -} - -void ShowResourceBrowserWindow() -{ - browserWindow.visible = true; - browserWindow.BringToFront(); - ui.focusElement = browserSearch; -} - -void HideResourceBrowserWindow() -{ - browserWindow.visible = false; -} - -void ToggleResourceFilterWindow() -{ - if (browserFilterWindow.visible) - HideResourceFilterWindow(); - else - ShowResourceFilterWindow(); -} -void HideResourceFilterWindow() -{ - browserFilterWindow.visible = false; -} - -void ShowResourceFilterWindow() -{ - int x = browserWindow.position.x + browserWindow.width - browserFilterWindow.width; - int y = browserWindow.position.y - browserFilterWindow.height - 1; - browserFilterWindow.position = IntVector2(x,y); - browserFilterWindow.visible = true; - browserFilterWindow.BringToFront(); -} - -void PopulateResourceDirFilters() -{ - UIElement@ resourceDirs = browserFilterWindow.GetChild("DirFilters", true); - resourceDirs.RemoveAllChildren(); - activeResourceDirFilters.Clear(); - for (uint i=0; i < cache.resourceDirs.length; ++i) - { - UIElement@ resourceDirHolder = UIElement(); - resourceDirs.AddChild(resourceDirHolder); - resourceDirHolder.layoutMode = LM_HORIZONTAL; - resourceDirHolder.layoutSpacing = 4; - resourceDirHolder.SetFixedHeight(16); - - Text@ label = Text(); - label.style = "EditorAttributeText"; - label.text = cache.resourceDirs[i].Replaced(fileSystem.programDir, ""); - CheckBox@ checkbox = CheckBox(); - checkbox.name = i; - checkbox.SetStyleAuto(); - checkbox.vars[TEXT_VAR_RESOURCE_DIR_ID] = i; - checkbox.checked = true; - SubscribeToEvent(checkbox, "Toggled", "HandleResourceDirFilterToggled"); - - - resourceDirHolder.AddChild(checkbox); - resourceDirHolder.AddChild(label); - } - -} - -void PopulateBrowserDirectories() -{ - browserDirList.RemoveAllItems(); - CreateDirList(rootDir); - browserDirList.selection = 0; -} - -void PopulateResourceBrowserFilesByDirectory(BrowserDir@ dir) -{ - @selectedBrowserDirectory = dir; - browserFileList.RemoveAllItems(); - if (dir is null) return; - - Array files; - for(uint x=0; x < dir.files.length; x++) - { - BrowserFile@ file = dir.files[x]; - - if (activeResourceTypeFilters.Find(file.resourceType) == -1) - files.Push(file); - } - - // Sort alphetically - browserSearchSortMode = BROWSER_SORT_MODE_ALPHA; - files.Sort(); - PopulateResourceBrowserResults(files); - browserResultsMessage.text = localization.Get("Showing files: ") + files.length; -} - - -void PopulateResourceBrowserBySearch() -{ - String query = browserSearch.text; - - Array scores; - Array scored; - Array filtered; - { - BrowserFile@ file; - for(uint x=0; x < browserFiles.length; x++) - { - @file = browserFiles[x]; - file.sortScore = -1; - if (activeResourceTypeFilters.Find(file.resourceType) > -1) - continue; - - if (activeResourceDirFilters.Find(file.resourceSourceIndex) > -1) - continue; - - int find = file.fullname.Find(query, 0, false); - if (find > -1) - { - int fudge = query.length - file.fullname.length; - int score = find * int(Abs(fudge*2)) + int(Abs(fudge)); - file.sortScore = score; - scored.Push(file); - scores.Push(score); - } - } - } - - // cut this down for a faster sort - if (scored.length > BROWSER_SEARCH_LIMIT) - { - scores.Sort(); - int scoreThreshold = scores[BROWSER_SEARCH_LIMIT]; - BrowserFile@ file; - for(uint x=0;x@ files) -{ - browserFileList.RemoveAllItems(); - for(uint i=0; i < files.length; ++i) - CreateFileList(files[i]); -} - -void RefreshBrowserResults() -{ - if (browserSearch.text.empty) - { - browserDirList.visible = true; - PopulateResourceBrowserFilesByDirectory(selectedBrowserDirectory); - } - else - { - browserDirList.visible = false; - PopulateResourceBrowserBySearch(); - } -} - -void HandleResourceTypeFilterToggleAllTypesToggled(StringHash eventType, VariantMap& eventData) -{ - CheckBox@ checkbox = eventData["Element"].GetPtr(); - UIElement@ filterHolder = browserFilterWindow.GetChild("TypeFilters", true); - Array children = filterHolder.GetChildren(true); - - ignoreRefreshBrowserResults = true; - for(uint i=0; i < children.length; ++i) - { - CheckBox@ filter = children[i]; - if (filter !is null) - filter.checked = checkbox.checked; - } - ignoreRefreshBrowserResults = false; - RefreshBrowserResults(); -} - -void HandleResourceTypeFilterToggled(StringHash eventType, VariantMap& eventData) -{ - CheckBox@ checkbox = eventData["Element"].GetPtr(); - if (!checkbox.vars.Contains(TEXT_VAR_RESOURCE_TYPE)) - return; - - int resourceType = checkbox.GetVar(TEXT_VAR_RESOURCE_TYPE).GetI32(); - int find = activeResourceTypeFilters.Find(resourceType); - - if (checkbox.checked && find != -1) - activeResourceTypeFilters.Erase(find); - else if (!checkbox.checked && find == -1) - activeResourceTypeFilters.Push(resourceType); - - if (ignoreRefreshBrowserResults == false) - RefreshBrowserResults(); -} - -void HandleResourceDirFilterToggleAllTypesToggled(StringHash eventType, VariantMap& eventData) -{ - CheckBox@ checkbox = eventData["Element"].GetPtr(); - UIElement@ filterHolder = browserFilterWindow.GetChild("DirFilters", true); - Array children = filterHolder.GetChildren(true); - - ignoreRefreshBrowserResults = true; - for(uint i=0; i < children.length; ++i) - { - CheckBox@ filter = children[i]; - if (filter !is null) - filter.checked = checkbox.checked; - } - ignoreRefreshBrowserResults = false; - RebuildResourceDatabase(); -} - -void HandleResourceDirFilterToggled(StringHash eventType, VariantMap& eventData) -{ - CheckBox@ checkbox = eventData["Element"].GetPtr(); - if (!checkbox.vars.Contains(TEXT_VAR_RESOURCE_DIR_ID)) - return; - - int resourceDir = checkbox.GetVar(TEXT_VAR_RESOURCE_DIR_ID).GetI32(); - int find = activeResourceDirFilters.Find(resourceDir); - - if (checkbox.checked && find != -1) - activeResourceDirFilters.Erase(find); - else if (!checkbox.checked && find == -1) - activeResourceDirFilters.Push(resourceDir); - - if (ignoreRefreshBrowserResults == false) - RebuildResourceDatabase(); -} - -void HandleRescanResourceBrowserClick(StringHash eventType, VariantMap& eventData) -{ - RebuildResourceDatabase(); -} - -void HandleResourceBrowserDirListSelectionChange(StringHash eventType, VariantMap& eventData) -{ - if (browserDirList.selection == M_MAX_UNSIGNED) - return; - - UIElement@ uiElement = browserDirList.GetItems()[browserDirList.selection]; - BrowserDir@ dir = GetBrowserDir(uiElement.vars[TEXT_VAR_DIR_ID].GetString()); - if (dir is null) - return; - - PopulateResourceBrowserFilesByDirectory(dir); -} - -void HandleResourceBrowserFileListSelectionChange(StringHash eventType, VariantMap& eventData) -{ - if (browserFileList.selection == M_MAX_UNSIGNED) - return; - - UIElement@ uiElement = browserFileList.GetItems()[browserFileList.selection]; - BrowserFile@ file = GetBrowserFileFromUIElement(uiElement); - if (file is null) - return; - - if (resourcePreviewNode !is null) - resourcePreviewNode.Remove(); - - resourcePreviewNode = resourcePreviewScene.CreateChild("PreviewNodeContainer"); - CreateResourcePreview(file.GetFullPath(), resourcePreviewNode); - - if (resourcePreviewNode !is null) - { - Array boxes; - Array staticModels = resourcePreviewNode.GetComponents("StaticModel", true); - Array animatedModels = resourcePreviewNode.GetComponents("AnimatedModel", true); - - for (uint i = 0; i < staticModels.length; ++i) - boxes.Push(cast(staticModels[i]).worldBoundingBox); - - for (uint i = 0; i < animatedModels.length; ++i) - boxes.Push(cast(animatedModels[i]).worldBoundingBox); - - if (boxes.length > 0) - { - Vector3 camPosition = Vector3(0.0, 0.0, -1.2); - BoundingBox biggestBox = boxes[0]; - for (uint i = 1; i < boxes.length; ++i) - { - if (boxes[i].size.length > biggestBox.size.length) - biggestBox = boxes[i]; - } - resourcePreviewCameraNode.position = biggestBox.center + camPosition * biggestBox.size.length; - } - - resourcePreviewScene.AddChild(resourcePreviewNode); - RefreshBrowserPreview(); - } -} - -void HandleResourceBrowserSearchTextChange(StringHash eventType, VariantMap& eventData) -{ - RefreshBrowserResults(); -} - -BrowserFile@ GetBrowserFileFromId(uint id) -{ - if (id == 0) - return null; - - BrowserFile@ file; - for(uint i=0; i(GetDrawableAtMousePostion()); - if (model !is null) - { - AssignMaterial(model, browserDragFile.resourceKey); - } - } - else if (browserDragFile.resourceType == RESOURCE_TYPE_PREFAB) - { - LoadNode(browserDragFile.GetFullPath(), null, true); - } - else if (browserDragFile.resourceType == RESOURCE_TYPE_MODEL) - { - Node@ createdNode = CreateNode(REPLICATED, true); - Model@ model = cache.GetResource("Model", browserDragFile.resourceKey); - if (model.skeleton.numBones > 0) - { - AnimatedModel@ am = createdNode.CreateComponent("AnimatedModel"); - am.model = model; - } - else - { - StaticModel@ sm = createdNode.CreateComponent("StaticModel"); - sm.model = model; - } - - AdjustNodePositionByAABB(createdNode); - } - - browserDragFile = null; - browserDragComponent = null; - browserDragNode = null; -} - -void HandleFileChanged(StringHash eventType, VariantMap& eventData) -{ - String filename = eventData["FileName"].GetString(); - BrowserFile@ file = GetBrowserFileFromPath(filename); - - if (file is null) - { - // TODO: new file logic when watchers are supported - return; - } - else - { - file.FileChanged(); - } -} - -Menu@ CreateBrowserFileActionMenu(String text, String handler, BrowserFile@ browserFile = null) -{ - Menu@ menu = CreateContextMenuItem(text, handler); - if (browserFile !is null) - menu.vars[TEXT_VAR_FILE_ID] = browserFile.id; - - return menu; -} - -int GetResourceType(String path) -{ - StringHash fileType; - return GetResourceType(path, fileType); -} - -int GetResourceType(String path, StringHash &out fileType, bool useCache = false) -{ - if (GetExtensionType(path, fileType) || GetBinaryType(path, fileType, useCache) || GetXmlType(path, fileType, useCache)) - return GetResourceType(fileType); - - return RESOURCE_TYPE_UNKNOWN; -} - -int GetResourceType(StringHash fileType) -{ - // Binary filetypes - if (fileType == BINARY_TYPE_SCENE) - return RESOURCE_TYPE_SCENE; - else if (fileType == BINARY_TYPE_PACKAGE) - return RESOURCE_TYPE_UNUSABLE; - else if (fileType == BINARY_TYPE_COMPRESSED_PACKAGE) - return RESOURCE_TYPE_UNUSABLE; - else if (fileType == BINARY_TYPE_ANGELSCRIPT) - return RESOURCE_TYPE_SCRIPTFILE; - else if (fileType == BINARY_TYPE_MODEL || fileType == BINARY_TYPE_MODEL2) - return RESOURCE_TYPE_MODEL; - else if (fileType == BINARY_TYPE_SHADER) - return RESOURCE_TYPE_UNUSABLE; - else if (fileType == BINARY_TYPE_ANIMATION) - return RESOURCE_TYPE_ANIMATION; - - // XML filetypes - else if (fileType == XML_TYPE_SCENE) - return RESOURCE_TYPE_SCENE; - else if (fileType == XML_TYPE_NODE) - return RESOURCE_TYPE_PREFAB; - else if(fileType == XML_TYPE_MATERIAL) - return RESOURCE_TYPE_MATERIAL; - else if(fileType == XML_TYPE_TECHNIQUE) - return RESOURCE_TYPE_TECHNIQUE; - else if(fileType == XML_TYPE_PARTICLEEFFECT) - return RESOURCE_TYPE_PARTICLEEFFECT; - else if(fileType == XML_TYPE_PARTICLEEMITTER) - return RESOURCE_TYPE_PARTICLEEMITTER; - else if(fileType == XML_TYPE_TEXTURE) - return RESOURCE_TYPE_TEXTURE; - else if(fileType == XML_TYPE_ELEMENT) - return RESOURCE_TYPE_UIELEMENT; - else if(fileType == XML_TYPE_ELEMENTS) - return RESOURCE_TYPE_UIELEMENTS; - else if (fileType == XML_TYPE_ANIMATION_SETTINGS) - return RESOURCE_TYPE_ANIMATION_SETTINGS; - else if (fileType == XML_TYPE_RENDERPATH) - return RESOURCE_TYPE_RENDERPATH; - else if (fileType == XML_TYPE_TEXTURE_ATLAS) - return RESOURCE_TYPE_TEXTURE_ATLAS; - else if (fileType == XML_TYPE_2D_PARTICLE_EFFECT) - return RESOURCE_TYPE_2D_PARTICLE_EFFECT; - else if (fileType == XML_TYPE_TEXTURE_3D) - return RESOURCE_TYPE_TEXTURE_3D; - else if (fileType == XML_TYPE_CUBEMAP) - return RESOURCE_TYPE_CUBEMAP; - else if (fileType == XML_TYPE_SPRITER_DATA) - return RESOURCE_TYPE_2D_ANIMATION_SET; - else if (fileType == XML_TYPE_GENERIC) - return RESOURCE_TYPE_GENERIC_XML; - - // JSON filetypes - else if (fileType == JSON_TYPE_SCENE) - return RESOURCE_TYPE_SCENE; - else if (fileType == JSON_TYPE_NODE) - return RESOURCE_TYPE_PREFAB; - else if(fileType == JSON_TYPE_MATERIAL) - return RESOURCE_TYPE_MATERIAL; - else if(fileType == JSON_TYPE_TECHNIQUE) - return RESOURCE_TYPE_TECHNIQUE; - else if(fileType == JSON_TYPE_PARTICLEEFFECT) - return RESOURCE_TYPE_PARTICLEEFFECT; - else if(fileType == JSON_TYPE_PARTICLEEMITTER) - return RESOURCE_TYPE_PARTICLEEMITTER; - else if(fileType == JSON_TYPE_TEXTURE) - return RESOURCE_TYPE_TEXTURE; - else if(fileType == JSON_TYPE_ELEMENT) - return RESOURCE_TYPE_UIELEMENT; - else if(fileType == JSON_TYPE_ELEMENTS) - return RESOURCE_TYPE_UIELEMENTS; - else if (fileType == JSON_TYPE_ANIMATION_SETTINGS) - return RESOURCE_TYPE_ANIMATION_SETTINGS; - else if (fileType == JSON_TYPE_RENDERPATH) - return RESOURCE_TYPE_RENDERPATH; - else if (fileType == JSON_TYPE_TEXTURE_ATLAS) - return RESOURCE_TYPE_TEXTURE_ATLAS; - else if (fileType == JSON_TYPE_2D_PARTICLE_EFFECT) - return RESOURCE_TYPE_2D_PARTICLE_EFFECT; - else if (fileType == JSON_TYPE_TEXTURE_3D) - return RESOURCE_TYPE_TEXTURE_3D; - else if (fileType == JSON_TYPE_CUBEMAP) - return RESOURCE_TYPE_CUBEMAP; - else if (fileType == JSON_TYPE_SPRITER_DATA) - return RESOURCE_TYPE_2D_ANIMATION_SET; - else if (fileType == JSON_TYPE_GENERIC) - return RESOURCE_TYPE_GENERIC_JSON; - - // Extension filetypes - else if (fileType == EXTENSION_TYPE_TTF) - return RESOURCE_TYPE_FONT; - else if (fileType == EXTENSION_TYPE_OTF) - return RESOURCE_TYPE_FONT; - else if (fileType == EXTENSION_TYPE_OGG) - return RESOURCE_TYPE_SOUND; - else if(fileType == EXTENSION_TYPE_WAV) - return RESOURCE_TYPE_SOUND; - else if(fileType == EXTENSION_TYPE_DDS) - return RESOURCE_TYPE_IMAGE; - else if(fileType == EXTENSION_TYPE_PNG) - return RESOURCE_TYPE_IMAGE; - else if(fileType == EXTENSION_TYPE_JPG) - return RESOURCE_TYPE_IMAGE; - else if(fileType == EXTENSION_TYPE_JPEG) - return RESOURCE_TYPE_IMAGE; - else if(fileType == EXTENSION_TYPE_HDR) - return RESOURCE_TYPE_IMAGE; - else if(fileType == EXTENSION_TYPE_BMP) - return RESOURCE_TYPE_IMAGE; - else if(fileType == EXTENSION_TYPE_TGA) - return RESOURCE_TYPE_IMAGE; - else if(fileType == EXTENSION_TYPE_KTX) - return RESOURCE_TYPE_IMAGE; - else if(fileType == EXTENSION_TYPE_PVR) - return RESOURCE_TYPE_IMAGE; - else if(fileType == EXTENSION_TYPE_OBJ) - return RESOURCE_TYPE_UNUSABLE; - else if(fileType == EXTENSION_TYPE_FBX) - return RESOURCE_TYPE_UNUSABLE; - else if(fileType == EXTENSION_TYPE_COLLADA) - return RESOURCE_TYPE_UNUSABLE; - else if(fileType == EXTENSION_TYPE_BLEND) - return RESOURCE_TYPE_UNUSABLE; - else if(fileType == EXTENSION_TYPE_ANGELSCRIPT) - return RESOURCE_TYPE_SCRIPTFILE; - else if(fileType == EXTENSION_TYPE_LUASCRIPT) - return RESOURCE_TYPE_SCRIPTFILE; - else if(fileType == EXTENSION_TYPE_HLSL) - return RESOURCE_TYPE_UNUSABLE; - else if(fileType == EXTENSION_TYPE_GLSL) - return RESOURCE_TYPE_UNUSABLE; - else if(fileType == EXTENSION_TYPE_FRAGMENTSHADER) - return RESOURCE_TYPE_UNUSABLE; - else if(fileType == EXTENSION_TYPE_VERTEXSHADER) - return RESOURCE_TYPE_UNUSABLE; - else if(fileType == EXTENSION_TYPE_HTML) - return RESOURCE_TYPE_UNUSABLE; - - return RESOURCE_TYPE_UNKNOWN; -} - -// TODO: Должно быть bool GetExtensionType(String path, StringHash &out fileType). -// Временный workaround для https://github.com/urho3d/urho3d/issues/3148 -bool GetExtensionType(String path, StringHash& fileType) -{ - StringHash type = StringHash(GetExtension(path)); - if (type == EXTENSION_TYPE_TTF) - fileType = EXTENSION_TYPE_TTF; - else if (type == EXTENSION_TYPE_OTF) - fileType = EXTENSION_TYPE_OTF; - else if (type == EXTENSION_TYPE_OGG) - fileType = EXTENSION_TYPE_OGG; - else if(type == EXTENSION_TYPE_WAV) - fileType = EXTENSION_TYPE_WAV; - else if(type == EXTENSION_TYPE_DDS) - fileType = EXTENSION_TYPE_DDS; - else if(type == EXTENSION_TYPE_PNG) - fileType = EXTENSION_TYPE_PNG; - else if(type == EXTENSION_TYPE_JPG) - fileType = EXTENSION_TYPE_JPG; - else if(type == EXTENSION_TYPE_JPEG) - fileType = EXTENSION_TYPE_JPEG; - else if(type == EXTENSION_TYPE_HDR) - fileType = EXTENSION_TYPE_HDR; - else if(type == EXTENSION_TYPE_BMP) - fileType = EXTENSION_TYPE_BMP; - else if(type == EXTENSION_TYPE_TGA) - fileType = EXTENSION_TYPE_TGA; - else if(type == EXTENSION_TYPE_KTX) - fileType = EXTENSION_TYPE_KTX; - else if(type == EXTENSION_TYPE_PVR) - fileType = EXTENSION_TYPE_PVR; - else if(type == EXTENSION_TYPE_OBJ) - fileType = EXTENSION_TYPE_OBJ; - else if(type == EXTENSION_TYPE_FBX) - fileType = EXTENSION_TYPE_FBX; - else if(type == EXTENSION_TYPE_COLLADA) - fileType = EXTENSION_TYPE_COLLADA; - else if(type == EXTENSION_TYPE_BLEND) - fileType = EXTENSION_TYPE_BLEND; - else if(type == EXTENSION_TYPE_ANGELSCRIPT) - fileType = EXTENSION_TYPE_ANGELSCRIPT; - else if(type == EXTENSION_TYPE_LUASCRIPT) - fileType = EXTENSION_TYPE_LUASCRIPT; - else if(type == EXTENSION_TYPE_HLSL) - fileType = EXTENSION_TYPE_HLSL; - else if(type == EXTENSION_TYPE_GLSL) - fileType = EXTENSION_TYPE_GLSL; - else if(type == EXTENSION_TYPE_FRAGMENTSHADER) - fileType = EXTENSION_TYPE_FRAGMENTSHADER; - else if(type == EXTENSION_TYPE_VERTEXSHADER) - fileType = EXTENSION_TYPE_VERTEXSHADER; - else if(type == EXTENSION_TYPE_HTML) - fileType = EXTENSION_TYPE_HTML; - else - return false; - - return true; -} - -bool GetBinaryType(String path, StringHash &out fileType, bool useCache = false) -{ - StringHash type; - if (useCache) - { - File@ file = cache.GetFile(path); - if (file is null) - return false; - - if (file.size == 0) - return false; - - type = StringHash(file.ReadFileID()); - } - else - { - File@ file = File(); - if (!file.Open(path)) - return false; - - if (file.size == 0) - return false; - - type = StringHash(file.ReadFileID()); - } - - if (type == BINARY_TYPE_SCENE) - fileType = BINARY_TYPE_SCENE; - else if (type == BINARY_TYPE_PACKAGE) - fileType = BINARY_TYPE_PACKAGE; - else if (type == BINARY_TYPE_COMPRESSED_PACKAGE) - fileType = BINARY_TYPE_COMPRESSED_PACKAGE; - else if (type == BINARY_TYPE_ANGELSCRIPT) - fileType = BINARY_TYPE_ANGELSCRIPT; - else if (type == BINARY_TYPE_MODEL || type == BINARY_TYPE_MODEL2) - fileType = BINARY_TYPE_MODEL; - else if (type == BINARY_TYPE_SHADER) - fileType = BINARY_TYPE_SHADER; - else if (type == BINARY_TYPE_ANIMATION) - fileType = BINARY_TYPE_ANIMATION; - else - return false; - - return true; -} - -bool GetXmlType(String path, StringHash &out fileType, bool useCache = false) -{ - if (GetFileName(path).length == 0) - return false; // .gitignore etc. - String extension = GetExtension(path); - if (extension == ".txt" || extension == ".json" || extension == ".icns" || extension == ".atlas") - return false; - - String name; - if (useCache) - { - XMLFile@ xml = cache.GetResource("XMLFile", path); - if (xml is null) - return false; - - name = xml.root.name; - } - else - { - File@ file = File(); - if (!file.Open(path)) - return false; - - if (file.size == 0) - return false; - - XMLFile@ xml = XMLFile(); - if (xml.Load(file)) - name = xml.root.name; - else - return false; - } - - bool found = false; - if (!name.empty) - { - found = true; - StringHash type = StringHash(name); - if (type == XML_TYPE_SCENE) - fileType = XML_TYPE_SCENE; - else if (type == XML_TYPE_NODE) - fileType = XML_TYPE_NODE; - else if(type == XML_TYPE_MATERIAL) - fileType = XML_TYPE_MATERIAL; - else if(type == XML_TYPE_TECHNIQUE) - fileType = XML_TYPE_TECHNIQUE; - else if(type == XML_TYPE_PARTICLEEFFECT) - fileType = XML_TYPE_PARTICLEEFFECT; - else if(type == XML_TYPE_PARTICLEEMITTER) - fileType = XML_TYPE_PARTICLEEMITTER; - else if(type == XML_TYPE_TEXTURE) - fileType = XML_TYPE_TEXTURE; - else if(type == XML_TYPE_ELEMENT) - fileType = XML_TYPE_ELEMENT; - else if(type == XML_TYPE_ELEMENTS) - fileType = XML_TYPE_ELEMENTS; - else if (type == XML_TYPE_ANIMATION_SETTINGS) - fileType = XML_TYPE_ANIMATION_SETTINGS; - else if (type == XML_TYPE_RENDERPATH) - fileType = XML_TYPE_RENDERPATH; - else if (type == XML_TYPE_TEXTURE_ATLAS) - fileType = XML_TYPE_TEXTURE_ATLAS; - else if (type == XML_TYPE_2D_PARTICLE_EFFECT) - fileType = XML_TYPE_2D_PARTICLE_EFFECT; - else if (type == XML_TYPE_TEXTURE_3D) - fileType = XML_TYPE_TEXTURE_3D; - else if (type == XML_TYPE_CUBEMAP) - fileType = XML_TYPE_CUBEMAP; - else if (type == XML_TYPE_SPRITER_DATA) - fileType = XML_TYPE_SPRITER_DATA; - else - fileType = XML_TYPE_GENERIC; - } - return found; -} - -String ResourceTypeName(int resourceType) -{ - if (resourceType == RESOURCE_TYPE_UNUSABLE) - return "Unusable"; - else if (resourceType == RESOURCE_TYPE_UNKNOWN) - return "Unknown"; - else if (resourceType == RESOURCE_TYPE_NOTSET) - return "Uninitialized"; - else if (resourceType == RESOURCE_TYPE_SCENE) - return "Scene"; - else if (resourceType == RESOURCE_TYPE_SCRIPTFILE) - return "Script File"; - else if (resourceType == RESOURCE_TYPE_MODEL) - return "Model"; - else if (resourceType == RESOURCE_TYPE_MATERIAL) - return "Material"; - else if (resourceType == RESOURCE_TYPE_ANIMATION) - return "Animation"; - else if (resourceType == RESOURCE_TYPE_IMAGE) - return "Image"; - else if (resourceType == RESOURCE_TYPE_SOUND) - return "Sound"; - else if (resourceType == RESOURCE_TYPE_TEXTURE) - return "Texture"; - else if (resourceType == RESOURCE_TYPE_FONT) - return "Font"; - else if (resourceType == RESOURCE_TYPE_PREFAB) - return "Prefab"; - else if (resourceType == RESOURCE_TYPE_TECHNIQUE) - return "Render Technique"; - else if (resourceType == RESOURCE_TYPE_PARTICLEEFFECT) - return "Particle Effect"; - else if (resourceType == RESOURCE_TYPE_PARTICLEEMITTER) - return "Particle Emitter"; - else if (resourceType == RESOURCE_TYPE_UIELEMENT) - return "UI Element"; - else if (resourceType == RESOURCE_TYPE_UIELEMENTS) - return "UI Elements"; - else if (resourceType == RESOURCE_TYPE_ANIMATION_SETTINGS) - return "Animation Settings"; - else if (resourceType == RESOURCE_TYPE_RENDERPATH) - return "Render Path"; - else if (resourceType == RESOURCE_TYPE_TEXTURE_ATLAS) - return "Texture Atlas"; - else if (resourceType == RESOURCE_TYPE_2D_PARTICLE_EFFECT) - return "2D Particle Effect"; - else if (resourceType == RESOURCE_TYPE_TEXTURE_3D) - return "Texture 3D"; - else if (resourceType == RESOURCE_TYPE_CUBEMAP) - return "Cubemap"; - else if (resourceType == RESOURCE_TYPE_2D_ANIMATION_SET) - return "2D Animation Set"; - else - return ""; -} - -class BrowserDir -{ - uint id; - String resourceKey; - String name; - Array children; - Array files; - - BrowserDir(String path_) - { - resourceKey = path_; - String parent = GetParentPath(path_); - name = path_; - name.Replace(parent, ""); - id = browserDirIndex++; - } - - int opCmp(BrowserDir@ b) - { - return name.opCmp(b.name); - } - - BrowserFile@ AddFile(String name, uint resourceSourceIndex, uint sourceType) - { - String path = resourceKey.length > 0 ? (resourceKey + "/" + name) : name; - - BrowserFile@ file = BrowserFile(path, resourceSourceIndex, sourceType); - files.Push(file); - return file; - } - -} - -class BrowserFile -{ - uint id; - uint resourceSourceIndex; - String resourceKey; - String name; - String fullname; - String extension; - StringHash fileType; - int resourceType = 0; - int sourceType = 0; - int sortScore = 0; - WeakHandle browserFileListRow; - - BrowserFile(String path_, uint resourceSourceIndex_, int sourceType_) - { - sourceType = sourceType_; - resourceSourceIndex = resourceSourceIndex_; - resourceKey = path_; - name = GetFileName(path_); - extension = GetExtension(path_); - fullname = GetFileNameAndExtension(path_); - id = browserFileIndex++; - } - - int opCmp(BrowserFile@ b) - { - if (browserSearchSortMode == 1) - return fullname.opCmp(b.fullname); - else - return sortScore - b.sortScore; - } - - String GetResourceSource() - { - if (sourceType == BROWSER_FILE_SOURCE_RESOURCE_DIR) - return cache.resourceDirs[resourceSourceIndex]; - else - return "Unknown"; - } - - String GetFullPath() - { - return String(cache.resourceDirs[resourceSourceIndex] + resourceKey); - } - - String GetPath() - { - return resourceKey; - } - - void DetermainResourceType() - { - resourceType = GetResourceType(GetFullPath(), fileType, false); - Text@ browserFileListRow_ = browserFileListRow.Get(); - if (browserFileListRow_ !is null) - { - InitializeBrowserFileListRow(browserFileListRow_, this); - } - } - - String ResourceTypeName() - { - return ::ResourceTypeName(resourceType); - } - - void FileChanged() - { - if (!fileSystem.FileExists(GetFullPath())) - { - } - else - { - } - } -} - -void CreateResourcePreview(String path, Node@ previewNode) -{ - resourceBrowserPreview.autoUpdate = false; - int resourceType = GetResourceType(path); - if (resourceType > 0) - { - File file; - file.Open(path); - - if (resourceType == RESOURCE_TYPE_MODEL) - { - Model@ model = Model(); - if (model.Load(file)) - { - StaticModel@ staticModel = previewNode.CreateComponent("StaticModel"); - staticModel.model = model; - return; - } - } - else if (resourceType == RESOURCE_TYPE_MATERIAL) - { - Material@ material = Material(); - if (material.Load(file)) - { - StaticModel@ staticModel = previewNode.CreateComponent("StaticModel"); - staticModel.model = cache.GetResource("Model", "Models/Sphere.mdl"); - staticModel.material = material; - return; - } - } - else if (resourceType == RESOURCE_TYPE_IMAGE) - { - Image@ image = Image(); - if (image.Load(file)) - { - StaticModel@ staticModel = previewNode.CreateComponent("StaticModel"); - staticModel.model = cache.GetResource("Model", "Models/Editor/ImagePlane.mdl"); - Material@ material = cache.GetResource("Material", "Materials/Editor/TexturedUnlit.xml"); - Texture2D@ texture = Texture2D(); - texture.SetData(@image, true); - material.textures[TextureUnit(0)] = texture; - staticModel.material = material; - return; - } - } - else if (resourceType == RESOURCE_TYPE_PREFAB) - { - if (GetExtension(path) == ".xml") - { - XMLFile xmlFile; - if(xmlFile.Load(file)) - if(previewNode.LoadXML(xmlFile.root) && (previewNode.GetComponents("StaticModel", true).length > 0 || previewNode.GetComponents("AnimatedModel", true).length > 0)) - { - return; - } - } - else if(previewNode.Load(file) && (previewNode.GetComponents("StaticModel", true).length > 0 || previewNode.GetComponents("AnimatedModel", true).length > 0)) - return; - - previewNode.RemoveAllChildren(); - previewNode.RemoveAllComponents(); - } - else if (resourceType == RESOURCE_TYPE_PARTICLEEFFECT) - { - ParticleEffect@ particleEffect = ParticleEffect(); - if (particleEffect.Load(file)) - { - ParticleEmitter@ particleEmitter = previewNode.CreateComponent("ParticleEmitter"); - particleEmitter.effect = particleEffect; - particleEffect.activeTime = 0.0; - particleEmitter.Reset(); - resourceBrowserPreview.autoUpdate = true; - return; - } - } - } - - StaticModel@ staticModel = previewNode.CreateComponent("StaticModel"); - staticModel.model = cache.GetResource("Model", "Models/Editor/ImagePlane.mdl"); - Material@ material = cache.GetResource("Material", "Materials/Editor/TexturedUnlit.xml"); - Texture2D@ texture = Texture2D(); - Image@ noPreviewImage = cache.GetResource("Image", "Textures/Editor/NoPreviewAvailable.png"); - texture.SetData(noPreviewImage, false); - material.textures[TextureUnit(0)] = texture; - staticModel.material = material; - - return; -} - -void RotateResourceBrowserPreview(StringHash eventType, VariantMap& eventData) -{ - int elemX = eventData["ElementX"].GetI32(); - int elemY = eventData["ElementY"].GetI32(); - - if (resourceBrowserPreview.height > 0 && resourceBrowserPreview.width > 0) - { - float yaw = ((resourceBrowserPreview.height / 2) - elemY) * (90.0 / resourceBrowserPreview.height); - float pitch = ((resourceBrowserPreview.width / 2) - elemX) * (90.0 / resourceBrowserPreview.width); - - resourcePreviewNode.rotation = resourcePreviewNode.rotation.Slerp(Quaternion(yaw, pitch, 0), 0.1); - RefreshBrowserPreview(); - } -} - -void RefreshBrowserPreview() -{ - resourceBrowserPreview.QueueUpdate(); -} - -class ResourceType -{ - int id; - String name; - ResourceType(int id_, String name_) - { - id = id_; - name = name_; - } - int opCmp(ResourceType@ b) - { - return name.opCmp(b.name); - } -} +UIElement@ browserWindow; +Window@ browserFilterWindow; +ListView@ browserDirList; +ListView@ browserFileList; +LineEdit@ browserSearch; +BrowserFile@ browserDragFile; +Node@ browserDragNode; +Component@ browserDragComponent; +View3D@ resourceBrowserPreview; +Scene@ resourcePreviewScene; +Node@ resourcePreviewNode; +Node@ resourcePreviewCameraNode; +Node@ resourcePreviewLightNode; +Light@ resourcePreviewLight; +int browserSearchSortMode = 0; + +BrowserDir@ rootDir; +Array browserFiles; +Dictionary browserDirs; +Array activeResourceTypeFilters; +Array activeResourceDirFilters; + +Array browserFilesToScan; +const uint BROWSER_WORKER_ITEMS_PER_TICK = 10; +const uint BROWSER_SEARCH_LIMIT = 50; +const int BROWSER_SORT_MODE_ALPHA = 1; +const int BROWSER_SORT_MODE_SEARCH = 2; + +const int RESOURCE_TYPE_UNUSABLE = -2; +const int RESOURCE_TYPE_UNKNOWN = -1; +const int RESOURCE_TYPE_NOTSET = 0; +const int RESOURCE_TYPE_SCENE = 1; +const int RESOURCE_TYPE_SCRIPTFILE = 2; +const int RESOURCE_TYPE_MODEL = 3; +const int RESOURCE_TYPE_MATERIAL = 4; +const int RESOURCE_TYPE_ANIMATION = 5; +const int RESOURCE_TYPE_IMAGE = 6; +const int RESOURCE_TYPE_SOUND = 7; +const int RESOURCE_TYPE_TEXTURE = 8; +const int RESOURCE_TYPE_FONT = 9; +const int RESOURCE_TYPE_PREFAB = 10; +const int RESOURCE_TYPE_TECHNIQUE = 11; +const int RESOURCE_TYPE_PARTICLEEFFECT = 12; +const int RESOURCE_TYPE_UIELEMENT = 13; +const int RESOURCE_TYPE_UIELEMENTS = 14; +const int RESOURCE_TYPE_ANIMATION_SETTINGS = 15; +const int RESOURCE_TYPE_RENDERPATH = 16; +const int RESOURCE_TYPE_TEXTURE_ATLAS = 17; +const int RESOURCE_TYPE_2D_PARTICLE_EFFECT = 18; +const int RESOURCE_TYPE_TEXTURE_3D = 19; +const int RESOURCE_TYPE_CUBEMAP = 20; +const int RESOURCE_TYPE_PARTICLEEMITTER = 21; +const int RESOURCE_TYPE_2D_ANIMATION_SET = 22; +const int RESOURCE_TYPE_GENERIC_XML = 23; +const int RESOURCE_TYPE_GENERIC_JSON = 24; + +// any resource type > 0 is valid +const int NUMBER_OF_VALID_RESOURCE_TYPES = 24; + +const StringHash XML_TYPE_SCENE("scene"); +const StringHash XML_TYPE_NODE("node"); +const StringHash XML_TYPE_MATERIAL("material"); +const StringHash XML_TYPE_TECHNIQUE("technique"); +const StringHash XML_TYPE_PARTICLEEFFECT("particleeffect"); +const StringHash XML_TYPE_PARTICLEEMITTER("particleemitter"); +const StringHash XML_TYPE_TEXTURE("texture"); +const StringHash XML_TYPE_ELEMENT("element"); +const StringHash XML_TYPE_ELEMENTS("elements"); +const StringHash XML_TYPE_ANIMATION_SETTINGS("animation"); +const StringHash XML_TYPE_RENDERPATH("renderpath"); +const StringHash XML_TYPE_TEXTURE_ATLAS("TextureAtlas"); +const StringHash XML_TYPE_2D_PARTICLE_EFFECT("particleEmitterConfig"); +const StringHash XML_TYPE_TEXTURE_3D("texture3d"); +const StringHash XML_TYPE_CUBEMAP("cubemap"); +const StringHash XML_TYPE_SPRITER_DATA("spriter_data"); +const StringHash XML_TYPE_GENERIC("xml"); + +const StringHash JSON_TYPE_SCENE("scene"); +const StringHash JSON_TYPE_NODE("node"); +const StringHash JSON_TYPE_MATERIAL("material"); +const StringHash JSON_TYPE_TECHNIQUE("technique"); +const StringHash JSON_TYPE_PARTICLEEFFECT("particleeffect"); +const StringHash JSON_TYPE_PARTICLEEMITTER("particleemitter"); +const StringHash JSON_TYPE_TEXTURE("texture"); +const StringHash JSON_TYPE_ELEMENT("element"); +const StringHash JSON_TYPE_ELEMENTS("elements"); +const StringHash JSON_TYPE_ANIMATION_SETTINGS("animation"); +const StringHash JSON_TYPE_RENDERPATH("renderpath"); +const StringHash JSON_TYPE_TEXTURE_ATLAS("TextureAtlas"); +const StringHash JSON_TYPE_2D_PARTICLE_EFFECT("particleEmitterConfig"); +const StringHash JSON_TYPE_TEXTURE_3D("texture3d"); +const StringHash JSON_TYPE_CUBEMAP("cubemap"); +const StringHash JSON_TYPE_SPRITER_DATA("spriter_data"); +const StringHash JSON_TYPE_GENERIC("json"); + +const StringHash BINARY_TYPE_SCENE("USCN"); +const StringHash BINARY_TYPE_PACKAGE("UPAK"); +const StringHash BINARY_TYPE_COMPRESSED_PACKAGE("ULZ4"); +const StringHash BINARY_TYPE_ANGELSCRIPT("ASBC"); +const StringHash BINARY_TYPE_MODEL("UMDL"); +const StringHash BINARY_TYPE_MODEL2("UMD2"); +const StringHash BINARY_TYPE_SHADER("USHD"); +const StringHash BINARY_TYPE_ANIMATION("UANI"); + +const StringHash EXTENSION_TYPE_TTF(".ttf"); +const StringHash EXTENSION_TYPE_OTF(".otf"); +const StringHash EXTENSION_TYPE_OGG(".ogg"); +const StringHash EXTENSION_TYPE_WAV(".wav"); +const StringHash EXTENSION_TYPE_DDS(".dds"); +const StringHash EXTENSION_TYPE_PNG(".png"); +const StringHash EXTENSION_TYPE_JPG(".jpg"); +const StringHash EXTENSION_TYPE_JPEG(".jpeg"); +const StringHash EXTENSION_TYPE_HDR(".hdr"); +const StringHash EXTENSION_TYPE_BMP(".bmp"); +const StringHash EXTENSION_TYPE_TGA(".tga"); +const StringHash EXTENSION_TYPE_KTX(".ktx"); +const StringHash EXTENSION_TYPE_PVR(".pvr"); +const StringHash EXTENSION_TYPE_OBJ(".obj"); +const StringHash EXTENSION_TYPE_FBX(".fbx"); +const StringHash EXTENSION_TYPE_COLLADA(".dae"); +const StringHash EXTENSION_TYPE_BLEND(".blend"); +const StringHash EXTENSION_TYPE_ANGELSCRIPT(".as"); +const StringHash EXTENSION_TYPE_LUASCRIPT(".lua"); +const StringHash EXTENSION_TYPE_HLSL(".hlsl"); +const StringHash EXTENSION_TYPE_GLSL(".glsl"); +const StringHash EXTENSION_TYPE_FRAGMENTSHADER(".frag"); +const StringHash EXTENSION_TYPE_VERTEXSHADER(".vert"); +const StringHash EXTENSION_TYPE_HTML(".html"); + +const StringHash TEXT_VAR_FILE_ID("browser_file_id"); +const StringHash TEXT_VAR_DIR_ID("browser_dir_id"); +const StringHash TEXT_VAR_RESOURCE_TYPE("resource_type"); +const StringHash TEXT_VAR_RESOURCE_DIR_ID("resource_dir_id"); + +const int BROWSER_FILE_SOURCE_RESOURCE_DIR = 1; + +uint browserDirIndex = 1; +uint browserFileIndex = 1; +BrowserDir@ selectedBrowserDirectory; +BrowserFile@ selectedBrowserFile; +Text@ browserStatusMessage; +Text@ browserResultsMessage; +bool ignoreRefreshBrowserResults = false; +String resourceDirsCache; + +void CreateResourceBrowser() +{ + if (browserWindow !is null) return; + + CreateResourceBrowserUI(); + InitResourceBrowserPreview(); + RebuildResourceDatabase(); +} + +void RebuildResourceDatabase() +{ + if (browserWindow is null) + return; + + String newResourceDirsCache = Join(cache.resourceDirs, ';'); + ScanResourceDirectories(); + if (newResourceDirsCache != resourceDirsCache) + { + resourceDirsCache = newResourceDirsCache; + PopulateResourceDirFilters(); + } + PopulateBrowserDirectories(); + PopulateResourceBrowserFilesByDirectory(rootDir); +} + +void ScanResourceDirectories() +{ + browserDirs.Clear(); + browserFiles.Clear(); + browserFilesToScan.Clear(); + + rootDir = BrowserDir(""); + browserDirs.Set("", @rootDir); + + // collect all of the items and sort them afterwards + for(uint i=0; i < cache.resourceDirs.length; ++i) + { + if (activeResourceDirFilters.Find(i) > -1) + continue; + + ScanResourceDir(i); + } +} + +// used to stop ui from blocking while determining file types +void DoResourceBrowserWork() +{ + if (browserFilesToScan.length == 0) + return; + + int counter = 0; + bool updateBrowserUI = false; + BrowserFile@ scanItem = browserFilesToScan[0]; + while(counter < BROWSER_WORKER_ITEMS_PER_TICK) + { + scanItem.DetermainResourceType(); + + // next + browserFilesToScan.Erase(0); + if (browserFilesToScan.length > 0) + @scanItem = browserFilesToScan[0]; + else + break; + counter++; + } + + if (browserFilesToScan.length > 0) + browserStatusMessage.text = localization.Get("Files left to scan: " )+ browserFilesToScan.length; + else + browserStatusMessage.text = localization.Get("Scan complete"); + +} + +void CreateResourceBrowserUI() +{ + browserWindow = LoadEditorUI("UI/EditorResourceBrowser.xml"); + browserDirList = browserWindow.GetChild("DirectoryList", true); + browserFileList = browserWindow.GetChild("FileList", true); + browserSearch = browserWindow.GetChild("Search", true); + browserStatusMessage = browserWindow.GetChild("StatusMessage", true); + browserResultsMessage = browserWindow.GetChild("ResultsMessage", true); + // browserWindow.visible = false; + browserWindow.opacity = uiMaxOpacity; + + browserFilterWindow = LoadEditorUI("UI/EditorResourceFilterWindow.xml"); + CreateResourceFilterUI(); + HideResourceFilterWindow(); + + int height = Min(ui.root.height / 4, 300); + browserWindow.SetSize(900, height); + browserWindow.SetPosition(35, ui.root.height - height - 25); + + CloseContextMenu(); + ui.root.AddChild(browserWindow); + ui.root.AddChild(browserFilterWindow); + + SubscribeToEvent(browserWindow.GetChild("CloseButton", true), "Released", "HideResourceBrowserWindow"); + SubscribeToEvent(browserWindow.GetChild("RescanButton", true), "Released", "HandleRescanResourceBrowserClick"); + SubscribeToEvent(browserWindow.GetChild("FilterButton", true), "Released", "ToggleResourceFilterWindow"); + SubscribeToEvent(browserDirList, "SelectionChanged", "HandleResourceBrowserDirListSelectionChange"); + SubscribeToEvent(browserSearch, "TextChanged", "HandleResourceBrowserSearchTextChange"); + SubscribeToEvent(browserFileList, "ItemClicked", "HandleBrowserFileClick"); + SubscribeToEvent(browserFileList, "SelectionChanged", "HandleResourceBrowserFileListSelectionChange"); + SubscribeToEvent(cache, "FileChanged", "HandleFileChanged"); +} + +void CreateResourceFilterUI() +{ + UIElement@ options = browserFilterWindow.GetChild("TypeOptions", true); + CheckBox@ toggleAllTypes = browserFilterWindow.GetChild("ToggleAllTypes", true); + CheckBox@ toggleAllResourceDirs = browserFilterWindow.GetChild("ToggleAllResourceDirs", true); + SubscribeToEvent(toggleAllTypes, "Toggled", "HandleResourceTypeFilterToggleAllTypesToggled"); + SubscribeToEvent(toggleAllResourceDirs, "Toggled", "HandleResourceDirFilterToggleAllTypesToggled"); + SubscribeToEvent(browserFilterWindow.GetChild("CloseButton", true), "Released", "HideResourceFilterWindow"); + + int columns = 2; + UIElement@ col1 = browserFilterWindow.GetChild("TypeFilterColumn1", true); + UIElement@ col2 = browserFilterWindow.GetChild("TypeFilterColumn2", true); + + // use array to get sort of items + Array sorted; + for (int i=1; i <= NUMBER_OF_VALID_RESOURCE_TYPES; ++i) + sorted.Push(ResourceType(i, ResourceTypeName(i))); + + // 2 unknown types are reserved for the top, the rest are alphabetized + sorted.Sort(); + sorted.Insert(0, ResourceType(RESOURCE_TYPE_UNKNOWN, ResourceTypeName(RESOURCE_TYPE_UNKNOWN)) ); + sorted.Insert(0, ResourceType(RESOURCE_TYPE_UNUSABLE, ResourceTypeName(RESOURCE_TYPE_UNUSABLE)) ); + uint halfColumns = uint( Ceil( float(sorted.length) / float(columns) ) ); + + for (uint i = 0; i < sorted.length; ++i) + { + ResourceType@ type = sorted[i]; + UIElement@ resourceTypeHolder = UIElement(); + if (i < halfColumns) + col1.AddChild(resourceTypeHolder); + else + col2.AddChild(resourceTypeHolder); + + resourceTypeHolder.layoutMode = LM_HORIZONTAL; + resourceTypeHolder.layoutSpacing = 4; + + Text@ label = Text(); + label.style = "EditorAttributeText"; + label.text = type.name; + CheckBox@ checkbox = CheckBox(); + checkbox.name = type.id; + checkbox.SetStyleAuto(); + checkbox.vars[TEXT_VAR_RESOURCE_TYPE] = i; + checkbox.checked = true; + SubscribeToEvent(checkbox, "Toggled", "HandleResourceTypeFilterToggled"); + + resourceTypeHolder.AddChild(checkbox); + resourceTypeHolder.AddChild(label); + } +} + +void CreateDirList(BrowserDir@ dir, UIElement@ parentUI = null) +{ + Text@ dirText = Text(); + browserDirList.InsertItem(browserDirList.numItems, dirText, parentUI); + dirText.style = "FileSelectorListText"; + dirText.text = dir.resourceKey.empty ? localization.Get("Root") : dir.name; + dirText.name = dir.resourceKey; + dirText.vars[TEXT_VAR_DIR_ID] = dir.resourceKey; + + // Sort directories alphetically + browserSearchSortMode = BROWSER_SORT_MODE_ALPHA; + dir.children.Sort(); + + for(uint i=0; i 0) + fileText.dragDropMode = DD_SOURCE; + + { + Text@ text = Text(); + fileText.AddChild(text); + text.style = "FileSelectorListText"; + text.text = file.fullname; + text.name = file.resourceKey; + } + + { + Text@ text = Text(); + fileText.AddChild(text); + text.style = "FileSelectorListText"; + text.text = file.ResourceTypeName(); + } + + if (file.resourceType == RESOURCE_TYPE_MATERIAL || + file.resourceType == RESOURCE_TYPE_MODEL || + file.resourceType == RESOURCE_TYPE_PARTICLEEFFECT || + file.resourceType == RESOURCE_TYPE_PREFAB + ) + { + SubscribeToEvent(fileText, "DragBegin", "HandleBrowserFileDragBegin"); + SubscribeToEvent(fileText, "DragEnd", "HandleBrowserFileDragEnd"); + } + +} + +void InitResourceBrowserPreview() +{ + resourcePreviewScene = Scene("PreviewScene"); + resourcePreviewScene.CreateComponent("Octree"); + PhysicsWorld@ physicsWorld = resourcePreviewScene.CreateComponent("PhysicsWorld"); + physicsWorld.enabled = false; + physicsWorld.gravity = Vector3(0.0, 0.0, 0.0); + + Node@ zoneNode = resourcePreviewScene.CreateChild("Zone"); + Zone@ zone = zoneNode.CreateComponent("Zone"); + zone.boundingBox = BoundingBox(-1000, 1000); + zone.ambientColor = Color(0.15, 0.15, 0.15); + zone.fogColor = Color(0, 0, 0); + zone.fogStart = 10.0; + zone.fogEnd = 100.0; + + resourcePreviewCameraNode = resourcePreviewScene.CreateChild("PreviewCamera"); + resourcePreviewCameraNode.position = Vector3(0, 0, -1.5); + Camera@ camera = resourcePreviewCameraNode.CreateComponent("Camera"); + camera.nearClip = 0.1f; + camera.farClip = 100.0f; + + resourcePreviewLightNode = resourcePreviewScene.CreateChild("PreviewLight"); + resourcePreviewLightNode.direction = Vector3(0.5, -0.5, 0.5); + resourcePreviewLight = resourcePreviewLightNode.CreateComponent("Light"); + resourcePreviewLight.lightType = LIGHT_DIRECTIONAL; + resourcePreviewLight.specularIntensity = 0.5; + + resourceBrowserPreview = browserWindow.GetChild("ResourceBrowserPreview", true); + resourceBrowserPreview.SetFixedHeight(200); + resourceBrowserPreview.SetFixedWidth(266); + resourceBrowserPreview.SetView(resourcePreviewScene, camera); + resourceBrowserPreview.autoUpdate = false; + + resourcePreviewNode = resourcePreviewScene.CreateChild("PreviewNodeContainer"); + + SubscribeToEvent(resourceBrowserPreview, "DragMove", "RotateResourceBrowserPreview"); + + RefreshBrowserPreview(); +} + +// Opens a contextual menu based on what resource item was actioned +void HandleBrowserFileClick(StringHash eventType, VariantMap& eventData) +{ + if (eventData["Button"].GetI32() != MOUSEB_RIGHT) + return; + + UIElement@ uiElement = eventData["Item"].GetPtr(); + BrowserFile@ file = GetBrowserFileFromUIElement(uiElement); + + if (file is null) + return; + + Array actions; + if (file.resourceType == RESOURCE_TYPE_MATERIAL) + { + actions.Push(CreateBrowserFileActionMenu("Edit", "HandleBrowserEditResource", file)); + } + else if (file.resourceType == RESOURCE_TYPE_MODEL) + { + actions.Push(CreateBrowserFileActionMenu("Instance Animated Model", "HandleBrowserInstantiateAnimatedModel", file)); + actions.Push(CreateBrowserFileActionMenu("Instance Static Model", "HandleBrowserInstantiateStaticModel", file)); + } + else if (file.resourceType == RESOURCE_TYPE_PREFAB) + { + actions.Push(CreateBrowserFileActionMenu("Instance Prefab", "HandleBrowserInstantiatePrefab", file)); + actions.Push(CreateBrowserFileActionMenu("Instance in Spawner", "HandleBrowserInstantiateInSpawnEditor", file)); + } + else if (file.fileType == EXTENSION_TYPE_OBJ || + file.fileType == EXTENSION_TYPE_COLLADA || + file.fileType == EXTENSION_TYPE_FBX || + file.fileType == EXTENSION_TYPE_BLEND) + { + actions.Push(CreateBrowserFileActionMenu("Import Model", "HandleBrowserImportModel", file)); + actions.Push(CreateBrowserFileActionMenu("Import Scene", "HandleBrowserImportScene", file)); + } + else if (file.resourceType == RESOURCE_TYPE_UIELEMENT) + { + actions.Push(CreateBrowserFileActionMenu("Open UI Layout", "HandleBrowserOpenUILayout", file)); + } + else if (file.resourceType == RESOURCE_TYPE_SCENE) + { + actions.Push(CreateBrowserFileActionMenu("Load Scene", "HandleBrowserLoadScene", file)); + } + else if (file.resourceType == RESOURCE_TYPE_SCRIPTFILE) + { + actions.Push(CreateBrowserFileActionMenu("Execute Script", "HandleBrowserRunScript", file)); + } + else if (file.resourceType == RESOURCE_TYPE_PARTICLEEFFECT) + { + actions.Push(CreateBrowserFileActionMenu("Edit", "HandleBrowserEditResource", file)); + } + + actions.Push(CreateBrowserFileActionMenu("Open", "HandleBrowserOpenResource", file)); + + ActivateContextMenu(actions); +} + +BrowserDir@ GetBrowserDir(String path) +{ + BrowserDir@ browserDir; + browserDirs.Get(path, @browserDir); + return browserDir; +} + +// Makes sure the entire directory tree exists and new dir is linked to parent +BrowserDir@ InitBrowserDir(String path) +{ + BrowserDir@ browserDir; + if (browserDirs.Get(path, @browserDir)) + return browserDir; + + Array parts = path.Split('/'); + Array finishedParts; + if (parts.length > 0) + { + BrowserDir@ parent = rootDir; + for( uint i = 0; i < parts.length; ++i ) + { + finishedParts.Push(parts[i]); + String currentPath = Join(finishedParts, "/"); + if (!browserDirs.Get(currentPath, @browserDir)) + { + browserDir = BrowserDir(currentPath); + browserDirs.Set(currentPath, @browserDir); + parent.children.Push(browserDir); + } + @parent = browserDir; + } + return browserDir; + } + return null; +} + +void ScanResourceDir(uint resourceDirIndex) +{ + String resourceDir = cache.resourceDirs[resourceDirIndex]; + ScanResourceDirFiles("", resourceDirIndex); + Array dirs = fileSystem.ScanDir(resourceDir, "*", SCAN_DIRS, true); + for (uint i=0; i < dirs.length; ++i) + { + String path = dirs[i]; + if (path.EndsWith(".")) + continue; + + InitBrowserDir(path); + ScanResourceDirFiles(path, resourceDirIndex); + } +} + +void ScanResourceDirFiles(String path, uint resourceDirIndex) +{ + String fullPath = cache.resourceDirs[resourceDirIndex] + path; + if (!fileSystem.DirExists(fullPath)) + return; + + BrowserDir@ dir = GetBrowserDir(path); + + if (dir is null) + return; + + // get files in directory + Array dirFiles = fileSystem.ScanDir(fullPath, "*.*", SCAN_FILES, false); + + // add new files + for (uint x=0; x < dirFiles.length; x++) + { + String filename = dirFiles[x]; + BrowserFile@ browserFile = dir.AddFile(filename, resourceDirIndex, BROWSER_FILE_SOURCE_RESOURCE_DIR); + browserFiles.Push(browserFile); + browserFilesToScan.Push(browserFile); + } +} + +bool ToggleResourceBrowserWindow() +{ + if (browserWindow.visible == false) + ShowResourceBrowserWindow(); + else + HideResourceBrowserWindow(); + return true; +} + +void ShowResourceBrowserWindow() +{ + browserWindow.visible = true; + browserWindow.BringToFront(); + ui.focusElement = browserSearch; +} + +void HideResourceBrowserWindow() +{ + browserWindow.visible = false; +} + +void ToggleResourceFilterWindow() +{ + if (browserFilterWindow.visible) + HideResourceFilterWindow(); + else + ShowResourceFilterWindow(); +} +void HideResourceFilterWindow() +{ + browserFilterWindow.visible = false; +} + +void ShowResourceFilterWindow() +{ + int x = browserWindow.position.x + browserWindow.width - browserFilterWindow.width; + int y = browserWindow.position.y - browserFilterWindow.height - 1; + browserFilterWindow.position = IntVector2(x,y); + browserFilterWindow.visible = true; + browserFilterWindow.BringToFront(); +} + +void PopulateResourceDirFilters() +{ + UIElement@ resourceDirs = browserFilterWindow.GetChild("DirFilters", true); + resourceDirs.RemoveAllChildren(); + activeResourceDirFilters.Clear(); + for (uint i=0; i < cache.resourceDirs.length; ++i) + { + UIElement@ resourceDirHolder = UIElement(); + resourceDirs.AddChild(resourceDirHolder); + resourceDirHolder.layoutMode = LM_HORIZONTAL; + resourceDirHolder.layoutSpacing = 4; + resourceDirHolder.SetFixedHeight(16); + + Text@ label = Text(); + label.style = "EditorAttributeText"; + label.text = cache.resourceDirs[i].Replaced(fileSystem.programDir, ""); + CheckBox@ checkbox = CheckBox(); + checkbox.name = i; + checkbox.SetStyleAuto(); + checkbox.vars[TEXT_VAR_RESOURCE_DIR_ID] = i; + checkbox.checked = true; + SubscribeToEvent(checkbox, "Toggled", "HandleResourceDirFilterToggled"); + + + resourceDirHolder.AddChild(checkbox); + resourceDirHolder.AddChild(label); + } + +} + +void PopulateBrowserDirectories() +{ + browserDirList.RemoveAllItems(); + CreateDirList(rootDir); + browserDirList.selection = 0; +} + +void PopulateResourceBrowserFilesByDirectory(BrowserDir@ dir) +{ + @selectedBrowserDirectory = dir; + browserFileList.RemoveAllItems(); + if (dir is null) return; + + Array files; + for(uint x=0; x < dir.files.length; x++) + { + BrowserFile@ file = dir.files[x]; + + if (activeResourceTypeFilters.Find(file.resourceType) == -1) + files.Push(file); + } + + // Sort alphetically + browserSearchSortMode = BROWSER_SORT_MODE_ALPHA; + files.Sort(); + PopulateResourceBrowserResults(files); + browserResultsMessage.text = localization.Get("Showing files: ") + files.length; +} + + +void PopulateResourceBrowserBySearch() +{ + String query = browserSearch.text; + + Array scores; + Array scored; + Array filtered; + { + BrowserFile@ file; + for(uint x=0; x < browserFiles.length; x++) + { + @file = browserFiles[x]; + file.sortScore = -1; + if (activeResourceTypeFilters.Find(file.resourceType) > -1) + continue; + + if (activeResourceDirFilters.Find(file.resourceSourceIndex) > -1) + continue; + + int find = file.fullname.Find(query, 0, false); + if (find > -1) + { + int fudge = query.length - file.fullname.length; + int score = find * int(Abs(fudge*2)) + int(Abs(fudge)); + file.sortScore = score; + scored.Push(file); + scores.Push(score); + } + } + } + + // cut this down for a faster sort + if (scored.length > BROWSER_SEARCH_LIMIT) + { + scores.Sort(); + int scoreThreshold = scores[BROWSER_SEARCH_LIMIT]; + BrowserFile@ file; + for(uint x=0;x@ files) +{ + browserFileList.RemoveAllItems(); + for(uint i=0; i < files.length; ++i) + CreateFileList(files[i]); +} + +void RefreshBrowserResults() +{ + if (browserSearch.text.empty) + { + browserDirList.visible = true; + PopulateResourceBrowserFilesByDirectory(selectedBrowserDirectory); + } + else + { + browserDirList.visible = false; + PopulateResourceBrowserBySearch(); + } +} + +void HandleResourceTypeFilterToggleAllTypesToggled(StringHash eventType, VariantMap& eventData) +{ + CheckBox@ checkbox = eventData["Element"].GetPtr(); + UIElement@ filterHolder = browserFilterWindow.GetChild("TypeFilters", true); + Array children = filterHolder.GetChildren(true); + + ignoreRefreshBrowserResults = true; + for(uint i=0; i < children.length; ++i) + { + CheckBox@ filter = children[i]; + if (filter !is null) + filter.checked = checkbox.checked; + } + ignoreRefreshBrowserResults = false; + RefreshBrowserResults(); +} + +void HandleResourceTypeFilterToggled(StringHash eventType, VariantMap& eventData) +{ + CheckBox@ checkbox = eventData["Element"].GetPtr(); + if (!checkbox.vars.Contains(TEXT_VAR_RESOURCE_TYPE)) + return; + + int resourceType = checkbox.GetVar(TEXT_VAR_RESOURCE_TYPE).GetI32(); + int find = activeResourceTypeFilters.Find(resourceType); + + if (checkbox.checked && find != -1) + activeResourceTypeFilters.Erase(find); + else if (!checkbox.checked && find == -1) + activeResourceTypeFilters.Push(resourceType); + + if (ignoreRefreshBrowserResults == false) + RefreshBrowserResults(); +} + +void HandleResourceDirFilterToggleAllTypesToggled(StringHash eventType, VariantMap& eventData) +{ + CheckBox@ checkbox = eventData["Element"].GetPtr(); + UIElement@ filterHolder = browserFilterWindow.GetChild("DirFilters", true); + Array children = filterHolder.GetChildren(true); + + ignoreRefreshBrowserResults = true; + for(uint i=0; i < children.length; ++i) + { + CheckBox@ filter = children[i]; + if (filter !is null) + filter.checked = checkbox.checked; + } + ignoreRefreshBrowserResults = false; + RebuildResourceDatabase(); +} + +void HandleResourceDirFilterToggled(StringHash eventType, VariantMap& eventData) +{ + CheckBox@ checkbox = eventData["Element"].GetPtr(); + if (!checkbox.vars.Contains(TEXT_VAR_RESOURCE_DIR_ID)) + return; + + int resourceDir = checkbox.GetVar(TEXT_VAR_RESOURCE_DIR_ID).GetI32(); + int find = activeResourceDirFilters.Find(resourceDir); + + if (checkbox.checked && find != -1) + activeResourceDirFilters.Erase(find); + else if (!checkbox.checked && find == -1) + activeResourceDirFilters.Push(resourceDir); + + if (ignoreRefreshBrowserResults == false) + RebuildResourceDatabase(); +} + +void HandleRescanResourceBrowserClick(StringHash eventType, VariantMap& eventData) +{ + RebuildResourceDatabase(); +} + +void HandleResourceBrowserDirListSelectionChange(StringHash eventType, VariantMap& eventData) +{ + if (browserDirList.selection == M_MAX_UNSIGNED) + return; + + UIElement@ uiElement = browserDirList.GetItems()[browserDirList.selection]; + BrowserDir@ dir = GetBrowserDir(uiElement.vars[TEXT_VAR_DIR_ID].GetString()); + if (dir is null) + return; + + PopulateResourceBrowserFilesByDirectory(dir); +} + +void HandleResourceBrowserFileListSelectionChange(StringHash eventType, VariantMap& eventData) +{ + if (browserFileList.selection == M_MAX_UNSIGNED) + return; + + UIElement@ uiElement = browserFileList.GetItems()[browserFileList.selection]; + BrowserFile@ file = GetBrowserFileFromUIElement(uiElement); + if (file is null) + return; + + if (resourcePreviewNode !is null) + resourcePreviewNode.Remove(); + + resourcePreviewNode = resourcePreviewScene.CreateChild("PreviewNodeContainer"); + CreateResourcePreview(file.GetFullPath(), resourcePreviewNode); + + if (resourcePreviewNode !is null) + { + Array boxes; + Array staticModels = resourcePreviewNode.GetComponents("StaticModel", true); + Array animatedModels = resourcePreviewNode.GetComponents("AnimatedModel", true); + + for (uint i = 0; i < staticModels.length; ++i) + boxes.Push(cast(staticModels[i]).worldBoundingBox); + + for (uint i = 0; i < animatedModels.length; ++i) + boxes.Push(cast(animatedModels[i]).worldBoundingBox); + + if (boxes.length > 0) + { + Vector3 camPosition = Vector3(0.0, 0.0, -1.2); + BoundingBox biggestBox = boxes[0]; + for (uint i = 1; i < boxes.length; ++i) + { + if (boxes[i].size.length > biggestBox.size.length) + biggestBox = boxes[i]; + } + resourcePreviewCameraNode.position = biggestBox.center + camPosition * biggestBox.size.length; + } + + resourcePreviewScene.AddChild(resourcePreviewNode); + RefreshBrowserPreview(); + } +} + +void HandleResourceBrowserSearchTextChange(StringHash eventType, VariantMap& eventData) +{ + RefreshBrowserResults(); +} + +BrowserFile@ GetBrowserFileFromId(uint id) +{ + if (id == 0) + return null; + + BrowserFile@ file; + for(uint i=0; i(GetDrawableAtMousePostion()); + if (model !is null) + { + AssignMaterial(model, browserDragFile.resourceKey); + } + } + else if (browserDragFile.resourceType == RESOURCE_TYPE_PREFAB) + { + LoadNode(browserDragFile.GetFullPath(), null, true); + } + else if (browserDragFile.resourceType == RESOURCE_TYPE_MODEL) + { + Node@ createdNode = CreateNode(REPLICATED, true); + Model@ model = cache.GetResource("Model", browserDragFile.resourceKey); + if (model.skeleton.numBones > 0) + { + AnimatedModel@ am = createdNode.CreateComponent("AnimatedModel"); + am.model = model; + } + else + { + StaticModel@ sm = createdNode.CreateComponent("StaticModel"); + sm.model = model; + } + + AdjustNodePositionByAABB(createdNode); + } + + browserDragFile = null; + browserDragComponent = null; + browserDragNode = null; +} + +void HandleFileChanged(StringHash eventType, VariantMap& eventData) +{ + String filename = eventData["FileName"].GetString(); + BrowserFile@ file = GetBrowserFileFromPath(filename); + + if (file is null) + { + // TODO: new file logic when watchers are supported + return; + } + else + { + file.FileChanged(); + } +} + +Menu@ CreateBrowserFileActionMenu(String text, String handler, BrowserFile@ browserFile = null) +{ + Menu@ menu = CreateContextMenuItem(text, handler); + if (browserFile !is null) + menu.vars[TEXT_VAR_FILE_ID] = browserFile.id; + + return menu; +} + +int GetResourceType(String path) +{ + StringHash fileType; + return GetResourceType(path, fileType); +} + +int GetResourceType(String path, StringHash &out fileType, bool useCache = false) +{ + if (GetExtensionType(path, fileType) || GetBinaryType(path, fileType, useCache) || GetXmlType(path, fileType, useCache)) + return GetResourceType(fileType); + + return RESOURCE_TYPE_UNKNOWN; +} + +int GetResourceType(StringHash fileType) +{ + // Binary filetypes + if (fileType == BINARY_TYPE_SCENE) + return RESOURCE_TYPE_SCENE; + else if (fileType == BINARY_TYPE_PACKAGE) + return RESOURCE_TYPE_UNUSABLE; + else if (fileType == BINARY_TYPE_COMPRESSED_PACKAGE) + return RESOURCE_TYPE_UNUSABLE; + else if (fileType == BINARY_TYPE_ANGELSCRIPT) + return RESOURCE_TYPE_SCRIPTFILE; + else if (fileType == BINARY_TYPE_MODEL || fileType == BINARY_TYPE_MODEL2) + return RESOURCE_TYPE_MODEL; + else if (fileType == BINARY_TYPE_SHADER) + return RESOURCE_TYPE_UNUSABLE; + else if (fileType == BINARY_TYPE_ANIMATION) + return RESOURCE_TYPE_ANIMATION; + + // XML filetypes + else if (fileType == XML_TYPE_SCENE) + return RESOURCE_TYPE_SCENE; + else if (fileType == XML_TYPE_NODE) + return RESOURCE_TYPE_PREFAB; + else if(fileType == XML_TYPE_MATERIAL) + return RESOURCE_TYPE_MATERIAL; + else if(fileType == XML_TYPE_TECHNIQUE) + return RESOURCE_TYPE_TECHNIQUE; + else if(fileType == XML_TYPE_PARTICLEEFFECT) + return RESOURCE_TYPE_PARTICLEEFFECT; + else if(fileType == XML_TYPE_PARTICLEEMITTER) + return RESOURCE_TYPE_PARTICLEEMITTER; + else if(fileType == XML_TYPE_TEXTURE) + return RESOURCE_TYPE_TEXTURE; + else if(fileType == XML_TYPE_ELEMENT) + return RESOURCE_TYPE_UIELEMENT; + else if(fileType == XML_TYPE_ELEMENTS) + return RESOURCE_TYPE_UIELEMENTS; + else if (fileType == XML_TYPE_ANIMATION_SETTINGS) + return RESOURCE_TYPE_ANIMATION_SETTINGS; + else if (fileType == XML_TYPE_RENDERPATH) + return RESOURCE_TYPE_RENDERPATH; + else if (fileType == XML_TYPE_TEXTURE_ATLAS) + return RESOURCE_TYPE_TEXTURE_ATLAS; + else if (fileType == XML_TYPE_2D_PARTICLE_EFFECT) + return RESOURCE_TYPE_2D_PARTICLE_EFFECT; + else if (fileType == XML_TYPE_TEXTURE_3D) + return RESOURCE_TYPE_TEXTURE_3D; + else if (fileType == XML_TYPE_CUBEMAP) + return RESOURCE_TYPE_CUBEMAP; + else if (fileType == XML_TYPE_SPRITER_DATA) + return RESOURCE_TYPE_2D_ANIMATION_SET; + else if (fileType == XML_TYPE_GENERIC) + return RESOURCE_TYPE_GENERIC_XML; + + // JSON filetypes + else if (fileType == JSON_TYPE_SCENE) + return RESOURCE_TYPE_SCENE; + else if (fileType == JSON_TYPE_NODE) + return RESOURCE_TYPE_PREFAB; + else if(fileType == JSON_TYPE_MATERIAL) + return RESOURCE_TYPE_MATERIAL; + else if(fileType == JSON_TYPE_TECHNIQUE) + return RESOURCE_TYPE_TECHNIQUE; + else if(fileType == JSON_TYPE_PARTICLEEFFECT) + return RESOURCE_TYPE_PARTICLEEFFECT; + else if(fileType == JSON_TYPE_PARTICLEEMITTER) + return RESOURCE_TYPE_PARTICLEEMITTER; + else if(fileType == JSON_TYPE_TEXTURE) + return RESOURCE_TYPE_TEXTURE; + else if(fileType == JSON_TYPE_ELEMENT) + return RESOURCE_TYPE_UIELEMENT; + else if(fileType == JSON_TYPE_ELEMENTS) + return RESOURCE_TYPE_UIELEMENTS; + else if (fileType == JSON_TYPE_ANIMATION_SETTINGS) + return RESOURCE_TYPE_ANIMATION_SETTINGS; + else if (fileType == JSON_TYPE_RENDERPATH) + return RESOURCE_TYPE_RENDERPATH; + else if (fileType == JSON_TYPE_TEXTURE_ATLAS) + return RESOURCE_TYPE_TEXTURE_ATLAS; + else if (fileType == JSON_TYPE_2D_PARTICLE_EFFECT) + return RESOURCE_TYPE_2D_PARTICLE_EFFECT; + else if (fileType == JSON_TYPE_TEXTURE_3D) + return RESOURCE_TYPE_TEXTURE_3D; + else if (fileType == JSON_TYPE_CUBEMAP) + return RESOURCE_TYPE_CUBEMAP; + else if (fileType == JSON_TYPE_SPRITER_DATA) + return RESOURCE_TYPE_2D_ANIMATION_SET; + else if (fileType == JSON_TYPE_GENERIC) + return RESOURCE_TYPE_GENERIC_JSON; + + // Extension filetypes + else if (fileType == EXTENSION_TYPE_TTF) + return RESOURCE_TYPE_FONT; + else if (fileType == EXTENSION_TYPE_OTF) + return RESOURCE_TYPE_FONT; + else if (fileType == EXTENSION_TYPE_OGG) + return RESOURCE_TYPE_SOUND; + else if(fileType == EXTENSION_TYPE_WAV) + return RESOURCE_TYPE_SOUND; + else if(fileType == EXTENSION_TYPE_DDS) + return RESOURCE_TYPE_IMAGE; + else if(fileType == EXTENSION_TYPE_PNG) + return RESOURCE_TYPE_IMAGE; + else if(fileType == EXTENSION_TYPE_JPG) + return RESOURCE_TYPE_IMAGE; + else if(fileType == EXTENSION_TYPE_JPEG) + return RESOURCE_TYPE_IMAGE; + else if(fileType == EXTENSION_TYPE_HDR) + return RESOURCE_TYPE_IMAGE; + else if(fileType == EXTENSION_TYPE_BMP) + return RESOURCE_TYPE_IMAGE; + else if(fileType == EXTENSION_TYPE_TGA) + return RESOURCE_TYPE_IMAGE; + else if(fileType == EXTENSION_TYPE_KTX) + return RESOURCE_TYPE_IMAGE; + else if(fileType == EXTENSION_TYPE_PVR) + return RESOURCE_TYPE_IMAGE; + else if(fileType == EXTENSION_TYPE_OBJ) + return RESOURCE_TYPE_UNUSABLE; + else if(fileType == EXTENSION_TYPE_FBX) + return RESOURCE_TYPE_UNUSABLE; + else if(fileType == EXTENSION_TYPE_COLLADA) + return RESOURCE_TYPE_UNUSABLE; + else if(fileType == EXTENSION_TYPE_BLEND) + return RESOURCE_TYPE_UNUSABLE; + else if(fileType == EXTENSION_TYPE_ANGELSCRIPT) + return RESOURCE_TYPE_SCRIPTFILE; + else if(fileType == EXTENSION_TYPE_LUASCRIPT) + return RESOURCE_TYPE_SCRIPTFILE; + else if(fileType == EXTENSION_TYPE_HLSL) + return RESOURCE_TYPE_UNUSABLE; + else if(fileType == EXTENSION_TYPE_GLSL) + return RESOURCE_TYPE_UNUSABLE; + else if(fileType == EXTENSION_TYPE_FRAGMENTSHADER) + return RESOURCE_TYPE_UNUSABLE; + else if(fileType == EXTENSION_TYPE_VERTEXSHADER) + return RESOURCE_TYPE_UNUSABLE; + else if(fileType == EXTENSION_TYPE_HTML) + return RESOURCE_TYPE_UNUSABLE; + + return RESOURCE_TYPE_UNKNOWN; +} + +// TODO: Должно быть bool GetExtensionType(String path, StringHash &out fileType). +// Временный workaround для https://github.com/urho3d/urho3d/issues/3148 +bool GetExtensionType(String path, StringHash& fileType) +{ + StringHash type = StringHash(GetExtension(path)); + if (type == EXTENSION_TYPE_TTF) + fileType = EXTENSION_TYPE_TTF; + else if (type == EXTENSION_TYPE_OTF) + fileType = EXTENSION_TYPE_OTF; + else if (type == EXTENSION_TYPE_OGG) + fileType = EXTENSION_TYPE_OGG; + else if(type == EXTENSION_TYPE_WAV) + fileType = EXTENSION_TYPE_WAV; + else if(type == EXTENSION_TYPE_DDS) + fileType = EXTENSION_TYPE_DDS; + else if(type == EXTENSION_TYPE_PNG) + fileType = EXTENSION_TYPE_PNG; + else if(type == EXTENSION_TYPE_JPG) + fileType = EXTENSION_TYPE_JPG; + else if(type == EXTENSION_TYPE_JPEG) + fileType = EXTENSION_TYPE_JPEG; + else if(type == EXTENSION_TYPE_HDR) + fileType = EXTENSION_TYPE_HDR; + else if(type == EXTENSION_TYPE_BMP) + fileType = EXTENSION_TYPE_BMP; + else if(type == EXTENSION_TYPE_TGA) + fileType = EXTENSION_TYPE_TGA; + else if(type == EXTENSION_TYPE_KTX) + fileType = EXTENSION_TYPE_KTX; + else if(type == EXTENSION_TYPE_PVR) + fileType = EXTENSION_TYPE_PVR; + else if(type == EXTENSION_TYPE_OBJ) + fileType = EXTENSION_TYPE_OBJ; + else if(type == EXTENSION_TYPE_FBX) + fileType = EXTENSION_TYPE_FBX; + else if(type == EXTENSION_TYPE_COLLADA) + fileType = EXTENSION_TYPE_COLLADA; + else if(type == EXTENSION_TYPE_BLEND) + fileType = EXTENSION_TYPE_BLEND; + else if(type == EXTENSION_TYPE_ANGELSCRIPT) + fileType = EXTENSION_TYPE_ANGELSCRIPT; + else if(type == EXTENSION_TYPE_LUASCRIPT) + fileType = EXTENSION_TYPE_LUASCRIPT; + else if(type == EXTENSION_TYPE_HLSL) + fileType = EXTENSION_TYPE_HLSL; + else if(type == EXTENSION_TYPE_GLSL) + fileType = EXTENSION_TYPE_GLSL; + else if(type == EXTENSION_TYPE_FRAGMENTSHADER) + fileType = EXTENSION_TYPE_FRAGMENTSHADER; + else if(type == EXTENSION_TYPE_VERTEXSHADER) + fileType = EXTENSION_TYPE_VERTEXSHADER; + else if(type == EXTENSION_TYPE_HTML) + fileType = EXTENSION_TYPE_HTML; + else + return false; + + return true; +} + +bool GetBinaryType(String path, StringHash &out fileType, bool useCache = false) +{ + StringHash type; + if (useCache) + { + File@ file = cache.GetFile(path); + if (file is null) + return false; + + if (file.size == 0) + return false; + + type = StringHash(file.ReadFileID()); + } + else + { + File@ file = File(); + if (!file.Open(path)) + return false; + + if (file.size == 0) + return false; + + type = StringHash(file.ReadFileID()); + } + + if (type == BINARY_TYPE_SCENE) + fileType = BINARY_TYPE_SCENE; + else if (type == BINARY_TYPE_PACKAGE) + fileType = BINARY_TYPE_PACKAGE; + else if (type == BINARY_TYPE_COMPRESSED_PACKAGE) + fileType = BINARY_TYPE_COMPRESSED_PACKAGE; + else if (type == BINARY_TYPE_ANGELSCRIPT) + fileType = BINARY_TYPE_ANGELSCRIPT; + else if (type == BINARY_TYPE_MODEL || type == BINARY_TYPE_MODEL2) + fileType = BINARY_TYPE_MODEL; + else if (type == BINARY_TYPE_SHADER) + fileType = BINARY_TYPE_SHADER; + else if (type == BINARY_TYPE_ANIMATION) + fileType = BINARY_TYPE_ANIMATION; + else + return false; + + return true; +} + +bool GetXmlType(String path, StringHash &out fileType, bool useCache = false) +{ + if (GetFileName(path).length == 0) + return false; // .gitignore etc. + String extension = GetExtension(path); + if (extension == ".txt" || extension == ".json" || extension == ".icns" || extension == ".atlas") + return false; + + String name; + if (useCache) + { + XMLFile@ xml = cache.GetResource("XMLFile", path); + if (xml is null) + return false; + + name = xml.root.name; + } + else + { + File@ file = File(); + if (!file.Open(path)) + return false; + + if (file.size == 0) + return false; + + XMLFile@ xml = XMLFile(); + if (xml.Load(file)) + name = xml.root.name; + else + return false; + } + + bool found = false; + if (!name.empty) + { + found = true; + StringHash type = StringHash(name); + if (type == XML_TYPE_SCENE) + fileType = XML_TYPE_SCENE; + else if (type == XML_TYPE_NODE) + fileType = XML_TYPE_NODE; + else if(type == XML_TYPE_MATERIAL) + fileType = XML_TYPE_MATERIAL; + else if(type == XML_TYPE_TECHNIQUE) + fileType = XML_TYPE_TECHNIQUE; + else if(type == XML_TYPE_PARTICLEEFFECT) + fileType = XML_TYPE_PARTICLEEFFECT; + else if(type == XML_TYPE_PARTICLEEMITTER) + fileType = XML_TYPE_PARTICLEEMITTER; + else if(type == XML_TYPE_TEXTURE) + fileType = XML_TYPE_TEXTURE; + else if(type == XML_TYPE_ELEMENT) + fileType = XML_TYPE_ELEMENT; + else if(type == XML_TYPE_ELEMENTS) + fileType = XML_TYPE_ELEMENTS; + else if (type == XML_TYPE_ANIMATION_SETTINGS) + fileType = XML_TYPE_ANIMATION_SETTINGS; + else if (type == XML_TYPE_RENDERPATH) + fileType = XML_TYPE_RENDERPATH; + else if (type == XML_TYPE_TEXTURE_ATLAS) + fileType = XML_TYPE_TEXTURE_ATLAS; + else if (type == XML_TYPE_2D_PARTICLE_EFFECT) + fileType = XML_TYPE_2D_PARTICLE_EFFECT; + else if (type == XML_TYPE_TEXTURE_3D) + fileType = XML_TYPE_TEXTURE_3D; + else if (type == XML_TYPE_CUBEMAP) + fileType = XML_TYPE_CUBEMAP; + else if (type == XML_TYPE_SPRITER_DATA) + fileType = XML_TYPE_SPRITER_DATA; + else + fileType = XML_TYPE_GENERIC; + } + return found; +} + +String ResourceTypeName(int resourceType) +{ + if (resourceType == RESOURCE_TYPE_UNUSABLE) + return "Unusable"; + else if (resourceType == RESOURCE_TYPE_UNKNOWN) + return "Unknown"; + else if (resourceType == RESOURCE_TYPE_NOTSET) + return "Uninitialized"; + else if (resourceType == RESOURCE_TYPE_SCENE) + return "Scene"; + else if (resourceType == RESOURCE_TYPE_SCRIPTFILE) + return "Script File"; + else if (resourceType == RESOURCE_TYPE_MODEL) + return "Model"; + else if (resourceType == RESOURCE_TYPE_MATERIAL) + return "Material"; + else if (resourceType == RESOURCE_TYPE_ANIMATION) + return "Animation"; + else if (resourceType == RESOURCE_TYPE_IMAGE) + return "Image"; + else if (resourceType == RESOURCE_TYPE_SOUND) + return "Sound"; + else if (resourceType == RESOURCE_TYPE_TEXTURE) + return "Texture"; + else if (resourceType == RESOURCE_TYPE_FONT) + return "Font"; + else if (resourceType == RESOURCE_TYPE_PREFAB) + return "Prefab"; + else if (resourceType == RESOURCE_TYPE_TECHNIQUE) + return "Render Technique"; + else if (resourceType == RESOURCE_TYPE_PARTICLEEFFECT) + return "Particle Effect"; + else if (resourceType == RESOURCE_TYPE_PARTICLEEMITTER) + return "Particle Emitter"; + else if (resourceType == RESOURCE_TYPE_UIELEMENT) + return "UI Element"; + else if (resourceType == RESOURCE_TYPE_UIELEMENTS) + return "UI Elements"; + else if (resourceType == RESOURCE_TYPE_ANIMATION_SETTINGS) + return "Animation Settings"; + else if (resourceType == RESOURCE_TYPE_RENDERPATH) + return "Render Path"; + else if (resourceType == RESOURCE_TYPE_TEXTURE_ATLAS) + return "Texture Atlas"; + else if (resourceType == RESOURCE_TYPE_2D_PARTICLE_EFFECT) + return "2D Particle Effect"; + else if (resourceType == RESOURCE_TYPE_TEXTURE_3D) + return "Texture 3D"; + else if (resourceType == RESOURCE_TYPE_CUBEMAP) + return "Cubemap"; + else if (resourceType == RESOURCE_TYPE_2D_ANIMATION_SET) + return "2D Animation Set"; + else + return ""; +} + +class BrowserDir +{ + uint id; + String resourceKey; + String name; + Array children; + Array files; + + BrowserDir(String path_) + { + resourceKey = path_; + String parent = GetParentPath(path_); + name = path_; + name.Replace(parent, ""); + id = browserDirIndex++; + } + + int opCmp(BrowserDir@ b) + { + return name.opCmp(b.name); + } + + BrowserFile@ AddFile(String name, uint resourceSourceIndex, uint sourceType) + { + String path = resourceKey.length > 0 ? (resourceKey + "/" + name) : name; + + BrowserFile@ file = BrowserFile(path, resourceSourceIndex, sourceType); + files.Push(file); + return file; + } + +} + +class BrowserFile +{ + uint id; + uint resourceSourceIndex; + String resourceKey; + String name; + String fullname; + String extension; + StringHash fileType; + int resourceType = 0; + int sourceType = 0; + int sortScore = 0; + WeakHandle browserFileListRow; + + BrowserFile(String path_, uint resourceSourceIndex_, int sourceType_) + { + sourceType = sourceType_; + resourceSourceIndex = resourceSourceIndex_; + resourceKey = path_; + name = GetFileName(path_); + extension = GetExtension(path_); + fullname = GetFileNameAndExtension(path_); + id = browserFileIndex++; + } + + int opCmp(BrowserFile@ b) + { + if (browserSearchSortMode == 1) + return fullname.opCmp(b.fullname); + else + return sortScore - b.sortScore; + } + + String GetResourceSource() + { + if (sourceType == BROWSER_FILE_SOURCE_RESOURCE_DIR) + return cache.resourceDirs[resourceSourceIndex]; + else + return "Unknown"; + } + + String GetFullPath() + { + return String(cache.resourceDirs[resourceSourceIndex] + resourceKey); + } + + String GetPath() + { + return resourceKey; + } + + void DetermainResourceType() + { + resourceType = GetResourceType(GetFullPath(), fileType, false); + Text@ browserFileListRow_ = browserFileListRow.Get(); + if (browserFileListRow_ !is null) + { + InitializeBrowserFileListRow(browserFileListRow_, this); + } + } + + String ResourceTypeName() + { + return ::ResourceTypeName(resourceType); + } + + void FileChanged() + { + if (!fileSystem.FileExists(GetFullPath())) + { + } + else + { + } + } +} + +void CreateResourcePreview(String path, Node@ previewNode) +{ + resourceBrowserPreview.autoUpdate = false; + int resourceType = GetResourceType(path); + if (resourceType > 0) + { + File file; + file.Open(path); + + if (resourceType == RESOURCE_TYPE_MODEL) + { + Model@ model = Model(); + if (model.Load(file)) + { + StaticModel@ staticModel = previewNode.CreateComponent("StaticModel"); + staticModel.model = model; + return; + } + } + else if (resourceType == RESOURCE_TYPE_MATERIAL) + { + Material@ material = Material(); + if (material.Load(file)) + { + StaticModel@ staticModel = previewNode.CreateComponent("StaticModel"); + staticModel.model = cache.GetResource("Model", "Editor/Models/Sphere.mdl"); + staticModel.material = material; + return; + } + } + else if (resourceType == RESOURCE_TYPE_IMAGE) + { + Image@ image = Image(); + if (image.Load(file)) + { + StaticModel@ staticModel = previewNode.CreateComponent("StaticModel"); + staticModel.model = cache.GetResource("Model", "Editor/Models/ImagePlane.mdl"); + Material@ material = cache.GetResource("Material", "Editor/Materials/TexturedUnlit.xml"); + Texture2D@ texture = Texture2D(); + texture.SetData(@image, true); + material.textures[TextureUnit(0)] = texture; + staticModel.material = material; + return; + } + } + else if (resourceType == RESOURCE_TYPE_PREFAB) + { + if (GetExtension(path) == ".xml") + { + XMLFile xmlFile; + if(xmlFile.Load(file)) + if(previewNode.LoadXML(xmlFile.root) && (previewNode.GetComponents("StaticModel", true).length > 0 || previewNode.GetComponents("AnimatedModel", true).length > 0)) + { + return; + } + } + else if(previewNode.Load(file) && (previewNode.GetComponents("StaticModel", true).length > 0 || previewNode.GetComponents("AnimatedModel", true).length > 0)) + return; + + previewNode.RemoveAllChildren(); + previewNode.RemoveAllComponents(); + } + else if (resourceType == RESOURCE_TYPE_PARTICLEEFFECT) + { + ParticleEffect@ particleEffect = ParticleEffect(); + if (particleEffect.Load(file)) + { + ParticleEmitter@ particleEmitter = previewNode.CreateComponent("ParticleEmitter"); + particleEmitter.effect = particleEffect; + particleEffect.activeTime = 0.0; + particleEmitter.Reset(); + resourceBrowserPreview.autoUpdate = true; + return; + } + } + } + + StaticModel@ staticModel = previewNode.CreateComponent("StaticModel"); + staticModel.model = cache.GetResource("Model", "Editor/Models/ImagePlane.mdl"); + Material@ material = cache.GetResource("Material", "Editor/Materials/TexturedUnlit.xml"); + Texture2D@ texture = Texture2D(); + Image@ noPreviewImage = cache.GetResource("Image", "Editor/Textures/NoPreviewAvailable.png"); + texture.SetData(noPreviewImage, false); + material.textures[TextureUnit(0)] = texture; + staticModel.material = material; + + return; +} + +void RotateResourceBrowserPreview(StringHash eventType, VariantMap& eventData) +{ + int elemX = eventData["ElementX"].GetI32(); + int elemY = eventData["ElementY"].GetI32(); + + if (resourceBrowserPreview.height > 0 && resourceBrowserPreview.width > 0) + { + float yaw = ((resourceBrowserPreview.height / 2) - elemY) * (90.0 / resourceBrowserPreview.height); + float pitch = ((resourceBrowserPreview.width / 2) - elemX) * (90.0 / resourceBrowserPreview.width); + + resourcePreviewNode.rotation = resourcePreviewNode.rotation.Slerp(Quaternion(yaw, pitch, 0), 0.1); + RefreshBrowserPreview(); + } +} + +void RefreshBrowserPreview() +{ + resourceBrowserPreview.QueueUpdate(); +} + +class ResourceType +{ + int id; + String name; + ResourceType(int id_, String name_) + { + id = id_; + name = name_; + } + int opCmp(ResourceType@ b) + { + return name.opCmp(b.name); + } +} diff --git a/bin/Data/Scripts/Editor/EditorScene.as b/bin/EditorData/Editor/Scripts/EditorScene.as similarity index 96% rename from bin/Data/Scripts/Editor/EditorScene.as rename to bin/EditorData/Editor/Scripts/EditorScene.as index 1d37f5daace..c9ba59ba80e 100644 --- a/bin/Data/Scripts/Editor/EditorScene.as +++ b/bin/EditorData/Editor/Scripts/EditorScene.as @@ -1,1664 +1,1664 @@ -/// Urho3D editor scene handling - -#include "Scripts/Editor/EditorHierarchyWindow.as" -#include "Scripts/Editor/EditorInspectorWindow.as" -#include "Scripts/Editor/EditorCubeCapture.as" - -const int PICK_GEOMETRIES = 0; -const int PICK_LIGHTS = 1; -const int PICK_ZONES = 2; -const int PICK_RIGIDBODIES = 3; -const int PICK_UI_ELEMENTS = 4; -const int MAX_PICK_MODES = 5; -const int MAX_UNDOSTACK_SIZE = 256; - -Scene@ editorScene; - -String instantiateFileName; -CreateMode instantiateMode = REPLICATED; -bool sceneModified = false; -bool runUpdate = false; - -Array selectedNodes; -Array selectedComponents; -Node@ editNode; -Array editNodes; -Array editComponents; -uint numEditableComponentsPerNode = 1; - -Array sceneCopyBuffer; - -bool suppressSceneChanges = false; -bool inSelectionModify = false; -bool skipMruScene = false; - -Array undoStack; -uint undoStackPos = 0; - -bool revertOnPause = false; -XMLFile@ revertData; - -Vector3 lastOffsetForSmartDuplicate; - -void ClearSceneSelection() -{ - selectedNodes.Clear(); - selectedComponents.Clear(); - editNode = null; - editNodes.Clear(); - editComponents.Clear(); - numEditableComponentsPerNode = 1; - - HideGizmo(); -} - -void CreateScene() -{ - // Create a scene only once here - editorScene = Scene(); - - // Allow access to the scene from the console - script.defaultScene = editorScene; - - // Always pause the scene, and do updates manually - editorScene.updateEnabled = false; -} - -bool ResetScene() -{ - ui.cursor.shape = CS_BUSY; - - if (messageBoxCallback is null && sceneModified) - { - MessageBox@ messageBox = MessageBox("Scene has been modified.\nContinue to reset?", "Warning"); - if (messageBox.window !is null) - { - Button@ cancelButton = messageBox.window.GetChild("CancelButton", true); - cancelButton.visible = true; - cancelButton.focus = true; - SubscribeToEvent(messageBox, "MessageACK", "HandleMessageAcknowledgement"); - messageBoxCallback = @ResetScene; - return false; - } - } - else - messageBoxCallback = null; - - suppressSceneChanges = true; - - // Create a scene with default values, these will be overridden when loading scenes - editorScene.Clear(); - editorScene.CreateComponent("Octree"); - editorScene.CreateComponent("DebugRenderer"); - - // Release resources that became unused after the scene clear - cache.ReleaseAllResources(false); - - sceneModified = false; - revertData = null; - StopSceneUpdate(); - - UpdateWindowTitle(); - DisableInspectorLock(); - UpdateHierarchyItem(editorScene, true); - ClearEditActions(); - - suppressSceneChanges = false; - - ResetCamera(); - CreateGizmo(); - CreateGrid(); - SetActiveViewport(viewports[0]); - - return true; -} - -void SetResourcePath(String newPath, bool usePreferredDir = true, bool additive = false) -{ - if (newPath.empty) - return; - if (!IsAbsolutePath(newPath)) - newPath = fileSystem.currentDir + newPath; - - if (usePreferredDir) - newPath = AddTrailingSlash(cache.GetPreferredResourceDir(newPath)); - else - newPath = AddTrailingSlash(newPath); - - if (newPath == sceneResourcePath) - return; - - // Remove the old scene resource path if any. However make sure that the default data paths do not get removed - if (!additive) - { - cache.ReleaseAllResources(false); - renderer.ReloadShaders(); - - String check = AddTrailingSlash(sceneResourcePath); - bool isDefaultResourcePath = check.Compare(fileSystem.programDir + "Data/", false) == 0 || - check.Compare(fileSystem.programDir + "CoreData/", false) == 0; - - if (!sceneResourcePath.empty && !isDefaultResourcePath) - cache.RemoveResourceDir(sceneResourcePath); - } - else - { - // If additive (path of a loaded prefab) check that the new path isn't already part of an old path - Array@ resourceDirs = cache.resourceDirs; - - for (uint i = 0; i < resourceDirs.length; ++i) - { - if (newPath.StartsWith(resourceDirs[i], false)) - return; - } - } - - // Add resource path as first priority so that it takes precedence over the default data paths - cache.AddResourceDir(newPath, 0); - RebuildResourceDatabase(); - - if (!additive) - { - sceneResourcePath = newPath; - uiScenePath = GetResourceSubPath(newPath, "Scenes"); - uiElementPath = GetResourceSubPath(newPath, "UI"); - uiNodePath = GetResourceSubPath(newPath, "Objects"); - uiScriptPath = GetResourceSubPath(newPath, "Scripts"); - uiParticlePath = GetResourceSubPath(newPath, "Particle"); - } -} - -String GetResourceSubPath(String basePath, const String&in subPath) -{ - basePath = AddTrailingSlash(basePath); - if (fileSystem.DirExists(basePath + subPath)) - return AddTrailingSlash(basePath + subPath); - else - return basePath; -} - -bool LoadScene(const String&in fileName) -{ - if (fileName.empty) - return false; - - ui.cursor.shape = CS_BUSY; - - // Always load the scene from the filesystem, not from resource paths - if (!fileSystem.FileExists(fileName)) - { - MessageBox("No such scene.\n" + fileName); - return false; - } - - File file(fileName, FILE_READ); - if (!file.open) - { - MessageBox("Could not open file.\n" + fileName); - return false; - } - - // Add the scene's resource path in case it's necessary - String newScenePath = GetPath(fileName); - if (!rememberResourcePath || !sceneResourcePath.StartsWith(newScenePath, false)) - SetResourcePath(newScenePath); - - suppressSceneChanges = true; - sceneModified = false; - revertData = null; - StopSceneUpdate(); - - String extension = GetExtension(fileName); - bool loaded; - if (extension == ".xml") - loaded = editorScene.LoadXML(file); - else if (extension == ".json") - loaded = editorScene.LoadJSON(file); - else - loaded = editorScene.Load(file); - - // Release resources which are not used by the new scene - cache.ReleaseAllResources(false); - - // Always pause the scene, and do updates manually - editorScene.updateEnabled = false; - - UpdateWindowTitle(); - DisableInspectorLock(); - UpdateHierarchyItem(editorScene, true); - CollapseHierarchy(); - ClearEditActions(); - - suppressSceneChanges = false; - - // global variable to mostly bypass adding mru upon importing tempscene - if (!skipMruScene) - UpdateSceneMru(fileName); - - skipMruScene = false; - - ResetCamera(); - CreateGizmo(); - CreateGrid(); - SetActiveViewport(viewports[0]); - - return loaded; -} - -bool SaveScene(const String&in fileName) -{ - if (fileName.empty) - return false; - - ui.cursor.shape = CS_BUSY; - - // Unpause when saving so that the scene will work properly when loaded outside the editor - editorScene.updateEnabled = true; - - MakeBackup(fileName); - File file(fileName, FILE_WRITE); - String extension = GetExtension(fileName); - bool success; - if (extension == ".xml") - success = editorScene.SaveXML(file); - else if (extension == ".json") - success = editorScene.SaveJSON(file); - else - success = editorScene.Save(file); - RemoveBackup(success, fileName); - - // Save all the terrains we've modified - terrainEditor.Save(); - - editorScene.updateEnabled = false; - - if (success) - { - UpdateSceneMru(fileName); - sceneModified = false; - UpdateWindowTitle(); - } - else - MessageBox("Could not save scene successfully!\nSee Urho3D.log for more detail."); - - return success; -} - -bool SaveSceneWithExistingName() -{ - if (editorScene.fileName.empty || editorScene.fileName == TEMP_SCENE_NAME) - return PickFile(); - else - return SaveScene(editorScene.fileName); -} - -Node@ CreateNode(CreateMode mode, bool raycastToMouse = false) -{ - Node@ newNode = null; - if (editNode !is null) - newNode = editNode.CreateChild("", mode); - else - newNode = editorScene.CreateChild("", mode); - newNode.worldPosition = GetNewNodePosition(raycastToMouse); - - // Create an undo action for the create - CreateNodeAction action; - action.Define(newNode); - SaveEditAction(action); - SetSceneModified(); - - FocusNode(newNode); - - return newNode; -} - -void CreateComponent(const String&in componentType) -{ - // If this is the root node, do not allow to create duplicate scene-global components - if (editNode is editorScene && CheckForExistingGlobalComponent(editNode, componentType)) - return; - - // Group for storing undo actions - EditActionGroup group; - - // For now, make a local node's all components local - /// \todo Allow to specify the createmode - for (uint i = 0; i < editNodes.length; ++i) - { - Component@ newComponent = editNodes[i].CreateComponent(componentType, editNodes[i].replicated ? REPLICATED : LOCAL); - if (newComponent !is null) - { - // Some components such as CollisionShape do not create their internal object before the first call to ApplyAttributes() - // to prevent unnecessary initialization with default values. Call now - newComponent.ApplyAttributes(); - - CreateComponentAction action; - action.Define(newComponent); - group.actions.Push(action); - } - } - - SaveEditActionGroup(group); - SetSceneModified(); - - // Although the edit nodes selection are not changed, call to ensure attribute inspector notices new components of the edit nodes - HandleHierarchyListSelectionChange(); -} - -void CreateLoadedComponent(Component@ component) -{ - if (component is null) return; - CreateComponentAction action; - action.Define(component); - SaveEditAction(action); - SetSceneModified(); - FocusComponent(component); -} - -Node@ LoadNode(const String&in fileName, Node@ parent = null, bool raycastToMouse = false) -{ - if (fileName.empty) - return null; - - if (!fileSystem.FileExists(fileName)) - { - MessageBox("No such node file.\n" + fileName); - return null; - } - - File file(fileName, FILE_READ); - if (!file.open) - { - MessageBox("Could not open file.\n" + fileName); - return null; - } - - ui.cursor.shape = CS_BUSY; - - // Before instantiating, add object's resource path if necessary - SetResourcePath(GetPath(fileName), true, true); - - Node@ newNode = InstantiateNodeFromFile(file, GetNewNodePosition(raycastToMouse), Quaternion(), 1, parent, instantiateMode); - if (newNode !is null) - { - FocusNode(newNode); - instantiateFileName = fileName; - } - return newNode; -} - -Node@ InstantiateNodeFromFile(File@ file, const Vector3& position, const Quaternion& rotation, float scaleMod = 1.0f, Node@ parent = null, CreateMode mode = REPLICATED) -{ - if (file is null) - return null; - - Node@ newNode; - uint numSceneComponent = editorScene.numComponents; - - suppressSceneChanges = true; - - String extension = GetExtension(file.name); - if (extension == ".xml") - newNode = editorScene.InstantiateXML(file, position, rotation, mode); - else if (extension == ".json") - newNode = editorScene.InstantiateJSON(file, position, rotation, mode); - else - newNode = editorScene.Instantiate(file, position, rotation, mode); - - suppressSceneChanges = false; - - if (parent !is null) - newNode.parent = parent; - - if (newNode !is null) - { - newNode.scale = newNode.scale * scaleMod; - - AdjustNodePositionByAABB(newNode); - - // Create an undo action for the load - CreateNodeAction action; - action.Define(newNode); - SaveEditAction(action); - SetSceneModified(); - - if (numSceneComponent != editorScene.numComponents) - UpdateHierarchyItem(editorScene); - else - UpdateHierarchyItem(newNode); - } - - return newNode; -} - -void AdjustNodePositionByAABB(Node@ newNode) -{ - if (alignToAABBBottom) - { - Drawable@ drawable = GetFirstDrawable(newNode); - if (drawable !is null) - { - BoundingBox aabb = drawable.worldBoundingBox; - Vector3 aabbBottomCenter(aabb.center.x, aabb.min.y, aabb.center.z); - Vector3 offset = aabbBottomCenter - newNode.worldPosition; - newNode.worldPosition = newNode.worldPosition - offset; - } - } -} - -bool SaveNode(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; - } - - String extension = GetExtension(fileName); - bool success; - if (extension == ".xml") - success = editNode.SaveXML(file); - else if (extension == ".json") - success = editNode.SaveJSON(file); - else - success = editNode.Save(file); - RemoveBackup(success, fileName); - - if (success) - instantiateFileName = fileName; - else - MessageBox("Could not save node successfully!\nSee Urho3D.log for more detail."); - - return success; -} - -void UpdateScene(float timeStep) -{ - if (runUpdate) - editorScene.Update(timeStep); -} - -void StopSceneUpdate() -{ - runUpdate = false; - audio.Stop(); - toolBarDirty = true; - - // If scene should revert on update stop, load saved data now - if (revertOnPause && revertData !is null) - { - suppressSceneChanges = true; - editorScene.Clear(); - editorScene.LoadXML(revertData.root); - CreateGrid(); - UpdateHierarchyItem(editorScene, true); - ClearEditActions(); - suppressSceneChanges = false; - } - - revertData = null; -} - -void StartSceneUpdate() -{ - runUpdate = true; - // Run audio playback only when scene is updating, so that audio components' time-dependent attributes stay constant when - // paused (similar to physics) - audio.Play(); - toolBarDirty = true; - - // Save scene data for reverting if enabled - if (revertOnPause) - { - revertData = XMLFile(); - XMLElement root = revertData.CreateRoot("scene"); - editorScene.SaveXML(root); - } - else - revertData = null; -} - -bool ToggleSceneUpdate() -{ - if (!runUpdate) - StartSceneUpdate(); - else - StopSceneUpdate(); - return true; -} - -bool ShowLayerMover() -{ - if (ui.focusElement is null) - return ShowLayerEditor(); - else - return false; -} - -void SetSceneModified() -{ - if (!sceneModified) - { - sceneModified = true; - UpdateWindowTitle(); - } -} - -bool SceneDelete() -{ - ui.cursor.shape = CS_BUSY; - - BeginSelectionModify(); - - // Clear the selection now to prevent repopulation of selectedNodes and selectedComponents combo - hierarchyList.ClearSelection(); - - // Group for storing undo actions - EditActionGroup group; - - // Remove nodes - for (uint i = 0; i < selectedNodes.length; ++i) - { - Node@ node = selectedNodes[i]; - if (node.parent is null || node.scene is null) - continue; // Root or already deleted - - uint nodeIndex = GetListIndex(node); - - // Create undo action - DeleteNodeAction action; - action.Define(node); - group.actions.Push(action); - - node.Remove(); - SetSceneModified(); - - // If deleting only one node, select the next item in the same index - if (selectedNodes.length == 1 && selectedComponents.empty) - hierarchyList.selection = nodeIndex; - } - - // Then remove components, if they still remain - for (uint i = 0; i < selectedComponents.length; ++i) - { - Component@ component = selectedComponents[i]; - Node@ node = component.node; - if (node is null) - continue; // Already deleted - - uint index = GetComponentListIndex(component); - uint nodeIndex = GetListIndex(node); - if (index == NO_ITEM || nodeIndex == NO_ITEM) - continue; - - // Do not allow to remove the Octree, DebugRenderer or MaterialCache2D or DrawableProxy2D from the root node - if (node is editorScene && (component.typeName == "Octree" || component.typeName == "DebugRenderer" || - component.typeName == "MaterialCache2D" || component.typeName == "DrawableProxy2D")) - continue; - - // Create undo action - DeleteComponentAction action; - action.Define(component); - group.actions.Push(action); - - node.RemoveComponent(component); - SetSceneModified(); - - // If deleting only one component, select the next item in the same index - if (selectedComponents.length == 1 && selectedNodes.empty) - hierarchyList.selection = index; - } - - SaveEditActionGroup(group); - - EndSelectionModify(); - return true; -} - -bool SceneCut() -{ - return SceneCopy() && SceneDelete(); -} - -bool SceneCopy() -{ - ui.cursor.shape = CS_BUSY; - - sceneCopyBuffer.Clear(); - - // Copy components - if (!selectedComponents.empty) - { - for (uint i = 0; i < selectedComponents.length; ++i) - { - XMLFile@ xml = XMLFile(); - XMLElement rootElem = xml.CreateRoot("component"); - selectedComponents[i].SaveXML(rootElem); - rootElem.SetBool("local", !selectedComponents[i].replicated); - sceneCopyBuffer.Push(xml); - } - } - // Copy nodes - else - { - for (uint i = 0; i < selectedNodes.length; ++i) - { - // Skip the root scene node as it cannot be copied - if (selectedNodes[i] is editorScene) - continue; - - XMLFile@ xml = XMLFile(); - XMLElement rootElem = xml.CreateRoot("node"); - selectedNodes[i].SaveXML(rootElem); - rootElem.SetBool("local", !selectedNodes[i].replicated); - sceneCopyBuffer.Push(xml); - } - } - - return true; -} - -bool ScenePaste(bool pasteRoot = false, bool duplication = false) -{ - ui.cursor.shape = CS_BUSY; - - // Group for storing undo actions - EditActionGroup group; - - for (uint i = 0; i < sceneCopyBuffer.length; ++i) - { - XMLElement rootElem = sceneCopyBuffer[i].root; - String mode = rootElem.name; - if (mode == "component" && editNode !is null) - { - // If this is the root node, do not allow to create duplicate scene-global components - if (editNode is editorScene && CheckForExistingGlobalComponent(editNode, rootElem.GetAttribute("type"))) - return false; - - // If copied component was local, make the new local too - Component@ newComponent = editNode.CreateComponent(rootElem.GetAttribute("type"), rootElem.GetBool("local") ? LOCAL : - REPLICATED); - if (newComponent is null) - return false; - - newComponent.LoadXML(rootElem); - newComponent.ApplyAttributes(); - - // Create an undo action - CreateComponentAction action; - action.Define(newComponent); - group.actions.Push(action); - } - else if (mode == "node") - { - // If copied node was local, make the new local too - Node@ newNode; - // Are we pasting into the root node? - if (pasteRoot) - newNode = editorScene.CreateChild("", rootElem.GetBool("local") ? LOCAL : REPLICATED); - else - { - // If we are duplicating or have the original node selected, paste into the selected nodes parent - if (duplication || editNode is null || editNode.id == rootElem.GetU32("id")) - { - if (editNode !is null && editNode.parent !is null) - newNode = editNode.parent.CreateChild("", rootElem.GetBool("local") ? LOCAL : REPLICATED); - else - newNode = editorScene.CreateChild("", rootElem.GetBool("local") ? LOCAL : REPLICATED); - } - // If we aren't duplicating, paste into the selected node - else - { - newNode = editNode.CreateChild("", rootElem.GetBool("local") ? LOCAL : REPLICATED); - } - } - - newNode.LoadXML(rootElem); - - // Create an undo action - CreateNodeAction action; - action.Define(newNode); - group.actions.Push(action); - } - } - - SaveEditActionGroup(group); - SetSceneModified(); - return true; -} - -bool SceneDuplicate() -{ - Array copy = sceneCopyBuffer; - - if (!SceneCopy()) - { - sceneCopyBuffer = copy; - return false; - } - if (!ScenePaste(false, true)) - { - sceneCopyBuffer = copy; - return false; - } - - sceneCopyBuffer = copy; - return true; -} - -bool SceneUnparent() -{ - if (!CheckHierarchyWindowFocus() || !selectedComponents.empty || selectedNodes.empty) - return false; - - ui.cursor.shape = CS_BUSY; - - // Group for storing undo actions - EditActionGroup group; - - // Parent selected nodes to root - Array changedNodes; - for (uint i = 0; i < selectedNodes.length; ++i) - { - Node@ sourceNode = selectedNodes[i]; - if (sourceNode.parent is null || sourceNode.parent is editorScene) - continue; // Root or already parented to root - - // Perform the reparenting, continue loop even if action fails - ReparentNodeAction action; - action.Define(sourceNode, editorScene); - group.actions.Push(action); - - SceneChangeParent(sourceNode, editorScene, false); - changedNodes.Push(sourceNode); - } - - // Reselect the changed nodes at their new position in the list - for (uint i = 0; i < changedNodes.length; ++i) - hierarchyList.AddSelection(GetListIndex(changedNodes[i])); - - SaveEditActionGroup(group); - SetSceneModified(); - - return true; -} - -bool NodesParentToLastSelected() -{ - if (lastSelectedNode.Get() is null) - return false; - - if (!CheckHierarchyWindowFocus() || !selectedComponents.empty || selectedNodes.empty) - return false; - - ui.cursor.shape = CS_BUSY; - - // Group for storing undo actions - EditActionGroup group; - - // Parent selected nodes to root - Array changedNodes; - - // Find new parent node it selected last - Node@ lastNode = lastSelectedNode.Get(); //GetListNode(hierarchyList.selection); - - for (uint i = 0; i < selectedNodes.length; ++i) - { - - Node@ sourceNode = selectedNodes[i]; - if ( sourceNode.id == lastNode.id) - continue; // Skip last node it is parent - - if (sourceNode.parent.id == lastNode.id) - continue; // Root or already parented to root - - // Perform the reparenting, continue loop even if action fails - ReparentNodeAction action; - action.Define(sourceNode, lastNode); - group.actions.Push(action); - - SceneChangeParent(sourceNode, lastNode, false); - changedNodes.Push(sourceNode); - } - - // Reselect the changed nodes at their new position in the list - for (uint i = 0; i < changedNodes.length; ++i) - hierarchyList.AddSelection(GetListIndex(changedNodes[i])); - - SaveEditActionGroup(group); - SetSceneModified(); - - return true; -} - -bool SceneSmartDuplicateNode() -{ - const float minOffset = 0.1; - - if (!CheckHierarchyWindowFocus() || !selectedComponents.empty - || selectedNodes.empty || lastSelectedNode.Get() is null) - return false; - - - Node@ node = lastSelectedNode.Get(); - Node@ parent = node.parent; - Vector3 offset = Vector3(1,0,0); // default offset - - if (parent is editorScene) // if parent of selected node is Scene make empty parent for it and place in same position; - { - parent = CreateNode(LOCAL); - SceneChangeParent(parent, editorScene, false); - parent.worldPosition = node.worldPosition; - parent.name = node.name + "Group"; - node.name = parent.name + "Instance" + String(parent.numChildren); - SceneChangeParent(node, parent, false); - parent = node.parent; - SelectNode(node, false); - } - - Vector3 size; - BoundingBox bb; - - // get bb for offset - Drawable@ drawable = GetFirstDrawable(node); - if (drawable !is null) - { - bb = drawable.boundingBox; - size = bb.size * drawable.node.worldScale; - offset = Vector3(size.x, 0, 0); - } - - // make offset on axis that select user by mouse - if (gizmoAxisX.selected) - { - if (size.x < minOffset) size.x = minOffset; - offset = node.worldRotation * Vector3(size.x,0,0); - } - else if (gizmoAxisY.selected) - { - if (size.y < minOffset) size.y = minOffset; - offset = node.worldRotation * Vector3(0,size.y,0); - } - else if (gizmoAxisZ.selected) - { - if (size.z < minOffset) size.z = minOffset; - offset = node.worldRotation * Vector3(0,0,size.z); - } - else - offset = lastOffsetForSmartDuplicate; - - Vector3 lastInstancePosition = node.worldPosition; - - SelectNode(node, false); - SceneDuplicate(); - Node@ newInstance = parent.children[parent.numChildren-1]; - SelectNode(newInstance, false); - newInstance.worldPosition = lastInstancePosition; - newInstance.Translate(offset, TransformSpace::World); - newInstance.name = parent.name + "Instance" + String(parent.numChildren-1); - - lastOffsetForSmartDuplicate = offset; - UpdateNodeAttributes(); - return true; -} - -bool ViewCloser() -{ - if (selectedNodes.length > 0 || selectedNodes.length > 0) - LocateNodesAndComponents(selectedNodes, selectedComponents); - - return true; -} - - -bool SceneToggleEnable() -{ - if (!CheckHierarchyWindowFocus()) - return false; - - ui.cursor.shape = CS_BUSY; - - EditActionGroup group; - - // Toggle enabled state of nodes recursively - for (uint i = 0; i < selectedNodes.length; ++i) - { - // Do not attempt to disable the Scene - if (selectedNodes[i].typeName == "Node") - { - bool oldEnabled = selectedNodes[i].enabled; - selectedNodes[i].SetEnabledRecursive(!oldEnabled); - - // Create undo action - ToggleNodeEnabledAction action; - action.Define(selectedNodes[i], oldEnabled); - group.actions.Push(action); - } - } - for (uint i = 0; i < selectedComponents.length; ++i) - { - // Some components purposefully do not expose the Enabled attribute, and it does not affect them in any way - // (Octree, PhysicsWorld). Check that the first attribute is in fact called "Is Enabled" - if (selectedComponents[i].numAttributes > 0 && selectedComponents[i].attributeInfos[0].name == "Is Enabled") - { - bool oldEnabled = selectedComponents[i].enabled; - selectedComponents[i].enabled = !oldEnabled; - - // Create undo action - EditAttributeAction action; - action.Define(selectedComponents[i], 0, Variant(oldEnabled)); - group.actions.Push(action); - } - } - - SaveEditActionGroup(group); - SetSceneModified(); - - return true; -} - -bool SceneEnableAllNodes() -{ - if (!CheckHierarchyWindowFocus()) - return false; - - ui.cursor.shape = CS_BUSY; - - EditActionGroup group; - - // Toggle enabled state of nodes recursively - Array allNodes; - allNodes = editorScene.GetChildren(true); - - for (uint i = 0; i < allNodes.length; ++i) - { - // Do not attempt to disable the Scene - if (allNodes[i].typeName == "Node") - { - bool oldEnabled = allNodes[i].enabled; - if (oldEnabled == false) - allNodes[i].SetEnabledRecursive(true); - - // Create undo action - ToggleNodeEnabledAction action; - action.Define(allNodes[i], oldEnabled); - group.actions.Push(action); - } - } - - Array allComponents; - allComponents = editorScene.GetComponents(); - - for (uint i = 0; i < allComponents.length; ++i) - { - // Some components purposefully do not expose the Enabled attribute, and it does not affect them in any way - // (Octree, PhysicsWorld). Check that the first attribute is in fact called "Is Enabled" - if (allComponents[i].numAttributes > 0 && allComponents[i].attributeInfos[0].name == "Is Enabled") - { - bool oldEnabled = allComponents[i].enabled; - allComponents[i].enabled = true; - - // Create undo action - EditAttributeAction action; - action.Define(allComponents[i], 0, Variant(oldEnabled)); - group.actions.Push(action); - } - } - - SaveEditActionGroup(group); - SetSceneModified(); - - return true; -} - -bool SceneChangeParent(Node@ sourceNode, Node@ targetNode, bool createUndoAction = true) -{ - // Create undo action if requested - if (createUndoAction) - { - ReparentNodeAction action; - action.Define(sourceNode, targetNode); - SaveEditAction(action); - } - - sourceNode.parent = targetNode; - SetSceneModified(); - - // Return true if success - if (sourceNode.parent is targetNode) - { - UpdateNodeAttributes(); // Parent change may have changed local transform - return true; - } - else - return false; -} - -bool SceneChangeParent(Node@ sourceNode, Array sourceNodes, Node@ targetNode, bool createUndoAction = true) -{ - // Create undo action if requested - if (createUndoAction) - { - ReparentNodeAction action; - action.Define(sourceNodes, targetNode); - SaveEditAction(action); - } - - for (uint i = 0; i < sourceNodes.length; ++i) - { - Node@ node = sourceNodes[i]; - node.parent = targetNode; - } - SetSceneModified(); - - // Return true if success - if (sourceNode.parent is targetNode) - { - UpdateNodeAttributes(); // Parent change may have changed local transform - return true; - } - else - return false; -} - -bool SceneReorder(Node@ sourceNode, Node@ targetNode) -{ - if (sourceNode is null || targetNode is null || sourceNode.parent is null || sourceNode.parent !is targetNode.parent) - return false; - if (sourceNode is targetNode) - return true; // No-op - - Node@ parent = sourceNode.parent; - uint destIndex = SceneFindChildIndex(parent, targetNode); - - ReorderNodeAction action; - action.Define(sourceNode, destIndex); - SaveEditAction(action); - PerformReorder(parent, sourceNode, destIndex); - return true; -} - -bool SceneReorder(Component@ sourceComponent, Component@ targetComponent) -{ - if (sourceComponent is null || targetComponent is null || sourceComponent.node !is targetComponent.node) - return false; - if (sourceComponent is targetComponent) - return true; // No-op - - Node@ node = sourceComponent.node; - uint destIndex = SceneFindComponentIndex(node, targetComponent); - - ReorderComponentAction action; - action.Define(sourceComponent, destIndex); - SaveEditAction(action); - PerformReorder(node, sourceComponent, destIndex); - return true; -} - -void PerformReorder(Node@ parent, Node@ child, uint destIndex) -{ - suppressSceneChanges = true; - - // Removal from scene zeroes the ID. Be prepared to restore it - uint oldId = child.id; - parent.RemoveChild(child); - child.id = oldId; - parent.AddChild(child, destIndex); - UpdateHierarchyItem(parent); // Force update to make sure the order is current - SetSceneModified(); - - suppressSceneChanges = false; -} - -void PerformReorder(Node@ node, Component@ component, uint destIndex) -{ - suppressSceneChanges = true; - - node.ReorderComponent(component, destIndex); - UpdateHierarchyItem(node); // Force update to make sure the order is current - SetSceneModified(); - - suppressSceneChanges = false; -} - -uint SceneFindChildIndex(Node@ parent, Node@ child) -{ - for (uint i = 0; i < parent.numChildren; ++i) - { - if (parent.children[i] is child) - return i; - } - - return -1; -} - -uint SceneFindComponentIndex(Node@ node, Component@ component) -{ - for (uint i = 0; i < node.numComponents; ++i) - { - if (node.components[i] is component) - return i; - } - - return -1; -} - -bool SceneResetPosition() -{ - if (editNode !is null) - { - Transform oldTransform; - oldTransform.Define(editNode); - - editNode.position = Vector3(0.0, 0.0, 0.0); - - // Create undo action - EditNodeTransformAction action; - action.Define(editNode, oldTransform); - SaveEditAction(action); - SetSceneModified(); - - UpdateNodeAttributes(); - return true; - } - else - return false; -} - -bool SceneResetRotation() -{ - if (editNode !is null) - { - Transform oldTransform; - oldTransform.Define(editNode); - - editNode.rotation = Quaternion(); - - // Create undo action - EditNodeTransformAction action; - action.Define(editNode, oldTransform); - SaveEditAction(action); - SetSceneModified(); - - UpdateNodeAttributes(); - return true; - } - else - return false; -} - -bool SceneResetScale() -{ - if (editNode !is null) - { - Transform oldTransform; - oldTransform.Define(editNode); - - editNode.scale = Vector3(1.0, 1.0, 1.0); - - // Create undo action - EditNodeTransformAction action; - action.Define(editNode, oldTransform); - SaveEditAction(action); - SetSceneModified(); - - UpdateNodeAttributes(); - return true; - } - else - return false; -} - -bool SceneResetTransform() -{ - if (editNode !is null) - { - Transform oldTransform; - oldTransform.Define(editNode); - - editNode.position = Vector3(0.0, 0.0, 0.0); - editNode.rotation = Quaternion(); - editNode.scale = Vector3(1.0, 1.0, 1.0); - - // Create undo action - EditNodeTransformAction action; - action.Define(editNode, oldTransform); - SaveEditAction(action); - SetSceneModified(); - - UpdateNodeAttributes(); - return true; - } - else - return false; -} - -bool SceneSelectAll() -{ - BeginSelectionModify(); - Array rootLevelNodes = editorScene.GetChildren(); - Array indices; - for (uint i = 0; i < rootLevelNodes.length; ++i) - indices.Push(GetListIndex(rootLevelNodes[i])); - hierarchyList.SetSelections(indices); - EndSelectionModify(); - - return true; -} - -bool SceneResetToDefault() -{ - ui.cursor.shape = CS_BUSY; - - // Group for storing undo actions - EditActionGroup group; - - // Reset selected component to their default - if (!selectedComponents.empty) - { - for (uint i = 0; i < selectedComponents.length; ++i) - { - Component@ component = selectedComponents[i]; - - ResetAttributesAction action; - action.Define(component); - group.actions.Push(action); - - component.ResetToDefault(); - component.ApplyAttributes(); - for (uint j = 0; j < component.numAttributes; ++j) - PostEditAttribute(component, j); - } - } - // OR reset selected nodes to their default - else - { - for (uint i = 0; i < selectedNodes.length; ++i) - { - Node@ node = selectedNodes[i]; - - ResetAttributesAction action; - action.Define(node); - group.actions.Push(action); - - node.ResetToDefault(); - node.ApplyAttributes(); - for (uint j = 0; j < node.numAttributes; ++j) - PostEditAttribute(node, j); - } - } - - SaveEditActionGroup(group); - SetSceneModified(); - attributesFullDirty = true; - - return true; -} - -bool SceneRebuildNavigation() -{ - ui.cursor.shape = CS_BUSY; - - Array@ navMeshes = editorScene.GetComponents("NavigationMesh", true); - if (navMeshes.empty) - { - @navMeshes = editorScene.GetComponents("DynamicNavigationMesh", true); - if (navMeshes.empty) - { - MessageBox("No NavigationMesh components in the scene, nothing to rebuild."); - return false; - } - } - - bool success = true; - for (uint i = 0; i < navMeshes.length; ++i) - { - NavigationMesh@ navMesh = navMeshes[i]; - if (!navMesh.Build()) - success = false; - } - - return success; -} - -bool SceneRenderZoneCubemaps() -{ - bool success = false; - Array capturedThisCall; - bool alreadyCapturing = activeCubeCapture.length > 0; // May have managed to quickly queue up a second round of zones to render cubemaps for - - for (uint i = 0; i < selectedNodes.length; ++i) - { - Array@ zones = selectedNodes[i].GetComponents("Zone", true); - for (uint z = 0; z < zones.length; ++z) - { - Zone@ zone = cast(zones[z]); - if (zone !is null) - { - activeCubeCapture.Push(EditorCubeCapture(zone)); - capturedThisCall.Push(zone); - } - } - } - - for (uint i = 0; i < selectedComponents.length; ++i) - { - Zone@ zone = cast(selectedComponents[i]); - if (zone !is null) - { - if (capturedThisCall.FindByRef(zone) < 0) - { - activeCubeCapture.Push(EditorCubeCapture(zone)); - capturedThisCall.Push(zone); - } - } - } - - // Start rendering cubemaps if there are any to render and the queue isn't already running - if (activeCubeCapture.length > 0 && !alreadyCapturing) - activeCubeCapture[0].Start(); - - if (capturedThisCall.length <= 0) - { - MessageBox("No zones selected to render cubemaps for/"); - } - return capturedThisCall.length > 0; -} - -bool SceneAddChildrenStaticModelGroup() -{ - StaticModelGroup@ smg = cast(editComponents.length > 0 ? editComponents[0] : null); - if (smg is null && editNode !is null) - smg = editNode.GetComponent("StaticModelGroup"); - - if (smg is null) - { - MessageBox("Must have a StaticModelGroup component selected."); - return false; - } - - uint attrIndex = GetAttributeIndex(smg, "Instance Nodes"); - Variant oldValue = smg.attributes[attrIndex]; - - Array children = smg.node.GetChildren(true); - for (uint i = 0; i < children.length; ++i) - smg.AddInstanceNode(children[i]); - - EditAttributeAction action; - action.Define(smg, attrIndex, oldValue); - SaveEditAction(action); - SetSceneModified(); - FocusComponent(smg); - - return true; -} - -bool SceneSetChildrenSplinePath(bool makeCycle) -{ - SplinePath@ sp = cast(editComponents.length > 0 ? editComponents[0] : null); - if (sp is null && editNode !is null) - sp = editNode.GetComponent("SplinePath"); - - if (sp is null) - { - MessageBox("Must have a SplinePath component selected."); - return false; - } - - uint attrIndex = GetAttributeIndex(sp, "Control Points"); - Variant oldValue = sp.attributes[attrIndex]; - - Array children = sp.node.GetChildren(true); - if (children.length >= 2) - { - sp.ClearControlPoints(); - for (uint i = 0; i < children.length; ++i) - sp.AddControlPoint(children[i]); - } - else - { - MessageBox("You must have a minimum two children Nodes in selected Node."); - return false; - } - - if (makeCycle) - sp.AddControlPoint(children[0]); - - EditAttributeAction action; - action.Define(sp, attrIndex, oldValue); - SaveEditAction(action); - SetSceneModified(); - FocusComponent(sp); - - return true; -} - -void AssignMaterial(StaticModel@ model, String materialPath) -{ - Material@ material = cache.GetResource("Material", materialPath); - if (material is null) - return; - - ResourceRefList materials = model.GetAttribute("Material").GetResourceRefList(); - Array oldMaterials; - for(uint i = 0; i < materials.length; ++i) - oldMaterials.Push(materials.names[i]); - - model.material = material; - - AssignMaterialAction action; - action.Define(model, oldMaterials, material); - SaveEditAction(action); - SetSceneModified(); - FocusComponent(model); -} - -void UpdateSceneMru(String filename) -{ - while (uiRecentScenes.Find(filename) > -1) - uiRecentScenes.Erase(uiRecentScenes.Find(filename)); - - uiRecentScenes.Insert(0, filename); - - for (uint i = uiRecentScenes.length - 1; i >= maxRecentSceneCount; i--) - uiRecentScenes.Erase(i); - - PopulateMruScenes(); -} - -Drawable@ GetFirstDrawable(Node@ node) -{ - Array nodes = node.GetChildren(true); - nodes.Insert(0, node); - - for (uint i = 0; i < nodes.length; ++i) - { - Array components = nodes[i].GetComponents(); - for (uint j = 0; j < components.length; ++j) - { - Drawable@ drawable = cast(components[j]); - if (drawable !is null) - return drawable; - } - } - - return null; -} - -void AssignModel(StaticModel@ assignee, String modelPath) -{ - Model@ model = cache.GetResource("Model", modelPath); - if (model is null) - return; - - Model@ oldModel = assignee.model; - assignee.model = model; - - AssignModelAction action; - action.Define(assignee, oldModel, model); - SaveEditAction(action); - SetSceneModified(); - FocusComponent(assignee); -} - -void CreateModelWithStaticModel(String filepath, Node@ parent) -{ - if (parent is null) - return; - /// \todo should be able to specify the createmode - if (parent is editorScene) - parent = CreateNode(REPLICATED); - - Model@ model = cache.GetResource("Model", filepath); - if (model is null) - return; - - StaticModel@ staticModel = parent.GetOrCreateComponent("StaticModel"); - staticModel.model = model; - if (applyMaterialList) - staticModel.ApplyMaterialList(); - CreateLoadedComponent(staticModel); -} - -void CreateModelWithAnimatedModel(String filepath, Node@ parent) -{ - if (parent is null) - return; - /// \todo should be able to specify the createmode - if (parent is editorScene) - parent = CreateNode(REPLICATED); - - Model@ model = cache.GetResource("Model", filepath); - if (model is null) - return; - - AnimatedModel@ animatedModel = parent.GetOrCreateComponent("AnimatedModel"); - animatedModel.model = model; - if (applyMaterialList) - animatedModel.ApplyMaterialList(); - CreateLoadedComponent(animatedModel); -} - -bool ColorWheelSetupBehaviorForColoring() -{ - Menu@ menu = GetEventSender(); - if (menu is null) - return false; - - coloringPropertyName = menu.name; - - if (coloringPropertyName == "menuCancel") return false; - - if (coloringComponent.typeName == "Light") - { - Light@ light = cast(coloringComponent); - if (light !is null) - { - if (coloringPropertyName == "menuLightColor") - { - coloringOldColor = light.color; - ShowColorWheelWithColor(coloringOldColor); - } - else if (coloringPropertyName == "menuSpecularIntensity") - { - // ColorWheel have only 0-1 range output of V-value(BW), and for huge-range values we divide in and multiply out - float scaledSpecular = light.specularIntensity * 0.1f; - coloringOldScalar = scaledSpecular; - ShowColorWheelWithColor(Color(scaledSpecular,scaledSpecular,scaledSpecular)); - - } - else if (coloringPropertyName == "menuBrightnessMultiplier") - { - float scaledBrightness = light.brightness * 0.1f; - coloringOldScalar = scaledBrightness; - ShowColorWheelWithColor(Color(scaledBrightness,scaledBrightness,scaledBrightness)); - } - } - } - else if (coloringComponent.typeName == "StaticModel") - { - StaticModel@ model = cast(coloringComponent); - if (model !is null) - { - Material@ mat = model.materials[0]; - if (mat !is null) - { - if (coloringPropertyName == "menuDiffuseColor") - { - Variant oldValue = mat.shaderParameters["MatDiffColor"]; - Array values = oldValue.ToString().Split(' '); - coloringOldColor = Color(values[0].ToFloat(),values[1].ToFloat(),values[2].ToFloat(),values[3].ToFloat()); //RGBA - ShowColorWheelWithColor(coloringOldColor); - } - else if (coloringPropertyName == "menuSpecularColor") - { - Variant oldValue = mat.shaderParameters["MatSpecColor"]; - Array values = oldValue.ToString().Split(' '); - coloringOldColor = Color(values[0].ToFloat(),values[1].ToFloat(),values[2].ToFloat()); - coloringOldScalar = values[3].ToFloat(); - ShowColorWheelWithColor(Color(coloringOldColor.r, coloringOldColor.g, coloringOldColor.b, coloringOldScalar/128.0f)); //RGB + shine - } - else if (coloringPropertyName == "menuEmissiveColor") - { - Variant oldValue = mat.shaderParameters["MatEmissiveColor"]; - Array values = oldValue.ToString().Split(' '); - coloringOldColor = Color(values[0].ToFloat(),values[1].ToFloat(),values[2].ToFloat()); // RGB - - - ShowColorWheelWithColor(coloringOldColor); - } - else if (coloringPropertyName == "menuEnvironmentMapColor") - { - Variant oldValue = mat.shaderParameters["MatEnvMapColor"]; - Array values = oldValue.ToString().Split(' '); - coloringOldColor = Color(values[0].ToFloat(),values[1].ToFloat(),values[2].ToFloat()); //RGB - - ShowColorWheelWithColor(coloringOldColor); - } - } - } - } - else if (coloringComponent.typeName == "Zone") - { - Zone@ zone = cast(coloringComponent); - if (zone !is null) - { - if (coloringPropertyName == "menuAmbientColor") - { - coloringOldColor = zone.ambientColor; - } - else if (coloringPropertyName == "menuFogColor") - { - coloringOldColor = zone.fogColor; - } - - ShowColorWheelWithColor(coloringOldColor); - } - } - else if (coloringComponent.typeName == "Text3D") - { - Text3D@ txt = cast(coloringComponent); - if (txt !is null) - { - if (coloringPropertyName == "c" || coloringPropertyName == "tl") - coloringOldColor = txt.colors[C_TOPLEFT]; - else if (coloringPropertyName == "tr") - coloringOldColor = txt.colors[C_TOPRIGHT]; - else if (coloringPropertyName == "bl") - coloringOldColor = txt.colors[C_BOTTOMLEFT]; - else if (coloringPropertyName == "br") - coloringOldColor = txt.colors[C_BOTTOMRIGHT]; - - ShowColorWheelWithColor(coloringOldColor); - } - } - return true; -} - +/// Urho3D editor scene handling + +#include "Editor/Scripts/EditorHierarchyWindow.as" +#include "Editor/Scripts/EditorInspectorWindow.as" +#include "Editor/Scripts/EditorCubeCapture.as" + +const int PICK_GEOMETRIES = 0; +const int PICK_LIGHTS = 1; +const int PICK_ZONES = 2; +const int PICK_RIGIDBODIES = 3; +const int PICK_UI_ELEMENTS = 4; +const int MAX_PICK_MODES = 5; +const int MAX_UNDOSTACK_SIZE = 256; + +Scene@ editorScene; + +String instantiateFileName; +CreateMode instantiateMode = REPLICATED; +bool sceneModified = false; +bool runUpdate = false; + +Array selectedNodes; +Array selectedComponents; +Node@ editNode; +Array editNodes; +Array editComponents; +uint numEditableComponentsPerNode = 1; + +Array sceneCopyBuffer; + +bool suppressSceneChanges = false; +bool inSelectionModify = false; +bool skipMruScene = false; + +Array undoStack; +uint undoStackPos = 0; + +bool revertOnPause = false; +XMLFile@ revertData; + +Vector3 lastOffsetForSmartDuplicate; + +void ClearSceneSelection() +{ + selectedNodes.Clear(); + selectedComponents.Clear(); + editNode = null; + editNodes.Clear(); + editComponents.Clear(); + numEditableComponentsPerNode = 1; + + HideGizmo(); +} + +void CreateScene() +{ + // Create a scene only once here + editorScene = Scene(); + + // Allow access to the scene from the console + script.defaultScene = editorScene; + + // Always pause the scene, and do updates manually + editorScene.updateEnabled = false; +} + +bool ResetScene() +{ + ui.cursor.shape = CS_BUSY; + + if (messageBoxCallback is null && sceneModified) + { + MessageBox@ messageBox = MessageBox("Scene has been modified.\nContinue to reset?", "Warning"); + if (messageBox.window !is null) + { + Button@ cancelButton = messageBox.window.GetChild("CancelButton", true); + cancelButton.visible = true; + cancelButton.focus = true; + SubscribeToEvent(messageBox, "MessageACK", "HandleMessageAcknowledgement"); + messageBoxCallback = @ResetScene; + return false; + } + } + else + messageBoxCallback = null; + + suppressSceneChanges = true; + + // Create a scene with default values, these will be overridden when loading scenes + editorScene.Clear(); + editorScene.CreateComponent("Octree"); + editorScene.CreateComponent("DebugRenderer"); + + // Release resources that became unused after the scene clear + cache.ReleaseAllResources(false); + + sceneModified = false; + revertData = null; + StopSceneUpdate(); + + UpdateWindowTitle(); + DisableInspectorLock(); + UpdateHierarchyItem(editorScene, true); + ClearEditActions(); + + suppressSceneChanges = false; + + ResetCamera(); + CreateGizmo(); + CreateGrid(); + SetActiveViewport(viewports[0]); + + return true; +} + +void SetResourcePath(String newPath, bool usePreferredDir = true, bool additive = false) +{ + if (newPath.empty) + return; + if (!IsAbsolutePath(newPath)) + newPath = fileSystem.currentDir + newPath; + + if (usePreferredDir) + newPath = AddTrailingSlash(cache.GetPreferredResourceDir(newPath)); + else + newPath = AddTrailingSlash(newPath); + + if (newPath == sceneResourcePath) + return; + + // Remove the old scene resource path if any. However make sure that the default data paths do not get removed + if (!additive) + { + cache.ReleaseAllResources(false); + renderer.ReloadShaders(); + + String check = AddTrailingSlash(sceneResourcePath); + bool isDefaultResourcePath = check.Compare(fileSystem.programDir + "Data/", false) == 0 || + check.Compare(fileSystem.programDir + "CoreData/", false) == 0; + + if (!sceneResourcePath.empty && !isDefaultResourcePath) + cache.RemoveResourceDir(sceneResourcePath); + } + else + { + // If additive (path of a loaded prefab) check that the new path isn't already part of an old path + Array@ resourceDirs = cache.resourceDirs; + + for (uint i = 0; i < resourceDirs.length; ++i) + { + if (newPath.StartsWith(resourceDirs[i], false)) + return; + } + } + + // Add resource path as first priority so that it takes precedence over the default data paths + cache.AddResourceDir(newPath, 0); + RebuildResourceDatabase(); + + if (!additive) + { + sceneResourcePath = newPath; + uiScenePath = GetResourceSubPath(newPath, "Scenes"); + uiElementPath = GetResourceSubPath(newPath, "UI"); + uiNodePath = GetResourceSubPath(newPath, "Objects"); + uiScriptPath = GetResourceSubPath(newPath, "Scripts"); + uiParticlePath = GetResourceSubPath(newPath, "Particle"); + } +} + +String GetResourceSubPath(String basePath, const String&in subPath) +{ + basePath = AddTrailingSlash(basePath); + if (fileSystem.DirExists(basePath + subPath)) + return AddTrailingSlash(basePath + subPath); + else + return basePath; +} + +bool LoadScene(const String&in fileName) +{ + if (fileName.empty) + return false; + + ui.cursor.shape = CS_BUSY; + + // Always load the scene from the filesystem, not from resource paths + if (!fileSystem.FileExists(fileName)) + { + MessageBox("No such scene.\n" + fileName); + return false; + } + + File file(fileName, FILE_READ); + if (!file.open) + { + MessageBox("Could not open file.\n" + fileName); + return false; + } + + // Add the scene's resource path in case it's necessary + String newScenePath = GetPath(fileName); + if (!rememberResourcePath || !sceneResourcePath.StartsWith(newScenePath, false)) + SetResourcePath(newScenePath); + + suppressSceneChanges = true; + sceneModified = false; + revertData = null; + StopSceneUpdate(); + + String extension = GetExtension(fileName); + bool loaded; + if (extension == ".xml") + loaded = editorScene.LoadXML(file); + else if (extension == ".json") + loaded = editorScene.LoadJSON(file); + else + loaded = editorScene.Load(file); + + // Release resources which are not used by the new scene + cache.ReleaseAllResources(false); + + // Always pause the scene, and do updates manually + editorScene.updateEnabled = false; + + UpdateWindowTitle(); + DisableInspectorLock(); + UpdateHierarchyItem(editorScene, true); + CollapseHierarchy(); + ClearEditActions(); + + suppressSceneChanges = false; + + // global variable to mostly bypass adding mru upon importing tempscene + if (!skipMruScene) + UpdateSceneMru(fileName); + + skipMruScene = false; + + ResetCamera(); + CreateGizmo(); + CreateGrid(); + SetActiveViewport(viewports[0]); + + return loaded; +} + +bool SaveScene(const String&in fileName) +{ + if (fileName.empty) + return false; + + ui.cursor.shape = CS_BUSY; + + // Unpause when saving so that the scene will work properly when loaded outside the editor + editorScene.updateEnabled = true; + + MakeBackup(fileName); + File file(fileName, FILE_WRITE); + String extension = GetExtension(fileName); + bool success; + if (extension == ".xml") + success = editorScene.SaveXML(file); + else if (extension == ".json") + success = editorScene.SaveJSON(file); + else + success = editorScene.Save(file); + RemoveBackup(success, fileName); + + // Save all the terrains we've modified + terrainEditor.Save(); + + editorScene.updateEnabled = false; + + if (success) + { + UpdateSceneMru(fileName); + sceneModified = false; + UpdateWindowTitle(); + } + else + MessageBox("Could not save scene successfully!\nSee Urho3D.log for more detail."); + + return success; +} + +bool SaveSceneWithExistingName() +{ + if (editorScene.fileName.empty || editorScene.fileName == TEMP_SCENE_NAME) + return PickFile(); + else + return SaveScene(editorScene.fileName); +} + +Node@ CreateNode(CreateMode mode, bool raycastToMouse = false) +{ + Node@ newNode = null; + if (editNode !is null) + newNode = editNode.CreateChild("", mode); + else + newNode = editorScene.CreateChild("", mode); + newNode.worldPosition = GetNewNodePosition(raycastToMouse); + + // Create an undo action for the create + CreateNodeAction action; + action.Define(newNode); + SaveEditAction(action); + SetSceneModified(); + + FocusNode(newNode); + + return newNode; +} + +void CreateComponent(const String&in componentType) +{ + // If this is the root node, do not allow to create duplicate scene-global components + if (editNode is editorScene && CheckForExistingGlobalComponent(editNode, componentType)) + return; + + // Group for storing undo actions + EditActionGroup group; + + // For now, make a local node's all components local + /// \todo Allow to specify the createmode + for (uint i = 0; i < editNodes.length; ++i) + { + Component@ newComponent = editNodes[i].CreateComponent(componentType, editNodes[i].replicated ? REPLICATED : LOCAL); + if (newComponent !is null) + { + // Some components such as CollisionShape do not create their internal object before the first call to ApplyAttributes() + // to prevent unnecessary initialization with default values. Call now + newComponent.ApplyAttributes(); + + CreateComponentAction action; + action.Define(newComponent); + group.actions.Push(action); + } + } + + SaveEditActionGroup(group); + SetSceneModified(); + + // Although the edit nodes selection are not changed, call to ensure attribute inspector notices new components of the edit nodes + HandleHierarchyListSelectionChange(); +} + +void CreateLoadedComponent(Component@ component) +{ + if (component is null) return; + CreateComponentAction action; + action.Define(component); + SaveEditAction(action); + SetSceneModified(); + FocusComponent(component); +} + +Node@ LoadNode(const String&in fileName, Node@ parent = null, bool raycastToMouse = false) +{ + if (fileName.empty) + return null; + + if (!fileSystem.FileExists(fileName)) + { + MessageBox("No such node file.\n" + fileName); + return null; + } + + File file(fileName, FILE_READ); + if (!file.open) + { + MessageBox("Could not open file.\n" + fileName); + return null; + } + + ui.cursor.shape = CS_BUSY; + + // Before instantiating, add object's resource path if necessary + SetResourcePath(GetPath(fileName), true, true); + + Node@ newNode = InstantiateNodeFromFile(file, GetNewNodePosition(raycastToMouse), Quaternion(), 1, parent, instantiateMode); + if (newNode !is null) + { + FocusNode(newNode); + instantiateFileName = fileName; + } + return newNode; +} + +Node@ InstantiateNodeFromFile(File@ file, const Vector3& position, const Quaternion& rotation, float scaleMod = 1.0f, Node@ parent = null, CreateMode mode = REPLICATED) +{ + if (file is null) + return null; + + Node@ newNode; + uint numSceneComponent = editorScene.numComponents; + + suppressSceneChanges = true; + + String extension = GetExtension(file.name); + if (extension == ".xml") + newNode = editorScene.InstantiateXML(file, position, rotation, mode); + else if (extension == ".json") + newNode = editorScene.InstantiateJSON(file, position, rotation, mode); + else + newNode = editorScene.Instantiate(file, position, rotation, mode); + + suppressSceneChanges = false; + + if (parent !is null) + newNode.parent = parent; + + if (newNode !is null) + { + newNode.scale = newNode.scale * scaleMod; + + AdjustNodePositionByAABB(newNode); + + // Create an undo action for the load + CreateNodeAction action; + action.Define(newNode); + SaveEditAction(action); + SetSceneModified(); + + if (numSceneComponent != editorScene.numComponents) + UpdateHierarchyItem(editorScene); + else + UpdateHierarchyItem(newNode); + } + + return newNode; +} + +void AdjustNodePositionByAABB(Node@ newNode) +{ + if (alignToAABBBottom) + { + Drawable@ drawable = GetFirstDrawable(newNode); + if (drawable !is null) + { + BoundingBox aabb = drawable.worldBoundingBox; + Vector3 aabbBottomCenter(aabb.center.x, aabb.min.y, aabb.center.z); + Vector3 offset = aabbBottomCenter - newNode.worldPosition; + newNode.worldPosition = newNode.worldPosition - offset; + } + } +} + +bool SaveNode(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; + } + + String extension = GetExtension(fileName); + bool success; + if (extension == ".xml") + success = editNode.SaveXML(file); + else if (extension == ".json") + success = editNode.SaveJSON(file); + else + success = editNode.Save(file); + RemoveBackup(success, fileName); + + if (success) + instantiateFileName = fileName; + else + MessageBox("Could not save node successfully!\nSee Urho3D.log for more detail."); + + return success; +} + +void UpdateScene(float timeStep) +{ + if (runUpdate) + editorScene.Update(timeStep); +} + +void StopSceneUpdate() +{ + runUpdate = false; + audio.Stop(); + toolBarDirty = true; + + // If scene should revert on update stop, load saved data now + if (revertOnPause && revertData !is null) + { + suppressSceneChanges = true; + editorScene.Clear(); + editorScene.LoadXML(revertData.root); + CreateGrid(); + UpdateHierarchyItem(editorScene, true); + ClearEditActions(); + suppressSceneChanges = false; + } + + revertData = null; +} + +void StartSceneUpdate() +{ + runUpdate = true; + // Run audio playback only when scene is updating, so that audio components' time-dependent attributes stay constant when + // paused (similar to physics) + audio.Play(); + toolBarDirty = true; + + // Save scene data for reverting if enabled + if (revertOnPause) + { + revertData = XMLFile(); + XMLElement root = revertData.CreateRoot("scene"); + editorScene.SaveXML(root); + } + else + revertData = null; +} + +bool ToggleSceneUpdate() +{ + if (!runUpdate) + StartSceneUpdate(); + else + StopSceneUpdate(); + return true; +} + +bool ShowLayerMover() +{ + if (ui.focusElement is null) + return ShowLayerEditor(); + else + return false; +} + +void SetSceneModified() +{ + if (!sceneModified) + { + sceneModified = true; + UpdateWindowTitle(); + } +} + +bool SceneDelete() +{ + ui.cursor.shape = CS_BUSY; + + BeginSelectionModify(); + + // Clear the selection now to prevent repopulation of selectedNodes and selectedComponents combo + hierarchyList.ClearSelection(); + + // Group for storing undo actions + EditActionGroup group; + + // Remove nodes + for (uint i = 0; i < selectedNodes.length; ++i) + { + Node@ node = selectedNodes[i]; + if (node.parent is null || node.scene is null) + continue; // Root or already deleted + + uint nodeIndex = GetListIndex(node); + + // Create undo action + DeleteNodeAction action; + action.Define(node); + group.actions.Push(action); + + node.Remove(); + SetSceneModified(); + + // If deleting only one node, select the next item in the same index + if (selectedNodes.length == 1 && selectedComponents.empty) + hierarchyList.selection = nodeIndex; + } + + // Then remove components, if they still remain + for (uint i = 0; i < selectedComponents.length; ++i) + { + Component@ component = selectedComponents[i]; + Node@ node = component.node; + if (node is null) + continue; // Already deleted + + uint index = GetComponentListIndex(component); + uint nodeIndex = GetListIndex(node); + if (index == NO_ITEM || nodeIndex == NO_ITEM) + continue; + + // Do not allow to remove the Octree, DebugRenderer or MaterialCache2D or DrawableProxy2D from the root node + if (node is editorScene && (component.typeName == "Octree" || component.typeName == "DebugRenderer" || + component.typeName == "MaterialCache2D" || component.typeName == "DrawableProxy2D")) + continue; + + // Create undo action + DeleteComponentAction action; + action.Define(component); + group.actions.Push(action); + + node.RemoveComponent(component); + SetSceneModified(); + + // If deleting only one component, select the next item in the same index + if (selectedComponents.length == 1 && selectedNodes.empty) + hierarchyList.selection = index; + } + + SaveEditActionGroup(group); + + EndSelectionModify(); + return true; +} + +bool SceneCut() +{ + return SceneCopy() && SceneDelete(); +} + +bool SceneCopy() +{ + ui.cursor.shape = CS_BUSY; + + sceneCopyBuffer.Clear(); + + // Copy components + if (!selectedComponents.empty) + { + for (uint i = 0; i < selectedComponents.length; ++i) + { + XMLFile@ xml = XMLFile(); + XMLElement rootElem = xml.CreateRoot("component"); + selectedComponents[i].SaveXML(rootElem); + rootElem.SetBool("local", !selectedComponents[i].replicated); + sceneCopyBuffer.Push(xml); + } + } + // Copy nodes + else + { + for (uint i = 0; i < selectedNodes.length; ++i) + { + // Skip the root scene node as it cannot be copied + if (selectedNodes[i] is editorScene) + continue; + + XMLFile@ xml = XMLFile(); + XMLElement rootElem = xml.CreateRoot("node"); + selectedNodes[i].SaveXML(rootElem); + rootElem.SetBool("local", !selectedNodes[i].replicated); + sceneCopyBuffer.Push(xml); + } + } + + return true; +} + +bool ScenePaste(bool pasteRoot = false, bool duplication = false) +{ + ui.cursor.shape = CS_BUSY; + + // Group for storing undo actions + EditActionGroup group; + + for (uint i = 0; i < sceneCopyBuffer.length; ++i) + { + XMLElement rootElem = sceneCopyBuffer[i].root; + String mode = rootElem.name; + if (mode == "component" && editNode !is null) + { + // If this is the root node, do not allow to create duplicate scene-global components + if (editNode is editorScene && CheckForExistingGlobalComponent(editNode, rootElem.GetAttribute("type"))) + return false; + + // If copied component was local, make the new local too + Component@ newComponent = editNode.CreateComponent(rootElem.GetAttribute("type"), rootElem.GetBool("local") ? LOCAL : + REPLICATED); + if (newComponent is null) + return false; + + newComponent.LoadXML(rootElem); + newComponent.ApplyAttributes(); + + // Create an undo action + CreateComponentAction action; + action.Define(newComponent); + group.actions.Push(action); + } + else if (mode == "node") + { + // If copied node was local, make the new local too + Node@ newNode; + // Are we pasting into the root node? + if (pasteRoot) + newNode = editorScene.CreateChild("", rootElem.GetBool("local") ? LOCAL : REPLICATED); + else + { + // If we are duplicating or have the original node selected, paste into the selected nodes parent + if (duplication || editNode is null || editNode.id == rootElem.GetU32("id")) + { + if (editNode !is null && editNode.parent !is null) + newNode = editNode.parent.CreateChild("", rootElem.GetBool("local") ? LOCAL : REPLICATED); + else + newNode = editorScene.CreateChild("", rootElem.GetBool("local") ? LOCAL : REPLICATED); + } + // If we aren't duplicating, paste into the selected node + else + { + newNode = editNode.CreateChild("", rootElem.GetBool("local") ? LOCAL : REPLICATED); + } + } + + newNode.LoadXML(rootElem); + + // Create an undo action + CreateNodeAction action; + action.Define(newNode); + group.actions.Push(action); + } + } + + SaveEditActionGroup(group); + SetSceneModified(); + return true; +} + +bool SceneDuplicate() +{ + Array copy = sceneCopyBuffer; + + if (!SceneCopy()) + { + sceneCopyBuffer = copy; + return false; + } + if (!ScenePaste(false, true)) + { + sceneCopyBuffer = copy; + return false; + } + + sceneCopyBuffer = copy; + return true; +} + +bool SceneUnparent() +{ + if (!CheckHierarchyWindowFocus() || !selectedComponents.empty || selectedNodes.empty) + return false; + + ui.cursor.shape = CS_BUSY; + + // Group for storing undo actions + EditActionGroup group; + + // Parent selected nodes to root + Array changedNodes; + for (uint i = 0; i < selectedNodes.length; ++i) + { + Node@ sourceNode = selectedNodes[i]; + if (sourceNode.parent is null || sourceNode.parent is editorScene) + continue; // Root or already parented to root + + // Perform the reparenting, continue loop even if action fails + ReparentNodeAction action; + action.Define(sourceNode, editorScene); + group.actions.Push(action); + + SceneChangeParent(sourceNode, editorScene, false); + changedNodes.Push(sourceNode); + } + + // Reselect the changed nodes at their new position in the list + for (uint i = 0; i < changedNodes.length; ++i) + hierarchyList.AddSelection(GetListIndex(changedNodes[i])); + + SaveEditActionGroup(group); + SetSceneModified(); + + return true; +} + +bool NodesParentToLastSelected() +{ + if (lastSelectedNode.Get() is null) + return false; + + if (!CheckHierarchyWindowFocus() || !selectedComponents.empty || selectedNodes.empty) + return false; + + ui.cursor.shape = CS_BUSY; + + // Group for storing undo actions + EditActionGroup group; + + // Parent selected nodes to root + Array changedNodes; + + // Find new parent node it selected last + Node@ lastNode = lastSelectedNode.Get(); //GetListNode(hierarchyList.selection); + + for (uint i = 0; i < selectedNodes.length; ++i) + { + + Node@ sourceNode = selectedNodes[i]; + if ( sourceNode.id == lastNode.id) + continue; // Skip last node it is parent + + if (sourceNode.parent.id == lastNode.id) + continue; // Root or already parented to root + + // Perform the reparenting, continue loop even if action fails + ReparentNodeAction action; + action.Define(sourceNode, lastNode); + group.actions.Push(action); + + SceneChangeParent(sourceNode, lastNode, false); + changedNodes.Push(sourceNode); + } + + // Reselect the changed nodes at their new position in the list + for (uint i = 0; i < changedNodes.length; ++i) + hierarchyList.AddSelection(GetListIndex(changedNodes[i])); + + SaveEditActionGroup(group); + SetSceneModified(); + + return true; +} + +bool SceneSmartDuplicateNode() +{ + const float minOffset = 0.1; + + if (!CheckHierarchyWindowFocus() || !selectedComponents.empty + || selectedNodes.empty || lastSelectedNode.Get() is null) + return false; + + + Node@ node = lastSelectedNode.Get(); + Node@ parent = node.parent; + Vector3 offset = Vector3(1,0,0); // default offset + + if (parent is editorScene) // if parent of selected node is Scene make empty parent for it and place in same position; + { + parent = CreateNode(LOCAL); + SceneChangeParent(parent, editorScene, false); + parent.worldPosition = node.worldPosition; + parent.name = node.name + "Group"; + node.name = parent.name + "Instance" + String(parent.numChildren); + SceneChangeParent(node, parent, false); + parent = node.parent; + SelectNode(node, false); + } + + Vector3 size; + BoundingBox bb; + + // get bb for offset + Drawable@ drawable = GetFirstDrawable(node); + if (drawable !is null) + { + bb = drawable.boundingBox; + size = bb.size * drawable.node.worldScale; + offset = Vector3(size.x, 0, 0); + } + + // make offset on axis that select user by mouse + if (gizmoAxisX.selected) + { + if (size.x < minOffset) size.x = minOffset; + offset = node.worldRotation * Vector3(size.x,0,0); + } + else if (gizmoAxisY.selected) + { + if (size.y < minOffset) size.y = minOffset; + offset = node.worldRotation * Vector3(0,size.y,0); + } + else if (gizmoAxisZ.selected) + { + if (size.z < minOffset) size.z = minOffset; + offset = node.worldRotation * Vector3(0,0,size.z); + } + else + offset = lastOffsetForSmartDuplicate; + + Vector3 lastInstancePosition = node.worldPosition; + + SelectNode(node, false); + SceneDuplicate(); + Node@ newInstance = parent.children[parent.numChildren-1]; + SelectNode(newInstance, false); + newInstance.worldPosition = lastInstancePosition; + newInstance.Translate(offset, TransformSpace::World); + newInstance.name = parent.name + "Instance" + String(parent.numChildren-1); + + lastOffsetForSmartDuplicate = offset; + UpdateNodeAttributes(); + return true; +} + +bool ViewCloser() +{ + if (selectedNodes.length > 0 || selectedNodes.length > 0) + LocateNodesAndComponents(selectedNodes, selectedComponents); + + return true; +} + + +bool SceneToggleEnable() +{ + if (!CheckHierarchyWindowFocus()) + return false; + + ui.cursor.shape = CS_BUSY; + + EditActionGroup group; + + // Toggle enabled state of nodes recursively + for (uint i = 0; i < selectedNodes.length; ++i) + { + // Do not attempt to disable the Scene + if (selectedNodes[i].typeName == "Node") + { + bool oldEnabled = selectedNodes[i].enabled; + selectedNodes[i].SetEnabledRecursive(!oldEnabled); + + // Create undo action + ToggleNodeEnabledAction action; + action.Define(selectedNodes[i], oldEnabled); + group.actions.Push(action); + } + } + for (uint i = 0; i < selectedComponents.length; ++i) + { + // Some components purposefully do not expose the Enabled attribute, and it does not affect them in any way + // (Octree, PhysicsWorld). Check that the first attribute is in fact called "Is Enabled" + if (selectedComponents[i].numAttributes > 0 && selectedComponents[i].attributeInfos[0].name == "Is Enabled") + { + bool oldEnabled = selectedComponents[i].enabled; + selectedComponents[i].enabled = !oldEnabled; + + // Create undo action + EditAttributeAction action; + action.Define(selectedComponents[i], 0, Variant(oldEnabled)); + group.actions.Push(action); + } + } + + SaveEditActionGroup(group); + SetSceneModified(); + + return true; +} + +bool SceneEnableAllNodes() +{ + if (!CheckHierarchyWindowFocus()) + return false; + + ui.cursor.shape = CS_BUSY; + + EditActionGroup group; + + // Toggle enabled state of nodes recursively + Array allNodes; + allNodes = editorScene.GetChildren(true); + + for (uint i = 0; i < allNodes.length; ++i) + { + // Do not attempt to disable the Scene + if (allNodes[i].typeName == "Node") + { + bool oldEnabled = allNodes[i].enabled; + if (oldEnabled == false) + allNodes[i].SetEnabledRecursive(true); + + // Create undo action + ToggleNodeEnabledAction action; + action.Define(allNodes[i], oldEnabled); + group.actions.Push(action); + } + } + + Array allComponents; + allComponents = editorScene.GetComponents(); + + for (uint i = 0; i < allComponents.length; ++i) + { + // Some components purposefully do not expose the Enabled attribute, and it does not affect them in any way + // (Octree, PhysicsWorld). Check that the first attribute is in fact called "Is Enabled" + if (allComponents[i].numAttributes > 0 && allComponents[i].attributeInfos[0].name == "Is Enabled") + { + bool oldEnabled = allComponents[i].enabled; + allComponents[i].enabled = true; + + // Create undo action + EditAttributeAction action; + action.Define(allComponents[i], 0, Variant(oldEnabled)); + group.actions.Push(action); + } + } + + SaveEditActionGroup(group); + SetSceneModified(); + + return true; +} + +bool SceneChangeParent(Node@ sourceNode, Node@ targetNode, bool createUndoAction = true) +{ + // Create undo action if requested + if (createUndoAction) + { + ReparentNodeAction action; + action.Define(sourceNode, targetNode); + SaveEditAction(action); + } + + sourceNode.parent = targetNode; + SetSceneModified(); + + // Return true if success + if (sourceNode.parent is targetNode) + { + UpdateNodeAttributes(); // Parent change may have changed local transform + return true; + } + else + return false; +} + +bool SceneChangeParent(Node@ sourceNode, Array sourceNodes, Node@ targetNode, bool createUndoAction = true) +{ + // Create undo action if requested + if (createUndoAction) + { + ReparentNodeAction action; + action.Define(sourceNodes, targetNode); + SaveEditAction(action); + } + + for (uint i = 0; i < sourceNodes.length; ++i) + { + Node@ node = sourceNodes[i]; + node.parent = targetNode; + } + SetSceneModified(); + + // Return true if success + if (sourceNode.parent is targetNode) + { + UpdateNodeAttributes(); // Parent change may have changed local transform + return true; + } + else + return false; +} + +bool SceneReorder(Node@ sourceNode, Node@ targetNode) +{ + if (sourceNode is null || targetNode is null || sourceNode.parent is null || sourceNode.parent !is targetNode.parent) + return false; + if (sourceNode is targetNode) + return true; // No-op + + Node@ parent = sourceNode.parent; + uint destIndex = SceneFindChildIndex(parent, targetNode); + + ReorderNodeAction action; + action.Define(sourceNode, destIndex); + SaveEditAction(action); + PerformReorder(parent, sourceNode, destIndex); + return true; +} + +bool SceneReorder(Component@ sourceComponent, Component@ targetComponent) +{ + if (sourceComponent is null || targetComponent is null || sourceComponent.node !is targetComponent.node) + return false; + if (sourceComponent is targetComponent) + return true; // No-op + + Node@ node = sourceComponent.node; + uint destIndex = SceneFindComponentIndex(node, targetComponent); + + ReorderComponentAction action; + action.Define(sourceComponent, destIndex); + SaveEditAction(action); + PerformReorder(node, sourceComponent, destIndex); + return true; +} + +void PerformReorder(Node@ parent, Node@ child, uint destIndex) +{ + suppressSceneChanges = true; + + // Removal from scene zeroes the ID. Be prepared to restore it + uint oldId = child.id; + parent.RemoveChild(child); + child.id = oldId; + parent.AddChild(child, destIndex); + UpdateHierarchyItem(parent); // Force update to make sure the order is current + SetSceneModified(); + + suppressSceneChanges = false; +} + +void PerformReorder(Node@ node, Component@ component, uint destIndex) +{ + suppressSceneChanges = true; + + node.ReorderComponent(component, destIndex); + UpdateHierarchyItem(node); // Force update to make sure the order is current + SetSceneModified(); + + suppressSceneChanges = false; +} + +uint SceneFindChildIndex(Node@ parent, Node@ child) +{ + for (uint i = 0; i < parent.numChildren; ++i) + { + if (parent.children[i] is child) + return i; + } + + return -1; +} + +uint SceneFindComponentIndex(Node@ node, Component@ component) +{ + for (uint i = 0; i < node.numComponents; ++i) + { + if (node.components[i] is component) + return i; + } + + return -1; +} + +bool SceneResetPosition() +{ + if (editNode !is null) + { + Transform oldTransform; + oldTransform.Define(editNode); + + editNode.position = Vector3(0.0, 0.0, 0.0); + + // Create undo action + EditNodeTransformAction action; + action.Define(editNode, oldTransform); + SaveEditAction(action); + SetSceneModified(); + + UpdateNodeAttributes(); + return true; + } + else + return false; +} + +bool SceneResetRotation() +{ + if (editNode !is null) + { + Transform oldTransform; + oldTransform.Define(editNode); + + editNode.rotation = Quaternion(); + + // Create undo action + EditNodeTransformAction action; + action.Define(editNode, oldTransform); + SaveEditAction(action); + SetSceneModified(); + + UpdateNodeAttributes(); + return true; + } + else + return false; +} + +bool SceneResetScale() +{ + if (editNode !is null) + { + Transform oldTransform; + oldTransform.Define(editNode); + + editNode.scale = Vector3(1.0, 1.0, 1.0); + + // Create undo action + EditNodeTransformAction action; + action.Define(editNode, oldTransform); + SaveEditAction(action); + SetSceneModified(); + + UpdateNodeAttributes(); + return true; + } + else + return false; +} + +bool SceneResetTransform() +{ + if (editNode !is null) + { + Transform oldTransform; + oldTransform.Define(editNode); + + editNode.position = Vector3(0.0, 0.0, 0.0); + editNode.rotation = Quaternion(); + editNode.scale = Vector3(1.0, 1.0, 1.0); + + // Create undo action + EditNodeTransformAction action; + action.Define(editNode, oldTransform); + SaveEditAction(action); + SetSceneModified(); + + UpdateNodeAttributes(); + return true; + } + else + return false; +} + +bool SceneSelectAll() +{ + BeginSelectionModify(); + Array rootLevelNodes = editorScene.GetChildren(); + Array indices; + for (uint i = 0; i < rootLevelNodes.length; ++i) + indices.Push(GetListIndex(rootLevelNodes[i])); + hierarchyList.SetSelections(indices); + EndSelectionModify(); + + return true; +} + +bool SceneResetToDefault() +{ + ui.cursor.shape = CS_BUSY; + + // Group for storing undo actions + EditActionGroup group; + + // Reset selected component to their default + if (!selectedComponents.empty) + { + for (uint i = 0; i < selectedComponents.length; ++i) + { + Component@ component = selectedComponents[i]; + + ResetAttributesAction action; + action.Define(component); + group.actions.Push(action); + + component.ResetToDefault(); + component.ApplyAttributes(); + for (uint j = 0; j < component.numAttributes; ++j) + PostEditAttribute(component, j); + } + } + // OR reset selected nodes to their default + else + { + for (uint i = 0; i < selectedNodes.length; ++i) + { + Node@ node = selectedNodes[i]; + + ResetAttributesAction action; + action.Define(node); + group.actions.Push(action); + + node.ResetToDefault(); + node.ApplyAttributes(); + for (uint j = 0; j < node.numAttributes; ++j) + PostEditAttribute(node, j); + } + } + + SaveEditActionGroup(group); + SetSceneModified(); + attributesFullDirty = true; + + return true; +} + +bool SceneRebuildNavigation() +{ + ui.cursor.shape = CS_BUSY; + + Array@ navMeshes = editorScene.GetComponents("NavigationMesh", true); + if (navMeshes.empty) + { + @navMeshes = editorScene.GetComponents("DynamicNavigationMesh", true); + if (navMeshes.empty) + { + MessageBox("No NavigationMesh components in the scene, nothing to rebuild."); + return false; + } + } + + bool success = true; + for (uint i = 0; i < navMeshes.length; ++i) + { + NavigationMesh@ navMesh = navMeshes[i]; + if (!navMesh.Build()) + success = false; + } + + return success; +} + +bool SceneRenderZoneCubemaps() +{ + bool success = false; + Array capturedThisCall; + bool alreadyCapturing = activeCubeCapture.length > 0; // May have managed to quickly queue up a second round of zones to render cubemaps for + + for (uint i = 0; i < selectedNodes.length; ++i) + { + Array@ zones = selectedNodes[i].GetComponents("Zone", true); + for (uint z = 0; z < zones.length; ++z) + { + Zone@ zone = cast(zones[z]); + if (zone !is null) + { + activeCubeCapture.Push(EditorCubeCapture(zone)); + capturedThisCall.Push(zone); + } + } + } + + for (uint i = 0; i < selectedComponents.length; ++i) + { + Zone@ zone = cast(selectedComponents[i]); + if (zone !is null) + { + if (capturedThisCall.FindByRef(zone) < 0) + { + activeCubeCapture.Push(EditorCubeCapture(zone)); + capturedThisCall.Push(zone); + } + } + } + + // Start rendering cubemaps if there are any to render and the queue isn't already running + if (activeCubeCapture.length > 0 && !alreadyCapturing) + activeCubeCapture[0].Start(); + + if (capturedThisCall.length <= 0) + { + MessageBox("No zones selected to render cubemaps for/"); + } + return capturedThisCall.length > 0; +} + +bool SceneAddChildrenStaticModelGroup() +{ + StaticModelGroup@ smg = cast(editComponents.length > 0 ? editComponents[0] : null); + if (smg is null && editNode !is null) + smg = editNode.GetComponent("StaticModelGroup"); + + if (smg is null) + { + MessageBox("Must have a StaticModelGroup component selected."); + return false; + } + + uint attrIndex = GetAttributeIndex(smg, "Instance Nodes"); + Variant oldValue = smg.attributes[attrIndex]; + + Array children = smg.node.GetChildren(true); + for (uint i = 0; i < children.length; ++i) + smg.AddInstanceNode(children[i]); + + EditAttributeAction action; + action.Define(smg, attrIndex, oldValue); + SaveEditAction(action); + SetSceneModified(); + FocusComponent(smg); + + return true; +} + +bool SceneSetChildrenSplinePath(bool makeCycle) +{ + SplinePath@ sp = cast(editComponents.length > 0 ? editComponents[0] : null); + if (sp is null && editNode !is null) + sp = editNode.GetComponent("SplinePath"); + + if (sp is null) + { + MessageBox("Must have a SplinePath component selected."); + return false; + } + + uint attrIndex = GetAttributeIndex(sp, "Control Points"); + Variant oldValue = sp.attributes[attrIndex]; + + Array children = sp.node.GetChildren(true); + if (children.length >= 2) + { + sp.ClearControlPoints(); + for (uint i = 0; i < children.length; ++i) + sp.AddControlPoint(children[i]); + } + else + { + MessageBox("You must have a minimum two children Nodes in selected Node."); + return false; + } + + if (makeCycle) + sp.AddControlPoint(children[0]); + + EditAttributeAction action; + action.Define(sp, attrIndex, oldValue); + SaveEditAction(action); + SetSceneModified(); + FocusComponent(sp); + + return true; +} + +void AssignMaterial(StaticModel@ model, String materialPath) +{ + Material@ material = cache.GetResource("Material", materialPath); + if (material is null) + return; + + ResourceRefList materials = model.GetAttribute("Material").GetResourceRefList(); + Array oldMaterials; + for(uint i = 0; i < materials.length; ++i) + oldMaterials.Push(materials.names[i]); + + model.material = material; + + AssignMaterialAction action; + action.Define(model, oldMaterials, material); + SaveEditAction(action); + SetSceneModified(); + FocusComponent(model); +} + +void UpdateSceneMru(String filename) +{ + while (uiRecentScenes.Find(filename) > -1) + uiRecentScenes.Erase(uiRecentScenes.Find(filename)); + + uiRecentScenes.Insert(0, filename); + + for (uint i = uiRecentScenes.length - 1; i >= maxRecentSceneCount; i--) + uiRecentScenes.Erase(i); + + PopulateMruScenes(); +} + +Drawable@ GetFirstDrawable(Node@ node) +{ + Array nodes = node.GetChildren(true); + nodes.Insert(0, node); + + for (uint i = 0; i < nodes.length; ++i) + { + Array components = nodes[i].GetComponents(); + for (uint j = 0; j < components.length; ++j) + { + Drawable@ drawable = cast(components[j]); + if (drawable !is null) + return drawable; + } + } + + return null; +} + +void AssignModel(StaticModel@ assignee, String modelPath) +{ + Model@ model = cache.GetResource("Model", modelPath); + if (model is null) + return; + + Model@ oldModel = assignee.model; + assignee.model = model; + + AssignModelAction action; + action.Define(assignee, oldModel, model); + SaveEditAction(action); + SetSceneModified(); + FocusComponent(assignee); +} + +void CreateModelWithStaticModel(String filepath, Node@ parent) +{ + if (parent is null) + return; + /// \todo should be able to specify the createmode + if (parent is editorScene) + parent = CreateNode(REPLICATED); + + Model@ model = cache.GetResource("Model", filepath); + if (model is null) + return; + + StaticModel@ staticModel = parent.GetOrCreateComponent("StaticModel"); + staticModel.model = model; + if (applyMaterialList) + staticModel.ApplyMaterialList(); + CreateLoadedComponent(staticModel); +} + +void CreateModelWithAnimatedModel(String filepath, Node@ parent) +{ + if (parent is null) + return; + /// \todo should be able to specify the createmode + if (parent is editorScene) + parent = CreateNode(REPLICATED); + + Model@ model = cache.GetResource("Model", filepath); + if (model is null) + return; + + AnimatedModel@ animatedModel = parent.GetOrCreateComponent("AnimatedModel"); + animatedModel.model = model; + if (applyMaterialList) + animatedModel.ApplyMaterialList(); + CreateLoadedComponent(animatedModel); +} + +bool ColorWheelSetupBehaviorForColoring() +{ + Menu@ menu = GetEventSender(); + if (menu is null) + return false; + + coloringPropertyName = menu.name; + + if (coloringPropertyName == "menuCancel") return false; + + if (coloringComponent.typeName == "Light") + { + Light@ light = cast(coloringComponent); + if (light !is null) + { + if (coloringPropertyName == "menuLightColor") + { + coloringOldColor = light.color; + ShowColorWheelWithColor(coloringOldColor); + } + else if (coloringPropertyName == "menuSpecularIntensity") + { + // ColorWheel have only 0-1 range output of V-value(BW), and for huge-range values we divide in and multiply out + float scaledSpecular = light.specularIntensity * 0.1f; + coloringOldScalar = scaledSpecular; + ShowColorWheelWithColor(Color(scaledSpecular,scaledSpecular,scaledSpecular)); + + } + else if (coloringPropertyName == "menuBrightnessMultiplier") + { + float scaledBrightness = light.brightness * 0.1f; + coloringOldScalar = scaledBrightness; + ShowColorWheelWithColor(Color(scaledBrightness,scaledBrightness,scaledBrightness)); + } + } + } + else if (coloringComponent.typeName == "StaticModel") + { + StaticModel@ model = cast(coloringComponent); + if (model !is null) + { + Material@ mat = model.materials[0]; + if (mat !is null) + { + if (coloringPropertyName == "menuDiffuseColor") + { + Variant oldValue = mat.shaderParameters["MatDiffColor"]; + Array values = oldValue.ToString().Split(' '); + coloringOldColor = Color(values[0].ToFloat(),values[1].ToFloat(),values[2].ToFloat(),values[3].ToFloat()); //RGBA + ShowColorWheelWithColor(coloringOldColor); + } + else if (coloringPropertyName == "menuSpecularColor") + { + Variant oldValue = mat.shaderParameters["MatSpecColor"]; + Array values = oldValue.ToString().Split(' '); + coloringOldColor = Color(values[0].ToFloat(),values[1].ToFloat(),values[2].ToFloat()); + coloringOldScalar = values[3].ToFloat(); + ShowColorWheelWithColor(Color(coloringOldColor.r, coloringOldColor.g, coloringOldColor.b, coloringOldScalar/128.0f)); //RGB + shine + } + else if (coloringPropertyName == "menuEmissiveColor") + { + Variant oldValue = mat.shaderParameters["MatEmissiveColor"]; + Array values = oldValue.ToString().Split(' '); + coloringOldColor = Color(values[0].ToFloat(),values[1].ToFloat(),values[2].ToFloat()); // RGB + + + ShowColorWheelWithColor(coloringOldColor); + } + else if (coloringPropertyName == "menuEnvironmentMapColor") + { + Variant oldValue = mat.shaderParameters["MatEnvMapColor"]; + Array values = oldValue.ToString().Split(' '); + coloringOldColor = Color(values[0].ToFloat(),values[1].ToFloat(),values[2].ToFloat()); //RGB + + ShowColorWheelWithColor(coloringOldColor); + } + } + } + } + else if (coloringComponent.typeName == "Zone") + { + Zone@ zone = cast(coloringComponent); + if (zone !is null) + { + if (coloringPropertyName == "menuAmbientColor") + { + coloringOldColor = zone.ambientColor; + } + else if (coloringPropertyName == "menuFogColor") + { + coloringOldColor = zone.fogColor; + } + + ShowColorWheelWithColor(coloringOldColor); + } + } + else if (coloringComponent.typeName == "Text3D") + { + Text3D@ txt = cast(coloringComponent); + if (txt !is null) + { + if (coloringPropertyName == "c" || coloringPropertyName == "tl") + coloringOldColor = txt.colors[C_TOPLEFT]; + else if (coloringPropertyName == "tr") + coloringOldColor = txt.colors[C_TOPRIGHT]; + else if (coloringPropertyName == "bl") + coloringOldColor = txt.colors[C_BOTTOMLEFT]; + else if (coloringPropertyName == "br") + coloringOldColor = txt.colors[C_BOTTOMRIGHT]; + + ShowColorWheelWithColor(coloringOldColor); + } + } + return true; +} + diff --git a/bin/Data/Scripts/Editor/EditorSecondaryToolbar.as b/bin/EditorData/Editor/Scripts/EditorSecondaryToolbar.as similarity index 100% rename from bin/Data/Scripts/Editor/EditorSecondaryToolbar.as rename to bin/EditorData/Editor/Scripts/EditorSecondaryToolbar.as diff --git a/bin/Data/Scripts/Editor/EditorSettings.as b/bin/EditorData/Editor/Scripts/EditorSettings.as similarity index 96% rename from bin/Data/Scripts/Editor/EditorSettings.as rename to bin/EditorData/Editor/Scripts/EditorSettings.as index dd05a53ba67..b41d6cfb46b 100644 --- a/bin/Data/Scripts/Editor/EditorSettings.as +++ b/bin/EditorData/Editor/Scripts/EditorSettings.as @@ -1,462 +1,462 @@ -// Urho3D editor settings dialog -bool subscribedToEditorSettings = false; -Window@ settingsDialog; -String defaultTags; - -void CreateEditorSettingsDialog() -{ - if (settingsDialog !is null) - return; - - settingsDialog = LoadEditorUI("UI/EditorSettingsDialog.xml"); - ui.root.AddChild(settingsDialog); - settingsDialog.opacity = uiMaxOpacity; - settingsDialog.height = 440; - CenterDialog(settingsDialog); - UpdateEditorSettingsDialog(); - HideEditorSettingsDialog(); -} - -void UpdateEditorSettingsDialog() -{ - if (settingsDialog is null) - return; - - LineEdit@ nearClipEdit = settingsDialog.GetChild("NearClipEdit", true); - nearClipEdit.text = String(viewNearClip); - - LineEdit@ farClipEdit = settingsDialog.GetChild("FarClipEdit", true); - farClipEdit.text = String(viewFarClip); - - LineEdit@ fovEdit = settingsDialog.GetChild("FOVEdit", true); - fovEdit.text = String(viewFov); - - LineEdit@ speedEdit = settingsDialog.GetChild("SpeedEdit", true); - speedEdit.text = String(cameraBaseSpeed); - - CheckBox@ limitRotationToggle = settingsDialog.GetChild("LimitRotationToggle", true); - limitRotationToggle.checked = limitRotation; - - - DropDownList@ mouseOrbitEdit = settingsDialog.GetChild("MouseOrbitEdit", true); - mouseOrbitEdit.selection = mouseOrbitMode; - - CheckBox@ middleMousePanToggle = settingsDialog.GetChild("MiddleMousePanToggle", true); - middleMousePanToggle.checked = mmbPanMode; - - CheckBox@ rotateAroundSelectToggle = settingsDialog.GetChild("RotateAroundSelectionToggle", true); - rotateAroundSelectToggle.checked = rotateAroundSelect; - - DropDownList@ hotKeysModeEdit = settingsDialog.GetChild("HotKeysModeEdit", true); - hotKeysModeEdit.selection = hotKeyMode; - - DropDownList@ newNodeModeEdit = settingsDialog.GetChild("NewNodeModeEdit", true); - newNodeModeEdit.selection = newNodeMode; - - LineEdit@ moveStepEdit = settingsDialog.GetChild("MoveStepEdit", true); - moveStepEdit.text = String(moveStep); - CheckBox@ moveSnapToggle = settingsDialog.GetChild("MoveSnapToggle", true); - moveSnapToggle.checked = moveSnap; - - LineEdit@ rotateStepEdit = settingsDialog.GetChild("RotateStepEdit", true); - rotateStepEdit.text = String(rotateStep); - CheckBox@ rotateSnapToggle = settingsDialog.GetChild("RotateSnapToggle", true); - rotateSnapToggle.checked = rotateSnap; - - LineEdit@ scaleStepEdit = settingsDialog.GetChild("ScaleStepEdit", true); - scaleStepEdit.text = String(scaleStep); - CheckBox@ scaleSnapToggle = settingsDialog.GetChild("ScaleSnapToggle", true); - scaleSnapToggle.checked = scaleSnap; - - CheckBox@ applyMaterialListToggle = settingsDialog.GetChild("ApplyMaterialListToggle", true); - applyMaterialListToggle.checked = applyMaterialList; - - CheckBox@ rememberResourcePathToggle = settingsDialog.GetChild("RememberResourcePathToggle", true); - rememberResourcePathToggle.checked = rememberResourcePath; - - LineEdit@ importOptionsEdit = settingsDialog.GetChild("ImportOptionsEdit", true); - importOptionsEdit.text = importOptions; - - DropDownList@ pickModeEdit = settingsDialog.GetChild("PickModeEdit", true); - pickModeEdit.selection = pickMode; - - LineEdit@ renderPathNameEdit = settingsDialog.GetChild("RenderPathNameEdit", true); - renderPathNameEdit.text = renderPathName; - - Button@ pickRenderPathButton = settingsDialog.GetChild("PickRenderPathButton", true); - - DropDownList@ textureQualityEdit = settingsDialog.GetChild("TextureQualityEdit", true); - textureQualityEdit.selection = renderer.textureQuality; - - DropDownList@ materialQualityEdit = settingsDialog.GetChild("MaterialQualityEdit", true); - materialQualityEdit.selection = renderer.materialQuality; - - DropDownList@ shadowResolutionEdit = settingsDialog.GetChild("ShadowResolutionEdit", true); - shadowResolutionEdit.selection = GetShadowResolution(); - - DropDownList@ shadowQualityEdit = settingsDialog.GetChild("ShadowQualityEdit", true); - shadowQualityEdit.selection = int(renderer.shadowQuality); - - LineEdit@ maxOccluderTrianglesEdit = settingsDialog.GetChild("MaxOccluderTrianglesEdit", true); - maxOccluderTrianglesEdit.text = String(renderer.maxOccluderTriangles); - - CheckBox@ specularLightingToggle = settingsDialog.GetChild("SpecularLightingToggle", true); - specularLightingToggle.checked = renderer.specularLighting; - - CheckBox@ dynamicInstancingToggle = settingsDialog.GetChild("DynamicInstancingToggle", true); - dynamicInstancingToggle.checked = renderer.dynamicInstancing; - - CheckBox@ frameLimiterToggle = settingsDialog.GetChild("FrameLimiterToggle", true); - frameLimiterToggle.checked = engine.maxFps > 0; - - CheckBox@ gammaCorrectionToggle = settingsDialog.GetChild("GammaCorrectionToggle", true); - gammaCorrectionToggle.checked = gammaCorrection; - - CheckBox@ HDRToggle = settingsDialog.GetChild("HDRToggle", true); - HDRToggle.checked = HDR; - - CheckBox@ sRGBToggle = settingsDialog.GetChild("SRGBToggle", true); - sRGBToggle.checked = graphics.sRGB; - - LineEdit@ cubemapPath = settingsDialog.GetChild("CubeMapGenPath", true); - cubemapPath.text = cubeMapGen_Path; - LineEdit@ cubemapName = settingsDialog.GetChild("CubeMapGenKey", true); - cubemapName.text = cubeMapGen_Name; - LineEdit@ cubemapSize = settingsDialog.GetChild("CubeMapGenSize", true); - cubemapSize.text = String(cubeMapGen_Size); - - LineEdit@ defaultTagsEdit = settingsDialog.GetChild("DefaultTagsEdit", true); - defaultTagsEdit.text = defaultTags.Trimmed(); - - - if (!subscribedToEditorSettings) - { - SubscribeToEvent(nearClipEdit, "TextChanged", "EditCameraNearClip"); - SubscribeToEvent(nearClipEdit, "TextFinished", "EditCameraNearClip"); - SubscribeToEvent(farClipEdit, "TextChanged", "EditCameraFarClip"); - SubscribeToEvent(farClipEdit, "TextFinished", "EditCameraFarClip"); - SubscribeToEvent(fovEdit, "TextChanged", "EditCameraFOV"); - SubscribeToEvent(fovEdit, "TextFinished", "EditCameraFOV"); - SubscribeToEvent(speedEdit, "TextChanged", "EditCameraSpeed"); - SubscribeToEvent(speedEdit, "TextFinished", "EditCameraSpeed"); - SubscribeToEvent(limitRotationToggle, "Toggled", "EditLimitRotation"); - SubscribeToEvent(middleMousePanToggle, "Toggled", "EditMiddleMousePan"); - SubscribeToEvent(rotateAroundSelectToggle, "Toggled", "EditRotateAroundSelect"); - SubscribeToEvent(mouseOrbitEdit, "ItemSelected", "EditMouseOrbitMode"); - SubscribeToEvent(hotKeysModeEdit, "ItemSelected", "EditHotKeyMode"); - SubscribeToEvent(newNodeModeEdit, "ItemSelected", "EditNewNodeMode"); - SubscribeToEvent(moveStepEdit, "TextChanged", "EditMoveStep"); - SubscribeToEvent(moveStepEdit, "TextFinished", "EditMoveStep"); - SubscribeToEvent(rotateStepEdit, "TextChanged", "EditRotateStep"); - SubscribeToEvent(rotateStepEdit, "TextFinished", "EditRotateStep"); - SubscribeToEvent(scaleStepEdit, "TextChanged", "EditScaleStep"); - SubscribeToEvent(scaleStepEdit, "TextFinished", "EditScaleStep"); - SubscribeToEvent(moveSnapToggle, "Toggled", "EditMoveSnap"); - SubscribeToEvent(rotateSnapToggle, "Toggled", "EditRotateSnap"); - SubscribeToEvent(scaleSnapToggle, "Toggled", "EditScaleSnap"); - SubscribeToEvent(rememberResourcePathToggle, "Toggled", "EditRememberResourcePath"); - SubscribeToEvent(applyMaterialListToggle, "Toggled", "EditApplyMaterialList"); - SubscribeToEvent(importOptionsEdit, "TextChanged", "EditImportOptions"); - SubscribeToEvent(importOptionsEdit, "TextFinished", "EditImportOptions"); - SubscribeToEvent(pickModeEdit, "ItemSelected", "EditPickMode"); - SubscribeToEvent(renderPathNameEdit, "TextFinished", "EditRenderPathName"); - SubscribeToEvent(pickRenderPathButton, "Released", "PickRenderPath"); - SubscribeToEvent(textureQualityEdit, "ItemSelected", "EditTextureQuality"); - SubscribeToEvent(materialQualityEdit, "ItemSelected", "EditMaterialQuality"); - SubscribeToEvent(shadowResolutionEdit, "ItemSelected", "EditShadowResolution"); - SubscribeToEvent(shadowQualityEdit, "ItemSelected", "EditShadowQuality"); - SubscribeToEvent(maxOccluderTrianglesEdit, "TextChanged", "EditMaxOccluderTriangles"); - SubscribeToEvent(maxOccluderTrianglesEdit, "TextFinished", "EditMaxOccluderTriangles"); - SubscribeToEvent(specularLightingToggle, "Toggled", "EditSpecularLighting"); - SubscribeToEvent(dynamicInstancingToggle, "Toggled", "EditDynamicInstancing"); - SubscribeToEvent(frameLimiterToggle, "Toggled", "EditFrameLimiter"); - SubscribeToEvent(gammaCorrectionToggle, "Toggled", "EditGammaCorrection"); - SubscribeToEvent(HDRToggle, "Toggled", "EditHDR"); - SubscribeToEvent(sRGBToggle, "Toggled", "EditSRGB"); - SubscribeToEvent(settingsDialog.GetChild("CloseButton", true), "Released", "HideEditorSettingsDialog"); - - SubscribeToEvent(cubemapPath, "TextChanged", "EditCubemapPath"); - SubscribeToEvent(cubemapPath, "TextFinished", "EditCubemapPath"); - SubscribeToEvent(cubemapName, "TextChanged", "EditCubemapName"); - SubscribeToEvent(cubemapName, "TextFinished", "EditCubemapName"); - SubscribeToEvent(cubemapSize, "TextChanged", "EditCubemapSize"); - SubscribeToEvent(cubemapSize, "TextFinished", "EditCubemapSize"); - - SubscribeToEvent(defaultTagsEdit, "TextFinished", "EditDefaultTags"); - - subscribedToEditorSettings = true; - } -} - -bool ToggleEditorSettingsDialog() -{ - if (settingsDialog.visible == false) - ShowEditorSettingsDialog(); - else - HideEditorSettingsDialog(); - return true; -} - -void ShowEditorSettingsDialog() -{ - UpdateEditorSettingsDialog(); - settingsDialog.visible = true; - settingsDialog.BringToFront(); -} - -void HideEditorSettingsDialog() -{ - settingsDialog.visible = false; -} - -void EditCameraNearClip(StringHash eventType, VariantMap& eventData) -{ - LineEdit@ edit = eventData["Element"].GetPtr(); - viewNearClip = edit.text.ToFloat(); - UpdateViewParameters(); - if (eventType == StringHash("TextFinished")) - edit.text = String(camera.nearClip); -} - -void EditCameraFarClip(StringHash eventType, VariantMap& eventData) -{ - LineEdit@ edit = eventData["Element"].GetPtr(); - viewFarClip = edit.text.ToFloat(); - UpdateViewParameters(); - if (eventType == StringHash("TextFinished")) - edit.text = String(camera.farClip); -} - -void EditCameraFOV(StringHash eventType, VariantMap& eventData) -{ - LineEdit@ edit = eventData["Element"].GetPtr(); - viewFov = edit.text.ToFloat(); - UpdateViewParameters(); - if (eventType == StringHash("TextFinished")) - edit.text = String(camera.fov); -} - -void EditCameraSpeed(StringHash eventType, VariantMap& eventData) -{ - LineEdit@ edit = eventData["Element"].GetPtr(); - cameraBaseSpeed = Max(edit.text.ToFloat(), 1.0); - if (eventType == StringHash("TextFinished")) - edit.text = String(cameraBaseSpeed); -} - -void EditLimitRotation(StringHash eventType, VariantMap& eventData) -{ - CheckBox@ edit = eventData["Element"].GetPtr(); - limitRotation = edit.checked; -} - -void EditMouseOrbitMode(StringHash eventType, VariantMap& eventData) -{ - DropDownList@ edit = eventData["Element"].GetPtr(); - mouseOrbitMode = edit.selection; -} - -void EditMiddleMousePan(StringHash eventType, VariantMap& eventData) -{ - mmbPanMode = cast(eventData["Element"].GetPtr()).checked; -} - -void EditRotateAroundSelect(StringHash eventType, VariantMap& eventData) -{ - rotateAroundSelect = cast(eventData["Element"].GetPtr()).checked; -} - -void EditHotKeyMode(StringHash eventType, VariantMap& eventData) -{ - DropDownList@ edit = eventData["Element"].GetPtr(); - hotKeyMode = edit.selection; - MessageBox("Please, restart Urho editor for applying changes.\n", " Notify "); - -} - -void EditNewNodeMode(StringHash eventType, VariantMap& eventData) -{ - DropDownList@ edit = eventData["Element"].GetPtr(); - newNodeMode = edit.selection; -} - -void EditMoveStep(StringHash eventType, VariantMap& eventData) -{ - LineEdit@ edit = eventData["Element"].GetPtr(); - moveStep = Max(edit.text.ToFloat(), 0.0); - if (eventType == StringHash("TextFinished")) - edit.text = String(moveStep); -} - -void EditRotateStep(StringHash eventType, VariantMap& eventData) -{ - LineEdit@ edit = eventData["Element"].GetPtr(); - rotateStep = Max(edit.text.ToFloat(), 0.0); - if (eventType == StringHash("TextFinished")) - edit.text = String(rotateStep); -} - -void EditScaleStep(StringHash eventType, VariantMap& eventData) -{ - LineEdit@ edit = eventData["Element"].GetPtr(); - scaleStep = Max(edit.text.ToFloat(), 0.0); - if (eventType == StringHash("TextFinished")) - edit.text = String(scaleStep); -} - -void EditMoveSnap(StringHash eventType, VariantMap& eventData) -{ - CheckBox@ edit = eventData["Element"].GetPtr(); - moveSnap = edit.checked; - toolBarDirty = true; -} - -void EditRotateSnap(StringHash eventType, VariantMap& eventData) -{ - CheckBox@ edit = eventData["Element"].GetPtr(); - rotateSnap = edit.checked; - toolBarDirty = true; -} - -void EditScaleSnap(StringHash eventType, VariantMap& eventData) -{ - CheckBox@ edit = eventData["Element"].GetPtr(); - scaleSnap = edit.checked; - toolBarDirty = true; -} - -void EditRememberResourcePath(StringHash eventType, VariantMap& eventData) -{ - CheckBox@ edit = eventData["Element"].GetPtr(); - rememberResourcePath = edit.checked; -} - -void EditApplyMaterialList(StringHash eventType, VariantMap& eventData) -{ - CheckBox@ edit = eventData["Element"].GetPtr(); - applyMaterialList = edit.checked; -} - -void EditImportOptions(StringHash eventType, VariantMap& eventData) -{ - LineEdit@ edit = eventData["Element"].GetPtr(); - importOptions = edit.text.Trimmed(); -} - -void EditPickMode(StringHash eventType, VariantMap& eventData) -{ - DropDownList@ edit = eventData["Element"].GetPtr(); - pickMode = edit.selection; -} - -void EditRenderPathName(StringHash eventType, VariantMap& eventData) -{ - LineEdit@ edit = eventData["Element"].GetPtr(); - SetRenderPath(edit.text); -} - -void PickRenderPath(StringHash eventType, VariantMap& eventData) -{ - CreateFileSelector("Load render path", "Load", "Cancel", uiRenderPathPath, uiRenderPathFilters, uiRenderPathFilter); - SubscribeToEvent(uiFileSelector, "FileSelected", "HandleLoadRenderPath"); -} - -void HandleLoadRenderPath(StringHash eventType, VariantMap& eventData) -{ - CloseFileSelector(uiRenderPathFilter, uiRenderPathPath); - SetRenderPath(GetResourceNameFromFullName(ExtractFileName(eventData))); - LineEdit@ renderPathNameEdit = settingsDialog.GetChild("RenderPathNameEdit", true); - renderPathNameEdit.text = renderPathName; -} - -void EditTextureQuality(StringHash eventType, VariantMap& eventData) -{ - DropDownList@ edit = eventData["Element"].GetPtr(); - renderer.textureQuality = edit.selection; -} - -void EditMaterialQuality(StringHash eventType, VariantMap& eventData) -{ - DropDownList@ edit = eventData["Element"].GetPtr(); - renderer.materialQuality = edit.selection; -} - -void EditShadowResolution(StringHash eventType, VariantMap& eventData) -{ - DropDownList@ edit = eventData["Element"].GetPtr(); - SetShadowResolution(edit.selection); -} - -void EditShadowQuality(StringHash eventType, VariantMap& eventData) -{ - DropDownList@ edit = eventData["Element"].GetPtr(); - renderer.shadowQuality = ShadowQuality(edit.selection); -} - -void EditMaxOccluderTriangles(StringHash eventType, VariantMap& eventData) -{ - LineEdit@ edit = eventData["Element"].GetPtr(); - renderer.maxOccluderTriangles = edit.text.ToI32(); - if (eventType == StringHash("TextFinished")) - edit.text = String(renderer.maxOccluderTriangles); -} - -void EditSpecularLighting(StringHash eventType, VariantMap& eventData) -{ - CheckBox@ edit = eventData["Element"].GetPtr(); - renderer.specularLighting = edit.checked; -} - -void EditDynamicInstancing(StringHash eventType, VariantMap& eventData) -{ - CheckBox@ edit = eventData["Element"].GetPtr(); - renderer.dynamicInstancing = edit.checked; -} - -void EditFrameLimiter(StringHash eventType, VariantMap& eventData) -{ - CheckBox@ edit = eventData["Element"].GetPtr(); - engine.maxFps = edit.checked ? 200 : 0; -} - -void EditGammaCorrection(StringHash eventType, VariantMap& eventData) -{ - CheckBox@ edit = eventData["Element"].GetPtr(); - SetGammaCorrection(edit.checked); -} - -void EditHDR(StringHash eventType, VariantMap& eventData) -{ - CheckBox@ edit = eventData["Element"].GetPtr(); - SetHDR(edit.checked); -} - -void EditSRGB(StringHash eventType, VariantMap& eventData) -{ - CheckBox@ edit = eventData["Element"].GetPtr(); - graphics.sRGB = edit.checked; -} - -void EditCubemapPath(StringHash eventType, VariantMap& eventData) -{ - LineEdit@ edit = eventData["Element"].GetPtr(); - cubeMapGen_Path = edit.text; -} - -void EditCubemapName(StringHash eventType, VariantMap& eventData) -{ - LineEdit@ edit = eventData["Element"].GetPtr(); - cubeMapGen_Name = edit.text; -} - -void EditCubemapSize(StringHash eventType, VariantMap& eventData) -{ - LineEdit@ edit = eventData["Element"].GetPtr(); - cubeMapGen_Size = edit.text.ToI32(); -} - -void EditDefaultTags(StringHash eventType, VariantMap& eventData) -{ - LineEdit@ edit = eventData["Element"].GetPtr(); - defaultTags = edit.text; -} +// Urho3D editor settings dialog +bool subscribedToEditorSettings = false; +Window@ settingsDialog; +String defaultTags; + +void CreateEditorSettingsDialog() +{ + if (settingsDialog !is null) + return; + + settingsDialog = LoadEditorUI("Editor/UI/EditorSettingsDialog.xml"); + ui.root.AddChild(settingsDialog); + settingsDialog.opacity = uiMaxOpacity; + settingsDialog.height = 440; + CenterDialog(settingsDialog); + UpdateEditorSettingsDialog(); + HideEditorSettingsDialog(); +} + +void UpdateEditorSettingsDialog() +{ + if (settingsDialog is null) + return; + + LineEdit@ nearClipEdit = settingsDialog.GetChild("NearClipEdit", true); + nearClipEdit.text = String(viewNearClip); + + LineEdit@ farClipEdit = settingsDialog.GetChild("FarClipEdit", true); + farClipEdit.text = String(viewFarClip); + + LineEdit@ fovEdit = settingsDialog.GetChild("FOVEdit", true); + fovEdit.text = String(viewFov); + + LineEdit@ speedEdit = settingsDialog.GetChild("SpeedEdit", true); + speedEdit.text = String(cameraBaseSpeed); + + CheckBox@ limitRotationToggle = settingsDialog.GetChild("LimitRotationToggle", true); + limitRotationToggle.checked = limitRotation; + + + DropDownList@ mouseOrbitEdit = settingsDialog.GetChild("MouseOrbitEdit", true); + mouseOrbitEdit.selection = mouseOrbitMode; + + CheckBox@ middleMousePanToggle = settingsDialog.GetChild("MiddleMousePanToggle", true); + middleMousePanToggle.checked = mmbPanMode; + + CheckBox@ rotateAroundSelectToggle = settingsDialog.GetChild("RotateAroundSelectionToggle", true); + rotateAroundSelectToggle.checked = rotateAroundSelect; + + DropDownList@ hotKeysModeEdit = settingsDialog.GetChild("HotKeysModeEdit", true); + hotKeysModeEdit.selection = hotKeyMode; + + DropDownList@ newNodeModeEdit = settingsDialog.GetChild("NewNodeModeEdit", true); + newNodeModeEdit.selection = newNodeMode; + + LineEdit@ moveStepEdit = settingsDialog.GetChild("MoveStepEdit", true); + moveStepEdit.text = String(moveStep); + CheckBox@ moveSnapToggle = settingsDialog.GetChild("MoveSnapToggle", true); + moveSnapToggle.checked = moveSnap; + + LineEdit@ rotateStepEdit = settingsDialog.GetChild("RotateStepEdit", true); + rotateStepEdit.text = String(rotateStep); + CheckBox@ rotateSnapToggle = settingsDialog.GetChild("RotateSnapToggle", true); + rotateSnapToggle.checked = rotateSnap; + + LineEdit@ scaleStepEdit = settingsDialog.GetChild("ScaleStepEdit", true); + scaleStepEdit.text = String(scaleStep); + CheckBox@ scaleSnapToggle = settingsDialog.GetChild("ScaleSnapToggle", true); + scaleSnapToggle.checked = scaleSnap; + + CheckBox@ applyMaterialListToggle = settingsDialog.GetChild("ApplyMaterialListToggle", true); + applyMaterialListToggle.checked = applyMaterialList; + + CheckBox@ rememberResourcePathToggle = settingsDialog.GetChild("RememberResourcePathToggle", true); + rememberResourcePathToggle.checked = rememberResourcePath; + + LineEdit@ importOptionsEdit = settingsDialog.GetChild("ImportOptionsEdit", true); + importOptionsEdit.text = importOptions; + + DropDownList@ pickModeEdit = settingsDialog.GetChild("PickModeEdit", true); + pickModeEdit.selection = pickMode; + + LineEdit@ renderPathNameEdit = settingsDialog.GetChild("RenderPathNameEdit", true); + renderPathNameEdit.text = renderPathName; + + Button@ pickRenderPathButton = settingsDialog.GetChild("PickRenderPathButton", true); + + DropDownList@ textureQualityEdit = settingsDialog.GetChild("TextureQualityEdit", true); + textureQualityEdit.selection = renderer.textureQuality; + + DropDownList@ materialQualityEdit = settingsDialog.GetChild("MaterialQualityEdit", true); + materialQualityEdit.selection = renderer.materialQuality; + + DropDownList@ shadowResolutionEdit = settingsDialog.GetChild("ShadowResolutionEdit", true); + shadowResolutionEdit.selection = GetShadowResolution(); + + DropDownList@ shadowQualityEdit = settingsDialog.GetChild("ShadowQualityEdit", true); + shadowQualityEdit.selection = int(renderer.shadowQuality); + + LineEdit@ maxOccluderTrianglesEdit = settingsDialog.GetChild("MaxOccluderTrianglesEdit", true); + maxOccluderTrianglesEdit.text = String(renderer.maxOccluderTriangles); + + CheckBox@ specularLightingToggle = settingsDialog.GetChild("SpecularLightingToggle", true); + specularLightingToggle.checked = renderer.specularLighting; + + CheckBox@ dynamicInstancingToggle = settingsDialog.GetChild("DynamicInstancingToggle", true); + dynamicInstancingToggle.checked = renderer.dynamicInstancing; + + CheckBox@ frameLimiterToggle = settingsDialog.GetChild("FrameLimiterToggle", true); + frameLimiterToggle.checked = engine.maxFps > 0; + + CheckBox@ gammaCorrectionToggle = settingsDialog.GetChild("GammaCorrectionToggle", true); + gammaCorrectionToggle.checked = gammaCorrection; + + CheckBox@ HDRToggle = settingsDialog.GetChild("HDRToggle", true); + HDRToggle.checked = HDR; + + CheckBox@ sRGBToggle = settingsDialog.GetChild("SRGBToggle", true); + sRGBToggle.checked = graphics.sRGB; + + LineEdit@ cubemapPath = settingsDialog.GetChild("CubeMapGenPath", true); + cubemapPath.text = cubeMapGen_Path; + LineEdit@ cubemapName = settingsDialog.GetChild("CubeMapGenKey", true); + cubemapName.text = cubeMapGen_Name; + LineEdit@ cubemapSize = settingsDialog.GetChild("CubeMapGenSize", true); + cubemapSize.text = String(cubeMapGen_Size); + + LineEdit@ defaultTagsEdit = settingsDialog.GetChild("DefaultTagsEdit", true); + defaultTagsEdit.text = defaultTags.Trimmed(); + + + if (!subscribedToEditorSettings) + { + SubscribeToEvent(nearClipEdit, "TextChanged", "EditCameraNearClip"); + SubscribeToEvent(nearClipEdit, "TextFinished", "EditCameraNearClip"); + SubscribeToEvent(farClipEdit, "TextChanged", "EditCameraFarClip"); + SubscribeToEvent(farClipEdit, "TextFinished", "EditCameraFarClip"); + SubscribeToEvent(fovEdit, "TextChanged", "EditCameraFOV"); + SubscribeToEvent(fovEdit, "TextFinished", "EditCameraFOV"); + SubscribeToEvent(speedEdit, "TextChanged", "EditCameraSpeed"); + SubscribeToEvent(speedEdit, "TextFinished", "EditCameraSpeed"); + SubscribeToEvent(limitRotationToggle, "Toggled", "EditLimitRotation"); + SubscribeToEvent(middleMousePanToggle, "Toggled", "EditMiddleMousePan"); + SubscribeToEvent(rotateAroundSelectToggle, "Toggled", "EditRotateAroundSelect"); + SubscribeToEvent(mouseOrbitEdit, "ItemSelected", "EditMouseOrbitMode"); + SubscribeToEvent(hotKeysModeEdit, "ItemSelected", "EditHotKeyMode"); + SubscribeToEvent(newNodeModeEdit, "ItemSelected", "EditNewNodeMode"); + SubscribeToEvent(moveStepEdit, "TextChanged", "EditMoveStep"); + SubscribeToEvent(moveStepEdit, "TextFinished", "EditMoveStep"); + SubscribeToEvent(rotateStepEdit, "TextChanged", "EditRotateStep"); + SubscribeToEvent(rotateStepEdit, "TextFinished", "EditRotateStep"); + SubscribeToEvent(scaleStepEdit, "TextChanged", "EditScaleStep"); + SubscribeToEvent(scaleStepEdit, "TextFinished", "EditScaleStep"); + SubscribeToEvent(moveSnapToggle, "Toggled", "EditMoveSnap"); + SubscribeToEvent(rotateSnapToggle, "Toggled", "EditRotateSnap"); + SubscribeToEvent(scaleSnapToggle, "Toggled", "EditScaleSnap"); + SubscribeToEvent(rememberResourcePathToggle, "Toggled", "EditRememberResourcePath"); + SubscribeToEvent(applyMaterialListToggle, "Toggled", "EditApplyMaterialList"); + SubscribeToEvent(importOptionsEdit, "TextChanged", "EditImportOptions"); + SubscribeToEvent(importOptionsEdit, "TextFinished", "EditImportOptions"); + SubscribeToEvent(pickModeEdit, "ItemSelected", "EditPickMode"); + SubscribeToEvent(renderPathNameEdit, "TextFinished", "EditRenderPathName"); + SubscribeToEvent(pickRenderPathButton, "Released", "PickRenderPath"); + SubscribeToEvent(textureQualityEdit, "ItemSelected", "EditTextureQuality"); + SubscribeToEvent(materialQualityEdit, "ItemSelected", "EditMaterialQuality"); + SubscribeToEvent(shadowResolutionEdit, "ItemSelected", "EditShadowResolution"); + SubscribeToEvent(shadowQualityEdit, "ItemSelected", "EditShadowQuality"); + SubscribeToEvent(maxOccluderTrianglesEdit, "TextChanged", "EditMaxOccluderTriangles"); + SubscribeToEvent(maxOccluderTrianglesEdit, "TextFinished", "EditMaxOccluderTriangles"); + SubscribeToEvent(specularLightingToggle, "Toggled", "EditSpecularLighting"); + SubscribeToEvent(dynamicInstancingToggle, "Toggled", "EditDynamicInstancing"); + SubscribeToEvent(frameLimiterToggle, "Toggled", "EditFrameLimiter"); + SubscribeToEvent(gammaCorrectionToggle, "Toggled", "EditGammaCorrection"); + SubscribeToEvent(HDRToggle, "Toggled", "EditHDR"); + SubscribeToEvent(sRGBToggle, "Toggled", "EditSRGB"); + SubscribeToEvent(settingsDialog.GetChild("CloseButton", true), "Released", "HideEditorSettingsDialog"); + + SubscribeToEvent(cubemapPath, "TextChanged", "EditCubemapPath"); + SubscribeToEvent(cubemapPath, "TextFinished", "EditCubemapPath"); + SubscribeToEvent(cubemapName, "TextChanged", "EditCubemapName"); + SubscribeToEvent(cubemapName, "TextFinished", "EditCubemapName"); + SubscribeToEvent(cubemapSize, "TextChanged", "EditCubemapSize"); + SubscribeToEvent(cubemapSize, "TextFinished", "EditCubemapSize"); + + SubscribeToEvent(defaultTagsEdit, "TextFinished", "EditDefaultTags"); + + subscribedToEditorSettings = true; + } +} + +bool ToggleEditorSettingsDialog() +{ + if (settingsDialog.visible == false) + ShowEditorSettingsDialog(); + else + HideEditorSettingsDialog(); + return true; +} + +void ShowEditorSettingsDialog() +{ + UpdateEditorSettingsDialog(); + settingsDialog.visible = true; + settingsDialog.BringToFront(); +} + +void HideEditorSettingsDialog() +{ + settingsDialog.visible = false; +} + +void EditCameraNearClip(StringHash eventType, VariantMap& eventData) +{ + LineEdit@ edit = eventData["Element"].GetPtr(); + viewNearClip = edit.text.ToFloat(); + UpdateViewParameters(); + if (eventType == StringHash("TextFinished")) + edit.text = String(camera.nearClip); +} + +void EditCameraFarClip(StringHash eventType, VariantMap& eventData) +{ + LineEdit@ edit = eventData["Element"].GetPtr(); + viewFarClip = edit.text.ToFloat(); + UpdateViewParameters(); + if (eventType == StringHash("TextFinished")) + edit.text = String(camera.farClip); +} + +void EditCameraFOV(StringHash eventType, VariantMap& eventData) +{ + LineEdit@ edit = eventData["Element"].GetPtr(); + viewFov = edit.text.ToFloat(); + UpdateViewParameters(); + if (eventType == StringHash("TextFinished")) + edit.text = String(camera.fov); +} + +void EditCameraSpeed(StringHash eventType, VariantMap& eventData) +{ + LineEdit@ edit = eventData["Element"].GetPtr(); + cameraBaseSpeed = Max(edit.text.ToFloat(), 1.0); + if (eventType == StringHash("TextFinished")) + edit.text = String(cameraBaseSpeed); +} + +void EditLimitRotation(StringHash eventType, VariantMap& eventData) +{ + CheckBox@ edit = eventData["Element"].GetPtr(); + limitRotation = edit.checked; +} + +void EditMouseOrbitMode(StringHash eventType, VariantMap& eventData) +{ + DropDownList@ edit = eventData["Element"].GetPtr(); + mouseOrbitMode = edit.selection; +} + +void EditMiddleMousePan(StringHash eventType, VariantMap& eventData) +{ + mmbPanMode = cast(eventData["Element"].GetPtr()).checked; +} + +void EditRotateAroundSelect(StringHash eventType, VariantMap& eventData) +{ + rotateAroundSelect = cast(eventData["Element"].GetPtr()).checked; +} + +void EditHotKeyMode(StringHash eventType, VariantMap& eventData) +{ + DropDownList@ edit = eventData["Element"].GetPtr(); + hotKeyMode = edit.selection; + MessageBox("Please, restart Urho editor for applying changes.\n", " Notify "); + +} + +void EditNewNodeMode(StringHash eventType, VariantMap& eventData) +{ + DropDownList@ edit = eventData["Element"].GetPtr(); + newNodeMode = edit.selection; +} + +void EditMoveStep(StringHash eventType, VariantMap& eventData) +{ + LineEdit@ edit = eventData["Element"].GetPtr(); + moveStep = Max(edit.text.ToFloat(), 0.0); + if (eventType == StringHash("TextFinished")) + edit.text = String(moveStep); +} + +void EditRotateStep(StringHash eventType, VariantMap& eventData) +{ + LineEdit@ edit = eventData["Element"].GetPtr(); + rotateStep = Max(edit.text.ToFloat(), 0.0); + if (eventType == StringHash("TextFinished")) + edit.text = String(rotateStep); +} + +void EditScaleStep(StringHash eventType, VariantMap& eventData) +{ + LineEdit@ edit = eventData["Element"].GetPtr(); + scaleStep = Max(edit.text.ToFloat(), 0.0); + if (eventType == StringHash("TextFinished")) + edit.text = String(scaleStep); +} + +void EditMoveSnap(StringHash eventType, VariantMap& eventData) +{ + CheckBox@ edit = eventData["Element"].GetPtr(); + moveSnap = edit.checked; + toolBarDirty = true; +} + +void EditRotateSnap(StringHash eventType, VariantMap& eventData) +{ + CheckBox@ edit = eventData["Element"].GetPtr(); + rotateSnap = edit.checked; + toolBarDirty = true; +} + +void EditScaleSnap(StringHash eventType, VariantMap& eventData) +{ + CheckBox@ edit = eventData["Element"].GetPtr(); + scaleSnap = edit.checked; + toolBarDirty = true; +} + +void EditRememberResourcePath(StringHash eventType, VariantMap& eventData) +{ + CheckBox@ edit = eventData["Element"].GetPtr(); + rememberResourcePath = edit.checked; +} + +void EditApplyMaterialList(StringHash eventType, VariantMap& eventData) +{ + CheckBox@ edit = eventData["Element"].GetPtr(); + applyMaterialList = edit.checked; +} + +void EditImportOptions(StringHash eventType, VariantMap& eventData) +{ + LineEdit@ edit = eventData["Element"].GetPtr(); + importOptions = edit.text.Trimmed(); +} + +void EditPickMode(StringHash eventType, VariantMap& eventData) +{ + DropDownList@ edit = eventData["Element"].GetPtr(); + pickMode = edit.selection; +} + +void EditRenderPathName(StringHash eventType, VariantMap& eventData) +{ + LineEdit@ edit = eventData["Element"].GetPtr(); + SetRenderPath(edit.text); +} + +void PickRenderPath(StringHash eventType, VariantMap& eventData) +{ + CreateFileSelector("Load render path", "Load", "Cancel", uiRenderPathPath, uiRenderPathFilters, uiRenderPathFilter); + SubscribeToEvent(uiFileSelector, "FileSelected", "HandleLoadRenderPath"); +} + +void HandleLoadRenderPath(StringHash eventType, VariantMap& eventData) +{ + CloseFileSelector(uiRenderPathFilter, uiRenderPathPath); + SetRenderPath(GetResourceNameFromFullName(ExtractFileName(eventData))); + LineEdit@ renderPathNameEdit = settingsDialog.GetChild("RenderPathNameEdit", true); + renderPathNameEdit.text = renderPathName; +} + +void EditTextureQuality(StringHash eventType, VariantMap& eventData) +{ + DropDownList@ edit = eventData["Element"].GetPtr(); + renderer.textureQuality = edit.selection; +} + +void EditMaterialQuality(StringHash eventType, VariantMap& eventData) +{ + DropDownList@ edit = eventData["Element"].GetPtr(); + renderer.materialQuality = edit.selection; +} + +void EditShadowResolution(StringHash eventType, VariantMap& eventData) +{ + DropDownList@ edit = eventData["Element"].GetPtr(); + SetShadowResolution(edit.selection); +} + +void EditShadowQuality(StringHash eventType, VariantMap& eventData) +{ + DropDownList@ edit = eventData["Element"].GetPtr(); + renderer.shadowQuality = ShadowQuality(edit.selection); +} + +void EditMaxOccluderTriangles(StringHash eventType, VariantMap& eventData) +{ + LineEdit@ edit = eventData["Element"].GetPtr(); + renderer.maxOccluderTriangles = edit.text.ToI32(); + if (eventType == StringHash("TextFinished")) + edit.text = String(renderer.maxOccluderTriangles); +} + +void EditSpecularLighting(StringHash eventType, VariantMap& eventData) +{ + CheckBox@ edit = eventData["Element"].GetPtr(); + renderer.specularLighting = edit.checked; +} + +void EditDynamicInstancing(StringHash eventType, VariantMap& eventData) +{ + CheckBox@ edit = eventData["Element"].GetPtr(); + renderer.dynamicInstancing = edit.checked; +} + +void EditFrameLimiter(StringHash eventType, VariantMap& eventData) +{ + CheckBox@ edit = eventData["Element"].GetPtr(); + engine.maxFps = edit.checked ? 200 : 0; +} + +void EditGammaCorrection(StringHash eventType, VariantMap& eventData) +{ + CheckBox@ edit = eventData["Element"].GetPtr(); + SetGammaCorrection(edit.checked); +} + +void EditHDR(StringHash eventType, VariantMap& eventData) +{ + CheckBox@ edit = eventData["Element"].GetPtr(); + SetHDR(edit.checked); +} + +void EditSRGB(StringHash eventType, VariantMap& eventData) +{ + CheckBox@ edit = eventData["Element"].GetPtr(); + graphics.sRGB = edit.checked; +} + +void EditCubemapPath(StringHash eventType, VariantMap& eventData) +{ + LineEdit@ edit = eventData["Element"].GetPtr(); + cubeMapGen_Path = edit.text; +} + +void EditCubemapName(StringHash eventType, VariantMap& eventData) +{ + LineEdit@ edit = eventData["Element"].GetPtr(); + cubeMapGen_Name = edit.text; +} + +void EditCubemapSize(StringHash eventType, VariantMap& eventData) +{ + LineEdit@ edit = eventData["Element"].GetPtr(); + cubeMapGen_Size = edit.text.ToI32(); +} + +void EditDefaultTags(StringHash eventType, VariantMap& eventData) +{ + LineEdit@ edit = eventData["Element"].GetPtr(); + defaultTags = edit.text; +} diff --git a/bin/Data/Scripts/Editor/EditorSoundType.as b/bin/EditorData/Editor/Scripts/EditorSoundType.as similarity index 94% rename from bin/Data/Scripts/Editor/EditorSoundType.as rename to bin/EditorData/Editor/Scripts/EditorSoundType.as index 7789be5bd10..17246fe8259 100644 --- a/bin/Data/Scripts/Editor/EditorSoundType.as +++ b/bin/EditorData/Editor/Scripts/EditorSoundType.as @@ -13,13 +13,13 @@ class SoundTypeMapping SoundTypeMapping() { } - + SoundTypeMapping(const String&in key, const float&in value) { this.key = key; this.value = Clamp(value, 0.0f, 1.0f); } - + void Update(float value) { this.value = Clamp(value, 0.0f, 1.0f); @@ -31,23 +31,23 @@ void CreateSoundTypeEditor() { if (soundTypeEditorWindow !is null) return; - - soundTypeEditorWindow = ui.LoadLayout(cache.GetResource("XMLFile", "UI/EditorSoundTypeWindow.xml")); + + soundTypeEditorWindow = ui.LoadLayout(cache.GetResource("XMLFile", "Editor/UI/EditorSoundTypeWindow.xml")); ui.root.AddChild(soundTypeEditorWindow); soundTypeEditorWindow.opacity = uiMaxOpacity; InitSoundTypeEditorWindow(); RefreshSoundTypeEditorWindow(); - int height = Min(ui.root.height - 60, 750); + int height = Min(ui.root.height - 60, 750); soundTypeEditorWindow.SetSize(400, 0); CenterDialog(soundTypeEditorWindow); HideSoundTypeEditor(); - + SubscribeToEvent(soundTypeEditorWindow.GetChild("CloseButton", true), "Released", "HideSoundTypeEditor"); SubscribeToEvent(soundTypeEditorWindow.GetChild("AddButton", true), "Released", "AddSoundTypeMapping"); - + SubscribeToEvent(soundTypeEditorWindow.GetChild("MasterValue", true), "TextFinished", "EditGain"); } @@ -55,15 +55,15 @@ void InitSoundTypeEditorWindow() { if (!mappings.Exists(SOUND_MASTER)) mappings[SOUND_MASTER] = SoundTypeMapping(SOUND_MASTER, audio.masterGain[SOUND_MASTER]); - + for (uint i = DEFAULT_SOUND_TYPES_COUNT; i < mappings.length; i++) { - String key = mappings.keys[i]; + String key = mappings.keys[i]; SoundTypeMapping@ mapping; if (mappings.Get(key, @mapping)) AddUserUIElements(key, mapping.value); - } + } } void RefreshSoundTypeEditorWindow() @@ -81,7 +81,7 @@ void RefreshUser(UIElement@ root) { for (uint i = DEFAULT_SOUND_TYPES_COUNT; i < mappings.length; i++) { - String key = mappings.keys[i]; + String key = mappings.keys[i]; UpdateMappingValue(key, root.GetChild(key, true)); } } @@ -92,7 +92,7 @@ void UpdateMappingValue(const String&in key, UIElement@ root) { LineEdit@ value = root.GetChild(key + "Value"); SoundTypeMapping@ mapping; - + if (mappings.Get(key, @mapping) && value !is null) { value.text = mapping.value; @@ -143,18 +143,18 @@ void AddSoundTypeMapping(StringHash eventType, VariantMap& eventData) UIElement@ button = eventData["Element"].GetPtr(); LineEdit@ key = button.parent.GetChild("Key"); LineEdit@ gain = button.parent.GetChild("Gain"); - + if (!key.text.empty && !gain.text.empty && !mappings.Exists(key.text)) { SoundTypeMapping@ mapping = SoundTypeMapping(key.text, gain.text.ToFloat()); - + mappings[key.text] = mapping; AddUserUIElements(key.text, mapping.value); } - + key.text = ""; gain.text = ""; - + RefreshSoundTypeEditorWindow(); } @@ -162,7 +162,7 @@ void DeleteSoundTypeMapping(StringHash eventType, VariantMap& eventData) { UIElement@ button = eventData["Element"].GetPtr(); UIElement@ parent = button.parent; - + mappings.Erase(parent.name); parent.Remove(); } @@ -171,12 +171,12 @@ void EditGain(StringHash eventType, VariantMap& eventData) { LineEdit@ input = eventData["Element"].GetPtr(); String key = input.parent.name; - + SoundTypeMapping@ mapping; - + if (mappings.Get(key, @mapping)) mapping.Update(input.text.ToFloat()); - + RefreshSoundTypeEditorWindow(); } @@ -205,7 +205,7 @@ void SaveSoundTypes(XMLElement&in root) { for (uint i = 0; i < mappings.length; i++) { - String key = mappings.keys[i]; + String key = mappings.keys[i]; SoundTypeMapping@ mapping; if (mappings.Get(key, @mapping)) @@ -219,7 +219,7 @@ void LoadSoundTypes(const XMLElement&in root) { String key = root.GetAttributeNames()[i]; float gain = root.GetFloat(key); - + if (!key.empty && !mappings.Exists(key)) mappings[key] = SoundTypeMapping(key, gain); } diff --git a/bin/Data/Scripts/Editor/EditorSpawn.as b/bin/EditorData/Editor/Scripts/EditorSpawn.as similarity index 99% rename from bin/Data/Scripts/Editor/EditorSpawn.as rename to bin/EditorData/Editor/Scripts/EditorSpawn.as index d434abb57d8..039750d53c4 100644 --- a/bin/Data/Scripts/Editor/EditorSpawn.as +++ b/bin/EditorData/Editor/Scripts/EditorSpawn.as @@ -31,7 +31,7 @@ void CreateSpawnEditor() if (spawnWindow !is null) return; - spawnWindow = LoadEditorUI("UI/EditorSpawnWindow.xml"); + spawnWindow = LoadEditorUI("Editor/UI/EditorSpawnWindow.xml"); ui.root.AddChild(spawnWindow); spawnWindow.opacity = uiMaxOpacity; diff --git a/bin/Data/Scripts/Editor/EditorTerrain.as b/bin/EditorData/Editor/Scripts/EditorTerrain.as similarity index 99% rename from bin/Data/Scripts/Editor/EditorTerrain.as rename to bin/EditorData/Editor/Scripts/EditorTerrain.as index ab55f562b2a..2d2cb295f73 100644 --- a/bin/Data/Scripts/Editor/EditorTerrain.as +++ b/bin/EditorData/Editor/Scripts/EditorTerrain.as @@ -88,7 +88,7 @@ class TerrainEditor if (window !is null) return; - window = LoadEditorUI("UI/EditorTerrainWindow.xml"); + window = LoadEditorUI("Editor/UI/EditorTerrainWindow.xml"); ui.root.AddChild(window); window.opacity = uiMaxOpacity; @@ -156,7 +156,7 @@ class TerrainEditor checkbox.enabled = true; } } - + // Hide the window void Hide() { @@ -417,7 +417,7 @@ class TerrainEditor private void LoadBrushes() { ListView@ terrainBrushes = window.GetChild("BrushesContainer", true); - String brushPath = "Textures/Editor/TerrainBrushes/"; + String brushPath = "Editor/Textures/TerrainBrushes/"; Array@ resourceDirs = cache.resourceDirs; String brushesFileLocation; diff --git a/bin/Data/Scripts/Editor/EditorToolBar.as b/bin/EditorData/Editor/Scripts/EditorToolBar.as similarity index 100% rename from bin/Data/Scripts/Editor/EditorToolBar.as rename to bin/EditorData/Editor/Scripts/EditorToolBar.as diff --git a/bin/Data/Scripts/Editor/EditorUI.as b/bin/EditorData/Editor/Scripts/EditorUI.as similarity index 96% rename from bin/Data/Scripts/Editor/EditorUI.as rename to bin/EditorData/Editor/Scripts/EditorUI.as index 170c14dbea1..36849bf2ece 100644 --- a/bin/Data/Scripts/Editor/EditorUI.as +++ b/bin/EditorData/Editor/Scripts/EditorUI.as @@ -1,2268 +1,2268 @@ -// Urho3D editor user interface - -XMLFile@ uiStyle; -XMLFile@ iconStyle; -UIElement@ uiMenuBar; -UIElement@ quickMenu; -Menu@ recentSceneMenu; -Window@ mruScenesPopup; -Array quickMenuItems; -FileSelector@ uiFileSelector; -String consoleCommandInterpreter; -Window@ contextMenu; -uint stepColoringGroupUpdate = 100; // ms -uint timeToNextColoringGroupUpdate = 0; - -const StringHash UI_ELEMENT_TYPE("UIElement"); -const StringHash WINDOW_TYPE("Window"); -const StringHash MENU_TYPE("Menu"); -const StringHash TEXT_TYPE("Text"); -const StringHash CURSOR_TYPE("Cursor"); - -const String AUTO_STYLE(""); // Empty string means auto style, i.e. applying style according to UI-element's type automatically -const String TEMP_SCENE_NAME("_tempscene_.xml"); -const String TEMP_BINARY_SCENE_NAME("_tempscene_.bin"); -const StringHash CALLBACK_VAR("Callback"); -const StringHash INDENT_MODIFIED_BY_ICON_VAR("IconIndented"); - -const StringHash VAR_CONTEXT_MENU_HANDLER("ContextMenuHandler"); - -const int SHOW_POPUP_INDICATOR = -1; -const uint MAX_QUICK_MENU_ITEMS = 10; - -const uint maxRecentSceneCount = 5; - -Array uiSceneFilters = {"*.xml", "*.json", "*.bin", "*.*"}; -Array uiElementFilters = {"*.xml"}; -Array uiAllFilters = {"*.*"}; -Array uiScriptFilters = {"*.as", "*.*"}; -Array uiParticleFilters = {"*.xml"}; -Array uiRenderPathFilters = {"*.xml"}; -Array uiExportPathFilters = {"*.obj"}; -uint uiSceneFilter = 0; -uint uiElementFilter = 0; -uint uiNodeFilter = 0; -uint uiImportFilter = 0; -uint uiScriptFilter = 0; -uint uiParticleFilter = 0; -uint uiRenderPathFilter = 0; -uint uiExportFilter = 0; -String uiScenePath = fileSystem.programDir + "Data/Scenes"; -String uiElementPath = fileSystem.programDir + "Data/UI"; -String uiNodePath = fileSystem.programDir + "Data/Objects"; -String uiImportPath; -String uiExportPath; -String uiScriptPath = fileSystem.programDir + "Data/Scripts"; -String uiParticlePath = fileSystem.programDir + "Data/Particles"; -String uiRenderPathPath = fileSystem.programDir + "CoreData/RenderPaths"; -Array uiRecentScenes; -String screenshotDir = fileSystem.programDir + "Screenshots"; - -bool uiFaded = false; -float uiMinOpacity = 0.3; -float uiMaxOpacity = 0.7; -bool uiHidden = false; - -TerrainEditor terrainEditor; - -void CreateUI() -{ - // Remove all existing UI content in case we are reloading the editor script - /// \todo The console will not be properly recreated as it has already been created once - ui.root.RemoveAllChildren(); - - uiStyle = GetEditorUIXMLFile("UI/DefaultStyle.xml"); - ui.root.defaultStyle = uiStyle; - iconStyle = GetEditorUIXMLFile("UI/EditorIcons.xml"); - - graphics.windowIcon = cache.GetResource("Image", "Textures/UrhoIcon.png"); - - CreateCursor(); - CreateMenuBar(); - CreateToolBar(); - CreateSecondaryToolBar(); - CreateQuickMenu(); - CreateContextMenu(); - CreateHierarchyWindow(); - CreateAttributeInspectorWindow(); - CreateEditorSettingsDialog(); - CreateEditorPreferencesDialog(); - CreateMaterialEditor(); - CreateParticleEffectEditor(); - CreateSpawnEditor(); - CreateSoundTypeEditor(); - CreateStatsBar(); - CreateConsole(); - CreateDebugHud(); - CreateResourceBrowser(); - CreateCamera(); - CreateLayerEditor(); - CreateColorWheel(); - CreateDuplicatorEditor(); - - terrainEditor.Create(); - - SubscribeToEvent("ScreenMode", "ResizeUI"); - SubscribeToEvent("MenuSelected", "HandleMenuSelected"); - SubscribeToEvent("ChangeLanguage", "HandleChangeLanguage"); - - SubscribeToEvent("WheelChangeColor", "HandleWheelChangeColor"); - SubscribeToEvent("WheelSelectColor", "HandleWheelSelectColor"); - SubscribeToEvent("WheelDiscardColor", "HandleWheelDiscardColor"); -} - -void ResizeUI() -{ - // Resize menu bar - uiMenuBar.SetFixedWidth(graphics.width / ui.scale); - - // Resize tool bar - toolBar.SetFixedWidth(graphics.width / ui.scale); - - // Resize secondary tool bar - secondaryToolBar.SetFixedHeight(graphics.height / ui.scale); - - // Relayout windows - Array children = ui.root.GetChildren(); - for (uint i = 0; i < children.length; ++i) - { - if (children[i].type == WINDOW_TYPE) - AdjustPosition(children[i]); - } - - // Relayout root UI element - editorUIElement.SetSize(graphics.width, graphics.height); - - // Set new viewport area and reset the viewport layout. Note: viewportArea is in scaled UI position. - viewportArea = IntRect(0, 0, graphics.width / ui.scale, graphics.height / ui.scale); - SetViewportMode(viewportMode); -} - -void AdjustPosition(Window@ window) -{ - IntVector2 position = window.position; - IntVector2 size = window.size; - IntVector2 extend = position + size; - if (extend.x > graphics.width) - position.x = Max(10, graphics.width - size.x - 10); - if (extend.y > graphics.height) - position.y = Max(100, graphics.height - size.y - 10); - window.position = position; -} - -void CreateCursor() -{ - Cursor@ cursor = Cursor("Cursor"); - cursor.SetStyleAuto(uiStyle); - cursor.SetPosition(graphics.width / 2, graphics.height / 2); - ui.cursor = cursor; - if (GetPlatform() == "Android" || GetPlatform() == "iOS") - ui.cursor.visible = false; -} - -// AngelScript does not support closures (yet), but funcdef should do just fine as a workaround for a few cases here for now -funcdef bool MENU_CALLBACK(); -Array menuCallbacks; -MENU_CALLBACK@ messageBoxCallback; - -void HandleQuickSearchChange(StringHash eventType, VariantMap& eventData) -{ - LineEdit@ search = eventData["Element"].GetPtr(); - if (search is null) - return; - - PerformQuickMenuSearch(search.text.ToLower().Trimmed()); -} - -void HandleQuickSearchFinish(StringHash eventType, VariantMap& eventData) -{ - Menu@ menu = quickMenu.GetChild("ResultsMenu", true); - if (menu is null) - return; - - String query = eventData["Text"].GetString(); - if (query.length <= 0) - return; - Array filtered; - { - QuickMenuItem@ qi; - for (uint x=0; x < quickMenuItems.length; x++) - { - @qi = quickMenuItems[x]; - int find = qi.action.Find(query, 0, false); - if (find > -1) - { - qi.sortScore = find; - filtered.Push(qi); - } - } - } - - filtered.Sort(); - if (!filtered.empty) - { - VariantMap data; - Menu@ item = CreateMenuItem(filtered[0].action, filtered[0].callback); - data["Element"] = item; - item.SendEvent("MenuSelected", data); - } -} - -void PerformQuickMenuSearch(const String&in query) -{ - Menu@ menu = quickMenu.GetChild("ResultsMenu", true); - if (menu is null) - return; - - menu.RemoveAllChildren(); - uint limit = 0; - - if (query.length > 0) - { - int lastIndex = 0; - uint score = 0; - int index = 0; - - Array filtered; - { - QuickMenuItem@ qi; - for (uint x=0; x < quickMenuItems.length; x++) - { - @qi = quickMenuItems[x]; - int find = qi.action.Find(query, 0, false); - if (find > -1) - { - qi.sortScore = find; - filtered.Push(qi); - } - } - } - - filtered.Sort(); - - { - QuickMenuItem@ qi; - limit = filtered.length > MAX_QUICK_MENU_ITEMS ? MAX_QUICK_MENU_ITEMS : filtered.length; - for (uint x=0; x < limit; x++) - { - @qi = filtered[x]; - Menu@ item = CreateMenuItem(qi.action, qi.callback); - item.SetMaxSize(1000,16); - menu.AddChild(item); - } - } - } - - menu.visible = limit > 0; - menu.SetFixedHeight(limit * 16); - quickMenu.BringToFront(); - quickMenu.SetFixedHeight(limit*16 + 62 + (menu.visible ? 6 : 0)); -} - -class QuickMenuItem -{ - String action; - MENU_CALLBACK@ callback; - uint sortScore = 0; - QuickMenuItem(){} - QuickMenuItem(String action, MENU_CALLBACK@ callback) - { - this.action = action; - this.callback = callback; - } - int opCmp(QuickMenuItem@ b) - { - return sortScore - b.sortScore; - } -} - -/// Create popup search menu. -void CreateQuickMenu() -{ - if (quickMenu !is null) - return; - - quickMenu = LoadEditorUI("UI/EditorQuickMenu.xml"); - quickMenu.enabled = false; - quickMenu.visible = false; - quickMenu.opacity = uiMaxOpacity; - - // Handle a dummy search in the quick menu to finalize its initial size to empty - PerformQuickMenuSearch(""); - - ui.root.AddChild(quickMenu); - LineEdit@ search = quickMenu.GetChild("Search", true); - SubscribeToEvent(search, "TextChanged", "HandleQuickSearchChange"); - SubscribeToEvent(search, "TextFinished", "HandleQuickSearchFinish"); - UIElement@ closeButton = quickMenu.GetChild("CloseButton", true); - SubscribeToEvent(closeButton, "Pressed", "ToggleQuickMenu"); -} - -void ToggleQuickMenu() -{ - quickMenu.enabled = !quickMenu.enabled && ui.cursor.visible; - quickMenu.visible = quickMenu.enabled; - if (quickMenu.enabled) - { - quickMenu.position = ui.cursorPosition - IntVector2(20,70); - LineEdit@ search = quickMenu.GetChild("Search", true); - search.text = ""; - search.focus = true; - } -} - -/// Create top menu bar. -void CreateMenuBar() -{ - uiMenuBar = BorderImage("MenuBar"); - ui.root.AddChild(uiMenuBar); - uiMenuBar.enabled = true; - uiMenuBar.style = "EditorMenuBar"; - uiMenuBar.SetLayout(LM_HORIZONTAL); - uiMenuBar.opacity = uiMaxOpacity; - uiMenuBar.SetFixedWidth(graphics.width / ui.scale); - - { - Menu@ menu = CreateMenu("File"); - Window@ popup = menu.popup; - popup.AddChild(CreateMenuItem("New scene", @ResetScene, KEY_N, QUAL_SHIFT | QUAL_CTRL)); - popup.AddChild(CreateMenuItem("Open scene...", @PickFile, KEY_O, QUAL_CTRL)); - popup.AddChild(CreateMenuItem("Save scene", @SaveSceneWithExistingName, KEY_S, QUAL_CTRL)); - popup.AddChild(CreateMenuItem("Save scene as...", @PickFile, KEY_S, QUAL_SHIFT | QUAL_CTRL)); - recentSceneMenu = CreateMenuItem("Open recent scene", null, SHOW_POPUP_INDICATOR); - popup.AddChild(recentSceneMenu); - mruScenesPopup = CreatePopup(recentSceneMenu); - PopulateMruScenes(); - CreateChildDivider(popup); - - Menu@ childMenu = CreateMenuItem("menu Load node", null, SHOW_POPUP_INDICATOR); - Window@ childPopup = CreatePopup(childMenu); - childPopup.AddChild(CreateMenuItem("As replicated...", @PickFile, 0, 0, true, "Load node as replicated...")); - childPopup.AddChild(CreateMenuItem("As local...", @PickFile, 0, 0, true, "Load node as local...")); - popup.AddChild(childMenu); - - popup.AddChild(CreateMenuItem("Save node as...", @PickFile)); - CreateChildDivider(popup); - popup.AddChild(CreateMenuItem("Import model...", @PickFile)); - popup.AddChild(CreateMenuItem("Import scene...", @PickFile)); - popup.AddChild(CreateMenuItem("Import animation...", @PickFile)); - CreateChildDivider(popup); - popup.AddChild(CreateMenuItem("Export scene to OBJ...", @PickFile)); - popup.AddChild(CreateMenuItem("Export selected to OBJ...", @PickFile)); - CreateChildDivider(popup); - popup.AddChild(CreateMenuItem("Run script...", @PickFile)); - popup.AddChild(CreateMenuItem("Set resource path...", @PickFile)); - CreateChildDivider(popup); - popup.AddChild(CreateMenuItem("Exit", @Exit)); - FinalizedPopupMenu(popup); - uiMenuBar.AddChild(menu); - } - - { - Menu@ menu = CreateMenu("Edit"); - Window@ popup = menu.popup; - popup.AddChild(CreateMenuItem("Undo", @Undo, KEY_Z, QUAL_CTRL)); - popup.AddChild(CreateMenuItem("Redo", @Redo, KEY_Y, QUAL_CTRL)); - CreateChildDivider(popup); - popup.AddChild(CreateMenuItem("Cut", @Cut, KEY_X, QUAL_CTRL)); - - if (hotKeyMode == HOTKEYS_MODE_STANDARD) - popup.AddChild(CreateMenuItem("Duplicate", @Duplicate, KEY_D, QUAL_CTRL)); - else if (hotKeyMode == HOTKEYS_MODE_BLENDER) - popup.AddChild(CreateMenuItem("Duplicate", @Duplicate, KEY_D, QUAL_SHIFT)); - - popup.AddChild(CreateMenuItem("Copy", @Copy, KEY_C, QUAL_CTRL)); - popup.AddChild(CreateMenuItem("Paste", @Paste, KEY_V, QUAL_CTRL)); - - if (hotKeyMode == HOTKEYS_MODE_STANDARD) - popup.AddChild(CreateMenuItem("Delete", @Delete, KEY_DELETE, QUAL_ANY)); - else if (hotKeyMode == HOTKEYS_MODE_BLENDER) - popup.AddChild(CreateMenuItem("Delete", @BlenderModeDelete, KEY_X, QUAL_ANY)); - - popup.AddChild(CreateMenuItem("Select all", @SelectAll, KEY_A, QUAL_CTRL)); - popup.AddChild(CreateMenuItem("Deselect all", @DeselectAll, KEY_A, QUAL_SHIFT | QUAL_CTRL)); - - CreateChildDivider(popup); - popup.AddChild(CreateMenuItem("Reset to default", @ResetToDefault)); - CreateChildDivider(popup); - - if (hotKeyMode == HOTKEYS_MODE_STANDARD) - { - popup.AddChild(CreateMenuItem("Reset position", @SceneResetPosition, '1' , QUAL_ALT)); - popup.AddChild(CreateMenuItem("Reset rotation", @SceneResetRotation, '2' , QUAL_ALT)); - popup.AddChild(CreateMenuItem("Reset scale", @SceneResetScale, '3' , QUAL_ALT)); - popup.AddChild(CreateMenuItem("Reset transform", @SceneResetTransform, KEY_Q , QUAL_ALT)); - } - else if (hotKeyMode == HOTKEYS_MODE_BLENDER) - { - popup.AddChild(CreateMenuItem("Reset position", @SceneResetPosition, KEY_G , QUAL_ALT)); - popup.AddChild(CreateMenuItem("Reset rotation", @SceneResetRotation, KEY_R, QUAL_ALT)); - popup.AddChild(CreateMenuItem("Reset scale", @SceneResetScale, KEY_S, QUAL_ALT)); - popup.AddChild(CreateMenuItem("Reset transform", @SceneResetTransform, KEY_Q , QUAL_ALT)); - } - - if (hotKeyMode == HOTKEYS_MODE_STANDARD) - { - popup.AddChild(CreateMenuItem("Enable/disable", @SceneToggleEnable, KEY_E, QUAL_CTRL)); - popup.AddChild(CreateMenuItem("Enable all", @SceneEnableAllNodes, KEY_E, QUAL_ALT)); - } - else if (hotKeyMode == HOTKEYS_MODE_BLENDER) - { - popup.AddChild(CreateMenuItem("Enable/disable", @SceneToggleEnable, KEY_H)); - popup.AddChild(CreateMenuItem("Enable all", @SceneEnableAllNodes, KEY_H, QUAL_ALT)); - } - - if (hotKeyMode == HOTKEYS_MODE_STANDARD) - popup.AddChild(CreateMenuItem("Unparent", @SceneUnparent, KEY_U, QUAL_CTRL)); - else if (hotKeyMode == HOTKEYS_MODE_BLENDER) - popup.AddChild(CreateMenuItem("Unparent", @SceneUnparent, KEY_P, QUAL_ALT)); - - if (hotKeyMode == HOTKEYS_MODE_STANDARD) - popup.AddChild(CreateMenuItem("Parent to last", @NodesParentToLastSelected, KEY_U)); - else if (hotKeyMode == HOTKEYS_MODE_BLENDER) - popup.AddChild(CreateMenuItem("Parent to last", @NodesParentToLastSelected, KEY_P, QUAL_CTRL)); - - CreateChildDivider(popup); - - if (hotKeyMode == HOTKEYS_MODE_STANDARD) - popup.AddChild(CreateMenuItem("Toggle update", @ToggleSceneUpdate, KEY_P, QUAL_CTRL)); - //else if (hotKeyMode == HOT_KEYS_MODE_BLENDER) - // popup.AddChild(CreateMenuItem("Toggle update", @ToggleSceneUpdate, KEY_P, QUAL_CTRL)); - - if (hotKeyMode == HOTKEYS_MODE_STANDARD) - { - popup.AddChild(CreateMenuItem("View closer", @ViewCloser, KEY_F)); - } - else if (hotKeyMode == HOTKEYS_MODE_BLENDER) - { - popup.AddChild(CreateMenuItem("Move to layer", @ShowLayerMover, KEY_M)); - popup.AddChild(CreateMenuItem("Smart Duplicate", @SceneSmartDuplicateNode, KEY_D, QUAL_ALT)); - popup.AddChild(CreateMenuItem("View closer", @ViewCloser, KEY_KP_PERIOD)); - } - popup.AddChild(CreateMenuItem("Color wheel", @ColorWheelBuildMenuSelectTypeColor, KEY_W, QUAL_ALT)); - popup.AddChild(CreateMenuItem("Show components icons", @ViewDebugIcons, KEY_I, QUAL_ALT)); - - CreateChildDivider(popup); - - popup.AddChild(CreateMenuItem("Stop test animation", @StopTestAnimation)); - CreateChildDivider(popup); - popup.AddChild(CreateMenuItem("Rebuild navigation data", @SceneRebuildNavigation)); - popup.AddChild(CreateMenuItem("Render Zone Cubemap", @SceneRenderZoneCubemaps)); - popup.AddChild(CreateMenuItem("Add children to SM-group", @SceneAddChildrenStaticModelGroup)); - Menu@ childMenu = CreateMenuItem("Set children as spline path", null, SHOW_POPUP_INDICATOR); - Window@ childPopup = CreatePopup(childMenu); - childPopup.AddChild(CreateMenuItem("Non-cyclic", @SetSplinePath, 0, 0, true, "Set non-cyclic spline path")); - childPopup.AddChild(CreateMenuItem("Cyclic", @SetSplinePath, 0, 0, true, "Set cyclic spline path")); - popup.AddChild(childMenu); - FinalizedPopupMenu(popup); - uiMenuBar.AddChild(menu); - } - - { - Menu@ menu = CreateMenu("Create"); - Window@ popup = menu.popup; - popup.AddChild(CreateMenuItem("Replicated node", @PickNode, 0, 0, true, "Create Replicated node")); - popup.AddChild(CreateMenuItem("Local node", @PickNode, 0, 0, true, "Create Local node")); - CreateChildDivider(popup); - - Menu@ childMenu = CreateMenuItem("Component", null, SHOW_POPUP_INDICATOR); - Window@ childPopup = CreatePopup(childMenu); - String[] objectCategories = GetObjectCategories(); - for (uint i = 0; i < objectCategories.length; ++i) - { - // Skip the UI category for the component menus - if (objectCategories[i] == "UI") - continue; - - Menu@ m = CreateMenuItem(objectCategories[i], null, SHOW_POPUP_INDICATOR); - Window@ p = CreatePopup(m); - String[] componentTypes = GetObjectsByCategory(objectCategories[i]); - for (uint j = 0; j < componentTypes.length; ++j) - p.AddChild(CreateIconizedMenuItem(componentTypes[j], @PickComponent, 0, 0, "", true, "Create " + componentTypes[j])); - childPopup.AddChild(m); - } - FinalizedPopupMenu(childPopup); - popup.AddChild(childMenu); - - childMenu = CreateMenuItem("Builtin object", null, SHOW_POPUP_INDICATOR); - childPopup = CreatePopup(childMenu); - String[] objects = { "Box", "Cone", "Cylinder", "Plane", "Pyramid", "Sphere", "TeaPot", "Torus" }; - for (uint i = 0; i < objects.length; ++i) - childPopup.AddChild(CreateIconizedMenuItem(objects[i], @PickBuiltinObject, 0, 0, "Node", true, "Create " + objects[i])); - popup.AddChild(childMenu); - CreateChildDivider(popup); - - childMenu = CreateMenuItem("UI-element", null, SHOW_POPUP_INDICATOR); - childPopup = CreatePopup(childMenu); - String[] uiElementTypes = GetObjectsByCategory("UI"); - for (uint i = 0; i < uiElementTypes.length; ++i) - { - if (uiElementTypes[i] != "UIElement") - childPopup.AddChild(CreateIconizedMenuItem(uiElementTypes[i], @PickUIElement, 0, 0, "", true, "Create " + uiElementTypes[i])); - } - CreateChildDivider(childPopup); - childPopup.AddChild(CreateIconizedMenuItem("UIElement", @PickUIElement)); - popup.AddChild(childMenu); - - FinalizedPopupMenu(popup); - uiMenuBar.AddChild(menu); - } - - { - Menu@ menu = CreateMenu("UI-layout"); - Window@ popup = menu.popup; - popup.AddChild(CreateMenuItem("Open UI-layout...", @PickFile, KEY_O, QUAL_ALT)); - popup.AddChild(CreateMenuItem("Save UI-layout", @SaveUILayoutWithExistingName, KEY_S, QUAL_ALT)); - popup.AddChild(CreateMenuItem("Save UI-layout as...", @PickFile)); - CreateChildDivider(popup); - popup.AddChild(CreateMenuItem("Close UI-layout", @CloseUILayout, KEY_C, QUAL_ALT)); - popup.AddChild(CreateMenuItem("Close all UI-layouts", @CloseAllUILayouts)); - CreateChildDivider(popup); - popup.AddChild(CreateMenuItem("Load child element...", @PickFile)); - popup.AddChild(CreateMenuItem("Save child element as...", @PickFile)); - CreateChildDivider(popup); - popup.AddChild(CreateMenuItem("Set default style...", @PickFile)); - FinalizedPopupMenu(popup); - uiMenuBar.AddChild(menu); - } - - { - Menu@ menu = CreateMenu("View"); - Window@ popup = menu.popup; - popup.AddChild(CreateMenuItem("Hierarchy", @ToggleHierarchyWindow, KEY_H, QUAL_CTRL)); - popup.AddChild(CreateMenuItem("Attribute inspector", @ToggleAttributeInspectorWindow, KEY_I, QUAL_CTRL)); - popup.AddChild(CreateMenuItem("Resource browser", @ToggleResourceBrowserWindow, KEY_B, QUAL_CTRL)); - popup.AddChild(CreateMenuItem("Material editor", @ToggleMaterialEditor)); - popup.AddChild(CreateMenuItem("Particle editor", @ToggleParticleEffectEditor)); - popup.AddChild(CreateMenuItem("Terrain editor", TerrainEditorShowCallback(terrainEditor.Show))); - popup.AddChild(CreateMenuItem("Spawn editor", @ToggleSpawnEditor)); - popup.AddChild(CreateMenuItem("Duplicator editor", @ToggleDuplicatorEditor)); - popup.AddChild(CreateMenuItem("Sound Type editor", @ToggleSoundTypeEditor)); - popup.AddChild(CreateMenuItem("Editor settings", @ToggleEditorSettingsDialog)); - popup.AddChild(CreateMenuItem("Editor preferences", @ToggleEditorPreferencesDialog)); - CreateChildDivider(popup); - popup.AddChild(CreateMenuItem("Hide editor", @ToggleUI, KEY_F12, QUAL_ANY)); - FinalizedPopupMenu(popup); - uiMenuBar.AddChild(menu); - } - - BorderImage@ spacer = BorderImage("MenuBarSpacer"); - uiMenuBar.AddChild(spacer); - spacer.style = "EditorMenuBar"; -} - -bool Exit() -{ - ui.cursor.shape = CS_BUSY; - - if (messageBoxCallback is null) - { - String message; - if (sceneModified) - message = "Scene has been modified.\n"; - - bool uiLayoutModified = false; - for (uint i = 0; i < editorUIElement.numChildren; ++i) - { - UIElement@ element = editorUIElement.children[i]; - if (element !is null && element.vars[MODIFIED_VAR].GetBool()) - { - uiLayoutModified = true; - message += "UI layout has been modified.\n"; - break; - } - } - - if (sceneModified || uiLayoutModified) - { - MessageBox@ messageBox = MessageBox(message + "Continue to exit?", "Warning"); - if (messageBox.window !is null) - { - Button@ cancelButton = messageBox.window.GetChild("CancelButton", true); - cancelButton.visible = true; - cancelButton.focus = true; - SubscribeToEvent(messageBox, "MessageACK", "HandleMessageAcknowledgement"); - messageBoxCallback = @Exit; - return false; - } - } - } - else - messageBoxCallback = null; - - engine.Exit(); - return true; -} - -void HandleExitRequested() -{ - if (!ui.HasModalElement()) - Exit(); -} - -bool PickFile() -{ - Menu@ menu = GetEventSender(); - if (menu is null) - return false; - - String action = menu.name; - if (action.empty) - return false; - - // File (Scene related) - if (action == "Open scene...") - { - CreateFileSelector("Open scene", "Open", "Cancel", uiScenePath, uiSceneFilters, uiSceneFilter); - SubscribeToEvent(uiFileSelector, "FileSelected", "HandleOpenSceneFile"); - } - else if (action == "Save scene as..." || action == "Save scene") - { - CreateFileSelector("Save scene as", "Save", "Cancel", uiScenePath, uiSceneFilters, uiSceneFilter); - uiFileSelector.fileName = GetFileNameAndExtension(editorScene.fileName); - SubscribeToEvent(uiFileSelector, "FileSelected", "HandleSaveSceneFile"); - } - else if (action == "As replicated..." || action == "Load node as replicated...") - { - instantiateMode = REPLICATED; - CreateFileSelector("fileSelector Load node", "Load", "Cancel", uiNodePath, uiSceneFilters, uiNodeFilter); - SubscribeToEvent(uiFileSelector, "FileSelected", "HandleLoadNodeFile"); - } - else if (action == "As local..." || action == "Load node as local...") - { - instantiateMode = LOCAL; - CreateFileSelector("Load node", "Load", "Cancel", uiNodePath, uiSceneFilters, uiNodeFilter); - SubscribeToEvent(uiFileSelector, "FileSelected", "HandleLoadNodeFile"); - } - else if (action == "Save node as...") - { - if (editNode !is null && editNode !is editorScene) - { - CreateFileSelector("Save node", "Save", "Cancel", uiNodePath, uiSceneFilters, uiNodeFilter); - uiFileSelector.fileName = GetFileNameAndExtension(instantiateFileName); - SubscribeToEvent(uiFileSelector, "FileSelected", "HandleSaveNodeFile"); - } - } - else if (action == "Import model...") - { - CreateFileSelector("Import model", "Import", "Cancel", uiImportPath, uiAllFilters, uiImportFilter); - SubscribeToEvent(uiFileSelector, "FileSelected", "HandleImportModel"); - } - else if (action == "Import animation...") - { - CreateFileSelector("Import animation", "Import", "Cancel", uiImportPath, uiAllFilters, uiImportFilter); - SubscribeToEvent(uiFileSelector, "FileSelected", "HandleImportAnimation"); - } - else if (action == "Import scene...") - { - CreateFileSelector("Import scene", "Import", "Cancel", uiImportPath, uiAllFilters, uiImportFilter); - SubscribeToEvent(uiFileSelector, "FileSelected", "HandleImportScene"); - } - else if (action == "Export scene to OBJ..." || action == "Export selected to OBJ...") - { - // Set these up together to share the "export settings" options - if (action == "Export scene to OBJ...") - { - CreateFileSelector("Export scene to OBJ", "Save", "Cancel", uiExportPath, uiExportPathFilters, uiExportFilter); - SubscribeToEvent(uiFileSelector, "FileSelected", "HandleExportSceneOBJ"); - } - else if (action == "Export selected to OBJ...") - { - CreateFileSelector("Export selected to OBJ", "Save", "Cancel", uiExportPath, uiExportPathFilters, uiExportFilter); - SubscribeToEvent(uiFileSelector, "FileSelected", "HandleExportSelectedOBJ"); - } - - Window@ window = uiFileSelector.window; - - UIElement@ optionsGroup = UIElement(); - optionsGroup.maxHeight = 30; - optionsGroup.layoutMode = LM_HORIZONTAL; - window.defaultStyle = uiStyle; - window.style = AUTO_STYLE; - - CheckBox@ checkRightHanded = CheckBox(); - checkRightHanded.checked = objExportRightHanded_; - checkRightHanded.defaultStyle = uiStyle; - checkRightHanded.style = AUTO_STYLE; - SubscribeToEvent(checkRightHanded, "Toggled", "HandleOBJRightHandedChanged"); - optionsGroup.AddChild(checkRightHanded); - - Text@ lblRightHanded = Text(); - lblRightHanded.defaultStyle = uiStyle; - lblRightHanded.style = AUTO_STYLE; - lblRightHanded.text = " Right handed"; - optionsGroup.AddChild(lblRightHanded); - - CheckBox@ checkZUp = CheckBox(); - checkZUp.checked = objExportZUp_; - checkZUp.defaultStyle = uiStyle; - checkZUp.style = AUTO_STYLE; - SubscribeToEvent(checkZUp, "Toggled", "HandleOBJZUpChanged"); - optionsGroup.AddChild(checkZUp); - - Text@ lblZUp = Text(); - lblZUp.defaultStyle = uiStyle; - lblZUp.style = AUTO_STYLE; - lblZUp.text = " Z Axis Up"; - optionsGroup.AddChild(lblZUp); - - window.AddChild(optionsGroup); - } - else if (action == "Run script...") - { - CreateFileSelector("Run script", "Run", "Cancel", uiScriptPath, uiScriptFilters, uiScriptFilter); - SubscribeToEvent(uiFileSelector, "FileSelected", "HandleRunScript"); - } - else if (action == "Set resource path...") - { - CreateFileSelector("Set resource path", "Set", "Cancel", sceneResourcePath, uiAllFilters, 0); - uiFileSelector.directoryMode = true; - SubscribeToEvent(uiFileSelector, "FileSelected", "HandleResourcePath"); - } - // UI-element - else if (action == "Open UI-layout...") - { - CreateFileSelector("Open UI-layout", "Open", "Cancel", uiElementPath, uiElementFilters, uiElementFilter); - SubscribeToEvent(uiFileSelector, "FileSelected", "HandleOpenUILayoutFile"); - } - else if (action == "Save UI-layout as..." || action == "Save UI-layout") - { - if (editUIElement !is null) - { - UIElement@ element = GetTopLevelUIElement(editUIElement); - if (element is null) - return false; - - CreateFileSelector("Save UI-layout as", "Save", "Cancel", uiElementPath, uiElementFilters, uiElementFilter); - uiFileSelector.fileName = GetFileNameAndExtension(element.GetVar(FILENAME_VAR).GetString()); - SubscribeToEvent(uiFileSelector, "FileSelected", "HandleSaveUILayoutFile"); - } - } - else if (action == "Load child element...") - { - if (editUIElement !is null) - { - CreateFileSelector("Load child element", "Load", "Cancel", uiElementPath, uiElementFilters, uiElementFilter); - SubscribeToEvent(uiFileSelector, "FileSelected", "HandleLoadChildUIElementFile"); - } - } - else if (action == "Save child element as...") - { - if (editUIElement !is null) - { - CreateFileSelector("Save child element", "Save", "Cancel", uiElementPath, uiElementFilters, uiElementFilter); - uiFileSelector.fileName = GetFileNameAndExtension(editUIElement.GetVar(CHILD_ELEMENT_FILENAME_VAR).GetString()); - SubscribeToEvent(uiFileSelector, "FileSelected", "HandleSaveChildUIElementFile"); - } - } - else if (action == "Set default style...") - { - CreateFileSelector("Set default style", "Set", "Cancel", uiElementPath, uiElementFilters, uiElementFilter); - SubscribeToEvent(uiFileSelector, "FileSelected", "HandleUIElementDefaultStyle"); - } - - return true; -} - -bool PickNode() -{ - Menu@ menu = GetEventSender(); - if (menu is null) - return false; - - String action = GetActionName(menu.name); - if (action.empty) - return false; - - CreateNode(action == "Replicated node" ? REPLICATED : LOCAL); - return true; -} - -bool PickComponent() -{ - if (editNodes.empty) - return false; - - Menu@ menu = GetEventSender(); - if (menu is null) - return false; - - String action = GetActionName(menu.name); - if (action.empty) - return false; - - CreateComponent(action); - return true; -} - -bool PickBuiltinObject() -{ - Menu@ menu = GetEventSender(); - if (menu is null) - return false; - - String action = GetActionName(menu.name); - if (action.empty) - return false; - - CreateBuiltinObject(action); - return true; -} - -bool PickUIElement() -{ - Menu@ menu = GetEventSender(); - if (menu is null) - return false; - - String action = GetActionName(menu.name); - if (action.empty) - return false; - - return NewUIElement(action); -} - -// When calling items from the quick menu, they have "Create" prepended for clarity. Strip that now to get the object name to create -String GetActionName(const String&in name) -{ - if (name.StartsWith("Create")) - return name.Substring(7); - else - return name; -} - -void HandleMenuSelected(StringHash eventType, VariantMap& eventData) -{ - Menu@ menu = eventData["Element"].GetPtr(); - if (menu is null) - return; - - HandlePopup(menu); - - quickMenu.visible = false; - quickMenu.enabled = false; - - // Execute the callback if available - Variant variant = menu.GetVar(CALLBACK_VAR); - if (!variant.empty) - menuCallbacks[variant.GetU32()](); -} - -Menu@ CreateMenuItem(const String&in title, MENU_CALLBACK@ callback = null, int accelKey = 0, int accelQual = 0, bool addToQuickMenu = true, String quickMenuText="", bool autoLocalize = true) -{ - Menu@ menu = Menu(title); - menu.defaultStyle = uiStyle; - menu.style = AUTO_STYLE; - menu.SetLayout(LM_HORIZONTAL, 0, IntRect(8, 2, 8, 2)); - if (accelKey > 0) - menu.SetAccelerator(accelKey, accelQual); - if (callback !is null) - { - menu.vars[CALLBACK_VAR] = menuCallbacks.length; - menuCallbacks.Push(callback); - } - - Text@ menuText = Text(); - menu.AddChild(menuText); - menuText.style = "EditorMenuText"; - menuText.text = title; - menuText.autoLocalizable = autoLocalize; - - if (addToQuickMenu) - AddQuickMenuItem(callback, quickMenuText.empty ? title : quickMenuText); - - if (accelKey != 0) - { - UIElement@ spacer = UIElement(); - spacer.minWidth = menuText.indentSpacing; - spacer.height = menuText.height; - menu.AddChild(spacer); - menu.AddChild(CreateAccelKeyText(accelKey, accelQual)); - } - - return menu; -} - -void AddQuickMenuItem(MENU_CALLBACK@ callback, String text) -{ - if (callback is null) - return; - - bool exists = false; - for (uint i=0;i 0) - menu.SetAccelerator(accelKey, accelQual); - if (callback !is null) - { - menu.vars[CALLBACK_VAR] = menuCallbacks.length; - menuCallbacks.Push(callback); - } - - Text@ menuText = Text(); - menu.AddChild(menuText); - menuText.style = "EditorMenuText"; - menuText.text = title; - // If icon type is not provided, use the title instead - IconizeUIElement(menuText, iconType.empty ? title : iconType); - - if (addToQuickMenu) - AddQuickMenuItem(callback, quickMenuText.empty ? title : quickMenuText); - - if (accelKey != 0) - { - menuText.layoutMode = LM_HORIZONTAL; - menuText.AddChild(CreateAccelKeyText(accelKey, accelQual)); - } - - return menu; -} - -/// Create a child divider in parent with vertical layout mode. It works on other parent element as well, not just parent menu. -void CreateChildDivider(UIElement@ parent) -{ - BorderImage@ divider = parent.CreateChild("BorderImage", "Divider"); - divider.style = "EditorDivider"; -} - -Window@ CreatePopup(Menu@ baseMenu) -{ - Window@ popup = Window(); - popup.defaultStyle = uiStyle; - popup.style = AUTO_STYLE; - popup.SetLayout(LM_VERTICAL, 1, IntRect(2, 6, 2, 6)); - baseMenu.popup = popup; - baseMenu.popupOffset = IntVector2(0, baseMenu.height); - - return popup; -} - -Menu@ CreateMenu(const String&in title) -{ - Menu@ menu = CreateMenuItem(title); - Text@ text = menu.children[0]; - menu.maxWidth = text.width + 20; - CreatePopup(menu); - - return menu; -} - -void HandleChangeLanguage(StringHash eventType, VariantMap& eventData) -{ - Array children = uiMenuBar.GetChildren(); - - for (uint i = 0; i < children.length - 2; ++i) // last 2 elements is not menu - { - // dirty hack: force recalc text size - children[i].maxWidth = 1000; - Text@ text = children[i].children[0]; - text.minWidth = 0; - text.maxWidth = 1; - text.ApplyAttributes(); - children[i].maxWidth = text.width + 20; - } - - RebuildResourceDatabase(); -} - -Text@ CreateAccelKeyText(int accelKey, int accelQual) -{ - Text@ accelKeyText = Text(); - accelKeyText.defaultStyle = uiStyle; - accelKeyText.style = "EditorMenuText"; - accelKeyText.textAlignment = HA_RIGHT; - - String text; - if (accelKey == KEY_DELETE) - text = "Del"; - else if (accelKey == KEY_SPACE) - text = "Space"; - // Cannot use range as the key constants below do not appear to be in sequence - else if (accelKey == KEY_F1) - text = "F1"; - else if (accelKey == KEY_F2) - text = "F2"; - else if (accelKey == KEY_F3) - text = "F3"; - else if (accelKey == KEY_F4) - text = "F4"; - else if (accelKey == KEY_F5) - text = "F5"; - else if (accelKey == KEY_F6) - text = "F6"; - else if (accelKey == KEY_F7) - text = "F7"; - else if (accelKey == KEY_F8) - text = "F8"; - else if (accelKey == KEY_F9) - text = "F9"; - else if (accelKey == KEY_F10) - text = "F10"; - else if (accelKey == KEY_F11) - text = "F11"; - else if (accelKey == KEY_F12) - text = "F12"; - else if (accelKey == SHOW_POPUP_INDICATOR) - text = ">"; - else if (accelKey == KEY_KP_PERIOD) - text = "NumPad ."; - else - text.AppendUTF8(accelKey); - if (accelQual & QUAL_ALT > 0) - text = "Alt+" + text; - if (accelQual & QUAL_SHIFT > 0) - text = "Shift+" + text; - if (accelQual & QUAL_CTRL > 0) - text = "Ctrl+" + text; - accelKeyText.text = text; - - return accelKeyText; -} - -void FinalizedPopupMenu(Window@ popup) -{ - // Find the maximum menu text width - Array children = popup.GetChildren(); - int maxWidth = 0; - for (uint i = 0; i < children.length; ++i) - { - UIElement@ element = children[i]; - if (element.type != MENU_TYPE) // Skip if not menu item - continue; - - int width = element.children[0].width; - if (width > maxWidth) - maxWidth = width; - } - - // Adjust the indent spacing to slightly wider than the maximum width - maxWidth += 20; - for (uint i = 0; i < children.length; ++i) - { - UIElement@ element = children[i]; - if (element.type != MENU_TYPE) - continue; - Menu@ menu = element; - - Text@ menuText = menu.children[0]; - if (menuText.numChildren == 1) // Skip if menu text does not have accel - menuText.children[0].indentSpacing = maxWidth; - - // Adjust the popup offset taking the indentation into effect - if (menu.popup !is null) - menu.popupOffset = IntVector2(menu.width, 0); - } -} - -void CreateFileSelector(const String&in title, const String&in ok, const String&in cancel, const String&in initialPath, Array@ filters, - uint initialFilter, bool autoLocalizeTitle = true) -{ - // Within the editor UI, the file selector is a kind of a "singleton". When the previous one is overwritten, also - // the events subscribed from it are disconnected, so new ones are safe to subscribe. - uiFileSelector = FileSelector(); - uiFileSelector.defaultStyle = uiStyle; - uiFileSelector.title = title; - uiFileSelector.titleText.autoLocalizable = autoLocalizeTitle; - uiFileSelector.path = initialPath; - uiFileSelector.SetButtonTexts(ok, cancel); - Text@ okText = cast(uiFileSelector.okButton.children[0]); - okText.autoLocalizable = true; - Text@ cancelText = cast(uiFileSelector.cancelButton.children[0]); - cancelText.autoLocalizable = true; - uiFileSelector.SetFilters(filters, initialFilter); - CenterDialog(uiFileSelector.window); -} - -void CloseFileSelector(uint&out filterIndex, String&out path) -{ - // Save filter & path for next time - filterIndex = uiFileSelector.filterIndex; - path = uiFileSelector.path; - - uiFileSelector = null; -} - -void CloseFileSelector() -{ - uiFileSelector = null; -} - -void CreateConsole() -{ - Console@ console = engine.CreateConsole(); - console.defaultStyle = uiStyle; - console.commandInterpreter = consoleCommandInterpreter; - console.numBufferedRows = 100; - console.autoVisibleOnError = true; -} - -void CreateDebugHud() -{ - engine.CreateDebugHud(); - debugHud.defaultStyle = uiStyle; - debugHud.mode = DebugHudElements::None; -} - -void CenterDialog(UIElement@ element) -{ - IntVector2 size = element.size; - element.SetPosition((ui.root.width - size.x) / 2, (ui.root.height - size.y) / 2); -} - -void CreateContextMenu() -{ - contextMenu = LoadEditorUI("UI/EditorContextMenu.xml"); - ui.root.AddChild(contextMenu); -} - -void UpdateWindowTitle() -{ - String sceneName = GetFileNameAndExtension(editorScene.fileName); - if (sceneName.empty || sceneName == TEMP_SCENE_NAME || sceneName == TEMP_BINARY_SCENE_NAME) - sceneName = "Untitled"; - if (sceneModified) - sceneName += "*"; - graphics.windowTitle = "Urho3D editor - " + sceneName; -} - -void HandlePopup(Menu@ menu) -{ - // Close the top level menu now unless the selected menu item has another popup - if (menu.popup !is null) - return; - - for (;;) - { - UIElement@ menuParent = menu.parent; - if (menuParent is null) - break; - - Menu@ nextMenu = menuParent.vars["Origin"].GetPtr(); - if (nextMenu is null) - break; - else - menu = nextMenu; - } - - if (menu.parent is uiMenuBar) - menu.showPopup = false; -} - -String ExtractFileName(VariantMap& eventData, bool forSave = false) -{ - String fileName; - - // Check for OK - if (eventData["OK"].GetBool()) - { - String filter = eventData["Filter"].GetString(); - fileName = eventData["FileName"].GetString(); - // Add default extension for saving if not specified - if (GetExtension(fileName).empty && forSave && filter != "*.*") - fileName = fileName + filter.Substring(1); - } - return fileName; -} - -void HandleOpenSceneFile(StringHash eventType, VariantMap& eventData) -{ - CloseFileSelector(uiSceneFilter, uiScenePath); - LoadScene(ExtractFileName(eventData)); - SendEvent(EDITOR_EVENT_SCENE_LOADED); -} - -void HandleSaveSceneFile(StringHash eventType, VariantMap& eventData) -{ - CloseFileSelector(uiSceneFilter, uiScenePath); - SaveScene(ExtractFileName(eventData, true)); -} - -void HandleLoadNodeFile(StringHash eventType, VariantMap& eventData) -{ - CloseFileSelector(uiNodeFilter, uiNodePath); - LoadNode(ExtractFileName(eventData)); -} - -void HandleSaveNodeFile(StringHash eventType, VariantMap& eventData) -{ - CloseFileSelector(uiNodeFilter, uiNodePath); - SaveNode(ExtractFileName(eventData, true)); -} - -void HandleImportModel(StringHash eventType, VariantMap& eventData) -{ - CloseFileSelector(uiImportFilter, uiImportPath); - ImportModel(ExtractFileName(eventData)); -} -void HandleImportAnimation(StringHash eventType, VariantMap& eventData) -{ - CloseFileSelector(uiImportFilter, uiImportPath); - ImportAnimation(ExtractFileName(eventData)); -} - -void HandleImportScene(StringHash eventType, VariantMap& eventData) -{ - CloseFileSelector(uiImportFilter, uiImportPath); - ImportScene(ExtractFileName(eventData)); -} - -void HandleExportSceneOBJ(StringHash eventType, VariantMap& eventData) -{ - CloseFileSelector(uiExportFilter, uiExportPath); - ExportSceneToOBJ(ExtractFileName(eventData)); -} - -void HandleExportSelectedOBJ(StringHash eventType, VariantMap& eventData) -{ - CloseFileSelector(uiExportFilter, uiExportPath); - ExportSelectedToOBJ(ExtractFileName(eventData)); -} - - -void ExecuteScript(const String&in fileName) -{ - if (fileName.empty) - return; - - File@ file = File(fileName, FILE_READ); - if (file.open) - { - String scriptCode; - while (!file.eof) - scriptCode += file.ReadLine() + "\n"; - file.Close(); - - if (script.Execute(scriptCode)) - log.Info("Script " + fileName + " ran successfully"); - } -} - -void HandleRunScript(StringHash eventType, VariantMap& eventData) -{ - CloseFileSelector(uiScriptFilter, uiScriptPath); - - suppressSceneChanges = true; - ExecuteScript(ExtractFileName(eventData)); - suppressSceneChanges = false; - - UpdateHierarchyItem(editorScene, true); - UpdateHierarchyItem(editorUIElement, true); -} - -void HandleResourcePath(StringHash eventType, VariantMap& eventData) -{ - String pathName = uiFileSelector.path; - CloseFileSelector(); - if (eventData["OK"].GetBool()) - SetResourcePath(pathName, false); -} - -void HandleOpenUILayoutFile(StringHash eventType, VariantMap& eventData) -{ - CloseFileSelector(uiElementFilter, uiElementPath); - OpenUILayout(ExtractFileName(eventData)); -} - -void HandleSaveUILayoutFile(StringHash eventType, VariantMap& eventData) -{ - CloseFileSelector(uiElementFilter, uiElementPath); - SaveUILayout(ExtractFileName(eventData, true)); -} - -void HandleLoadChildUIElementFile(StringHash eventType, VariantMap& eventData) -{ - CloseFileSelector(uiElementFilter, uiElementPath); - LoadChildUIElement(ExtractFileName(eventData)); -} - -void HandleSaveChildUIElementFile(StringHash eventType, VariantMap& eventData) -{ - CloseFileSelector(uiElementFilter, uiElementPath); - SaveChildUIElement(ExtractFileName(eventData, true)); -} - -void HandleUIElementDefaultStyle(StringHash eventType, VariantMap& eventData) -{ - CloseFileSelector(uiElementFilter, uiElementPath); - SetUIElementDefaultStyle(ExtractFileName(eventData)); -} - -void HandleHotKeysBlender( VariantMap& eventData) -{ - int key = eventData["Key"].GetI32(); - int viewDirection = eventData["Qualifiers"].GetI32() == QUAL_CTRL ? -1 : 1; - - if (key == KEY_ESCAPE) - { - if (uiHidden) - UnhideUI(); - else if (console.visible) - console.visible = false; - else if (contextMenu.visible) - CloseContextMenu(); - else if (quickMenu.visible) - { - quickMenu.visible = false; - quickMenu.enabled = false; - } - else - { - UIElement@ front = ui.frontElement; - if (front is settingsDialog || front is preferencesDialog) - { - ui.focusElement = null; - front.visible = false; - } - } - } - // Ignore other keys when UI has a modal element - else if (ui.HasModalElement()) - return; - - else if (key == KEY_F1) - console.Toggle(); - else if (key == KEY_F2) - ToggleRenderingDebug(); - else if (key == KEY_F3) - TogglePhysicsDebug(); - else if (key == KEY_F4) - ToggleOctreeDebug(); - else if (key == KEY_F5) - ToggleNavigationDebug(); - else if (key == KEY_F11) - { - Image@ screenshot = Image(); - graphics.TakeScreenShot(screenshot); - if (!fileSystem.DirExists(screenshotDir)) - fileSystem.CreateDir(screenshotDir); - screenshot.SavePNG(screenshotDir + "/Screenshot_" + - time.timeStamp.Replaced(':', '_').Replaced('.', '_').Replaced(' ', '_') + ".png"); - } - // In Blender, HOME key is for locating the selected objects by pan and - // the PERIOD key of keypad is for moving the camera to focus the selected. - // Here we ignore the difference. - else if ((key == KEY_HOME || key == KEY_KP_PERIOD) && ui.focusElement is null) - { - if (selectedNodes.length > 0 || selectedComponents.length > 0) - { - LocateNodesAndComponents(selectedNodes, selectedComponents); - } - } - else if (key == KEY_KP_1 && ui.focusElement is null) // Front view - { - cameraSmoothInterpolate.Finish(); - - Vector3 pos = -Vector3(0.0, 0.0, cameraNode.position.length * viewDirection); - Quaternion rot = Quaternion(Vector3::FORWARD, Vector3(0, 0, viewDirection)); - - cameraSmoothInterpolate.SetCameraNodePosition(cameraNode.position, pos); - cameraSmoothInterpolate.SetCameraNodeRotation(cameraNode.rotation, rot); - cameraSmoothInterpolate.Start(0.5f); - } - else if ((key == KEY_KP_3 || key == KEY_KP_9) && ui.focusElement is null) // Side view - { - cameraSmoothInterpolate.Finish(); - - Vector3 pos = -Vector3(cameraNode.position.length * -viewDirection, 0.0, 0.0); - Quaternion rot = Quaternion(Vector3::FORWARD, Vector3(-viewDirection, 0, 0)); - - cameraSmoothInterpolate.SetCameraNodePosition(cameraNode.position, pos); - cameraSmoothInterpolate.SetCameraNodeRotation(cameraNode.rotation, rot); - cameraSmoothInterpolate.Start(0.5f); - } - else if (key == KEY_KP_7 && ui.focusElement is null) // Top view - { - cameraSmoothInterpolate.Finish(); - - Vector3 pos = -Vector3(0.0, cameraNode.position.length * -viewDirection, 0.0); - Quaternion rot = Quaternion(Vector3::FORWARD, Vector3(0, -viewDirection, 0)); - - cameraSmoothInterpolate.SetCameraNodePosition(cameraNode.position, pos); - cameraSmoothInterpolate.SetCameraNodeRotation(cameraNode.rotation, rot); - cameraSmoothInterpolate.Start(0.5f); - } - else if (key == KEY_KP_5 && ui.focusElement is null) - { - activeViewport.ToggleOrthographic(); - } - else if (key == '4' && ui.focusElement is null) - editMode = EDIT_SELECT; - else if (key == '5' && ui.focusElement is null) - axisMode = AxisMode(axisMode ^ AXIS_LOCAL); - else if (key == '6' && ui.focusElement is null) - { - --pickMode; - if (pickMode < PICK_GEOMETRIES) - pickMode = MAX_PICK_MODES - 1; - } - else if (key == '7' && ui.focusElement is null) - { - ++pickMode; - if (pickMode >= MAX_PICK_MODES) - pickMode = PICK_GEOMETRIES; - } - else if (key == KEY_Z && eventData["Qualifiers"].GetI32() != QUAL_CTRL) - { - if (ui.focusElement is null) - { - fillMode = FillMode(fillMode + 1); - if (fillMode > FILL_POINT) - fillMode = FILL_SOLID; - - // Update camera fill mode - SetFillMode(fillMode); - } - } - else if (key == KEY_SPACE) - { - if (ui.cursor.visible && ui.focusElement is null) - ToggleQuickMenu(); - } - else - { - SteppedObjectManipulation(key); - } - - if ((ui.focusElement is null) && (selectedNodes.length > 0) && !cameraFlyMode) - { - if (eventData["Qualifiers"].GetI32() == QUAL_ALT) // reset transformations - { - if (key == KEY_G) - SceneResetPosition(); - else if (key == KEY_R) - SceneResetRotation(); - else if (key == KEY_S) - SceneResetScale(); - } - else if (eventData["Qualifiers"].GetI32() != QUAL_CTRL) // set transformations - { - if (key == KEY_G) - { - editMode = EDIT_MOVE; - axisMode = AxisMode(axisMode ^ AXIS_LOCAL); - - } - else if (key == KEY_R) - { - editMode = EDIT_ROTATE; - axisMode = AxisMode(axisMode ^ AXIS_LOCAL); - - } - else if (key == KEY_S) - { - editMode = EDIT_SCALE; - axisMode = AxisMode(axisMode ^ AXIS_LOCAL); - } - } - } - - toolBarDirty = true; -} - -void HandleHotKeysStandard(VariantMap& eventData) -{ - int key = eventData["Key"].GetI32(); - int viewDirection = eventData["Qualifiers"].GetI32() == QUAL_CTRL ? -1 : 1; - - if (key == KEY_ESCAPE) - { - if (uiHidden) - UnhideUI(); - else if (console.visible) - console.visible = false; - else if (contextMenu.visible) - CloseContextMenu(); - else if (quickMenu.visible) - { - quickMenu.visible = false; - quickMenu.enabled = false; - } - else - { - UIElement@ front = ui.frontElement; - if (front is settingsDialog || front is preferencesDialog) - { - ui.focusElement = null; - front.visible = false; - } - } - } - - // Ignore other keys when UI has a modal element - else if (ui.HasModalElement()) - return; - - else if (key == KEY_F1) - console.Toggle(); - else if (key == KEY_F2) - ToggleRenderingDebug(); - else if (key == KEY_F3) - TogglePhysicsDebug(); - else if (key == KEY_F4) - ToggleOctreeDebug(); - else if (key == KEY_F5) - ToggleNavigationDebug(); - else if (key == KEY_F11) - { - Image@ screenshot = Image(); - graphics.TakeScreenShot(screenshot); - if (!fileSystem.DirExists(screenshotDir)) - fileSystem.CreateDir(screenshotDir); - screenshot.SavePNG(screenshotDir + "/Screenshot_" + - time.timeStamp.Replaced(':', '_').Replaced('.', '_').Replaced(' ', '_') + ".png"); - } - else if ((key == KEY_HOME || key == KEY_F) && ui.focusElement is null) - { - if (selectedNodes.length > 0 || selectedComponents.length > 0) - { - LocateNodesAndComponents(selectedNodes, selectedComponents); - } - } - else if (key == KEY_KP_1 && ui.focusElement is null) // Front view - { - cameraSmoothInterpolate.Finish(); - - Vector3 pos = -Vector3(0.0, 0.0, cameraNode.position.length * viewDirection); - Quaternion rot = Quaternion(Vector3::FORWARD, Vector3(0, 0, viewDirection)); - - cameraSmoothInterpolate.SetCameraNodePosition(cameraNode.position, pos); - cameraSmoothInterpolate.SetCameraNodeRotation(cameraNode.rotation, rot); - cameraSmoothInterpolate.Start(0.5f); - } - else if ((key == KEY_KP_3 || key == KEY_KP_9) && ui.focusElement is null) // Side view - { - cameraSmoothInterpolate.Finish(); - - Vector3 pos = -Vector3(cameraNode.position.length * -viewDirection, 0.0, 0.0); - Quaternion rot = Quaternion(Vector3::FORWARD, Vector3(-viewDirection, 0, 0)); - - cameraSmoothInterpolate.SetCameraNodePosition(cameraNode.position, pos); - cameraSmoothInterpolate.SetCameraNodeRotation(cameraNode.rotation, rot); - cameraSmoothInterpolate.Start(0.5f); - } - else if (key == KEY_KP_7 && ui.focusElement is null) // Top view - { - cameraSmoothInterpolate.Finish(); - - Vector3 pos = -Vector3(0.0, cameraNode.position.length * -viewDirection, 0.0); - Quaternion rot = Quaternion(Vector3::FORWARD, Vector3(0, -viewDirection, 0)); - - cameraSmoothInterpolate.SetCameraNodePosition(cameraNode.position, pos); - cameraSmoothInterpolate.SetCameraNodeRotation(cameraNode.rotation, rot); - cameraSmoothInterpolate.Start(0.5f); - } - else if (key == KEY_KP_5 && ui.focusElement is null) - { - activeViewport.ToggleOrthographic(); - } - else if (eventData["Qualifiers"].GetI32() == QUAL_CTRL) - { - if (key == '1') - editMode = EDIT_MOVE; - else if (key == '2') - editMode = EDIT_ROTATE; - else if (key == '3') - editMode = EDIT_SCALE; - else if (key == '4') - editMode = EDIT_SELECT; - else if (key == '5') - axisMode = AxisMode(axisMode ^ AXIS_LOCAL); - else if (key == '6') - { - --pickMode; - if (pickMode < PICK_GEOMETRIES) - pickMode = MAX_PICK_MODES - 1; - } - else if (key == '7') - { - ++pickMode; - if (pickMode >= MAX_PICK_MODES) - pickMode = PICK_GEOMETRIES; - } - else if (key == KEY_W) - { - fillMode = FillMode(fillMode + 1); - if (fillMode > FILL_POINT) - fillMode = FILL_SOLID; - - // Update camera fill mode - SetFillMode(fillMode); - } - else if (key == KEY_SPACE) - { - if (ui.cursor.visible) - ToggleQuickMenu(); - } - else - SteppedObjectManipulation(key); - - toolBarDirty = true; - } -} - -void HandleKeyDown(StringHash eventType, VariantMap& eventData) -{ - if (hotKeyMode == HOTKEYS_MODE_STANDARD) - { - HandleHotKeysStandard(eventData); - } - else if( hotKeyMode == HOTKEYS_MODE_BLENDER) - { - HandleHotKeysBlender(eventData); - } -} - -void UnfadeUI() -{ - FadeUI(false); -} - -void FadeUI(bool fade = true) -{ - if (uiHidden || uiFaded == fade) - return; - - float opacity = (uiFaded = fade) ? uiMinOpacity : uiMaxOpacity; - Array children = ui.root.GetChildren(); - for (uint i = 0; i < children.length; ++i) - { - // Texts, popup&modal windows (which are anyway only in ui.modalRoot), and editorUIElement are excluded - if (children[i].type != TEXT_TYPE && children[i] !is editorUIElement) - children[i].opacity = opacity; - } -} - -bool ToggleUI() -{ - HideUI(!uiHidden); - return true; -} - -void UnhideUI() -{ - HideUI(false); -} - -void HideUI(bool hide = true) -{ - if (uiHidden == hide) - return; - - // Note: we could set ui.root.visible = false and it would hide the whole hierarchy. - // However in this case we need the editorUIElement to stay visible - bool visible = !(uiHidden = hide); - Array children = ui.root.GetChildren(); - for (uint i = 0; i < children.length; ++i) - { - // Cursor and editorUIElement are excluded - if (children[i].type != CURSOR_TYPE && children[i] !is editorUIElement) - { - if (visible) - { - if (!children[i].visible) - children[i].visible = children[i].vars["HideUI"].GetBool(); - } - else - { - children[i].vars["HideUI"] = children[i].visible; - children[i].visible = false; - } - } - } -} - -void IconizeUIElement(UIElement@ element, const String&in iconType) -{ - // Check if the icon has been created before - BorderImage@ icon = element.GetChild("Icon"); - - // If iconType is empty, it is a request to remove the existing icon - if (iconType.empty) - { - // Remove the icon if it exists - if (icon !is null) - icon.Remove(); - - // Revert back the indent but only if it is indented by this function - if (element.vars[INDENT_MODIFIED_BY_ICON_VAR].GetBool()) - element.indent = 0; - - return; - } - - // The UI element must itself has been indented to reserve the space for the icon - if (element.indent == 0) - { - element.indent = 1; - element.vars[INDENT_MODIFIED_BY_ICON_VAR] = true; - } - - // If no icon yet then create one with the correct indent and size in respect to the UI element - if (icon is null) - { - // The icon is placed at one indent level less than the UI element - icon = BorderImage("Icon"); - icon.indent = element.indent - 1; - icon.SetFixedSize(element.indentWidth - 2, 14); - element.InsertChild(0, icon); // Ensure icon is added as the first child - } - - // Set the icon type - if (!icon.SetStyle(iconType, iconStyle)) - icon.SetStyle("Unknown", iconStyle); // If fails then use an 'unknown' icon type - icon.color = Color(1,1,1,1); // Reset to enabled color -} - -void SetIconEnabledColor(UIElement@ element, bool enabled, bool partial = false) -{ - BorderImage@ icon = element.GetChild("Icon"); - if (icon !is null) - { - if (partial) - { - icon.colors[C_TOPLEFT] = Color(1,1,1,1); - icon.colors[C_BOTTOMLEFT] = Color(1,1,1,1); - icon.colors[C_TOPRIGHT] = Color(1,0,0,1); - icon.colors[C_BOTTOMRIGHT] = Color(1,0,0,1); - } - else - icon.color = enabled ? Color(1,1,1,1) : Color(1,0,0,1); - } -} - -void UpdateDirtyUI() -{ - UpdateDirtyToolBar(); - terrainEditor.UpdateDirty(); - - // Perform hierarchy selection latently after the new selections are finalized (used in undo/redo action) - if (!hierarchyUpdateSelections.empty) - { - hierarchyList.SetSelections(hierarchyUpdateSelections); - hierarchyUpdateSelections.Clear(); - } - - // Perform some event-triggered updates latently in case a large hierarchy was changed - if (attributesFullDirty || attributesDirty) - UpdateAttributeInspector(attributesFullDirty); -} - -void HandleMessageAcknowledgement(StringHash eventType, VariantMap& eventData) -{ - if (eventData["OK"].GetBool()) - messageBoxCallback(); - else - messageBoxCallback = null; -} - -void PopulateMruScenes() -{ - mruScenesPopup.RemoveAllChildren(); - if (uiRecentScenes.length > 0) - { - recentSceneMenu.enabled = true; - for (uint i=0; i < uiRecentScenes.length; ++i) - mruScenesPopup.AddChild(CreateMenuItem(uiRecentScenes[i], @LoadMostRecentScene, 0, 0, false, "", false)); - } - else - recentSceneMenu.enabled = false; - -} - -bool LoadMostRecentScene() -{ - Menu@ menu = GetEventSender(); - if (menu is null) - return false; - - Text@ text = menu.GetChildren()[0]; - if (text is null) - return false; - - return LoadScene(text.text); -} - -// Set from click to false if opening menu procedurally. -void OpenContextMenu(bool fromClick=true) -{ - if (contextMenu is null) - return; - - contextMenu.enabled = true; - contextMenu.visible = true; - contextMenu.BringToFront(); - if (fromClick) - contextMenuActionWaitFrame=true; -} - -void CloseContextMenu() -{ - if (contextMenu is null) - return; - - contextMenu.enabled = false; - contextMenu.visible = false; -} - -void ActivateContextMenu(Array actions) -{ - contextMenu.RemoveAllChildren(); - for (uint i=0; i< actions.length; ++i) - { - contextMenu.AddChild(actions[i]); - } - contextMenu.SetFixedHeight(24*actions.length+6); - contextMenu.position = ui.cursor.screenPosition + IntVector2(10,-10); - OpenContextMenu(); -} - -Menu@ CreateContextMenuItem(String text, String handler, String menuName = "", bool autoLocalize = true) -{ - Menu@ menu = Menu(); - menu.defaultStyle = uiStyle; - menu.style = AUTO_STYLE; - menu.name = menuName; - menu.SetLayout(LM_HORIZONTAL, 0, IntRect(8, 2, 8, 2)); - Text@ menuText = Text(); - menuText.style = "EditorMenuText"; - menu.AddChild(menuText); - menuText.text = text; - menuText.autoLocalizable = autoLocalize; - menu.vars[VAR_CONTEXT_MENU_HANDLER] = handler; - SubscribeToEvent(menu, "Released", "ContextMenuEventWrapper"); - return menu; -} - -void ContextMenuEventWrapper(StringHash eventType, VariantMap& eventData) -{ - UIElement@ uiElement = eventData["Element"].GetPtr(); - if (uiElement is null) - return; - - String handler = uiElement.vars[VAR_CONTEXT_MENU_HANDLER].GetString(); - if (!handler.empty) - { - SubscribeToEvent(uiElement, "Released", handler); - uiElement.SendEvent("Released", eventData); - } - CloseContextMenu(); -} - -/// Load a UI XML file used by the editor -XMLFile@ GetEditorUIXMLFile(const String&in fileName) -{ - // Prefer the executable path to avoid using the user's resource path, which may point - // to an outdated Urho installation - String fullFileName = fileSystem.programDir + "Data/" + fileName; - if (fileSystem.FileExists(fullFileName)) - { - File@ file = File(fullFileName, FILE_READ); - XMLFile@ xml = XMLFile(); - xml.name = fileName; - if (xml.Load(file)) - return xml; - } - - // Fallback to resource system - return cache.GetResource("XMLFile", fileName); -} - - -/// Load an UI layout used by the editor -UIElement@ LoadEditorUI(const String&in fileName) -{ - return ui.LoadLayout(GetEditorUIXMLFile(fileName)); -} - -/// Set node children as a spline path, either cyclic or non-cyclic -bool SetSplinePath() -{ - Menu@ menu = GetEventSender(); - if (menu is null) - return false; - - return SceneSetChildrenSplinePath(menu.name == "Cyclic"); -} - -bool ColorWheelBuildMenuSelectTypeColor() -{ - if (selectedNodes.empty && selectedComponents.empty) return false; - editMode = EDIT_SELECT; - - // do coloring only for single selected object - // start with trying to find single component - if (selectedComponents.length == 1) - { - coloringComponent = selectedComponents[0]; - } - // else try to get first component from selected node - else if (selectedNodes.length == 1) - { - Array components = selectedNodes[0].GetComponents(); - if (components.length > 0) - { - coloringComponent = components[0]; - } - } - else - return false; - - if (coloringComponent is null) return false; - - Array actions; - - if (coloringComponent.typeName == "Light") - { - actions.Push(CreateContextMenuItem("Light color", "HandleColorWheelMenu", "menuLightColor")); - actions.Push(CreateContextMenuItem("Specular intensity", "HandleColorWheelMenu", "menuSpecularIntensity")); - actions.Push(CreateContextMenuItem("Brightness multiplier", "HandleColorWheelMenu", "menuBrightnessMultiplier")); - - actions.Push(CreateContextMenuItem("Cancel", "HandleColorWheelMenu", "menuCancel")); - - } - else if (coloringComponent.typeName == "StaticModel") - { - actions.Push(CreateContextMenuItem("Diffuse color", "HandleColorWheelMenu", "menuDiffuseColor")); - actions.Push(CreateContextMenuItem("Specular color", "HandleColorWheelMenu", "menuSpecularColor")); - actions.Push(CreateContextMenuItem("Emissive color", "HandleColorWheelMenu", "menuEmissiveColor")); - actions.Push(CreateContextMenuItem("Environment map color", "HandleColorWheelMenu", "menuEnvironmentMapColor")); - - actions.Push(CreateContextMenuItem("Cancel", "HandleColorWheelMenu", "menuCancel")); - } - else if (coloringComponent.typeName == "Zone") - { - actions.Push(CreateContextMenuItem("Ambient color", "HandleColorWheelMenu", "menuAmbientColor")); - actions.Push(CreateContextMenuItem("Fog color", "HandleColorWheelMenu", "menuFogColor")); - - actions.Push(CreateContextMenuItem("Cancel", "HandleColorWheelMenu", "menuCancel")); - } - else if (coloringComponent.typeName == "Text3D") - { - actions.Push(CreateContextMenuItem("Color", "HandleColorWheelMenu", "c")); - actions.Push(CreateContextMenuItem("Top left color", "HandleColorWheelMenu", "tl")); - actions.Push(CreateContextMenuItem("Top right color", "HandleColorWheelMenu", "tr")); - actions.Push(CreateContextMenuItem("Bottom left color", "HandleColorWheelMenu", "bl")); - actions.Push(CreateContextMenuItem("Bottom right color", "HandleColorWheelMenu", "br")); - actions.Push(CreateContextMenuItem("Cancel", "HandleColorWheelMenu", "menuCancel")); - } - if (actions.length > 0) { - ActivateContextMenu(actions); - return true; - } - - return false; -} - -void HandleColorWheelMenu() -{ - ColorWheelSetupBehaviorForColoring(); -} - -// color was changed, update color of all colorGroup for immediate preview; -void HandleWheelChangeColor(StringHash eventType, VariantMap& eventData) -{ - if (timeToNextColoringGroupUpdate > time.systemTime) return; - - if (coloringComponent !is null) - { - Color c = eventData["Color"].GetColor(); // current ColorWheel - // preview new color - if (coloringComponent.typeName == "Light") - { - Light@ light = cast(coloringComponent); - if (light !is null) - { - if (coloringPropertyName == "menuLightColor") - { - light.color = c; - } - else if (coloringPropertyName == "menuSpecularIntensity") - { - // multiply out - light.specularIntensity = c.Value() * 10.0f; - - } - else if (coloringPropertyName == "menuBrightnessMultiplier") - { - light.brightness = c.Value() * 10.0f; - - } - - attributesDirty = true; - } - } - else if (coloringComponent.typeName == "StaticModel") - { - StaticModel@ model = cast(coloringComponent); - if (model !is null) - { - Material@ mat = model.materials[0]; - if (mat !is null) - { - if (coloringPropertyName == "menuDiffuseColor") - { - Variant oldValue = mat.shaderParameters["MatDiffColor"]; - Variant newValue; - String valueString; - valueString += String(c.r).Substring(0,5); - valueString += " "; - valueString += String(c.g).Substring(0,5); - valueString += " "; - valueString += String(c.b).Substring(0,5); - valueString += " "; - valueString += String(c.a).Substring(0,5); - newValue.FromString(oldValue.type, valueString); - mat.shaderParameters["MatDiffColor"] = newValue; - } - else if (coloringPropertyName == "menuSpecularColor") - { - Variant oldValue = mat.shaderParameters["MatSpecColor"]; - Variant newValue; - String valueString; - valueString += String(c.r).Substring(0,5); - valueString += " "; - valueString += String(c.g).Substring(0,5); - valueString += " "; - valueString += String(c.b).Substring(0,5); - valueString += " "; - valueString += String(c.a * 128).Substring(0,5); - newValue.FromString(oldValue.type, valueString); - mat.shaderParameters["MatSpecColor"] = newValue; - } - else if (coloringPropertyName == "menuEmissiveColor") - { - Variant oldValue = mat.shaderParameters["MatEmissiveColor"]; - Variant newValue; - String valueString; - valueString += String(c.r).Substring(0,5); - valueString += " "; - valueString += String(c.g).Substring(0,5); - valueString += " "; - valueString += String(c.b).Substring(0,5); - valueString += " "; - valueString += String(c.a).Substring(0,5); - newValue.FromString(oldValue.type, valueString); - mat.shaderParameters["MatEmissiveColor"] = newValue; - } - else if (coloringPropertyName == "menuEnvironmentMapColor") - { - Variant oldValue = mat.shaderParameters["MatEnvMapColor"]; - Variant newValue; - String valueString; - valueString += String(c.r).Substring(0,5); - valueString += " "; - valueString += String(c.g).Substring(0,5); - valueString += " "; - valueString += String(c.b).Substring(0,5); - valueString += " "; - valueString += String(c.a).Substring(0,5); - newValue.FromString(oldValue.type, valueString); - mat.shaderParameters["MatEnvMapColor"] = newValue; - } - } - } - } - else if (coloringComponent.typeName == "Zone") - { - Zone@ zone = cast(coloringComponent); - if (zone !is null) - { - if (coloringPropertyName == "menuAmbientColor") - { - zone.ambientColor = c; - } - else if (coloringPropertyName == "menuFogColor") - { - zone.fogColor = c; - } - - attributesDirty = true; - } - } - else if (coloringComponent.typeName == "Text3D") - { - Text3D@ txt = cast(coloringComponent); - if (txt !is null) - { - if (coloringPropertyName == "c") - txt.color = c; - else if (coloringPropertyName == "tl") - txt.colors[C_TOPLEFT] = c; - else if (coloringPropertyName == "tr") - txt.colors[C_TOPRIGHT] = c; - else if (coloringPropertyName == "bl") - txt.colors[C_BOTTOMLEFT] = c; - else if (coloringPropertyName == "br") - txt.colors[C_BOTTOMRIGHT] = c; - attributesDirty = true; - } - } - } - - timeToNextColoringGroupUpdate = time.systemTime + stepColoringGroupUpdate; -} - -// Return old colors, wheel was closed or color discarded -void HandleWheelDiscardColor(StringHash eventType, VariantMap& eventData) -{ - if (coloringComponent !is null) - { - //Color oldColor = eventData["Color"].GetColor(); //Old color from ColorWheel from ShowColorWheelWithColor(old) - Color oldColor = coloringOldColor; - - // preview new color - if (coloringComponent.typeName == "Light") - { - Light@ light = cast(coloringComponent); - if (light !is null) - { - if (coloringPropertyName == "menuLightColor") - { - light.color = oldColor; - } - else if (coloringPropertyName == "menuSpecularIntensity") - { - light.specularIntensity = coloringOldScalar * 10.0f; - - } - else if (coloringPropertyName == "menuBrightnessMultiplier") - { - light.brightness = coloringOldScalar * 10.0f; - - } - - attributesDirty = true; - } - } - else if (coloringComponent.typeName == "StaticModel") - { - StaticModel@ model = cast(coloringComponent); - if (model !is null) - { - Material@ mat = model.materials[0]; - if (mat !is null) - { - if (coloringPropertyName == "menuDiffuseColor") - { - Variant oldValue = mat.shaderParameters["MatDiffColor"]; - Variant newValue; - String valueString; - valueString += String(oldColor.r).Substring(0,5); - valueString += " "; - valueString += String(oldColor.g).Substring(0,5); - valueString += " "; - valueString += String(oldColor.b).Substring(0,5); - valueString += " "; - valueString += String(oldColor.a).Substring(0,5); - newValue.FromString(oldValue.type, valueString); - mat.shaderParameters["MatDiffColor"] = newValue; - } - else if (coloringPropertyName == "menuSpecularColor") - { - Variant oldValue = mat.shaderParameters["MatSpecColor"]; - Variant newValue; - String valueString; - valueString += String(oldColor.r).Substring(0,5); - valueString += " "; - valueString += String(oldColor.g).Substring(0,5); - valueString += " "; - valueString += String(oldColor.b).Substring(0,5); - valueString += " "; - valueString += String(coloringOldScalar).Substring(0,5); - newValue.FromString(oldValue.type, valueString); - mat.shaderParameters["MatSpecColor"] = newValue; - } - else if (coloringPropertyName == "menuEmissiveColor") - { - Variant oldValue = mat.shaderParameters["MatEmissiveColor"]; - Variant newValue; - String valueString; - valueString += String(oldColor.r).Substring(0,5); - valueString += " "; - valueString += String(oldColor.g).Substring(0,5); - valueString += " "; - valueString += String(oldColor.b).Substring(0,5); - valueString += " "; - valueString += String(oldColor.a).Substring(0,5); - newValue.FromString(oldValue.type, valueString); - mat.shaderParameters["MatEmissiveColor"] = newValue; - } - else if (coloringPropertyName == "menuEnvironmentMapColor") - { - Variant oldValue = mat.shaderParameters["MatEnvMapColor"]; - Variant newValue; - String valueString; - valueString += String(oldColor.r).Substring(0,5); - valueString += " "; - valueString += String(oldColor.g).Substring(0,5); - valueString += " "; - valueString += String(oldColor.b).Substring(0,5); - valueString += " "; - valueString += String(oldColor.a).Substring(0,5); - newValue.FromString(oldValue.type, valueString); - mat.shaderParameters["MatEnvMapColor"] = newValue; - } - } - } - } - else if (coloringComponent.typeName == "Zone") - { - Zone@ zone = cast(coloringComponent); - if (zone !is null) - { - if (coloringPropertyName == "menuAmbientColor") - { - zone.ambientColor = oldColor; - } - else if (coloringPropertyName == "menuFogColor") - { - zone.fogColor = oldColor; - } - - attributesDirty = true; - } - } - } -} - -// Applying color wheel changes to material -void HandleWheelSelectColor(StringHash eventType, VariantMap& eventData) -{ - if (coloringComponent !is null) - if (coloringComponent.typeName == "StaticModel") - { - Color c = eventData["Color"].GetColor(); //Selected color from ColorWheel - StaticModel@ model = cast(coloringComponent); - if (model !is null) - { - Material@ mat = model.materials[0]; - if (mat !is null) - { - editMaterial = mat; - SaveMaterial(); - } - } - } -} - -bool ViewDebugIcons() -{ - debugIconsShow = !debugIconsShow; - return true; -} +// Urho3D editor user interface + +XMLFile@ uiStyle; +XMLFile@ iconStyle; +UIElement@ uiMenuBar; +UIElement@ quickMenu; +Menu@ recentSceneMenu; +Window@ mruScenesPopup; +Array quickMenuItems; +FileSelector@ uiFileSelector; +String consoleCommandInterpreter; +Window@ contextMenu; +uint stepColoringGroupUpdate = 100; // ms +uint timeToNextColoringGroupUpdate = 0; + +const StringHash UI_ELEMENT_TYPE("UIElement"); +const StringHash WINDOW_TYPE("Window"); +const StringHash MENU_TYPE("Menu"); +const StringHash TEXT_TYPE("Text"); +const StringHash CURSOR_TYPE("Cursor"); + +const String AUTO_STYLE(""); // Empty string means auto style, i.e. applying style according to UI-element's type automatically +const String TEMP_SCENE_NAME("_tempscene_.xml"); +const String TEMP_BINARY_SCENE_NAME("_tempscene_.bin"); +const StringHash CALLBACK_VAR("Callback"); +const StringHash INDENT_MODIFIED_BY_ICON_VAR("IconIndented"); + +const StringHash VAR_CONTEXT_MENU_HANDLER("ContextMenuHandler"); + +const int SHOW_POPUP_INDICATOR = -1; +const uint MAX_QUICK_MENU_ITEMS = 10; + +const uint maxRecentSceneCount = 5; + +Array uiSceneFilters = {"*.xml", "*.json", "*.bin", "*.*"}; +Array uiElementFilters = {"*.xml"}; +Array uiAllFilters = {"*.*"}; +Array uiScriptFilters = {"*.as", "*.*"}; +Array uiParticleFilters = {"*.xml"}; +Array uiRenderPathFilters = {"*.xml"}; +Array uiExportPathFilters = {"*.obj"}; +uint uiSceneFilter = 0; +uint uiElementFilter = 0; +uint uiNodeFilter = 0; +uint uiImportFilter = 0; +uint uiScriptFilter = 0; +uint uiParticleFilter = 0; +uint uiRenderPathFilter = 0; +uint uiExportFilter = 0; +String uiScenePath = fileSystem.programDir + "Data/Scenes"; +String uiElementPath = fileSystem.programDir + "Data/UI"; +String uiNodePath = fileSystem.programDir + "Data/Objects"; +String uiImportPath; +String uiExportPath; +String uiScriptPath = fileSystem.programDir + "Data/Scripts"; +String uiParticlePath = fileSystem.programDir + "Data/Particles"; +String uiRenderPathPath = fileSystem.programDir + "CoreData/RenderPaths"; +Array uiRecentScenes; +String screenshotDir = fileSystem.programDir + "Screenshots"; + +bool uiFaded = false; +float uiMinOpacity = 0.3; +float uiMaxOpacity = 0.7; +bool uiHidden = false; + +TerrainEditor terrainEditor; + +void CreateUI() +{ + // Remove all existing UI content in case we are reloading the editor script + /// \todo The console will not be properly recreated as it has already been created once + ui.root.RemoveAllChildren(); + + uiStyle = GetEditorUIXMLFile("Editor/UI/EditorStyle.xml"); + ui.root.defaultStyle = uiStyle; + iconStyle = GetEditorUIXMLFile("Editor/UI/EditorIcons.xml"); + + graphics.windowIcon = cache.GetResource("Image", "Editor/Textures/UrhoIcon.png"); + + CreateCursor(); + CreateMenuBar(); + CreateToolBar(); + CreateSecondaryToolBar(); + CreateQuickMenu(); + CreateContextMenu(); + CreateHierarchyWindow(); + CreateAttributeInspectorWindow(); + CreateEditorSettingsDialog(); + CreateEditorPreferencesDialog(); + CreateMaterialEditor(); + CreateParticleEffectEditor(); + CreateSpawnEditor(); + CreateSoundTypeEditor(); + CreateStatsBar(); + CreateConsole(); + CreateDebugHud(); + CreateResourceBrowser(); + CreateCamera(); + CreateLayerEditor(); + CreateColorWheel(); + CreateDuplicatorEditor(); + + terrainEditor.Create(); + + SubscribeToEvent("ScreenMode", "ResizeUI"); + SubscribeToEvent("MenuSelected", "HandleMenuSelected"); + SubscribeToEvent("ChangeLanguage", "HandleChangeLanguage"); + + SubscribeToEvent("WheelChangeColor", "HandleWheelChangeColor"); + SubscribeToEvent("WheelSelectColor", "HandleWheelSelectColor"); + SubscribeToEvent("WheelDiscardColor", "HandleWheelDiscardColor"); +} + +void ResizeUI() +{ + // Resize menu bar + uiMenuBar.SetFixedWidth(graphics.width / ui.scale); + + // Resize tool bar + toolBar.SetFixedWidth(graphics.width / ui.scale); + + // Resize secondary tool bar + secondaryToolBar.SetFixedHeight(graphics.height / ui.scale); + + // Relayout windows + Array children = ui.root.GetChildren(); + for (uint i = 0; i < children.length; ++i) + { + if (children[i].type == WINDOW_TYPE) + AdjustPosition(children[i]); + } + + // Relayout root UI element + editorUIElement.SetSize(graphics.width, graphics.height); + + // Set new viewport area and reset the viewport layout. Note: viewportArea is in scaled UI position. + viewportArea = IntRect(0, 0, graphics.width / ui.scale, graphics.height / ui.scale); + SetViewportMode(viewportMode); +} + +void AdjustPosition(Window@ window) +{ + IntVector2 position = window.position; + IntVector2 size = window.size; + IntVector2 extend = position + size; + if (extend.x > graphics.width) + position.x = Max(10, graphics.width - size.x - 10); + if (extend.y > graphics.height) + position.y = Max(100, graphics.height - size.y - 10); + window.position = position; +} + +void CreateCursor() +{ + Cursor@ cursor = Cursor("Cursor"); + cursor.SetStyleAuto(uiStyle); + cursor.SetPosition(graphics.width / 2, graphics.height / 2); + ui.cursor = cursor; + if (GetPlatform() == "Android" || GetPlatform() == "iOS") + ui.cursor.visible = false; +} + +// AngelScript does not support closures (yet), but funcdef should do just fine as a workaround for a few cases here for now +funcdef bool MENU_CALLBACK(); +Array menuCallbacks; +MENU_CALLBACK@ messageBoxCallback; + +void HandleQuickSearchChange(StringHash eventType, VariantMap& eventData) +{ + LineEdit@ search = eventData["Element"].GetPtr(); + if (search is null) + return; + + PerformQuickMenuSearch(search.text.ToLower().Trimmed()); +} + +void HandleQuickSearchFinish(StringHash eventType, VariantMap& eventData) +{ + Menu@ menu = quickMenu.GetChild("ResultsMenu", true); + if (menu is null) + return; + + String query = eventData["Text"].GetString(); + if (query.length <= 0) + return; + Array filtered; + { + QuickMenuItem@ qi; + for (uint x=0; x < quickMenuItems.length; x++) + { + @qi = quickMenuItems[x]; + int find = qi.action.Find(query, 0, false); + if (find > -1) + { + qi.sortScore = find; + filtered.Push(qi); + } + } + } + + filtered.Sort(); + if (!filtered.empty) + { + VariantMap data; + Menu@ item = CreateMenuItem(filtered[0].action, filtered[0].callback); + data["Element"] = item; + item.SendEvent("MenuSelected", data); + } +} + +void PerformQuickMenuSearch(const String&in query) +{ + Menu@ menu = quickMenu.GetChild("ResultsMenu", true); + if (menu is null) + return; + + menu.RemoveAllChildren(); + uint limit = 0; + + if (query.length > 0) + { + int lastIndex = 0; + uint score = 0; + int index = 0; + + Array filtered; + { + QuickMenuItem@ qi; + for (uint x=0; x < quickMenuItems.length; x++) + { + @qi = quickMenuItems[x]; + int find = qi.action.Find(query, 0, false); + if (find > -1) + { + qi.sortScore = find; + filtered.Push(qi); + } + } + } + + filtered.Sort(); + + { + QuickMenuItem@ qi; + limit = filtered.length > MAX_QUICK_MENU_ITEMS ? MAX_QUICK_MENU_ITEMS : filtered.length; + for (uint x=0; x < limit; x++) + { + @qi = filtered[x]; + Menu@ item = CreateMenuItem(qi.action, qi.callback); + item.SetMaxSize(1000,16); + menu.AddChild(item); + } + } + } + + menu.visible = limit > 0; + menu.SetFixedHeight(limit * 16); + quickMenu.BringToFront(); + quickMenu.SetFixedHeight(limit*16 + 62 + (menu.visible ? 6 : 0)); +} + +class QuickMenuItem +{ + String action; + MENU_CALLBACK@ callback; + uint sortScore = 0; + QuickMenuItem(){} + QuickMenuItem(String action, MENU_CALLBACK@ callback) + { + this.action = action; + this.callback = callback; + } + int opCmp(QuickMenuItem@ b) + { + return sortScore - b.sortScore; + } +} + +/// Create popup search menu. +void CreateQuickMenu() +{ + if (quickMenu !is null) + return; + + quickMenu = LoadEditorUI("Editor/UI/EditorQuickMenu.xml"); + quickMenu.enabled = false; + quickMenu.visible = false; + quickMenu.opacity = uiMaxOpacity; + + // Handle a dummy search in the quick menu to finalize its initial size to empty + PerformQuickMenuSearch(""); + + ui.root.AddChild(quickMenu); + LineEdit@ search = quickMenu.GetChild("Search", true); + SubscribeToEvent(search, "TextChanged", "HandleQuickSearchChange"); + SubscribeToEvent(search, "TextFinished", "HandleQuickSearchFinish"); + UIElement@ closeButton = quickMenu.GetChild("CloseButton", true); + SubscribeToEvent(closeButton, "Pressed", "ToggleQuickMenu"); +} + +void ToggleQuickMenu() +{ + quickMenu.enabled = !quickMenu.enabled && ui.cursor.visible; + quickMenu.visible = quickMenu.enabled; + if (quickMenu.enabled) + { + quickMenu.position = ui.cursorPosition - IntVector2(20,70); + LineEdit@ search = quickMenu.GetChild("Search", true); + search.text = ""; + search.focus = true; + } +} + +/// Create top menu bar. +void CreateMenuBar() +{ + uiMenuBar = BorderImage("MenuBar"); + ui.root.AddChild(uiMenuBar); + uiMenuBar.enabled = true; + uiMenuBar.style = "EditorMenuBar"; + uiMenuBar.SetLayout(LM_HORIZONTAL); + uiMenuBar.opacity = uiMaxOpacity; + uiMenuBar.SetFixedWidth(graphics.width / ui.scale); + + { + Menu@ menu = CreateMenu("File"); + Window@ popup = menu.popup; + popup.AddChild(CreateMenuItem("New scene", @ResetScene, KEY_N, QUAL_SHIFT | QUAL_CTRL)); + popup.AddChild(CreateMenuItem("Open scene...", @PickFile, KEY_O, QUAL_CTRL)); + popup.AddChild(CreateMenuItem("Save scene", @SaveSceneWithExistingName, KEY_S, QUAL_CTRL)); + popup.AddChild(CreateMenuItem("Save scene as...", @PickFile, KEY_S, QUAL_SHIFT | QUAL_CTRL)); + recentSceneMenu = CreateMenuItem("Open recent scene", null, SHOW_POPUP_INDICATOR); + popup.AddChild(recentSceneMenu); + mruScenesPopup = CreatePopup(recentSceneMenu); + PopulateMruScenes(); + CreateChildDivider(popup); + + Menu@ childMenu = CreateMenuItem("menu Load node", null, SHOW_POPUP_INDICATOR); + Window@ childPopup = CreatePopup(childMenu); + childPopup.AddChild(CreateMenuItem("As replicated...", @PickFile, 0, 0, true, "Load node as replicated...")); + childPopup.AddChild(CreateMenuItem("As local...", @PickFile, 0, 0, true, "Load node as local...")); + popup.AddChild(childMenu); + + popup.AddChild(CreateMenuItem("Save node as...", @PickFile)); + CreateChildDivider(popup); + popup.AddChild(CreateMenuItem("Import model...", @PickFile)); + popup.AddChild(CreateMenuItem("Import scene...", @PickFile)); + popup.AddChild(CreateMenuItem("Import animation...", @PickFile)); + CreateChildDivider(popup); + popup.AddChild(CreateMenuItem("Export scene to OBJ...", @PickFile)); + popup.AddChild(CreateMenuItem("Export selected to OBJ...", @PickFile)); + CreateChildDivider(popup); + popup.AddChild(CreateMenuItem("Run script...", @PickFile)); + popup.AddChild(CreateMenuItem("Set resource path...", @PickFile)); + CreateChildDivider(popup); + popup.AddChild(CreateMenuItem("Exit", @Exit)); + FinalizedPopupMenu(popup); + uiMenuBar.AddChild(menu); + } + + { + Menu@ menu = CreateMenu("Edit"); + Window@ popup = menu.popup; + popup.AddChild(CreateMenuItem("Undo", @Undo, KEY_Z, QUAL_CTRL)); + popup.AddChild(CreateMenuItem("Redo", @Redo, KEY_Y, QUAL_CTRL)); + CreateChildDivider(popup); + popup.AddChild(CreateMenuItem("Cut", @Cut, KEY_X, QUAL_CTRL)); + + if (hotKeyMode == HOTKEYS_MODE_STANDARD) + popup.AddChild(CreateMenuItem("Duplicate", @Duplicate, KEY_D, QUAL_CTRL)); + else if (hotKeyMode == HOTKEYS_MODE_BLENDER) + popup.AddChild(CreateMenuItem("Duplicate", @Duplicate, KEY_D, QUAL_SHIFT)); + + popup.AddChild(CreateMenuItem("Copy", @Copy, KEY_C, QUAL_CTRL)); + popup.AddChild(CreateMenuItem("Paste", @Paste, KEY_V, QUAL_CTRL)); + + if (hotKeyMode == HOTKEYS_MODE_STANDARD) + popup.AddChild(CreateMenuItem("Delete", @Delete, KEY_DELETE, QUAL_ANY)); + else if (hotKeyMode == HOTKEYS_MODE_BLENDER) + popup.AddChild(CreateMenuItem("Delete", @BlenderModeDelete, KEY_X, QUAL_ANY)); + + popup.AddChild(CreateMenuItem("Select all", @SelectAll, KEY_A, QUAL_CTRL)); + popup.AddChild(CreateMenuItem("Deselect all", @DeselectAll, KEY_A, QUAL_SHIFT | QUAL_CTRL)); + + CreateChildDivider(popup); + popup.AddChild(CreateMenuItem("Reset to default", @ResetToDefault)); + CreateChildDivider(popup); + + if (hotKeyMode == HOTKEYS_MODE_STANDARD) + { + popup.AddChild(CreateMenuItem("Reset position", @SceneResetPosition, '1' , QUAL_ALT)); + popup.AddChild(CreateMenuItem("Reset rotation", @SceneResetRotation, '2' , QUAL_ALT)); + popup.AddChild(CreateMenuItem("Reset scale", @SceneResetScale, '3' , QUAL_ALT)); + popup.AddChild(CreateMenuItem("Reset transform", @SceneResetTransform, KEY_Q , QUAL_ALT)); + } + else if (hotKeyMode == HOTKEYS_MODE_BLENDER) + { + popup.AddChild(CreateMenuItem("Reset position", @SceneResetPosition, KEY_G , QUAL_ALT)); + popup.AddChild(CreateMenuItem("Reset rotation", @SceneResetRotation, KEY_R, QUAL_ALT)); + popup.AddChild(CreateMenuItem("Reset scale", @SceneResetScale, KEY_S, QUAL_ALT)); + popup.AddChild(CreateMenuItem("Reset transform", @SceneResetTransform, KEY_Q , QUAL_ALT)); + } + + if (hotKeyMode == HOTKEYS_MODE_STANDARD) + { + popup.AddChild(CreateMenuItem("Enable/disable", @SceneToggleEnable, KEY_E, QUAL_CTRL)); + popup.AddChild(CreateMenuItem("Enable all", @SceneEnableAllNodes, KEY_E, QUAL_ALT)); + } + else if (hotKeyMode == HOTKEYS_MODE_BLENDER) + { + popup.AddChild(CreateMenuItem("Enable/disable", @SceneToggleEnable, KEY_H)); + popup.AddChild(CreateMenuItem("Enable all", @SceneEnableAllNodes, KEY_H, QUAL_ALT)); + } + + if (hotKeyMode == HOTKEYS_MODE_STANDARD) + popup.AddChild(CreateMenuItem("Unparent", @SceneUnparent, KEY_U, QUAL_CTRL)); + else if (hotKeyMode == HOTKEYS_MODE_BLENDER) + popup.AddChild(CreateMenuItem("Unparent", @SceneUnparent, KEY_P, QUAL_ALT)); + + if (hotKeyMode == HOTKEYS_MODE_STANDARD) + popup.AddChild(CreateMenuItem("Parent to last", @NodesParentToLastSelected, KEY_U)); + else if (hotKeyMode == HOTKEYS_MODE_BLENDER) + popup.AddChild(CreateMenuItem("Parent to last", @NodesParentToLastSelected, KEY_P, QUAL_CTRL)); + + CreateChildDivider(popup); + + if (hotKeyMode == HOTKEYS_MODE_STANDARD) + popup.AddChild(CreateMenuItem("Toggle update", @ToggleSceneUpdate, KEY_P, QUAL_CTRL)); + //else if (hotKeyMode == HOT_KEYS_MODE_BLENDER) + // popup.AddChild(CreateMenuItem("Toggle update", @ToggleSceneUpdate, KEY_P, QUAL_CTRL)); + + if (hotKeyMode == HOTKEYS_MODE_STANDARD) + { + popup.AddChild(CreateMenuItem("View closer", @ViewCloser, KEY_F)); + } + else if (hotKeyMode == HOTKEYS_MODE_BLENDER) + { + popup.AddChild(CreateMenuItem("Move to layer", @ShowLayerMover, KEY_M)); + popup.AddChild(CreateMenuItem("Smart Duplicate", @SceneSmartDuplicateNode, KEY_D, QUAL_ALT)); + popup.AddChild(CreateMenuItem("View closer", @ViewCloser, KEY_KP_PERIOD)); + } + popup.AddChild(CreateMenuItem("Color wheel", @ColorWheelBuildMenuSelectTypeColor, KEY_W, QUAL_ALT)); + popup.AddChild(CreateMenuItem("Show components icons", @ViewDebugIcons, KEY_I, QUAL_ALT)); + + CreateChildDivider(popup); + + popup.AddChild(CreateMenuItem("Stop test animation", @StopTestAnimation)); + CreateChildDivider(popup); + popup.AddChild(CreateMenuItem("Rebuild navigation data", @SceneRebuildNavigation)); + popup.AddChild(CreateMenuItem("Render Zone Cubemap", @SceneRenderZoneCubemaps)); + popup.AddChild(CreateMenuItem("Add children to SM-group", @SceneAddChildrenStaticModelGroup)); + Menu@ childMenu = CreateMenuItem("Set children as spline path", null, SHOW_POPUP_INDICATOR); + Window@ childPopup = CreatePopup(childMenu); + childPopup.AddChild(CreateMenuItem("Non-cyclic", @SetSplinePath, 0, 0, true, "Set non-cyclic spline path")); + childPopup.AddChild(CreateMenuItem("Cyclic", @SetSplinePath, 0, 0, true, "Set cyclic spline path")); + popup.AddChild(childMenu); + FinalizedPopupMenu(popup); + uiMenuBar.AddChild(menu); + } + + { + Menu@ menu = CreateMenu("Create"); + Window@ popup = menu.popup; + popup.AddChild(CreateMenuItem("Replicated node", @PickNode, 0, 0, true, "Create Replicated node")); + popup.AddChild(CreateMenuItem("Local node", @PickNode, 0, 0, true, "Create Local node")); + CreateChildDivider(popup); + + Menu@ childMenu = CreateMenuItem("Component", null, SHOW_POPUP_INDICATOR); + Window@ childPopup = CreatePopup(childMenu); + String[] objectCategories = GetObjectCategories(); + for (uint i = 0; i < objectCategories.length; ++i) + { + // Skip the UI category for the component menus + if (objectCategories[i] == "UI") + continue; + + Menu@ m = CreateMenuItem(objectCategories[i], null, SHOW_POPUP_INDICATOR); + Window@ p = CreatePopup(m); + String[] componentTypes = GetObjectsByCategory(objectCategories[i]); + for (uint j = 0; j < componentTypes.length; ++j) + p.AddChild(CreateIconizedMenuItem(componentTypes[j], @PickComponent, 0, 0, "", true, "Create " + componentTypes[j])); + childPopup.AddChild(m); + } + FinalizedPopupMenu(childPopup); + popup.AddChild(childMenu); + + childMenu = CreateMenuItem("Builtin object", null, SHOW_POPUP_INDICATOR); + childPopup = CreatePopup(childMenu); + String[] objects = { "Box", "Cone", "Cylinder", "Plane", "Pyramid", "Sphere", "TeaPot", "Torus" }; + for (uint i = 0; i < objects.length; ++i) + childPopup.AddChild(CreateIconizedMenuItem(objects[i], @PickBuiltinObject, 0, 0, "Node", true, "Create " + objects[i])); + popup.AddChild(childMenu); + CreateChildDivider(popup); + + childMenu = CreateMenuItem("UI-element", null, SHOW_POPUP_INDICATOR); + childPopup = CreatePopup(childMenu); + String[] uiElementTypes = GetObjectsByCategory("UI"); + for (uint i = 0; i < uiElementTypes.length; ++i) + { + if (uiElementTypes[i] != "UIElement") + childPopup.AddChild(CreateIconizedMenuItem(uiElementTypes[i], @PickUIElement, 0, 0, "", true, "Create " + uiElementTypes[i])); + } + CreateChildDivider(childPopup); + childPopup.AddChild(CreateIconizedMenuItem("UIElement", @PickUIElement)); + popup.AddChild(childMenu); + + FinalizedPopupMenu(popup); + uiMenuBar.AddChild(menu); + } + + { + Menu@ menu = CreateMenu("UI-layout"); + Window@ popup = menu.popup; + popup.AddChild(CreateMenuItem("Open UI-layout...", @PickFile, KEY_O, QUAL_ALT)); + popup.AddChild(CreateMenuItem("Save UI-layout", @SaveUILayoutWithExistingName, KEY_S, QUAL_ALT)); + popup.AddChild(CreateMenuItem("Save UI-layout as...", @PickFile)); + CreateChildDivider(popup); + popup.AddChild(CreateMenuItem("Close UI-layout", @CloseUILayout, KEY_C, QUAL_ALT)); + popup.AddChild(CreateMenuItem("Close all UI-layouts", @CloseAllUILayouts)); + CreateChildDivider(popup); + popup.AddChild(CreateMenuItem("Load child element...", @PickFile)); + popup.AddChild(CreateMenuItem("Save child element as...", @PickFile)); + CreateChildDivider(popup); + popup.AddChild(CreateMenuItem("Set default style...", @PickFile)); + FinalizedPopupMenu(popup); + uiMenuBar.AddChild(menu); + } + + { + Menu@ menu = CreateMenu("View"); + Window@ popup = menu.popup; + popup.AddChild(CreateMenuItem("Hierarchy", @ToggleHierarchyWindow, KEY_H, QUAL_CTRL)); + popup.AddChild(CreateMenuItem("Attribute inspector", @ToggleAttributeInspectorWindow, KEY_I, QUAL_CTRL)); + popup.AddChild(CreateMenuItem("Resource browser", @ToggleResourceBrowserWindow, KEY_B, QUAL_CTRL)); + popup.AddChild(CreateMenuItem("Material editor", @ToggleMaterialEditor)); + popup.AddChild(CreateMenuItem("Particle editor", @ToggleParticleEffectEditor)); + popup.AddChild(CreateMenuItem("Terrain editor", TerrainEditorShowCallback(terrainEditor.Show))); + popup.AddChild(CreateMenuItem("Spawn editor", @ToggleSpawnEditor)); + popup.AddChild(CreateMenuItem("Duplicator editor", @ToggleDuplicatorEditor)); + popup.AddChild(CreateMenuItem("Sound Type editor", @ToggleSoundTypeEditor)); + popup.AddChild(CreateMenuItem("Editor settings", @ToggleEditorSettingsDialog)); + popup.AddChild(CreateMenuItem("Editor preferences", @ToggleEditorPreferencesDialog)); + CreateChildDivider(popup); + popup.AddChild(CreateMenuItem("Hide editor", @ToggleUI, KEY_F12, QUAL_ANY)); + FinalizedPopupMenu(popup); + uiMenuBar.AddChild(menu); + } + + BorderImage@ spacer = BorderImage("MenuBarSpacer"); + uiMenuBar.AddChild(spacer); + spacer.style = "EditorMenuBar"; +} + +bool Exit() +{ + ui.cursor.shape = CS_BUSY; + + if (messageBoxCallback is null) + { + String message; + if (sceneModified) + message = "Scene has been modified.\n"; + + bool uiLayoutModified = false; + for (uint i = 0; i < editorUIElement.numChildren; ++i) + { + UIElement@ element = editorUIElement.children[i]; + if (element !is null && element.vars[MODIFIED_VAR].GetBool()) + { + uiLayoutModified = true; + message += "UI layout has been modified.\n"; + break; + } + } + + if (sceneModified || uiLayoutModified) + { + MessageBox@ messageBox = MessageBox(message + "Continue to exit?", "Warning"); + if (messageBox.window !is null) + { + Button@ cancelButton = messageBox.window.GetChild("CancelButton", true); + cancelButton.visible = true; + cancelButton.focus = true; + SubscribeToEvent(messageBox, "MessageACK", "HandleMessageAcknowledgement"); + messageBoxCallback = @Exit; + return false; + } + } + } + else + messageBoxCallback = null; + + engine.Exit(); + return true; +} + +void HandleExitRequested() +{ + if (!ui.HasModalElement()) + Exit(); +} + +bool PickFile() +{ + Menu@ menu = GetEventSender(); + if (menu is null) + return false; + + String action = menu.name; + if (action.empty) + return false; + + // File (Scene related) + if (action == "Open scene...") + { + CreateFileSelector("Open scene", "Open", "Cancel", uiScenePath, uiSceneFilters, uiSceneFilter); + SubscribeToEvent(uiFileSelector, "FileSelected", "HandleOpenSceneFile"); + } + else if (action == "Save scene as..." || action == "Save scene") + { + CreateFileSelector("Save scene as", "Save", "Cancel", uiScenePath, uiSceneFilters, uiSceneFilter); + uiFileSelector.fileName = GetFileNameAndExtension(editorScene.fileName); + SubscribeToEvent(uiFileSelector, "FileSelected", "HandleSaveSceneFile"); + } + else if (action == "As replicated..." || action == "Load node as replicated...") + { + instantiateMode = REPLICATED; + CreateFileSelector("fileSelector Load node", "Load", "Cancel", uiNodePath, uiSceneFilters, uiNodeFilter); + SubscribeToEvent(uiFileSelector, "FileSelected", "HandleLoadNodeFile"); + } + else if (action == "As local..." || action == "Load node as local...") + { + instantiateMode = LOCAL; + CreateFileSelector("Load node", "Load", "Cancel", uiNodePath, uiSceneFilters, uiNodeFilter); + SubscribeToEvent(uiFileSelector, "FileSelected", "HandleLoadNodeFile"); + } + else if (action == "Save node as...") + { + if (editNode !is null && editNode !is editorScene) + { + CreateFileSelector("Save node", "Save", "Cancel", uiNodePath, uiSceneFilters, uiNodeFilter); + uiFileSelector.fileName = GetFileNameAndExtension(instantiateFileName); + SubscribeToEvent(uiFileSelector, "FileSelected", "HandleSaveNodeFile"); + } + } + else if (action == "Import model...") + { + CreateFileSelector("Import model", "Import", "Cancel", uiImportPath, uiAllFilters, uiImportFilter); + SubscribeToEvent(uiFileSelector, "FileSelected", "HandleImportModel"); + } + else if (action == "Import animation...") + { + CreateFileSelector("Import animation", "Import", "Cancel", uiImportPath, uiAllFilters, uiImportFilter); + SubscribeToEvent(uiFileSelector, "FileSelected", "HandleImportAnimation"); + } + else if (action == "Import scene...") + { + CreateFileSelector("Import scene", "Import", "Cancel", uiImportPath, uiAllFilters, uiImportFilter); + SubscribeToEvent(uiFileSelector, "FileSelected", "HandleImportScene"); + } + else if (action == "Export scene to OBJ..." || action == "Export selected to OBJ...") + { + // Set these up together to share the "export settings" options + if (action == "Export scene to OBJ...") + { + CreateFileSelector("Export scene to OBJ", "Save", "Cancel", uiExportPath, uiExportPathFilters, uiExportFilter); + SubscribeToEvent(uiFileSelector, "FileSelected", "HandleExportSceneOBJ"); + } + else if (action == "Export selected to OBJ...") + { + CreateFileSelector("Export selected to OBJ", "Save", "Cancel", uiExportPath, uiExportPathFilters, uiExportFilter); + SubscribeToEvent(uiFileSelector, "FileSelected", "HandleExportSelectedOBJ"); + } + + Window@ window = uiFileSelector.window; + + UIElement@ optionsGroup = UIElement(); + optionsGroup.maxHeight = 30; + optionsGroup.layoutMode = LM_HORIZONTAL; + window.defaultStyle = uiStyle; + window.style = AUTO_STYLE; + + CheckBox@ checkRightHanded = CheckBox(); + checkRightHanded.checked = objExportRightHanded_; + checkRightHanded.defaultStyle = uiStyle; + checkRightHanded.style = AUTO_STYLE; + SubscribeToEvent(checkRightHanded, "Toggled", "HandleOBJRightHandedChanged"); + optionsGroup.AddChild(checkRightHanded); + + Text@ lblRightHanded = Text(); + lblRightHanded.defaultStyle = uiStyle; + lblRightHanded.style = AUTO_STYLE; + lblRightHanded.text = " Right handed"; + optionsGroup.AddChild(lblRightHanded); + + CheckBox@ checkZUp = CheckBox(); + checkZUp.checked = objExportZUp_; + checkZUp.defaultStyle = uiStyle; + checkZUp.style = AUTO_STYLE; + SubscribeToEvent(checkZUp, "Toggled", "HandleOBJZUpChanged"); + optionsGroup.AddChild(checkZUp); + + Text@ lblZUp = Text(); + lblZUp.defaultStyle = uiStyle; + lblZUp.style = AUTO_STYLE; + lblZUp.text = " Z Axis Up"; + optionsGroup.AddChild(lblZUp); + + window.AddChild(optionsGroup); + } + else if (action == "Run script...") + { + CreateFileSelector("Run script", "Run", "Cancel", uiScriptPath, uiScriptFilters, uiScriptFilter); + SubscribeToEvent(uiFileSelector, "FileSelected", "HandleRunScript"); + } + else if (action == "Set resource path...") + { + CreateFileSelector("Set resource path", "Set", "Cancel", sceneResourcePath, uiAllFilters, 0); + uiFileSelector.directoryMode = true; + SubscribeToEvent(uiFileSelector, "FileSelected", "HandleResourcePath"); + } + // UI-element + else if (action == "Open UI-layout...") + { + CreateFileSelector("Open UI-layout", "Open", "Cancel", uiElementPath, uiElementFilters, uiElementFilter); + SubscribeToEvent(uiFileSelector, "FileSelected", "HandleOpenUILayoutFile"); + } + else if (action == "Save UI-layout as..." || action == "Save UI-layout") + { + if (editUIElement !is null) + { + UIElement@ element = GetTopLevelUIElement(editUIElement); + if (element is null) + return false; + + CreateFileSelector("Save UI-layout as", "Save", "Cancel", uiElementPath, uiElementFilters, uiElementFilter); + uiFileSelector.fileName = GetFileNameAndExtension(element.GetVar(FILENAME_VAR).GetString()); + SubscribeToEvent(uiFileSelector, "FileSelected", "HandleSaveUILayoutFile"); + } + } + else if (action == "Load child element...") + { + if (editUIElement !is null) + { + CreateFileSelector("Load child element", "Load", "Cancel", uiElementPath, uiElementFilters, uiElementFilter); + SubscribeToEvent(uiFileSelector, "FileSelected", "HandleLoadChildUIElementFile"); + } + } + else if (action == "Save child element as...") + { + if (editUIElement !is null) + { + CreateFileSelector("Save child element", "Save", "Cancel", uiElementPath, uiElementFilters, uiElementFilter); + uiFileSelector.fileName = GetFileNameAndExtension(editUIElement.GetVar(CHILD_ELEMENT_FILENAME_VAR).GetString()); + SubscribeToEvent(uiFileSelector, "FileSelected", "HandleSaveChildUIElementFile"); + } + } + else if (action == "Set default style...") + { + CreateFileSelector("Set default style", "Set", "Cancel", uiElementPath, uiElementFilters, uiElementFilter); + SubscribeToEvent(uiFileSelector, "FileSelected", "HandleUIElementDefaultStyle"); + } + + return true; +} + +bool PickNode() +{ + Menu@ menu = GetEventSender(); + if (menu is null) + return false; + + String action = GetActionName(menu.name); + if (action.empty) + return false; + + CreateNode(action == "Replicated node" ? REPLICATED : LOCAL); + return true; +} + +bool PickComponent() +{ + if (editNodes.empty) + return false; + + Menu@ menu = GetEventSender(); + if (menu is null) + return false; + + String action = GetActionName(menu.name); + if (action.empty) + return false; + + CreateComponent(action); + return true; +} + +bool PickBuiltinObject() +{ + Menu@ menu = GetEventSender(); + if (menu is null) + return false; + + String action = GetActionName(menu.name); + if (action.empty) + return false; + + CreateBuiltinObject(action); + return true; +} + +bool PickUIElement() +{ + Menu@ menu = GetEventSender(); + if (menu is null) + return false; + + String action = GetActionName(menu.name); + if (action.empty) + return false; + + return NewUIElement(action); +} + +// When calling items from the quick menu, they have "Create" prepended for clarity. Strip that now to get the object name to create +String GetActionName(const String&in name) +{ + if (name.StartsWith("Create")) + return name.Substring(7); + else + return name; +} + +void HandleMenuSelected(StringHash eventType, VariantMap& eventData) +{ + Menu@ menu = eventData["Element"].GetPtr(); + if (menu is null) + return; + + HandlePopup(menu); + + quickMenu.visible = false; + quickMenu.enabled = false; + + // Execute the callback if available + Variant variant = menu.GetVar(CALLBACK_VAR); + if (!variant.empty) + menuCallbacks[variant.GetU32()](); +} + +Menu@ CreateMenuItem(const String&in title, MENU_CALLBACK@ callback = null, int accelKey = 0, int accelQual = 0, bool addToQuickMenu = true, String quickMenuText="", bool autoLocalize = true) +{ + Menu@ menu = Menu(title); + menu.defaultStyle = uiStyle; + menu.style = AUTO_STYLE; + menu.SetLayout(LM_HORIZONTAL, 0, IntRect(8, 2, 8, 2)); + if (accelKey > 0) + menu.SetAccelerator(accelKey, accelQual); + if (callback !is null) + { + menu.vars[CALLBACK_VAR] = menuCallbacks.length; + menuCallbacks.Push(callback); + } + + Text@ menuText = Text(); + menu.AddChild(menuText); + menuText.style = "EditorMenuText"; + menuText.text = title; + menuText.autoLocalizable = autoLocalize; + + if (addToQuickMenu) + AddQuickMenuItem(callback, quickMenuText.empty ? title : quickMenuText); + + if (accelKey != 0) + { + UIElement@ spacer = UIElement(); + spacer.minWidth = menuText.indentSpacing; + spacer.height = menuText.height; + menu.AddChild(spacer); + menu.AddChild(CreateAccelKeyText(accelKey, accelQual)); + } + + return menu; +} + +void AddQuickMenuItem(MENU_CALLBACK@ callback, String text) +{ + if (callback is null) + return; + + bool exists = false; + for (uint i=0;i 0) + menu.SetAccelerator(accelKey, accelQual); + if (callback !is null) + { + menu.vars[CALLBACK_VAR] = menuCallbacks.length; + menuCallbacks.Push(callback); + } + + Text@ menuText = Text(); + menu.AddChild(menuText); + menuText.style = "EditorMenuText"; + menuText.text = title; + // If icon type is not provided, use the title instead + IconizeUIElement(menuText, iconType.empty ? title : iconType); + + if (addToQuickMenu) + AddQuickMenuItem(callback, quickMenuText.empty ? title : quickMenuText); + + if (accelKey != 0) + { + menuText.layoutMode = LM_HORIZONTAL; + menuText.AddChild(CreateAccelKeyText(accelKey, accelQual)); + } + + return menu; +} + +/// Create a child divider in parent with vertical layout mode. It works on other parent element as well, not just parent menu. +void CreateChildDivider(UIElement@ parent) +{ + BorderImage@ divider = parent.CreateChild("BorderImage", "Divider"); + divider.style = "EditorDivider"; +} + +Window@ CreatePopup(Menu@ baseMenu) +{ + Window@ popup = Window(); + popup.defaultStyle = uiStyle; + popup.style = AUTO_STYLE; + popup.SetLayout(LM_VERTICAL, 1, IntRect(2, 6, 2, 6)); + baseMenu.popup = popup; + baseMenu.popupOffset = IntVector2(0, baseMenu.height); + + return popup; +} + +Menu@ CreateMenu(const String&in title) +{ + Menu@ menu = CreateMenuItem(title); + Text@ text = menu.children[0]; + menu.maxWidth = text.width + 20; + CreatePopup(menu); + + return menu; +} + +void HandleChangeLanguage(StringHash eventType, VariantMap& eventData) +{ + Array children = uiMenuBar.GetChildren(); + + for (uint i = 0; i < children.length - 2; ++i) // last 2 elements is not menu + { + // dirty hack: force recalc text size + children[i].maxWidth = 1000; + Text@ text = children[i].children[0]; + text.minWidth = 0; + text.maxWidth = 1; + text.ApplyAttributes(); + children[i].maxWidth = text.width + 20; + } + + RebuildResourceDatabase(); +} + +Text@ CreateAccelKeyText(int accelKey, int accelQual) +{ + Text@ accelKeyText = Text(); + accelKeyText.defaultStyle = uiStyle; + accelKeyText.style = "EditorMenuText"; + accelKeyText.textAlignment = HA_RIGHT; + + String text; + if (accelKey == KEY_DELETE) + text = "Del"; + else if (accelKey == KEY_SPACE) + text = "Space"; + // Cannot use range as the key constants below do not appear to be in sequence + else if (accelKey == KEY_F1) + text = "F1"; + else if (accelKey == KEY_F2) + text = "F2"; + else if (accelKey == KEY_F3) + text = "F3"; + else if (accelKey == KEY_F4) + text = "F4"; + else if (accelKey == KEY_F5) + text = "F5"; + else if (accelKey == KEY_F6) + text = "F6"; + else if (accelKey == KEY_F7) + text = "F7"; + else if (accelKey == KEY_F8) + text = "F8"; + else if (accelKey == KEY_F9) + text = "F9"; + else if (accelKey == KEY_F10) + text = "F10"; + else if (accelKey == KEY_F11) + text = "F11"; + else if (accelKey == KEY_F12) + text = "F12"; + else if (accelKey == SHOW_POPUP_INDICATOR) + text = ">"; + else if (accelKey == KEY_KP_PERIOD) + text = "NumPad ."; + else + text.AppendUTF8(accelKey); + if (accelQual & QUAL_ALT > 0) + text = "Alt+" + text; + if (accelQual & QUAL_SHIFT > 0) + text = "Shift+" + text; + if (accelQual & QUAL_CTRL > 0) + text = "Ctrl+" + text; + accelKeyText.text = text; + + return accelKeyText; +} + +void FinalizedPopupMenu(Window@ popup) +{ + // Find the maximum menu text width + Array children = popup.GetChildren(); + int maxWidth = 0; + for (uint i = 0; i < children.length; ++i) + { + UIElement@ element = children[i]; + if (element.type != MENU_TYPE) // Skip if not menu item + continue; + + int width = element.children[0].width; + if (width > maxWidth) + maxWidth = width; + } + + // Adjust the indent spacing to slightly wider than the maximum width + maxWidth += 20; + for (uint i = 0; i < children.length; ++i) + { + UIElement@ element = children[i]; + if (element.type != MENU_TYPE) + continue; + Menu@ menu = element; + + Text@ menuText = menu.children[0]; + if (menuText.numChildren == 1) // Skip if menu text does not have accel + menuText.children[0].indentSpacing = maxWidth; + + // Adjust the popup offset taking the indentation into effect + if (menu.popup !is null) + menu.popupOffset = IntVector2(menu.width, 0); + } +} + +void CreateFileSelector(const String&in title, const String&in ok, const String&in cancel, const String&in initialPath, Array@ filters, + uint initialFilter, bool autoLocalizeTitle = true) +{ + // Within the editor UI, the file selector is a kind of a "singleton". When the previous one is overwritten, also + // the events subscribed from it are disconnected, so new ones are safe to subscribe. + uiFileSelector = FileSelector(); + uiFileSelector.defaultStyle = uiStyle; + uiFileSelector.title = title; + uiFileSelector.titleText.autoLocalizable = autoLocalizeTitle; + uiFileSelector.path = initialPath; + uiFileSelector.SetButtonTexts(ok, cancel); + Text@ okText = cast(uiFileSelector.okButton.children[0]); + okText.autoLocalizable = true; + Text@ cancelText = cast(uiFileSelector.cancelButton.children[0]); + cancelText.autoLocalizable = true; + uiFileSelector.SetFilters(filters, initialFilter); + CenterDialog(uiFileSelector.window); +} + +void CloseFileSelector(uint&out filterIndex, String&out path) +{ + // Save filter & path for next time + filterIndex = uiFileSelector.filterIndex; + path = uiFileSelector.path; + + uiFileSelector = null; +} + +void CloseFileSelector() +{ + uiFileSelector = null; +} + +void CreateConsole() +{ + Console@ console = engine.CreateConsole(); + console.defaultStyle = uiStyle; + console.commandInterpreter = consoleCommandInterpreter; + console.numBufferedRows = 100; + console.autoVisibleOnError = true; +} + +void CreateDebugHud() +{ + engine.CreateDebugHud(); + debugHud.defaultStyle = uiStyle; + debugHud.mode = DebugHudElements::None; +} + +void CenterDialog(UIElement@ element) +{ + IntVector2 size = element.size; + element.SetPosition((ui.root.width - size.x) / 2, (ui.root.height - size.y) / 2); +} + +void CreateContextMenu() +{ + contextMenu = LoadEditorUI("Editor/UI/EditorContextMenu.xml"); + ui.root.AddChild(contextMenu); +} + +void UpdateWindowTitle() +{ + String sceneName = GetFileNameAndExtension(editorScene.fileName); + if (sceneName.empty || sceneName == TEMP_SCENE_NAME || sceneName == TEMP_BINARY_SCENE_NAME) + sceneName = "Untitled"; + if (sceneModified) + sceneName += "*"; + graphics.windowTitle = "Urho3D editor - " + sceneName; +} + +void HandlePopup(Menu@ menu) +{ + // Close the top level menu now unless the selected menu item has another popup + if (menu.popup !is null) + return; + + for (;;) + { + UIElement@ menuParent = menu.parent; + if (menuParent is null) + break; + + Menu@ nextMenu = menuParent.vars["Origin"].GetPtr(); + if (nextMenu is null) + break; + else + menu = nextMenu; + } + + if (menu.parent is uiMenuBar) + menu.showPopup = false; +} + +String ExtractFileName(VariantMap& eventData, bool forSave = false) +{ + String fileName; + + // Check for OK + if (eventData["OK"].GetBool()) + { + String filter = eventData["Filter"].GetString(); + fileName = eventData["FileName"].GetString(); + // Add default extension for saving if not specified + if (GetExtension(fileName).empty && forSave && filter != "*.*") + fileName = fileName + filter.Substring(1); + } + return fileName; +} + +void HandleOpenSceneFile(StringHash eventType, VariantMap& eventData) +{ + CloseFileSelector(uiSceneFilter, uiScenePath); + LoadScene(ExtractFileName(eventData)); + SendEvent(EDITOR_EVENT_SCENE_LOADED); +} + +void HandleSaveSceneFile(StringHash eventType, VariantMap& eventData) +{ + CloseFileSelector(uiSceneFilter, uiScenePath); + SaveScene(ExtractFileName(eventData, true)); +} + +void HandleLoadNodeFile(StringHash eventType, VariantMap& eventData) +{ + CloseFileSelector(uiNodeFilter, uiNodePath); + LoadNode(ExtractFileName(eventData)); +} + +void HandleSaveNodeFile(StringHash eventType, VariantMap& eventData) +{ + CloseFileSelector(uiNodeFilter, uiNodePath); + SaveNode(ExtractFileName(eventData, true)); +} + +void HandleImportModel(StringHash eventType, VariantMap& eventData) +{ + CloseFileSelector(uiImportFilter, uiImportPath); + ImportModel(ExtractFileName(eventData)); +} +void HandleImportAnimation(StringHash eventType, VariantMap& eventData) +{ + CloseFileSelector(uiImportFilter, uiImportPath); + ImportAnimation(ExtractFileName(eventData)); +} + +void HandleImportScene(StringHash eventType, VariantMap& eventData) +{ + CloseFileSelector(uiImportFilter, uiImportPath); + ImportScene(ExtractFileName(eventData)); +} + +void HandleExportSceneOBJ(StringHash eventType, VariantMap& eventData) +{ + CloseFileSelector(uiExportFilter, uiExportPath); + ExportSceneToOBJ(ExtractFileName(eventData)); +} + +void HandleExportSelectedOBJ(StringHash eventType, VariantMap& eventData) +{ + CloseFileSelector(uiExportFilter, uiExportPath); + ExportSelectedToOBJ(ExtractFileName(eventData)); +} + + +void ExecuteScript(const String&in fileName) +{ + if (fileName.empty) + return; + + File@ file = File(fileName, FILE_READ); + if (file.open) + { + String scriptCode; + while (!file.eof) + scriptCode += file.ReadLine() + "\n"; + file.Close(); + + if (script.Execute(scriptCode)) + log.Info("Script " + fileName + " ran successfully"); + } +} + +void HandleRunScript(StringHash eventType, VariantMap& eventData) +{ + CloseFileSelector(uiScriptFilter, uiScriptPath); + + suppressSceneChanges = true; + ExecuteScript(ExtractFileName(eventData)); + suppressSceneChanges = false; + + UpdateHierarchyItem(editorScene, true); + UpdateHierarchyItem(editorUIElement, true); +} + +void HandleResourcePath(StringHash eventType, VariantMap& eventData) +{ + String pathName = uiFileSelector.path; + CloseFileSelector(); + if (eventData["OK"].GetBool()) + SetResourcePath(pathName, false); +} + +void HandleOpenUILayoutFile(StringHash eventType, VariantMap& eventData) +{ + CloseFileSelector(uiElementFilter, uiElementPath); + OpenUILayout(ExtractFileName(eventData)); +} + +void HandleSaveUILayoutFile(StringHash eventType, VariantMap& eventData) +{ + CloseFileSelector(uiElementFilter, uiElementPath); + SaveUILayout(ExtractFileName(eventData, true)); +} + +void HandleLoadChildUIElementFile(StringHash eventType, VariantMap& eventData) +{ + CloseFileSelector(uiElementFilter, uiElementPath); + LoadChildUIElement(ExtractFileName(eventData)); +} + +void HandleSaveChildUIElementFile(StringHash eventType, VariantMap& eventData) +{ + CloseFileSelector(uiElementFilter, uiElementPath); + SaveChildUIElement(ExtractFileName(eventData, true)); +} + +void HandleUIElementDefaultStyle(StringHash eventType, VariantMap& eventData) +{ + CloseFileSelector(uiElementFilter, uiElementPath); + SetUIElementDefaultStyle(ExtractFileName(eventData)); +} + +void HandleHotKeysBlender( VariantMap& eventData) +{ + int key = eventData["Key"].GetI32(); + int viewDirection = eventData["Qualifiers"].GetI32() == QUAL_CTRL ? -1 : 1; + + if (key == KEY_ESCAPE) + { + if (uiHidden) + UnhideUI(); + else if (console.visible) + console.visible = false; + else if (contextMenu.visible) + CloseContextMenu(); + else if (quickMenu.visible) + { + quickMenu.visible = false; + quickMenu.enabled = false; + } + else + { + UIElement@ front = ui.frontElement; + if (front is settingsDialog || front is preferencesDialog) + { + ui.focusElement = null; + front.visible = false; + } + } + } + // Ignore other keys when UI has a modal element + else if (ui.HasModalElement()) + return; + + else if (key == KEY_F1) + console.Toggle(); + else if (key == KEY_F2) + ToggleRenderingDebug(); + else if (key == KEY_F3) + TogglePhysicsDebug(); + else if (key == KEY_F4) + ToggleOctreeDebug(); + else if (key == KEY_F5) + ToggleNavigationDebug(); + else if (key == KEY_F11) + { + Image@ screenshot = Image(); + graphics.TakeScreenShot(screenshot); + if (!fileSystem.DirExists(screenshotDir)) + fileSystem.CreateDir(screenshotDir); + screenshot.SavePNG(screenshotDir + "/Screenshot_" + + time.timeStamp.Replaced(':', '_').Replaced('.', '_').Replaced(' ', '_') + ".png"); + } + // In Blender, HOME key is for locating the selected objects by pan and + // the PERIOD key of keypad is for moving the camera to focus the selected. + // Here we ignore the difference. + else if ((key == KEY_HOME || key == KEY_KP_PERIOD) && ui.focusElement is null) + { + if (selectedNodes.length > 0 || selectedComponents.length > 0) + { + LocateNodesAndComponents(selectedNodes, selectedComponents); + } + } + else if (key == KEY_KP_1 && ui.focusElement is null) // Front view + { + cameraSmoothInterpolate.Finish(); + + Vector3 pos = -Vector3(0.0, 0.0, cameraNode.position.length * viewDirection); + Quaternion rot = Quaternion(Vector3::FORWARD, Vector3(0, 0, viewDirection)); + + cameraSmoothInterpolate.SetCameraNodePosition(cameraNode.position, pos); + cameraSmoothInterpolate.SetCameraNodeRotation(cameraNode.rotation, rot); + cameraSmoothInterpolate.Start(0.5f); + } + else if ((key == KEY_KP_3 || key == KEY_KP_9) && ui.focusElement is null) // Side view + { + cameraSmoothInterpolate.Finish(); + + Vector3 pos = -Vector3(cameraNode.position.length * -viewDirection, 0.0, 0.0); + Quaternion rot = Quaternion(Vector3::FORWARD, Vector3(-viewDirection, 0, 0)); + + cameraSmoothInterpolate.SetCameraNodePosition(cameraNode.position, pos); + cameraSmoothInterpolate.SetCameraNodeRotation(cameraNode.rotation, rot); + cameraSmoothInterpolate.Start(0.5f); + } + else if (key == KEY_KP_7 && ui.focusElement is null) // Top view + { + cameraSmoothInterpolate.Finish(); + + Vector3 pos = -Vector3(0.0, cameraNode.position.length * -viewDirection, 0.0); + Quaternion rot = Quaternion(Vector3::FORWARD, Vector3(0, -viewDirection, 0)); + + cameraSmoothInterpolate.SetCameraNodePosition(cameraNode.position, pos); + cameraSmoothInterpolate.SetCameraNodeRotation(cameraNode.rotation, rot); + cameraSmoothInterpolate.Start(0.5f); + } + else if (key == KEY_KP_5 && ui.focusElement is null) + { + activeViewport.ToggleOrthographic(); + } + else if (key == '4' && ui.focusElement is null) + editMode = EDIT_SELECT; + else if (key == '5' && ui.focusElement is null) + axisMode = AxisMode(axisMode ^ AXIS_LOCAL); + else if (key == '6' && ui.focusElement is null) + { + --pickMode; + if (pickMode < PICK_GEOMETRIES) + pickMode = MAX_PICK_MODES - 1; + } + else if (key == '7' && ui.focusElement is null) + { + ++pickMode; + if (pickMode >= MAX_PICK_MODES) + pickMode = PICK_GEOMETRIES; + } + else if (key == KEY_Z && eventData["Qualifiers"].GetI32() != QUAL_CTRL) + { + if (ui.focusElement is null) + { + fillMode = FillMode(fillMode + 1); + if (fillMode > FILL_POINT) + fillMode = FILL_SOLID; + + // Update camera fill mode + SetFillMode(fillMode); + } + } + else if (key == KEY_SPACE) + { + if (ui.cursor.visible && ui.focusElement is null) + ToggleQuickMenu(); + } + else + { + SteppedObjectManipulation(key); + } + + if ((ui.focusElement is null) && (selectedNodes.length > 0) && !cameraFlyMode) + { + if (eventData["Qualifiers"].GetI32() == QUAL_ALT) // reset transformations + { + if (key == KEY_G) + SceneResetPosition(); + else if (key == KEY_R) + SceneResetRotation(); + else if (key == KEY_S) + SceneResetScale(); + } + else if (eventData["Qualifiers"].GetI32() != QUAL_CTRL) // set transformations + { + if (key == KEY_G) + { + editMode = EDIT_MOVE; + axisMode = AxisMode(axisMode ^ AXIS_LOCAL); + + } + else if (key == KEY_R) + { + editMode = EDIT_ROTATE; + axisMode = AxisMode(axisMode ^ AXIS_LOCAL); + + } + else if (key == KEY_S) + { + editMode = EDIT_SCALE; + axisMode = AxisMode(axisMode ^ AXIS_LOCAL); + } + } + } + + toolBarDirty = true; +} + +void HandleHotKeysStandard(VariantMap& eventData) +{ + int key = eventData["Key"].GetI32(); + int viewDirection = eventData["Qualifiers"].GetI32() == QUAL_CTRL ? -1 : 1; + + if (key == KEY_ESCAPE) + { + if (uiHidden) + UnhideUI(); + else if (console.visible) + console.visible = false; + else if (contextMenu.visible) + CloseContextMenu(); + else if (quickMenu.visible) + { + quickMenu.visible = false; + quickMenu.enabled = false; + } + else + { + UIElement@ front = ui.frontElement; + if (front is settingsDialog || front is preferencesDialog) + { + ui.focusElement = null; + front.visible = false; + } + } + } + + // Ignore other keys when UI has a modal element + else if (ui.HasModalElement()) + return; + + else if (key == KEY_F1) + console.Toggle(); + else if (key == KEY_F2) + ToggleRenderingDebug(); + else if (key == KEY_F3) + TogglePhysicsDebug(); + else if (key == KEY_F4) + ToggleOctreeDebug(); + else if (key == KEY_F5) + ToggleNavigationDebug(); + else if (key == KEY_F11) + { + Image@ screenshot = Image(); + graphics.TakeScreenShot(screenshot); + if (!fileSystem.DirExists(screenshotDir)) + fileSystem.CreateDir(screenshotDir); + screenshot.SavePNG(screenshotDir + "/Screenshot_" + + time.timeStamp.Replaced(':', '_').Replaced('.', '_').Replaced(' ', '_') + ".png"); + } + else if ((key == KEY_HOME || key == KEY_F) && ui.focusElement is null) + { + if (selectedNodes.length > 0 || selectedComponents.length > 0) + { + LocateNodesAndComponents(selectedNodes, selectedComponents); + } + } + else if (key == KEY_KP_1 && ui.focusElement is null) // Front view + { + cameraSmoothInterpolate.Finish(); + + Vector3 pos = -Vector3(0.0, 0.0, cameraNode.position.length * viewDirection); + Quaternion rot = Quaternion(Vector3::FORWARD, Vector3(0, 0, viewDirection)); + + cameraSmoothInterpolate.SetCameraNodePosition(cameraNode.position, pos); + cameraSmoothInterpolate.SetCameraNodeRotation(cameraNode.rotation, rot); + cameraSmoothInterpolate.Start(0.5f); + } + else if ((key == KEY_KP_3 || key == KEY_KP_9) && ui.focusElement is null) // Side view + { + cameraSmoothInterpolate.Finish(); + + Vector3 pos = -Vector3(cameraNode.position.length * -viewDirection, 0.0, 0.0); + Quaternion rot = Quaternion(Vector3::FORWARD, Vector3(-viewDirection, 0, 0)); + + cameraSmoothInterpolate.SetCameraNodePosition(cameraNode.position, pos); + cameraSmoothInterpolate.SetCameraNodeRotation(cameraNode.rotation, rot); + cameraSmoothInterpolate.Start(0.5f); + } + else if (key == KEY_KP_7 && ui.focusElement is null) // Top view + { + cameraSmoothInterpolate.Finish(); + + Vector3 pos = -Vector3(0.0, cameraNode.position.length * -viewDirection, 0.0); + Quaternion rot = Quaternion(Vector3::FORWARD, Vector3(0, -viewDirection, 0)); + + cameraSmoothInterpolate.SetCameraNodePosition(cameraNode.position, pos); + cameraSmoothInterpolate.SetCameraNodeRotation(cameraNode.rotation, rot); + cameraSmoothInterpolate.Start(0.5f); + } + else if (key == KEY_KP_5 && ui.focusElement is null) + { + activeViewport.ToggleOrthographic(); + } + else if (eventData["Qualifiers"].GetI32() == QUAL_CTRL) + { + if (key == '1') + editMode = EDIT_MOVE; + else if (key == '2') + editMode = EDIT_ROTATE; + else if (key == '3') + editMode = EDIT_SCALE; + else if (key == '4') + editMode = EDIT_SELECT; + else if (key == '5') + axisMode = AxisMode(axisMode ^ AXIS_LOCAL); + else if (key == '6') + { + --pickMode; + if (pickMode < PICK_GEOMETRIES) + pickMode = MAX_PICK_MODES - 1; + } + else if (key == '7') + { + ++pickMode; + if (pickMode >= MAX_PICK_MODES) + pickMode = PICK_GEOMETRIES; + } + else if (key == KEY_W) + { + fillMode = FillMode(fillMode + 1); + if (fillMode > FILL_POINT) + fillMode = FILL_SOLID; + + // Update camera fill mode + SetFillMode(fillMode); + } + else if (key == KEY_SPACE) + { + if (ui.cursor.visible) + ToggleQuickMenu(); + } + else + SteppedObjectManipulation(key); + + toolBarDirty = true; + } +} + +void HandleKeyDown(StringHash eventType, VariantMap& eventData) +{ + if (hotKeyMode == HOTKEYS_MODE_STANDARD) + { + HandleHotKeysStandard(eventData); + } + else if( hotKeyMode == HOTKEYS_MODE_BLENDER) + { + HandleHotKeysBlender(eventData); + } +} + +void UnfadeUI() +{ + FadeUI(false); +} + +void FadeUI(bool fade = true) +{ + if (uiHidden || uiFaded == fade) + return; + + float opacity = (uiFaded = fade) ? uiMinOpacity : uiMaxOpacity; + Array children = ui.root.GetChildren(); + for (uint i = 0; i < children.length; ++i) + { + // Texts, popup&modal windows (which are anyway only in ui.modalRoot), and editorUIElement are excluded + if (children[i].type != TEXT_TYPE && children[i] !is editorUIElement) + children[i].opacity = opacity; + } +} + +bool ToggleUI() +{ + HideUI(!uiHidden); + return true; +} + +void UnhideUI() +{ + HideUI(false); +} + +void HideUI(bool hide = true) +{ + if (uiHidden == hide) + return; + + // Note: we could set ui.root.visible = false and it would hide the whole hierarchy. + // However in this case we need the editorUIElement to stay visible + bool visible = !(uiHidden = hide); + Array children = ui.root.GetChildren(); + for (uint i = 0; i < children.length; ++i) + { + // Cursor and editorUIElement are excluded + if (children[i].type != CURSOR_TYPE && children[i] !is editorUIElement) + { + if (visible) + { + if (!children[i].visible) + children[i].visible = children[i].vars["HideUI"].GetBool(); + } + else + { + children[i].vars["HideUI"] = children[i].visible; + children[i].visible = false; + } + } + } +} + +void IconizeUIElement(UIElement@ element, const String&in iconType) +{ + // Check if the icon has been created before + BorderImage@ icon = element.GetChild("Icon"); + + // If iconType is empty, it is a request to remove the existing icon + if (iconType.empty) + { + // Remove the icon if it exists + if (icon !is null) + icon.Remove(); + + // Revert back the indent but only if it is indented by this function + if (element.vars[INDENT_MODIFIED_BY_ICON_VAR].GetBool()) + element.indent = 0; + + return; + } + + // The UI element must itself has been indented to reserve the space for the icon + if (element.indent == 0) + { + element.indent = 1; + element.vars[INDENT_MODIFIED_BY_ICON_VAR] = true; + } + + // If no icon yet then create one with the correct indent and size in respect to the UI element + if (icon is null) + { + // The icon is placed at one indent level less than the UI element + icon = BorderImage("Icon"); + icon.indent = element.indent - 1; + icon.SetFixedSize(element.indentWidth - 2, 14); + element.InsertChild(0, icon); // Ensure icon is added as the first child + } + + // Set the icon type + if (!icon.SetStyle(iconType, iconStyle)) + icon.SetStyle("Unknown", iconStyle); // If fails then use an 'unknown' icon type + icon.color = Color(1,1,1,1); // Reset to enabled color +} + +void SetIconEnabledColor(UIElement@ element, bool enabled, bool partial = false) +{ + BorderImage@ icon = element.GetChild("Icon"); + if (icon !is null) + { + if (partial) + { + icon.colors[C_TOPLEFT] = Color(1,1,1,1); + icon.colors[C_BOTTOMLEFT] = Color(1,1,1,1); + icon.colors[C_TOPRIGHT] = Color(1,0,0,1); + icon.colors[C_BOTTOMRIGHT] = Color(1,0,0,1); + } + else + icon.color = enabled ? Color(1,1,1,1) : Color(1,0,0,1); + } +} + +void UpdateDirtyUI() +{ + UpdateDirtyToolBar(); + terrainEditor.UpdateDirty(); + + // Perform hierarchy selection latently after the new selections are finalized (used in undo/redo action) + if (!hierarchyUpdateSelections.empty) + { + hierarchyList.SetSelections(hierarchyUpdateSelections); + hierarchyUpdateSelections.Clear(); + } + + // Perform some event-triggered updates latently in case a large hierarchy was changed + if (attributesFullDirty || attributesDirty) + UpdateAttributeInspector(attributesFullDirty); +} + +void HandleMessageAcknowledgement(StringHash eventType, VariantMap& eventData) +{ + if (eventData["OK"].GetBool()) + messageBoxCallback(); + else + messageBoxCallback = null; +} + +void PopulateMruScenes() +{ + mruScenesPopup.RemoveAllChildren(); + if (uiRecentScenes.length > 0) + { + recentSceneMenu.enabled = true; + for (uint i=0; i < uiRecentScenes.length; ++i) + mruScenesPopup.AddChild(CreateMenuItem(uiRecentScenes[i], @LoadMostRecentScene, 0, 0, false, "", false)); + } + else + recentSceneMenu.enabled = false; + +} + +bool LoadMostRecentScene() +{ + Menu@ menu = GetEventSender(); + if (menu is null) + return false; + + Text@ text = menu.GetChildren()[0]; + if (text is null) + return false; + + return LoadScene(text.text); +} + +// Set from click to false if opening menu procedurally. +void OpenContextMenu(bool fromClick=true) +{ + if (contextMenu is null) + return; + + contextMenu.enabled = true; + contextMenu.visible = true; + contextMenu.BringToFront(); + if (fromClick) + contextMenuActionWaitFrame=true; +} + +void CloseContextMenu() +{ + if (contextMenu is null) + return; + + contextMenu.enabled = false; + contextMenu.visible = false; +} + +void ActivateContextMenu(Array actions) +{ + contextMenu.RemoveAllChildren(); + for (uint i=0; i< actions.length; ++i) + { + contextMenu.AddChild(actions[i]); + } + contextMenu.SetFixedHeight(24*actions.length+6); + contextMenu.position = ui.cursor.screenPosition + IntVector2(10,-10); + OpenContextMenu(); +} + +Menu@ CreateContextMenuItem(String text, String handler, String menuName = "", bool autoLocalize = true) +{ + Menu@ menu = Menu(); + menu.defaultStyle = uiStyle; + menu.style = AUTO_STYLE; + menu.name = menuName; + menu.SetLayout(LM_HORIZONTAL, 0, IntRect(8, 2, 8, 2)); + Text@ menuText = Text(); + menuText.style = "EditorMenuText"; + menu.AddChild(menuText); + menuText.text = text; + menuText.autoLocalizable = autoLocalize; + menu.vars[VAR_CONTEXT_MENU_HANDLER] = handler; + SubscribeToEvent(menu, "Released", "ContextMenuEventWrapper"); + return menu; +} + +void ContextMenuEventWrapper(StringHash eventType, VariantMap& eventData) +{ + UIElement@ uiElement = eventData["Element"].GetPtr(); + if (uiElement is null) + return; + + String handler = uiElement.vars[VAR_CONTEXT_MENU_HANDLER].GetString(); + if (!handler.empty) + { + SubscribeToEvent(uiElement, "Released", handler); + uiElement.SendEvent("Released", eventData); + } + CloseContextMenu(); +} + +/// Load a UI XML file used by the editor +XMLFile@ GetEditorUIXMLFile(const String&in fileName) +{ + // Prefer the executable path to avoid using the user's resource path, which may point + // to an outdated Urho installation + String fullFileName = fileSystem.programDir + "Data/" + fileName; + if (fileSystem.FileExists(fullFileName)) + { + File@ file = File(fullFileName, FILE_READ); + XMLFile@ xml = XMLFile(); + xml.name = fileName; + if (xml.Load(file)) + return xml; + } + + // Fallback to resource system + return cache.GetResource("XMLFile", fileName); +} + + +/// Load an UI layout used by the editor +UIElement@ LoadEditorUI(const String&in fileName) +{ + return ui.LoadLayout(GetEditorUIXMLFile(fileName)); +} + +/// Set node children as a spline path, either cyclic or non-cyclic +bool SetSplinePath() +{ + Menu@ menu = GetEventSender(); + if (menu is null) + return false; + + return SceneSetChildrenSplinePath(menu.name == "Cyclic"); +} + +bool ColorWheelBuildMenuSelectTypeColor() +{ + if (selectedNodes.empty && selectedComponents.empty) return false; + editMode = EDIT_SELECT; + + // do coloring only for single selected object + // start with trying to find single component + if (selectedComponents.length == 1) + { + coloringComponent = selectedComponents[0]; + } + // else try to get first component from selected node + else if (selectedNodes.length == 1) + { + Array components = selectedNodes[0].GetComponents(); + if (components.length > 0) + { + coloringComponent = components[0]; + } + } + else + return false; + + if (coloringComponent is null) return false; + + Array actions; + + if (coloringComponent.typeName == "Light") + { + actions.Push(CreateContextMenuItem("Light color", "HandleColorWheelMenu", "menuLightColor")); + actions.Push(CreateContextMenuItem("Specular intensity", "HandleColorWheelMenu", "menuSpecularIntensity")); + actions.Push(CreateContextMenuItem("Brightness multiplier", "HandleColorWheelMenu", "menuBrightnessMultiplier")); + + actions.Push(CreateContextMenuItem("Cancel", "HandleColorWheelMenu", "menuCancel")); + + } + else if (coloringComponent.typeName == "StaticModel") + { + actions.Push(CreateContextMenuItem("Diffuse color", "HandleColorWheelMenu", "menuDiffuseColor")); + actions.Push(CreateContextMenuItem("Specular color", "HandleColorWheelMenu", "menuSpecularColor")); + actions.Push(CreateContextMenuItem("Emissive color", "HandleColorWheelMenu", "menuEmissiveColor")); + actions.Push(CreateContextMenuItem("Environment map color", "HandleColorWheelMenu", "menuEnvironmentMapColor")); + + actions.Push(CreateContextMenuItem("Cancel", "HandleColorWheelMenu", "menuCancel")); + } + else if (coloringComponent.typeName == "Zone") + { + actions.Push(CreateContextMenuItem("Ambient color", "HandleColorWheelMenu", "menuAmbientColor")); + actions.Push(CreateContextMenuItem("Fog color", "HandleColorWheelMenu", "menuFogColor")); + + actions.Push(CreateContextMenuItem("Cancel", "HandleColorWheelMenu", "menuCancel")); + } + else if (coloringComponent.typeName == "Text3D") + { + actions.Push(CreateContextMenuItem("Color", "HandleColorWheelMenu", "c")); + actions.Push(CreateContextMenuItem("Top left color", "HandleColorWheelMenu", "tl")); + actions.Push(CreateContextMenuItem("Top right color", "HandleColorWheelMenu", "tr")); + actions.Push(CreateContextMenuItem("Bottom left color", "HandleColorWheelMenu", "bl")); + actions.Push(CreateContextMenuItem("Bottom right color", "HandleColorWheelMenu", "br")); + actions.Push(CreateContextMenuItem("Cancel", "HandleColorWheelMenu", "menuCancel")); + } + if (actions.length > 0) { + ActivateContextMenu(actions); + return true; + } + + return false; +} + +void HandleColorWheelMenu() +{ + ColorWheelSetupBehaviorForColoring(); +} + +// color was changed, update color of all colorGroup for immediate preview; +void HandleWheelChangeColor(StringHash eventType, VariantMap& eventData) +{ + if (timeToNextColoringGroupUpdate > time.systemTime) return; + + if (coloringComponent !is null) + { + Color c = eventData["Color"].GetColor(); // current ColorWheel + // preview new color + if (coloringComponent.typeName == "Light") + { + Light@ light = cast(coloringComponent); + if (light !is null) + { + if (coloringPropertyName == "menuLightColor") + { + light.color = c; + } + else if (coloringPropertyName == "menuSpecularIntensity") + { + // multiply out + light.specularIntensity = c.Value() * 10.0f; + + } + else if (coloringPropertyName == "menuBrightnessMultiplier") + { + light.brightness = c.Value() * 10.0f; + + } + + attributesDirty = true; + } + } + else if (coloringComponent.typeName == "StaticModel") + { + StaticModel@ model = cast(coloringComponent); + if (model !is null) + { + Material@ mat = model.materials[0]; + if (mat !is null) + { + if (coloringPropertyName == "menuDiffuseColor") + { + Variant oldValue = mat.shaderParameters["MatDiffColor"]; + Variant newValue; + String valueString; + valueString += String(c.r).Substring(0,5); + valueString += " "; + valueString += String(c.g).Substring(0,5); + valueString += " "; + valueString += String(c.b).Substring(0,5); + valueString += " "; + valueString += String(c.a).Substring(0,5); + newValue.FromString(oldValue.type, valueString); + mat.shaderParameters["MatDiffColor"] = newValue; + } + else if (coloringPropertyName == "menuSpecularColor") + { + Variant oldValue = mat.shaderParameters["MatSpecColor"]; + Variant newValue; + String valueString; + valueString += String(c.r).Substring(0,5); + valueString += " "; + valueString += String(c.g).Substring(0,5); + valueString += " "; + valueString += String(c.b).Substring(0,5); + valueString += " "; + valueString += String(c.a * 128).Substring(0,5); + newValue.FromString(oldValue.type, valueString); + mat.shaderParameters["MatSpecColor"] = newValue; + } + else if (coloringPropertyName == "menuEmissiveColor") + { + Variant oldValue = mat.shaderParameters["MatEmissiveColor"]; + Variant newValue; + String valueString; + valueString += String(c.r).Substring(0,5); + valueString += " "; + valueString += String(c.g).Substring(0,5); + valueString += " "; + valueString += String(c.b).Substring(0,5); + valueString += " "; + valueString += String(c.a).Substring(0,5); + newValue.FromString(oldValue.type, valueString); + mat.shaderParameters["MatEmissiveColor"] = newValue; + } + else if (coloringPropertyName == "menuEnvironmentMapColor") + { + Variant oldValue = mat.shaderParameters["MatEnvMapColor"]; + Variant newValue; + String valueString; + valueString += String(c.r).Substring(0,5); + valueString += " "; + valueString += String(c.g).Substring(0,5); + valueString += " "; + valueString += String(c.b).Substring(0,5); + valueString += " "; + valueString += String(c.a).Substring(0,5); + newValue.FromString(oldValue.type, valueString); + mat.shaderParameters["MatEnvMapColor"] = newValue; + } + } + } + } + else if (coloringComponent.typeName == "Zone") + { + Zone@ zone = cast(coloringComponent); + if (zone !is null) + { + if (coloringPropertyName == "menuAmbientColor") + { + zone.ambientColor = c; + } + else if (coloringPropertyName == "menuFogColor") + { + zone.fogColor = c; + } + + attributesDirty = true; + } + } + else if (coloringComponent.typeName == "Text3D") + { + Text3D@ txt = cast(coloringComponent); + if (txt !is null) + { + if (coloringPropertyName == "c") + txt.color = c; + else if (coloringPropertyName == "tl") + txt.colors[C_TOPLEFT] = c; + else if (coloringPropertyName == "tr") + txt.colors[C_TOPRIGHT] = c; + else if (coloringPropertyName == "bl") + txt.colors[C_BOTTOMLEFT] = c; + else if (coloringPropertyName == "br") + txt.colors[C_BOTTOMRIGHT] = c; + attributesDirty = true; + } + } + } + + timeToNextColoringGroupUpdate = time.systemTime + stepColoringGroupUpdate; +} + +// Return old colors, wheel was closed or color discarded +void HandleWheelDiscardColor(StringHash eventType, VariantMap& eventData) +{ + if (coloringComponent !is null) + { + //Color oldColor = eventData["Color"].GetColor(); //Old color from ColorWheel from ShowColorWheelWithColor(old) + Color oldColor = coloringOldColor; + + // preview new color + if (coloringComponent.typeName == "Light") + { + Light@ light = cast(coloringComponent); + if (light !is null) + { + if (coloringPropertyName == "menuLightColor") + { + light.color = oldColor; + } + else if (coloringPropertyName == "menuSpecularIntensity") + { + light.specularIntensity = coloringOldScalar * 10.0f; + + } + else if (coloringPropertyName == "menuBrightnessMultiplier") + { + light.brightness = coloringOldScalar * 10.0f; + + } + + attributesDirty = true; + } + } + else if (coloringComponent.typeName == "StaticModel") + { + StaticModel@ model = cast(coloringComponent); + if (model !is null) + { + Material@ mat = model.materials[0]; + if (mat !is null) + { + if (coloringPropertyName == "menuDiffuseColor") + { + Variant oldValue = mat.shaderParameters["MatDiffColor"]; + Variant newValue; + String valueString; + valueString += String(oldColor.r).Substring(0,5); + valueString += " "; + valueString += String(oldColor.g).Substring(0,5); + valueString += " "; + valueString += String(oldColor.b).Substring(0,5); + valueString += " "; + valueString += String(oldColor.a).Substring(0,5); + newValue.FromString(oldValue.type, valueString); + mat.shaderParameters["MatDiffColor"] = newValue; + } + else if (coloringPropertyName == "menuSpecularColor") + { + Variant oldValue = mat.shaderParameters["MatSpecColor"]; + Variant newValue; + String valueString; + valueString += String(oldColor.r).Substring(0,5); + valueString += " "; + valueString += String(oldColor.g).Substring(0,5); + valueString += " "; + valueString += String(oldColor.b).Substring(0,5); + valueString += " "; + valueString += String(coloringOldScalar).Substring(0,5); + newValue.FromString(oldValue.type, valueString); + mat.shaderParameters["MatSpecColor"] = newValue; + } + else if (coloringPropertyName == "menuEmissiveColor") + { + Variant oldValue = mat.shaderParameters["MatEmissiveColor"]; + Variant newValue; + String valueString; + valueString += String(oldColor.r).Substring(0,5); + valueString += " "; + valueString += String(oldColor.g).Substring(0,5); + valueString += " "; + valueString += String(oldColor.b).Substring(0,5); + valueString += " "; + valueString += String(oldColor.a).Substring(0,5); + newValue.FromString(oldValue.type, valueString); + mat.shaderParameters["MatEmissiveColor"] = newValue; + } + else if (coloringPropertyName == "menuEnvironmentMapColor") + { + Variant oldValue = mat.shaderParameters["MatEnvMapColor"]; + Variant newValue; + String valueString; + valueString += String(oldColor.r).Substring(0,5); + valueString += " "; + valueString += String(oldColor.g).Substring(0,5); + valueString += " "; + valueString += String(oldColor.b).Substring(0,5); + valueString += " "; + valueString += String(oldColor.a).Substring(0,5); + newValue.FromString(oldValue.type, valueString); + mat.shaderParameters["MatEnvMapColor"] = newValue; + } + } + } + } + else if (coloringComponent.typeName == "Zone") + { + Zone@ zone = cast(coloringComponent); + if (zone !is null) + { + if (coloringPropertyName == "menuAmbientColor") + { + zone.ambientColor = oldColor; + } + else if (coloringPropertyName == "menuFogColor") + { + zone.fogColor = oldColor; + } + + attributesDirty = true; + } + } + } +} + +// Applying color wheel changes to material +void HandleWheelSelectColor(StringHash eventType, VariantMap& eventData) +{ + if (coloringComponent !is null) + if (coloringComponent.typeName == "StaticModel") + { + Color c = eventData["Color"].GetColor(); //Selected color from ColorWheel + StaticModel@ model = cast(coloringComponent); + if (model !is null) + { + Material@ mat = model.materials[0]; + if (mat !is null) + { + editMaterial = mat; + SaveMaterial(); + } + } + } +} + +bool ViewDebugIcons() +{ + debugIconsShow = !debugIconsShow; + return true; +} diff --git a/bin/Data/Scripts/Editor/EditorUIElement.as b/bin/EditorData/Editor/Scripts/EditorUIElement.as similarity index 96% rename from bin/Data/Scripts/Editor/EditorUIElement.as rename to bin/EditorData/Editor/Scripts/EditorUIElement.as index 18dcc836b5a..5a5803e36ac 100644 --- a/bin/Data/Scripts/Editor/EditorUIElement.as +++ b/bin/EditorData/Editor/Scripts/EditorUIElement.as @@ -1,723 +1,723 @@ -// Urho3D editor UI-element handling - -UIElement@ editorUIElement; -XMLFile@ uiElementDefaultStyle; -Array availableStyles; - -UIElement@ editUIElement; -Array selectedUIElements; -Array editUIElements; - -Array 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.GetU32("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.GetU32("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 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 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; -} +// Urho3D editor UI-element handling + +UIElement@ editorUIElement; +XMLFile@ uiElementDefaultStyle; +Array availableStyles; + +UIElement@ editUIElement; +Array selectedUIElements; +Array editUIElements; + +Array 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.GetU32("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.GetU32("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 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 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; +} diff --git a/bin/Data/Scripts/Editor/EditorView.as b/bin/EditorData/Editor/Scripts/EditorView.as similarity index 96% rename from bin/Data/Scripts/Editor/EditorView.as rename to bin/EditorData/Editor/Scripts/EditorView.as index 138a98a358a..21bdff1434c 100644 --- a/bin/Data/Scripts/Editor/EditorView.as +++ b/bin/EditorData/Editor/Scripts/EditorView.as @@ -1,2839 +1,2839 @@ -// Urho3D editor view & camera functions - -WeakHandle previewCamera; - -Node@ cameraLookAtNode; -Node@ cameraNode; -Camera@ camera; - -float orthoCameraZoom = 1.0f; - -Node@ gridNode; -CustomGeometry@ grid; - -UIElement@ viewportUI; // holds the viewport ui, convienent for clearing and hiding -uint setViewportCursor = 0; // used to set cursor in post update -uint resizingBorder = 0; // current border that is dragging -uint viewportMode = VIEWPORT_SINGLE; -int viewportBorderOffset = 2; // used to center borders over viewport seams, should be half of width -int viewportBorderWidth = 4; // width of a viewport resize border -IntRect viewportArea; // the area where the editor viewport is. if we ever want to have the viewport not take up the whole screen this abstracts that. NOTE: viewportArea is in scaled UI position. -IntRect viewportUIClipBorder = IntRect(27, 60, 0, 0); // used to clip viewport borders, the borders are ugly when going behind the transparent toolbars -RenderPath@ renderPath; // Renderpath to use on all views -String renderPathName; -bool gammaCorrection = false; -bool HDR = false; -bool contextMenuActionWaitFrame = false; -bool cameraFlyMode = true; -int hotKeyMode = 0; // used for checking that kind of style manipulation user are prefer (see HotKeysMode) -Vector3 lastSelectedNodesCenterPoint = Vector3(0,0,0); // for Blender mode to avoid own origin rotation when no nodes are selected. preserve last center for this -WeakHandle lastSelectedNode = null; -WeakHandle lastSelectedDrawable = null; -WeakHandle lastSelectedComponent = null; -Component@ coloringComponent = null; -String coloringTypeName; -String coloringPropertyName; -Color coloringOldColor; -float coloringOldScalar; -bool debugRenderDisabled = false; -bool restoreViewport = false; -IntVector2 oldHierarchyWindowPosition; // used for restore hierarchy position when switch between viewport modes -int oldHierarchyWindowHeight; -IntVector2 oldInspectorWindowPosition; // used for restore inspector position when switch between viewport modes -int oldInspectorWindowHeight; - -const uint VIEWPORT_BORDER_H = 0x00000001; -const uint VIEWPORT_BORDER_H1 = 0x00000002; -const uint VIEWPORT_BORDER_H2 = 0x00000004; -const uint VIEWPORT_BORDER_V = 0x00000010; -const uint VIEWPORT_BORDER_V1 = 0x00000020; -const uint VIEWPORT_BORDER_V2 = 0x00000040; - -const uint VIEWPORT_SINGLE = 0x00000000; -const uint VIEWPORT_COMPACT = 0x00009000; -const uint VIEWPORT_TOP = 0x00000100; -const uint VIEWPORT_BOTTOM = 0x00000200; -const uint VIEWPORT_LEFT = 0x00000400; -const uint VIEWPORT_RIGHT = 0x00000800; -const uint VIEWPORT_TOP_LEFT = 0x00001000; -const uint VIEWPORT_TOP_RIGHT = 0x00002000; -const uint VIEWPORT_BOTTOM_LEFT = 0x00004000; -const uint VIEWPORT_BOTTOM_RIGHT = 0x00008000; - -// Combinations for easier testing -const uint VIEWPORT_BORDER_H_ANY = 0x00000007; -const uint VIEWPORT_BORDER_V_ANY = 0x00000070; -const uint VIEWPORT_SPLIT_H = 0x0000f300; -const uint VIEWPORT_SPLIT_V = 0x0000fc00; -const uint VIEWPORT_SPLIT_HV = 0x0000f000; -const uint VIEWPORT_TOP_ANY = 0x00003300; -const uint VIEWPORT_BOTTOM_ANY = 0x0000c200; -const uint VIEWPORT_LEFT_ANY = 0x00005400; -const uint VIEWPORT_RIGHT_ANY = 0x0000c800; -const uint VIEWPORT_QUAD = 0x0000f000; - -enum HotKeysMode -{ - HOTKEYS_MODE_STANDARD = 0, - HOTKEYS_MODE_BLENDER -} - -enum EditMode -{ - EDIT_MOVE = 0, - EDIT_ROTATE, - EDIT_SCALE, - EDIT_SELECT, - EDIT_SPAWN -} - -enum AxisMode -{ - AXIS_WORLD = 0, - AXIS_LOCAL -} - -enum SnapScaleMode -{ - SNAP_SCALE_FULL = 0, - SNAP_SCALE_HALF, - SNAP_SCALE_QUARTER -} - -void ResizeString(String& str, uint newSize) -{ - uint oldSize = str.Length(); - str.Resize(newSize); - for (uint i = oldSize; i < newSize; ++i) - str[i] = ' '; -} - -/// Convert rect from scaled UI position to system position -IntRect IntRectUIToSystem(const IntRect& rect) -{ - IntRect ret = rect; - ret.left = FloorToInt(ret.left * ui.scale); - ret.top = FloorToInt(ret.top * ui.scale); - ret.right = FloorToInt(ret.right * ui.scale); - ret.bottom = FloorToInt(ret.bottom * ui.scale); - - return ret; -} - -/// Convert rect from system position to scaled UI position -IntRect IntRectSystemToUI(const IntRect& rect) -{ - IntRect ret = rect; - ret.left = FloorToInt(ret.left / ui.scale); - ret.top = FloorToInt(ret.top / ui.scale); - ret.right = FloorToInt(ret.right / ui.scale); - ret.bottom = FloorToInt(ret.bottom / ui.scale); - - return ret; -} - -// Holds info about a viewport such as camera settings and splits up shared resources -class ViewportContext -{ - float cameraYaw = 0; - float cameraPitch = 0; - Camera@ camera; - Node@ cameraLookAtNode; - Node@ cameraNode; - SoundListener@ soundListener; - Viewport@ viewport; - bool enabled = false; - uint index = 0; - uint viewportId = 0; - UIElement@ viewportContextUI; - UIElement@ statusBar; - Text@ cameraPosText; - - Window@ settingsWindow; - LineEdit@ cameraPosX; - LineEdit@ cameraPosY; - LineEdit@ cameraPosZ; - LineEdit@ cameraRotX; - LineEdit@ cameraRotY; - LineEdit@ cameraRotZ; - LineEdit@ cameraZoom; - LineEdit@ cameraOrthoSize; - CheckBox@ cameraOrthographic; - - ViewportContext(IntRect viewRect, uint index_, uint viewportId_) - { - cameraNode = Node(); - cameraLookAtNode = Node(); - cameraLookAtNode.AddChild(cameraNode); - camera = cameraNode.CreateComponent("Camera"); - orthoCameraZoom = camera.zoom; - camera.fillMode = fillMode; - soundListener = cameraNode.CreateComponent("SoundListener"); - - // Here we convert viewRect from scaled UI position to system position - IntRect sysRect = IntRectUIToSystem(viewRect); - - viewport = Viewport(editorScene, camera, sysRect, renderPath); - index = index_; - viewportId = viewportId_; - camera.viewMask = 0xffffffff; // It's easier to only have 1 gizmo active this viewport is shared with the gizmo - } - - void ResetCamera() - { - cameraSmoothInterpolate.Stop(); - - cameraLookAtNode.position = Vector3(0, 0, 0); - cameraLookAtNode.rotation = Quaternion(); - - cameraNode.position = Vector3(0, 5, -10); - // Look at the origin so user can see the scene. - cameraNode.rotation = Quaternion(Vector3(0, 0, 1), -cameraNode.position); - ReacquireCameraYawPitch(); - UpdateSettingsUI(); - } - - void ReacquireCameraYawPitch() - { - cameraYaw = cameraNode.rotation.yaw; - cameraPitch = cameraNode.rotation.pitch; - } - - void CreateViewportContextUI() - { - Font@ font = cache.GetResource("Font", "Fonts/Anonymous Pro.ttf"); - - viewportContextUI = UIElement(); - viewportUI.AddChild(viewportContextUI); - viewportContextUI.SetPosition(viewport.rect.left, viewport.rect.top); - viewportContextUI.SetFixedSize(viewport.rect.width / ui.scale, viewport.rect.height / ui.scale); // viewport.rect is in system position - viewportContextUI.clipChildren = true; - - statusBar = BorderImage("ToolBar"); - statusBar.style = "EditorToolBar"; - viewportContextUI.AddChild(statusBar); - - statusBar.SetLayout(LM_HORIZONTAL); - statusBar.SetAlignment(HA_LEFT, VA_BOTTOM); - statusBar.layoutSpacing = 4; - statusBar.opacity = uiMaxOpacity; - - Button@ settingsButton = CreateSmallToolBarButton("Settings"); - statusBar.AddChild(settingsButton); - - cameraPosText = Text(); - statusBar.AddChild(cameraPosText); - - cameraPosText.SetFont(font, 11); - cameraPosText.color = Color(1, 1, 0); - cameraPosText.textEffect = TE_SHADOW; - cameraPosText.priority = -100; - - settingsWindow = LoadEditorUI("UI/EditorViewport.xml"); - settingsWindow.opacity = uiMaxOpacity; - settingsWindow.visible = false; - viewportContextUI.AddChild(settingsWindow); - - cameraPosX = settingsWindow.GetChild("PositionX", true); - cameraPosY = settingsWindow.GetChild("PositionY", true); - cameraPosZ = settingsWindow.GetChild("PositionZ", true); - cameraRotX = settingsWindow.GetChild("RotationX", true); - cameraRotY = settingsWindow.GetChild("RotationY", true); - cameraRotZ = settingsWindow.GetChild("RotationZ", true); - cameraOrthographic = settingsWindow.GetChild("Orthographic", true); - cameraZoom = settingsWindow.GetChild("Zoom", true); - cameraOrthoSize = settingsWindow.GetChild("OrthoSize", true); - - SubscribeToEvent(cameraPosX, "TextChanged", "HandleSettingsLineEditTextChange"); - SubscribeToEvent(cameraPosY, "TextChanged", "HandleSettingsLineEditTextChange"); - SubscribeToEvent(cameraPosZ, "TextChanged", "HandleSettingsLineEditTextChange"); - SubscribeToEvent(cameraRotX, "TextChanged", "HandleSettingsLineEditTextChange"); - SubscribeToEvent(cameraRotY, "TextChanged", "HandleSettingsLineEditTextChange"); - SubscribeToEvent(cameraRotZ, "TextChanged", "HandleSettingsLineEditTextChange"); - SubscribeToEvent(cameraZoom, "TextChanged", "HandleSettingsLineEditTextChange"); - SubscribeToEvent(cameraOrthoSize, "TextChanged", "HandleSettingsLineEditTextChange"); - SubscribeToEvent(cameraOrthographic, "Toggled", "HandleOrthographicToggled"); - - SubscribeToEvent(settingsButton, "Released", "ToggleViewportSettingsWindow"); - SubscribeToEvent(settingsWindow.GetChild("ResetCamera", true), "Released", "ResetCamera"); - SubscribeToEvent(settingsWindow.GetChild("CopyTransform", true), "Released", "HandleCopyTransformClicked"); - SubscribeToEvent(settingsWindow.GetChild("CloseButton", true), "Released", "CloseViewportSettingsWindow"); - SubscribeToEvent(settingsWindow.GetChild("Refresh", true), "Released", "UpdateSettingsUI"); - HandleResize(); - } - - void HandleResize() - { - viewportContextUI.SetPosition(viewport.rect.left / ui.scale, viewport.rect.top / ui.scale); - viewportContextUI.SetFixedSize(viewport.rect.width / ui.scale, viewport.rect.height / ui.scale); // viewport.rect is in system position - if (viewport.rect.left < 34) - { - statusBar.layoutBorder = IntRect(34 - viewport.rect.left, 4, 4, 8); - IntVector2 pos = settingsWindow.position; - pos.x = 32 - viewport.rect.left; - settingsWindow.position = pos; - } - else - { - statusBar.layoutBorder = IntRect(8, 4, 4, 8); - IntVector2 pos = settingsWindow.position; - pos.x = 5; - settingsWindow.position = pos; - } - - statusBar.SetFixedSize(viewport.rect.width / ui.scale, 22); - } - - void ToggleOrthographic() - { - SetOrthographic(!camera.orthographic); - } - - void SetOrthographic(bool orthographic) - { - camera.orthographic = orthographic; - if (camera.orthographic) - camera.zoom = orthoCameraZoom; - else - camera.zoom = 1.0f; - - UpdateSettingsUI(); - } - - void Update(float timeStep) - { - // Update camera smooth move - if (cameraSmoothInterpolate.IsRunning()) - { - cameraSmoothInterpolate.Update(timeStep); - } - - Vector3 cameraPos = cameraNode.position; - String xText(cameraPos.x); - String yText(cameraPos.y); - String zText(cameraPos.z); - ResizeString(xText, 8); - ResizeString(yText, 8); - ResizeString(zText, 8); - - cameraPosText.text = String( - "Pos: " + xText + " " + yText + " " + zText + - " Zoom: " + camera.zoom); - - cameraPosText.size = cameraPosText.minSize; - } - - void ToggleViewportSettingsWindow() - { - if (settingsWindow.visible) - CloseViewportSettingsWindow(); - else - OpenViewportSettingsWindow(); - } - - void OpenViewportSettingsWindow() - { - UpdateSettingsUI(); - /* settingsWindow.position = */ - settingsWindow.visible = true; - settingsWindow.BringToFront(); - } - - void CloseViewportSettingsWindow() - { - settingsWindow.visible = false; - } - - void UpdateSettingsUI() - { - cameraPosX.text = String(Floor(cameraNode.position.x * 1000) / 1000); - cameraPosY.text = String(Floor(cameraNode.position.y * 1000) / 1000); - cameraPosZ.text = String(Floor(cameraNode.position.z * 1000) / 1000); - cameraRotX.text = String(Floor(cameraNode.rotation.pitch * 1000) / 1000); - cameraRotY.text = String(Floor(cameraNode.rotation.yaw * 1000) / 1000); - cameraRotZ.text = String(Floor(cameraNode.rotation.roll * 1000) / 1000); - cameraZoom.text = String(Floor(camera.zoom * 1000) / 1000); - cameraOrthoSize.text = String(Floor(camera.orthoSize * 1000) / 1000); - // FIXME: this line below appears to be not only redundant but may cause infinite loop as well on Clang build - // cameraOrthographic.checked = camera.orthographic; - } - - void HandleOrthographicToggled(StringHash eventType, VariantMap& eventData) - { - SetOrthographic(cameraOrthographic.checked); - } - - void HandleSettingsLineEditTextChange(StringHash eventType, VariantMap& eventData) - { - LineEdit@ element = eventData["Element"].GetPtr(); - if (element.text == "") - return; - - if (element is cameraRotX || element is cameraRotY || element is cameraRotZ) - { - Vector3 euler = cameraNode.rotation.eulerAngles; - if (element is cameraRotX) - euler.x = element.text.ToFloat(); - else if (element is cameraRotY) - euler.y = element.text.ToFloat(); - else if (element is cameraRotZ) - euler.z = element.text.ToFloat(); - - cameraNode.rotation = Quaternion(euler); - } - else if (element is cameraPosX || element is cameraPosY || element is cameraPosZ) - { - Vector3 pos = cameraNode.position; - if (element is cameraPosX) - pos.x = element.text.ToFloat(); - else if (element is cameraPosY) - pos.y = element.text.ToFloat(); - else if (element is cameraPosZ) - pos.z = element.text.ToFloat(); - - cameraNode.position = pos; - } - else if (element is cameraZoom) - camera.zoom = element.text.ToFloat(); - else if (element is cameraOrthoSize) - camera.orthoSize = element.text.ToFloat(); - } - void HandleCopyTransformClicked(StringHash eventType, VariantMap& eventData) - { - if (editNode !is null) - { - editNode.position = cameraNode.position; - editNode.rotation = cameraNode.rotation; - } - } -} - -Array viewports; -ViewportContext@ activeViewport; - -Text@ editorModeText; -Text@ renderStatsText; -Text@ modelInfoText; - -EditMode editMode = EDIT_MOVE; -AxisMode axisMode = AXIS_WORLD; -FillMode fillMode = FILL_SOLID; -SnapScaleMode snapScaleMode = SNAP_SCALE_FULL; - -float viewNearClip = 0.1; -float viewFarClip = 1000.0; -float viewFov = 45.0; - - -float cameraBaseSpeed = 3; -float cameraBaseRotationSpeed = 0.2; -float cameraShiftSpeedMultiplier = 5; -float moveStep = 0.5; -float rotateStep = 5; -float scaleStep = 0.1; -float snapScale = 1.0; -bool limitRotation = false; -bool moveSnap = false; -bool rotateSnap = false; -bool scaleSnap = false; -bool renderingDebug = false; -bool physicsDebug = false; -bool octreeDebug = false; -bool navigationDebug = false; -int pickMode = PICK_GEOMETRIES; -bool orbiting = false; - -enum MouseOrbitMode -{ - ORBIT_RELATIVE = 0, - ORBIT_WRAP -} - -bool toggledMouseLock = false; -int mouseOrbitMode = ORBIT_RELATIVE; -bool mmbPanMode = false; -bool rotateAroundSelect = false; - -enum NewNodeMode -{ - NEW_NODE_CAMERA_LOOKAT = 0, - NEW_NODE_IN_CENTER, - NEW_NODE_RAYCAST -} - -int newNodeMode = NEW_NODE_CAMERA_LOOKAT; - -bool showGrid = true; -bool grid2DMode = false; -uint gridSize = 16; -uint gridSubdivisions = 3; -float gridScale = 8.0; -Color gridColor(0.1, 0.1, 0.1); -Color gridSubdivisionColor(0.05, 0.05, 0.05); -Color gridXColor(0.5, 0.1, 0.1); -Color gridYColor(0.1, 0.5, 0.1); -Color gridZColor(0.1, 0.1, 0.5); - -Array pickModeDrawableTypes = { - DrawableTypes::Geometry, - DrawableTypes::Light, - DrawableTypes::Zone -}; - -Array editModeText = { - "Move", - "Rotate", - "Scale", - "Select", - "Spawn" -}; - -Array axisModeText = { - "World", - "Local" -}; - -Array pickModeText = { - "Geometries", - "Lights", - "Zones", - "Rigidbodies", - "UI-elements" -}; - -Array fillModeText = { - "Solid", - "Wire", - "Point" -}; - -// This class provides smooth translation/rotation/zoom interpolation for the editor camera -class CameraSmoothInterpolate -{ - Vector3 lookAtNodeBeginPos; - Vector3 cameraNodeBeginPos; - - Vector3 lookAtNodeEndPos; - Vector3 cameraNodeEndPos; - - Quaternion cameraNodeBeginRot; - Quaternion cameraNodeEndRot; - - float cameraBeginZoom; - float cameraEndZoom; - - bool isRunning = false; - float duration = 0.0f; - float elapsedTime = 0.0f; - - bool interpLookAtNodePos = false; - bool interpCameraNodePos = false; - bool interpCameraRot = false; - bool interpCameraZoom = false; - - CameraSmoothInterpolate() - { - } - - void SetLookAtNodePosition(Vector3 lookAtBeginPos, Vector3 lookAtEndPos) - { - lookAtNodeBeginPos = lookAtBeginPos; - lookAtNodeEndPos = lookAtEndPos; - interpLookAtNodePos = true; - } - - void SetCameraNodePosition(Vector3 cameraBeginPos, Vector3 cameraEndPos) - { - cameraNodeBeginPos = cameraBeginPos; - cameraNodeEndPos = cameraEndPos; - interpCameraNodePos = true; - } - - void SetCameraNodeRotation(Quaternion cameraBeginRot, Quaternion cameraEndRot) - { - cameraNodeBeginRot = cameraBeginRot; - cameraNodeEndRot = cameraEndRot; - interpCameraRot = true; - } - - void SetCameraZoom(float beginZoom, float endZoom) - { - cameraBeginZoom = beginZoom; - cameraEndZoom = endZoom; - interpCameraZoom = true; - } - - void Start(float duration_) - { - if (cameraLookAtNode is null || cameraNode is null || camera is null) - return; - - duration = duration_; - elapsedTime = 0.0f; - isRunning = true; - } - - void Stop() - { - interpLookAtNodePos = false; - interpCameraNodePos = false; - interpCameraRot = false; - interpCameraZoom = false; - - isRunning = false; - } - - void Finish() - { - if (!isRunning) - return; - - if (cameraLookAtNode is null || cameraNode is null || camera is null) - return; - - if (interpLookAtNodePos) - cameraLookAtNode.worldPosition = lookAtNodeEndPos; - - if (interpCameraNodePos) - cameraNode.position = cameraNodeEndPos; - - if (interpCameraRot) - { - cameraNode.rotation = cameraNodeEndRot; - ReacquireCameraYawPitch(); - } - - if (interpCameraZoom) - { - orthoCameraZoom = cameraEndZoom; - camera.zoom = cameraEndZoom; - } - - interpLookAtNodePos = false; - interpCameraNodePos = false; - interpCameraRot = false; - interpCameraZoom = false; - - isRunning = false; - } - - bool IsRunning() const - { - return isRunning; - } - - // Cubic easing out - // http://robertpenner.com/easing/ - float EaseOut(float t, float b , float c, float d) - { - return c * ((t = t / d - 1) * t * t + 1) + b; - } - - void Update(float timeStep) - { - if (!isRunning) - return; - - if (cameraLookAtNode is null || cameraNode is null || camera is null) - return; - - elapsedTime += timeStep; - - if (elapsedTime <= duration) - { - float factor = EaseOut(elapsedTime, 0.0f, 1.0f, duration); - - if (interpLookAtNodePos) - cameraLookAtNode.worldPosition = lookAtNodeBeginPos + (lookAtNodeEndPos - lookAtNodeBeginPos) * factor; - - if (interpCameraNodePos) - cameraNode.position = cameraNodeBeginPos + (cameraNodeEndPos - cameraNodeBeginPos) * factor; - - if (interpCameraRot) - { - cameraNode.rotation = cameraNodeBeginRot.Slerp(cameraNodeEndRot, factor); - ReacquireCameraYawPitch(); - } - - if (interpCameraZoom) - { - orthoCameraZoom = cameraBeginZoom + (cameraEndZoom - cameraBeginZoom) * factor; - camera.zoom = orthoCameraZoom; - } - } - else - { - Finish(); - } - } -} - - -CameraSmoothInterpolate cameraSmoothInterpolate; // Camera smooth interpolation control - -void SetRenderPath(const String&in newRenderPathName) -{ - renderPath = null; - renderPathName = newRenderPathName.Trimmed(); - - if (renderPathName.length > 0) - { - File@ file = cache.GetFile(renderPathName); - if (file !is null) - { - XMLFile@ xml = XMLFile(); - if (xml.Load(file)) - { - renderPath = RenderPath(); - if (!renderPath.Load(xml)) - renderPath = null; - } - } - } - - if (renderPath is null) - renderPath = renderer.defaultRenderPath.Clone(); - - // Append gamma correction postprocess and disable/enable it as requested - renderPath.Append(cache.GetResource("XMLFile", "PostProcess/GammaCorrection.xml")); - renderPath.SetEnabled("GammaCorrection", gammaCorrection); - - renderer.hdrRendering = HDR; - - for (uint i = 0; i < renderer.numViewports; ++i) - renderer.viewports[i].renderPath = renderPath; - - if (materialPreview !is null && materialPreview.viewport !is null) - materialPreview.viewport.renderPath = renderPath; - - if (particleEffectPreview !is null && particleEffectPreview.viewport !is null) - particleEffectPreview.viewport.renderPath = renderPath; -} - -void SetGammaCorrection(bool enable) -{ - gammaCorrection = enable; - if (renderPath !is null) - renderPath.SetEnabled("GammaCorrection", gammaCorrection); -} - -void SetHDR(bool enable) -{ - HDR = enable; - if (renderer !is null) - renderer.hdrRendering = HDR; -} - -void CreateCamera() -{ - // Set the initial viewport rect - viewportArea = IntRect(0, 0, graphics.width / ui.scale, graphics.height / ui.scale); - - // Set viewport single to store default hierarchy/inspector height/positions - if(viewportMode == VIEWPORT_COMPACT) - { - SetViewportMode(VIEWPORT_SINGLE); - SetViewportMode(VIEWPORT_COMPACT); - } - else - { - SetViewportMode(viewportMode); - } - - SetActiveViewport(viewports[0]); - - // Note: the camera is not inside the scene, so that it is not listed, and does not get deleted - ResetCamera(); - - // Set initial renderpath if defined - SetRenderPath(renderPathName); -} - -// Create any UI associated with changing the editor viewports -void CreateViewportUI() -{ - if (viewportUI is null) - { - viewportUI = UIElement(); - ui.root.AddChild(viewportUI); - } - - viewportUI.SetFixedSize(viewportArea.width, viewportArea.height); // viewportArea is alreay in scaled UI position - viewportUI.position = IntVector2(viewportArea.top, viewportArea.left); - viewportUI.clipChildren = true; - viewportUI.clipBorder = viewportUIClipBorder; - viewportUI.RemoveAllChildren(); - viewportUI.priority = -2000; - - Array borders; - - IntRect top; - IntRect bottom; - IntRect left; - IntRect right; - IntRect topLeft; - IntRect topRight; - IntRect bottomLeft; - IntRect bottomRight; - - for (uint i = 0; i < viewports.length; ++i) - { - ViewportContext@ vc = viewports[i]; - vc.CreateViewportContextUI(); - - // Here we convert system position to scaled UI position - IntRect rect = IntRectSystemToUI(vc.viewport.rect); - - if (vc.viewportId & VIEWPORT_TOP > 0) - top = rect; - else if (vc.viewportId & VIEWPORT_BOTTOM > 0) - bottom = rect; - else if (vc.viewportId & VIEWPORT_LEFT > 0) - left = rect; - else if (vc.viewportId & VIEWPORT_RIGHT > 0) - right = rect; - else if (vc.viewportId & VIEWPORT_TOP_LEFT > 0) - topLeft = rect; - else if (vc.viewportId & VIEWPORT_TOP_RIGHT > 0) - topRight = rect; - else if (vc.viewportId & VIEWPORT_BOTTOM_LEFT > 0) - bottomLeft = rect; - else if (vc.viewportId & VIEWPORT_BOTTOM_RIGHT > 0) - bottomRight = rect; - } - - // Creates resize borders based on the mode set - if (viewportMode == VIEWPORT_QUAD) // independent borders for quad isn't easy - { - borders.Push(CreateViewportDragBorder(VIEWPORT_BORDER_V, topLeft.right - viewportBorderOffset, topLeft.top, viewportBorderWidth, viewportArea.height)); - borders.Push(CreateViewportDragBorder(VIEWPORT_BORDER_H, topLeft.left, topLeft.bottom-viewportBorderOffset, viewportArea.width, viewportBorderWidth)); - } - else - { - // Figures what borders to create based on mode - if (viewportMode & (VIEWPORT_LEFT|VIEWPORT_RIGHT) > 0) - { - borders.Push( - viewportMode & VIEWPORT_LEFT > 0 ? - CreateViewportDragBorder(VIEWPORT_BORDER_V, left.right-viewportBorderOffset, left.top, viewportBorderWidth, left.height) : - CreateViewportDragBorder(VIEWPORT_BORDER_V, right.left-viewportBorderOffset, right.top, viewportBorderWidth, right.height) - ); - } - else - { - if (viewportMode & (VIEWPORT_TOP_LEFT|VIEWPORT_TOP_RIGHT) > 0) - borders.Push(CreateViewportDragBorder(VIEWPORT_BORDER_V1, topLeft.right-viewportBorderOffset, topLeft.top, viewportBorderWidth, topLeft.height)); - if (viewportMode & (VIEWPORT_BOTTOM_LEFT|VIEWPORT_BOTTOM_RIGHT) > 0) - borders.Push(CreateViewportDragBorder(VIEWPORT_BORDER_V2, bottomLeft.right-viewportBorderOffset, bottomLeft.top, viewportBorderWidth, bottomLeft.height)); - } - - if (viewportMode & (VIEWPORT_TOP|VIEWPORT_BOTTOM) > 0) - { - borders.Push( - viewportMode & VIEWPORT_TOP > 0 ? - CreateViewportDragBorder(VIEWPORT_BORDER_H, top.left, top.bottom-viewportBorderOffset, top.width, viewportBorderWidth) : - CreateViewportDragBorder(VIEWPORT_BORDER_H, bottom.left, bottom.top-viewportBorderOffset, bottom.width, viewportBorderWidth) - ); - } - else - { - if (viewportMode & (VIEWPORT_TOP_LEFT|VIEWPORT_BOTTOM_LEFT) > 0) - borders.Push(CreateViewportDragBorder(VIEWPORT_BORDER_H1, topLeft.left, topLeft.bottom-viewportBorderOffset, topLeft.width, viewportBorderWidth)); - if (viewportMode & (VIEWPORT_TOP_RIGHT|VIEWPORT_BOTTOM_RIGHT) > 0) - borders.Push(CreateViewportDragBorder(VIEWPORT_BORDER_H2, topRight.left, topRight.bottom-viewportBorderOffset, topRight.width, viewportBorderWidth)); - } - } -} - -BorderImage@ CreateViewportDragBorder(uint value, int posX, int posY, int sizeX, int sizeY) -{ - BorderImage@ border = BorderImage(); - viewportUI.AddChild(border); - border.name = "border"; - border.style = "ViewportBorder"; - border.vars["VIEWMODE"] = value; - border.SetFixedSize(sizeX, sizeY); // relevant size gets set by viewport later - border.position = IntVector2(posX, posY); - border.opacity = uiMaxOpacity; - SubscribeToEvent(border, "DragMove", "HandleViewportBorderDragMove"); - SubscribeToEvent(border, "DragEnd", "HandleViewportBorderDragEnd"); - return border; -} - -void SetFillMode(FillMode fillMode_) -{ - fillMode = fillMode_; - for (uint i = 0; i < viewports.length; ++i) - viewports[i].camera.fillMode = fillMode_; -} - -// Sets the viewport mode -void SetViewportMode(uint mode = VIEWPORT_SINGLE) -{ - // Remember old viewport positions - Array cameralookAtPositions; - Array cameraLookAtRotations; - Array cameraPositions; - Array cameraRotations; - for (uint i = 0; i < viewports.length; ++i) - { - cameralookAtPositions.Push(viewports[i].cameraLookAtNode.position); - cameraLookAtRotations.Push(viewports[i].cameraLookAtNode.rotation); - - cameraPositions.Push(viewports[i].cameraNode.position); - cameraRotations.Push(viewports[i].cameraNode.rotation); - } - - viewports.Clear(); - - if(mode == VIEWPORT_COMPACT) - { - // Remember old hierarchy/inspector height/positions - if(viewportMode != VIEWPORT_COMPACT){ - restoreViewport = true; - oldHierarchyWindowPosition = hierarchyWindow.position; - oldHierarchyWindowHeight = hierarchyWindow.height; - oldInspectorWindowPosition = attributeInspectorWindow.position; - oldInspectorWindowHeight = attributeInspectorWindow.height; - } - - // Move and scale hierarchy window to left of screen - ShowHierarchyWindow(); - hierarchyWindow.position = IntVector2(secondaryToolBar.width,toolBar.height + uiMenuBar.height); - hierarchyWindow.height = viewportArea.height-(toolBar.height + uiMenuBar.height); - - // Move and scale inspector window to left of screen - ShowAttributeInspectorWindow(); - attributeInspectorWindow.position = IntVector2(viewportArea.width-attributeInspectorWindow.width,toolBar.height + uiMenuBar.height); - attributeInspectorWindow.height = viewportArea.height-(toolBar.height + uiMenuBar.height); - - // Hide close button and disable resize/movement inspector/hierarchy of windows - attributeInspectorWindow.GetChild("CloseButton",true).visible = false; - attributeInspectorWindow.resizable = false; - attributeInspectorWindow.movable = false; - hierarchyWindow.GetChild("CloseButton",true).visible = false; - hierarchyWindow.resizable = false; - hierarchyWindow.movable = false; - - // Create viewport on center of window - { - uint viewport = 0; - ViewportContext@ vc = ViewportContext( - IntRect( - secondaryToolBar.width + hierarchyWindow.width, - toolBar.height + uiMenuBar.height, - viewportArea.width-attributeInspectorWindow.width, - viewportArea.height), - viewports.length + 1, - viewportMode & (VIEWPORT_TOP|VIEWPORT_LEFT|VIEWPORT_TOP_LEFT) - ); - viewports.Push(vc); - } - viewportMode = mode; - - } - else - { - if(viewportMode == VIEWPORT_COMPACT) - { - // Restore hierarchy/inspector windows height/positions - if(restoreViewport) - { - hierarchyWindow.position = oldHierarchyWindowPosition; - hierarchyWindow.height = oldHierarchyWindowHeight; - attributeInspectorWindow.position = oldInspectorWindowPosition; - attributeInspectorWindow.height = oldInspectorWindowHeight; - } - - // Show close button and enable resize/movement of inspector/hierarchy windows - attributeInspectorWindow.GetChild("CloseButton",true).visible = true; - attributeInspectorWindow.resizable = true; - attributeInspectorWindow.movable = true; - hierarchyWindow.GetChild("CloseButton",true).visible = true; - hierarchyWindow.resizable = true; - hierarchyWindow.movable = true; - } - - viewportMode = mode; - - // Always have quad a - { - uint viewport = 0; - ViewportContext@ vc = ViewportContext( - IntRect( - 0, - 0, - mode & (VIEWPORT_LEFT|VIEWPORT_TOP_LEFT) > 0 ? viewportArea.width / 2 : viewportArea.width, - mode & (VIEWPORT_TOP|VIEWPORT_TOP_LEFT) > 0 ? viewportArea.height / 2 : viewportArea.height), - viewports.length + 1, - viewportMode & (VIEWPORT_TOP|VIEWPORT_LEFT|VIEWPORT_TOP_LEFT) - ); - viewports.Push(vc); - } - - uint topRight = viewportMode & (VIEWPORT_RIGHT|VIEWPORT_TOP_RIGHT); - if (topRight > 0) - { - ViewportContext@ vc = ViewportContext( - IntRect( - viewportArea.width/2, - 0, - viewportArea.width, - mode & VIEWPORT_TOP_RIGHT > 0 ? viewportArea.height / 2 : viewportArea.height), - viewports.length + 1, - topRight - ); - viewports.Push(vc); - } - - uint bottomLeft = viewportMode & (VIEWPORT_BOTTOM|VIEWPORT_BOTTOM_LEFT); - if (bottomLeft > 0) - { - ViewportContext@ vc = ViewportContext( - IntRect( - 0, - viewportArea.height / 2, - mode & (VIEWPORT_BOTTOM_LEFT) > 0 ? viewportArea.width / 2 : viewportArea.width, - viewportArea.height), - viewports.length + 1, - bottomLeft - ); - viewports.Push(vc); - } - - uint bottomRight = viewportMode & (VIEWPORT_BOTTOM_RIGHT); - if (bottomRight > 0) - { - ViewportContext@ vc = ViewportContext( - IntRect( - viewportArea.width / 2, - viewportArea.height / 2, - viewportArea.width, - viewportArea.height), - viewports.length + 1, - bottomRight - ); - viewports.Push(vc); - } - - } - - renderer.numViewports = viewports.length; - for (uint i = 0; i < viewports.length; ++i) - renderer.viewports[i] = viewports[i].viewport; - - // Restore camera positions as applicable. Default new viewports to the last camera position - if (cameraPositions.length > 0) - { - for (uint i = 0; i < viewports.length; ++i) - { - uint src = i; - if (src >= cameraPositions.length) - src = cameraPositions.length - 1; - - viewports[i].cameraLookAtNode.position = cameralookAtPositions[src]; - viewports[i].cameraLookAtNode.rotation = cameraLookAtRotations[src]; - - viewports[i].cameraNode.position = cameraPositions[src]; - viewports[i].cameraNode.rotation = cameraRotations[src]; - } - } - - ReacquireCameraYawPitch(); - UpdateViewParameters(); - UpdateCameraPreview(); - CreateViewportUI(); -} - -// Create a preview viewport if a camera component is selected -void UpdateCameraPreview() -{ - previewCamera = null; - StringHash cameraType("Camera"); - - for (uint i = 0; i < selectedComponents.length; ++i) - { - if (selectedComponents[i].type == cameraType) - { - // Take the first encountered camera - previewCamera = selectedComponents[i]; - break; - } - } - // Also try nodes if not found from components - if (previewCamera.Get() is null) - { - for (uint i = 0; i < selectedNodes.length; ++i) - { - previewCamera = selectedNodes[i].GetComponent("Camera"); - if (previewCamera.Get() !is null) - break; - } - } - - // Remove extra viewport if it exists and no camera is selected - if (previewCamera.Get() is null) - { - if (renderer.numViewports > viewports.length) - renderer.numViewports = viewports.length; - } - else - { - if (renderer.numViewports < viewports.length + 1) - renderer.numViewports = viewports.length + 1; - - int previewWidth = graphics.width / 4; - int previewHeight = previewWidth * 9 / 16; - int previewX = graphics.width - 10 - previewWidth; - int previewY = graphics.height - 30 - previewHeight; - - Viewport@ previewView = Viewport(); - previewView.scene = editorScene; - previewView.camera = previewCamera.Get(); - previewView.rect = IntRect(previewX, previewY, previewX + previewWidth, previewY + previewHeight); - previewView.renderPath = renderPath; - renderer.viewports[viewports.length] = previewView; - } -} - -void HandleViewportBorderDragMove(StringHash eventType, VariantMap& eventData) -{ - UIElement@ dragBorder = eventData["Element"].GetPtr(); - if (dragBorder is null) - return; - - uint hPos; - uint vPos; - - // Moves border to new cursor position, restricts motion to 1 axis, and keeps borders within view area - if (resizingBorder & VIEWPORT_BORDER_V_ANY > 0) - { - hPos = Clamp(ui.cursorPosition.x, 150, viewportArea.width-150); - vPos = dragBorder.position.y; - dragBorder.position = IntVector2(hPos, vPos); - } - if (resizingBorder & VIEWPORT_BORDER_H_ANY > 0) - { - vPos = Clamp(ui.cursorPosition.y, 150, viewportArea.height-150); - hPos = dragBorder.position.x; - dragBorder.position = IntVector2(hPos, vPos); - } - - // Move all dependent borders - Array borders = viewportUI.GetChildren(); - for (uint i = 0; i < borders.length; ++i) - { - BorderImage@ border = borders[i]; - if (border is null || border is dragBorder || border.name != "border") - continue; - - uint borderViewMode = border.vars["VIEWMODE"].GetU32(); - if (resizingBorder == VIEWPORT_BORDER_H) - { - if (borderViewMode == VIEWPORT_BORDER_V1) - { - border.SetFixedHeight(vPos); - } - else if (borderViewMode == VIEWPORT_BORDER_V2) - { - border.position = IntVector2(border.position.x, vPos); - border.SetFixedHeight(viewportArea.height - vPos); - } - } - else if (resizingBorder == VIEWPORT_BORDER_V) - { - if (borderViewMode == VIEWPORT_BORDER_H1) - { - border.SetFixedWidth(hPos); - } - else if (borderViewMode == VIEWPORT_BORDER_H2) - { - border.position = IntVector2(hPos, border.position.y); - border.SetFixedWidth(viewportArea.width - hPos); - } - } - } -} - -void HandleViewportBorderDragEnd(StringHash eventType, VariantMap& eventData) -{ - // Sets the new viewports by checking all the dependencies - Array children = viewportUI.GetChildren(); - Array borders; - - BorderImage@ borderV; - BorderImage@ borderV1; - BorderImage@ borderV2; - BorderImage@ borderH; - BorderImage@ borderH1; - BorderImage@ borderH2; - - for (uint i = 0; i < children.length; ++i) - { - if (children[i].name == "border") - { - BorderImage@ border = children[i]; - uint mode = border.vars["VIEWMODE"].GetU32(); - if (mode == VIEWPORT_BORDER_V) - borderV = border; - else if (mode == VIEWPORT_BORDER_V1) - borderV1 = border; - else if (mode == VIEWPORT_BORDER_V2) - borderV2 = border; - else if (mode == VIEWPORT_BORDER_H) - borderH = border; - else if (mode == VIEWPORT_BORDER_H1) - borderH1 = border; - else if (mode == VIEWPORT_BORDER_H2) - borderH2 = border; - } - } - - IntRect top; - IntRect bottom; - IntRect left; - IntRect right; - IntRect topLeft; - IntRect topRight; - IntRect bottomLeft; - IntRect bottomRight; - - for (uint i = 0; i < viewports.length; ++i) - { - ViewportContext@ vc = viewports[i]; - if (vc.viewportId & VIEWPORT_TOP > 0) - top = vc.viewport.rect; - else if (vc.viewportId & VIEWPORT_BOTTOM > 0) - bottom = vc.viewport.rect; - else if (vc.viewportId & VIEWPORT_LEFT > 0) - left = vc.viewport.rect; - else if (vc.viewportId & VIEWPORT_RIGHT > 0) - right = vc.viewport.rect; - else if (vc.viewportId & VIEWPORT_TOP_LEFT > 0) - topLeft = vc.viewport.rect; - else if (vc.viewportId & VIEWPORT_TOP_RIGHT > 0) - topRight = vc.viewport.rect; - else if (vc.viewportId & VIEWPORT_BOTTOM_LEFT > 0) - bottomLeft = vc.viewport.rect; - else if (vc.viewportId & VIEWPORT_BOTTOM_RIGHT > 0) - bottomRight = vc.viewport.rect; - } - - if (borderV !is null) - { - if (viewportMode & VIEWPORT_LEFT > 0) - left.right = borderV.position.x + viewportBorderOffset; - if (viewportMode & VIEWPORT_TOP_LEFT > 0) - topLeft.right = borderV.position.x + viewportBorderOffset; - if (viewportMode & VIEWPORT_TOP_RIGHT > 0) - topRight.left = borderV.position.x + viewportBorderOffset; - if (viewportMode & VIEWPORT_RIGHT > 0) - right.left = borderV.position.x + viewportBorderOffset; - if (viewportMode & VIEWPORT_BOTTOM_LEFT > 0) - bottomLeft.right = borderV.position.x + viewportBorderOffset; - if (viewportMode & VIEWPORT_BOTTOM_RIGHT > 0) - bottomRight.left = borderV.position.x + viewportBorderOffset; - } - else - { - if (borderV1 !is null) - { - if (viewportMode & VIEWPORT_TOP_LEFT > 0) - topLeft.right = borderV1.position.x + viewportBorderOffset; - if (viewportMode & VIEWPORT_TOP_RIGHT > 0) - topRight.left = borderV1.position.x + viewportBorderOffset; - } - if (borderV2 !is null) - { - if (viewportMode & VIEWPORT_BOTTOM_LEFT > 0) - bottomLeft.right = borderV2.position.x + viewportBorderOffset; - if (viewportMode & VIEWPORT_BOTTOM_RIGHT > 0) - bottomRight.left = borderV2.position.x + viewportBorderOffset; - } - } - - if (borderH !is null) - { - if (viewportMode & VIEWPORT_TOP > 0) - top.bottom = borderH.position.y + viewportBorderOffset; - if (viewportMode & VIEWPORT_TOP_LEFT > 0) - topLeft.bottom = borderH.position.y + viewportBorderOffset; - if (viewportMode & VIEWPORT_BOTTOM_LEFT > 0) - bottomLeft.top = borderH.position.y + viewportBorderOffset; - if (viewportMode & VIEWPORT_BOTTOM > 0) - bottom.top = borderH.position.y + viewportBorderOffset; - if (viewportMode & VIEWPORT_TOP_RIGHT > 0) - topRight.bottom = borderH.position.y + viewportBorderOffset; - if (viewportMode & VIEWPORT_BOTTOM_RIGHT > 0) - bottomRight.top = borderH.position.y + viewportBorderOffset; - } - else - { - if (borderH1 !is null) - { - if (viewportMode & VIEWPORT_TOP_LEFT > 0) - topLeft.bottom = borderH1.position.y+viewportBorderOffset; - if (viewportMode & VIEWPORT_BOTTOM_LEFT > 0) - bottomLeft.top = borderH1.position.y+viewportBorderOffset; - } - if (borderH2 !is null) - { - if (viewportMode & VIEWPORT_TOP_RIGHT > 0) - topRight.bottom = borderH2.position.y+viewportBorderOffset; - if (viewportMode & VIEWPORT_BOTTOM_RIGHT > 0) - bottomRight.top = borderH2.position.y+viewportBorderOffset; - } - } - - // Applies the calculated changes - for (uint i = 0; i < viewports.length; ++i) - { - ViewportContext@ vc = viewports[i]; - if (vc.viewportId & VIEWPORT_TOP > 0) - vc.viewport.rect = top; - else if (vc.viewportId & VIEWPORT_BOTTOM > 0) - vc.viewport.rect = bottom; - else if (vc.viewportId & VIEWPORT_LEFT > 0) - vc.viewport.rect = left; - else if (vc.viewportId & VIEWPORT_RIGHT > 0) - vc.viewport.rect = right; - else if (vc.viewportId & VIEWPORT_TOP_LEFT > 0) - vc.viewport.rect = topLeft; - else if (vc.viewportId & VIEWPORT_TOP_RIGHT > 0) - vc.viewport.rect = topRight; - else if (vc.viewportId & VIEWPORT_BOTTOM_LEFT > 0) - vc.viewport.rect = bottomLeft; - else if (vc.viewportId & VIEWPORT_BOTTOM_RIGHT > 0) - vc.viewport.rect = bottomRight; - vc.HandleResize(); - } - - // End drag state - resizingBorder = 0; - setViewportCursor = 0; -} - -void SetViewportCursor() -{ - if (setViewportCursor & VIEWPORT_BORDER_V_ANY > 0) - ui.cursor.shape = CS_RESIZEHORIZONTAL; - else if (setViewportCursor & VIEWPORT_BORDER_H_ANY > 0) - ui.cursor.shape = CS_RESIZEVERTICAL; -} - -void SetActiveViewport(ViewportContext@ context) -{ - // Sets the global variables to the current context - @cameraLookAtNode = context.cameraLookAtNode; - @cameraNode = context.cameraNode; - @camera = context.camera; - @audio.listener = context.soundListener; - - // Camera is created before gizmo, this gets called again after UI is created - if (gizmo !is null) - gizmo.viewMask = camera.viewMask; - - @activeViewport = context; - - // If a mode is changed while in a drag or hovering over a border these can get out of sync - resizingBorder = 0; - setViewportCursor = 0; -} - -void ResetCamera() -{ - for (uint i = 0; i < viewports.length; ++i) - viewports[i].ResetCamera(); -} - -void ReacquireCameraYawPitch() -{ - for (uint i = 0; i < viewports.length; ++i) - viewports[i].ReacquireCameraYawPitch(); -} - -void UpdateViewParameters() -{ - for (uint i = 0; i < viewports.length; ++i) - { - viewports[i].camera.nearClip = viewNearClip; - viewports[i].camera.farClip = viewFarClip; - viewports[i].camera.fov = viewFov; - } -} - -void CreateGrid() -{ - if (gridNode !is null) - gridNode.Remove(); - - gridNode = Node(); - gridNode.name = "EditorGrid"; - grid = gridNode.CreateComponent("CustomGeometry"); - grid.numGeometries = 1; - grid.material = cache.GetResource("Material", "Materials/VColUnlit.xml"); - grid.viewMask = 0x80000000; // Editor raycasts use viewmask 0x7fffffff - grid.occludee = false; - - UpdateGrid(); -} - -void HideGrid() -{ - if (grid !is null) - grid.enabled = false; -} - -void ShowGrid() -{ - if (grid !is null) - { - grid.enabled = true; - - if (editorScene.octree !is null) - editorScene.octree.AddManualDrawable(grid); - } -} - -void UpdateGrid(bool updateGridGeometry = true) -{ - showGrid ? ShowGrid() : HideGrid(); - gridNode.scale = Vector3(gridScale, gridScale, gridScale); - - if (!updateGridGeometry) - return; - - uint size = uint(Floor(gridSize / 2) * 2); - float halfSizeScaled = size / 2; - float scale = 1.0; - uint subdivisionSize = uint(Pow(2.0f, float(gridSubdivisions))); - - if (subdivisionSize > 0) - { - size *= subdivisionSize; - scale /= subdivisionSize; - } - - uint halfSize = size / 2; - - grid.BeginGeometry(0, LINE_LIST); - float lineOffset = -halfSizeScaled; - for (uint i = 0; i <= size; ++i) - { - bool lineCenter = i == halfSize; - bool lineSubdiv = !Equals(Mod(i, subdivisionSize), 0.0); - - if (!grid2DMode) - { - grid.DefineVertex(Vector3(lineOffset, 0.0, halfSizeScaled)); - grid.DefineColor(lineCenter ? gridZColor : (lineSubdiv ? gridSubdivisionColor : gridColor)); - grid.DefineVertex(Vector3(lineOffset, 0.0, -halfSizeScaled)); - grid.DefineColor(lineCenter ? gridZColor : (lineSubdiv ? gridSubdivisionColor : gridColor)); - - grid.DefineVertex(Vector3(-halfSizeScaled, 0.0, lineOffset)); - grid.DefineColor(lineCenter ? gridXColor : (lineSubdiv ? gridSubdivisionColor : gridColor)); - grid.DefineVertex(Vector3(halfSizeScaled, 0.0, lineOffset)); - grid.DefineColor(lineCenter ? gridXColor : (lineSubdiv ? gridSubdivisionColor : gridColor)); - } - else - { - grid.DefineVertex(Vector3(lineOffset, halfSizeScaled, 0.0)); - grid.DefineColor(lineCenter ? gridYColor : (lineSubdiv ? gridSubdivisionColor : gridColor)); - grid.DefineVertex(Vector3(lineOffset, -halfSizeScaled, 0.0)); - grid.DefineColor(lineCenter ? gridYColor : (lineSubdiv ? gridSubdivisionColor : gridColor)); - - grid.DefineVertex(Vector3(-halfSizeScaled, lineOffset, 0.0)); - grid.DefineColor(lineCenter ? gridXColor : (lineSubdiv ? gridSubdivisionColor : gridColor)); - grid.DefineVertex(Vector3(halfSizeScaled, lineOffset, 0.0)); - grid.DefineColor(lineCenter ? gridXColor : (lineSubdiv ? gridSubdivisionColor : gridColor)); - } - - lineOffset += scale; - } - grid.Commit(); -} - -void CreateStatsBar() -{ - editorModeText = Text(); - ui.root.AddChild(editorModeText); - renderStatsText = Text(); - ui.root.AddChild(renderStatsText); - modelInfoText = Text(); - ui.root.AddChild(modelInfoText); -} - -void SetupStatsBarText(Text@ text, Font@ font, int x, int y, HorizontalAlignment hAlign, VerticalAlignment vAlign) -{ - text.position = IntVector2(x, y); - text.horizontalAlignment = hAlign; - text.verticalAlignment = vAlign; - text.SetFont(font, 11); - text.color = Color(1, 1, 0); - text.textEffect = TE_SHADOW; - text.priority = -100; -} - -void UpdateStats(float timeStep) -{ - String adding = ""; - // Todo: add localization - if (hotKeyMode == HOTKEYS_MODE_BLENDER) - adding = localization.Get(" CameraFlyMode: ") + (cameraFlyMode ? "True" : "False"); - - editorModeText.text = String( - localization.Get("Mode: ") + localization.Get(editModeText[editMode]) + - localization.Get(" Axis: ") + localization.Get(axisModeText[axisMode]) + - localization.Get(" Pick: ") + localization.Get(pickModeText[pickMode]) + - localization.Get(" Fill: ") + localization.Get(fillModeText[fillMode]) + - localization.Get(" Updates: ") + (runUpdate ? localization.Get("Running") : localization.Get("Paused") + adding)); - - renderStatsText.text = String( - localization.Get("Tris: ") + renderer.numPrimitives + - localization.Get(" Batches: ") + renderer.numBatches + - localization.Get(" Lights: ") + renderer.numLights[true] + - localization.Get(" Shadowmaps: ") + renderer.numShadowMaps[true] + - localization.Get(" Occluders: ") + renderer.numOccluders[true]); - - editorModeText.size = editorModeText.minSize; - renderStatsText.size = renderStatsText.minSize; - - // Relayout stats bar - Font@ font = cache.GetResource("Font", "Fonts/Anonymous Pro.ttf"); - - if(viewportMode != VIEWPORT_COMPACT) - { - if (ui.root.width >= editorModeText.size.x + renderStatsText.size.x + 45) - { - SetupStatsBarText(editorModeText, font, 35, 64, HA_LEFT, VA_TOP); - SetupStatsBarText(renderStatsText, font, -4, 64, HA_RIGHT, VA_TOP); - SetupStatsBarText(modelInfoText, font, 35, 88, HA_LEFT, VA_TOP); - } - else - { - SetupStatsBarText(editorModeText, font, 35, 64, HA_LEFT, VA_TOP); - SetupStatsBarText(renderStatsText, font, 35, 78, HA_LEFT, VA_TOP); - SetupStatsBarText(modelInfoText, font, 35, 102, HA_LEFT, VA_TOP); - } - } - else - { - SetupStatsBarText(editorModeText, font, secondaryToolBar.width + hierarchyWindow.width + 10 , 64, HA_LEFT, VA_TOP); - SetupStatsBarText(renderStatsText, font, secondaryToolBar.width + hierarchyWindow.width + 10 , 84, HA_LEFT, VA_TOP); - SetupStatsBarText(modelInfoText, font, secondaryToolBar.width + hierarchyWindow.width + 10, 104, HA_LEFT, VA_TOP); - } -} - -void UpdateViewports(float timeStep) -{ - for(uint i = 0; i < viewports.length; ++i) - { - ViewportContext@ viewportContext = viewports[i]; - viewportContext.Update(timeStep); - } -} - -void SetMouseMode(bool enable) -{ - if (enable) - { - if (mouseOrbitMode == ORBIT_RELATIVE) - { - input.mouseMode = MM_RELATIVE; - ui.cursor.visible = false; - } - else if (mouseOrbitMode == ORBIT_WRAP) - input.mouseMode = MM_WRAP; - } - else - { - input.mouseMode = MM_ABSOLUTE; - ui.cursor.visible = true; - } -} - -void SetMouseLock() -{ - toggledMouseLock = true; - SetMouseMode(true); - FadeUI(); -} - -void ReleaseMouseLock() -{ - if (toggledMouseLock) - { - toggledMouseLock = false; - SetMouseMode(false); - } -} - -void CameraPan(Vector3 trans) -{ - cameraSmoothInterpolate.Stop(); - - cameraLookAtNode.Translate(trans); -} - -void CameraMoveForward(Vector3 trans) -{ - cameraSmoothInterpolate.Stop(); - - cameraNode.Translate(trans, TransformSpace::Parent); -} - -void CameraRotateAroundLookAt(Quaternion rot) -{ - cameraSmoothInterpolate.Stop(); - - cameraNode.rotation = rot; - - Vector3 dir = cameraNode.direction; - dir.Normalize(); - - float dist = cameraNode.position.length; - - cameraNode.position = -dir * dist; -} - -void CameraRotateAroundCenter(Quaternion rot) -{ - cameraSmoothInterpolate.Stop(); - - cameraNode.rotation = rot; - - Vector3 oldPos = cameraNode.worldPosition; - - Vector3 dir = cameraNode.worldDirection; - dir.Normalize(); - - float dist = cameraNode.position.length; - - cameraLookAtNode.worldPosition = cameraNode.worldPosition + dir * dist; - cameraNode.worldPosition = oldPos; -} - -void CameraRotateAroundSelect(Quaternion rot) -{ - cameraSmoothInterpolate.Stop(); - - cameraNode.rotation = rot; - - Vector3 dir = cameraNode.direction; - dir.Normalize(); - - float dist = cameraNode.position.length; - - cameraNode.position = -dir * dist; - - Vector3 centerPoint; - if ((selectedNodes.length > 0 || selectedComponents.length > 0)) - centerPoint = SelectedNodesCenterPoint(); - else - centerPoint = lastSelectedNodesCenterPoint; - - // legacy way, camera look-at will jump to the selection - cameraLookAtNode.worldPosition = centerPoint; -} - -void CameraZoom(float zoom) -{ - cameraSmoothInterpolate.Stop(); - - camera.zoom = Clamp(zoom, .1, 30); -} - -void HandleStandardUserInput(float timeStep) -{ - // Speedup camera move if Shift key is down - float speedMultiplier = 1.0; - if (input.keyDown[KEY_LSHIFT]) - speedMultiplier = cameraShiftSpeedMultiplier; - - // Handle FPS mode - if (!input.keyDown[KEY_LCTRL] && !input.keyDown[KEY_LALT]) - { - if (input.keyDown[KEY_W] || input.keyDown[KEY_UP]) - { - Vector3 dir = cameraNode.direction; - dir.Normalize(); - - CameraPan(dir * timeStep * cameraBaseSpeed * speedMultiplier); - FadeUI(); - } - if (input.keyDown[KEY_S] || input.keyDown[KEY_DOWN]) - { - Vector3 dir = cameraNode.direction; - dir.Normalize(); - - CameraPan(-dir * timeStep * cameraBaseSpeed * speedMultiplier); - FadeUI(); - } - if (input.keyDown[KEY_A] || input.keyDown[KEY_LEFT]) - { - Vector3 dir = cameraNode.right; - dir.Normalize(); - - CameraPan(-dir * timeStep * cameraBaseSpeed * speedMultiplier); - FadeUI(); - } - if (input.keyDown[KEY_D] || input.keyDown[KEY_RIGHT]) - { - Vector3 dir = cameraNode.right; - dir.Normalize(); - - CameraPan(dir * timeStep * cameraBaseSpeed * speedMultiplier); - FadeUI(); - } - if (input.keyDown[KEY_E] || input.keyDown[KEY_PAGEUP]) - { - Vector3 dir = cameraNode.up; - dir.Normalize(); - - CameraPan(dir * timeStep * cameraBaseSpeed * speedMultiplier); - FadeUI(); - } - if (input.keyDown[KEY_Q] || input.keyDown[KEY_PAGEDOWN]) - { - Vector3 dir = cameraNode.up; - dir.Normalize(); - - CameraPan(-dir * timeStep * cameraBaseSpeed * speedMultiplier); - FadeUI(); - } - } - - // Zoom in/out - if (input.mouseMoveWheel != 0 && ui.GetElementAt(ui.cursor.position) is null) - { - float distance = cameraNode.position.length; - float ratio = distance / 40.0f; - float factor = ratio < 1.0f ? ratio : 1.0f; - - if (!camera.orthographic) - { - Vector3 dir = cameraNode.direction; - dir.Normalize(); - dir *= input.mouseMoveWheel * 40 * timeStep * cameraBaseSpeed * speedMultiplier * factor; - - CameraMoveForward(dir); - } - else - { - float zoom = camera.zoom + input.mouseMoveWheel * speedMultiplier * factor; - - CameraZoom(zoom); - } - } - - - // Rotate/orbit/pan camera - bool changeCamViewButton = false; - - changeCamViewButton = input.mouseButtonDown[MOUSEB_RIGHT] || input.mouseButtonDown[MOUSEB_MIDDLE]; - - if (changeCamViewButton) - { - SetMouseLock(); - - IntVector2 mouseMove = input.mouseMove; - if (mouseMove.x != 0 || mouseMove.y != 0) - { - bool panTheCamera = false; - - if (input.mouseButtonDown[MOUSEB_MIDDLE]) - { - if (mmbPanMode) - panTheCamera = !input.keyDown[KEY_LSHIFT]; - else - panTheCamera = input.keyDown[KEY_LSHIFT]; - } - - // Pan the camera - if (panTheCamera) - { - Vector3 right = -cameraNode.worldRight; - right.Normalize(); - right *= mouseMove.x; - Vector3 up = cameraNode.worldUp; - up.Normalize(); - up *= mouseMove.y; - - Vector3 trans = (right + up) * timeStep * cameraBaseSpeed * 0.5; - - CameraPan(trans); - } - else // Rotate the camera - { - activeViewport.cameraYaw += mouseMove.x * cameraBaseRotationSpeed; - activeViewport.cameraPitch += mouseMove.y * cameraBaseRotationSpeed; - - if (limitRotation) - activeViewport.cameraPitch = Clamp(activeViewport.cameraPitch, -90.0, 90.0); - - Quaternion rot = Quaternion(activeViewport.cameraPitch, activeViewport.cameraYaw, 0); - - if (input.mouseButtonDown[MOUSEB_MIDDLE]) // Rotate around the camera center - { - if (rotateAroundSelect) - CameraRotateAroundSelect(rot); - else - CameraRotateAroundLookAt(rot); - - orbiting = true; - } - else // Rotate around the look-at - { - CameraRotateAroundCenter(rot); - - orbiting = true; - } - } - } - } - else - ReleaseMouseLock(); - - if (orbiting && !input.mouseButtonDown[MOUSEB_MIDDLE]) - orbiting = false; -} - -void HandleBlenderUserInput(float timeStep) -{ - if (ui.HasModalElement() || ui.focusElement !is null) - { - ReleaseMouseLock(); - return; - } - - // Check for camara fly mode - if (input.keyDown[KEY_LSHIFT] && input.keyPress[KEY_F]) - { - cameraFlyMode = !cameraFlyMode; - } - - // Speedup camera move if Shift key is down - float speedMultiplier = 1.0; - if (input.keyDown[KEY_LSHIFT]) - speedMultiplier = cameraShiftSpeedMultiplier; - - // Handle FPS mode - if (!input.keyDown[KEY_LCTRL] && !input.keyDown[KEY_LALT]) - { - if (cameraFlyMode /*&& !input.keyDown[KEY_LSHIFT]*/) - { - if (input.keyDown[KEY_W] || input.keyDown[KEY_UP]) - { - Vector3 dir = cameraNode.direction; - dir.Normalize(); - - CameraPan(dir * timeStep * cameraBaseSpeed * speedMultiplier); - FadeUI(); - } - if (input.keyDown[KEY_S] || input.keyDown[KEY_DOWN]) - { - Vector3 dir = cameraNode.direction; - dir.Normalize(); - - CameraPan(-dir * timeStep * cameraBaseSpeed * speedMultiplier); - FadeUI(); - } - if (input.keyDown[KEY_A] || input.keyDown[KEY_LEFT]) - { - Vector3 dir = cameraNode.right; - dir.Normalize(); - - CameraPan(-dir * timeStep * cameraBaseSpeed * speedMultiplier); - FadeUI(); - } - if (input.keyDown[KEY_D] || input.keyDown[KEY_RIGHT]) - { - Vector3 dir = cameraNode.right; - dir.Normalize(); - - CameraPan(dir * timeStep * cameraBaseSpeed * speedMultiplier); - FadeUI(); - } - if (input.keyDown[KEY_E] || input.keyDown[KEY_PAGEUP]) - { - Vector3 dir = cameraNode.up; - dir.Normalize(); - - CameraPan(dir * timeStep * cameraBaseSpeed * speedMultiplier); - FadeUI(); - } - if (input.keyDown[KEY_Q] || input.keyDown[KEY_PAGEDOWN]) - { - Vector3 dir = cameraNode.up; - dir.Normalize(); - - CameraPan(-dir * timeStep * cameraBaseSpeed * speedMultiplier); - FadeUI(); - } - } - } - - if (input.mouseMoveWheel != 0 && ui.GetElementAt(ui.cursor.position) is null) - { - if (!camera.orthographic) - { - if (input.keyDown[KEY_LSHIFT]) - { - Vector3 dir = cameraNode.up; - dir.Normalize(); - - CameraPan(dir * input.mouseMoveWheel * 5 * timeStep * cameraBaseSpeed * speedMultiplier); - } - else if (input.keyDown[KEY_LCTRL]) - { - Vector3 dir = cameraNode.right; - dir.Normalize(); - - CameraPan(dir * input.mouseMoveWheel * 5 * timeStep * cameraBaseSpeed * speedMultiplier); - } - else // Zoom in/out - { - float distance = cameraNode.position.length; - float ratio = distance / 40.0f; - float factor = ratio < 1.0f ? ratio : 1.0f; - - Vector3 dir = cameraNode.direction; - dir.Normalize(); - dir *= input.mouseMoveWheel * 40 * timeStep * cameraBaseSpeed * speedMultiplier * factor; - - CameraMoveForward(dir); - } - } - else - { - if (input.keyDown[KEY_LSHIFT]) - { - Vector3 dir = cameraNode.up; - dir.Normalize(); - - CameraPan(dir * input.mouseMoveWheel * timeStep * cameraBaseSpeed * speedMultiplier * 4.0f); - } - else if (input.keyDown[KEY_LCTRL]) - { - Vector3 dir = cameraNode.right; - dir.Normalize(); - - CameraPan(dir * input.mouseMoveWheel * timeStep * cameraBaseSpeed * speedMultiplier * 4.0f); - } - else - { - float zoom = camera.zoom + input.mouseMoveWheel * speedMultiplier * 0.5f; - - CameraZoom(zoom); - } - } - } - - // Rotate/orbit/pan camera - bool changeCamViewButton = input.mouseButtonDown[MOUSEB_MIDDLE] || cameraFlyMode; - - if (input.mouseButtonPress[MOUSEB_RIGHT] || input.keyDown[KEY_ESCAPE]) - cameraFlyMode = false; - - if (changeCamViewButton) - { - SetMouseLock(); - - IntVector2 mouseMove = input.mouseMove; - if (mouseMove.x != 0 || mouseMove.y != 0) - { - bool panTheCamera = false; - - if (!cameraFlyMode) - panTheCamera = input.keyDown[KEY_LSHIFT]; - - if (panTheCamera) - { - Vector3 right = -cameraNode.worldRight; - right.Normalize(); - right *= mouseMove.x; - Vector3 up = cameraNode.worldUp; - up.Normalize(); - up *= mouseMove.y; - - Vector3 trans = (right + up) * timeStep * cameraBaseSpeed * 0.5; - - CameraPan(trans); - } - else - { - activeViewport.cameraYaw += mouseMove.x * cameraBaseRotationSpeed; - activeViewport.cameraPitch += mouseMove.y * cameraBaseRotationSpeed; - - if (limitRotation) - activeViewport.cameraPitch = Clamp(activeViewport.cameraPitch, -90.0, 90.0); - - Quaternion rot = Quaternion(activeViewport.cameraPitch, activeViewport.cameraYaw, 0); - - if (cameraFlyMode) - { - CameraRotateAroundCenter(rot); - orbiting = true; - } - else if (input.mouseButtonDown[MOUSEB_MIDDLE]) - { - if (rotateAroundSelect) - CameraRotateAroundSelect(rot); - else - CameraRotateAroundLookAt(rot); - - orbiting = true; - } - } - } - } - else - ReleaseMouseLock(); - - if (orbiting && !input.mouseButtonDown[MOUSEB_MIDDLE]) - orbiting = false; - - // force to select component node for manipulation if selected only component and not his node - if ((editMode != EDIT_SELECT && editNodes.empty) && lastSelectedComponent.Get() !is null) - { - if (lastSelectedComponent.Get() !is null) - { - Component@ component = lastSelectedComponent.Get(); - SelectNode(component.node, false); - } - } -} - -void UpdateView(float timeStep) -{ - if (ui.HasModalElement() || ui.focusElement !is null) - { - ReleaseMouseLock(); - return; - } - - if (hotKeyMode == HOTKEYS_MODE_STANDARD) - { - HandleStandardUserInput(timeStep); - } - else if (hotKeyMode == HOTKEYS_MODE_BLENDER) - { - HandleBlenderUserInput(timeStep); - } - - if (!editNodes.empty && editMode != EDIT_SELECT && input.keyDown[KEY_LCTRL]) - { - Vector3 adjust(0, 0, 0); - if (input.keyDown[KEY_UP]) - adjust.z = 1; - if (input.keyDown[KEY_DOWN]) - adjust.z = -1; - if (input.keyDown[KEY_LEFT]) - adjust.x = -1; - if (input.keyDown[KEY_RIGHT]) - adjust.x = 1; - if (input.keyDown[KEY_PAGEUP]) - adjust.y = 1; - if (input.keyDown[KEY_PAGEDOWN]) - adjust.y = -1; - if (editMode == EDIT_SCALE) - { - if (input.keyDown[KEY_KP_PLUS]) - adjust = Vector3(1, 1, 1); - if (input.keyDown[KEY_KP_MINUS]) - adjust = Vector3(-1, -1, -1); - } - - if (adjust == Vector3(0, 0, 0)) - return; - - bool moved = false; - adjust *= timeStep * 10; - - switch (editMode) - { - case EDIT_MOVE: - if (!moveSnap) - moved = MoveNodes(adjust * moveStep); - break; - - case EDIT_ROTATE: - if (!rotateSnap) - moved = RotateNodes(adjust * rotateStep); - break; - - case EDIT_SCALE: - if (!scaleSnap) - moved = ScaleNodes(adjust * scaleStep); - break; - } - - if (moved) - UpdateNodeAttributes(); - } - - // If not dragging - if (resizingBorder == 0) - { - UIElement@ uiElement = ui.GetElementAt(ui.cursorPosition); - if (uiElement !is null && uiElement.vars.Contains("VIEWMODE")) - { - setViewportCursor = uiElement.vars["VIEWMODE"].GetU32(); - if (input.mouseButtonDown[MOUSEB_LEFT]) - resizingBorder = setViewportCursor; - } - } -} - -void SteppedObjectManipulation(int key) -{ - if (editNodes.empty || editMode == EDIT_SELECT) - return; - - // Do not react in non-snapped mode, because that is handled in frame update - if (editMode == EDIT_MOVE && !moveSnap) - return; - if (editMode == EDIT_ROTATE && !rotateSnap) - return; - if (editMode == EDIT_SCALE && !scaleSnap) - return; - - Vector3 adjust(0, 0, 0); - if (key == KEY_UP) - adjust.z = 1; - if (key == KEY_DOWN) - adjust.z = -1; - if (key == KEY_LEFT) - adjust.x = -1; - if (key == KEY_RIGHT) - adjust.x = 1; - if (key == KEY_PAGEUP) - adjust.y = 1; - if (key == KEY_PAGEDOWN) - adjust.y = -1; - if (editMode == EDIT_SCALE) - { - if (key == KEY_KP_PLUS) - adjust = Vector3(1, 1, 1); - if (key == KEY_KP_MINUS) - adjust = Vector3(-1, -1, -1); - } - - if (adjust == Vector3(0, 0, 0)) - return; - - bool moved = false; - - switch (editMode) - { - case EDIT_MOVE: - moved = MoveNodes(adjust); - break; - - case EDIT_ROTATE: - { - float rotateStepScaled = rotateStep * snapScale; - moved = RotateNodes(adjust * rotateStepScaled); - } - break; - - case EDIT_SCALE: - { - float scaleStepScaled = scaleStep * snapScale; - moved = ScaleNodes(adjust * scaleStepScaled); - } - break; - } - - if (moved) - UpdateNodeAttributes(); -} - -void HandlePostRenderUpdate() -{ - DebugRenderer@ debug = editorScene.debugRenderer; - if (debug is null || orbiting || debugRenderDisabled) - return; - - // Visualize the currently selected nodes - for (uint i = 0; i < selectedNodes.length; ++i) - DrawNodeDebug(selectedNodes[i], debug); - - // Visualize the currently selected components - for (uint i = 0; i < selectedComponents.length; ++i) - selectedComponents[i].DrawDebugGeometry(debug, false); - - // Visualize the currently selected UI-elements - for (uint i = 0; i < selectedUIElements.length; ++i) - ui.DebugDraw(selectedUIElements[i]); - - if (renderingDebug) - renderer.DrawDebugGeometry(false); - - if (physicsDebug && editorScene.physicsWorld !is null) - editorScene.physicsWorld.DrawDebugGeometry(true); - - if (physicsDebug && editorScene.physicsWorld2D !is null) - { - bool needDraw = true; - for (uint i = 0; i < selectedComponents.length; ++i) - { - if (cast(selectedComponents[i]) !is null) - { - needDraw = false; // Already drawed - break; - } - } - - if (needDraw) - physicsWorld2D.DrawDebugGeometry(); - } - - if (octreeDebug && editorScene.octree !is null) - editorScene.octree.DrawDebugGeometry(true); - - if (navigationDebug) - { - CrowdManager@ crowdManager = editorScene.GetComponent("CrowdManager"); - if (crowdManager !is null) - crowdManager.DrawDebugGeometry(true); - - Array@ navMeshes = editorScene.GetComponents("NavigationMesh", true); - for (uint i = 0; i < navMeshes.length; ++i) - cast(navMeshes[i]).DrawDebugGeometry(true); - - Array@ dynNavMeshes = editorScene.GetComponents("DynamicNavigationMesh", true); - for (uint i = 0; i < dynNavMeshes.length; ++i) - cast(dynNavMeshes[i]).DrawDebugGeometry(true); - } - - if (setViewportCursor | resizingBorder > 0) - { - SetViewportCursor(); - if (resizingBorder == 0) - setViewportCursor = 0; - } - - ViewRaycast(false); -} - -void DrawNodeDebug(Node@ node, DebugRenderer@ debug, bool drawNode = true) -{ - if (drawNode) - debug.AddNode(node, 1.0, false); - - // Exception for the scene to avoid bringing the editor to its knees: drawing either the whole hierarchy or the subsystem- - // components can have a large performance hit. Also do not draw terrain child nodes due to their large amount - // (TerrainPatch component itself draws nothing as debug geometry) - if (node !is editorScene && node.GetComponent("Terrain") is null) - { - for (uint j = 0; j < node.numComponents; ++j) - node.components[j].DrawDebugGeometry(debug, false); - - // To avoid cluttering the view, do not draw the node axes for child nodes - for (uint k = 0; k < node.numChildren; ++k) - DrawNodeDebug(node.children[k], debug, false); - } -} - -void ViewMouseMove() -{ - Ray cameraRay = GetActiveViewportCameraRay(); - Component@ selectedComponent; - - if (pickMode < PICK_RIGIDBODIES && editorScene.octree !is null) - { - RayQueryResult result = editorScene.octree.RaycastSingle(cameraRay, RAY_TRIANGLE, camera.farClip, - pickModeDrawableTypes[pickMode], 0x7fffffff); - - if (result.drawable !is null && result.drawable.typeName == "TerrainPatch" && result.drawable.node.parent !is null) - { - Terrain@ terrainComponent = result.drawable.node.parent.GetComponent("Terrain"); - terrainEditor.UpdateBrushVisualizer(terrainComponent, result.position); - } - else { - terrainEditor.HideBrushVisualizer(); - } - } - - // setting mouse position based on mouse position - if (ui.IsDragging()) { } - else if (ui.focusElement !is null || input.mouseButtonDown[MOUSEB_LEFT|MOUSEB_MIDDLE|MOUSEB_RIGHT]) - return; - - IntVector2 pos = ui.cursor.position; - pos = ui.ConvertUIToSystem(pos); - for (uint i = 0; i < viewports.length; ++i) - { - ViewportContext@ vc = viewports[i]; - if (vc !is activeViewport && vc.viewport.rect.IsInside(pos) == INSIDE) - SetActiveViewport(vc); - } -} - -void ViewMouseClick() -{ - ViewRaycast(true); -} - -Ray GetActiveViewportCameraRay() -{ - IntVector2 pos = ui.ConvertUIToSystem(ui.cursorPosition); - IntRect view = activeViewport.viewport.rect; - return camera.GetScreenRay( - float(pos.x - view.left) / view.width, - float(pos.y - view.top) / view.height - ); -} - -void ViewMouseClickEnd() -{ - // checks to close open popup windows - IntVector2 pos = ui.cursorPosition; - if (contextMenu !is null && contextMenu.enabled) - { - if (contextMenuActionWaitFrame) - contextMenuActionWaitFrame = false; - else - { - if (!contextMenu.IsInside(pos, true)) - CloseContextMenu(); - } - } - if (quickMenu !is null && quickMenu.enabled) - { - bool enabled = quickMenu.IsInside(pos, true); - quickMenu.enabled = enabled; - quickMenu.visible = enabled; - } -} - -void ViewRaycast(bool mouseClick) -{ - // Ignore if UI has modal element - if (ui.HasModalElement()) - return; - - // Ignore if mouse is grabbed by other operation - if (input.mouseGrabbed) - return; - - IntVector2 pos = ui.cursorPosition; - UIElement@ elementAtPos = ui.GetElementAt(pos, pickMode != PICK_UI_ELEMENTS); - if (editMode == EDIT_SPAWN) - { - if(mouseClick && input.mouseButtonPress[MOUSEB_LEFT] && elementAtPos is null) - SpawnObject(); - return; - } - - // Do not raycast / change selection if hovering over the gizmo - if (IsGizmoSelected()) - return; - - DebugRenderer@ debug = editorScene.debugRenderer; - - if (pickMode == PICK_UI_ELEMENTS) - { - bool leftClick = mouseClick && input.mouseButtonPress[MOUSEB_LEFT]; - bool multiselect = input.qualifierDown[QUAL_CTRL]; - - // Only interested in user-created UI elements - if (elementAtPos !is null && elementAtPos !is editorUIElement && elementAtPos.GetElementEventSender() is editorUIElement) - { - ui.DebugDraw(elementAtPos); - - if (leftClick) - SelectUIElement(elementAtPos, multiselect); - } - // If clicked on emptiness in non-multiselect mode, clear the selection - else if (leftClick && !multiselect && ui.GetElementAt(pos) is null) - hierarchyList.ClearSelection(); - - return; - } - - // Do not raycast / change selection if hovering over a UI element when not in PICK_UI_ELEMENTS Mode - if (elementAtPos !is null) - return; - - Ray cameraRay = GetActiveViewportCameraRay(); - Component@ selectedComponent; - - if (pickMode < PICK_RIGIDBODIES) - { - if (editorScene.octree is null) - return; - - RayQueryResult result = editorScene.octree.RaycastSingle(cameraRay, RAY_TRIANGLE, camera.farClip, - pickModeDrawableTypes[pickMode], 0x7fffffff); - - if (result.drawable !is null) - { - Drawable@ drawable = result.drawable; - - // for actual last selected node or component in both modes - if (hotKeyMode == HOTKEYS_MODE_STANDARD) - { - if (input.mouseButtonDown[MOUSEB_LEFT]) - { - lastSelectedNode = drawable.node; - lastSelectedDrawable = drawable; - lastSelectedComponent = drawable; - } - } - else if (hotKeyMode == HOTKEYS_MODE_BLENDER) - { - if (input.mouseButtonDown[MOUSEB_RIGHT]) - { - lastSelectedNode = drawable.node; - lastSelectedDrawable = drawable; - lastSelectedComponent = drawable; - } - } - - // If selecting a terrain patch, select the parent terrain instead - if (drawable.typeName != "TerrainPatch") - { - selectedComponent = drawable; - if (debug !is null) - { - debug.AddNode(drawable.node, 1.0, false); - drawable.DrawDebugGeometry(debug, false); - } - } - else if (drawable.node.parent !is null){ - Terrain@ terrainComponent = drawable.node.parent.GetComponent("Terrain"); - selectedComponent = terrainComponent; - if (selectedComponent is terrainComponent && input.mouseButtonDown[MOUSEB_LEFT]) - { - selectedComponent = terrainComponent; - terrainEditor.Work(terrainComponent, result.position); - } - else - { - terrainEditor.targetColorSelected = false; - } - } - } - } - else - { - if (editorScene.physicsWorld is null) - return; - - // If we are not running the actual physics update, refresh collisions before raycasting - if (!runUpdate) - editorScene.physicsWorld.UpdateCollisions(); - - PhysicsRaycastResult result = editorScene.physicsWorld.RaycastSingle(cameraRay, camera.farClip); - if (result.body !is null) - { - RigidBody@ body = result.body; - if (debug !is null) - { - debug.AddNode(body.node, 1.0, false); - body.DrawDebugGeometry(debug, false); - } - selectedComponent = body; - } - } - - bool multiselect = false; - bool componentSelectQualifier = false; - bool mouseButtonPressRL = false; - - if (hotKeyMode == HOTKEYS_MODE_STANDARD) - { - mouseButtonPressRL = input.mouseButtonPress[MOUSEB_LEFT]; - componentSelectQualifier = input.qualifierDown[QUAL_SHIFT]; - multiselect = input.qualifierDown[QUAL_CTRL]; - } - else if (hotKeyMode == HOTKEYS_MODE_BLENDER) - { - mouseButtonPressRL = input.mouseButtonPress[MOUSEB_RIGHT]; - componentSelectQualifier = input.qualifierDown[QUAL_CTRL]; - multiselect = input.qualifierDown[QUAL_SHIFT]; - } - - if (mouseClick && mouseButtonPressRL) - { - if (selectedComponent !is null) - { - if (componentSelectQualifier) - { - // If we are selecting components, but have nodes in existing selection, do not multiselect to prevent confusion - if (!selectedNodes.empty) - multiselect = false; - SelectComponent(selectedComponent, multiselect); - } - else - { - // If we are selecting nodes, but have components in existing selection, do not multiselect to prevent confusion - if (!selectedComponents.empty) - multiselect = false; - SelectNode(selectedComponent.node, multiselect); - } - } - else - { - // If clicked on emptiness in non-multiselect mode, clear the selection - if (!multiselect) - SelectComponent(null, false); - } - } -} - -Vector3 GetNewNodePosition(bool raycastToMouse = false) -{ - if (newNodeMode == NEW_NODE_IN_CENTER) - return Vector3(0, 0, 0); - if (newNodeMode == NEW_NODE_RAYCAST) - { - Ray cameraRay = raycastToMouse ? GetActiveViewportCameraRay() : camera.GetScreenRay(0.5, 0.5); - Vector3 position, normal; - if (GetSpawnPosition(cameraRay, camera.farClip, position, normal, 0, false)) - return position; - } - return cameraLookAtNode.worldPosition; -} - -int GetShadowResolution() -{ - if (!renderer.drawShadows) - return 0; - int level = 1; - int res = renderer.shadowMapSize; - while (res > 512) - { - res >>= 1; - ++level; - } - if (level > 3) - level = 3; - - return level; -} - -void SetShadowResolution(int level) -{ - if (level <= 0) - { - renderer.drawShadows = false; - return; - } - else - { - renderer.drawShadows = true; - renderer.shadowMapSize = 256 << level; - } -} - -void ToggleRenderingDebug() -{ - renderingDebug = !renderingDebug; -} - -void TogglePhysicsDebug() -{ - physicsDebug = !physicsDebug; -} - -void ToggleOctreeDebug() -{ - octreeDebug = !octreeDebug; -} - -void ToggleNavigationDebug() -{ - navigationDebug = !navigationDebug; -} - -bool StopTestAnimation() -{ - testAnimState = null; - return true; -} - -void MergeNodeBoundingBox(BoundingBox &inout box, Array&inout visitedComponents, Node@ node) -{ - if (node is null || node is editorScene) - return; - - // if node has no component, merge its world position - if (node.numComponents == 0) - { - box.Merge(node.worldPosition); - } - - // Merge components bounding box of this node - for (uint i = 0; i < node.numComponents; ++i) - { - MergeComponentBoundingBox(box, visitedComponents, node.components[i]); - } - - // Merge bounding boxes of child nodes recursively - for (uint i = 0; i < node.numChildren; ++i) - { - Node@ child = node.children[i]; - MergeNodeBoundingBox(box, visitedComponents, child); - } -} - -void MergeComponentBoundingBox(BoundingBox &inout box, Array&inout visitedComponents, Component@ component) -{ - if (component is null || visitedComponents.FindByRef(component) != -1) - return; - - Drawable@ drawable = cast(component); - - // Merge drawable component's bounding box. Skip skybox, as its box is very large, as well as lights - if (drawable !is null && cast(drawable) is null && cast(drawable) is null) - { - box.Merge(drawable.worldBoundingBox); - visitedComponents.Push(component); - return; - } - - // If the component is not a drawable, merge the world position of its node - if (component.node !is editorScene) - box.Merge(component.node.worldPosition); - - visitedComponents.Push(component); -} - -void LocateNodes(Array nodes) -{ - if (nodes.empty || (nodes.length == 1 && nodes[0] is editorScene)) - return; - - // Calculate bounding box of all nodes - BoundingBox box; - Array visitedComponents; - - for (uint i = 0; i < nodes.length; ++i) - { - MergeNodeBoundingBox(box, visitedComponents, nodes[i]); - } - - FitCamera(box, true); -} - -void LocateComponents(Array components) -{ - if (components.empty || components.length == 1 && components[0].node is editorScene) - return; - - // Calculate bounding box of all nodes - BoundingBox box; - Array visitedComponents; - - for (uint i = 0; i < components.length; ++i) - { - MergeComponentBoundingBox(box, visitedComponents, components[i]); - } - - FitCamera(box, true); -} - -void LocateNodesAndComponents(Array nodes, Array components) -{ - if (nodes.length == 0 && components.length == 0) - return; - - // Calculate bounding box of all nodes - BoundingBox box; - Array visitedComponents; - - if (!nodes.empty && !(nodes.length == 1 && nodes[0] is editorScene)) - { - for (uint i = 0; i < nodes.length; ++i) - { - MergeNodeBoundingBox(box, visitedComponents, nodes[i]); - } - } - - if (!components.empty) - { - for (uint i = 0; i < components.length; ++i) - { - MergeComponentBoundingBox(box, visitedComponents, components[i]); - } - } - - FitCamera(box, true); -} - -void FitCamera(BoundingBox box, bool smooth) -{ - // Calculate proper camera distance - fit the bounding sphere into the camera frustum - Sphere sphere = Sphere(box); - - float aspect = camera.aspectRatio; - float fov = 0.0f; - - // Choose the small one from vertical and horizontal fovs - if (aspect > 1.0f) - fov = camera.fov; - else - fov = camera.fov * aspect; - - fov *= 0.5f; - - if (sphere.radius < 1.0f) - sphere.radius = 1.0f; - - float distance = sphere.radius / Sin(fov); - - if (distance > viewFarClip) - distance = viewFarClip; - - Vector3 dir = cameraNode.direction; - dir.Normalize(); - - // Make the distance a little farther - distance *= 1.1f; - - // Set zoom value a little bigger - float zoom = camera.orthoSize / (sphere.radius * 2.0f); - zoom *= 1.1f; - - // We put the pivot node to the center of the bounding sphere - // and put the camera node to the opposite of view direction - Vector3 lookAtPos = sphere.center; - Vector3 cameraPos = -dir * distance; - - cameraSmoothInterpolate.Stop(); - - if (smooth) - { - cameraSmoothInterpolate.SetLookAtNodePosition(cameraLookAtNode.worldPosition, lookAtPos); - cameraSmoothInterpolate.SetCameraNodePosition(cameraNode.position, cameraPos); - - if (camera.orthographic) - cameraSmoothInterpolate.SetCameraZoom(camera.zoom, zoom); - - cameraSmoothInterpolate.Start(0.5f); - } - else - { - cameraLookAtNode.worldPosition = lookAtPos; - cameraNode.position = cameraPos; - - if (camera.orthographic) - camera.zoom = zoom; - } -} - -Vector3 SelectedNodesCenterPoint() -{ - Vector3 centerPoint; - uint count = selectedNodes.length; - for (uint i = 0; i < selectedNodes.length; ++i) - centerPoint += selectedNodes[i].worldPosition; - - for (uint i = 0; i < selectedComponents.length; ++i) - { - Drawable@ drawable = cast(selectedComponents[i]); - count++; - if (drawable !is null) - centerPoint += drawable.node.LocalToWorld(drawable.boundingBox.center); - else - centerPoint += selectedComponents[i].node.worldPosition; - } - - if (count > 0) - { - lastSelectedNodesCenterPoint = centerPoint / count; - return centerPoint / count; - } - else - { - lastSelectedNodesCenterPoint = centerPoint; - return centerPoint; - } -} - -Drawable@ GetDrawableAtMousePostion() -{ - IntVector2 pos = ui.ConvertUIToSystem(ui.cursorPosition); - Ray cameraRay = camera.GetScreenRay(float(pos.x) / activeViewport.viewport.rect.width, float(pos.y) / activeViewport.viewport.rect.height); - - if (editorScene.octree is null) - return null; - - RayQueryResult result = editorScene.octree.RaycastSingle(cameraRay, RAY_TRIANGLE, camera.farClip, DrawableTypes::Geometry, 0x7fffffff); - - return result.drawable; -} - -void HandleBeginViewUpdate(StringHash eventType, VariantMap& eventData) -{ - // Hide gizmo, grid and debug icons from any camera other then active viewport - if (eventData["Camera"].GetPtr() !is camera) - { - if (gizmo !is null) - gizmo.viewMask = 0; - } - if (eventData["Camera"].GetPtr() is previewCamera.Get()) - { - suppressSceneChanges = true; - if (grid !is null) - grid.viewMask = 0; - if (debugIconsNode !is null) - debugIconsNode.enabled = false; - suppressSceneChanges = false; - } -} - -void HandleEndViewUpdate(StringHash eventType, VariantMap& eventData) -{ - // Restore gizmo and grid after camera view update - if (eventData["Camera"].GetPtr() !is camera) - { - if (gizmo !is null) - gizmo.viewMask = 0x80000000; - } - if (eventData["Camera"].GetPtr() is previewCamera.Get()) - { - suppressSceneChanges = true; - if (grid !is null) - grid.viewMask = 0x80000000; - if (debugIconsNode !is null) - debugIconsNode.enabled = true; - suppressSceneChanges = false; - } -} - -bool debugWasEnabled = true; - -void HandleBeginViewRender(StringHash eventType, VariantMap& eventData) -{ - // Hide debug geometry from preview camera - if (eventData["Camera"].GetPtr() is previewCamera.Get()) - { - DebugRenderer@ debug = editorScene.GetComponent("DebugRenderer"); - if (debug !is null) - { - suppressSceneChanges = true; // Do not want UI update now - debugWasEnabled = debug.enabled; - debug.enabled = false; - suppressSceneChanges = false; - } - } -} - -void HandleEndViewRender(StringHash eventType, VariantMap& eventData) -{ - // Restore debug geometry after preview camera render - if (eventData["Camera"].GetPtr() is previewCamera.Get()) - { - DebugRenderer@ debug = editorScene.GetComponent("DebugRenderer"); - if (debug !is null) - { - suppressSceneChanges = true; // Do not want UI update now - debug.enabled = debugWasEnabled; - suppressSceneChanges = false; - } - } -} +// Urho3D editor view & camera functions + +WeakHandle previewCamera; + +Node@ cameraLookAtNode; +Node@ cameraNode; +Camera@ camera; + +float orthoCameraZoom = 1.0f; + +Node@ gridNode; +CustomGeometry@ grid; + +UIElement@ viewportUI; // holds the viewport ui, convienent for clearing and hiding +uint setViewportCursor = 0; // used to set cursor in post update +uint resizingBorder = 0; // current border that is dragging +uint viewportMode = VIEWPORT_SINGLE; +int viewportBorderOffset = 2; // used to center borders over viewport seams, should be half of width +int viewportBorderWidth = 4; // width of a viewport resize border +IntRect viewportArea; // the area where the editor viewport is. if we ever want to have the viewport not take up the whole screen this abstracts that. NOTE: viewportArea is in scaled UI position. +IntRect viewportUIClipBorder = IntRect(27, 60, 0, 0); // used to clip viewport borders, the borders are ugly when going behind the transparent toolbars +RenderPath@ renderPath; // Renderpath to use on all views +String renderPathName; +bool gammaCorrection = false; +bool HDR = false; +bool contextMenuActionWaitFrame = false; +bool cameraFlyMode = true; +int hotKeyMode = 0; // used for checking that kind of style manipulation user are prefer (see HotKeysMode) +Vector3 lastSelectedNodesCenterPoint = Vector3(0,0,0); // for Blender mode to avoid own origin rotation when no nodes are selected. preserve last center for this +WeakHandle lastSelectedNode = null; +WeakHandle lastSelectedDrawable = null; +WeakHandle lastSelectedComponent = null; +Component@ coloringComponent = null; +String coloringTypeName; +String coloringPropertyName; +Color coloringOldColor; +float coloringOldScalar; +bool debugRenderDisabled = false; +bool restoreViewport = false; +IntVector2 oldHierarchyWindowPosition; // used for restore hierarchy position when switch between viewport modes +int oldHierarchyWindowHeight; +IntVector2 oldInspectorWindowPosition; // used for restore inspector position when switch between viewport modes +int oldInspectorWindowHeight; + +const uint VIEWPORT_BORDER_H = 0x00000001; +const uint VIEWPORT_BORDER_H1 = 0x00000002; +const uint VIEWPORT_BORDER_H2 = 0x00000004; +const uint VIEWPORT_BORDER_V = 0x00000010; +const uint VIEWPORT_BORDER_V1 = 0x00000020; +const uint VIEWPORT_BORDER_V2 = 0x00000040; + +const uint VIEWPORT_SINGLE = 0x00000000; +const uint VIEWPORT_COMPACT = 0x00009000; +const uint VIEWPORT_TOP = 0x00000100; +const uint VIEWPORT_BOTTOM = 0x00000200; +const uint VIEWPORT_LEFT = 0x00000400; +const uint VIEWPORT_RIGHT = 0x00000800; +const uint VIEWPORT_TOP_LEFT = 0x00001000; +const uint VIEWPORT_TOP_RIGHT = 0x00002000; +const uint VIEWPORT_BOTTOM_LEFT = 0x00004000; +const uint VIEWPORT_BOTTOM_RIGHT = 0x00008000; + +// Combinations for easier testing +const uint VIEWPORT_BORDER_H_ANY = 0x00000007; +const uint VIEWPORT_BORDER_V_ANY = 0x00000070; +const uint VIEWPORT_SPLIT_H = 0x0000f300; +const uint VIEWPORT_SPLIT_V = 0x0000fc00; +const uint VIEWPORT_SPLIT_HV = 0x0000f000; +const uint VIEWPORT_TOP_ANY = 0x00003300; +const uint VIEWPORT_BOTTOM_ANY = 0x0000c200; +const uint VIEWPORT_LEFT_ANY = 0x00005400; +const uint VIEWPORT_RIGHT_ANY = 0x0000c800; +const uint VIEWPORT_QUAD = 0x0000f000; + +enum HotKeysMode +{ + HOTKEYS_MODE_STANDARD = 0, + HOTKEYS_MODE_BLENDER +} + +enum EditMode +{ + EDIT_MOVE = 0, + EDIT_ROTATE, + EDIT_SCALE, + EDIT_SELECT, + EDIT_SPAWN +} + +enum AxisMode +{ + AXIS_WORLD = 0, + AXIS_LOCAL +} + +enum SnapScaleMode +{ + SNAP_SCALE_FULL = 0, + SNAP_SCALE_HALF, + SNAP_SCALE_QUARTER +} + +void ResizeString(String& str, uint newSize) +{ + uint oldSize = str.Length(); + str.Resize(newSize); + for (uint i = oldSize; i < newSize; ++i) + str[i] = ' '; +} + +/// Convert rect from scaled UI position to system position +IntRect IntRectUIToSystem(const IntRect& rect) +{ + IntRect ret = rect; + ret.left = FloorToInt(ret.left * ui.scale); + ret.top = FloorToInt(ret.top * ui.scale); + ret.right = FloorToInt(ret.right * ui.scale); + ret.bottom = FloorToInt(ret.bottom * ui.scale); + + return ret; +} + +/// Convert rect from system position to scaled UI position +IntRect IntRectSystemToUI(const IntRect& rect) +{ + IntRect ret = rect; + ret.left = FloorToInt(ret.left / ui.scale); + ret.top = FloorToInt(ret.top / ui.scale); + ret.right = FloorToInt(ret.right / ui.scale); + ret.bottom = FloorToInt(ret.bottom / ui.scale); + + return ret; +} + +// Holds info about a viewport such as camera settings and splits up shared resources +class ViewportContext +{ + float cameraYaw = 0; + float cameraPitch = 0; + Camera@ camera; + Node@ cameraLookAtNode; + Node@ cameraNode; + SoundListener@ soundListener; + Viewport@ viewport; + bool enabled = false; + uint index = 0; + uint viewportId = 0; + UIElement@ viewportContextUI; + UIElement@ statusBar; + Text@ cameraPosText; + + Window@ settingsWindow; + LineEdit@ cameraPosX; + LineEdit@ cameraPosY; + LineEdit@ cameraPosZ; + LineEdit@ cameraRotX; + LineEdit@ cameraRotY; + LineEdit@ cameraRotZ; + LineEdit@ cameraZoom; + LineEdit@ cameraOrthoSize; + CheckBox@ cameraOrthographic; + + ViewportContext(IntRect viewRect, uint index_, uint viewportId_) + { + cameraNode = Node(); + cameraLookAtNode = Node(); + cameraLookAtNode.AddChild(cameraNode); + camera = cameraNode.CreateComponent("Camera"); + orthoCameraZoom = camera.zoom; + camera.fillMode = fillMode; + soundListener = cameraNode.CreateComponent("SoundListener"); + + // Here we convert viewRect from scaled UI position to system position + IntRect sysRect = IntRectUIToSystem(viewRect); + + viewport = Viewport(editorScene, camera, sysRect, renderPath); + index = index_; + viewportId = viewportId_; + camera.viewMask = 0xffffffff; // It's easier to only have 1 gizmo active this viewport is shared with the gizmo + } + + void ResetCamera() + { + cameraSmoothInterpolate.Stop(); + + cameraLookAtNode.position = Vector3(0, 0, 0); + cameraLookAtNode.rotation = Quaternion(); + + cameraNode.position = Vector3(0, 5, -10); + // Look at the origin so user can see the scene. + cameraNode.rotation = Quaternion(Vector3(0, 0, 1), -cameraNode.position); + ReacquireCameraYawPitch(); + UpdateSettingsUI(); + } + + void ReacquireCameraYawPitch() + { + cameraYaw = cameraNode.rotation.yaw; + cameraPitch = cameraNode.rotation.pitch; + } + + void CreateViewportContextUI() + { + Font@ font = cache.GetResource("Font", "Editor/Fonts/Hack-Regular.ttf"); + + viewportContextUI = UIElement(); + viewportUI.AddChild(viewportContextUI); + viewportContextUI.SetPosition(viewport.rect.left, viewport.rect.top); + viewportContextUI.SetFixedSize(viewport.rect.width / ui.scale, viewport.rect.height / ui.scale); // viewport.rect is in system position + viewportContextUI.clipChildren = true; + + statusBar = BorderImage("ToolBar"); + statusBar.style = "EditorToolBar"; + viewportContextUI.AddChild(statusBar); + + statusBar.SetLayout(LM_HORIZONTAL); + statusBar.SetAlignment(HA_LEFT, VA_BOTTOM); + statusBar.layoutSpacing = 4; + statusBar.opacity = uiMaxOpacity; + + Button@ settingsButton = CreateSmallToolBarButton("Settings"); + statusBar.AddChild(settingsButton); + + cameraPosText = Text(); + statusBar.AddChild(cameraPosText); + + cameraPosText.SetFont(font, 11); + cameraPosText.color = Color(1, 1, 0); + cameraPosText.textEffect = TE_SHADOW; + cameraPosText.priority = -100; + + settingsWindow = LoadEditorUI("Editor/UI/EditorViewport.xml"); + settingsWindow.opacity = uiMaxOpacity; + settingsWindow.visible = false; + viewportContextUI.AddChild(settingsWindow); + + cameraPosX = settingsWindow.GetChild("PositionX", true); + cameraPosY = settingsWindow.GetChild("PositionY", true); + cameraPosZ = settingsWindow.GetChild("PositionZ", true); + cameraRotX = settingsWindow.GetChild("RotationX", true); + cameraRotY = settingsWindow.GetChild("RotationY", true); + cameraRotZ = settingsWindow.GetChild("RotationZ", true); + cameraOrthographic = settingsWindow.GetChild("Orthographic", true); + cameraZoom = settingsWindow.GetChild("Zoom", true); + cameraOrthoSize = settingsWindow.GetChild("OrthoSize", true); + + SubscribeToEvent(cameraPosX, "TextChanged", "HandleSettingsLineEditTextChange"); + SubscribeToEvent(cameraPosY, "TextChanged", "HandleSettingsLineEditTextChange"); + SubscribeToEvent(cameraPosZ, "TextChanged", "HandleSettingsLineEditTextChange"); + SubscribeToEvent(cameraRotX, "TextChanged", "HandleSettingsLineEditTextChange"); + SubscribeToEvent(cameraRotY, "TextChanged", "HandleSettingsLineEditTextChange"); + SubscribeToEvent(cameraRotZ, "TextChanged", "HandleSettingsLineEditTextChange"); + SubscribeToEvent(cameraZoom, "TextChanged", "HandleSettingsLineEditTextChange"); + SubscribeToEvent(cameraOrthoSize, "TextChanged", "HandleSettingsLineEditTextChange"); + SubscribeToEvent(cameraOrthographic, "Toggled", "HandleOrthographicToggled"); + + SubscribeToEvent(settingsButton, "Released", "ToggleViewportSettingsWindow"); + SubscribeToEvent(settingsWindow.GetChild("ResetCamera", true), "Released", "ResetCamera"); + SubscribeToEvent(settingsWindow.GetChild("CopyTransform", true), "Released", "HandleCopyTransformClicked"); + SubscribeToEvent(settingsWindow.GetChild("CloseButton", true), "Released", "CloseViewportSettingsWindow"); + SubscribeToEvent(settingsWindow.GetChild("Refresh", true), "Released", "UpdateSettingsUI"); + HandleResize(); + } + + void HandleResize() + { + viewportContextUI.SetPosition(viewport.rect.left / ui.scale, viewport.rect.top / ui.scale); + viewportContextUI.SetFixedSize(viewport.rect.width / ui.scale, viewport.rect.height / ui.scale); // viewport.rect is in system position + if (viewport.rect.left < 34) + { + statusBar.layoutBorder = IntRect(34 - viewport.rect.left, 4, 4, 8); + IntVector2 pos = settingsWindow.position; + pos.x = 32 - viewport.rect.left; + settingsWindow.position = pos; + } + else + { + statusBar.layoutBorder = IntRect(8, 4, 4, 8); + IntVector2 pos = settingsWindow.position; + pos.x = 5; + settingsWindow.position = pos; + } + + statusBar.SetFixedSize(viewport.rect.width / ui.scale, 22); + } + + void ToggleOrthographic() + { + SetOrthographic(!camera.orthographic); + } + + void SetOrthographic(bool orthographic) + { + camera.orthographic = orthographic; + if (camera.orthographic) + camera.zoom = orthoCameraZoom; + else + camera.zoom = 1.0f; + + UpdateSettingsUI(); + } + + void Update(float timeStep) + { + // Update camera smooth move + if (cameraSmoothInterpolate.IsRunning()) + { + cameraSmoothInterpolate.Update(timeStep); + } + + Vector3 cameraPos = cameraNode.position; + String xText(cameraPos.x); + String yText(cameraPos.y); + String zText(cameraPos.z); + ResizeString(xText, 8); + ResizeString(yText, 8); + ResizeString(zText, 8); + + cameraPosText.text = String( + "Pos: " + xText + " " + yText + " " + zText + + " Zoom: " + camera.zoom); + + cameraPosText.size = cameraPosText.minSize; + } + + void ToggleViewportSettingsWindow() + { + if (settingsWindow.visible) + CloseViewportSettingsWindow(); + else + OpenViewportSettingsWindow(); + } + + void OpenViewportSettingsWindow() + { + UpdateSettingsUI(); + /* settingsWindow.position = */ + settingsWindow.visible = true; + settingsWindow.BringToFront(); + } + + void CloseViewportSettingsWindow() + { + settingsWindow.visible = false; + } + + void UpdateSettingsUI() + { + cameraPosX.text = String(Floor(cameraNode.position.x * 1000) / 1000); + cameraPosY.text = String(Floor(cameraNode.position.y * 1000) / 1000); + cameraPosZ.text = String(Floor(cameraNode.position.z * 1000) / 1000); + cameraRotX.text = String(Floor(cameraNode.rotation.pitch * 1000) / 1000); + cameraRotY.text = String(Floor(cameraNode.rotation.yaw * 1000) / 1000); + cameraRotZ.text = String(Floor(cameraNode.rotation.roll * 1000) / 1000); + cameraZoom.text = String(Floor(camera.zoom * 1000) / 1000); + cameraOrthoSize.text = String(Floor(camera.orthoSize * 1000) / 1000); + // FIXME: this line below appears to be not only redundant but may cause infinite loop as well on Clang build + // cameraOrthographic.checked = camera.orthographic; + } + + void HandleOrthographicToggled(StringHash eventType, VariantMap& eventData) + { + SetOrthographic(cameraOrthographic.checked); + } + + void HandleSettingsLineEditTextChange(StringHash eventType, VariantMap& eventData) + { + LineEdit@ element = eventData["Element"].GetPtr(); + if (element.text == "") + return; + + if (element is cameraRotX || element is cameraRotY || element is cameraRotZ) + { + Vector3 euler = cameraNode.rotation.eulerAngles; + if (element is cameraRotX) + euler.x = element.text.ToFloat(); + else if (element is cameraRotY) + euler.y = element.text.ToFloat(); + else if (element is cameraRotZ) + euler.z = element.text.ToFloat(); + + cameraNode.rotation = Quaternion(euler); + } + else if (element is cameraPosX || element is cameraPosY || element is cameraPosZ) + { + Vector3 pos = cameraNode.position; + if (element is cameraPosX) + pos.x = element.text.ToFloat(); + else if (element is cameraPosY) + pos.y = element.text.ToFloat(); + else if (element is cameraPosZ) + pos.z = element.text.ToFloat(); + + cameraNode.position = pos; + } + else if (element is cameraZoom) + camera.zoom = element.text.ToFloat(); + else if (element is cameraOrthoSize) + camera.orthoSize = element.text.ToFloat(); + } + void HandleCopyTransformClicked(StringHash eventType, VariantMap& eventData) + { + if (editNode !is null) + { + editNode.position = cameraNode.position; + editNode.rotation = cameraNode.rotation; + } + } +} + +Array viewports; +ViewportContext@ activeViewport; + +Text@ editorModeText; +Text@ renderStatsText; +Text@ modelInfoText; + +EditMode editMode = EDIT_MOVE; +AxisMode axisMode = AXIS_WORLD; +FillMode fillMode = FILL_SOLID; +SnapScaleMode snapScaleMode = SNAP_SCALE_FULL; + +float viewNearClip = 0.1; +float viewFarClip = 1000.0; +float viewFov = 45.0; + + +float cameraBaseSpeed = 3; +float cameraBaseRotationSpeed = 0.2; +float cameraShiftSpeedMultiplier = 5; +float moveStep = 0.5; +float rotateStep = 5; +float scaleStep = 0.1; +float snapScale = 1.0; +bool limitRotation = false; +bool moveSnap = false; +bool rotateSnap = false; +bool scaleSnap = false; +bool renderingDebug = false; +bool physicsDebug = false; +bool octreeDebug = false; +bool navigationDebug = false; +int pickMode = PICK_GEOMETRIES; +bool orbiting = false; + +enum MouseOrbitMode +{ + ORBIT_RELATIVE = 0, + ORBIT_WRAP +} + +bool toggledMouseLock = false; +int mouseOrbitMode = ORBIT_RELATIVE; +bool mmbPanMode = false; +bool rotateAroundSelect = false; + +enum NewNodeMode +{ + NEW_NODE_CAMERA_LOOKAT = 0, + NEW_NODE_IN_CENTER, + NEW_NODE_RAYCAST +} + +int newNodeMode = NEW_NODE_CAMERA_LOOKAT; + +bool showGrid = true; +bool grid2DMode = false; +uint gridSize = 16; +uint gridSubdivisions = 3; +float gridScale = 8.0; +Color gridColor(0.1, 0.1, 0.1); +Color gridSubdivisionColor(0.05, 0.05, 0.05); +Color gridXColor(0.5, 0.1, 0.1); +Color gridYColor(0.1, 0.5, 0.1); +Color gridZColor(0.1, 0.1, 0.5); + +Array pickModeDrawableTypes = { + DrawableTypes::Geometry, + DrawableTypes::Light, + DrawableTypes::Zone +}; + +Array editModeText = { + "Move", + "Rotate", + "Scale", + "Select", + "Spawn" +}; + +Array axisModeText = { + "World", + "Local" +}; + +Array pickModeText = { + "Geometries", + "Lights", + "Zones", + "Rigidbodies", + "UI-elements" +}; + +Array fillModeText = { + "Solid", + "Wire", + "Point" +}; + +// This class provides smooth translation/rotation/zoom interpolation for the editor camera +class CameraSmoothInterpolate +{ + Vector3 lookAtNodeBeginPos; + Vector3 cameraNodeBeginPos; + + Vector3 lookAtNodeEndPos; + Vector3 cameraNodeEndPos; + + Quaternion cameraNodeBeginRot; + Quaternion cameraNodeEndRot; + + float cameraBeginZoom; + float cameraEndZoom; + + bool isRunning = false; + float duration = 0.0f; + float elapsedTime = 0.0f; + + bool interpLookAtNodePos = false; + bool interpCameraNodePos = false; + bool interpCameraRot = false; + bool interpCameraZoom = false; + + CameraSmoothInterpolate() + { + } + + void SetLookAtNodePosition(Vector3 lookAtBeginPos, Vector3 lookAtEndPos) + { + lookAtNodeBeginPos = lookAtBeginPos; + lookAtNodeEndPos = lookAtEndPos; + interpLookAtNodePos = true; + } + + void SetCameraNodePosition(Vector3 cameraBeginPos, Vector3 cameraEndPos) + { + cameraNodeBeginPos = cameraBeginPos; + cameraNodeEndPos = cameraEndPos; + interpCameraNodePos = true; + } + + void SetCameraNodeRotation(Quaternion cameraBeginRot, Quaternion cameraEndRot) + { + cameraNodeBeginRot = cameraBeginRot; + cameraNodeEndRot = cameraEndRot; + interpCameraRot = true; + } + + void SetCameraZoom(float beginZoom, float endZoom) + { + cameraBeginZoom = beginZoom; + cameraEndZoom = endZoom; + interpCameraZoom = true; + } + + void Start(float duration_) + { + if (cameraLookAtNode is null || cameraNode is null || camera is null) + return; + + duration = duration_; + elapsedTime = 0.0f; + isRunning = true; + } + + void Stop() + { + interpLookAtNodePos = false; + interpCameraNodePos = false; + interpCameraRot = false; + interpCameraZoom = false; + + isRunning = false; + } + + void Finish() + { + if (!isRunning) + return; + + if (cameraLookAtNode is null || cameraNode is null || camera is null) + return; + + if (interpLookAtNodePos) + cameraLookAtNode.worldPosition = lookAtNodeEndPos; + + if (interpCameraNodePos) + cameraNode.position = cameraNodeEndPos; + + if (interpCameraRot) + { + cameraNode.rotation = cameraNodeEndRot; + ReacquireCameraYawPitch(); + } + + if (interpCameraZoom) + { + orthoCameraZoom = cameraEndZoom; + camera.zoom = cameraEndZoom; + } + + interpLookAtNodePos = false; + interpCameraNodePos = false; + interpCameraRot = false; + interpCameraZoom = false; + + isRunning = false; + } + + bool IsRunning() const + { + return isRunning; + } + + // Cubic easing out + // http://robertpenner.com/easing/ + float EaseOut(float t, float b , float c, float d) + { + return c * ((t = t / d - 1) * t * t + 1) + b; + } + + void Update(float timeStep) + { + if (!isRunning) + return; + + if (cameraLookAtNode is null || cameraNode is null || camera is null) + return; + + elapsedTime += timeStep; + + if (elapsedTime <= duration) + { + float factor = EaseOut(elapsedTime, 0.0f, 1.0f, duration); + + if (interpLookAtNodePos) + cameraLookAtNode.worldPosition = lookAtNodeBeginPos + (lookAtNodeEndPos - lookAtNodeBeginPos) * factor; + + if (interpCameraNodePos) + cameraNode.position = cameraNodeBeginPos + (cameraNodeEndPos - cameraNodeBeginPos) * factor; + + if (interpCameraRot) + { + cameraNode.rotation = cameraNodeBeginRot.Slerp(cameraNodeEndRot, factor); + ReacquireCameraYawPitch(); + } + + if (interpCameraZoom) + { + orthoCameraZoom = cameraBeginZoom + (cameraEndZoom - cameraBeginZoom) * factor; + camera.zoom = orthoCameraZoom; + } + } + else + { + Finish(); + } + } +} + + +CameraSmoothInterpolate cameraSmoothInterpolate; // Camera smooth interpolation control + +void SetRenderPath(const String&in newRenderPathName) +{ + renderPath = null; + renderPathName = newRenderPathName.Trimmed(); + + if (renderPathName.length > 0) + { + File@ file = cache.GetFile(renderPathName); + if (file !is null) + { + XMLFile@ xml = XMLFile(); + if (xml.Load(file)) + { + renderPath = RenderPath(); + if (!renderPath.Load(xml)) + renderPath = null; + } + } + } + + if (renderPath is null) + renderPath = renderer.defaultRenderPath.Clone(); + + // Append gamma correction postprocess and disable/enable it as requested + renderPath.Append(cache.GetResource("XMLFile", "PostProcess/GammaCorrection.xml")); + renderPath.SetEnabled("GammaCorrection", gammaCorrection); + + renderer.hdrRendering = HDR; + + for (uint i = 0; i < renderer.numViewports; ++i) + renderer.viewports[i].renderPath = renderPath; + + if (materialPreview !is null && materialPreview.viewport !is null) + materialPreview.viewport.renderPath = renderPath; + + if (particleEffectPreview !is null && particleEffectPreview.viewport !is null) + particleEffectPreview.viewport.renderPath = renderPath; +} + +void SetGammaCorrection(bool enable) +{ + gammaCorrection = enable; + if (renderPath !is null) + renderPath.SetEnabled("GammaCorrection", gammaCorrection); +} + +void SetHDR(bool enable) +{ + HDR = enable; + if (renderer !is null) + renderer.hdrRendering = HDR; +} + +void CreateCamera() +{ + // Set the initial viewport rect + viewportArea = IntRect(0, 0, graphics.width / ui.scale, graphics.height / ui.scale); + + // Set viewport single to store default hierarchy/inspector height/positions + if(viewportMode == VIEWPORT_COMPACT) + { + SetViewportMode(VIEWPORT_SINGLE); + SetViewportMode(VIEWPORT_COMPACT); + } + else + { + SetViewportMode(viewportMode); + } + + SetActiveViewport(viewports[0]); + + // Note: the camera is not inside the scene, so that it is not listed, and does not get deleted + ResetCamera(); + + // Set initial renderpath if defined + SetRenderPath(renderPathName); +} + +// Create any UI associated with changing the editor viewports +void CreateViewportUI() +{ + if (viewportUI is null) + { + viewportUI = UIElement(); + ui.root.AddChild(viewportUI); + } + + viewportUI.SetFixedSize(viewportArea.width, viewportArea.height); // viewportArea is alreay in scaled UI position + viewportUI.position = IntVector2(viewportArea.top, viewportArea.left); + viewportUI.clipChildren = true; + viewportUI.clipBorder = viewportUIClipBorder; + viewportUI.RemoveAllChildren(); + viewportUI.priority = -2000; + + Array borders; + + IntRect top; + IntRect bottom; + IntRect left; + IntRect right; + IntRect topLeft; + IntRect topRight; + IntRect bottomLeft; + IntRect bottomRight; + + for (uint i = 0; i < viewports.length; ++i) + { + ViewportContext@ vc = viewports[i]; + vc.CreateViewportContextUI(); + + // Here we convert system position to scaled UI position + IntRect rect = IntRectSystemToUI(vc.viewport.rect); + + if (vc.viewportId & VIEWPORT_TOP > 0) + top = rect; + else if (vc.viewportId & VIEWPORT_BOTTOM > 0) + bottom = rect; + else if (vc.viewportId & VIEWPORT_LEFT > 0) + left = rect; + else if (vc.viewportId & VIEWPORT_RIGHT > 0) + right = rect; + else if (vc.viewportId & VIEWPORT_TOP_LEFT > 0) + topLeft = rect; + else if (vc.viewportId & VIEWPORT_TOP_RIGHT > 0) + topRight = rect; + else if (vc.viewportId & VIEWPORT_BOTTOM_LEFT > 0) + bottomLeft = rect; + else if (vc.viewportId & VIEWPORT_BOTTOM_RIGHT > 0) + bottomRight = rect; + } + + // Creates resize borders based on the mode set + if (viewportMode == VIEWPORT_QUAD) // independent borders for quad isn't easy + { + borders.Push(CreateViewportDragBorder(VIEWPORT_BORDER_V, topLeft.right - viewportBorderOffset, topLeft.top, viewportBorderWidth, viewportArea.height)); + borders.Push(CreateViewportDragBorder(VIEWPORT_BORDER_H, topLeft.left, topLeft.bottom-viewportBorderOffset, viewportArea.width, viewportBorderWidth)); + } + else + { + // Figures what borders to create based on mode + if (viewportMode & (VIEWPORT_LEFT|VIEWPORT_RIGHT) > 0) + { + borders.Push( + viewportMode & VIEWPORT_LEFT > 0 ? + CreateViewportDragBorder(VIEWPORT_BORDER_V, left.right-viewportBorderOffset, left.top, viewportBorderWidth, left.height) : + CreateViewportDragBorder(VIEWPORT_BORDER_V, right.left-viewportBorderOffset, right.top, viewportBorderWidth, right.height) + ); + } + else + { + if (viewportMode & (VIEWPORT_TOP_LEFT|VIEWPORT_TOP_RIGHT) > 0) + borders.Push(CreateViewportDragBorder(VIEWPORT_BORDER_V1, topLeft.right-viewportBorderOffset, topLeft.top, viewportBorderWidth, topLeft.height)); + if (viewportMode & (VIEWPORT_BOTTOM_LEFT|VIEWPORT_BOTTOM_RIGHT) > 0) + borders.Push(CreateViewportDragBorder(VIEWPORT_BORDER_V2, bottomLeft.right-viewportBorderOffset, bottomLeft.top, viewportBorderWidth, bottomLeft.height)); + } + + if (viewportMode & (VIEWPORT_TOP|VIEWPORT_BOTTOM) > 0) + { + borders.Push( + viewportMode & VIEWPORT_TOP > 0 ? + CreateViewportDragBorder(VIEWPORT_BORDER_H, top.left, top.bottom-viewportBorderOffset, top.width, viewportBorderWidth) : + CreateViewportDragBorder(VIEWPORT_BORDER_H, bottom.left, bottom.top-viewportBorderOffset, bottom.width, viewportBorderWidth) + ); + } + else + { + if (viewportMode & (VIEWPORT_TOP_LEFT|VIEWPORT_BOTTOM_LEFT) > 0) + borders.Push(CreateViewportDragBorder(VIEWPORT_BORDER_H1, topLeft.left, topLeft.bottom-viewportBorderOffset, topLeft.width, viewportBorderWidth)); + if (viewportMode & (VIEWPORT_TOP_RIGHT|VIEWPORT_BOTTOM_RIGHT) > 0) + borders.Push(CreateViewportDragBorder(VIEWPORT_BORDER_H2, topRight.left, topRight.bottom-viewportBorderOffset, topRight.width, viewportBorderWidth)); + } + } +} + +BorderImage@ CreateViewportDragBorder(uint value, int posX, int posY, int sizeX, int sizeY) +{ + BorderImage@ border = BorderImage(); + viewportUI.AddChild(border); + border.name = "border"; + border.style = "ViewportBorder"; + border.vars["VIEWMODE"] = value; + border.SetFixedSize(sizeX, sizeY); // relevant size gets set by viewport later + border.position = IntVector2(posX, posY); + border.opacity = uiMaxOpacity; + SubscribeToEvent(border, "DragMove", "HandleViewportBorderDragMove"); + SubscribeToEvent(border, "DragEnd", "HandleViewportBorderDragEnd"); + return border; +} + +void SetFillMode(FillMode fillMode_) +{ + fillMode = fillMode_; + for (uint i = 0; i < viewports.length; ++i) + viewports[i].camera.fillMode = fillMode_; +} + +// Sets the viewport mode +void SetViewportMode(uint mode = VIEWPORT_SINGLE) +{ + // Remember old viewport positions + Array cameralookAtPositions; + Array cameraLookAtRotations; + Array cameraPositions; + Array cameraRotations; + for (uint i = 0; i < viewports.length; ++i) + { + cameralookAtPositions.Push(viewports[i].cameraLookAtNode.position); + cameraLookAtRotations.Push(viewports[i].cameraLookAtNode.rotation); + + cameraPositions.Push(viewports[i].cameraNode.position); + cameraRotations.Push(viewports[i].cameraNode.rotation); + } + + viewports.Clear(); + + if(mode == VIEWPORT_COMPACT) + { + // Remember old hierarchy/inspector height/positions + if(viewportMode != VIEWPORT_COMPACT){ + restoreViewport = true; + oldHierarchyWindowPosition = hierarchyWindow.position; + oldHierarchyWindowHeight = hierarchyWindow.height; + oldInspectorWindowPosition = attributeInspectorWindow.position; + oldInspectorWindowHeight = attributeInspectorWindow.height; + } + + // Move and scale hierarchy window to left of screen + ShowHierarchyWindow(); + hierarchyWindow.position = IntVector2(secondaryToolBar.width,toolBar.height + uiMenuBar.height); + hierarchyWindow.height = viewportArea.height-(toolBar.height + uiMenuBar.height); + + // Move and scale inspector window to left of screen + ShowAttributeInspectorWindow(); + attributeInspectorWindow.position = IntVector2(viewportArea.width-attributeInspectorWindow.width,toolBar.height + uiMenuBar.height); + attributeInspectorWindow.height = viewportArea.height-(toolBar.height + uiMenuBar.height); + + // Hide close button and disable resize/movement inspector/hierarchy of windows + attributeInspectorWindow.GetChild("CloseButton",true).visible = false; + attributeInspectorWindow.resizable = false; + attributeInspectorWindow.movable = false; + hierarchyWindow.GetChild("CloseButton",true).visible = false; + hierarchyWindow.resizable = false; + hierarchyWindow.movable = false; + + // Create viewport on center of window + { + uint viewport = 0; + ViewportContext@ vc = ViewportContext( + IntRect( + secondaryToolBar.width + hierarchyWindow.width, + toolBar.height + uiMenuBar.height, + viewportArea.width-attributeInspectorWindow.width, + viewportArea.height), + viewports.length + 1, + viewportMode & (VIEWPORT_TOP|VIEWPORT_LEFT|VIEWPORT_TOP_LEFT) + ); + viewports.Push(vc); + } + viewportMode = mode; + + } + else + { + if(viewportMode == VIEWPORT_COMPACT) + { + // Restore hierarchy/inspector windows height/positions + if(restoreViewport) + { + hierarchyWindow.position = oldHierarchyWindowPosition; + hierarchyWindow.height = oldHierarchyWindowHeight; + attributeInspectorWindow.position = oldInspectorWindowPosition; + attributeInspectorWindow.height = oldInspectorWindowHeight; + } + + // Show close button and enable resize/movement of inspector/hierarchy windows + attributeInspectorWindow.GetChild("CloseButton",true).visible = true; + attributeInspectorWindow.resizable = true; + attributeInspectorWindow.movable = true; + hierarchyWindow.GetChild("CloseButton",true).visible = true; + hierarchyWindow.resizable = true; + hierarchyWindow.movable = true; + } + + viewportMode = mode; + + // Always have quad a + { + uint viewport = 0; + ViewportContext@ vc = ViewportContext( + IntRect( + 0, + 0, + mode & (VIEWPORT_LEFT|VIEWPORT_TOP_LEFT) > 0 ? viewportArea.width / 2 : viewportArea.width, + mode & (VIEWPORT_TOP|VIEWPORT_TOP_LEFT) > 0 ? viewportArea.height / 2 : viewportArea.height), + viewports.length + 1, + viewportMode & (VIEWPORT_TOP|VIEWPORT_LEFT|VIEWPORT_TOP_LEFT) + ); + viewports.Push(vc); + } + + uint topRight = viewportMode & (VIEWPORT_RIGHT|VIEWPORT_TOP_RIGHT); + if (topRight > 0) + { + ViewportContext@ vc = ViewportContext( + IntRect( + viewportArea.width/2, + 0, + viewportArea.width, + mode & VIEWPORT_TOP_RIGHT > 0 ? viewportArea.height / 2 : viewportArea.height), + viewports.length + 1, + topRight + ); + viewports.Push(vc); + } + + uint bottomLeft = viewportMode & (VIEWPORT_BOTTOM|VIEWPORT_BOTTOM_LEFT); + if (bottomLeft > 0) + { + ViewportContext@ vc = ViewportContext( + IntRect( + 0, + viewportArea.height / 2, + mode & (VIEWPORT_BOTTOM_LEFT) > 0 ? viewportArea.width / 2 : viewportArea.width, + viewportArea.height), + viewports.length + 1, + bottomLeft + ); + viewports.Push(vc); + } + + uint bottomRight = viewportMode & (VIEWPORT_BOTTOM_RIGHT); + if (bottomRight > 0) + { + ViewportContext@ vc = ViewportContext( + IntRect( + viewportArea.width / 2, + viewportArea.height / 2, + viewportArea.width, + viewportArea.height), + viewports.length + 1, + bottomRight + ); + viewports.Push(vc); + } + + } + + renderer.numViewports = viewports.length; + for (uint i = 0; i < viewports.length; ++i) + renderer.viewports[i] = viewports[i].viewport; + + // Restore camera positions as applicable. Default new viewports to the last camera position + if (cameraPositions.length > 0) + { + for (uint i = 0; i < viewports.length; ++i) + { + uint src = i; + if (src >= cameraPositions.length) + src = cameraPositions.length - 1; + + viewports[i].cameraLookAtNode.position = cameralookAtPositions[src]; + viewports[i].cameraLookAtNode.rotation = cameraLookAtRotations[src]; + + viewports[i].cameraNode.position = cameraPositions[src]; + viewports[i].cameraNode.rotation = cameraRotations[src]; + } + } + + ReacquireCameraYawPitch(); + UpdateViewParameters(); + UpdateCameraPreview(); + CreateViewportUI(); +} + +// Create a preview viewport if a camera component is selected +void UpdateCameraPreview() +{ + previewCamera = null; + StringHash cameraType("Camera"); + + for (uint i = 0; i < selectedComponents.length; ++i) + { + if (selectedComponents[i].type == cameraType) + { + // Take the first encountered camera + previewCamera = selectedComponents[i]; + break; + } + } + // Also try nodes if not found from components + if (previewCamera.Get() is null) + { + for (uint i = 0; i < selectedNodes.length; ++i) + { + previewCamera = selectedNodes[i].GetComponent("Camera"); + if (previewCamera.Get() !is null) + break; + } + } + + // Remove extra viewport if it exists and no camera is selected + if (previewCamera.Get() is null) + { + if (renderer.numViewports > viewports.length) + renderer.numViewports = viewports.length; + } + else + { + if (renderer.numViewports < viewports.length + 1) + renderer.numViewports = viewports.length + 1; + + int previewWidth = graphics.width / 4; + int previewHeight = previewWidth * 9 / 16; + int previewX = graphics.width - 10 - previewWidth; + int previewY = graphics.height - 30 - previewHeight; + + Viewport@ previewView = Viewport(); + previewView.scene = editorScene; + previewView.camera = previewCamera.Get(); + previewView.rect = IntRect(previewX, previewY, previewX + previewWidth, previewY + previewHeight); + previewView.renderPath = renderPath; + renderer.viewports[viewports.length] = previewView; + } +} + +void HandleViewportBorderDragMove(StringHash eventType, VariantMap& eventData) +{ + UIElement@ dragBorder = eventData["Element"].GetPtr(); + if (dragBorder is null) + return; + + uint hPos; + uint vPos; + + // Moves border to new cursor position, restricts motion to 1 axis, and keeps borders within view area + if (resizingBorder & VIEWPORT_BORDER_V_ANY > 0) + { + hPos = Clamp(ui.cursorPosition.x, 150, viewportArea.width-150); + vPos = dragBorder.position.y; + dragBorder.position = IntVector2(hPos, vPos); + } + if (resizingBorder & VIEWPORT_BORDER_H_ANY > 0) + { + vPos = Clamp(ui.cursorPosition.y, 150, viewportArea.height-150); + hPos = dragBorder.position.x; + dragBorder.position = IntVector2(hPos, vPos); + } + + // Move all dependent borders + Array borders = viewportUI.GetChildren(); + for (uint i = 0; i < borders.length; ++i) + { + BorderImage@ border = borders[i]; + if (border is null || border is dragBorder || border.name != "border") + continue; + + uint borderViewMode = border.vars["VIEWMODE"].GetU32(); + if (resizingBorder == VIEWPORT_BORDER_H) + { + if (borderViewMode == VIEWPORT_BORDER_V1) + { + border.SetFixedHeight(vPos); + } + else if (borderViewMode == VIEWPORT_BORDER_V2) + { + border.position = IntVector2(border.position.x, vPos); + border.SetFixedHeight(viewportArea.height - vPos); + } + } + else if (resizingBorder == VIEWPORT_BORDER_V) + { + if (borderViewMode == VIEWPORT_BORDER_H1) + { + border.SetFixedWidth(hPos); + } + else if (borderViewMode == VIEWPORT_BORDER_H2) + { + border.position = IntVector2(hPos, border.position.y); + border.SetFixedWidth(viewportArea.width - hPos); + } + } + } +} + +void HandleViewportBorderDragEnd(StringHash eventType, VariantMap& eventData) +{ + // Sets the new viewports by checking all the dependencies + Array children = viewportUI.GetChildren(); + Array borders; + + BorderImage@ borderV; + BorderImage@ borderV1; + BorderImage@ borderV2; + BorderImage@ borderH; + BorderImage@ borderH1; + BorderImage@ borderH2; + + for (uint i = 0; i < children.length; ++i) + { + if (children[i].name == "border") + { + BorderImage@ border = children[i]; + uint mode = border.vars["VIEWMODE"].GetU32(); + if (mode == VIEWPORT_BORDER_V) + borderV = border; + else if (mode == VIEWPORT_BORDER_V1) + borderV1 = border; + else if (mode == VIEWPORT_BORDER_V2) + borderV2 = border; + else if (mode == VIEWPORT_BORDER_H) + borderH = border; + else if (mode == VIEWPORT_BORDER_H1) + borderH1 = border; + else if (mode == VIEWPORT_BORDER_H2) + borderH2 = border; + } + } + + IntRect top; + IntRect bottom; + IntRect left; + IntRect right; + IntRect topLeft; + IntRect topRight; + IntRect bottomLeft; + IntRect bottomRight; + + for (uint i = 0; i < viewports.length; ++i) + { + ViewportContext@ vc = viewports[i]; + if (vc.viewportId & VIEWPORT_TOP > 0) + top = vc.viewport.rect; + else if (vc.viewportId & VIEWPORT_BOTTOM > 0) + bottom = vc.viewport.rect; + else if (vc.viewportId & VIEWPORT_LEFT > 0) + left = vc.viewport.rect; + else if (vc.viewportId & VIEWPORT_RIGHT > 0) + right = vc.viewport.rect; + else if (vc.viewportId & VIEWPORT_TOP_LEFT > 0) + topLeft = vc.viewport.rect; + else if (vc.viewportId & VIEWPORT_TOP_RIGHT > 0) + topRight = vc.viewport.rect; + else if (vc.viewportId & VIEWPORT_BOTTOM_LEFT > 0) + bottomLeft = vc.viewport.rect; + else if (vc.viewportId & VIEWPORT_BOTTOM_RIGHT > 0) + bottomRight = vc.viewport.rect; + } + + if (borderV !is null) + { + if (viewportMode & VIEWPORT_LEFT > 0) + left.right = borderV.position.x + viewportBorderOffset; + if (viewportMode & VIEWPORT_TOP_LEFT > 0) + topLeft.right = borderV.position.x + viewportBorderOffset; + if (viewportMode & VIEWPORT_TOP_RIGHT > 0) + topRight.left = borderV.position.x + viewportBorderOffset; + if (viewportMode & VIEWPORT_RIGHT > 0) + right.left = borderV.position.x + viewportBorderOffset; + if (viewportMode & VIEWPORT_BOTTOM_LEFT > 0) + bottomLeft.right = borderV.position.x + viewportBorderOffset; + if (viewportMode & VIEWPORT_BOTTOM_RIGHT > 0) + bottomRight.left = borderV.position.x + viewportBorderOffset; + } + else + { + if (borderV1 !is null) + { + if (viewportMode & VIEWPORT_TOP_LEFT > 0) + topLeft.right = borderV1.position.x + viewportBorderOffset; + if (viewportMode & VIEWPORT_TOP_RIGHT > 0) + topRight.left = borderV1.position.x + viewportBorderOffset; + } + if (borderV2 !is null) + { + if (viewportMode & VIEWPORT_BOTTOM_LEFT > 0) + bottomLeft.right = borderV2.position.x + viewportBorderOffset; + if (viewportMode & VIEWPORT_BOTTOM_RIGHT > 0) + bottomRight.left = borderV2.position.x + viewportBorderOffset; + } + } + + if (borderH !is null) + { + if (viewportMode & VIEWPORT_TOP > 0) + top.bottom = borderH.position.y + viewportBorderOffset; + if (viewportMode & VIEWPORT_TOP_LEFT > 0) + topLeft.bottom = borderH.position.y + viewportBorderOffset; + if (viewportMode & VIEWPORT_BOTTOM_LEFT > 0) + bottomLeft.top = borderH.position.y + viewportBorderOffset; + if (viewportMode & VIEWPORT_BOTTOM > 0) + bottom.top = borderH.position.y + viewportBorderOffset; + if (viewportMode & VIEWPORT_TOP_RIGHT > 0) + topRight.bottom = borderH.position.y + viewportBorderOffset; + if (viewportMode & VIEWPORT_BOTTOM_RIGHT > 0) + bottomRight.top = borderH.position.y + viewportBorderOffset; + } + else + { + if (borderH1 !is null) + { + if (viewportMode & VIEWPORT_TOP_LEFT > 0) + topLeft.bottom = borderH1.position.y+viewportBorderOffset; + if (viewportMode & VIEWPORT_BOTTOM_LEFT > 0) + bottomLeft.top = borderH1.position.y+viewportBorderOffset; + } + if (borderH2 !is null) + { + if (viewportMode & VIEWPORT_TOP_RIGHT > 0) + topRight.bottom = borderH2.position.y+viewportBorderOffset; + if (viewportMode & VIEWPORT_BOTTOM_RIGHT > 0) + bottomRight.top = borderH2.position.y+viewportBorderOffset; + } + } + + // Applies the calculated changes + for (uint i = 0; i < viewports.length; ++i) + { + ViewportContext@ vc = viewports[i]; + if (vc.viewportId & VIEWPORT_TOP > 0) + vc.viewport.rect = top; + else if (vc.viewportId & VIEWPORT_BOTTOM > 0) + vc.viewport.rect = bottom; + else if (vc.viewportId & VIEWPORT_LEFT > 0) + vc.viewport.rect = left; + else if (vc.viewportId & VIEWPORT_RIGHT > 0) + vc.viewport.rect = right; + else if (vc.viewportId & VIEWPORT_TOP_LEFT > 0) + vc.viewport.rect = topLeft; + else if (vc.viewportId & VIEWPORT_TOP_RIGHT > 0) + vc.viewport.rect = topRight; + else if (vc.viewportId & VIEWPORT_BOTTOM_LEFT > 0) + vc.viewport.rect = bottomLeft; + else if (vc.viewportId & VIEWPORT_BOTTOM_RIGHT > 0) + vc.viewport.rect = bottomRight; + vc.HandleResize(); + } + + // End drag state + resizingBorder = 0; + setViewportCursor = 0; +} + +void SetViewportCursor() +{ + if (setViewportCursor & VIEWPORT_BORDER_V_ANY > 0) + ui.cursor.shape = CS_RESIZEHORIZONTAL; + else if (setViewportCursor & VIEWPORT_BORDER_H_ANY > 0) + ui.cursor.shape = CS_RESIZEVERTICAL; +} + +void SetActiveViewport(ViewportContext@ context) +{ + // Sets the global variables to the current context + @cameraLookAtNode = context.cameraLookAtNode; + @cameraNode = context.cameraNode; + @camera = context.camera; + @audio.listener = context.soundListener; + + // Camera is created before gizmo, this gets called again after UI is created + if (gizmo !is null) + gizmo.viewMask = camera.viewMask; + + @activeViewport = context; + + // If a mode is changed while in a drag or hovering over a border these can get out of sync + resizingBorder = 0; + setViewportCursor = 0; +} + +void ResetCamera() +{ + for (uint i = 0; i < viewports.length; ++i) + viewports[i].ResetCamera(); +} + +void ReacquireCameraYawPitch() +{ + for (uint i = 0; i < viewports.length; ++i) + viewports[i].ReacquireCameraYawPitch(); +} + +void UpdateViewParameters() +{ + for (uint i = 0; i < viewports.length; ++i) + { + viewports[i].camera.nearClip = viewNearClip; + viewports[i].camera.farClip = viewFarClip; + viewports[i].camera.fov = viewFov; + } +} + +void CreateGrid() +{ + if (gridNode !is null) + gridNode.Remove(); + + gridNode = Node(); + gridNode.name = "EditorGrid"; + grid = gridNode.CreateComponent("CustomGeometry"); + grid.numGeometries = 1; + grid.material = cache.GetResource("Material", "Materials/VColUnlit.xml"); + grid.viewMask = 0x80000000; // Editor raycasts use viewmask 0x7fffffff + grid.occludee = false; + + UpdateGrid(); +} + +void HideGrid() +{ + if (grid !is null) + grid.enabled = false; +} + +void ShowGrid() +{ + if (grid !is null) + { + grid.enabled = true; + + if (editorScene.octree !is null) + editorScene.octree.AddManualDrawable(grid); + } +} + +void UpdateGrid(bool updateGridGeometry = true) +{ + showGrid ? ShowGrid() : HideGrid(); + gridNode.scale = Vector3(gridScale, gridScale, gridScale); + + if (!updateGridGeometry) + return; + + uint size = uint(Floor(gridSize / 2) * 2); + float halfSizeScaled = size / 2; + float scale = 1.0; + uint subdivisionSize = uint(Pow(2.0f, float(gridSubdivisions))); + + if (subdivisionSize > 0) + { + size *= subdivisionSize; + scale /= subdivisionSize; + } + + uint halfSize = size / 2; + + grid.BeginGeometry(0, LINE_LIST); + float lineOffset = -halfSizeScaled; + for (uint i = 0; i <= size; ++i) + { + bool lineCenter = i == halfSize; + bool lineSubdiv = !Equals(Mod(i, subdivisionSize), 0.0); + + if (!grid2DMode) + { + grid.DefineVertex(Vector3(lineOffset, 0.0, halfSizeScaled)); + grid.DefineColor(lineCenter ? gridZColor : (lineSubdiv ? gridSubdivisionColor : gridColor)); + grid.DefineVertex(Vector3(lineOffset, 0.0, -halfSizeScaled)); + grid.DefineColor(lineCenter ? gridZColor : (lineSubdiv ? gridSubdivisionColor : gridColor)); + + grid.DefineVertex(Vector3(-halfSizeScaled, 0.0, lineOffset)); + grid.DefineColor(lineCenter ? gridXColor : (lineSubdiv ? gridSubdivisionColor : gridColor)); + grid.DefineVertex(Vector3(halfSizeScaled, 0.0, lineOffset)); + grid.DefineColor(lineCenter ? gridXColor : (lineSubdiv ? gridSubdivisionColor : gridColor)); + } + else + { + grid.DefineVertex(Vector3(lineOffset, halfSizeScaled, 0.0)); + grid.DefineColor(lineCenter ? gridYColor : (lineSubdiv ? gridSubdivisionColor : gridColor)); + grid.DefineVertex(Vector3(lineOffset, -halfSizeScaled, 0.0)); + grid.DefineColor(lineCenter ? gridYColor : (lineSubdiv ? gridSubdivisionColor : gridColor)); + + grid.DefineVertex(Vector3(-halfSizeScaled, lineOffset, 0.0)); + grid.DefineColor(lineCenter ? gridXColor : (lineSubdiv ? gridSubdivisionColor : gridColor)); + grid.DefineVertex(Vector3(halfSizeScaled, lineOffset, 0.0)); + grid.DefineColor(lineCenter ? gridXColor : (lineSubdiv ? gridSubdivisionColor : gridColor)); + } + + lineOffset += scale; + } + grid.Commit(); +} + +void CreateStatsBar() +{ + editorModeText = Text(); + ui.root.AddChild(editorModeText); + renderStatsText = Text(); + ui.root.AddChild(renderStatsText); + modelInfoText = Text(); + ui.root.AddChild(modelInfoText); +} + +void SetupStatsBarText(Text@ text, Font@ font, int x, int y, HorizontalAlignment hAlign, VerticalAlignment vAlign) +{ + text.position = IntVector2(x, y); + text.horizontalAlignment = hAlign; + text.verticalAlignment = vAlign; + text.SetFont(font, 11); + text.color = Color(1, 1, 0); + text.textEffect = TE_SHADOW; + text.priority = -100; +} + +void UpdateStats(float timeStep) +{ + String adding = ""; + // Todo: add localization + if (hotKeyMode == HOTKEYS_MODE_BLENDER) + adding = localization.Get(" CameraFlyMode: ") + (cameraFlyMode ? "True" : "False"); + + editorModeText.text = String( + localization.Get("Mode: ") + localization.Get(editModeText[editMode]) + + localization.Get(" Axis: ") + localization.Get(axisModeText[axisMode]) + + localization.Get(" Pick: ") + localization.Get(pickModeText[pickMode]) + + localization.Get(" Fill: ") + localization.Get(fillModeText[fillMode]) + + localization.Get(" Updates: ") + (runUpdate ? localization.Get("Running") : localization.Get("Paused") + adding)); + + renderStatsText.text = String( + localization.Get("Tris: ") + renderer.numPrimitives + + localization.Get(" Batches: ") + renderer.numBatches + + localization.Get(" Lights: ") + renderer.numLights[true] + + localization.Get(" Shadowmaps: ") + renderer.numShadowMaps[true] + + localization.Get(" Occluders: ") + renderer.numOccluders[true]); + + editorModeText.size = editorModeText.minSize; + renderStatsText.size = renderStatsText.minSize; + + // Relayout stats bar + Font@ font = cache.GetResource("Font", "Editor/Fonts/Hack-Regular.ttf"); + + if(viewportMode != VIEWPORT_COMPACT) + { + if (ui.root.width >= editorModeText.size.x + renderStatsText.size.x + 45) + { + SetupStatsBarText(editorModeText, font, 35, 64, HA_LEFT, VA_TOP); + SetupStatsBarText(renderStatsText, font, -4, 64, HA_RIGHT, VA_TOP); + SetupStatsBarText(modelInfoText, font, 35, 88, HA_LEFT, VA_TOP); + } + else + { + SetupStatsBarText(editorModeText, font, 35, 64, HA_LEFT, VA_TOP); + SetupStatsBarText(renderStatsText, font, 35, 78, HA_LEFT, VA_TOP); + SetupStatsBarText(modelInfoText, font, 35, 102, HA_LEFT, VA_TOP); + } + } + else + { + SetupStatsBarText(editorModeText, font, secondaryToolBar.width + hierarchyWindow.width + 10 , 64, HA_LEFT, VA_TOP); + SetupStatsBarText(renderStatsText, font, secondaryToolBar.width + hierarchyWindow.width + 10 , 84, HA_LEFT, VA_TOP); + SetupStatsBarText(modelInfoText, font, secondaryToolBar.width + hierarchyWindow.width + 10, 104, HA_LEFT, VA_TOP); + } +} + +void UpdateViewports(float timeStep) +{ + for(uint i = 0; i < viewports.length; ++i) + { + ViewportContext@ viewportContext = viewports[i]; + viewportContext.Update(timeStep); + } +} + +void SetMouseMode(bool enable) +{ + if (enable) + { + if (mouseOrbitMode == ORBIT_RELATIVE) + { + input.mouseMode = MM_RELATIVE; + ui.cursor.visible = false; + } + else if (mouseOrbitMode == ORBIT_WRAP) + input.mouseMode = MM_WRAP; + } + else + { + input.mouseMode = MM_ABSOLUTE; + ui.cursor.visible = true; + } +} + +void SetMouseLock() +{ + toggledMouseLock = true; + SetMouseMode(true); + FadeUI(); +} + +void ReleaseMouseLock() +{ + if (toggledMouseLock) + { + toggledMouseLock = false; + SetMouseMode(false); + } +} + +void CameraPan(Vector3 trans) +{ + cameraSmoothInterpolate.Stop(); + + cameraLookAtNode.Translate(trans); +} + +void CameraMoveForward(Vector3 trans) +{ + cameraSmoothInterpolate.Stop(); + + cameraNode.Translate(trans, TransformSpace::Parent); +} + +void CameraRotateAroundLookAt(Quaternion rot) +{ + cameraSmoothInterpolate.Stop(); + + cameraNode.rotation = rot; + + Vector3 dir = cameraNode.direction; + dir.Normalize(); + + float dist = cameraNode.position.length; + + cameraNode.position = -dir * dist; +} + +void CameraRotateAroundCenter(Quaternion rot) +{ + cameraSmoothInterpolate.Stop(); + + cameraNode.rotation = rot; + + Vector3 oldPos = cameraNode.worldPosition; + + Vector3 dir = cameraNode.worldDirection; + dir.Normalize(); + + float dist = cameraNode.position.length; + + cameraLookAtNode.worldPosition = cameraNode.worldPosition + dir * dist; + cameraNode.worldPosition = oldPos; +} + +void CameraRotateAroundSelect(Quaternion rot) +{ + cameraSmoothInterpolate.Stop(); + + cameraNode.rotation = rot; + + Vector3 dir = cameraNode.direction; + dir.Normalize(); + + float dist = cameraNode.position.length; + + cameraNode.position = -dir * dist; + + Vector3 centerPoint; + if ((selectedNodes.length > 0 || selectedComponents.length > 0)) + centerPoint = SelectedNodesCenterPoint(); + else + centerPoint = lastSelectedNodesCenterPoint; + + // legacy way, camera look-at will jump to the selection + cameraLookAtNode.worldPosition = centerPoint; +} + +void CameraZoom(float zoom) +{ + cameraSmoothInterpolate.Stop(); + + camera.zoom = Clamp(zoom, .1, 30); +} + +void HandleStandardUserInput(float timeStep) +{ + // Speedup camera move if Shift key is down + float speedMultiplier = 1.0; + if (input.keyDown[KEY_LSHIFT]) + speedMultiplier = cameraShiftSpeedMultiplier; + + // Handle FPS mode + if (!input.keyDown[KEY_LCTRL] && !input.keyDown[KEY_LALT]) + { + if (input.keyDown[KEY_W] || input.keyDown[KEY_UP]) + { + Vector3 dir = cameraNode.direction; + dir.Normalize(); + + CameraPan(dir * timeStep * cameraBaseSpeed * speedMultiplier); + FadeUI(); + } + if (input.keyDown[KEY_S] || input.keyDown[KEY_DOWN]) + { + Vector3 dir = cameraNode.direction; + dir.Normalize(); + + CameraPan(-dir * timeStep * cameraBaseSpeed * speedMultiplier); + FadeUI(); + } + if (input.keyDown[KEY_A] || input.keyDown[KEY_LEFT]) + { + Vector3 dir = cameraNode.right; + dir.Normalize(); + + CameraPan(-dir * timeStep * cameraBaseSpeed * speedMultiplier); + FadeUI(); + } + if (input.keyDown[KEY_D] || input.keyDown[KEY_RIGHT]) + { + Vector3 dir = cameraNode.right; + dir.Normalize(); + + CameraPan(dir * timeStep * cameraBaseSpeed * speedMultiplier); + FadeUI(); + } + if (input.keyDown[KEY_E] || input.keyDown[KEY_PAGEUP]) + { + Vector3 dir = cameraNode.up; + dir.Normalize(); + + CameraPan(dir * timeStep * cameraBaseSpeed * speedMultiplier); + FadeUI(); + } + if (input.keyDown[KEY_Q] || input.keyDown[KEY_PAGEDOWN]) + { + Vector3 dir = cameraNode.up; + dir.Normalize(); + + CameraPan(-dir * timeStep * cameraBaseSpeed * speedMultiplier); + FadeUI(); + } + } + + // Zoom in/out + if (input.mouseMoveWheel != 0 && ui.GetElementAt(ui.cursor.position) is null) + { + float distance = cameraNode.position.length; + float ratio = distance / 40.0f; + float factor = ratio < 1.0f ? ratio : 1.0f; + + if (!camera.orthographic) + { + Vector3 dir = cameraNode.direction; + dir.Normalize(); + dir *= input.mouseMoveWheel * 40 * timeStep * cameraBaseSpeed * speedMultiplier * factor; + + CameraMoveForward(dir); + } + else + { + float zoom = camera.zoom + input.mouseMoveWheel * speedMultiplier * factor; + + CameraZoom(zoom); + } + } + + + // Rotate/orbit/pan camera + bool changeCamViewButton = false; + + changeCamViewButton = input.mouseButtonDown[MOUSEB_RIGHT] || input.mouseButtonDown[MOUSEB_MIDDLE]; + + if (changeCamViewButton) + { + SetMouseLock(); + + IntVector2 mouseMove = input.mouseMove; + if (mouseMove.x != 0 || mouseMove.y != 0) + { + bool panTheCamera = false; + + if (input.mouseButtonDown[MOUSEB_MIDDLE]) + { + if (mmbPanMode) + panTheCamera = !input.keyDown[KEY_LSHIFT]; + else + panTheCamera = input.keyDown[KEY_LSHIFT]; + } + + // Pan the camera + if (panTheCamera) + { + Vector3 right = -cameraNode.worldRight; + right.Normalize(); + right *= mouseMove.x; + Vector3 up = cameraNode.worldUp; + up.Normalize(); + up *= mouseMove.y; + + Vector3 trans = (right + up) * timeStep * cameraBaseSpeed * 0.5; + + CameraPan(trans); + } + else // Rotate the camera + { + activeViewport.cameraYaw += mouseMove.x * cameraBaseRotationSpeed; + activeViewport.cameraPitch += mouseMove.y * cameraBaseRotationSpeed; + + if (limitRotation) + activeViewport.cameraPitch = Clamp(activeViewport.cameraPitch, -90.0, 90.0); + + Quaternion rot = Quaternion(activeViewport.cameraPitch, activeViewport.cameraYaw, 0); + + if (input.mouseButtonDown[MOUSEB_MIDDLE]) // Rotate around the camera center + { + if (rotateAroundSelect) + CameraRotateAroundSelect(rot); + else + CameraRotateAroundLookAt(rot); + + orbiting = true; + } + else // Rotate around the look-at + { + CameraRotateAroundCenter(rot); + + orbiting = true; + } + } + } + } + else + ReleaseMouseLock(); + + if (orbiting && !input.mouseButtonDown[MOUSEB_MIDDLE]) + orbiting = false; +} + +void HandleBlenderUserInput(float timeStep) +{ + if (ui.HasModalElement() || ui.focusElement !is null) + { + ReleaseMouseLock(); + return; + } + + // Check for camara fly mode + if (input.keyDown[KEY_LSHIFT] && input.keyPress[KEY_F]) + { + cameraFlyMode = !cameraFlyMode; + } + + // Speedup camera move if Shift key is down + float speedMultiplier = 1.0; + if (input.keyDown[KEY_LSHIFT]) + speedMultiplier = cameraShiftSpeedMultiplier; + + // Handle FPS mode + if (!input.keyDown[KEY_LCTRL] && !input.keyDown[KEY_LALT]) + { + if (cameraFlyMode /*&& !input.keyDown[KEY_LSHIFT]*/) + { + if (input.keyDown[KEY_W] || input.keyDown[KEY_UP]) + { + Vector3 dir = cameraNode.direction; + dir.Normalize(); + + CameraPan(dir * timeStep * cameraBaseSpeed * speedMultiplier); + FadeUI(); + } + if (input.keyDown[KEY_S] || input.keyDown[KEY_DOWN]) + { + Vector3 dir = cameraNode.direction; + dir.Normalize(); + + CameraPan(-dir * timeStep * cameraBaseSpeed * speedMultiplier); + FadeUI(); + } + if (input.keyDown[KEY_A] || input.keyDown[KEY_LEFT]) + { + Vector3 dir = cameraNode.right; + dir.Normalize(); + + CameraPan(-dir * timeStep * cameraBaseSpeed * speedMultiplier); + FadeUI(); + } + if (input.keyDown[KEY_D] || input.keyDown[KEY_RIGHT]) + { + Vector3 dir = cameraNode.right; + dir.Normalize(); + + CameraPan(dir * timeStep * cameraBaseSpeed * speedMultiplier); + FadeUI(); + } + if (input.keyDown[KEY_E] || input.keyDown[KEY_PAGEUP]) + { + Vector3 dir = cameraNode.up; + dir.Normalize(); + + CameraPan(dir * timeStep * cameraBaseSpeed * speedMultiplier); + FadeUI(); + } + if (input.keyDown[KEY_Q] || input.keyDown[KEY_PAGEDOWN]) + { + Vector3 dir = cameraNode.up; + dir.Normalize(); + + CameraPan(-dir * timeStep * cameraBaseSpeed * speedMultiplier); + FadeUI(); + } + } + } + + if (input.mouseMoveWheel != 0 && ui.GetElementAt(ui.cursor.position) is null) + { + if (!camera.orthographic) + { + if (input.keyDown[KEY_LSHIFT]) + { + Vector3 dir = cameraNode.up; + dir.Normalize(); + + CameraPan(dir * input.mouseMoveWheel * 5 * timeStep * cameraBaseSpeed * speedMultiplier); + } + else if (input.keyDown[KEY_LCTRL]) + { + Vector3 dir = cameraNode.right; + dir.Normalize(); + + CameraPan(dir * input.mouseMoveWheel * 5 * timeStep * cameraBaseSpeed * speedMultiplier); + } + else // Zoom in/out + { + float distance = cameraNode.position.length; + float ratio = distance / 40.0f; + float factor = ratio < 1.0f ? ratio : 1.0f; + + Vector3 dir = cameraNode.direction; + dir.Normalize(); + dir *= input.mouseMoveWheel * 40 * timeStep * cameraBaseSpeed * speedMultiplier * factor; + + CameraMoveForward(dir); + } + } + else + { + if (input.keyDown[KEY_LSHIFT]) + { + Vector3 dir = cameraNode.up; + dir.Normalize(); + + CameraPan(dir * input.mouseMoveWheel * timeStep * cameraBaseSpeed * speedMultiplier * 4.0f); + } + else if (input.keyDown[KEY_LCTRL]) + { + Vector3 dir = cameraNode.right; + dir.Normalize(); + + CameraPan(dir * input.mouseMoveWheel * timeStep * cameraBaseSpeed * speedMultiplier * 4.0f); + } + else + { + float zoom = camera.zoom + input.mouseMoveWheel * speedMultiplier * 0.5f; + + CameraZoom(zoom); + } + } + } + + // Rotate/orbit/pan camera + bool changeCamViewButton = input.mouseButtonDown[MOUSEB_MIDDLE] || cameraFlyMode; + + if (input.mouseButtonPress[MOUSEB_RIGHT] || input.keyDown[KEY_ESCAPE]) + cameraFlyMode = false; + + if (changeCamViewButton) + { + SetMouseLock(); + + IntVector2 mouseMove = input.mouseMove; + if (mouseMove.x != 0 || mouseMove.y != 0) + { + bool panTheCamera = false; + + if (!cameraFlyMode) + panTheCamera = input.keyDown[KEY_LSHIFT]; + + if (panTheCamera) + { + Vector3 right = -cameraNode.worldRight; + right.Normalize(); + right *= mouseMove.x; + Vector3 up = cameraNode.worldUp; + up.Normalize(); + up *= mouseMove.y; + + Vector3 trans = (right + up) * timeStep * cameraBaseSpeed * 0.5; + + CameraPan(trans); + } + else + { + activeViewport.cameraYaw += mouseMove.x * cameraBaseRotationSpeed; + activeViewport.cameraPitch += mouseMove.y * cameraBaseRotationSpeed; + + if (limitRotation) + activeViewport.cameraPitch = Clamp(activeViewport.cameraPitch, -90.0, 90.0); + + Quaternion rot = Quaternion(activeViewport.cameraPitch, activeViewport.cameraYaw, 0); + + if (cameraFlyMode) + { + CameraRotateAroundCenter(rot); + orbiting = true; + } + else if (input.mouseButtonDown[MOUSEB_MIDDLE]) + { + if (rotateAroundSelect) + CameraRotateAroundSelect(rot); + else + CameraRotateAroundLookAt(rot); + + orbiting = true; + } + } + } + } + else + ReleaseMouseLock(); + + if (orbiting && !input.mouseButtonDown[MOUSEB_MIDDLE]) + orbiting = false; + + // force to select component node for manipulation if selected only component and not his node + if ((editMode != EDIT_SELECT && editNodes.empty) && lastSelectedComponent.Get() !is null) + { + if (lastSelectedComponent.Get() !is null) + { + Component@ component = lastSelectedComponent.Get(); + SelectNode(component.node, false); + } + } +} + +void UpdateView(float timeStep) +{ + if (ui.HasModalElement() || ui.focusElement !is null) + { + ReleaseMouseLock(); + return; + } + + if (hotKeyMode == HOTKEYS_MODE_STANDARD) + { + HandleStandardUserInput(timeStep); + } + else if (hotKeyMode == HOTKEYS_MODE_BLENDER) + { + HandleBlenderUserInput(timeStep); + } + + if (!editNodes.empty && editMode != EDIT_SELECT && input.keyDown[KEY_LCTRL]) + { + Vector3 adjust(0, 0, 0); + if (input.keyDown[KEY_UP]) + adjust.z = 1; + if (input.keyDown[KEY_DOWN]) + adjust.z = -1; + if (input.keyDown[KEY_LEFT]) + adjust.x = -1; + if (input.keyDown[KEY_RIGHT]) + adjust.x = 1; + if (input.keyDown[KEY_PAGEUP]) + adjust.y = 1; + if (input.keyDown[KEY_PAGEDOWN]) + adjust.y = -1; + if (editMode == EDIT_SCALE) + { + if (input.keyDown[KEY_KP_PLUS]) + adjust = Vector3(1, 1, 1); + if (input.keyDown[KEY_KP_MINUS]) + adjust = Vector3(-1, -1, -1); + } + + if (adjust == Vector3(0, 0, 0)) + return; + + bool moved = false; + adjust *= timeStep * 10; + + switch (editMode) + { + case EDIT_MOVE: + if (!moveSnap) + moved = MoveNodes(adjust * moveStep); + break; + + case EDIT_ROTATE: + if (!rotateSnap) + moved = RotateNodes(adjust * rotateStep); + break; + + case EDIT_SCALE: + if (!scaleSnap) + moved = ScaleNodes(adjust * scaleStep); + break; + } + + if (moved) + UpdateNodeAttributes(); + } + + // If not dragging + if (resizingBorder == 0) + { + UIElement@ uiElement = ui.GetElementAt(ui.cursorPosition); + if (uiElement !is null && uiElement.vars.Contains("VIEWMODE")) + { + setViewportCursor = uiElement.vars["VIEWMODE"].GetU32(); + if (input.mouseButtonDown[MOUSEB_LEFT]) + resizingBorder = setViewportCursor; + } + } +} + +void SteppedObjectManipulation(int key) +{ + if (editNodes.empty || editMode == EDIT_SELECT) + return; + + // Do not react in non-snapped mode, because that is handled in frame update + if (editMode == EDIT_MOVE && !moveSnap) + return; + if (editMode == EDIT_ROTATE && !rotateSnap) + return; + if (editMode == EDIT_SCALE && !scaleSnap) + return; + + Vector3 adjust(0, 0, 0); + if (key == KEY_UP) + adjust.z = 1; + if (key == KEY_DOWN) + adjust.z = -1; + if (key == KEY_LEFT) + adjust.x = -1; + if (key == KEY_RIGHT) + adjust.x = 1; + if (key == KEY_PAGEUP) + adjust.y = 1; + if (key == KEY_PAGEDOWN) + adjust.y = -1; + if (editMode == EDIT_SCALE) + { + if (key == KEY_KP_PLUS) + adjust = Vector3(1, 1, 1); + if (key == KEY_KP_MINUS) + adjust = Vector3(-1, -1, -1); + } + + if (adjust == Vector3(0, 0, 0)) + return; + + bool moved = false; + + switch (editMode) + { + case EDIT_MOVE: + moved = MoveNodes(adjust); + break; + + case EDIT_ROTATE: + { + float rotateStepScaled = rotateStep * snapScale; + moved = RotateNodes(adjust * rotateStepScaled); + } + break; + + case EDIT_SCALE: + { + float scaleStepScaled = scaleStep * snapScale; + moved = ScaleNodes(adjust * scaleStepScaled); + } + break; + } + + if (moved) + UpdateNodeAttributes(); +} + +void HandlePostRenderUpdate() +{ + DebugRenderer@ debug = editorScene.debugRenderer; + if (debug is null || orbiting || debugRenderDisabled) + return; + + // Visualize the currently selected nodes + for (uint i = 0; i < selectedNodes.length; ++i) + DrawNodeDebug(selectedNodes[i], debug); + + // Visualize the currently selected components + for (uint i = 0; i < selectedComponents.length; ++i) + selectedComponents[i].DrawDebugGeometry(debug, false); + + // Visualize the currently selected UI-elements + for (uint i = 0; i < selectedUIElements.length; ++i) + ui.DebugDraw(selectedUIElements[i]); + + if (renderingDebug) + renderer.DrawDebugGeometry(false); + + if (physicsDebug && editorScene.physicsWorld !is null) + editorScene.physicsWorld.DrawDebugGeometry(true); + + if (physicsDebug && editorScene.physicsWorld2D !is null) + { + bool needDraw = true; + for (uint i = 0; i < selectedComponents.length; ++i) + { + if (cast(selectedComponents[i]) !is null) + { + needDraw = false; // Already drawed + break; + } + } + + if (needDraw) + physicsWorld2D.DrawDebugGeometry(); + } + + if (octreeDebug && editorScene.octree !is null) + editorScene.octree.DrawDebugGeometry(true); + + if (navigationDebug) + { + CrowdManager@ crowdManager = editorScene.GetComponent("CrowdManager"); + if (crowdManager !is null) + crowdManager.DrawDebugGeometry(true); + + Array@ navMeshes = editorScene.GetComponents("NavigationMesh", true); + for (uint i = 0; i < navMeshes.length; ++i) + cast(navMeshes[i]).DrawDebugGeometry(true); + + Array@ dynNavMeshes = editorScene.GetComponents("DynamicNavigationMesh", true); + for (uint i = 0; i < dynNavMeshes.length; ++i) + cast(dynNavMeshes[i]).DrawDebugGeometry(true); + } + + if (setViewportCursor | resizingBorder > 0) + { + SetViewportCursor(); + if (resizingBorder == 0) + setViewportCursor = 0; + } + + ViewRaycast(false); +} + +void DrawNodeDebug(Node@ node, DebugRenderer@ debug, bool drawNode = true) +{ + if (drawNode) + debug.AddNode(node, 1.0, false); + + // Exception for the scene to avoid bringing the editor to its knees: drawing either the whole hierarchy or the subsystem- + // components can have a large performance hit. Also do not draw terrain child nodes due to their large amount + // (TerrainPatch component itself draws nothing as debug geometry) + if (node !is editorScene && node.GetComponent("Terrain") is null) + { + for (uint j = 0; j < node.numComponents; ++j) + node.components[j].DrawDebugGeometry(debug, false); + + // To avoid cluttering the view, do not draw the node axes for child nodes + for (uint k = 0; k < node.numChildren; ++k) + DrawNodeDebug(node.children[k], debug, false); + } +} + +void ViewMouseMove() +{ + Ray cameraRay = GetActiveViewportCameraRay(); + Component@ selectedComponent; + + if (pickMode < PICK_RIGIDBODIES && editorScene.octree !is null) + { + RayQueryResult result = editorScene.octree.RaycastSingle(cameraRay, RAY_TRIANGLE, camera.farClip, + pickModeDrawableTypes[pickMode], 0x7fffffff); + + if (result.drawable !is null && result.drawable.typeName == "TerrainPatch" && result.drawable.node.parent !is null) + { + Terrain@ terrainComponent = result.drawable.node.parent.GetComponent("Terrain"); + terrainEditor.UpdateBrushVisualizer(terrainComponent, result.position); + } + else { + terrainEditor.HideBrushVisualizer(); + } + } + + // setting mouse position based on mouse position + if (ui.IsDragging()) { } + else if (ui.focusElement !is null || input.mouseButtonDown[MOUSEB_LEFT|MOUSEB_MIDDLE|MOUSEB_RIGHT]) + return; + + IntVector2 pos = ui.cursor.position; + pos = ui.ConvertUIToSystem(pos); + for (uint i = 0; i < viewports.length; ++i) + { + ViewportContext@ vc = viewports[i]; + if (vc !is activeViewport && vc.viewport.rect.IsInside(pos) == INSIDE) + SetActiveViewport(vc); + } +} + +void ViewMouseClick() +{ + ViewRaycast(true); +} + +Ray GetActiveViewportCameraRay() +{ + IntVector2 pos = ui.ConvertUIToSystem(ui.cursorPosition); + IntRect view = activeViewport.viewport.rect; + return camera.GetScreenRay( + float(pos.x - view.left) / view.width, + float(pos.y - view.top) / view.height + ); +} + +void ViewMouseClickEnd() +{ + // checks to close open popup windows + IntVector2 pos = ui.cursorPosition; + if (contextMenu !is null && contextMenu.enabled) + { + if (contextMenuActionWaitFrame) + contextMenuActionWaitFrame = false; + else + { + if (!contextMenu.IsInside(pos, true)) + CloseContextMenu(); + } + } + if (quickMenu !is null && quickMenu.enabled) + { + bool enabled = quickMenu.IsInside(pos, true); + quickMenu.enabled = enabled; + quickMenu.visible = enabled; + } +} + +void ViewRaycast(bool mouseClick) +{ + // Ignore if UI has modal element + if (ui.HasModalElement()) + return; + + // Ignore if mouse is grabbed by other operation + if (input.mouseGrabbed) + return; + + IntVector2 pos = ui.cursorPosition; + UIElement@ elementAtPos = ui.GetElementAt(pos, pickMode != PICK_UI_ELEMENTS); + if (editMode == EDIT_SPAWN) + { + if(mouseClick && input.mouseButtonPress[MOUSEB_LEFT] && elementAtPos is null) + SpawnObject(); + return; + } + + // Do not raycast / change selection if hovering over the gizmo + if (IsGizmoSelected()) + return; + + DebugRenderer@ debug = editorScene.debugRenderer; + + if (pickMode == PICK_UI_ELEMENTS) + { + bool leftClick = mouseClick && input.mouseButtonPress[MOUSEB_LEFT]; + bool multiselect = input.qualifierDown[QUAL_CTRL]; + + // Only interested in user-created UI elements + if (elementAtPos !is null && elementAtPos !is editorUIElement && elementAtPos.GetElementEventSender() is editorUIElement) + { + ui.DebugDraw(elementAtPos); + + if (leftClick) + SelectUIElement(elementAtPos, multiselect); + } + // If clicked on emptiness in non-multiselect mode, clear the selection + else if (leftClick && !multiselect && ui.GetElementAt(pos) is null) + hierarchyList.ClearSelection(); + + return; + } + + // Do not raycast / change selection if hovering over a UI element when not in PICK_UI_ELEMENTS Mode + if (elementAtPos !is null) + return; + + Ray cameraRay = GetActiveViewportCameraRay(); + Component@ selectedComponent; + + if (pickMode < PICK_RIGIDBODIES) + { + if (editorScene.octree is null) + return; + + RayQueryResult result = editorScene.octree.RaycastSingle(cameraRay, RAY_TRIANGLE, camera.farClip, + pickModeDrawableTypes[pickMode], 0x7fffffff); + + if (result.drawable !is null) + { + Drawable@ drawable = result.drawable; + + // for actual last selected node or component in both modes + if (hotKeyMode == HOTKEYS_MODE_STANDARD) + { + if (input.mouseButtonDown[MOUSEB_LEFT]) + { + lastSelectedNode = drawable.node; + lastSelectedDrawable = drawable; + lastSelectedComponent = drawable; + } + } + else if (hotKeyMode == HOTKEYS_MODE_BLENDER) + { + if (input.mouseButtonDown[MOUSEB_RIGHT]) + { + lastSelectedNode = drawable.node; + lastSelectedDrawable = drawable; + lastSelectedComponent = drawable; + } + } + + // If selecting a terrain patch, select the parent terrain instead + if (drawable.typeName != "TerrainPatch") + { + selectedComponent = drawable; + if (debug !is null) + { + debug.AddNode(drawable.node, 1.0, false); + drawable.DrawDebugGeometry(debug, false); + } + } + else if (drawable.node.parent !is null){ + Terrain@ terrainComponent = drawable.node.parent.GetComponent("Terrain"); + selectedComponent = terrainComponent; + if (selectedComponent is terrainComponent && input.mouseButtonDown[MOUSEB_LEFT]) + { + selectedComponent = terrainComponent; + terrainEditor.Work(terrainComponent, result.position); + } + else + { + terrainEditor.targetColorSelected = false; + } + } + } + } + else + { + if (editorScene.physicsWorld is null) + return; + + // If we are not running the actual physics update, refresh collisions before raycasting + if (!runUpdate) + editorScene.physicsWorld.UpdateCollisions(); + + PhysicsRaycastResult result = editorScene.physicsWorld.RaycastSingle(cameraRay, camera.farClip); + if (result.body !is null) + { + RigidBody@ body = result.body; + if (debug !is null) + { + debug.AddNode(body.node, 1.0, false); + body.DrawDebugGeometry(debug, false); + } + selectedComponent = body; + } + } + + bool multiselect = false; + bool componentSelectQualifier = false; + bool mouseButtonPressRL = false; + + if (hotKeyMode == HOTKEYS_MODE_STANDARD) + { + mouseButtonPressRL = input.mouseButtonPress[MOUSEB_LEFT]; + componentSelectQualifier = input.qualifierDown[QUAL_SHIFT]; + multiselect = input.qualifierDown[QUAL_CTRL]; + } + else if (hotKeyMode == HOTKEYS_MODE_BLENDER) + { + mouseButtonPressRL = input.mouseButtonPress[MOUSEB_RIGHT]; + componentSelectQualifier = input.qualifierDown[QUAL_CTRL]; + multiselect = input.qualifierDown[QUAL_SHIFT]; + } + + if (mouseClick && mouseButtonPressRL) + { + if (selectedComponent !is null) + { + if (componentSelectQualifier) + { + // If we are selecting components, but have nodes in existing selection, do not multiselect to prevent confusion + if (!selectedNodes.empty) + multiselect = false; + SelectComponent(selectedComponent, multiselect); + } + else + { + // If we are selecting nodes, but have components in existing selection, do not multiselect to prevent confusion + if (!selectedComponents.empty) + multiselect = false; + SelectNode(selectedComponent.node, multiselect); + } + } + else + { + // If clicked on emptiness in non-multiselect mode, clear the selection + if (!multiselect) + SelectComponent(null, false); + } + } +} + +Vector3 GetNewNodePosition(bool raycastToMouse = false) +{ + if (newNodeMode == NEW_NODE_IN_CENTER) + return Vector3(0, 0, 0); + if (newNodeMode == NEW_NODE_RAYCAST) + { + Ray cameraRay = raycastToMouse ? GetActiveViewportCameraRay() : camera.GetScreenRay(0.5, 0.5); + Vector3 position, normal; + if (GetSpawnPosition(cameraRay, camera.farClip, position, normal, 0, false)) + return position; + } + return cameraLookAtNode.worldPosition; +} + +int GetShadowResolution() +{ + if (!renderer.drawShadows) + return 0; + int level = 1; + int res = renderer.shadowMapSize; + while (res > 512) + { + res >>= 1; + ++level; + } + if (level > 3) + level = 3; + + return level; +} + +void SetShadowResolution(int level) +{ + if (level <= 0) + { + renderer.drawShadows = false; + return; + } + else + { + renderer.drawShadows = true; + renderer.shadowMapSize = 256 << level; + } +} + +void ToggleRenderingDebug() +{ + renderingDebug = !renderingDebug; +} + +void TogglePhysicsDebug() +{ + physicsDebug = !physicsDebug; +} + +void ToggleOctreeDebug() +{ + octreeDebug = !octreeDebug; +} + +void ToggleNavigationDebug() +{ + navigationDebug = !navigationDebug; +} + +bool StopTestAnimation() +{ + testAnimState = null; + return true; +} + +void MergeNodeBoundingBox(BoundingBox &inout box, Array&inout visitedComponents, Node@ node) +{ + if (node is null || node is editorScene) + return; + + // if node has no component, merge its world position + if (node.numComponents == 0) + { + box.Merge(node.worldPosition); + } + + // Merge components bounding box of this node + for (uint i = 0; i < node.numComponents; ++i) + { + MergeComponentBoundingBox(box, visitedComponents, node.components[i]); + } + + // Merge bounding boxes of child nodes recursively + for (uint i = 0; i < node.numChildren; ++i) + { + Node@ child = node.children[i]; + MergeNodeBoundingBox(box, visitedComponents, child); + } +} + +void MergeComponentBoundingBox(BoundingBox &inout box, Array&inout visitedComponents, Component@ component) +{ + if (component is null || visitedComponents.FindByRef(component) != -1) + return; + + Drawable@ drawable = cast(component); + + // Merge drawable component's bounding box. Skip skybox, as its box is very large, as well as lights + if (drawable !is null && cast(drawable) is null && cast(drawable) is null) + { + box.Merge(drawable.worldBoundingBox); + visitedComponents.Push(component); + return; + } + + // If the component is not a drawable, merge the world position of its node + if (component.node !is editorScene) + box.Merge(component.node.worldPosition); + + visitedComponents.Push(component); +} + +void LocateNodes(Array nodes) +{ + if (nodes.empty || (nodes.length == 1 && nodes[0] is editorScene)) + return; + + // Calculate bounding box of all nodes + BoundingBox box; + Array visitedComponents; + + for (uint i = 0; i < nodes.length; ++i) + { + MergeNodeBoundingBox(box, visitedComponents, nodes[i]); + } + + FitCamera(box, true); +} + +void LocateComponents(Array components) +{ + if (components.empty || components.length == 1 && components[0].node is editorScene) + return; + + // Calculate bounding box of all nodes + BoundingBox box; + Array visitedComponents; + + for (uint i = 0; i < components.length; ++i) + { + MergeComponentBoundingBox(box, visitedComponents, components[i]); + } + + FitCamera(box, true); +} + +void LocateNodesAndComponents(Array nodes, Array components) +{ + if (nodes.length == 0 && components.length == 0) + return; + + // Calculate bounding box of all nodes + BoundingBox box; + Array visitedComponents; + + if (!nodes.empty && !(nodes.length == 1 && nodes[0] is editorScene)) + { + for (uint i = 0; i < nodes.length; ++i) + { + MergeNodeBoundingBox(box, visitedComponents, nodes[i]); + } + } + + if (!components.empty) + { + for (uint i = 0; i < components.length; ++i) + { + MergeComponentBoundingBox(box, visitedComponents, components[i]); + } + } + + FitCamera(box, true); +} + +void FitCamera(BoundingBox box, bool smooth) +{ + // Calculate proper camera distance - fit the bounding sphere into the camera frustum + Sphere sphere = Sphere(box); + + float aspect = camera.aspectRatio; + float fov = 0.0f; + + // Choose the small one from vertical and horizontal fovs + if (aspect > 1.0f) + fov = camera.fov; + else + fov = camera.fov * aspect; + + fov *= 0.5f; + + if (sphere.radius < 1.0f) + sphere.radius = 1.0f; + + float distance = sphere.radius / Sin(fov); + + if (distance > viewFarClip) + distance = viewFarClip; + + Vector3 dir = cameraNode.direction; + dir.Normalize(); + + // Make the distance a little farther + distance *= 1.1f; + + // Set zoom value a little bigger + float zoom = camera.orthoSize / (sphere.radius * 2.0f); + zoom *= 1.1f; + + // We put the pivot node to the center of the bounding sphere + // and put the camera node to the opposite of view direction + Vector3 lookAtPos = sphere.center; + Vector3 cameraPos = -dir * distance; + + cameraSmoothInterpolate.Stop(); + + if (smooth) + { + cameraSmoothInterpolate.SetLookAtNodePosition(cameraLookAtNode.worldPosition, lookAtPos); + cameraSmoothInterpolate.SetCameraNodePosition(cameraNode.position, cameraPos); + + if (camera.orthographic) + cameraSmoothInterpolate.SetCameraZoom(camera.zoom, zoom); + + cameraSmoothInterpolate.Start(0.5f); + } + else + { + cameraLookAtNode.worldPosition = lookAtPos; + cameraNode.position = cameraPos; + + if (camera.orthographic) + camera.zoom = zoom; + } +} + +Vector3 SelectedNodesCenterPoint() +{ + Vector3 centerPoint; + uint count = selectedNodes.length; + for (uint i = 0; i < selectedNodes.length; ++i) + centerPoint += selectedNodes[i].worldPosition; + + for (uint i = 0; i < selectedComponents.length; ++i) + { + Drawable@ drawable = cast(selectedComponents[i]); + count++; + if (drawable !is null) + centerPoint += drawable.node.LocalToWorld(drawable.boundingBox.center); + else + centerPoint += selectedComponents[i].node.worldPosition; + } + + if (count > 0) + { + lastSelectedNodesCenterPoint = centerPoint / count; + return centerPoint / count; + } + else + { + lastSelectedNodesCenterPoint = centerPoint; + return centerPoint; + } +} + +Drawable@ GetDrawableAtMousePostion() +{ + IntVector2 pos = ui.ConvertUIToSystem(ui.cursorPosition); + Ray cameraRay = camera.GetScreenRay(float(pos.x) / activeViewport.viewport.rect.width, float(pos.y) / activeViewport.viewport.rect.height); + + if (editorScene.octree is null) + return null; + + RayQueryResult result = editorScene.octree.RaycastSingle(cameraRay, RAY_TRIANGLE, camera.farClip, DrawableTypes::Geometry, 0x7fffffff); + + return result.drawable; +} + +void HandleBeginViewUpdate(StringHash eventType, VariantMap& eventData) +{ + // Hide gizmo, grid and debug icons from any camera other then active viewport + if (eventData["Camera"].GetPtr() !is camera) + { + if (gizmo !is null) + gizmo.viewMask = 0; + } + if (eventData["Camera"].GetPtr() is previewCamera.Get()) + { + suppressSceneChanges = true; + if (grid !is null) + grid.viewMask = 0; + if (debugIconsNode !is null) + debugIconsNode.enabled = false; + suppressSceneChanges = false; + } +} + +void HandleEndViewUpdate(StringHash eventType, VariantMap& eventData) +{ + // Restore gizmo and grid after camera view update + if (eventData["Camera"].GetPtr() !is camera) + { + if (gizmo !is null) + gizmo.viewMask = 0x80000000; + } + if (eventData["Camera"].GetPtr() is previewCamera.Get()) + { + suppressSceneChanges = true; + if (grid !is null) + grid.viewMask = 0x80000000; + if (debugIconsNode !is null) + debugIconsNode.enabled = true; + suppressSceneChanges = false; + } +} + +bool debugWasEnabled = true; + +void HandleBeginViewRender(StringHash eventType, VariantMap& eventData) +{ + // Hide debug geometry from preview camera + if (eventData["Camera"].GetPtr() is previewCamera.Get()) + { + DebugRenderer@ debug = editorScene.GetComponent("DebugRenderer"); + if (debug !is null) + { + suppressSceneChanges = true; // Do not want UI update now + debugWasEnabled = debug.enabled; + debug.enabled = false; + suppressSceneChanges = false; + } + } +} + +void HandleEndViewRender(StringHash eventType, VariantMap& eventData) +{ + // Restore debug geometry after preview camera render + if (eventData["Camera"].GetPtr() is previewCamera.Get()) + { + DebugRenderer@ debug = editorScene.GetComponent("DebugRenderer"); + if (debug !is null) + { + suppressSceneChanges = true; // Do not want UI update now + debug.enabled = debugWasEnabled; + suppressSceneChanges = false; + } + } +} diff --git a/bin/Data/Scripts/Editor/EditorViewDebugIcons.as b/bin/EditorData/Editor/Scripts/EditorViewDebugIcons.as similarity index 98% rename from bin/Data/Scripts/Editor/EditorViewDebugIcons.as rename to bin/EditorData/Editor/Scripts/EditorViewDebugIcons.as index a24dc422491..e67ed1dff6f 100644 --- a/bin/Data/Scripts/Editor/EditorViewDebugIcons.as +++ b/bin/EditorData/Editor/Scripts/EditorViewDebugIcons.as @@ -1,7 +1,7 @@ // Editor debug icons // to add new std debug icons just add IconType, IconsTypesMaterials and ComponentTypes items -enum IconsTypes +enum IconsTypes { ICON_POINT_LIGHT = 0, ICON_SPOT_LIGHT, @@ -18,7 +18,7 @@ enum IconsTypes ICON_COUNT } -enum IconsColorType +enum IconsColorType { ICON_COLOR_DEFAULT = 0, ICON_COLOR_SPLINE_PATH_BEGIN, @@ -77,7 +77,7 @@ void CreateDebugIcons(Node@ tempNode) for (int i = 0; i < ICON_COUNT; ++i) { debugIconsSet[i] = tempNode.CreateComponent("BillboardSet"); - debugIconsSet[i].material = cache.GetResource("Material", "Materials/Editor/" + iconsTypesMaterials[i]); + debugIconsSet[i].material = cache.GetResource("Material", "Editor/Materials/" + iconsTypesMaterials[i]); debugIconsSet[i].sorted = true; debugIconsSet[i].temporary = true; debugIconsSet[i].fixedScreenSize = true; @@ -91,7 +91,7 @@ void UpdateViewDebugIcons() debugIconsNode = editorScene.GetChild("DebugIconsContainer", true); - if (debugIconsNode is null) + if (debugIconsNode is null) { debugIconsNode = editorScene.CreateChild("DebugIconsContainer", LOCAL); debugIconsNode.temporary = true; diff --git a/bin/Data/Scripts/Editor/EditorViewPaintSelection.as b/bin/EditorData/Editor/Scripts/EditorViewPaintSelection.as similarity index 98% rename from bin/Data/Scripts/Editor/EditorViewPaintSelection.as rename to bin/EditorData/Editor/Scripts/EditorViewPaintSelection.as index 7ec10fd115c..b65fd4dfa5a 100644 --- a/bin/Data/Scripts/Editor/EditorViewPaintSelection.as +++ b/bin/EditorData/Editor/Scripts/EditorViewPaintSelection.as @@ -32,7 +32,7 @@ void CreatePaintSelectionTool() paintSelectionImage = BorderImage("Icon"); paintSelectionImage.temporary = true; paintSelectionImage.SetFixedSize(paintSelectionBrushDefaultSize.x,paintSelectionBrushDefaultSize.y); - paintSelectionImage.texture = cache.GetResource("Texture2D", "Textures/Editor/SelectionCircle.png"); + paintSelectionImage.texture = cache.GetResource("Texture2D", "Editor/Textures/SelectionCircle.png"); paintSelectionImage.imageRect = IntRect(0,0,512,512); paintSelectionImage.priority = -5; paintSelectionImage.color = Color(1,1,1); @@ -65,7 +65,7 @@ void UpdatePaintSelection() // Set visibility for all origins EditorPaintSelectionUIContainer.visible = EditorPaintSelectionShow; - if (viewportMode!=VIEWPORT_SINGLE) + if (viewportMode!=VIEWPORT_SINGLE) EditorPaintSelectionUIContainer.visible = false; if (EditorPaintSelectionShow) diff --git a/bin/Data/Scripts/Editor/EditorViewSelectableOrigins.as b/bin/EditorData/Editor/Scripts/EditorViewSelectableOrigins.as similarity index 97% rename from bin/Data/Scripts/Editor/EditorViewSelectableOrigins.as rename to bin/EditorData/Editor/Scripts/EditorViewSelectableOrigins.as index caf433fdaae..980e98db294 100644 --- a/bin/Data/Scripts/Editor/EditorViewSelectableOrigins.as +++ b/bin/EditorData/Editor/Scripts/EditorViewSelectableOrigins.as @@ -58,7 +58,7 @@ void HandleOriginToggled(StringHash eventType, VariantMap& eventData) if (IsSceneOrigin(origin)) { int nodeID = origin.vars[ORIGIN_NODEID_VAR].GetI32(); - if (editorScene !is null) + if (editorScene !is null) { bool goBackAndSelectNodeParent = input.qualifierDown[QUAL_CTRL]; bool multiSelect = input.qualifierDown[QUAL_SHIFT]; @@ -84,14 +84,14 @@ void ShowOrigins(bool isVisible = true) if (EditorOriginUIContainer is null) CreateOriginsContainer(); - + EditorOriginUIContainer.visible = isVisible; } void UpdateOrigins() { // Early out if Origins are disabled - if (!EditorOriginShow) return; + if (!EditorOriginShow) return; CheckKeyboardQualifers(); @@ -111,9 +111,9 @@ void UpdateOrigins() // Set visibility for all origins EditorOriginUIContainer.visible = EditorOriginShow; - if (viewportMode!=VIEWPORT_SINGLE) + if (viewportMode!=VIEWPORT_SINGLE) EditorOriginUIContainer.visible = false; - + // Forced read nodes for some reason: if ((originsNodes.length < 1) || rebuildSceneOrigins) { @@ -177,7 +177,7 @@ void UpdateOrigins() originsNames[i].visible = true; } else - { + { if (showNamesForAll || (isOriginsHovered && originHoveredIndex == i)) originsNames[i].text = NodeInfo(originsNodes[i], selectedNodeInfoState); } @@ -194,7 +194,7 @@ void UpdateOrigins() { VisibilityOrigin(j, false); } - } + } } } @@ -229,7 +229,7 @@ void ShowSelectedNodeOrigin(Node@ node, int index) originsIcons[index].color = ORIGIN_COLOR_SELECTED; else originsIcons[index].color = ORIGIN_COLOR_DISABLED; - + originsIcons[index].SetFixedSize(ORIGIN_ICON_SIZE_SELECTED.x,ORIGIN_ICON_SIZE_SELECTED.y); // if selected node chaged, reset some vars @@ -251,14 +251,14 @@ void ShowSelectedNodeOrigin(Node@ node, int index) } } -void CreateOrigin(int index, bool isVisible = false) +void CreateOrigin(int index, bool isVisible = false) { if (originsIcons.length < index) return; originsIcons[index] = BorderImage("Icon"); originsIcons[index].temporary = true; originsIcons[index].SetFixedSize(ORIGIN_ICON_SIZE.x,ORIGIN_ICON_SIZE.y); - originsIcons[index].texture = cache.GetResource("Texture2D", "Textures/Editor/EditorIcons.png"); + originsIcons[index].texture = cache.GetResource("Texture2D", "Editor/Textures/EditorIcons.png"); originsIcons[index].imageRect = IntRect(0,0,14,14); originsIcons[index].priority = -1000; originsIcons[index].color = ORIGIN_COLOR; @@ -270,7 +270,7 @@ void CreateOrigin(int index, bool isVisible = false) originsNames[index] = Text(); originsNames[index].visible = false; - originsNames[index].SetFont(cache.GetResource("Font", "Fonts/Anonymous Pro.ttf"), NAMES_SIZE); + originsNames[index].SetFont(cache.GetResource("Font", "Editor/Fonts/Hack-Regular.ttf"), NAMES_SIZE); originsNames[index].color = ORIGIN_COLOR_TEXT; //originsNames[index].textEffect = TE_STROKE; originsNames[index].temporary = true; diff --git a/bin/Data/EditorStrings.json b/bin/EditorData/Editor/Strings/EditorStrings.json similarity index 100% rename from bin/Data/EditorStrings.json rename to bin/EditorData/Editor/Strings/EditorStrings.json diff --git a/bin/Data/Textures/Editor/BW.png b/bin/EditorData/Editor/Textures/BW.png similarity index 100% rename from bin/Data/Textures/Editor/BW.png rename to bin/EditorData/Editor/Textures/BW.png diff --git a/bin/Data/Textures/Editor/EditorIcons.png b/bin/EditorData/Editor/Textures/EditorIcons.png similarity index 100% rename from bin/Data/Textures/Editor/EditorIcons.png rename to bin/EditorData/Editor/Textures/EditorIcons.png diff --git a/bin/Data/Textures/Editor/EditorIcons.xml b/bin/EditorData/Editor/Textures/EditorIcons.xml similarity index 100% rename from bin/Data/Textures/Editor/EditorIcons.xml rename to bin/EditorData/Editor/Textures/EditorIcons.xml diff --git a/bin/Data/Textures/Editor/HSV20.png b/bin/EditorData/Editor/Textures/HSV20.png similarity index 100% rename from bin/Data/Textures/Editor/HSV20.png rename to bin/EditorData/Editor/Textures/HSV20.png diff --git a/bin/Data/Textures/Editor/IconCamera.png b/bin/EditorData/Editor/Textures/IconCamera.png similarity index 100% rename from bin/Data/Textures/Editor/IconCamera.png rename to bin/EditorData/Editor/Textures/IconCamera.png diff --git a/bin/Data/Textures/Editor/IconCollisionTrigger.png b/bin/EditorData/Editor/Textures/IconCollisionTrigger.png similarity index 100% rename from bin/Data/Textures/Editor/IconCollisionTrigger.png rename to bin/EditorData/Editor/Textures/IconCollisionTrigger.png diff --git a/bin/Data/Textures/Editor/IconCustomGeometry.png b/bin/EditorData/Editor/Textures/IconCustomGeometry.png similarity index 100% rename from bin/Data/Textures/Editor/IconCustomGeometry.png rename to bin/EditorData/Editor/Textures/IconCustomGeometry.png diff --git a/bin/Data/Textures/Editor/IconLight.png b/bin/EditorData/Editor/Textures/IconLight.png similarity index 100% rename from bin/Data/Textures/Editor/IconLight.png rename to bin/EditorData/Editor/Textures/IconLight.png diff --git a/bin/Data/Textures/Editor/IconParticleEmitter.png b/bin/EditorData/Editor/Textures/IconParticleEmitter.png similarity index 100% rename from bin/Data/Textures/Editor/IconParticleEmitter.png rename to bin/EditorData/Editor/Textures/IconParticleEmitter.png diff --git a/bin/Data/Textures/Editor/IconPointLight.png b/bin/EditorData/Editor/Textures/IconPointLight.png similarity index 100% rename from bin/Data/Textures/Editor/IconPointLight.png rename to bin/EditorData/Editor/Textures/IconPointLight.png diff --git a/bin/Data/Textures/Editor/IconSoundListener.png b/bin/EditorData/Editor/Textures/IconSoundListener.png similarity index 100% rename from bin/Data/Textures/Editor/IconSoundListener.png rename to bin/EditorData/Editor/Textures/IconSoundListener.png diff --git a/bin/Data/Textures/Editor/IconSoundSource.png b/bin/EditorData/Editor/Textures/IconSoundSource.png similarity index 100% rename from bin/Data/Textures/Editor/IconSoundSource.png rename to bin/EditorData/Editor/Textures/IconSoundSource.png diff --git a/bin/Data/Textures/Editor/IconSplinePathPoint.png b/bin/EditorData/Editor/Textures/IconSplinePathPoint.png similarity index 100% rename from bin/Data/Textures/Editor/IconSplinePathPoint.png rename to bin/EditorData/Editor/Textures/IconSplinePathPoint.png diff --git a/bin/Data/Textures/Editor/IconSpotLight.png b/bin/EditorData/Editor/Textures/IconSpotLight.png similarity index 100% rename from bin/Data/Textures/Editor/IconSpotLight.png rename to bin/EditorData/Editor/Textures/IconSpotLight.png diff --git a/bin/Data/Textures/Editor/IconZone.png b/bin/EditorData/Editor/Textures/IconZone.png similarity index 100% rename from bin/Data/Textures/Editor/IconZone.png rename to bin/EditorData/Editor/Textures/IconZone.png diff --git a/bin/EditorData/Editor/Textures/LogoLarge.png b/bin/EditorData/Editor/Textures/LogoLarge.png new file mode 100644 index 0000000000000000000000000000000000000000..d61b5d8c8d888c125b05ee64f5a58f32c916e084 GIT binary patch literal 27159 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 z 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 + + + diff --git a/bin/Data/Textures/Editor/NoPreviewAvailable.png b/bin/EditorData/Editor/Textures/NoPreviewAvailable.png similarity index 100% rename from bin/Data/Textures/Editor/NoPreviewAvailable.png rename to bin/EditorData/Editor/Textures/NoPreviewAvailable.png diff --git a/bin/Data/Textures/Editor/SelectionCircle.png b/bin/EditorData/Editor/Textures/SelectionCircle.png similarity index 100% rename from bin/Data/Textures/Editor/SelectionCircle.png rename to bin/EditorData/Editor/Textures/SelectionCircle.png diff --git a/bin/Data/Textures/Editor/TerrainBrushes/large_dots.png b/bin/EditorData/Editor/Textures/TerrainBrushes/large_dots.png similarity index 100% rename from bin/Data/Textures/Editor/TerrainBrushes/large_dots.png rename to bin/EditorData/Editor/Textures/TerrainBrushes/large_dots.png diff --git a/bin/Data/Textures/Editor/TerrainBrushes/large_hard.png b/bin/EditorData/Editor/Textures/TerrainBrushes/large_hard.png similarity index 100% rename from bin/Data/Textures/Editor/TerrainBrushes/large_hard.png rename to bin/EditorData/Editor/Textures/TerrainBrushes/large_hard.png diff --git a/bin/Data/Textures/Editor/TerrainBrushes/large_med.png b/bin/EditorData/Editor/Textures/TerrainBrushes/large_med.png similarity index 100% rename from bin/Data/Textures/Editor/TerrainBrushes/large_med.png rename to bin/EditorData/Editor/Textures/TerrainBrushes/large_med.png diff --git a/bin/Data/Textures/Editor/TerrainBrushes/large_soft.png b/bin/EditorData/Editor/Textures/TerrainBrushes/large_soft.png similarity index 100% rename from bin/Data/Textures/Editor/TerrainBrushes/large_soft.png rename to bin/EditorData/Editor/Textures/TerrainBrushes/large_soft.png diff --git a/bin/Data/Textures/Editor/TerrainBrushes/tiny_hard.png b/bin/EditorData/Editor/Textures/TerrainBrushes/tiny_hard.png similarity index 100% rename from bin/Data/Textures/Editor/TerrainBrushes/tiny_hard.png rename to bin/EditorData/Editor/Textures/TerrainBrushes/tiny_hard.png diff --git a/bin/Data/Textures/Editor/TerrainBrushes/tiny_med.png b/bin/EditorData/Editor/Textures/TerrainBrushes/tiny_med.png similarity index 100% rename from bin/Data/Textures/Editor/TerrainBrushes/tiny_med.png rename to bin/EditorData/Editor/Textures/TerrainBrushes/tiny_med.png diff --git a/bin/Data/Textures/Editor/TerrainBrushes/tiny_soft.png b/bin/EditorData/Editor/Textures/TerrainBrushes/tiny_soft.png similarity index 100% rename from bin/Data/Textures/Editor/TerrainBrushes/tiny_soft.png rename to bin/EditorData/Editor/Textures/TerrainBrushes/tiny_soft.png diff --git a/bin/EditorData/Editor/Textures/UI.png b/bin/EditorData/Editor/Textures/UI.png new file mode 100644 index 0000000000000000000000000000000000000000..b197e77784f651debe27767d62d453fdf1f298a9 GIT binary patch literal 19772 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 O7+`qqCaOXg9r+(Xgx|pc literal 0 HcmV?d00001 diff --git a/bin/EditorData/Editor/Textures/UI.xml b/bin/EditorData/Editor/Textures/UI.xml new file mode 100644 index 00000000000..b59d2a0a6d5 --- /dev/null +++ b/bin/EditorData/Editor/Textures/UI.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/bin/EditorData/Editor/Textures/UrhoIcon.png b/bin/EditorData/Editor/Textures/UrhoIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..f133f7fe1c2cf8b0520c0429a238b098ca3b8629 GIT binary patch literal 4686 zcmV-U60z-xP)PXH~M+bp91m~F4;AE#-*iJduh+~%Hp%nUfk8$(a94}KgOm_TlBy#`%EyT zdx1ABK)e-nOK(4+Gmy^PqsTceidhsTPJfcP?NKPvYljQPeIxEUr_At|zw1JA%&gnkEquM#UsDCjW#(|FF+R?|dg8x#((&ewppNws8GT z*RpljAe^f4=am>5>`nF_=tzXMPsEA%0{^`7i9ekHi}>Wm&d&s){Mz;%1#i=~qN+z0 zEfcg8qg9NKV_F|}S<*9!Ph9pXPCE891d3XHj89*8HJi8WB0MIh{Y=O!NnR)%tJr*? z!*1Nx7g){1No=p;o4?%kmI}D>Kw-+E5ohJ0VSmw+YkJ(_Q5Pe13hktpXIRs-#%I~d z3;4vP*V572jX(m5fB4RgJhbXT1{_VtWeIslkr$F2iU3XB8fwWfa22DS5beaMAy|6t4c@b+~R0VHFa^p+kqb?#rL1 z9@GgZL;Hz>yiDPv3DAwXjwW^jMAvpVs?|&-4!e&& zc>g?q%X7V0Y0yqYk`3cT2*=X9w!-NPXK}?ve~stlk=8-rV6Eb-H{Za)p+Rb~pzSPk z7<|0}Jpn$jh5$_M5WUXAk=%joBJRN99RsZ|>`bbC9twPD>5 ztzr@L3sOnSP=qP{LZcF|`k=i9IrumXwX)?fSwPpn;y5CYq2X=zL0x81AI2OZ*y z#yh`8=#Imuvql?<(jHFlCMQmEC+NDk!@_$2`?@@^`=#7(wr+1JZP{8TaYACZPVCl6 z+<@2(uwIORP%&3bV%ZtX>F=J4Kp~I_6dN`^$1OkkSB$k(LzuD*3QAF}TErl9Op>(5 zmg>D8b)%_UeHgrx`00o}%AMOC<>+(d7d~+P>k6UsZk_xY?RpEJ-PkG;CrY8cdOF-X z+6};qnfh`Y3+K#X+3A0lRvRn9I9Mb3`gd<2j$-Pur1yQ2N{gb7X7FWLFncZ^diP&Z zABz!#qyn-~(ne+A*+_gG?a|)0$3xm1xbBiAuPNZHANA+_?!rH<-&~Rr8N6F3Zp>bQ z@nc$EDzl)yi}#&%DZW!6tvCvS;-S?K@cc^~h_#^ObV1v6NuA8fgO=Gd7Emd)5!N)x zn5G`3@J6c)JU77uIh@$X*NV=QUd@U}T8rPenh3m?fXx801!A}fl{MUp@oYG`U0;xWtg?zxnTt;Dgx#D;nm z&<<{rn_&GMZo>mfDx!oY=hmmW$C)c%S-@F1g*m0mw+|bUIN=2Aj}PNV^sMV*?&Kcc z`;JQxLV-=;tra}`(lb22X#?6osb3PfDSRDG&1#aGWqQwSa(*7IEf|Bd)x!x>NH@-< zz)3?y`nog@i4#yP4>~^4Kl1&n-J=9d?V0+y5Z4_{#*;Pi#zjb1(-xJPQSITJlb12I zb2VHEoa zD@ojt#0{~|D6T(NYB{*)q6T-fjJmz~D-Y$j2#eDp2y>dqS^^9qhVTWSzV=qS+PZO-hmeXmig9;?9`kX$ z9FF#~u<=ld4_MM@2+>YR;zUUAfFlR_5^(!O541W{E3=2ph=|>Q#0}7Hh$}5c;ZTu2 zlMb{qoo+5Z_rsJ5Es(KWYq1s{UGp%9Muss4+-ZV9WwCEHA)xl>STJ)T0%)tWbJ_c^ zL~Dy1hp5NFjl5>aIB^auOlJ9j#0iMA=V9&uev3S&|C{e`Z3<9b`Lk`Ys7Bo(tP?lE z3qn!Ep-mR(cyow(m^hY7!#T?tEmf1EEFxH6;6HI znIO_|yY{LtQYp2NBnB~%pn^1E-8hF)NwbI~ZjcIb0%9i(Y2=`Yf}!)90^BK;ch~E& z^@N}(R0f{Hm!(H}!z88_ySU`MKS!xFf6}gNvB0_wYk6?>0~joFk{~FUH1@L&>#c94 zJJ-V5i{FJ10wE>7m!rL{3vCQG5jauWxDqFt0G?WYqLA1Lh+7Aw($!1GbD)@?AK93q zP&f!&1g?;HNGd^z^7c;Nv-|?4b@!)7Gknqq!Op$g`PHiXL0Tv{gvW+NBDK7*F0EVI zX;XRLf7S{R0%5^s3I>OV(8i#&A$CHnn~a0Ihl3|hgexE?6t0XZw29890F6@=ND4xw z;7D8{aRP}sselZ;aXs|-Z%DL82!ZJ_Bq|}W29I3YPCIibiQ@q{Ip*IpwBq^WC1hi)486LLTdIB27p z)iJ&vPq%s*2W7xQT(}b@TX(DD$$mjBCW6&CG zRpN$uoH&mg=hH4434AY~CVbX68o8sj!B|UT42g`IOP92P2B4A@d;)wrJGf44fnpjH zgQL~3o@-j)M-pQR9F5ZocRYMIK@_C;f=qx!AXvO`F(A;!zz%TgK5mr5jdFN#9wLe7 z_^I`+ZN~rY_x_B>);*547Mb^mTQm5~@=BU<;3GUxln$$jGIwGHKj_HtpKi z|H~IaNQ88db6k>q*o5~4hXWtsfubv+7HgoE3W$>}PIWZdH(FrKYQ{)M%_fZ10%rF1 zW36s38ydA-N0h7VIwx`L%mqj(5i%8U{QTpo zlv=O|#3q4TlZS&JB_bULOU-J^EeZRd4A;Z>vC0sm^^bDg^cK-MDr3K%o(^CL`$ELt z9NS;qMQ>*h^ZVzf2|U4~jWM;mm&=!5%8Z^F6x;%{dS~+CWh*#w!SP5TvMvt3=hISd z;l5Q5qC-&6<|$MZrEVyo$m5U~iae@0hgj-XME{hGuW^fr(7`A{kz?hOagJh7-_m_9YITkZ%)wmxFM2JD`=zjQe; zdZw8-t0nFl62^^D+1^wo9!e+%){pYurRPv7mNOB&f(VHa5~ao~l@KE9d!kuR4VFdo zkHb?w4?n&NZNRU4Og=_5EpHLoiA5>^FX5#50e62&Zw!X{JJO?s=Cu-S23fs%kacs7 z8y9fnd|DEt9JW(28rJyok8UQ6YG@r|OoB1G*_e(L(SR3O_+;kKMp{xYUA_X0fCS<} zD2lX^CRl1VY3?a%Rx@{cXkWN1sSa(kcLI815;wx=R=Z(Wl|4x!w)R>#%B3RWJSy@L zGZgnc`UqeC;kSu(K&(Twi8CQ)Tu3Hn;?-EaT5B=ZG>btT#Tkagb0r>v8sk-PEL)c6 z%bLDcYk&1O3<5uHeD0BFqkjNCV0Ro+wtHsTuUO&XG>^Hz=x4Y#n@F25*O9N3d?AHMm$q(*=m%`Vw&(ZppLgHE#+Ns1 zjxideO*4$*B<7OO|0N@1HF~BArd^T{xG8Ysu(xgvGuraDbxbAqeQbE2iMa*1dm`+s z9k2fmIM*H+Eu*ci=F2&^Jr*_>WSfi-J2gSfiZQz_8=l+74_4mEn)QDGtC==sGD<0| z)tC(4T7xy?E7JeI>lgg={=3OJg4tI@#AS0-=u9h>Y-jB22d+4B z;5r^4)7_XA)PsaQ?apVZwF)DPfJ|0pXG2Zb3#hEeQ7$h~r`i(pI*>8T$ zkMCMZ9EZ%CF&*D`sD(AIy75b_+3*}nNhX~ZLVrYEr0_!c^ou@QTN~BZUbka^JZeUO zj{qBA3G(&aBby9-4>%rI3bA;4o0!*AX3l=>tR2Ev20z;#*0w(WAOPNBw}q z*r3D;hXwu9*|vKxL)9uu3EJn3QT{-cP#O~JQVP0lQRnUMA8tQ*aMeH!*biI@JojpF zulFvc3tSIe0Z3X3t~hymtL$qpcvA-{^$&h_6KwvlRsoQ~Sl6^u9+zEUG*!y~6{@CA2oIqL~&Lyo{hhI_%JU9Igtu3)W>K+&H z0&p+zQ{ccG1$aw-?2%n_fyKZ)U^dVROk(^5+>iq916~H!0Z#!t{+KWQKXq)AxXL@& Q+yDRo07*qoM6N<$f>Tf6UjP6A literal 0 HcmV?d00001 diff --git a/bin/Data/UI/EditorColorWheel.xml b/bin/EditorData/Editor/UI/EditorColorWheel.xml similarity index 99% rename from bin/Data/UI/EditorColorWheel.xml rename to bin/EditorData/Editor/UI/EditorColorWheel.xml index 51e3e19b8bb..66bc7d303be 100644 --- a/bin/Data/UI/EditorColorWheel.xml +++ b/bin/EditorData/Editor/UI/EditorColorWheel.xml @@ -13,7 +13,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/bin/Data/UI/EditorContextMenu.xml b/bin/EditorData/Editor/UI/EditorContextMenu.xml similarity index 100% rename from bin/Data/UI/EditorContextMenu.xml rename to bin/EditorData/Editor/UI/EditorContextMenu.xml diff --git a/bin/Data/UI/EditorDuplicator.xml b/bin/EditorData/Editor/UI/EditorDuplicator.xml similarity index 100% rename from bin/Data/UI/EditorDuplicator.xml rename to bin/EditorData/Editor/UI/EditorDuplicator.xml diff --git a/bin/Data/UI/EditorHierarchyWindow.xml b/bin/EditorData/Editor/UI/EditorHierarchyWindow.xml similarity index 100% rename from bin/Data/UI/EditorHierarchyWindow.xml rename to bin/EditorData/Editor/UI/EditorHierarchyWindow.xml diff --git a/bin/Data/UI/EditorIcons.xml b/bin/EditorData/Editor/UI/EditorIcons.xml similarity index 54% rename from bin/Data/UI/EditorIcons.xml rename to bin/EditorData/Editor/UI/EditorIcons.xml index da26e42eefc..5de02869771 100755 --- a/bin/Data/UI/EditorIcons.xml +++ b/bin/EditorData/Editor/UI/EditorIcons.xml @@ -1,458 +1,458 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bin/Data/UI/EditorInspectorWindow.xml b/bin/EditorData/Editor/UI/EditorInspectorWindow.xml similarity index 95% rename from bin/Data/UI/EditorInspectorWindow.xml rename to bin/EditorData/Editor/UI/EditorInspectorWindow.xml index 4c173a9d51c..cb57ef7b6b2 100644 --- a/bin/Data/UI/EditorInspectorWindow.xml +++ b/bin/EditorData/Editor/UI/EditorInspectorWindow.xml @@ -1,35 +1,35 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bin/Data/UI/EditorInspector_Attribute.xml b/bin/EditorData/Editor/UI/EditorInspector_Attribute.xml similarity index 97% rename from bin/Data/UI/EditorInspector_Attribute.xml rename to bin/EditorData/Editor/UI/EditorInspector_Attribute.xml index 9bbdfdd7477..76cfc85d9fc 100644 --- a/bin/Data/UI/EditorInspector_Attribute.xml +++ b/bin/EditorData/Editor/UI/EditorInspector_Attribute.xml @@ -1,30 +1,30 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bin/Data/UI/EditorInspector_Style.xml b/bin/EditorData/Editor/UI/EditorInspector_Style.xml similarity index 100% rename from bin/Data/UI/EditorInspector_Style.xml rename to bin/EditorData/Editor/UI/EditorInspector_Style.xml diff --git a/bin/Data/UI/EditorInspector_Tags.xml b/bin/EditorData/Editor/UI/EditorInspector_Tags.xml similarity index 100% rename from bin/Data/UI/EditorInspector_Tags.xml rename to bin/EditorData/Editor/UI/EditorInspector_Tags.xml diff --git a/bin/Data/UI/EditorInspector_Variable.xml b/bin/EditorData/Editor/UI/EditorInspector_Variable.xml similarity index 100% rename from bin/Data/UI/EditorInspector_Variable.xml rename to bin/EditorData/Editor/UI/EditorInspector_Variable.xml diff --git a/bin/Data/UI/EditorLayersWindow.xml b/bin/EditorData/Editor/UI/EditorLayersWindow.xml similarity index 100% rename from bin/Data/UI/EditorLayersWindow.xml rename to bin/EditorData/Editor/UI/EditorLayersWindow.xml diff --git a/bin/Data/UI/EditorMaterialWindow.xml b/bin/EditorData/Editor/UI/EditorMaterialWindow.xml similarity index 98% rename from bin/Data/UI/EditorMaterialWindow.xml rename to bin/EditorData/Editor/UI/EditorMaterialWindow.xml index 1a8d366520e..85f090f1e4b 100644 --- a/bin/Data/UI/EditorMaterialWindow.xml +++ b/bin/EditorData/Editor/UI/EditorMaterialWindow.xml @@ -1,462 +1,462 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bin/Data/UI/EditorParticleEffectWindow.xml b/bin/EditorData/Editor/UI/EditorParticleEffectWindow.xml similarity index 100% rename from bin/Data/UI/EditorParticleEffectWindow.xml rename to bin/EditorData/Editor/UI/EditorParticleEffectWindow.xml diff --git a/bin/Data/UI/EditorPreferencesDialog.xml b/bin/EditorData/Editor/UI/EditorPreferencesDialog.xml similarity index 98% rename from bin/Data/UI/EditorPreferencesDialog.xml rename to bin/EditorData/Editor/UI/EditorPreferencesDialog.xml index 9ce6b7dc24f..6bdcc25b621 100644 --- a/bin/Data/UI/EditorPreferencesDialog.xml +++ b/bin/EditorData/Editor/UI/EditorPreferencesDialog.xml @@ -1,405 +1,405 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bin/Data/UI/EditorQuickMenu.xml b/bin/EditorData/Editor/UI/EditorQuickMenu.xml similarity index 97% rename from bin/Data/UI/EditorQuickMenu.xml rename to bin/EditorData/Editor/UI/EditorQuickMenu.xml index e471b53188d..8a9f048b671 100644 --- a/bin/Data/UI/EditorQuickMenu.xml +++ b/bin/EditorData/Editor/UI/EditorQuickMenu.xml @@ -1,52 +1,52 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bin/Data/UI/EditorResourceBrowser.xml b/bin/EditorData/Editor/UI/EditorResourceBrowser.xml similarity index 99% rename from bin/Data/UI/EditorResourceBrowser.xml rename to bin/EditorData/Editor/UI/EditorResourceBrowser.xml index c5e52a130e7..ad67a5e6c30 100644 --- a/bin/Data/UI/EditorResourceBrowser.xml +++ b/bin/EditorData/Editor/UI/EditorResourceBrowser.xml @@ -50,7 +50,7 @@ - + diff --git a/bin/Data/UI/EditorResourceFilterWindow.xml b/bin/EditorData/Editor/UI/EditorResourceFilterWindow.xml similarity index 100% rename from bin/Data/UI/EditorResourceFilterWindow.xml rename to bin/EditorData/Editor/UI/EditorResourceFilterWindow.xml diff --git a/bin/Data/UI/EditorSettingsDialog.xml b/bin/EditorData/Editor/UI/EditorSettingsDialog.xml similarity index 98% rename from bin/Data/UI/EditorSettingsDialog.xml rename to bin/EditorData/Editor/UI/EditorSettingsDialog.xml index 290a0ff7746..aa08bd91d15 100644 --- a/bin/Data/UI/EditorSettingsDialog.xml +++ b/bin/EditorData/Editor/UI/EditorSettingsDialog.xml @@ -1,582 +1,582 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bin/Data/UI/EditorSoundTypeWindow.xml b/bin/EditorData/Editor/UI/EditorSoundTypeWindow.xml similarity index 100% rename from bin/Data/UI/EditorSoundTypeWindow.xml rename to bin/EditorData/Editor/UI/EditorSoundTypeWindow.xml diff --git a/bin/Data/UI/EditorSpawnWindow.xml b/bin/EditorData/Editor/UI/EditorSpawnWindow.xml similarity index 100% rename from bin/Data/UI/EditorSpawnWindow.xml rename to bin/EditorData/Editor/UI/EditorSpawnWindow.xml diff --git a/bin/EditorData/Editor/UI/EditorStyle.xml b/bin/EditorData/Editor/UI/EditorStyle.xml new file mode 100755 index 00000000000..d2b3b91a3f2 --- /dev/null +++ b/bin/EditorData/Editor/UI/EditorStyle.xml @@ -0,0 +1,403 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bin/Data/UI/EditorTerrainWindow.xml b/bin/EditorData/Editor/UI/EditorTerrainWindow.xml similarity index 94% rename from bin/Data/UI/EditorTerrainWindow.xml rename to bin/EditorData/Editor/UI/EditorTerrainWindow.xml index 155289291aa..e41d0de036c 100644 --- a/bin/Data/UI/EditorTerrainWindow.xml +++ b/bin/EditorData/Editor/UI/EditorTerrainWindow.xml @@ -84,7 +84,7 @@ - + @@ -94,7 +94,7 @@ - + @@ -104,7 +104,7 @@ - + @@ -115,7 +115,7 @@