diff --git a/Unreal/Plugins/PS_Editor/Content/UI/BP_PS_Editor_HUD.uasset b/Unreal/Plugins/PS_Editor/Content/UI/BP_PS_Editor_HUD.uasset new file mode 100644 index 0000000..7c6b8ba Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/BP_PS_Editor_HUD.uasset differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/assets/gizmo_rotate.png b/Unreal/Plugins/PS_Editor/Content/UI/assets/gizmo_rotate.png new file mode 100644 index 0000000..b2e838d Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/assets/gizmo_rotate.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/assets/gizmo_scale.png b/Unreal/Plugins/PS_Editor/Content/UI/assets/gizmo_scale.png new file mode 100644 index 0000000..68c7755 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/assets/gizmo_scale.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/assets/gizmo_translate.png b/Unreal/Plugins/PS_Editor/Content/UI/assets/gizmo_translate.png new file mode 100644 index 0000000..f281000 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/assets/gizmo_translate.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/assets/thumb_fx.png b/Unreal/Plugins/PS_Editor/Content/UI/assets/thumb_fx.png new file mode 100644 index 0000000..1f2877c Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/assets/thumb_fx.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/assets/thumb_npc.png b/Unreal/Plugins/PS_Editor/Content/UI/assets/thumb_npc.png new file mode 100644 index 0000000..b981d4d Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/assets/thumb_npc.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/assets/thumb_prop.png b/Unreal/Plugins/PS_Editor/Content/UI/assets/thumb_prop.png new file mode 100644 index 0000000..adca269 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/assets/thumb_prop.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/assets/thumb_spline.png b/Unreal/Plugins/PS_Editor/Content/UI/assets/thumb_spline.png new file mode 100644 index 0000000..353ffcf Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/assets/thumb_spline.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/assets/thumb_weapon.png b/Unreal/Plugins/PS_Editor/Content/UI/assets/thumb_weapon.png new file mode 100644 index 0000000..c4da438 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/assets/thumb_weapon.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/Bell.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/Bell.png new file mode 100644 index 0000000..88b4aea Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/Bell.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/Box.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/Box.png new file mode 100644 index 0000000..bbd013f Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/Box.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/Camera.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/Camera.png new file mode 100644 index 0000000..0fd120f Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/Camera.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/Check.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/Check.png new file mode 100644 index 0000000..e9e0d5d Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/Check.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/ChevronDown.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/ChevronDown.png new file mode 100644 index 0000000..f618a43 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/ChevronDown.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/ChevronLeft.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/ChevronLeft.png new file mode 100644 index 0000000..b9ec48f Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/ChevronLeft.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/ChevronRight.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/ChevronRight.png new file mode 100644 index 0000000..5f2aa6b Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/ChevronRight.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/Command.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/Command.png new file mode 100644 index 0000000..040ff8f Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/Command.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/Diamond.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/Diamond.png new file mode 100644 index 0000000..724db30 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/Diamond.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/Download.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/Download.png new file mode 100644 index 0000000..562377b Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/Download.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/Eye.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/Eye.png new file mode 100644 index 0000000..2f0a709 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/Eye.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/EyeOff.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/EyeOff.png new file mode 100644 index 0000000..0f52510 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/EyeOff.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/Film.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/Film.png new file mode 100644 index 0000000..7536803 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/Film.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/Flag.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/Flag.png new file mode 100644 index 0000000..49569f7 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/Flag.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/Folder.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/Folder.png new file mode 100644 index 0000000..cf00290 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/Folder.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/Globe.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/Globe.png new file mode 100644 index 0000000..3c0339a Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/Globe.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/Grid.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/Grid.png new file mode 100644 index 0000000..e47b2f6 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/Grid.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/Help.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/Help.png new file mode 100644 index 0000000..d41667f Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/Help.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/Layers.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/Layers.png new file mode 100644 index 0000000..827151a Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/Layers.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/Lightbulb.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/Lightbulb.png new file mode 100644 index 0000000..db05be0 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/Lightbulb.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/Lock.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/Lock.png new file mode 100644 index 0000000..9a3138d Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/Lock.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/Map.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/Map.png new file mode 100644 index 0000000..8bc45d5 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/Map.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/Minus.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/Minus.png new file mode 100644 index 0000000..a7389d9 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/Minus.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/More.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/More.png new file mode 100644 index 0000000..b75b60f Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/More.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/Move.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/Move.png new file mode 100644 index 0000000..3feba5a Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/Move.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/PanelBottom.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/PanelBottom.png new file mode 100644 index 0000000..4111f2d Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/PanelBottom.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/PanelRight.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/PanelRight.png new file mode 100644 index 0000000..d2ca65e Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/PanelRight.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/Pause.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/Pause.png new file mode 100644 index 0000000..401f56d Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/Pause.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/Play.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/Play.png new file mode 100644 index 0000000..cc939d3 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/Play.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/Plus.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/Plus.png new file mode 100644 index 0000000..4d7d2e9 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/Plus.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/Record.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/Record.png new file mode 100644 index 0000000..6937300 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/Record.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/Rotate.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/Rotate.png new file mode 100644 index 0000000..2a7e861 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/Rotate.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/Save.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/Save.png new file mode 100644 index 0000000..0d16bf3 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/Save.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/Scale.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/Scale.png new file mode 100644 index 0000000..5821bf9 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/Scale.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/Search.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/Search.png new file mode 100644 index 0000000..7eb12db Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/Search.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/Settings.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/Settings.png new file mode 100644 index 0000000..28e883c Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/Settings.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/Sidebar.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/Sidebar.png new file mode 100644 index 0000000..9964ec0 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/Sidebar.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/SkipBack.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/SkipBack.png new file mode 100644 index 0000000..61b4002 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/SkipBack.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/SkipFwd.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/SkipFwd.png new file mode 100644 index 0000000..1ed97cd Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/SkipFwd.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/Snap.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/Snap.png new file mode 100644 index 0000000..3cd25e0 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/Snap.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/Spline.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/Spline.png new file mode 100644 index 0000000..950b4c1 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/Spline.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/Stop.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/Stop.png new file mode 100644 index 0000000..560deae Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/Stop.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/Target.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/Target.png new file mode 100644 index 0000000..de70c04 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/Target.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/Unlock.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/Unlock.png new file mode 100644 index 0000000..c37a54e Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/Unlock.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/User.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/User.png new file mode 100644 index 0000000..1728473 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/User.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/Zap.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/Zap.png new file mode 100644 index 0000000..985b4e6 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/Zap.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Bell.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Bell.png new file mode 100644 index 0000000..93c3264 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Bell.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Box.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Box.png new file mode 100644 index 0000000..fa82aef Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Box.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Camera.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Camera.png new file mode 100644 index 0000000..f091c0c Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Camera.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Check.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Check.png new file mode 100644 index 0000000..2ee5549 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Check.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/ChevronDown.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/ChevronDown.png new file mode 100644 index 0000000..4398564 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/ChevronDown.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/ChevronLeft.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/ChevronLeft.png new file mode 100644 index 0000000..a5191fc Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/ChevronLeft.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/ChevronRight.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/ChevronRight.png new file mode 100644 index 0000000..3a390a2 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/ChevronRight.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Command.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Command.png new file mode 100644 index 0000000..2a967a6 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Command.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Diamond.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Diamond.png new file mode 100644 index 0000000..0e6d998 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Diamond.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Download.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Download.png new file mode 100644 index 0000000..319d159 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Download.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Eye.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Eye.png new file mode 100644 index 0000000..8e3d79a Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Eye.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/EyeOff.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/EyeOff.png new file mode 100644 index 0000000..2a937f4 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/EyeOff.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Film.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Film.png new file mode 100644 index 0000000..3052be9 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Film.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Flag.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Flag.png new file mode 100644 index 0000000..b922a03 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Flag.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Folder.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Folder.png new file mode 100644 index 0000000..e6648b9 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Folder.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Globe.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Globe.png new file mode 100644 index 0000000..c1c05fa Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Globe.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Grid.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Grid.png new file mode 100644 index 0000000..4940892 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Grid.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Help.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Help.png new file mode 100644 index 0000000..ebb5ab7 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Help.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Layers.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Layers.png new file mode 100644 index 0000000..3e437d3 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Layers.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Lightbulb.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Lightbulb.png new file mode 100644 index 0000000..57d6836 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Lightbulb.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Lock.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Lock.png new file mode 100644 index 0000000..e7f4c60 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Lock.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Map.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Map.png new file mode 100644 index 0000000..a58dea8 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Map.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Minus.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Minus.png new file mode 100644 index 0000000..a65fb41 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Minus.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/More.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/More.png new file mode 100644 index 0000000..a678838 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/More.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Move.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Move.png new file mode 100644 index 0000000..f496abe Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Move.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/PanelBottom.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/PanelBottom.png new file mode 100644 index 0000000..f6149a4 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/PanelBottom.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/PanelRight.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/PanelRight.png new file mode 100644 index 0000000..5df7127 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/PanelRight.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Pause.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Pause.png new file mode 100644 index 0000000..ff592eb Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Pause.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Play.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Play.png new file mode 100644 index 0000000..5b9b9d2 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Play.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Plus.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Plus.png new file mode 100644 index 0000000..e78929a Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Plus.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Record.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Record.png new file mode 100644 index 0000000..ff1f0d1 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Record.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Rotate.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Rotate.png new file mode 100644 index 0000000..3d7908b Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Rotate.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Save.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Save.png new file mode 100644 index 0000000..0cfa5df Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Save.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Scale.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Scale.png new file mode 100644 index 0000000..2ed2f2f Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Scale.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Search.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Search.png new file mode 100644 index 0000000..439b3dc Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Search.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Settings.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Settings.png new file mode 100644 index 0000000..4ae2a4e Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Settings.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Sidebar.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Sidebar.png new file mode 100644 index 0000000..b47303b Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Sidebar.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/SkipBack.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/SkipBack.png new file mode 100644 index 0000000..a2b766b Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/SkipBack.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/SkipFwd.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/SkipFwd.png new file mode 100644 index 0000000..c3b1e22 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/SkipFwd.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Snap.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Snap.png new file mode 100644 index 0000000..6abb817 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Snap.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Spline.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Spline.png new file mode 100644 index 0000000..04598d1 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Spline.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Stop.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Stop.png new file mode 100644 index 0000000..82478f4 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Stop.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Target.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Target.png new file mode 100644 index 0000000..d8ea395 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Target.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Unlock.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Unlock.png new file mode 100644 index 0000000..60220c3 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Unlock.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/User.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/User.png new file mode 100644 index 0000000..67728d4 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/User.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Zap.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Zap.png new file mode 100644 index 0000000..8801906 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_16/Zap.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Bell.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Bell.png new file mode 100644 index 0000000..3774325 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Bell.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Box.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Box.png new file mode 100644 index 0000000..209f820 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Box.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Camera.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Camera.png new file mode 100644 index 0000000..358bad6 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Camera.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Check.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Check.png new file mode 100644 index 0000000..5818a62 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Check.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/ChevronDown.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/ChevronDown.png new file mode 100644 index 0000000..244de6d Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/ChevronDown.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/ChevronLeft.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/ChevronLeft.png new file mode 100644 index 0000000..be89500 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/ChevronLeft.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/ChevronRight.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/ChevronRight.png new file mode 100644 index 0000000..3133fa2 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/ChevronRight.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Command.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Command.png new file mode 100644 index 0000000..846c82d Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Command.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Diamond.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Diamond.png new file mode 100644 index 0000000..7c72304 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Diamond.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Download.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Download.png new file mode 100644 index 0000000..164f9c0 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Download.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Eye.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Eye.png new file mode 100644 index 0000000..d164da1 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Eye.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/EyeOff.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/EyeOff.png new file mode 100644 index 0000000..6cb5c45 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/EyeOff.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Film.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Film.png new file mode 100644 index 0000000..79a9315 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Film.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Flag.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Flag.png new file mode 100644 index 0000000..175f844 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Flag.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Folder.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Folder.png new file mode 100644 index 0000000..35e54aa Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Folder.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Globe.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Globe.png new file mode 100644 index 0000000..3d0a558 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Globe.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Grid.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Grid.png new file mode 100644 index 0000000..6d20828 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Grid.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Help.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Help.png new file mode 100644 index 0000000..5d5e7e7 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Help.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Layers.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Layers.png new file mode 100644 index 0000000..27a2a7f Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Layers.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Lightbulb.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Lightbulb.png new file mode 100644 index 0000000..95360e0 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Lightbulb.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Lock.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Lock.png new file mode 100644 index 0000000..cff0221 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Lock.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Map.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Map.png new file mode 100644 index 0000000..d14d2c1 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Map.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Minus.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Minus.png new file mode 100644 index 0000000..c61f4da Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Minus.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/More.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/More.png new file mode 100644 index 0000000..7f7066f Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/More.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Move.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Move.png new file mode 100644 index 0000000..4338350 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Move.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/PanelBottom.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/PanelBottom.png new file mode 100644 index 0000000..7cce210 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/PanelBottom.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/PanelRight.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/PanelRight.png new file mode 100644 index 0000000..df65912 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/PanelRight.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Pause.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Pause.png new file mode 100644 index 0000000..4cbb527 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Pause.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Play.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Play.png new file mode 100644 index 0000000..4af39d5 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Play.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Plus.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Plus.png new file mode 100644 index 0000000..663cbc1 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Plus.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Record.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Record.png new file mode 100644 index 0000000..7192d8f Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Record.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Rotate.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Rotate.png new file mode 100644 index 0000000..7077fc8 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Rotate.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Save.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Save.png new file mode 100644 index 0000000..b39d097 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Save.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Scale.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Scale.png new file mode 100644 index 0000000..f62553a Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Scale.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Search.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Search.png new file mode 100644 index 0000000..921b93c Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Search.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Settings.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Settings.png new file mode 100644 index 0000000..9aeb0f8 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Settings.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Sidebar.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Sidebar.png new file mode 100644 index 0000000..ec8d026 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Sidebar.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/SkipBack.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/SkipBack.png new file mode 100644 index 0000000..26bf5a7 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/SkipBack.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/SkipFwd.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/SkipFwd.png new file mode 100644 index 0000000..dee98b0 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/SkipFwd.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Snap.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Snap.png new file mode 100644 index 0000000..8c9542a Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Snap.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Spline.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Spline.png new file mode 100644 index 0000000..bb88d7c Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Spline.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Stop.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Stop.png new file mode 100644 index 0000000..d93801c Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Stop.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Target.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Target.png new file mode 100644 index 0000000..4451ca5 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Target.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Unlock.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Unlock.png new file mode 100644 index 0000000..50797c0 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Unlock.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/User.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/User.png new file mode 100644 index 0000000..86d9404 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/User.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Zap.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Zap.png new file mode 100644 index 0000000..5515bcb Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_20/Zap.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Bell.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Bell.png new file mode 100644 index 0000000..e2942d6 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Bell.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Box.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Box.png new file mode 100644 index 0000000..b82eaa4 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Box.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Camera.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Camera.png new file mode 100644 index 0000000..83e04fb Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Camera.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Check.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Check.png new file mode 100644 index 0000000..c09747f Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Check.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/ChevronDown.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/ChevronDown.png new file mode 100644 index 0000000..d4e337d Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/ChevronDown.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/ChevronLeft.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/ChevronLeft.png new file mode 100644 index 0000000..a5869f5 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/ChevronLeft.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/ChevronRight.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/ChevronRight.png new file mode 100644 index 0000000..24e2f62 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/ChevronRight.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Command.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Command.png new file mode 100644 index 0000000..0ffe21d Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Command.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Diamond.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Diamond.png new file mode 100644 index 0000000..c64372f Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Diamond.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Download.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Download.png new file mode 100644 index 0000000..90a77ad Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Download.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Eye.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Eye.png new file mode 100644 index 0000000..c85d78f Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Eye.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/EyeOff.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/EyeOff.png new file mode 100644 index 0000000..ceab853 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/EyeOff.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Film.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Film.png new file mode 100644 index 0000000..2db366d Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Film.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Flag.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Flag.png new file mode 100644 index 0000000..905425f Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Flag.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Folder.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Folder.png new file mode 100644 index 0000000..08c0049 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Folder.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Globe.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Globe.png new file mode 100644 index 0000000..ce49689 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Globe.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Grid.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Grid.png new file mode 100644 index 0000000..6ba0ea7 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Grid.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Help.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Help.png new file mode 100644 index 0000000..4eedc6c Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Help.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Layers.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Layers.png new file mode 100644 index 0000000..610c3a5 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Layers.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Lightbulb.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Lightbulb.png new file mode 100644 index 0000000..fe48bd1 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Lightbulb.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Lock.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Lock.png new file mode 100644 index 0000000..3ca3ded Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Lock.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Map.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Map.png new file mode 100644 index 0000000..92e1e5b Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Map.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Minus.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Minus.png new file mode 100644 index 0000000..3f35b31 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Minus.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/More.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/More.png new file mode 100644 index 0000000..5e8f3bb Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/More.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Move.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Move.png new file mode 100644 index 0000000..505b409 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Move.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/PanelBottom.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/PanelBottom.png new file mode 100644 index 0000000..14eed70 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/PanelBottom.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/PanelRight.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/PanelRight.png new file mode 100644 index 0000000..be4c9c8 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/PanelRight.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Pause.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Pause.png new file mode 100644 index 0000000..3eebd4a Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Pause.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Play.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Play.png new file mode 100644 index 0000000..9e33832 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Play.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Plus.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Plus.png new file mode 100644 index 0000000..cd936ed Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Plus.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Record.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Record.png new file mode 100644 index 0000000..1e1ad45 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Record.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Rotate.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Rotate.png new file mode 100644 index 0000000..338804e Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Rotate.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Save.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Save.png new file mode 100644 index 0000000..7b1cc2b Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Save.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Search.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Search.png new file mode 100644 index 0000000..b59155c Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Search.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Settings.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Settings.png new file mode 100644 index 0000000..5b047cb Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Settings.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Sidebar.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Sidebar.png new file mode 100644 index 0000000..9145e2d Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Sidebar.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/SkipBack.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/SkipBack.png new file mode 100644 index 0000000..80b9951 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/SkipBack.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/SkipFwd.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/SkipFwd.png new file mode 100644 index 0000000..c185526 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/SkipFwd.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Snap.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Snap.png new file mode 100644 index 0000000..18e20e2 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Snap.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Spline.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Spline.png new file mode 100644 index 0000000..528a96d Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Spline.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Stop.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Stop.png new file mode 100644 index 0000000..b7b0cab Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Stop.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Target.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Target.png new file mode 100644 index 0000000..3c9e6df Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Target.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Unlock.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Unlock.png new file mode 100644 index 0000000..5616c8d Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Unlock.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/User.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/User.png new file mode 100644 index 0000000..7b85740 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/User.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Zap.png b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Zap.png new file mode 100644 index 0000000..2e83d96 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/icons/icons_24/Zap.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice/README.md b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice/README.md new file mode 100644 index 0000000..3a5e40c --- /dev/null +++ b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice/README.md @@ -0,0 +1,38 @@ +# Unreal 9-slice pack + +Atlas layout (each file): **192 × 192 px**, 3×3 equal tiles of **64 × 64 px**. + +## Import into Unreal (UMG) + +1. Import the PNG as a Texture2D. +2. Open the Texture and set **Compression Settings = UserInterface2D (RGBA)**, **Mip Gen Settings = NoMipmaps**, **sRGB = true**. +3. Create (or open) a `Slate Brush` / Widget image and set: + - **Image** = the texture + - **Draw As** = `Box` + - **Margin** = **L 0.3333 · R 0.3333 · T 0.3333 · B 0.3333** (exactly 1/3) + +The box brush will now stretch the center and tile/stretch edges while keeping corners intact — resize freely. + +## Files + +| File | Radius | Use | +| --------------------------------- | ------ | ------------------------------------- | +| `panel_rounded_big.png` | 24 px | Panel background — rounded | +| `panel_rounded_small.png` | 12 px | Panel background — half-rounded | +| `button_big_normal.png` | 24 px | Button — normal state | +| `button_big_hover.png` | 24 px | Button — hover state | +| `button_big_pressed.png` | 24 px | Button — pressed state | +| `button_small_normal.png` | 12 px | Half-rounded button — normal | +| `button_small_hover.png` | 12 px | Half-rounded button — hover | +| `button_small_pressed.png` | 12 px | Half-rounded button — pressed | + +`*_guide.png` files are the same assets with dashed cut lines overlaid — **for reference only**, do not ship. + +## Button states in UMG + +Create a `Button` widget and set its Style: +- **Normal** → `button_*_normal.png` (same margin 0.3333) +- **Hovered** → `button_*_hover.png` +- **Pressed** → `button_*_pressed.png` + +Keep the same margin on all three so the geometry stays consistent. diff --git a/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice/button_big_hover.png b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice/button_big_hover.png new file mode 100644 index 0000000..5ee1be6 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice/button_big_hover.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice/button_big_hover_guide.png b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice/button_big_hover_guide.png new file mode 100644 index 0000000..b265940 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice/button_big_hover_guide.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice/button_big_normal.png b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice/button_big_normal.png new file mode 100644 index 0000000..2fb5d31 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice/button_big_normal.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice/button_big_normal_guide.png b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice/button_big_normal_guide.png new file mode 100644 index 0000000..b265940 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice/button_big_normal_guide.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice/button_big_pressed.png b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice/button_big_pressed.png new file mode 100644 index 0000000..7251a74 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice/button_big_pressed.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice/button_big_pressed_guide.png b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice/button_big_pressed_guide.png new file mode 100644 index 0000000..b265940 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice/button_big_pressed_guide.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice/button_small_hover.png b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice/button_small_hover.png new file mode 100644 index 0000000..94f26bd Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice/button_small_hover.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice/button_small_hover_guide.png b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice/button_small_hover_guide.png new file mode 100644 index 0000000..b265940 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice/button_small_hover_guide.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice/button_small_normal.png b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice/button_small_normal.png new file mode 100644 index 0000000..51c08fe Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice/button_small_normal.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice/button_small_normal_guide.png b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice/button_small_normal_guide.png new file mode 100644 index 0000000..b265940 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice/button_small_normal_guide.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice/button_small_pressed.png b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice/button_small_pressed.png new file mode 100644 index 0000000..896d39e Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice/button_small_pressed.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice/button_small_pressed_guide.png b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice/button_small_pressed_guide.png new file mode 100644 index 0000000..b265940 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice/button_small_pressed_guide.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice/panel_rounded_big.png b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice/panel_rounded_big.png new file mode 100644 index 0000000..c5be5f3 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice/panel_rounded_big.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice/panel_rounded_big_guide.png b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice/panel_rounded_big_guide.png new file mode 100644 index 0000000..b265940 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice/panel_rounded_big_guide.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice/panel_rounded_small.png b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice/panel_rounded_small.png new file mode 100644 index 0000000..c0ddd98 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice/panel_rounded_small.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice/panel_rounded_small_guide.png b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice/panel_rounded_small_guide.png new file mode 100644 index 0000000..b265940 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice/panel_rounded_small_guide.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice_blue/README.md b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice_blue/README.md new file mode 100644 index 0000000..0697d4d --- /dev/null +++ b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice_blue/README.md @@ -0,0 +1,29 @@ +# Unreal 9-slice pack — BLUE variant + +Same geometry as the default pack: +- Atlas **192 × 192 px**, 3×3 tiles of **64 × 64 px** +- UE `Draw As = Box`, **Margin = 0.3333** on all sides +- Big radius **24 px**, Small radius **12 px** + +Accent blue = **#4aa8ff** (Z-axis / prototype accent). + +## Files + +| File | Radius | Use | +| --------------------------------- | ------ | ------------------------------------ | +| `panel_rounded_big.png` | 24 px | Panel — rounded (blue-tinted stroke) | +| `panel_rounded_small.png` | 12 px | Panel — half-rounded | +| `button_big_normal.png` | 24 px | Button — normal | +| `button_big_hover.png` | 24 px | Button — hover (brighter blue edge) | +| `button_big_pressed.png` | 24 px | Button — pressed (deep, inverted) | +| `button_small_normal.png` | 12 px | Half-rounded button — normal | +| `button_small_hover.png` | 12 px | Half-rounded button — hover | +| `button_small_pressed.png` | 12 px | Half-rounded button — pressed | + +`*_guide.png` files include the dashed cut lines for reference — do not ship. + +## Import settings (UMG) + +1. Texture2D → **Compression = UserInterface2D (RGBA)**, **sRGB = true**, **NoMipmaps**. +2. Slate Brush → **Draw As = Box**, **Margin = 0.3333** on all four sides. +3. On a `Button` widget, map the three button PNGs to Normal / Hovered / Pressed. diff --git a/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice_blue/button_big_hover.png b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice_blue/button_big_hover.png new file mode 100644 index 0000000..15223e9 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice_blue/button_big_hover.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice_blue/button_big_hover_guide.png b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice_blue/button_big_hover_guide.png new file mode 100644 index 0000000..b265940 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice_blue/button_big_hover_guide.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice_blue/button_big_normal.png b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice_blue/button_big_normal.png new file mode 100644 index 0000000..4943ae2 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice_blue/button_big_normal.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice_blue/button_big_normal_guide.png b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice_blue/button_big_normal_guide.png new file mode 100644 index 0000000..b265940 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice_blue/button_big_normal_guide.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice_blue/button_big_pressed.png b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice_blue/button_big_pressed.png new file mode 100644 index 0000000..6f15042 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice_blue/button_big_pressed.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice_blue/button_big_pressed_guide.png b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice_blue/button_big_pressed_guide.png new file mode 100644 index 0000000..b265940 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice_blue/button_big_pressed_guide.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice_blue/button_small_hover.png b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice_blue/button_small_hover.png new file mode 100644 index 0000000..1664f6d Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice_blue/button_small_hover.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice_blue/button_small_hover_guide.png b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice_blue/button_small_hover_guide.png new file mode 100644 index 0000000..b265940 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice_blue/button_small_hover_guide.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice_blue/button_small_normal.png b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice_blue/button_small_normal.png new file mode 100644 index 0000000..62c2c57 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice_blue/button_small_normal.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice_blue/button_small_normal_guide.png b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice_blue/button_small_normal_guide.png new file mode 100644 index 0000000..b265940 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice_blue/button_small_normal_guide.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice_blue/button_small_pressed.png b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice_blue/button_small_pressed.png new file mode 100644 index 0000000..ca2edf1 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice_blue/button_small_pressed.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice_blue/button_small_pressed_guide.png b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice_blue/button_small_pressed_guide.png new file mode 100644 index 0000000..b265940 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice_blue/button_small_pressed_guide.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice_blue/panel_rounded_big.png b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice_blue/panel_rounded_big.png new file mode 100644 index 0000000..376106a Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice_blue/panel_rounded_big.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice_blue/panel_rounded_big_guide.png b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice_blue/panel_rounded_big_guide.png new file mode 100644 index 0000000..b265940 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice_blue/panel_rounded_big_guide.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice_blue/panel_rounded_small.png b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice_blue/panel_rounded_small.png new file mode 100644 index 0000000..957876e Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice_blue/panel_rounded_small.png differ diff --git a/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice_blue/panel_rounded_small_guide.png b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice_blue/panel_rounded_small_guide.png new file mode 100644 index 0000000..b265940 Binary files /dev/null and b/Unreal/Plugins/PS_Editor/Content/UI/unreal_9slice_blue/panel_rounded_small_guide.png differ diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.cpp index e495d52..1701221 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.cpp @@ -18,6 +18,7 @@ #include "PS_Editor_ChildMovable.h" #include "PS_Editor_HUD.h" #include "PS_Editor_MainWidget.h" +#include "PS_Editor_MainWidget_Legacy.h" #include "Components/SplineComponent.h" #include "DrawDebugHelpers.h" #include "PS_Editor_ConfirmDialog.h" @@ -1031,13 +1032,20 @@ void APS_Editor_Pawn::HandleDelete(const FInputActionValue& Value) // 1st priority: if a timeline key is selected in the HUD, delete it and stop. if (APS_Editor_HUD* EditorHUD = Cast(EditorPC->GetHUD())) { - if (UPS_Editor_MainWidget* MW = EditorHUD->GetMainWidget()) + if (UPS_Editor_MainWidget* MW = EditorHUD->GetMainWidget_NewSlate()) { if (MW->TryDeleteSelectedTimelineKey()) { return; } } + else if (UPS_Editor_MainWidget_Legacy* MWL = EditorHUD->GetMainWidget_Legacy()) + { + if (MWL->TryDeleteSelectedTimelineKey()) + { + return; + } + } } // In SplineEditing mode with a point selected: delete that point diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.cpp index 2a2fc0b..0ae5fb8 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.cpp @@ -16,6 +16,7 @@ #include "PS_Editor_SceneLoader.h" #include "PS_Editor_HUD.h" #include "PS_Editor_MainWidget.h" +#include "PS_Editor_MainWidget_Legacy.h" #include "Camera/PlayerCameraManager.h" #include "PS_Editor_GameInstanceSubsystem.h" #include "GameFramework/GameModeBase.h" @@ -429,10 +430,14 @@ void APS_Editor_PlayerController::ShowLoadingScreen(bool bShow) { if (APS_Editor_HUD* EditorHUD = Cast(GetHUD())) { - if (UPS_Editor_MainWidget* Widget = EditorHUD->GetMainWidget()) + if (UPS_Editor_MainWidget* Widget = EditorHUD->GetMainWidget_NewSlate()) { Widget->ShowLoadingScreen(bShow); } + else if (UPS_Editor_MainWidget_Legacy* WidgetLegacy = EditorHUD->GetMainWidget_Legacy()) + { + WidgetLegacy->ShowLoadingScreen(bShow); + } } } diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/PS_Editor.Build.cs b/Unreal/Plugins/PS_Editor/Source/PS_Editor/PS_Editor.Build.cs index acb2b44..6cf9c9e 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/PS_Editor.Build.cs +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/PS_Editor.Build.cs @@ -13,6 +13,7 @@ public class PS_Editor : ModuleRules PublicIncludePaths.Add(Path.Combine(SourceDir, "GameMode")); PublicIncludePaths.Add(Path.Combine(SourceDir, "UI")); PublicIncludePaths.Add(Path.Combine(SourceDir, "UI", "Widgets")); + PublicIncludePaths.Add(Path.Combine(SourceDir, "UI", "Style")); PublicIncludePaths.Add(Path.Combine(SourceDir, "Selection")); PublicIncludePaths.Add(Path.Combine(SourceDir, "Gizmo")); PublicIncludePaths.Add(Path.Combine(SourceDir, "Spawn")); @@ -38,7 +39,9 @@ public class PS_Editor : ModuleRules PrivateDependencyModuleNames.AddRange(new string[] { "Slate", - "SlateCore" + "SlateCore", + "ImageWrapper", + "Projects" }); // Editor-only: CaptureAllThumbnails uses ThumbnailTools, material auto-generation uses AssetRegistry diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/PS_Editor_HUD.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/PS_Editor_HUD.cpp index 78eb76e..0a0efa0 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/PS_Editor_HUD.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/PS_Editor_HUD.cpp @@ -1,5 +1,7 @@ #include "PS_Editor_HUD.h" #include "PS_Editor_MainWidget.h" +#include "PS_Editor_MainWidget_Legacy.h" +#include "PS_Editor_MainWidget_UMG.h" #include "PS_Editor_PlayerController.h" #include "PS_Editor_SelectionManager.h" #include "PS_Editor_SpawnManager.h" @@ -15,16 +17,84 @@ void APS_Editor_HUD::BeginPlay() APlayerController* PC = GetOwningPlayerController(); if (!PC) return; - MainWidget = CreateWidget(PC, UPS_Editor_MainWidget::StaticClass()); - if (!MainWidget) return; + APS_Editor_PlayerController* EditorPC = Cast(PC); - if (APS_Editor_PlayerController* EditorPC = Cast(PC)) + switch (UIMode) { - MainWidget->SetSelectionManager(EditorPC->GetSelectionManager()); - MainWidget->SetSpawnManager(EditorPC->GetSpawnManager()); - MainWidget->SetSceneSerializer(EditorPC->GetSceneSerializer()); + case EPS_Editor_UIMode::Legacy: + { + UPS_Editor_MainWidget_Legacy* W = CreateWidget(PC, UPS_Editor_MainWidget_Legacy::StaticClass()); + if (W && EditorPC) + { + W->SetSelectionManager(EditorPC->GetSelectionManager()); + W->SetSpawnManager(EditorPC->GetSpawnManager()); + W->SetSceneSerializer(EditorPC->GetSceneSerializer()); + } + MainWidget = W; + break; } - MainWidget->AddToViewport(0); + case EPS_Editor_UIMode::NewSlate: + { + UPS_Editor_MainWidget* W = CreateWidget(PC, UPS_Editor_MainWidget::StaticClass()); + if (W && EditorPC) + { + W->SetSelectionManager(EditorPC->GetSelectionManager()); + W->SetSpawnManager(EditorPC->GetSpawnManager()); + W->SetSceneSerializer(EditorPC->GetSceneSerializer()); + } + MainWidget = W; + break; + } + + case EPS_Editor_UIMode::UMG: + { + UClass* WidgetClass = UMGWidgetClass.LoadSynchronous(); + if (!WidgetClass) + { + UE_LOG(LogTemp, Error, TEXT("PS_Editor_HUD: UIMode=UMG but UMGWidgetClass is not set. Falling back to NewSlate.")); + UPS_Editor_MainWidget* W = CreateWidget(PC, UPS_Editor_MainWidget::StaticClass()); + if (W && EditorPC) + { + W->SetSelectionManager(EditorPC->GetSelectionManager()); + W->SetSpawnManager(EditorPC->GetSpawnManager()); + W->SetSceneSerializer(EditorPC->GetSceneSerializer()); + } + MainWidget = W; + } + else + { + UPS_Editor_MainWidget_UMG* W = CreateWidget(PC, WidgetClass); + if (W && EditorPC) + { + W->SetSelectionManager(EditorPC->GetSelectionManager()); + W->SetSpawnManager(EditorPC->GetSpawnManager()); + W->SetSceneSerializer(EditorPC->GetSceneSerializer()); + } + MainWidget = W; + } + break; + } + } + + if (MainWidget) + { + MainWidget->AddToViewport(0); + } }); } + +UPS_Editor_MainWidget* APS_Editor_HUD::GetMainWidget_NewSlate() const +{ + return Cast(MainWidget); +} + +UPS_Editor_MainWidget_Legacy* APS_Editor_HUD::GetMainWidget_Legacy() const +{ + return Cast(MainWidget); +} + +UPS_Editor_MainWidget_UMG* APS_Editor_HUD::GetMainWidget_UMG() const +{ + return Cast(MainWidget); +} diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/PS_Editor_HUD.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/PS_Editor_HUD.h index 13a83f0..c55716b 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/PS_Editor_HUD.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/PS_Editor_HUD.h @@ -4,11 +4,29 @@ #include "GameFramework/HUD.h" #include "PS_Editor_HUD.generated.h" +class UUserWidget; class UPS_Editor_MainWidget; +class UPS_Editor_MainWidget_Legacy; +class UPS_Editor_MainWidget_UMG; + +/** UI implementation flavor. */ +UENUM(BlueprintType) +enum class EPS_Editor_UIMode : uint8 +{ + /** Original Slate-based widget (pre-redesign). Stable fallback. */ + Legacy UMETA(DisplayName = "Legacy (Slate)"), + + /** Refactored floating-HUD Slate widget (CodeDesign redesign, work-in-progress). */ + NewSlate UMETA(DisplayName = "New (Slate)"), + + /** User-authored UMG Widget Blueprint deriving from UPS_Editor_MainWidget_UMG. + * Requires UMGWidgetClass to be assigned. */ + UMG UMETA(DisplayName = "UMG Widget Blueprint"), +}; /** * HUD for the PS_Editor runtime editing mode. - * Creates and manages the main editor widget. + * Creates and manages the main editor widget. The flavor is selected via UIMode. */ UCLASS() class PS_EDITOR_API APS_Editor_HUD : public AHUD @@ -16,14 +34,32 @@ class PS_EDITOR_API APS_Editor_HUD : public AHUD GENERATED_BODY() public: - /** Returns the main editor widget instance. */ + /** Which UI implementation to instantiate. Set via Details panel on the HUD Blueprint. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "ASTERION|PS_Editor|UI") + EPS_Editor_UIMode UIMode = EPS_Editor_UIMode::NewSlate; + + /** UMG Widget Blueprint class to spawn when UIMode == UMG. + * Must derive from UPS_Editor_MainWidget_UMG. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "ASTERION|PS_Editor|UI", meta = (EditCondition = "UIMode == EPS_Editor_UIMode::UMG")) + TSoftClassPtr UMGWidgetClass; + + /** Returns the active main editor widget (cast as needed). */ UFUNCTION(BlueprintCallable, Category = "ASTERION|PS_Editor|UI") - UPS_Editor_MainWidget* GetMainWidget() const { return MainWidget; } + UUserWidget* GetMainWidget() const { return MainWidget; } + + /** Backward-compat accessor for the refactored Slate widget (returns nullptr in Legacy/UMG modes). */ + UPS_Editor_MainWidget* GetMainWidget_NewSlate() const; + + /** Accessor for the legacy Slate widget (returns nullptr in NewSlate/UMG modes). */ + UPS_Editor_MainWidget_Legacy* GetMainWidget_Legacy() const; + + /** Accessor for the UMG widget (returns nullptr in Legacy/NewSlate modes). */ + UPS_Editor_MainWidget_UMG* GetMainWidget_UMG() const; protected: virtual void BeginPlay() override; private: UPROPERTY() - TObjectPtr MainWidget; + TObjectPtr MainWidget; }; diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Style/PS_Editor_UIStyle.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Style/PS_Editor_UIStyle.cpp new file mode 100644 index 0000000..044f323 --- /dev/null +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Style/PS_Editor_UIStyle.cpp @@ -0,0 +1,558 @@ +#include "PS_Editor_UIStyle.h" + +#include "PS_Editor_UIStyleAsset.h" +#include "Styling/CoreStyle.h" +#include "Styling/SlateBrush.h" +#include "Styling/SlateTypes.h" +#include "Widgets/Images/SImage.h" +#include "Widgets/Layout/SBorder.h" +#include "Widgets/Layout/SBox.h" +#include "Widgets/SBoxPanel.h" +#include "Widgets/Text/STextBlock.h" +#include "Engine/Texture2D.h" +#include "IImageWrapper.h" +#include "IImageWrapperModule.h" +#include "Modules/ModuleManager.h" +#include "Misc/FileHelper.h" +#include "Misc/Paths.h" +#include "HAL/FileManager.h" +#include "Interfaces/IPluginManager.h" +#include "TextureResource.h" +#include "PixelFormat.h" + +namespace FPS_Editor_UIStyle +{ + // Hardcoded fallbacks matching the DataAsset defaults. + namespace Defaults + { + static const FLinearColor BgPrimary = FLinearColor(0.027f, 0.039f, 0.059f, 1.0f); + static const FLinearColor BgSecondary = FLinearColor(0.043f, 0.067f, 0.094f, 1.0f); + static const FLinearColor GlassStrong = FLinearColor(0.0784f, 0.1216f, 0.1804f, 0.88f); + static const FLinearColor GlassSoft = FLinearColor(0.0549f, 0.0863f, 0.1333f, 0.72f); + static const FLinearColor GlassBorder = FLinearColor(1.0f, 1.0f, 1.0f, 0.10f); + static const FLinearColor GlassBorderStrong = FLinearColor(1.0f, 1.0f, 1.0f, 0.18f); + static const FLinearColor TextPrimary = FLinearColor(0.902f, 0.925f, 0.953f, 1.0f); + static const FLinearColor TextMuted = FLinearColor(0.722f, 0.769f, 0.824f, 1.0f); + static const FLinearColor TextDim = FLinearColor(0.502f, 0.569f, 0.647f, 1.0f); + static const FLinearColor TextDisabled = FLinearColor(0.353f, 0.416f, 0.494f, 1.0f); + static const FLinearColor Accent = FLinearColor(1.0f, 0.416f, 0.239f, 1.0f); + static const FLinearColor AccentSoft = FLinearColor(1.0f, 0.416f, 0.239f, 0.14f); + static const FLinearColor AccentRing = FLinearColor(1.0f, 0.416f, 0.239f, 0.35f); + static const FLinearColor Ok = FLinearColor(0.243f, 0.812f, 0.557f, 1.0f); + static const FLinearColor Warn = FLinearColor(0.961f, 0.725f, 0.267f, 1.0f); + static const FLinearColor Err = FLinearColor(1.0f, 0.353f, 0.431f, 1.0f); + static const FLinearColor Info = FLinearColor(0.290f, 0.659f, 1.0f, 1.0f); + static const FLinearColor AxisX = FLinearColor(1.0f, 0.302f, 0.369f, 1.0f); + static const FLinearColor AxisY = FLinearColor(0.290f, 0.839f, 0.427f, 1.0f); + static const FLinearColor AxisZ = FLinearColor(0.290f, 0.659f, 1.0f, 1.0f); + + static constexpr int32 FontSizeXSmall = 9; + static constexpr int32 FontSizeSmall = 10; + static constexpr int32 FontSizeBody = 11; + static constexpr int32 FontSizeHeading = 13; + static constexpr int32 FontSizeLarge = 16; + + static constexpr float RadiusXS = 3.0f; + static constexpr float RadiusS = 5.0f; + static constexpr float RadiusM = 7.0f; + static constexpr float RadiusL = 9.0f; + static constexpr float PadPanel = 10.0f; + static constexpr float PadRow = 6.0f; + } + +#define PSUI_COLOR_ACCESSOR(Name) \ + FLinearColor Name(const UPS_Editor_UIStyleAsset* S) { return S ? S->Name : Defaults::Name; } + + PSUI_COLOR_ACCESSOR(BgPrimary) + PSUI_COLOR_ACCESSOR(BgSecondary) + PSUI_COLOR_ACCESSOR(GlassStrong) + PSUI_COLOR_ACCESSOR(GlassSoft) + PSUI_COLOR_ACCESSOR(GlassBorder) + PSUI_COLOR_ACCESSOR(GlassBorderStrong) + PSUI_COLOR_ACCESSOR(TextPrimary) + PSUI_COLOR_ACCESSOR(TextMuted) + PSUI_COLOR_ACCESSOR(TextDim) + PSUI_COLOR_ACCESSOR(TextDisabled) + PSUI_COLOR_ACCESSOR(Accent) + PSUI_COLOR_ACCESSOR(AccentSoft) + PSUI_COLOR_ACCESSOR(AccentRing) + PSUI_COLOR_ACCESSOR(Ok) + PSUI_COLOR_ACCESSOR(Warn) + PSUI_COLOR_ACCESSOR(Err) + PSUI_COLOR_ACCESSOR(Info) + PSUI_COLOR_ACCESSOR(AxisX) + PSUI_COLOR_ACCESSOR(AxisY) + PSUI_COLOR_ACCESSOR(AxisZ) + +#undef PSUI_COLOR_ACCESSOR + + FSlateFontInfo BodyFont(const UPS_Editor_UIStyleAsset* S) + { + return FCoreStyle::GetDefaultFontStyle("Regular", S ? S->FontSizeBody : Defaults::FontSizeBody); + } + FSlateFontInfo BodyFontBold(const UPS_Editor_UIStyleAsset* S) + { + return FCoreStyle::GetDefaultFontStyle("Bold", S ? S->FontSizeBody : Defaults::FontSizeBody); + } + FSlateFontInfo SmallFont(const UPS_Editor_UIStyleAsset* S) + { + return FCoreStyle::GetDefaultFontStyle("Regular", S ? S->FontSizeSmall : Defaults::FontSizeSmall); + } + FSlateFontInfo HeadingFont(const UPS_Editor_UIStyleAsset* S) + { + return FCoreStyle::GetDefaultFontStyle("Regular", S ? S->FontSizeHeading : Defaults::FontSizeHeading); + } + FSlateFontInfo HeadingFontBold(const UPS_Editor_UIStyleAsset* S) + { + return FCoreStyle::GetDefaultFontStyle("Bold", S ? S->FontSizeHeading : Defaults::FontSizeHeading); + } + FSlateFontInfo LargeFont(const UPS_Editor_UIStyleAsset* S) + { + return FCoreStyle::GetDefaultFontStyle("Bold", S ? S->FontSizeLarge : Defaults::FontSizeLarge); + } + FSlateFontInfo MonoFont(const UPS_Editor_UIStyleAsset* S) + { + // Slate default fonts do not ship a mono family. Use Regular at the mono size and rely on tabular digits where possible. + return FCoreStyle::GetDefaultFontStyle("Mono", S ? S->FontSizeBody : Defaults::FontSizeBody); + } + + float RadiusXS(const UPS_Editor_UIStyleAsset* S) { return S ? S->RadiusXS : Defaults::RadiusXS; } + float RadiusS(const UPS_Editor_UIStyleAsset* S) { return S ? S->RadiusS : Defaults::RadiusS; } + float RadiusM(const UPS_Editor_UIStyleAsset* S) { return S ? S->RadiusM : Defaults::RadiusM; } + float RadiusL(const UPS_Editor_UIStyleAsset* S) { return S ? S->RadiusL : Defaults::RadiusL; } + + FMargin PadPanel(const UPS_Editor_UIStyleAsset* S) + { + const float V = S ? S->PadPanel : Defaults::PadPanel; + return FMargin(V); + } + FMargin PadRow(const UPS_Editor_UIStyleAsset* S) + { + const float V = S ? S->PadRow : Defaults::PadRow; + return FMargin(V); + } + + TSharedRef MakeGlassPanel(const UPS_Editor_UIStyleAsset* S, bool bStrong, TSharedRef Content, FMargin Padding) + { + const FLinearColor Fill = bStrong ? GlassStrong(S) : GlassSoft(S); + return SNew(SBorder) + .BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush")) + .BorderBackgroundColor(Fill) + .Padding(Padding) + [ + Content + ]; + } + + // Internal widget that owns its brush — ensures the brush outlives the SImage. + class SPSEditorIconWidget : public SCompoundWidget + { + public: + SLATE_BEGIN_ARGS(SPSEditorIconWidget) + : _Texture(nullptr) + , _Size(16.f) + , _Color(FLinearColor::White) + , _FollowForeground(false) + , _Fallback(TEXT("?")) + {} + SLATE_ARGUMENT(UTexture2D*, Texture) + SLATE_ARGUMENT(float, Size) + SLATE_ARGUMENT(FLinearColor, Color) + SLATE_ARGUMENT(bool, FollowForeground) + SLATE_ARGUMENT(const TCHAR*, Fallback) + SLATE_END_ARGS() + + void Construct(const FArguments& InArgs) + { + const float Size = InArgs._Size; + const FSlateColor Tint = InArgs._FollowForeground + ? FSlateColor::UseForeground() + : FSlateColor(InArgs._Color); + + if (InArgs._Texture) + { + IconBrush.SetResourceObject(InArgs._Texture); + IconBrush.ImageSize = FVector2D(Size, Size); + IconBrush.DrawAs = ESlateBrushDrawType::Image; + + ChildSlot + [ + SNew(SBox) + .WidthOverride(Size) + .HeightOverride(Size) + [ + SNew(SImage) + .Image(&IconBrush) + .ColorAndOpacity(Tint) + ] + ]; + } + else + { + const TCHAR* Glyph = InArgs._Fallback ? InArgs._Fallback : TEXT("?"); + ChildSlot + [ + SNew(SBox) + .WidthOverride(Size) + .HeightOverride(Size) + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(FText::FromString(Glyph)) + .Font(FCoreStyle::GetDefaultFontStyle("Regular", FMath::Max(8, FMath::RoundToInt(Size * 0.72f)))) + .ColorAndOpacity(Tint) + ] + ]; + } + } + + private: + FSlateBrush IconBrush; + }; + + TSharedRef MakeIcon(const UPS_Editor_UIStyleAsset* S, EPS_Editor_Icon Icon, float Size, FLinearColor Tint) + { + FPS_Editor_IconData Data = S ? S->GetIcon(Icon) : FPS_Editor_IconData{}; + return SNew(SPSEditorIconWidget) + .Texture(Data.Texture) + .Size(Size) + .Color(Tint) + .FollowForeground(false) + .Fallback(Data.Fallback); + } + + TSharedRef MakeIconFollowForeground(const UPS_Editor_UIStyleAsset* S, EPS_Editor_Icon Icon, float Size) + { + FPS_Editor_IconData Data = S ? S->GetIcon(Icon) : FPS_Editor_IconData{}; + return SNew(SPSEditorIconWidget) + .Texture(Data.Texture) + .Size(Size) + .FollowForeground(true) + .Fallback(Data.Fallback); + } + + // ---- Brush / style factories ---- + + static FLinearColor ResolveToneFill(const UPS_Editor_UIStyleAsset* S, EPanelTone Tone) + { + switch (Tone) + { + case EPanelTone::GlassStrong: return GlassStrong(S); + case EPanelTone::GlassSoft: return GlassSoft(S); + // Pill uses white tint so callers can recolor via BorderBackgroundColor. + case EPanelTone::Pill: return FLinearColor::White; + case EPanelTone::FieldFill: return FLinearColor(0.0f, 0.0f, 0.0f, 0.28f); + case EPanelTone::AccentFill: return Accent(S); + case EPanelTone::AccentSoftFill: return AccentSoft(S); + } + return GlassStrong(S); + } + + void InitPanelBrush(FSlateBrush& Out, const UPS_Editor_UIStyleAsset* S, EPanelTone Tone, float CornerRadius) + { + Out.DrawAs = ESlateBrushDrawType::RoundedBox; + Out.TintColor = FSlateColor(ResolveToneFill(S, Tone)); + Out.OutlineSettings.CornerRadii = FVector4(CornerRadius, CornerRadius, CornerRadius, CornerRadius); + Out.OutlineSettings.RoundingType = ESlateBrushRoundingType::FixedRadius; + // Pill / Accent / Field have no outline; panels have a subtle glass border. + const bool bWantsBorder = (Tone == EPanelTone::GlassStrong || Tone == EPanelTone::GlassSoft); + Out.OutlineSettings.Width = bWantsBorder ? 1.0f : 0.0f; + Out.OutlineSettings.Color = FSlateColor(Tone == EPanelTone::AccentFill + ? AccentRing(S) + : GlassBorder(S)); + Out.Margin = FMargin(0.0f); + } + + void InitFlatButtonStyle(FButtonStyle& Out, const UPS_Editor_UIStyleAsset* S, float CornerRadius) + { + auto Make = [CornerRadius](FLinearColor Fill, FLinearColor Border) + { + FSlateBrush B; + B.DrawAs = ESlateBrushDrawType::RoundedBox; + B.TintColor = FSlateColor(Fill); + B.OutlineSettings.CornerRadii = FVector4(CornerRadius, CornerRadius, CornerRadius, CornerRadius); + B.OutlineSettings.RoundingType = ESlateBrushRoundingType::FixedRadius; + B.OutlineSettings.Width = Border.A > 0.f ? 1.f : 0.f; + B.OutlineSettings.Color = FSlateColor(Border); + return B; + }; + + const FLinearColor Transparent = FLinearColor(0, 0, 0, 0); + const FLinearColor HoverFill = FLinearColor(1, 1, 1, 0.06f); + const FLinearColor PressedFill = FLinearColor(1, 1, 1, 0.10f); + + Out.Normal = Make(Transparent, Transparent); + Out.Hovered = Make(HoverFill, Transparent); + Out.Pressed = Make(PressedFill, Transparent); + Out.Disabled = Make(Transparent, Transparent); + + Out.NormalPadding = FMargin(8, 4); + Out.PressedPadding = FMargin(8, 4); + Out.NormalForeground = FSlateColor(TextPrimary(S)); + Out.HoveredForeground = FSlateColor(TextPrimary(S)); + Out.PressedForeground = FSlateColor(TextPrimary(S)); + Out.DisabledForeground = FSlateColor(TextDisabled(S)); + } + + void InitAccentButtonStyle(FButtonStyle& Out, const UPS_Editor_UIStyleAsset* S, float CornerRadius) + { + auto Make = [CornerRadius](FLinearColor Fill) + { + FSlateBrush B; + B.DrawAs = ESlateBrushDrawType::RoundedBox; + B.TintColor = FSlateColor(Fill); + B.OutlineSettings.CornerRadii = FVector4(CornerRadius, CornerRadius, CornerRadius, CornerRadius); + B.OutlineSettings.RoundingType = ESlateBrushRoundingType::FixedRadius; + B.OutlineSettings.Width = 0.0f; + return B; + }; + + const FLinearColor AccentC = Accent(S); + const FLinearColor AccentHov(FMath::Min(AccentC.R * 1.08f, 1.f), FMath::Min(AccentC.G * 1.08f, 1.f), FMath::Min(AccentC.B * 1.08f, 1.f), AccentC.A); + const FLinearColor AccentDim(AccentC.R * 0.88f, AccentC.G * 0.88f, AccentC.B * 0.88f, AccentC.A); + + Out.Normal = Make(AccentC); + Out.Hovered = Make(AccentHov); + Out.Pressed = Make(AccentDim); + Out.Disabled = Make(AccentSoft(S)); + + Out.NormalPadding = FMargin(10, 4); + Out.PressedPadding = FMargin(10, 4); + + // Dark text on accent (design uses #1a0a00). + const FLinearColor DarkOnAccent(0.10f, 0.04f, 0.0f, 1.0f); + Out.NormalForeground = FSlateColor(DarkOnAccent); + Out.HoveredForeground = FSlateColor(DarkOnAccent); + Out.PressedForeground = FSlateColor(DarkOnAccent); + Out.DisabledForeground = FSlateColor(TextDisabled(S)); + } + + void InitTextBoxStyle(FEditableTextBoxStyle& Out, const UPS_Editor_UIStyleAsset* S, float CornerRadius) + { + auto Make = [CornerRadius](FLinearColor Fill, FLinearColor Border) + { + FSlateBrush B; + B.DrawAs = ESlateBrushDrawType::RoundedBox; + B.TintColor = FSlateColor(Fill); + B.OutlineSettings.CornerRadii = FVector4(CornerRadius, CornerRadius, CornerRadius, CornerRadius); + B.OutlineSettings.RoundingType = ESlateBrushRoundingType::FixedRadius; + B.OutlineSettings.Width = Border.A > 0.f ? 1.f : 0.f; + B.OutlineSettings.Color = FSlateColor(Border); + return B; + }; + + const FLinearColor Fill = FLinearColor(0, 0, 0, 0.28f); + const FLinearColor Border = GlassBorder(S); + const FLinearColor Focus = AccentRing(S); + + Out.BackgroundImageNormal = Make(Fill, Border); + Out.BackgroundImageHovered = Make(Fill, GlassBorderStrong(S)); + Out.BackgroundImageFocused = Make(Fill, Focus); + Out.BackgroundImageReadOnly = Make(Fill, Border); + + Out.Padding = FMargin(6, 3); + Out.ForegroundColor = FSlateColor(TextPrimary(S)); + Out.TextStyle.ColorAndOpacity = FSlateColor(TextPrimary(S)); + Out.TextStyle.Font = BodyFont(S); + } + + // ---- Runtime PNG loader ---- + + static UTexture2D* LoadTextureFromPng(const FString& FilePath) + { + TArray RawFileData; + if (!FFileHelper::LoadFileToArray(RawFileData, *FilePath)) + { + return nullptr; + } + + IImageWrapperModule& ImageWrapperModule = FModuleManager::LoadModuleChecked(FName("ImageWrapper")); + TSharedPtr ImageWrapper = ImageWrapperModule.CreateImageWrapper(EImageFormat::PNG); + if (!ImageWrapper.IsValid() || !ImageWrapper->SetCompressed(RawFileData.GetData(), RawFileData.Num())) + { + return nullptr; + } + + TArray UncompressedBGRA; + if (!ImageWrapper->GetRaw(ERGBFormat::BGRA, 8, UncompressedBGRA)) + { + return nullptr; + } + + const int32 Width = ImageWrapper->GetWidth(); + const int32 Height = ImageWrapper->GetHeight(); + if (Width <= 0 || Height <= 0) return nullptr; + + UTexture2D* Texture = UTexture2D::CreateTransient(Width, Height, PF_B8G8R8A8); + if (!Texture) return nullptr; + + void* TextureData = Texture->GetPlatformData()->Mips[0].BulkData.Lock(LOCK_READ_WRITE); + FMemory::Memcpy(TextureData, UncompressedBGRA.GetData(), UncompressedBGRA.Num()); + Texture->GetPlatformData()->Mips[0].BulkData.Unlock(); + + Texture->SRGB = false; + Texture->CompressionSettings = TC_EditorIcon; + Texture->Filter = TextureFilter::TF_Trilinear; + Texture->UpdateResource(); + + return Texture; + } + + static FString ResolvePluginIconsDir() + { + TSharedPtr Plugin = IPluginManager::Get().FindPlugin(TEXT("PS_Editor")); + if (Plugin.IsValid()) + { + return Plugin->GetContentDir() / TEXT("UI/icons"); + } + // Fallback: assume relative to project + return FPaths::ProjectContentDir() / TEXT("UI/icons"); + } + + struct FIconSlot + { + EPS_Editor_Icon Icon; + const TCHAR* FileStem; + TObjectPtr UPS_Editor_UIStyleAsset::* Member; + }; + + TSharedPtr Load9SliceBrushFromPluginContent(const FString& RelativePath) + { + TSharedPtr Plugin = IPluginManager::Get().FindPlugin(TEXT("PS_Editor")); + const FString BaseDir = Plugin.IsValid() ? Plugin->GetContentDir() : (FPaths::ProjectContentDir()); + const FString Path = BaseDir / TEXT("UI/unreal_9slice") / RelativePath; + if (!IFileManager::Get().FileExists(*Path)) return nullptr; + + UTexture2D* Tex = LoadTextureFromPng(Path); + if (!Tex) return nullptr; + Tex->AddToRoot(); + + TSharedRef Brush = MakeShared(); + Brush->SetResourceObject(Tex); + Brush->ImageSize = FVector2D(Tex->GetSizeX(), Tex->GetSizeY()); + Brush->DrawAs = ESlateBrushDrawType::Box; + // PNG is 192x192 with rounded shape inside a 64px corner. We slice tighter than the README's + // 0.333 so small widgets (topbar height ~32-40px, buttons ~24-28px) still render all corners. + // 0.25 = 48px corners, which is above the 24px rounded radius but keeps the slice inside the + // transparent buffer surrounding the shape. + Brush->Margin = FMargin(0.25f, 0.25f, 0.25f, 0.25f); + Brush->TintColor = FSlateColor(FLinearColor::White); + return Brush; + } + + bool InitNineSliceButtonStyle(FButtonStyle& Out, const UPS_Editor_UIStyleAsset* S, + const FString& NormalStem, const FString& HoverStem, const FString& PressedStem, + FLinearColor NormalTint, FLinearColor HoverTint, FLinearColor PressedTint) + { + TSharedPtr NormalBrush = Load9SliceBrushFromPluginContent(NormalStem + TEXT(".png")); + TSharedPtr HoverBrush = Load9SliceBrushFromPluginContent(HoverStem + TEXT(".png")); + TSharedPtr PressedBrush = Load9SliceBrushFromPluginContent(PressedStem + TEXT(".png")); + + if (!NormalBrush.IsValid() || !HoverBrush.IsValid() || !PressedBrush.IsValid()) return false; + + NormalBrush->TintColor = FSlateColor(NormalTint); + HoverBrush->TintColor = FSlateColor(HoverTint); + PressedBrush->TintColor = FSlateColor(PressedTint); + + Out.Normal = *NormalBrush; + Out.Hovered = *HoverBrush; + Out.Pressed = *PressedBrush; + Out.Disabled = *NormalBrush; + Out.Disabled.TintColor = FSlateColor(FLinearColor(NormalTint.R, NormalTint.G, NormalTint.B, NormalTint.A * 0.35f)); + + Out.NormalPadding = FMargin(10, 4); + Out.PressedPadding = FMargin(10, 4); + // State-differentiated foregrounds — any child widget using FSlateColor::UseForeground() + // (typical for icons and labels inside buttons) will automatically flip color per state. + Out.NormalForeground = FSlateColor(TextMuted(S)); + Out.HoveredForeground = FSlateColor(TextPrimary(S)); + Out.PressedForeground = FSlateColor(Accent(S)); + Out.DisabledForeground = FSlateColor(TextDisabled(S)); + return true; + } + + int32 PopulateIconsFromPluginContent(UPS_Editor_UIStyleAsset* StyleAsset) + { + if (!StyleAsset) return 0; + const FString IconsDir = ResolvePluginIconsDir(); + + // Maps each icon slot to the PNG filename the user provided. + static const FIconSlot Slots[] = { + { EPS_Editor_Icon::Move, TEXT("Move"), &UPS_Editor_UIStyleAsset::IconMove }, + { EPS_Editor_Icon::Rotate, TEXT("Rotate"), &UPS_Editor_UIStyleAsset::IconRotate }, + { EPS_Editor_Icon::Scale, TEXT("Scale"), &UPS_Editor_UIStyleAsset::IconScale }, + { EPS_Editor_Icon::Snap, TEXT("Snap"), &UPS_Editor_UIStyleAsset::IconSnap }, + { EPS_Editor_Icon::Grid, TEXT("Grid"), &UPS_Editor_UIStyleAsset::IconGrid }, + { EPS_Editor_Icon::World, TEXT("Globe"), &UPS_Editor_UIStyleAsset::IconWorld }, + { EPS_Editor_Icon::Focus, TEXT("Target"), &UPS_Editor_UIStyleAsset::IconFocus }, + + { EPS_Editor_Icon::Play, TEXT("Play"), &UPS_Editor_UIStyleAsset::IconPlay }, + { EPS_Editor_Icon::Pause, TEXT("Pause"), &UPS_Editor_UIStyleAsset::IconPause }, + { EPS_Editor_Icon::Stop, TEXT("Stop"), &UPS_Editor_UIStyleAsset::IconStop }, + { EPS_Editor_Icon::Record, TEXT("Record"), &UPS_Editor_UIStyleAsset::IconRecord }, + { EPS_Editor_Icon::SkipBack, TEXT("SkipBack"), &UPS_Editor_UIStyleAsset::IconSkipBack }, + { EPS_Editor_Icon::SkipForward, TEXT("SkipFwd"), &UPS_Editor_UIStyleAsset::IconSkipForward }, + { EPS_Editor_Icon::Simulate, TEXT("Zap"), &UPS_Editor_UIStyleAsset::IconSimulate }, + + { EPS_Editor_Icon::Eye, TEXT("Eye"), &UPS_Editor_UIStyleAsset::IconEye }, + { EPS_Editor_Icon::EyeOff, TEXT("EyeOff"), &UPS_Editor_UIStyleAsset::IconEyeOff }, + { EPS_Editor_Icon::Lock, TEXT("Lock"), &UPS_Editor_UIStyleAsset::IconLock }, + { EPS_Editor_Icon::Unlock, TEXT("Unlock"), &UPS_Editor_UIStyleAsset::IconUnlock }, + { EPS_Editor_Icon::Sidebar, TEXT("Sidebar"), &UPS_Editor_UIStyleAsset::IconSidebar }, + { EPS_Editor_Icon::PanelRight, TEXT("PanelRight"), &UPS_Editor_UIStyleAsset::IconPanelRight }, + { EPS_Editor_Icon::PanelBottom, TEXT("PanelBottom"), &UPS_Editor_UIStyleAsset::IconPanelBottom }, + + { EPS_Editor_Icon::Save, TEXT("Save"), &UPS_Editor_UIStyleAsset::IconSave }, + { EPS_Editor_Icon::Load, TEXT("Folder"), &UPS_Editor_UIStyleAsset::IconLoad }, + { EPS_Editor_Icon::Folder, TEXT("Folder"), &UPS_Editor_UIStyleAsset::IconFolder }, + { EPS_Editor_Icon::NewFile, TEXT("Plus"), &UPS_Editor_UIStyleAsset::IconNew }, + + { EPS_Editor_Icon::Plus, TEXT("Plus"), &UPS_Editor_UIStyleAsset::IconPlus }, + { EPS_Editor_Icon::Minus, TEXT("Minus"), &UPS_Editor_UIStyleAsset::IconMinus }, + { EPS_Editor_Icon::Check, TEXT("Check"), &UPS_Editor_UIStyleAsset::IconCheck }, + + { EPS_Editor_Icon::Search, TEXT("Search"), &UPS_Editor_UIStyleAsset::IconSearch }, + { EPS_Editor_Icon::Settings, TEXT("Settings"), &UPS_Editor_UIStyleAsset::IconSettings }, + { EPS_Editor_Icon::ChevronDown, TEXT("ChevronDown"), &UPS_Editor_UIStyleAsset::IconChevronDown }, + { EPS_Editor_Icon::ChevronRight, TEXT("ChevronRight"), &UPS_Editor_UIStyleAsset::IconChevronRight }, + { EPS_Editor_Icon::ChevronLeft, TEXT("ChevronLeft"), &UPS_Editor_UIStyleAsset::IconChevronLeft }, + { EPS_Editor_Icon::Command, TEXT("Command"), &UPS_Editor_UIStyleAsset::IconCommand }, + { EPS_Editor_Icon::More, TEXT("More"), &UPS_Editor_UIStyleAsset::IconMore }, + { EPS_Editor_Icon::Help, TEXT("Help"), &UPS_Editor_UIStyleAsset::IconHelp }, + { EPS_Editor_Icon::Bell, TEXT("Bell"), &UPS_Editor_UIStyleAsset::IconBell }, + { EPS_Editor_Icon::Download, TEXT("Download"), &UPS_Editor_UIStyleAsset::IconDownload }, + + { EPS_Editor_Icon::Character, TEXT("User"), &UPS_Editor_UIStyleAsset::IconCharacter }, + { EPS_Editor_Icon::SplineObj, TEXT("Spline"), &UPS_Editor_UIStyleAsset::IconSpline }, + { EPS_Editor_Icon::Prop, TEXT("Box"), &UPS_Editor_UIStyleAsset::IconProp }, + { EPS_Editor_Icon::Light, TEXT("Lightbulb"), &UPS_Editor_UIStyleAsset::IconLight }, + { EPS_Editor_Icon::Camera, TEXT("Camera"), &UPS_Editor_UIStyleAsset::IconCamera }, + { EPS_Editor_Icon::Trigger, TEXT("Zap"), &UPS_Editor_UIStyleAsset::IconTrigger }, + { EPS_Editor_Icon::Layers, TEXT("Layers"), &UPS_Editor_UIStyleAsset::IconLayers }, + { EPS_Editor_Icon::Film, TEXT("Film"), &UPS_Editor_UIStyleAsset::IconFilm }, + { EPS_Editor_Icon::Box, TEXT("Box"), &UPS_Editor_UIStyleAsset::IconBox }, + { EPS_Editor_Icon::Flag, TEXT("Flag"), &UPS_Editor_UIStyleAsset::IconFlag }, + { EPS_Editor_Icon::Map, TEXT("Map"), &UPS_Editor_UIStyleAsset::IconMap }, + + { EPS_Editor_Icon::BrandMark, TEXT("Diamond"), &UPS_Editor_UIStyleAsset::IconBrandMark }, + }; + + int32 Loaded = 0; + for (const FIconSlot& Slot : Slots) + { + if (StyleAsset->*(Slot.Member)) continue; // user already assigned it, keep it + + const FString Path = IconsDir / FString::Printf(TEXT("%s.png"), Slot.FileStem); + if (!IFileManager::Get().FileExists(*Path)) continue; + + if (UTexture2D* Tex = LoadTextureFromPng(Path)) + { + Tex->AddToRoot(); // transient, keep alive while plugin runs + StyleAsset->*(Slot.Member) = Tex; + Loaded++; + } + } + return Loaded; + } +} diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Style/PS_Editor_UIStyle.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Style/PS_Editor_UIStyle.h new file mode 100644 index 0000000..f710413 --- /dev/null +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Style/PS_Editor_UIStyle.h @@ -0,0 +1,110 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Fonts/SlateFontInfo.h" +#include "Styling/SlateColor.h" +#include "Styling/SlateTypes.h" +#include "Widgets/SCompoundWidget.h" +#include "PS_Editor_UIStyleAsset.h" + +class UPS_Editor_UIStyleAsset; +class SWidget; +struct FSlateBrush; +struct FButtonStyle; +struct FEditableTextBoxStyle; + +/** + * Style accessors for PS_Editor UI. Every function takes a (nullable) style asset + * and returns a value ready to feed to Slate. If the asset is null, hardcoded + * fallbacks matching the DataAsset defaults are returned so the UI never breaks. + */ +namespace FPS_Editor_UIStyle +{ + // ---- Colors ---- + PS_EDITOR_API FLinearColor BgPrimary(const UPS_Editor_UIStyleAsset* S); + PS_EDITOR_API FLinearColor BgSecondary(const UPS_Editor_UIStyleAsset* S); + PS_EDITOR_API FLinearColor GlassStrong(const UPS_Editor_UIStyleAsset* S); + PS_EDITOR_API FLinearColor GlassSoft(const UPS_Editor_UIStyleAsset* S); + PS_EDITOR_API FLinearColor GlassBorder(const UPS_Editor_UIStyleAsset* S); + PS_EDITOR_API FLinearColor GlassBorderStrong(const UPS_Editor_UIStyleAsset* S); + + PS_EDITOR_API FLinearColor TextPrimary(const UPS_Editor_UIStyleAsset* S); + PS_EDITOR_API FLinearColor TextMuted(const UPS_Editor_UIStyleAsset* S); + PS_EDITOR_API FLinearColor TextDim(const UPS_Editor_UIStyleAsset* S); + PS_EDITOR_API FLinearColor TextDisabled(const UPS_Editor_UIStyleAsset* S); + + PS_EDITOR_API FLinearColor Accent(const UPS_Editor_UIStyleAsset* S); + PS_EDITOR_API FLinearColor AccentSoft(const UPS_Editor_UIStyleAsset* S); + PS_EDITOR_API FLinearColor AccentRing(const UPS_Editor_UIStyleAsset* S); + + PS_EDITOR_API FLinearColor Ok(const UPS_Editor_UIStyleAsset* S); + PS_EDITOR_API FLinearColor Warn(const UPS_Editor_UIStyleAsset* S); + PS_EDITOR_API FLinearColor Err(const UPS_Editor_UIStyleAsset* S); + PS_EDITOR_API FLinearColor Info(const UPS_Editor_UIStyleAsset* S); + + PS_EDITOR_API FLinearColor AxisX(const UPS_Editor_UIStyleAsset* S); + PS_EDITOR_API FLinearColor AxisY(const UPS_Editor_UIStyleAsset* S); + PS_EDITOR_API FLinearColor AxisZ(const UPS_Editor_UIStyleAsset* S); + + // ---- Fonts ---- + PS_EDITOR_API FSlateFontInfo BodyFont(const UPS_Editor_UIStyleAsset* S); + PS_EDITOR_API FSlateFontInfo BodyFontBold(const UPS_Editor_UIStyleAsset* S); + PS_EDITOR_API FSlateFontInfo SmallFont(const UPS_Editor_UIStyleAsset* S); + PS_EDITOR_API FSlateFontInfo HeadingFont(const UPS_Editor_UIStyleAsset* S); + PS_EDITOR_API FSlateFontInfo HeadingFontBold(const UPS_Editor_UIStyleAsset* S); + PS_EDITOR_API FSlateFontInfo LargeFont(const UPS_Editor_UIStyleAsset* S); + PS_EDITOR_API FSlateFontInfo MonoFont(const UPS_Editor_UIStyleAsset* S); + + // ---- Layout ---- + PS_EDITOR_API float RadiusXS(const UPS_Editor_UIStyleAsset* S); + PS_EDITOR_API float RadiusS(const UPS_Editor_UIStyleAsset* S); + PS_EDITOR_API float RadiusM(const UPS_Editor_UIStyleAsset* S); + PS_EDITOR_API float RadiusL(const UPS_Editor_UIStyleAsset* S); + PS_EDITOR_API FMargin PadPanel(const UPS_Editor_UIStyleAsset* S); + PS_EDITOR_API FMargin PadRow(const UPS_Editor_UIStyleAsset* S); + + // ---- Composite helpers ---- + /** Build an SBorder with glass fill + border, wrapping Content. */ + PS_EDITOR_API TSharedRef MakeGlassPanel(const UPS_Editor_UIStyleAsset* S, bool bStrong, TSharedRef Content, FMargin Padding = FMargin(12.f)); + + /** Build an icon widget with a fixed tint. Returns SImage if the slot has a texture; otherwise falls back to an STextBlock with a unicode glyph. */ + PS_EDITOR_API TSharedRef MakeIcon(const UPS_Editor_UIStyleAsset* S, EPS_Editor_Icon Icon, float Size = 16.f, FLinearColor Tint = FLinearColor::White); + + /** Build an icon widget that inherits the parent button's foreground color (for normal/hover/pressed state tinting). */ + PS_EDITOR_API TSharedRef MakeIconFollowForeground(const UPS_Editor_UIStyleAsset* S, EPS_Editor_Icon Icon, float Size = 16.f); + + // ---- Brush / style factories ---- + // These construct FSlateBrush / FButtonStyle / FEditableTextBoxStyle objects. The caller is + // responsible for keeping them alive (typically as widget members) because Slate only stores + // raw pointers and never copies the brush contents. + enum class EPanelTone : uint8 { GlassStrong, GlassSoft, Pill, FieldFill, AccentFill, AccentSoftFill }; + + /** Rounded-box brush matching the design tokens. Width/corner radius from the style tokens. */ + PS_EDITOR_API void InitPanelBrush(FSlateBrush& Out, const UPS_Editor_UIStyleAsset* S, EPanelTone Tone, float CornerRadius = 10.f); + + /** Flat button style without chrome. Hover/pressed use subtle accent tint. */ + PS_EDITOR_API void InitFlatButtonStyle(FButtonStyle& Out, const UPS_Editor_UIStyleAsset* S, float CornerRadius = 6.f); + + /** Accent button style (orange fill, dark text). */ + PS_EDITOR_API void InitAccentButtonStyle(FButtonStyle& Out, const UPS_Editor_UIStyleAsset* S, float CornerRadius = 6.f); + + /** Minimal editable text box style with thin rounded border. */ + PS_EDITOR_API void InitTextBoxStyle(FEditableTextBoxStyle& Out, const UPS_Editor_UIStyleAsset* S, float CornerRadius = 4.f); + + /** Scan /UI/icons/ for PNG files and populate all unassigned icon slots on the asset. + * Files are expected as ".png" (matching the EPS_Editor_Icon enum name, e.g. "Move.png"). + * Already-assigned slots are preserved. Returns the number of icons loaded. */ + PS_EDITOR_API int32 PopulateIconsFromPluginContent(UPS_Editor_UIStyleAsset* StyleAsset); + + /** Load a PNG as a 9-slice Box brush (Margin 0.333 on all sides). Returns nullptr if the file is missing. */ + PS_EDITOR_API TSharedPtr Load9SliceBrushFromPluginContent(const FString& RelativePath); + + /** Initialize a button style from three 9-slice PNGs (normal/hover/pressed). Falls back to flat + * procedural style if any file is missing. NormalSlice/HoverSlice/PressedSlice are paths relative + * to Content/UI/unreal_9slice/. */ + PS_EDITOR_API bool InitNineSliceButtonStyle(FButtonStyle& Out, const UPS_Editor_UIStyleAsset* S, + const FString& NormalStem, const FString& HoverStem, const FString& PressedStem, + FLinearColor NormalTint = FLinearColor::White, + FLinearColor HoverTint = FLinearColor::White, + FLinearColor PressedTint = FLinearColor::White); +} diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Style/PS_Editor_UIStyleAsset.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Style/PS_Editor_UIStyleAsset.cpp new file mode 100644 index 0000000..bf6dce8 --- /dev/null +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Style/PS_Editor_UIStyleAsset.cpp @@ -0,0 +1,72 @@ +#include "PS_Editor_UIStyleAsset.h" + +FPS_Editor_IconData UPS_Editor_UIStyleAsset::GetIcon(EPS_Editor_Icon Icon) const +{ + switch (Icon) + { + case EPS_Editor_Icon::Move: return { IconMove, TEXT("\u2194") }; // ↔ + case EPS_Editor_Icon::Rotate: return { IconRotate, TEXT("\u21BB") }; // ↻ + case EPS_Editor_Icon::Scale: return { IconScale, TEXT("\u2922") }; // ⤢ + case EPS_Editor_Icon::Snap: return { IconSnap, TEXT("\u29C7") }; // ⧇ + case EPS_Editor_Icon::Grid: return { IconGrid, TEXT("\u25A6") }; // ▦ + case EPS_Editor_Icon::World: return { IconWorld, TEXT("\u25CE") }; // ◎ + case EPS_Editor_Icon::Focus: return { IconFocus, TEXT("\u2316") }; // ⌖ + + case EPS_Editor_Icon::Play: return { IconPlay, TEXT("\u25B6") }; // ▶ + case EPS_Editor_Icon::Pause: return { IconPause, TEXT("\u23F8") }; // ⏸ + case EPS_Editor_Icon::Stop: return { IconStop, TEXT("\u25A0") }; // ■ + case EPS_Editor_Icon::Record: return { IconRecord, TEXT("\u25CF") }; // ● + case EPS_Editor_Icon::SkipBack: return { IconSkipBack, TEXT("\u23EE") }; // ⏮ + case EPS_Editor_Icon::SkipForward: return { IconSkipForward, TEXT("\u23ED") }; // ⏭ + case EPS_Editor_Icon::Simulate: return { IconSimulate, TEXT("\u26A1") }; // ⚡ + + case EPS_Editor_Icon::Eye: return { IconEye, TEXT("\u25C9") }; // ◉ + case EPS_Editor_Icon::EyeOff: return { IconEyeOff, TEXT("\u00D8") }; // Ø + case EPS_Editor_Icon::Lock: return { IconLock, TEXT("\u2B23") }; // ⬣ + case EPS_Editor_Icon::Unlock: return { IconUnlock, TEXT("\u2B21") }; // ⬡ + case EPS_Editor_Icon::Sidebar: return { IconSidebar, TEXT("\u258C") }; // ▌ + case EPS_Editor_Icon::PanelRight: return { IconPanelRight, TEXT("\u2590") }; // ▐ + case EPS_Editor_Icon::PanelBottom: return { IconPanelBottom, TEXT("\u2584") }; // ▄ + + case EPS_Editor_Icon::Save: return { IconSave, TEXT("\u2193") }; // ↓ + case EPS_Editor_Icon::Load: return { IconLoad, TEXT("\u2191") }; // ↑ + case EPS_Editor_Icon::Folder: return { IconFolder, TEXT("\u25B1") }; // ▱ + case EPS_Editor_Icon::NewFile: return { IconNew, TEXT("\u2795") }; // ➕ + + case EPS_Editor_Icon::Plus: return { IconPlus, TEXT("+") }; + case EPS_Editor_Icon::Minus: return { IconMinus, TEXT("\u2212") }; // − + case EPS_Editor_Icon::Delete: return { IconDelete, TEXT("\u2716") }; // ✖ + case EPS_Editor_Icon::Duplicate: return { IconDuplicate, TEXT("\u29C9") }; // ⧉ + case EPS_Editor_Icon::Undo: return { IconUndo, TEXT("\u21B6") }; // ↶ + case EPS_Editor_Icon::Redo: return { IconRedo, TEXT("\u21B7") }; // ↷ + case EPS_Editor_Icon::Check: return { IconCheck, TEXT("\u2713") }; // ✓ + case EPS_Editor_Icon::Close: return { IconClose, TEXT("\u00D7") }; // × + + case EPS_Editor_Icon::Search: return { IconSearch, TEXT("\u2315") }; // ⌕ + case EPS_Editor_Icon::Settings: return { IconSettings, TEXT("\u2699") }; // ⚙ + case EPS_Editor_Icon::ChevronDown: return { IconChevronDown, TEXT("\u25BE") }; // ▾ + case EPS_Editor_Icon::ChevronRight: return { IconChevronRight, TEXT("\u25B8") }; // ▸ + case EPS_Editor_Icon::ChevronLeft: return { IconChevronLeft, TEXT("\u25C2") }; // ◂ + case EPS_Editor_Icon::Command: return { IconCommand, TEXT("\u2318") }; // ⌘ + case EPS_Editor_Icon::More: return { IconMore, TEXT("\u22EF") }; // ⋯ + case EPS_Editor_Icon::Help: return { IconHelp, TEXT("?") }; + case EPS_Editor_Icon::Bell: return { IconBell, TEXT("!") }; + case EPS_Editor_Icon::Download: return { IconDownload, TEXT("v") }; + + case EPS_Editor_Icon::Character: return { IconCharacter, TEXT("\u263A") }; // ☺ + case EPS_Editor_Icon::SplineObj: return { IconSpline, TEXT("\u223F") }; // ∿ + case EPS_Editor_Icon::Prop: return { IconProp, TEXT("\u25A3") }; // ▣ + case EPS_Editor_Icon::Light: return { IconLight, TEXT("\u2600") }; // ☀ + case EPS_Editor_Icon::Camera: return { IconCamera, TEXT("\u25A4") }; // ▤ + case EPS_Editor_Icon::Trigger: return { IconTrigger, TEXT("\u26A1") }; // ⚡ + case EPS_Editor_Icon::Layers: return { IconLayers, TEXT("\u2263") }; // ≣ + case EPS_Editor_Icon::Film: return { IconFilm, TEXT("\u25A9") }; // ▩ + case EPS_Editor_Icon::Box: return { IconBox, TEXT("\u25A2") }; // ▢ + case EPS_Editor_Icon::Flag: return { IconFlag, TEXT("F") }; + case EPS_Editor_Icon::Map: return { IconMap, TEXT("M") }; + + case EPS_Editor_Icon::BrandMark: return { IconBrandMark, TEXT("\u25C6") }; // ◆ + + default: return { nullptr, TEXT("?") }; + } +} diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Style/PS_Editor_UIStyleAsset.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Style/PS_Editor_UIStyleAsset.h new file mode 100644 index 0000000..61a176f --- /dev/null +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Style/PS_Editor_UIStyleAsset.h @@ -0,0 +1,235 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Engine/DataAsset.h" +#include "Engine/Texture2D.h" +#include "PS_Editor_UIStyleAsset.generated.h" + +/** Icon slot identifier. Used by FPS_Editor_UIStyle::MakeIcon() to fetch a texture from the style asset. */ +UENUM(BlueprintType) +enum class EPS_Editor_Icon : uint8 +{ + None = 0, + + // Transform tools + Move, Rotate, Scale, Snap, Grid, World, Focus, + + // Playback + Play, Pause, Stop, Record, SkipBack, SkipForward, Simulate, + + // View / visibility + Eye, EyeOff, Lock, Unlock, Sidebar, PanelRight, PanelBottom, + + // File + Save, Load, Folder, NewFile, + + // Edit + Plus, Minus, Delete, Duplicate, Undo, Redo, Check, Close, + + // Chrome / Nav + Search, Settings, ChevronDown, ChevronRight, ChevronLeft, Command, More, Help, Bell, Download, + + // Object types + Character, SplineObj, Prop, Light, Camera, Trigger, Layers, Film, Box, Flag, Map, + + // Brand + BrandMark, +}; + +/** Bundle returned by GetIcon: texture (may be null) + unicode fallback glyph. */ +struct FPS_Editor_IconData +{ + UTexture2D* Texture = nullptr; + const TCHAR* Fallback = TEXT("?"); +}; + +/** + * UI style configuration for PS_Editor. Create a DataAsset instance at + * /PS_Editor/Style/DA_PS_Editor_UIStyle and assign icon textures + tweak colors. + * If absent at runtime, the FPS_Editor_UIStyle helpers fall back to hardcoded values + * that match the defaults defined here. + * + * Icons specs: + * - PNG 32x32 with alpha + * - White stroke (#ffffff) on transparent background, stroke width 1.5-2 px (lucide-style) + * - Import settings: sRGB OFF, MipGen NoMipMaps, Texture Group UI, Compression UserInterface2D + */ +UCLASS(BlueprintType) +class PS_EDITOR_API UPS_Editor_UIStyleAsset : public UDataAsset +{ + GENERATED_BODY() + +public: + // ==================== COLORS ==================== + UPROPERTY(EditAnywhere, Category = "Colors|Background") + FLinearColor BgPrimary = FLinearColor(0.027f, 0.039f, 0.059f, 1.0f); // #070a0f + + UPROPERTY(EditAnywhere, Category = "Colors|Background") + FLinearColor BgSecondary = FLinearColor(0.043f, 0.067f, 0.094f, 1.0f); // #0b1118 + + UPROPERTY(EditAnywhere, Category = "Colors|Glass") + FLinearColor GlassStrong = FLinearColor(0.0784f, 0.1216f, 0.1804f, 0.88f); // #141f2e @ 88% + + UPROPERTY(EditAnywhere, Category = "Colors|Glass") + FLinearColor GlassSoft = FLinearColor(0.0549f, 0.0863f, 0.1333f, 0.72f); // #0e1622 @ 72% + + UPROPERTY(EditAnywhere, Category = "Colors|Glass") + FLinearColor GlassBorder = FLinearColor(1.0f, 1.0f, 1.0f, 0.10f); + + UPROPERTY(EditAnywhere, Category = "Colors|Glass") + FLinearColor GlassBorderStrong = FLinearColor(1.0f, 1.0f, 1.0f, 0.18f); + + UPROPERTY(EditAnywhere, Category = "Colors|Text") + FLinearColor TextPrimary = FLinearColor(0.902f, 0.925f, 0.953f, 1.0f); // #e6ecf3 + + UPROPERTY(EditAnywhere, Category = "Colors|Text") + FLinearColor TextMuted = FLinearColor(0.722f, 0.769f, 0.824f, 1.0f); // #b8c4d2 + + UPROPERTY(EditAnywhere, Category = "Colors|Text") + FLinearColor TextDim = FLinearColor(0.502f, 0.569f, 0.647f, 1.0f); // #8091a5 + + UPROPERTY(EditAnywhere, Category = "Colors|Text") + FLinearColor TextDisabled = FLinearColor(0.353f, 0.416f, 0.494f, 1.0f); // #5a6a7e + + UPROPERTY(EditAnywhere, Category = "Colors|Accent") + FLinearColor Accent = FLinearColor(1.0f, 0.416f, 0.239f, 1.0f); // #ff6a3d + + UPROPERTY(EditAnywhere, Category = "Colors|Accent") + FLinearColor Accent2 = FLinearColor(1.0f, 0.627f, 0.420f, 1.0f); // #ffa06b + + UPROPERTY(EditAnywhere, Category = "Colors|Accent") + FLinearColor AccentSoft = FLinearColor(1.0f, 0.416f, 0.239f, 0.14f); + + UPROPERTY(EditAnywhere, Category = "Colors|Accent") + FLinearColor AccentRing = FLinearColor(1.0f, 0.416f, 0.239f, 0.35f); + + UPROPERTY(EditAnywhere, Category = "Colors|Status") + FLinearColor Ok = FLinearColor(0.243f, 0.812f, 0.557f, 1.0f); // #3ecf8e + + UPROPERTY(EditAnywhere, Category = "Colors|Status") + FLinearColor Warn = FLinearColor(0.961f, 0.725f, 0.267f, 1.0f); // #f5b944 + + UPROPERTY(EditAnywhere, Category = "Colors|Status") + FLinearColor Err = FLinearColor(1.0f, 0.353f, 0.431f, 1.0f); // #ff5a6e + + UPROPERTY(EditAnywhere, Category = "Colors|Status") + FLinearColor Info = FLinearColor(0.290f, 0.659f, 1.0f, 1.0f); // #4aa8ff (z) + + UPROPERTY(EditAnywhere, Category = "Colors|Axis") + FLinearColor AxisX = FLinearColor(1.0f, 0.302f, 0.369f, 1.0f); // #ff4d5e + + UPROPERTY(EditAnywhere, Category = "Colors|Axis") + FLinearColor AxisY = FLinearColor(0.290f, 0.839f, 0.427f, 1.0f); // #4ad66d + + UPROPERTY(EditAnywhere, Category = "Colors|Axis") + FLinearColor AxisZ = FLinearColor(0.290f, 0.659f, 1.0f, 1.0f); // #4aa8ff + + // ==================== FONTS ==================== + UPROPERTY(EditAnywhere, Category = "Fonts") + int32 FontSizeXSmall = 9; + + UPROPERTY(EditAnywhere, Category = "Fonts") + int32 FontSizeSmall = 10; + + UPROPERTY(EditAnywhere, Category = "Fonts") + int32 FontSizeBody = 11; + + UPROPERTY(EditAnywhere, Category = "Fonts") + int32 FontSizeHeading = 13; + + UPROPERTY(EditAnywhere, Category = "Fonts") + int32 FontSizeLarge = 16; + + // ==================== LAYOUT ==================== + // Spec radii (halved from v1 → matches the 9-slice radii) + UPROPERTY(EditAnywhere, Category = "Layout") + float RadiusXS = 3.0f; + + UPROPERTY(EditAnywhere, Category = "Layout") + float RadiusS = 5.0f; + + UPROPERTY(EditAnywhere, Category = "Layout") + float RadiusM = 7.0f; + + UPROPERTY(EditAnywhere, Category = "Layout") + float RadiusL = 9.0f; + + UPROPERTY(EditAnywhere, Category = "Layout") + float RadiusXL = 12.0f; + + UPROPERTY(EditAnywhere, Category = "Layout") + float PadPanel = 12.0f; + + UPROPERTY(EditAnywhere, Category = "Layout") + float PadRow = 6.0f; + + UPROPERTY(EditAnywhere, Category = "Layout") + float PadTight = 4.0f; + + // ==================== ICONS ==================== + UPROPERTY(EditAnywhere, Category = "Icons|Transform") TObjectPtr IconMove; + UPROPERTY(EditAnywhere, Category = "Icons|Transform") TObjectPtr IconRotate; + UPROPERTY(EditAnywhere, Category = "Icons|Transform") TObjectPtr IconScale; + UPROPERTY(EditAnywhere, Category = "Icons|Transform") TObjectPtr IconSnap; + UPROPERTY(EditAnywhere, Category = "Icons|Transform") TObjectPtr IconGrid; + UPROPERTY(EditAnywhere, Category = "Icons|Transform") TObjectPtr IconWorld; + UPROPERTY(EditAnywhere, Category = "Icons|Transform") TObjectPtr IconFocus; + + UPROPERTY(EditAnywhere, Category = "Icons|Playback") TObjectPtr IconPlay; + UPROPERTY(EditAnywhere, Category = "Icons|Playback") TObjectPtr IconPause; + UPROPERTY(EditAnywhere, Category = "Icons|Playback") TObjectPtr IconStop; + UPROPERTY(EditAnywhere, Category = "Icons|Playback") TObjectPtr IconRecord; + UPROPERTY(EditAnywhere, Category = "Icons|Playback") TObjectPtr IconSkipBack; + UPROPERTY(EditAnywhere, Category = "Icons|Playback") TObjectPtr IconSkipForward; + UPROPERTY(EditAnywhere, Category = "Icons|Playback") TObjectPtr IconSimulate; + + UPROPERTY(EditAnywhere, Category = "Icons|View") TObjectPtr IconEye; + UPROPERTY(EditAnywhere, Category = "Icons|View") TObjectPtr IconEyeOff; + UPROPERTY(EditAnywhere, Category = "Icons|View") TObjectPtr IconLock; + UPROPERTY(EditAnywhere, Category = "Icons|View") TObjectPtr IconUnlock; + UPROPERTY(EditAnywhere, Category = "Icons|View") TObjectPtr IconSidebar; + UPROPERTY(EditAnywhere, Category = "Icons|View") TObjectPtr IconPanelRight; + UPROPERTY(EditAnywhere, Category = "Icons|View") TObjectPtr IconPanelBottom; + + UPROPERTY(EditAnywhere, Category = "Icons|File") TObjectPtr IconSave; + UPROPERTY(EditAnywhere, Category = "Icons|File") TObjectPtr IconLoad; + UPROPERTY(EditAnywhere, Category = "Icons|File") TObjectPtr IconFolder; + UPROPERTY(EditAnywhere, Category = "Icons|File") TObjectPtr IconNew; + + UPROPERTY(EditAnywhere, Category = "Icons|Edit") TObjectPtr IconPlus; + UPROPERTY(EditAnywhere, Category = "Icons|Edit") TObjectPtr IconMinus; + UPROPERTY(EditAnywhere, Category = "Icons|Edit") TObjectPtr IconDelete; + UPROPERTY(EditAnywhere, Category = "Icons|Edit") TObjectPtr IconDuplicate; + UPROPERTY(EditAnywhere, Category = "Icons|Edit") TObjectPtr IconUndo; + UPROPERTY(EditAnywhere, Category = "Icons|Edit") TObjectPtr IconRedo; + UPROPERTY(EditAnywhere, Category = "Icons|Edit") TObjectPtr IconCheck; + UPROPERTY(EditAnywhere, Category = "Icons|Edit") TObjectPtr IconClose; + + UPROPERTY(EditAnywhere, Category = "Icons|Nav") TObjectPtr IconSearch; + UPROPERTY(EditAnywhere, Category = "Icons|Nav") TObjectPtr IconSettings; + UPROPERTY(EditAnywhere, Category = "Icons|Nav") TObjectPtr IconChevronDown; + UPROPERTY(EditAnywhere, Category = "Icons|Nav") TObjectPtr IconChevronRight; + UPROPERTY(EditAnywhere, Category = "Icons|Nav") TObjectPtr IconChevronLeft; + UPROPERTY(EditAnywhere, Category = "Icons|Nav") TObjectPtr IconCommand; + UPROPERTY(EditAnywhere, Category = "Icons|Nav") TObjectPtr IconMore; + UPROPERTY(EditAnywhere, Category = "Icons|Nav") TObjectPtr IconHelp; + UPROPERTY(EditAnywhere, Category = "Icons|Nav") TObjectPtr IconBell; + UPROPERTY(EditAnywhere, Category = "Icons|Nav") TObjectPtr IconDownload; + + UPROPERTY(EditAnywhere, Category = "Icons|Objects") TObjectPtr IconCharacter; + UPROPERTY(EditAnywhere, Category = "Icons|Objects") TObjectPtr IconSpline; + UPROPERTY(EditAnywhere, Category = "Icons|Objects") TObjectPtr IconProp; + UPROPERTY(EditAnywhere, Category = "Icons|Objects") TObjectPtr IconLight; + UPROPERTY(EditAnywhere, Category = "Icons|Objects") TObjectPtr IconCamera; + UPROPERTY(EditAnywhere, Category = "Icons|Objects") TObjectPtr IconTrigger; + UPROPERTY(EditAnywhere, Category = "Icons|Objects") TObjectPtr IconLayers; + UPROPERTY(EditAnywhere, Category = "Icons|Objects") TObjectPtr IconFilm; + UPROPERTY(EditAnywhere, Category = "Icons|Objects") TObjectPtr IconBox; + UPROPERTY(EditAnywhere, Category = "Icons|Objects") TObjectPtr IconFlag; + UPROPERTY(EditAnywhere, Category = "Icons|Objects") TObjectPtr IconMap; + + UPROPERTY(EditAnywhere, Category = "Icons|Brand") TObjectPtr IconBrandMark; + + /** Resolve an icon slot to texture + unicode fallback glyph. Never returns nullptr struct. */ + FPS_Editor_IconData GetIcon(EPS_Editor_Icon Icon) const; +}; diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.cpp index 370ee4e..00fc42b 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.cpp @@ -32,6 +32,13 @@ #include "PS_Editor_GameInstanceSubsystem.h" #include "PS_Editor_TimelineSubsystem.h" #include "Engine/World.h" +#include "PS_Editor_UIStyleAsset.h" +#include "PS_Editor_UIStyle.h" +#include "Widgets/Layout/SBox.h" +#include "Framework/Application/SlateApplication.h" +#include "InputAction.h" +#include "TimerManager.h" +#include "Widgets/Input/SMenuAnchor.h" // Forward declaration — the real definition lives further down with the timeline UI block, // but the toolbar Simulate button + NativeTick both need to look up the subsystem too. @@ -52,8 +59,119 @@ void UPS_Editor_MainWidget::SetSceneSerializer(UPS_Editor_SceneSerializer* InSer SceneSerializer = InSerializer; } +void UPS_Editor_MainWidget::EnsureStyleResolved() +{ + if (!StyleAsset) + { + static const TCHAR* Candidates[] = + { + TEXT("/PS_Editor/Style/DA_PS_Editor_UIStyle.DA_PS_Editor_UIStyle"), + TEXT("/Game/PS_Editor/Style/DA_PS_Editor_UIStyle.DA_PS_Editor_UIStyle"), + TEXT("/Game/Style/DA_PS_Editor_UIStyle.DA_PS_Editor_UIStyle"), + }; + for (const TCHAR* Path : Candidates) + { + FSoftObjectPath SoftPath(Path); + if (UObject* Loaded = SoftPath.TryLoad()) + { + StyleAsset = Cast(Loaded); + if (StyleAsset) { break; } + } + } + } + + // No user-authored asset — spin up a transient one with defaults. + if (!StyleAsset) + { + StyleAsset = NewObject(this); + } + + // Auto-populate icons from disk for any slot the user hasn't assigned yet. + FPS_Editor_UIStyle::PopulateIconsFromPluginContent(StyleAsset); + + EnsureStyleBrushes(); +} + +void UPS_Editor_MainWidget::EnsureStyleBrushes() +{ + if (BrushGlassStrong.IsValid()) return; + + const UPS_Editor_UIStyleAsset* S = StyleAsset; + + // Panels: 9-slice panel_rounded_small.png (12 px radius) for all HUDs — matches the design. + BrushGlassStrong = FPS_Editor_UIStyle::Load9SliceBrushFromPluginContent(TEXT("panel_rounded_small.png")); + if (BrushGlassStrong.IsValid()) + { + BrushGlassStrong->TintColor = FSlateColor(FPS_Editor_UIStyle::GlassStrong(S)); + } + else + { + UE_LOG(LogTemp, Warning, TEXT("PS_Editor: panel_rounded_small.png missing from Content/UI/unreal_9slice")); + BrushGlassStrong = MakeShared(); + } + + BrushGlassSoft = FPS_Editor_UIStyle::Load9SliceBrushFromPluginContent(TEXT("panel_rounded_small.png")); + if (BrushGlassSoft.IsValid()) + { + BrushGlassSoft->TintColor = FSlateColor(FPS_Editor_UIStyle::GlassSoft(S)); + } + else + { + BrushGlassSoft = MakeShared(); + } + + // Small utility brushes (pill, field, accent) stay procedural — no 9-slice asset needed for them. + BrushPill = MakeShared(); + FPS_Editor_UIStyle::InitPanelBrush(*BrushPill, S, FPS_Editor_UIStyle::EPanelTone::Pill, 999.0f); + + BrushField = MakeShared(); + FPS_Editor_UIStyle::InitPanelBrush(*BrushField, S, FPS_Editor_UIStyle::EPanelTone::FieldFill, FPS_Editor_UIStyle::RadiusS(S)); + + BrushAccent = MakeShared(); + FPS_Editor_UIStyle::InitPanelBrush(*BrushAccent, S, FPS_Editor_UIStyle::EPanelTone::AccentFill, FPS_Editor_UIStyle::RadiusS(S)); + + BrushAccentSoft = MakeShared(); + FPS_Editor_UIStyle::InitPanelBrush(*BrushAccentSoft, S, FPS_Editor_UIStyle::EPanelTone::AccentSoftFill, FPS_Editor_UIStyle::RadiusS(S)); + + // Flat button: 9-slice "button_small_*" tinted for subtle hover (white with alpha steps). + StyleFlatButton = MakeShared(); + const FLinearColor FlatNormal(1.f, 1.f, 1.f, 0.0f); + const FLinearColor FlatHover(1.f, 1.f, 1.f, 0.08f); + const FLinearColor FlatPressed(1.f, 1.f, 1.f, 0.14f); + if (!FPS_Editor_UIStyle::InitNineSliceButtonStyle(*StyleFlatButton, S, + TEXT("button_small_normal"), TEXT("button_small_hover"), TEXT("button_small_pressed"), + FlatNormal, FlatHover, FlatPressed)) + { + UE_LOG(LogTemp, Warning, TEXT("PS_Editor: button_small_*.png missing")); + } + + // Accent button: 9-slice "button_big_*" tinted with the accent color palette. + StyleAccentButton = MakeShared(); + const FLinearColor AccentC = FPS_Editor_UIStyle::Accent(S); + const FLinearColor AccentHov(FMath::Min(AccentC.R * 1.08f, 1.f), FMath::Min(AccentC.G * 1.08f, 1.f), FMath::Min(AccentC.B * 1.08f, 1.f), AccentC.A); + const FLinearColor AccentDim(AccentC.R * 0.88f, AccentC.G * 0.88f, AccentC.B * 0.88f, AccentC.A); + if (FPS_Editor_UIStyle::InitNineSliceButtonStyle(*StyleAccentButton, S, + TEXT("button_big_normal"), TEXT("button_big_hover"), TEXT("button_big_pressed"), + AccentC, AccentHov, AccentDim)) + { + const FLinearColor DarkOnAccent(0.10f, 0.04f, 0.0f, 1.0f); + StyleAccentButton->NormalForeground = FSlateColor(DarkOnAccent); + StyleAccentButton->HoveredForeground = FSlateColor(DarkOnAccent); + StyleAccentButton->PressedForeground = FSlateColor(DarkOnAccent); + } + else + { + UE_LOG(LogTemp, Warning, TEXT("PS_Editor: button_big_*.png missing")); + } + + StyleTextBox = MakeShared(); + FPS_Editor_UIStyle::InitTextBoxStyle(*StyleTextBox, S, 4.0f); +} + TSharedRef UPS_Editor_MainWidget::RebuildWidget() { + EnsureStyleResolved(); + return SNew(SOverlay) // Loading screen (behind everything — first slot = bottom layer) + SOverlay::Slot() @@ -76,304 +194,235 @@ TSharedRef UPS_Editor_MainWidget::RebuildWidget() ] ] ] - // Top-left: Title + Toolbar + + // ============= TOP BAR (full-width, 40px, floating) ============= + SOverlay::Slot() - .HAlign(HAlign_Left) + .HAlign(HAlign_Fill) .VAlign(VAlign_Top) - .Padding(16.0f) + .Padding(10.0f, 10.0f, 10.0f, 0.0f) [ - SNew(SVerticalBox) - // Title bar - + SVerticalBox::Slot() - .AutoHeight() - [ - SNew(SBorder) - .BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush")) - .BorderBackgroundColor(FLinearColor(0.05f, 0.05f, 0.05f, 1.0f)) - .Padding(FMargin(12.0f, 8.0f)) - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot() - .AutoWidth() - .VAlign(VAlign_Center) - [ - SNew(STextBlock) - .Text(FText::FromString(TEXT("PS Editor"))) - .ColorAndOpacity(FLinearColor(0.2f, 0.7f, 1.0f, 1.0f)) - .Font(FCoreStyle::GetDefaultFontStyle("Bold", 14)) - ] - + SHorizontalBox::Slot() - .AutoWidth() - .VAlign(VAlign_Center) - .Padding(12.0f, 0.0f, 0.0f, 0.0f) - [ - SAssignNew(ModeText, STextBlock) - .Text(FText::FromString(TEXT("Translate (Z)"))) - .ColorAndOpacity(FLinearColor(0.7f, 0.7f, 0.7f, 1.0f)) - .Font(FCoreStyle::GetDefaultFontStyle("Regular", 11)) - ] - + SHorizontalBox::Slot() - .FillWidth(1.0f) - [ - SNullWidget::NullWidget - ] - + SHorizontalBox::Slot() - .AutoWidth() - .VAlign(VAlign_Center) - .Padding(8.0f, 0.0f, 0.0f, 0.0f) - [ - SNew(SButton) - .OnClicked_Lambda([this]() -> FReply - { - OnCloseClicked(); - return FReply::Handled(); - }) - [ - SNew(STextBlock) - .Text(FText::FromString(TEXT("X"))) - .Font(FCoreStyle::GetDefaultFontStyle("Bold", 12)) - .ColorAndOpacity(FLinearColor(1.0f, 0.3f, 0.3f)) - ] - ] - ] - ] - // Toolbar - + SVerticalBox::Slot() - .AutoHeight() - .Padding(0.0f, 4.0f, 0.0f, 0.0f) - [ - BuildToolbar() - ] - // Scene save/load - + SVerticalBox::Slot() - .AutoHeight() - .Padding(0.0f, 4.0f, 0.0f, 0.0f) - [ - BuildSceneButtons() - ] + BuildTopbar() ] - // Left panel: Spawn Catalogue + + // ============= FLOATING TOOLBAR (center-top) ============= + SOverlay::Slot() - .HAlign(HAlign_Left) - .VAlign(VAlign_Center) - .Padding(16.0f, 80.0f, 0.0f, 60.0f) + .HAlign(HAlign_Center) + .VAlign(VAlign_Top) + .Padding(0.0f, 62.0f, 0.0f, 0.0f) [ - BuildSpawnCatalog() + BuildToolbar() ] - // Right panel: Properties + Outliner + + // ============= VIEW CUBE (top-right) ============= + SOverlay::Slot() .HAlign(HAlign_Right) .VAlign(VAlign_Top) - .Padding(16.0f, 16.0f, 16.0f, 60.0f) + .Padding(0.0f, 62.0f, 10.0f, 0.0f) [ - SNew(SVerticalBox) - + SVerticalBox::Slot() - .AutoHeight() + BuildViewCube() + ] + + // ============= OUTLINER (left, top-to-middle) ============= + + SOverlay::Slot() + .HAlign(HAlign_Left) + .VAlign(VAlign_Top) + .Padding(10.0f, 62.0f, 0.0f, 0.0f) + [ + SNew(SBox) + .WidthOverride(260.0f) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot().AutoHeight() + [ + BuildOutliner() + ] + + SVerticalBox::Slot().AutoHeight().Padding(0.0f, 6.0f, 0.0f, 0.0f) + [ + BuildSceneButtons() + ] + + SVerticalBox::Slot().AutoHeight().Padding(0.0f, 6.0f, 0.0f, 0.0f) + [ + BuildSpawnCatalog() + ] + ] + ] + + // ============= INSPECTOR (right, top-to-middle) ============= + + SOverlay::Slot() + .HAlign(HAlign_Right) + .VAlign(VAlign_Top) + .Padding(0.0f, 62.0f, 10.0f, 0.0f) + [ + SNew(SBox) + .WidthOverride(290.0f) + .Padding(FMargin(0.0f, 72.0f, 0.0f, 0.0f)) // push below view cube [ BuildPropertiesPanel() ] - + SVerticalBox::Slot() - .FillHeight(1.0f) - .Padding(0.0f, 4.0f, 0.0f, 0.0f) - [ - BuildOutliner() - ] ] - // Timeline panel (hidden by default; toggled via toolbar button) + + // ============= TIMELINE (bottom, fill) ============= + SOverlay::Slot() .HAlign(HAlign_Fill) .VAlign(VAlign_Bottom) - .Padding(16.0f, 0.0f, 16.0f, 60.0f) + .Padding(10.0f, 0.0f, 10.0f, 42.0f) [ BuildTimelinePanel() ] - // Bottom notification / help bar + + // ============= STATS HUD (bottom-left) ============= + + SOverlay::Slot() + .HAlign(HAlign_Left) + .VAlign(VAlign_Bottom) + .Padding(10.0f) + [ + BuildStatsHUD() + ] + + // ============= HINT HUD (bottom-right) ============= + + SOverlay::Slot() + .HAlign(HAlign_Right) + .VAlign(VAlign_Bottom) + .Padding(10.0f) + [ + BuildHintHUD() + ] + + // ============= SPLINE EDIT BAR (bottom-center, conditional) ============= + SOverlay::Slot() .HAlign(HAlign_Center) .VAlign(VAlign_Bottom) - .Padding(16.0f) + .Padding(0.0f, 0.0f, 0.0f, 80.0f) [ - SNew(SBorder) - .BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush")) - .BorderBackgroundColor(FLinearColor(0.05f, 0.05f, 0.05f, 1.0f)) - .Padding(FMargin(16.0f, 6.0f)) - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot().FillWidth(1.0f).VAlign(VAlign_Center) - [ - SAssignNew(HelpBarText, STextBlock) - .Text(FText::FromString(TEXT("LMB: Select/Pan | RMB: Fly | Alt+LMB/MMB: Orbit | Z/E/R: Translate/Rotate/Scale | End: Snap | Del: Delete | S: Spline"))) - .ColorAndOpacity(FLinearColor(0.6f, 0.6f, 0.6f, 1.0f)) - .Font(FCoreStyle::GetDefaultFontStyle("Regular", 9)) - ] - + SHorizontalBox::Slot().AutoWidth().Padding(12.0f, 0.0f, 0.0f, 0.0f).VAlign(VAlign_Center) - [ - SAssignNew(SplineEditButton, SButton) - .Visibility(EVisibility::Collapsed) - .OnClicked_Lambda([this]() -> FReply - { - if (APS_Editor_PlayerController* EditorPC = Cast(GetOwningPlayer())) - { - if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement) - { - EditorPC->FinishSplineEditing(); - } - else if (UPS_Editor_SelectionManager* SelMgr = SelectionManager.Get()) - { - for (const TWeakObjectPtr& Weak : SelMgr->GetSelectedActors()) - { - if (Cast(Weak.Get())) - { - EditorPC->BeginSplineEditing(Weak.Get()); - break; - } - } - } - } - return FReply::Handled(); - }) - [ - SNew(STextBlock) - .Text(FText::FromString(TEXT("Edit Spline"))) - .Font(FCoreStyle::GetDefaultFontStyle("Bold", 10)) - .ColorAndOpacity(FLinearColor(1.0f, 0.6f, 0.0f)) - ] - ] - + SHorizontalBox::Slot().AutoWidth().Padding(8.0f, 0.0f, 0.0f, 0.0f).VAlign(VAlign_Center) - [ - SAssignNew(ChildEditButton, SButton) - .Visibility(EVisibility::Collapsed) - .OnClicked_Lambda([this]() -> FReply - { - if (APS_Editor_PlayerController* EditorPC = Cast(GetOwningPlayer())) - { - if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement) - { - EditorPC->FinishPointPlacement(); - } - else if (UPS_Editor_SelectionManager* SelMgr = SelectionManager.Get()) - { - for (const TWeakObjectPtr& Weak : SelMgr->GetSelectedActors()) - { - if (Weak.IsValid() && Weak->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass())) - { - EditorPC->BeginPointPlacementAppend(Weak.Get()); - break; - } - } - } - } - return FReply::Handled(); - }) - [ - SNew(STextBlock) - .Text(FText::FromString(TEXT("Edit Children"))) - .Font(FCoreStyle::GetDefaultFontStyle("Bold", 10)) - .ColorAndOpacity(FLinearColor(0.3f, 0.8f, 1.0f)) - ] - ] - + SHorizontalBox::Slot().AutoWidth().Padding(8.0f, 0.0f, 0.0f, 0.0f).VAlign(VAlign_Center) - [ - SAssignNew(SplineDeletePointButton, SButton) - .Visibility(EVisibility::Collapsed) - .OnClicked_Lambda([this]() -> FReply - { - // Simulate Delete key press for the selected spline point - if (APS_Editor_Pawn* Pawn = Cast(GetOwningPlayerPawn())) - { - Pawn->HandleDelete(FInputActionValue()); - } - return FReply::Handled(); - }) - [ - SNew(STextBlock) - .Text(FText::FromString(TEXT("Delete Point"))) - .Font(FCoreStyle::GetDefaultFontStyle("Bold", 10)) - .ColorAndOpacity(FLinearColor(1.0f, 0.3f, 0.2f)) - ] - ] - + SHorizontalBox::Slot().AutoWidth().Padding(8.0f, 0.0f, 0.0f, 0.0f).VAlign(VAlign_Center) - [ - SAssignNew(SplineFinishButton, SButton) - .Visibility(EVisibility::Collapsed) - .OnClicked_Lambda([this]() -> FReply - { - if (APS_Editor_PlayerController* EditorPC = Cast(GetOwningPlayer())) - { - if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement) - EditorPC->FinishPointPlacement(); - else if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement) - EditorPC->FinishSplineEditing(); - } - return FReply::Handled(); - }) - [ - SNew(STextBlock) - .Text(FText::FromString(TEXT("Done"))) - .Font(FCoreStyle::GetDefaultFontStyle("Bold", 10)) - .ColorAndOpacity(FLinearColor(0.3f, 0.9f, 0.3f)) - ] - ] - ] + BuildSplineEditBar() ] - // BaseLevel selection popup + + // ============= TOAST (bottom-center) ============= + + SOverlay::Slot() + .HAlign(HAlign_Center) + .VAlign(VAlign_Bottom) + .Padding(0.0f, 0.0f, 0.0f, 160.0f) + [ + BuildToast() + ] + + // New Scenario modal (BaseLevel selection) + SOverlay::Slot() .HAlign(HAlign_Center) .VAlign(VAlign_Center) [ SAssignNew(BaseLevelPopup, SBorder) .Visibility(EVisibility::Collapsed) - .BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush")) - .BorderBackgroundColor(FLinearColor(0.08f, 0.08f, 0.08f, 0.97f)) - .Padding(FMargin(24.0f, 16.0f)) + .BorderImage(BrushGlassStrong.Get()) + .Padding(FMargin(0.0f)) [ - SNew(SVerticalBox) - + SVerticalBox::Slot().AutoHeight().HAlign(HAlign_Center).Padding(0.0f, 0.0f, 0.0f, 12.0f) + SNew(SBox) + .WidthOverride(720.0f) [ - SNew(STextBlock) - .Text(FText::FromString(TEXT("Select Base Level"))) - .Font(FCoreStyle::GetDefaultFontStyle("Bold", 14)) - .ColorAndOpacity(FLinearColor::White) - ] - + SVerticalBox::Slot().FillHeight(1.0f).MaxHeight(400.0f) - [ - SNew(SScrollBox) - .ConsumeMouseWheel(EConsumeMouseWheel::Always) - + SScrollBox::Slot() + SNew(SVerticalBox) + // Header with title + minus close button + + SVerticalBox::Slot().AutoHeight().Padding(FMargin(18.0f, 14.0f)) [ - SAssignNew(BaseLevelGridContainer, SVerticalBox) - ] - ] - + SVerticalBox::Slot().AutoHeight().HAlign(HAlign_Center).Padding(0.0f, 12.0f, 0.0f, 0.0f) - [ - SNew(SHorizontalBox) - + SHorizontalBox::Slot().AutoWidth().Padding(8.0f, 0.0f) - [ - SNew(SButton) - .OnClicked_Lambda([this]() -> FReply - { - ConfirmBaseLevelSelection(); - return FReply::Handled(); - }) + SNew(SHorizontalBox) + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0.0f, 0.0f, 10.0f, 0.0f) + [ + FPS_Editor_UIStyle::MakeIconFollowForeground(StyleAsset, EPS_Editor_Icon::Plus, 14.0f) + ] + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center) [ SNew(STextBlock) - .Text(FText::FromString(TEXT("Validate"))) - .Font(FCoreStyle::GetDefaultFontStyle("Bold", 11)) - .ColorAndOpacity(FLinearColor(0.2f, 0.9f, 0.2f)) + .Text(FText::FromString(TEXT("New Scenario"))) + .Font(FPS_Editor_UIStyle::HeadingFontBold(StyleAsset)) + .ColorAndOpacity(FPS_Editor_UIStyle::TextPrimary(StyleAsset)) + ] + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(8.0f, 0.0f, 0.0f, 0.0f) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("\u00B7 Select a base level"))) + .Font(FPS_Editor_UIStyle::BodyFont(StyleAsset)) + .ColorAndOpacity(FPS_Editor_UIStyle::TextDim(StyleAsset)) + ] + + SHorizontalBox::Slot().FillWidth(1.0f) + [ SNullWidget::NullWidget ] + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center) + [ + SNew(SButton) + .ButtonStyle(StyleFlatButton.Get()) + .ContentPadding(FMargin(6.0f, 3.0f)) + .OnClicked_Lambda([this]() -> FReply { CancelBaseLevelPopup(); return FReply::Handled(); }) + [ + FPS_Editor_UIStyle::MakeIconFollowForeground(StyleAsset, EPS_Editor_Icon::Minus, 12.0f) + ] ] ] - + SHorizontalBox::Slot().AutoWidth().Padding(8.0f, 0.0f) + // Separator + + SVerticalBox::Slot().AutoHeight() [ - SNew(SButton) - .OnClicked_Lambda([this]() -> FReply - { - CancelBaseLevelPopup(); - return FReply::Handled(); - }) + SNew(SBox).HeightOverride(1.0f) [ - SNew(STextBlock) - .Text(FText::FromString(TEXT("Cancel"))) - .Font(FCoreStyle::GetDefaultFontStyle("Regular", 11)) + SNew(SBorder) + .BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush")) + .BorderBackgroundColor(FPS_Editor_UIStyle::GlassBorder(StyleAsset)) + ] + ] + // Grid area + + SVerticalBox::Slot().FillHeight(1.0f).Padding(14.0f).MaxHeight(480.0f) + [ + SNew(SScrollBox) + .ConsumeMouseWheel(EConsumeMouseWheel::Always) + + SScrollBox::Slot() + [ + SAssignNew(BaseLevelGridContainer, SVerticalBox) + ] + ] + // Footer separator + + SVerticalBox::Slot().AutoHeight() + [ + SNew(SBox).HeightOverride(1.0f) + [ + SNew(SBorder) + .BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush")) + .BorderBackgroundColor(FPS_Editor_UIStyle::GlassBorder(StyleAsset)) + ] + ] + // Footer buttons + + SVerticalBox::Slot().AutoHeight().Padding(FMargin(12.0f)) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot().FillWidth(1.0f) [ SNullWidget::NullWidget ] + + SHorizontalBox::Slot().AutoWidth().Padding(0.0f, 0.0f, 8.0f, 0.0f) + [ + SNew(SButton) + .ButtonStyle(StyleFlatButton.Get()) + .ContentPadding(FMargin(14.0f, 6.0f)) + .OnClicked_Lambda([this]() -> FReply { CancelBaseLevelPopup(); return FReply::Handled(); }) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Cancel"))) + .Font(FPS_Editor_UIStyle::BodyFont(StyleAsset)) + .ColorAndOpacity(FPS_Editor_UIStyle::TextMuted(StyleAsset)) + ] + ] + + SHorizontalBox::Slot().AutoWidth() + [ + SNew(SButton) + .ButtonStyle(StyleAccentButton.Get()) + .ContentPadding(FMargin(14.0f, 6.0f)) + .OnClicked_Lambda([this]() -> FReply { ConfirmBaseLevelSelection(); return FReply::Handled(); }) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0.0f, 0.0f, 6.0f, 0.0f) + [ + FPS_Editor_UIStyle::MakeIcon(StyleAsset, EPS_Editor_Icon::Plus, 12.0f, FLinearColor(0.10f, 0.04f, 0.0f, 1.0f)) + ] + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Create scenario"))) + .Font(FPS_Editor_UIStyle::BodyFontBold(StyleAsset)) + .ColorAndOpacity(FLinearColor(0.10f, 0.04f, 0.0f, 1.0f)) + ] + ] ] ] ] @@ -386,8 +435,7 @@ TSharedRef UPS_Editor_MainWidget::RebuildWidget() [ SAssignNew(CloseConfirmOverlay, SBorder) .Visibility(EVisibility::Collapsed) - .BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush")) - .BorderBackgroundColor(FLinearColor(0.08f, 0.08f, 0.08f, 0.97f)) + .BorderImage(BrushGlassStrong.Get()) .Padding(FMargin(32.0f, 20.0f)) [ SNew(SVerticalBox) @@ -404,6 +452,7 @@ TSharedRef UPS_Editor_MainWidget::RebuildWidget() + SHorizontalBox::Slot().AutoWidth().Padding(6.0f, 0.0f) [ SNew(SButton) + .ButtonStyle(StyleFlatButton.Get()) .OnClicked_Lambda([this]() -> FReply { ExecuteClose(true); @@ -419,6 +468,7 @@ TSharedRef UPS_Editor_MainWidget::RebuildWidget() + SHorizontalBox::Slot().AutoWidth().Padding(6.0f, 0.0f) [ SNew(SButton) + .ButtonStyle(StyleFlatButton.Get()) .OnClicked_Lambda([this]() -> FReply { ExecuteClose(false); @@ -434,6 +484,7 @@ TSharedRef UPS_Editor_MainWidget::RebuildWidget() + SHorizontalBox::Slot().AutoWidth().Padding(6.0f, 0.0f) [ SNew(SButton) + .ButtonStyle(StyleFlatButton.Get()) .OnClicked_Lambda([this]() -> FReply { if (CloseConfirmOverlay.IsValid()) @@ -448,100 +499,215 @@ TSharedRef UPS_Editor_MainWidget::RebuildWidget() ] ] ] + ] + + // ============= COMMAND PALETTE (modal, Ctrl+K) ============= + + SOverlay::Slot() + .HAlign(HAlign_Center) + .VAlign(VAlign_Top) + .Padding(0.0f, 120.0f, 0.0f, 0.0f) + [ + BuildCommandPalette() ]; } TSharedRef UPS_Editor_MainWidget::BuildToolbar() { - auto MakeButton = [this](const FText& Label, EPS_Editor_TransformMode Mode) -> TSharedRef + const UPS_Editor_UIStyleAsset* S = StyleAsset; + + // Tool button: icon + label + keybind chip. Background switches to accent when the mode is active. + auto MakeToolButton = [this, S](EPS_Editor_Icon Icon, const FString& Label, const FString& Key, EPS_Editor_TransformMode Mode) -> TSharedRef + { + auto GetBrush = [this, Mode]() -> const FSlateBrush* + { + UPS_Editor_SelectionManager* SM = SelectionManager.Get(); + const bool bActive = SM && SM->GetTransformMode() == Mode; + return bActive ? BrushAccent.Get() : nullptr; + }; + auto GetTextColor = [this, S, Mode]() -> FSlateColor + { + UPS_Editor_SelectionManager* SM = SelectionManager.Get(); + const bool bActive = SM && SM->GetTransformMode() == Mode; + return bActive ? FSlateColor(FLinearColor(0.10f, 0.04f, 0.0f, 1.0f)) : FSlateColor(FPS_Editor_UIStyle::TextPrimary(S)); + }; + auto GetIconColor = [this, S, Mode]() -> FLinearColor + { + UPS_Editor_SelectionManager* SM = SelectionManager.Get(); + const bool bActive = SM && SM->GetTransformMode() == Mode; + return bActive ? FLinearColor(0.10f, 0.04f, 0.0f, 1.0f) : FPS_Editor_UIStyle::TextPrimary(S); + }; + + return SNew(SButton) + .ButtonStyle(StyleFlatButton.Get()) + .ContentPadding(FMargin(0.0f)) + .OnClicked_Lambda([this, Mode]() -> FReply { OnModeButtonClicked(Mode); return FReply::Handled(); }) + [ + SNew(SBorder) + .BorderImage_Lambda(GetBrush) + .Padding(FMargin(10.0f, 4.0f)) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0.0f, 0.0f, 6.0f, 0.0f) + [ + FPS_Editor_UIStyle::MakeIcon(S, Icon, 13.0f, GetIconColor()) + ] + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(FText::FromString(Label)) + .Font(FPS_Editor_UIStyle::BodyFont(S)) + .ColorAndOpacity_Lambda(GetTextColor) + ] + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(6.0f, 0.0f, 0.0f, 0.0f) + [ + SNew(SBorder) + .BorderImage(BrushField.Get()) + .Padding(FMargin(4.0f, 0.0f)) + [ + SNew(STextBlock) + .Text(FText::FromString(Key)) + .Font(FPS_Editor_UIStyle::SmallFont(S)) + .ColorAndOpacity(FPS_Editor_UIStyle::TextMuted(S)) + ] + ] + ] + ]; + }; + + // Toggle button: icon + label. Highlighted via a dynamic brush when bActive returns true. + // Two overlayed icons (A and B) are swapped based on IsActive() — covers World/Local, Snap on/off visual cues. + auto MakeToggleButton = [this, S](EPS_Editor_Icon IconWhenActive, EPS_Editor_Icon IconWhenInactive, TAttribute LabelAttr, TFunction IsActive, TFunction OnClick) -> TSharedRef { return SNew(SButton) - .OnClicked_Lambda([this, Mode]() -> FReply - { - OnModeButtonClicked(Mode); - return FReply::Handled(); - }) + .ButtonStyle(StyleFlatButton.Get()) + .ContentPadding(FMargin(0.0f)) + .OnClicked_Lambda([OnClick]() -> FReply { OnClick(); return FReply::Handled(); }) [ - SNew(STextBlock) - .Text(Label) - .Font(FCoreStyle::GetDefaultFontStyle("Regular", 10)) - .Justification(ETextJustify::Center) + SNew(SBorder) + .BorderImage_Lambda([this, IsActive]() -> const FSlateBrush* { return IsActive() ? BrushAccentSoft.Get() : nullptr; }) + .Padding(FMargin(10.0f, 4.0f)) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0.0f, 0.0f, 6.0f, 0.0f) + [ + SNew(SOverlay) + + SOverlay::Slot() + [ + SNew(SBox) + .Visibility_Lambda([IsActive]() -> EVisibility { return IsActive() ? EVisibility::Visible : EVisibility::Collapsed; }) + [ + FPS_Editor_UIStyle::MakeIcon(S, IconWhenActive, 12.0f, FPS_Editor_UIStyle::Accent(S)) + ] + ] + + SOverlay::Slot() + [ + SNew(SBox) + .Visibility_Lambda([IsActive]() -> EVisibility { return IsActive() ? EVisibility::Collapsed : EVisibility::Visible; }) + [ + FPS_Editor_UIStyle::MakeIcon(S, IconWhenInactive, 12.0f, FPS_Editor_UIStyle::TextMuted(S)) + ] + ] + ] + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text_Lambda([LabelAttr]() -> FText { return FText::FromString(LabelAttr.Get()); }) + .Font(FPS_Editor_UIStyle::BodyFont(S)) + .ColorAndOpacity(FPS_Editor_UIStyle::TextPrimary(S)) + ] + ] + ]; + }; + + auto MakeSeparator = [S]() -> TSharedRef + { + return SNew(SBox).WidthOverride(1.0f).HeightOverride(18.0f).Padding(FMargin(4.0f, 0.0f)) + [ + SNew(SBorder) + .BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush")) + .BorderBackgroundColor(FPS_Editor_UIStyle::GlassBorder(S)) ]; }; return SNew(SBorder) - .BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush")) - .BorderBackgroundColor(FLinearColor(0.05f, 0.05f, 0.05f, 1.0f)) - .Padding(FMargin(8.0f, 4.0f)) + .BorderImage(BrushGlassStrong.Get()) + .Padding(FMargin(4.0f)) [ SNew(SHorizontalBox) - + SHorizontalBox::Slot().AutoWidth().Padding(2.0f, 0.0f) - [ MakeButton(FText::FromString(TEXT("Translate (Z)")), EPS_Editor_TransformMode::Translate) ] - + SHorizontalBox::Slot().AutoWidth().Padding(2.0f, 0.0f) - [ MakeButton(FText::FromString(TEXT("Rotate (E)")), EPS_Editor_TransformMode::Rotate) ] - + SHorizontalBox::Slot().AutoWidth().Padding(2.0f, 0.0f) - [ MakeButton(FText::FromString(TEXT("Scale (R)")), EPS_Editor_TransformMode::Scale) ] - + SHorizontalBox::Slot().AutoWidth().Padding(8.0f, 0.0f, 2.0f, 0.0f) + + SHorizontalBox::Slot().AutoWidth() + [ MakeToolButton(EPS_Editor_Icon::Move, TEXT("Move"), TEXT("W"), EPS_Editor_TransformMode::Translate) ] + + SHorizontalBox::Slot().AutoWidth() + [ MakeToolButton(EPS_Editor_Icon::Rotate, TEXT("Rotate"), TEXT("E"), EPS_Editor_TransformMode::Rotate) ] + + SHorizontalBox::Slot().AutoWidth() + [ MakeToolButton(EPS_Editor_Icon::Scale, TEXT("Scale"), TEXT("R"), EPS_Editor_TransformMode::Scale) ] + + SHorizontalBox::Slot().AutoWidth() [ MakeSeparator() ] + + SHorizontalBox::Slot().AutoWidth() [ + // Snap: same icon (Snap) always, different label + highlight when active + MakeToggleButton(EPS_Editor_Icon::Snap, EPS_Editor_Icon::Snap, + TAttribute::CreateLambda([this]() -> FString + { + UPS_Editor_SelectionManager* SM = SelectionManager.Get(); + return (SM && SM->bSnapTranslation) ? TEXT("Snap On") : TEXT("Snap"); + }), + [this]() -> bool { UPS_Editor_SelectionManager* SM = SelectionManager.Get(); return SM && SM->bSnapTranslation; }, + [this]() + { + if (UPS_Editor_SelectionManager* SM = SelectionManager.Get()) + { + SM->bSnapTranslation = !SM->bSnapTranslation; + SM->bSnapRotation = SM->bSnapTranslation; + SM->bSnapScale = SM->bSnapTranslation; + } + }) + ] + + SHorizontalBox::Slot().AutoWidth() + [ + // World/Local: Globe when world, Target when local (per spec) + MakeToggleButton(EPS_Editor_Icon::World, EPS_Editor_Icon::Focus, + TAttribute::CreateLambda([this]() -> FString + { + UPS_Editor_SelectionManager* SM = SelectionManager.Get(); + return (SM && SM->GetTransformSpace() == EPS_Editor_Space::World) ? TEXT("World") : TEXT("Local"); + }), + [this]() -> bool { UPS_Editor_SelectionManager* SM = SelectionManager.Get(); return SM && SM->GetTransformSpace() == EPS_Editor_Space::World; }, + [this]() { if (UPS_Editor_SelectionManager* SM = SelectionManager.Get()) SM->ToggleTransformSpace(); }) + ] + + SHorizontalBox::Slot().AutoWidth() [ MakeSeparator() ] + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center) + [ + // Grid toggle (no active backend yet — UI only) SNew(SButton) + .ButtonStyle(StyleFlatButton.Get()) + .ContentPadding(FMargin(8.0f, 4.0f)) + .ToolTipText(FText::FromString(TEXT("Toggle grid"))) + [ + FPS_Editor_UIStyle::MakeIconFollowForeground(S, EPS_Editor_Icon::Grid, 13.0f) + ] + ] + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center) + [ + // Focus / Snap to ground + SNew(SButton) + .ButtonStyle(StyleFlatButton.Get()) + .ContentPadding(FMargin(8.0f, 4.0f)) + .ToolTipText(FText::FromString(TEXT("Snap to ground"))) .OnClicked_Lambda([this]() -> FReply { - if (UPS_Editor_SelectionManager* SM = SelectionManager.Get()) - { - SM->SnapSelectionToGround(); - } + if (UPS_Editor_SelectionManager* SM = SelectionManager.Get()) SM->SnapSelectionToGround(); return FReply::Handled(); }) [ - SNew(STextBlock) - .Text(FText::FromString(TEXT("Snap Ground"))) - .Font(FCoreStyle::GetDefaultFontStyle("Regular", 10)) + FPS_Editor_UIStyle::MakeIconFollowForeground(S, EPS_Editor_Icon::Focus, 13.0f) ] ] - // World/Local toggle - + SHorizontalBox::Slot().AutoWidth().Padding(8.0f, 0.0f, 2.0f, 0.0f) + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(2.0f, 0.0f, 0.0f, 0.0f) [ - SNew(SButton) - .OnClicked_Lambda([this]() -> FReply - { - if (UPS_Editor_SelectionManager* SM = SelectionManager.Get()) - { - SM->ToggleTransformSpace(); - } - return FReply::Handled(); - }) - [ - SAssignNew(SpaceText, STextBlock) - .Text(FText::FromString(TEXT("World"))) - .Font(FCoreStyle::GetDefaultFontStyle("Regular", 10)) - ] - ] - // Snap toggle + value - + SHorizontalBox::Slot().AutoWidth().Padding(8.0f, 0.0f, 2.0f, 0.0f) - [ - SNew(SButton) - .OnClicked_Lambda([this]() -> FReply - { - if (UPS_Editor_SelectionManager* SM = SelectionManager.Get()) - { - SM->bSnapTranslation = !SM->bSnapTranslation; - SM->bSnapRotation = SM->bSnapTranslation; - SM->bSnapScale = SM->bSnapTranslation; - } - return FReply::Handled(); - }) - [ - SAssignNew(SnapText, STextBlock) - .Text(FText::FromString(TEXT("Snap: Off"))) - .Font(FCoreStyle::GetDefaultFontStyle("Regular", 10)) - ] - ] - + SHorizontalBox::Slot().AutoWidth().Padding(2.0f, 0.0f).VAlign(VAlign_Center) - [ - SNew(SBox).MinDesiredWidth(40.0f) + SNew(SBox).MinDesiredWidth(36.0f) [ SAssignNew(SnapSizeField, SEditableTextBox) - .Font(FCoreStyle::GetDefaultFontStyle("Regular", 9)) + .Style(StyleTextBox.Get()) + .Font(FPS_Editor_UIStyle::SmallFont(S)) .HintText(FText::FromString(TEXT("10"))) .OnTextCommitted_Lambda([this](const FText& Text, ETextCommit::Type) { @@ -563,9 +729,10 @@ TSharedRef UPS_Editor_MainWidget::BuildToolbar() TSharedRef UPS_Editor_MainWidget::BuildPropertiesPanel() { + const UPS_Editor_UIStyleAsset* S = StyleAsset; const FMargin RowPadding(0.0f, 2.0f); - const FSlateFontInfo LabelFont = FCoreStyle::GetDefaultFontStyle("Regular", 9); - const FSlateFontInfo ValueFont = FCoreStyle::GetDefaultFontStyle("Regular", 9); + const FSlateFontInfo LabelFont = FPS_Editor_UIStyle::SmallFont(S); + const FSlateFontInfo ValueFont = FPS_Editor_UIStyle::SmallFont(S); const float LabelWidth = 14.0f; const float FieldWidth = 70.0f; @@ -589,6 +756,7 @@ TSharedRef UPS_Editor_MainWidget::BuildPropertiesPanel() .FillWidth(1.0f) [ SAssignNew(Field, SEditableTextBox) + .Style(StyleTextBox.Get()) .Font(ValueFont) .MinDesiredWidth(FieldWidth) .OnTextCommitted_Lambda([this](const FText&, ETextCommit::Type) @@ -615,13 +783,12 @@ TSharedRef UPS_Editor_MainWidget::BuildPropertiesPanel() [ MakeField(Z, ColorZ) ]; }; - const FLinearColor RedX(0.8f, 0.15f, 0.1f); - const FLinearColor GreenY(0.15f, 0.6f, 0.1f); - const FLinearColor BlueZ(0.1f, 0.3f, 0.8f); + const FLinearColor RedX = FPS_Editor_UIStyle::AxisX(S); + const FLinearColor GreenY = FPS_Editor_UIStyle::AxisY(S); + const FLinearColor BlueZ = FPS_Editor_UIStyle::AxisZ(S); return SNew(SBorder) - .BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush")) - .BorderBackgroundColor(FLinearColor(0.05f, 0.05f, 0.05f, 1.0f)) + .BorderImage(BrushGlassStrong.Get()) .Padding(FMargin(10.0f, 8.0f)) [ SNew(SVerticalBox) @@ -632,16 +799,16 @@ TSharedRef UPS_Editor_MainWidget::BuildPropertiesPanel() [ SAssignNew(SelectionInfoText, STextBlock) .Text(FText::FromString(TEXT("No selection"))) - .Font(FCoreStyle::GetDefaultFontStyle("Bold", 10)) - .ColorAndOpacity(FLinearColor(0.8f, 0.8f, 0.8f)) + .Font(FPS_Editor_UIStyle::BodyFontBold(S)) + .ColorAndOpacity(FPS_Editor_UIStyle::TextPrimary(S)) ] ] + SVerticalBox::Slot().AutoHeight().Padding(RowPadding) [ SNew(STextBlock) .Text(FText::FromString(TEXT("Location"))) - .Font(FCoreStyle::GetDefaultFontStyle("Regular", 9)) - .ColorAndOpacity(FLinearColor(0.5f, 0.5f, 0.5f)) + .Font(FPS_Editor_UIStyle::SmallFont(S)) + .ColorAndOpacity(FPS_Editor_UIStyle::TextMuted(S)) ] + SVerticalBox::Slot().AutoHeight().Padding(RowPadding) [ MakeRow(FText::GetEmpty(), LocX, LocY, LocZ, RedX, GreenY, BlueZ) ] @@ -649,8 +816,8 @@ TSharedRef UPS_Editor_MainWidget::BuildPropertiesPanel() [ SNew(STextBlock) .Text(FText::FromString(TEXT("Rotation"))) - .Font(FCoreStyle::GetDefaultFontStyle("Regular", 9)) - .ColorAndOpacity(FLinearColor(0.5f, 0.5f, 0.5f)) + .Font(FPS_Editor_UIStyle::SmallFont(S)) + .ColorAndOpacity(FPS_Editor_UIStyle::TextMuted(S)) ] + SVerticalBox::Slot().AutoHeight().Padding(RowPadding) [ MakeRow(FText::GetEmpty(), RotX, RotY, RotZ, RedX, GreenY, BlueZ) ] @@ -658,8 +825,8 @@ TSharedRef UPS_Editor_MainWidget::BuildPropertiesPanel() [ SNew(STextBlock) .Text(FText::FromString(TEXT("Scale"))) - .Font(FCoreStyle::GetDefaultFontStyle("Regular", 9)) - .ColorAndOpacity(FLinearColor(0.5f, 0.5f, 0.5f)) + .Font(FPS_Editor_UIStyle::SmallFont(S)) + .ColorAndOpacity(FPS_Editor_UIStyle::TextMuted(S)) ] + SVerticalBox::Slot().AutoHeight().Padding(RowPadding) [ MakeRow(FText::GetEmpty(), ScaX, ScaY, ScaZ, RedX, GreenY, BlueZ) ] @@ -672,10 +839,10 @@ TSharedRef UPS_Editor_MainWidget::BuildPropertiesPanel() TSharedRef UPS_Editor_MainWidget::BuildSceneButtons() { + const UPS_Editor_UIStyleAsset* S = StyleAsset; return SNew(SBorder) - .BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush")) - .BorderBackgroundColor(FLinearColor(0.05f, 0.05f, 0.05f, 1.0f)) - .Padding(FMargin(8.0f, 4.0f)) + .BorderImage(BrushGlassStrong.Get()) + .Padding(FMargin(8.0f, 6.0f)) [ SNew(SVerticalBox) // Save row: text field + save button @@ -689,6 +856,7 @@ TSharedRef UPS_Editor_MainWidget::BuildSceneButtons() .Padding(0.0f, 0.0f, 4.0f, 0.0f) [ SAssignNew(SaveNameField, SEditableTextBox) + .Style(StyleTextBox.Get()) .HintText(FText::FromString(TEXT("Scenario name..."))) .Font(FCoreStyle::GetDefaultFontStyle("Regular", 10)) ] @@ -696,6 +864,7 @@ TSharedRef UPS_Editor_MainWidget::BuildSceneButtons() .AutoWidth() [ SNew(SButton) + .ButtonStyle(StyleFlatButton.Get()) .OnClicked_Lambda([this]() -> FReply { // Block save during simulation @@ -745,6 +914,7 @@ TSharedRef UPS_Editor_MainWidget::BuildSceneButtons() .Padding(4.0f, 0.0f, 0.0f, 0.0f) [ SNew(SButton) + .ButtonStyle(StyleFlatButton.Get()) .OnClicked_Lambda([this]() -> FReply { IDesktopPlatform* DesktopPlatform = FDesktopPlatformModule::Get(); @@ -800,6 +970,7 @@ TSharedRef UPS_Editor_MainWidget::BuildSceneButtons() .Padding(0.0f, 0.0f, 4.0f, 0.0f) [ SNew(SButton) + .ButtonStyle(StyleFlatButton.Get()) .OnClicked_Lambda([this]() -> FReply { if (SceneSerializer.IsValid() && SpawnManager.IsValid()) @@ -821,6 +992,7 @@ TSharedRef UPS_Editor_MainWidget::BuildSceneButtons() .Padding(4.0f, 0.0f, 0.0f, 0.0f) [ SNew(SButton) + .ButtonStyle(StyleFlatButton.Get()) .OnClicked_Lambda([this]() -> FReply { APS_Editor_PlayerController* EditorPC = Cast(GetOwningPlayer()); @@ -909,6 +1081,7 @@ TSharedRef UPS_Editor_MainWidget::BuildSceneButtons() .Padding(4.0f, 0.0f, 0.0f, 0.0f) [ SNew(SButton) + .ButtonStyle(StyleFlatButton.Get()) .ToolTipText(FText::FromString(TEXT("Start/stop simulation. If a timeline exists, also plays/pauses it in sync."))) .OnClicked_Lambda([this]() -> FReply { @@ -957,6 +1130,7 @@ TSharedRef UPS_Editor_MainWidget::BuildSceneButtons() .Padding(4.0f, 0.0f, 0.0f, 0.0f) [ SNew(SButton) + .ButtonStyle(StyleFlatButton.Get()) .ToolTipText(FText::FromString(TEXT("Show/hide the timeline panel"))) .OnClicked_Lambda([this]() -> FReply { @@ -975,6 +1149,7 @@ TSharedRef UPS_Editor_MainWidget::BuildSceneButtons() .Padding(4.0f, 0.0f, 0.0f, 0.0f) [ SNew(SButton) + .ButtonStyle(StyleFlatButton.Get()) .OnClicked_Lambda([this]() -> FReply { IDesktopPlatform* DesktopPlatform = FDesktopPlatformModule::Get(); @@ -1087,6 +1262,7 @@ TSharedRef UPS_Editor_MainWidget::BuildSceneButtons() + SHorizontalBox::Slot().FillWidth(1.0f).VAlign(VAlign_Center) [ SNew(SButton) + .ButtonStyle(StyleFlatButton.Get()) .OnClicked_Lambda([this]() -> FReply { OpenBaseLevelPopup(); @@ -1112,21 +1288,27 @@ TSharedRef UPS_Editor_MainWidget::BuildSceneButtons() TSharedRef UPS_Editor_MainWidget::BuildSpawnCatalog() { + const UPS_Editor_UIStyleAsset* S = StyleAsset; TSharedRef CatalogList = SNew(SVerticalBox); - // Header CatalogList->AddSlot() .AutoHeight() - .Padding(0.0f, 0.0f, 0.0f, 4.0f) + .Padding(0.0f, 0.0f, 0.0f, 6.0f) [ - SNew(STextBlock) - .Text(FText::FromString(TEXT("Spawn Catalogue"))) - .Font(FCoreStyle::GetDefaultFontStyle("Bold", 11)) - .ColorAndOpacity(FLinearColor(0.8f, 0.8f, 0.8f)) + SNew(SHorizontalBox) + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0.0f, 0.0f, 6.0f, 0.0f) + [ + FPS_Editor_UIStyle::MakeIcon(S, EPS_Editor_Icon::Plus, 11.0f, FPS_Editor_UIStyle::Accent(S)) + ] + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Spawn"))) + .Font(FPS_Editor_UIStyle::BodyFontBold(S)) + .ColorAndOpacity(FPS_Editor_UIStyle::TextPrimary(S)) + ] ]; - // Note: entries will be populated in NativeConstruct when SpawnManager is set. - // For now, show a placeholder. The actual list is built dynamically. CatalogList->AddSlot() .AutoHeight() [ @@ -1134,13 +1316,11 @@ TSharedRef UPS_Editor_MainWidget::BuildSpawnCatalog() ]; return SNew(SBorder) - .BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush")) - .BorderBackgroundColor(FLinearColor(0.05f, 0.05f, 0.05f, 1.0f)) + .BorderImage(BrushGlassStrong.Get()) .Padding(FMargin(10.0f, 8.0f)) [ SNew(SBox) - .MinDesiredWidth(180.0f) - .MaxDesiredHeight(400.0f) + .MaxDesiredHeight(260.0f) [ SNew(SScrollBox) .ConsumeMouseWheel(EConsumeMouseWheel::Always) @@ -1287,6 +1467,7 @@ void UPS_Editor_MainWidget::PopulateSceneList() .Padding(0.0f, 1.0f) [ SNew(SButton) + .ButtonStyle(StyleFlatButton.Get()) .OnClicked_Lambda([this, Name]() -> FReply { if (SceneSerializer.IsValid() && SpawnManager.IsValid()) @@ -1498,6 +1679,7 @@ void UPS_Editor_MainWidget::PopulateSpawnCatalog() .Padding(4.0f, 4.0f) [ SNew(SButton) + .ButtonStyle(StyleFlatButton.Get()) .OnClicked_Lambda([this, EntryIndex]() -> FReply { // Get SpawnManager fresh from PlayerController @@ -1552,6 +1734,74 @@ void UPS_Editor_MainWidget::NativeTick(const FGeometry& MyGeometry, float InDelt { Super::NativeTick(MyGeometry, InDeltaTime); + // Update FPS stats (exponential moving average, updates ~2x/sec) + if (InDeltaTime > 0.0f) + { + const float InstantFps = 1.0f / InDeltaTime; + SmoothedFps = FMath::Lerp(SmoothedFps, InstantFps, FMath::Clamp(InDeltaTime * 2.0f, 0.0f, 1.0f)); + + if (StatsFpsText.IsValid()) + { + const FLinearColor FpsColor = SmoothedFps >= 50.0f + ? FPS_Editor_UIStyle::Ok(StyleAsset) + : (SmoothedFps >= 30.0f + ? FPS_Editor_UIStyle::Warn(StyleAsset) + : FPS_Editor_UIStyle::Err(StyleAsset)); + StatsFpsText->SetText(FText::FromString(FString::Printf(TEXT("FPS %.0f"), SmoothedFps))); + StatsFpsText->SetColorAndOpacity(FSlateColor(FpsColor)); + } + } + + // Update scene name display in topbar based on current scenario + if (TopbarSceneName.IsValid()) + { + if (APS_Editor_PlayerController* PC = Cast(GetOwningPlayer())) + { + if (UGameInstance* GI = PC->GetGameInstance()) + { + if (UPS_Editor_GameInstanceSubsystem* Sub = GI->GetSubsystem()) + { + const FString& ScenarioName = Sub->LastEditedScenarioName; + const FString Display = ScenarioName.IsEmpty() ? TEXT("untitled") : ScenarioName; + TopbarSceneName->SetText(FText::FromString(Display)); + } + } + } + } + + // Update simulate / status dot color in topbar + if (TopbarStatusDot.IsValid() || TopbarSimulateText.IsValid()) + { + if (APS_Editor_PlayerController* PC = Cast(GetOwningPlayer())) + { + const bool bSimulating = PC->IsSimulating(); + if (TopbarStatusDot.IsValid()) + { + TopbarStatusDot->SetBorderBackgroundColor(bSimulating + ? FPS_Editor_UIStyle::Ok(StyleAsset) + : FPS_Editor_UIStyle::Accent(StyleAsset)); + } + if (TopbarSimulateText.IsValid()) + { + TopbarSimulateText->SetText(FText::FromString(bSimulating ? TEXT("Stop") : TEXT("Simulate"))); + TopbarSimulateText->SetColorAndOpacity(FSlateColor(bSimulating + ? FPS_Editor_UIStyle::Err(StyleAsset) + : FPS_Editor_UIStyle::TextPrimary(StyleAsset))); + } + } + } + + // Sync SplineEditBar visibility to its child buttons (visible if any child is visible) + if (SplineEditBar.IsValid()) + { + const bool bAnyVisible = + (SplineEditButton.IsValid() && SplineEditButton->GetVisibility() != EVisibility::Collapsed) || + (ChildEditButton.IsValid() && ChildEditButton->GetVisibility() != EVisibility::Collapsed) || + (SplineDeletePointButton.IsValid() && SplineDeletePointButton->GetVisibility() != EVisibility::Collapsed) || + (SplineFinishButton.IsValid() && SplineFinishButton->GetVisibility() != EVisibility::Collapsed); + SplineEditBar->SetVisibility(bAnyVisible ? EVisibility::Visible : EVisibility::Collapsed); + } + // Track dirty state: detect spawned actor count changes (spawn, delete, undo, paste) if (APS_Editor_PlayerController* EditorPC = Cast(GetOwningPlayer())) { @@ -1847,26 +2097,37 @@ void UPS_Editor_MainWidget::ApplyTransformFromUI() TSharedRef UPS_Editor_MainWidget::BuildOutliner() { + const UPS_Editor_UIStyleAsset* S = StyleAsset; return SNew(SBorder) - .BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush")) - .BorderBackgroundColor(FLinearColor(0.05f, 0.05f, 0.05f, 1.0f)) + .BorderImage(BrushGlassStrong.Get()) .Padding(FMargin(10.0f, 8.0f)) [ SNew(SVerticalBox) + SVerticalBox::Slot().AutoHeight() [ - SNew(STextBlock) - .Text(FText::FromString(TEXT("Scene Objects"))) - .Font(FCoreStyle::GetDefaultFontStyle("Bold", 10)) - .ColorAndOpacity(FLinearColor(0.8f, 0.8f, 0.8f)) - ] - + SVerticalBox::Slot().FillHeight(1.0f).Padding(0.0f, 4.0f, 0.0f, 0.0f) - [ - SNew(SScrollBox) - .ConsumeMouseWheel(EConsumeMouseWheel::Always) - + SScrollBox::Slot() + SNew(SHorizontalBox) + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0.0f, 0.0f, 6.0f, 0.0f) [ - SAssignNew(OutlinerListContainer, SVerticalBox) + FPS_Editor_UIStyle::MakeIconFollowForeground(S, EPS_Editor_Icon::Layers, 11.0f) + ] + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Scene"))) + .Font(FPS_Editor_UIStyle::BodyFontBold(S)) + .ColorAndOpacity(FPS_Editor_UIStyle::TextPrimary(S)) + ] + ] + + SVerticalBox::Slot().FillHeight(1.0f).Padding(0.0f, 6.0f, 0.0f, 0.0f) + [ + SNew(SBox).MaxDesiredHeight(280.0f) + [ + SNew(SScrollBox) + .ConsumeMouseWheel(EConsumeMouseWheel::Always) + + SScrollBox::Slot() + [ + SAssignNew(OutlinerListContainer, SVerticalBox) + ] ] ] ]; @@ -1895,21 +2156,28 @@ void UPS_Editor_MainWidget::RefreshOutliner() OutlinerListContainer->ClearChildren(); UPS_Editor_SelectionManager* SelMgr = SelectionManager.Get(); - const FSlateFontInfo ItemFont = FCoreStyle::GetDefaultFontStyle("Regular", 9); + const UPS_Editor_UIStyleAsset* S = StyleAsset; + const FSlateFontInfo ItemFont = FPS_Editor_UIStyle::SmallFont(S); + const FSlateFontInfo HeaderFont = FPS_Editor_UIStyle::BodyFontBold(S); + + // Group actors by category + struct FOutlinerRow { AActor* Actor; FString DisplayName; int32 DisplayIndex; }; + TMap> Groups; + int32 DisplayIndex = 0; for (int32 i = 0; i < SpawnedActors.Num(); i++) { AActor* Actor = SpawnedActors[i].Get(); if (!Actor || Actor->IsHidden()) continue; - // Build display name + DisplayIndex++; + + FString Category = TEXT("Default"); FString DisplayName; if (UPS_Editor_SpawnableComponent* SC = Actor->FindComponentByClass()) { - if (!SC->DisplayName.IsEmpty()) - { - DisplayName = SC->DisplayName; - } + if (!SC->Category.IsEmpty()) Category = SC->Category; + if (!SC->DisplayName.IsEmpty()) DisplayName = SC->DisplayName; } if (DisplayName.IsEmpty()) { @@ -1917,43 +2185,112 @@ void UPS_Editor_MainWidget::RefreshOutliner() DisplayName.RemoveFromEnd(TEXT("_C")); } - // Append index for disambiguation - DisplayName = FString::Printf(TEXT("%s #%d"), *DisplayName, i + 1); + Groups.FindOrAdd(Category).Add({ Actor, DisplayName, DisplayIndex }); + } - const bool bIsSelected = SelMgr && SelMgr->IsActorSelected(Actor); - const FLinearColor TextColor = bIsSelected ? FLinearColor(0.2f, 0.7f, 1.0f) : FLinearColor(0.7f, 0.7f, 0.7f); - const FLinearColor BgColor = bIsSelected ? FLinearColor(0.15f, 0.25f, 0.35f, 0.8f) : FLinearColor(0.0f, 0.0f, 0.0f, 0.0f); + // Sort categories alphabetically (Default first) + TArray CategoryNames; + Groups.GetKeys(CategoryNames); + CategoryNames.Sort([](const FString& A, const FString& B) + { + if (A == TEXT("Default")) return true; + if (B == TEXT("Default")) return false; + return A < B; + }); - TWeakObjectPtr WeakActor = Actor; + for (const FString& GroupName : CategoryNames) + { + const TArray& Rows = Groups[GroupName]; - OutlinerListContainer->AddSlot().AutoHeight().Padding(0.0f, 1.0f) + // Category header + OutlinerListContainer->AddSlot().AutoHeight().Padding(0.0f, 4.0f, 0.0f, 2.0f) [ - SNew(SButton) - .ButtonStyle(FCoreStyle::Get(), "NoBorder") - .ContentPadding(FMargin(6.0f, 3.0f)) - .OnClicked_Lambda([this, WeakActor]() -> FReply - { - AActor* Act = WeakActor.Get(); - UPS_Editor_SelectionManager* SM = SelectionManager.Get(); - if (Act && SM) - { - SM->ClearSelection(); - SM->SelectActor(Act); - } - return FReply::Handled(); - }) + SNew(SHorizontalBox) + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0.0f, 0.0f, 4.0f, 0.0f) [ - SNew(SBorder) - .BorderBackgroundColor(BgColor) - .Padding(FMargin(4.0f, 2.0f)) - [ - SNew(STextBlock) - .Text(FText::FromString(DisplayName)) - .Font(ItemFont) - .ColorAndOpacity(TextColor) - ] + FPS_Editor_UIStyle::MakeIconFollowForeground(S, EPS_Editor_Icon::ChevronDown, 10.0f) + ] + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(FText::FromString(GroupName.ToUpper())) + .Font(FPS_Editor_UIStyle::SmallFont(S)) + .ColorAndOpacity(FPS_Editor_UIStyle::TextDim(S)) + ] + + SHorizontalBox::Slot().FillWidth(1.0f) + [ SNullWidget::NullWidget ] + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(FText::FromString(FString::Printf(TEXT("%d"), Rows.Num()))) + .Font(FPS_Editor_UIStyle::SmallFont(S)) + .ColorAndOpacity(FPS_Editor_UIStyle::TextDisabled(S)) ] ]; + + for (const FOutlinerRow& Row : Rows) + { + AActor* Actor = Row.Actor; + const bool bIsSelected = SelMgr && SelMgr->IsActorSelected(Actor); + const FLinearColor TextColor = bIsSelected ? FPS_Editor_UIStyle::Accent(S) : FPS_Editor_UIStyle::TextPrimary(S); + const FLinearColor BgColor = bIsSelected ? FPS_Editor_UIStyle::AccentSoft(S) : FLinearColor(0.0f, 0.0f, 0.0f, 0.0f); + + TWeakObjectPtr WeakActor = Actor; + const FString LabelText = FString::Printf(TEXT("%s #%d"), *Row.DisplayName, Row.DisplayIndex); + + // Pick icon based on category keyword (naive but covers the common cases) + EPS_Editor_Icon ItemIcon = EPS_Editor_Icon::Box; + const FString GroupLower = GroupName.ToLower(); + if (GroupLower.Contains(TEXT("npc")) || GroupLower.Contains(TEXT("char")) || GroupLower.Contains(TEXT("enemy")) || GroupLower.Contains(TEXT("civil"))) ItemIcon = EPS_Editor_Icon::Character; + else if (GroupLower.Contains(TEXT("spline")) || GroupLower.Contains(TEXT("path"))) ItemIcon = EPS_Editor_Icon::SplineObj; + else if (GroupLower.Contains(TEXT("trigger")) || GroupLower.Contains(TEXT("zone"))) ItemIcon = EPS_Editor_Icon::Trigger; + else if (GroupLower.Contains(TEXT("camera"))) ItemIcon = EPS_Editor_Icon::Camera; + else if (GroupLower.Contains(TEXT("light"))) ItemIcon = EPS_Editor_Icon::Light; + else if (GroupLower.Contains(TEXT("flag")) || GroupLower.Contains(TEXT("wayp"))) ItemIcon = EPS_Editor_Icon::Flag; + else if (GroupLower.Contains(TEXT("area")) || GroupLower.Contains(TEXT("map"))) ItemIcon = EPS_Editor_Icon::Map; + + OutlinerListContainer->AddSlot().AutoHeight().Padding(8.0f, 1.0f, 0.0f, 1.0f) + [ + SNew(SButton) + .ButtonStyle(StyleFlatButton.Get()) + .ContentPadding(FMargin(6.0f, 3.0f)) + .OnClicked_Lambda([this, WeakActor]() -> FReply + { + AActor* Act = WeakActor.Get(); + UPS_Editor_SelectionManager* SM = SelectionManager.Get(); + if (Act && SM) + { + SM->ClearSelection(); + SM->SelectActor(Act); + } + return FReply::Handled(); + }) + [ + SNew(SBorder) + .BorderImage(BrushField.Get()) + .BorderBackgroundColor(BgColor) + .Padding(FMargin(6.0f, 3.0f)) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0.0f, 0.0f, 6.0f, 0.0f) + [ + FPS_Editor_UIStyle::MakeIcon(S, ItemIcon, 11.0f, TextColor) + ] + + SHorizontalBox::Slot().FillWidth(1.0f).VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(FText::FromString(LabelText)) + .Font(ItemFont) + .ColorAndOpacity(TextColor) + ] + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(4.0f, 0.0f, 0.0f, 0.0f) + [ + FPS_Editor_UIStyle::MakeIconFollowForeground(S, EPS_Editor_Icon::Eye, 10.0f) + ] + ] + ] + ]; + } } } @@ -2177,6 +2514,7 @@ void UPS_Editor_MainWidget::RebuildDynamicProperties() auto MakeTextField = [&](TSharedPtr& Field) -> TSharedRef { return SAssignNew(Field, SEditableTextBox) + .Style(StyleTextBox.Get()) .Font(ValueFont) .MinDesiredWidth(55.0f) .OnTextCommitted_Lambda([this, RowIndex](const FText&, ETextCommit::Type) @@ -2220,6 +2558,7 @@ void UPS_Editor_MainWidget::RebuildDynamicProperties() { // Unsupported struct: single text field with ExportText format ValueWidget = SAssignNew(Row.SingleField, SEditableTextBox) + .Style(StyleTextBox.Get()) .Font(ValueFont) .MinDesiredWidth(70.0f) .OnTextCommitted_Lambda([this, RowIndex](const FText&, ETextCommit::Type) @@ -2232,6 +2571,7 @@ void UPS_Editor_MainWidget::RebuildDynamicProperties() { // Default: single text field (float, int, string, etc.) ValueWidget = SAssignNew(Row.SingleField, SEditableTextBox) + .Style(StyleTextBox.Get()) .Font(ValueFont) .MinDesiredWidth(70.0f) .OnTextCommitted_Lambda([this, RowIndex](const FText&, ETextCommit::Type) @@ -2255,6 +2595,7 @@ void UPS_Editor_MainWidget::RebuildDynamicProperties() + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(4.0f, 0.0f, 0.0f, 0.0f) [ SNew(SButton) + .ButtonStyle(StyleFlatButton.Get()) .ToolTipText(FText::FromString(TEXT("Add a keyframe at the current timeline time"))) .ContentPadding(FMargin(4.0f, 0.0f)) .OnClicked_Lambda([this, CapturedRowIndex]() -> FReply @@ -2710,8 +3051,7 @@ void UPS_Editor_MainWidget::OpenBaseLevelPopup() Card->AddSlot().AutoHeight().HAlign(HAlign_Center) [ SNew(SBorder) - .BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush")) - .BorderBackgroundColor(FLinearColor(0.15f, 0.15f, 0.15f)) + .BorderImage(BrushField.Get()) [ SNew(SBox) .WidthOverride(CardWidth) @@ -2745,11 +3085,12 @@ void UPS_Editor_MainWidget::OpenBaseLevelPopup() .MinDesiredWidth(CardWidth + 16.0f) [ SAssignNew(CardBorder, SBorder) - .BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush")) - .BorderBackgroundColor(FLinearColor(0.1f, 0.1f, 0.1f, 0.0f)) + .BorderImage(BrushField.Get()) + .BorderBackgroundColor(FLinearColor(0.0f, 0.0f, 0.0f, 0.0f)) .Padding(FMargin(2.0f)) [ SNew(SButton) + .ButtonStyle(StyleFlatButton.Get()) .OnClicked_Lambda([this, Name]() -> FReply { PendingBaseLevelSelection = Name; @@ -2838,8 +3179,8 @@ void UPS_Editor_MainWidget::UpdateBaseLevelHighlight() { const bool bSelected = (Pair.Key == PendingBaseLevelSelection); Pair.Value->SetBorderBackgroundColor(bSelected - ? FLinearColor(0.2f, 0.6f, 1.0f, 0.8f) - : FLinearColor(0.1f, 0.1f, 0.1f, 0.0f)); + ? FPS_Editor_UIStyle::AccentSoft(StyleAsset) + : FLinearColor(0.0f, 0.0f, 0.0f, 0.0f)); } } } @@ -3145,13 +3486,13 @@ private: TSharedRef UPS_Editor_MainWidget::BuildTimelinePanel() { - const FSlateFontInfo Small = FCoreStyle::GetDefaultFontStyle("Regular", 9); - const FSlateFontInfo Bold = FCoreStyle::GetDefaultFontStyle("Bold", 10); + const UPS_Editor_UIStyleAsset* S = StyleAsset; + const FSlateFontInfo Small = FPS_Editor_UIStyle::SmallFont(S); + const FSlateFontInfo Bold = FPS_Editor_UIStyle::BodyFontBold(S); return SAssignNew(TimelinePanel, SBorder) .Visibility(EVisibility::Collapsed) - .BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush")) - .BorderBackgroundColor(FLinearColor(0.05f, 0.05f, 0.05f, 0.95f)) + .BorderImage(BrushGlassStrong.Get()) .Padding(FMargin(12.0f, 8.0f)) [ SNew(SVerticalBox) @@ -3159,34 +3500,23 @@ TSharedRef UPS_Editor_MainWidget::BuildTimelinePanel() + SVerticalBox::Slot().AutoHeight().Padding(0.0f, 0.0f, 0.0f, 6.0f) [ SNew(SHorizontalBox) + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0.0f, 0.0f, 6.0f, 0.0f) + [ + FPS_Editor_UIStyle::MakeIconFollowForeground(S, EPS_Editor_Icon::Film, 12.0f) + ] + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center) [ SNew(STextBlock) .Text(FText::FromString(TEXT("Timeline"))) - .Font(FCoreStyle::GetDefaultFontStyle("Bold", 12)) - .ColorAndOpacity(FLinearColor(0.6f, 0.8f, 1.0f)) + .Font(FPS_Editor_UIStyle::HeadingFontBold(S)) + .ColorAndOpacity(FPS_Editor_UIStyle::TextPrimary(S)) ] + // Restart (SkipBack) + SHorizontalBox::Slot().AutoWidth().Padding(12.0f, 0.0f, 0.0f, 0.0f).VAlign(VAlign_Center) [ SNew(SButton) - .OnClicked_Lambda([this]() -> FReply - { - if (UPS_Editor_TimelineSubsystem* TS = GetTimelineSubsystemFromWidget(this)) - { - if (TS->IsPlaying()) TS->Pause(); else TS->Play(); - } - return FReply::Handled(); - }) - [ - SAssignNew(TimelinePlayButtonText, STextBlock) - .Text(FText::FromString(TEXT("Play"))) - .Font(Bold) - .ColorAndOpacity(FLinearColor(0.2f, 0.9f, 0.2f)) - ] - ] - + SHorizontalBox::Slot().AutoWidth().Padding(4.0f, 0.0f, 0.0f, 0.0f).VAlign(VAlign_Center) - [ - SNew(SButton) + .ButtonStyle(StyleFlatButton.Get()) + .ContentPadding(FMargin(6.0f, 4.0f)) .ToolTipText(FText::FromString(TEXT("Rewind to t=0 and replay from the start"))) .OnClicked_Lambda([this]() -> FReply { @@ -3197,15 +3527,57 @@ TSharedRef UPS_Editor_MainWidget::BuildTimelinePanel() return FReply::Handled(); }) [ - SNew(STextBlock) - .Text(FText::FromString(TEXT("Restart"))) - .Font(Bold) - .ColorAndOpacity(FLinearColor(0.6f, 0.8f, 1.0f)) + FPS_Editor_UIStyle::MakeIconFollowForeground(S, EPS_Editor_Icon::SkipBack, 13.0f) ] ] + // Play / Pause (icon swaps based on IsPlaying) + SHorizontalBox::Slot().AutoWidth().Padding(4.0f, 0.0f, 0.0f, 0.0f).VAlign(VAlign_Center) [ SNew(SButton) + .ButtonStyle(StyleFlatButton.Get()) + .ContentPadding(FMargin(6.0f, 4.0f)) + .OnClicked_Lambda([this]() -> FReply + { + if (UPS_Editor_TimelineSubsystem* TS = GetTimelineSubsystemFromWidget(this)) + { + if (TS->IsPlaying()) TS->Pause(); else TS->Play(); + } + return FReply::Handled(); + }) + [ + SNew(SOverlay) + + SOverlay::Slot() + [ + SNew(SBox) + .Visibility_Lambda([this]() -> EVisibility + { + UPS_Editor_TimelineSubsystem* TS = GetTimelineSubsystemFromWidget(this); + return (TS && TS->IsPlaying()) ? EVisibility::Collapsed : EVisibility::Visible; + }) + [ + FPS_Editor_UIStyle::MakeIcon(S, EPS_Editor_Icon::Play, 14.0f, FPS_Editor_UIStyle::Ok(S)) + ] + ] + + SOverlay::Slot() + [ + SNew(SBox) + .Visibility_Lambda([this]() -> EVisibility + { + UPS_Editor_TimelineSubsystem* TS = GetTimelineSubsystemFromWidget(this); + return (TS && TS->IsPlaying()) ? EVisibility::Visible : EVisibility::Collapsed; + }) + [ + FPS_Editor_UIStyle::MakeIcon(S, EPS_Editor_Icon::Pause, 14.0f, FPS_Editor_UIStyle::Warn(S)) + ] + ] + ] + ] + // Stop + + SHorizontalBox::Slot().AutoWidth().Padding(4.0f, 0.0f, 0.0f, 0.0f).VAlign(VAlign_Center) + [ + SNew(SButton) + .ButtonStyle(StyleFlatButton.Get()) + .ContentPadding(FMargin(6.0f, 4.0f)) .ToolTipText(FText::FromString(TEXT("Stop: rewind + restore actor transforms"))) .OnClicked_Lambda([this]() -> FReply { @@ -3216,12 +3588,16 @@ TSharedRef UPS_Editor_MainWidget::BuildTimelinePanel() return FReply::Handled(); }) [ - SNew(STextBlock) - .Text(FText::FromString(TEXT("Stop"))) - .Font(Bold) - .ColorAndOpacity(FLinearColor(1.0f, 0.4f, 0.2f)) + FPS_Editor_UIStyle::MakeIcon(S, EPS_Editor_Icon::Stop, 13.0f, FPS_Editor_UIStyle::Err(S)) ] ] + // Keep TimelinePlayButtonText as a hidden compat ref for existing setText callers. + + SHorizontalBox::Slot().AutoWidth() + [ + SAssignNew(TimelinePlayButtonText, STextBlock) + .Visibility(EVisibility::Collapsed) + .Text(FText::GetEmpty()) + ] + SHorizontalBox::Slot().AutoWidth().Padding(12.0f, 0.0f, 0.0f, 0.0f).VAlign(VAlign_Center) [ SAssignNew(TimelineTimeLabel, STextBlock) @@ -3245,6 +3621,7 @@ TSharedRef UPS_Editor_MainWidget::BuildTimelinePanel() SNew(SBox).MinDesiredWidth(60.0f) [ SAssignNew(TimelineDurationBox, SEditableTextBox) + .Style(StyleTextBox.Get()) .Font(Small) .HintText(FText::FromString(TEXT("10"))) .OnTextCommitted_Lambda([this](const FText& Text, ETextCommit::Type) @@ -3515,6 +3892,7 @@ void UPS_Editor_MainWidget::RebuildTimelineTracksList() SNew(SBox).WidthOverride(24.0f) [ SNew(SButton) + .ButtonStyle(StyleFlatButton.Get()) .ToolTipText(FText::FromString(TEXT("Remove this track"))) .ContentPadding(FMargin(2.0f, 0.0f)) .OnClicked_Lambda([this, TrackIdx]() -> FReply @@ -3569,6 +3947,7 @@ void UPS_Editor_MainWidget::RebuildTimelineTracksList() SNew(SBox).WidthOverride(50.0f) [ SNew(SButton) + .ButtonStyle(StyleFlatButton.Get()) .ToolTipText(FText::FromString(TEXT("Toggle interpolation on the key at (or before) the current time"))) .ContentPadding(FMargin(2.0f, 0.0f)) .OnClicked_Lambda([this, TrackIdx]() -> FReply @@ -3682,3 +4061,846 @@ bool UPS_Editor_MainWidget::TryDeleteSelectedTimelineKey() bSceneDirty = true; return true; } + +// ======================================================================== +// NEW HUD WIDGETS (floating design) +// ======================================================================== + +TSharedRef UPS_Editor_MainWidget::BuildTopbar() +{ + const UPS_Editor_UIStyleAsset* S = StyleAsset; + + return SNew(SBorder) + .BorderImage(BrushGlassStrong.Get()) + .Padding(FMargin(8.0f, 4.0f)) + [ + SNew(SHorizontalBox) + // Brand mark + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(4.0f, 0.0f, 6.0f, 0.0f) + [ + FPS_Editor_UIStyle::MakeIcon(S, EPS_Editor_Icon::BrandMark, 18.0f, FPS_Editor_UIStyle::Accent(S)) + ] + // Brand name + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0.0f, 0.0f, 12.0f, 0.0f) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("PS Editor"))) + .Font(FPS_Editor_UIStyle::HeadingFontBold(S)) + .ColorAndOpacity(FPS_Editor_UIStyle::TextPrimary(S)) + ] + // File dropdown menu + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0.0f, 0.0f, 6.0f, 0.0f) + [ + SAssignNew(FileMenuAnchor, SMenuAnchor) + .Placement(MenuPlacement_BelowAnchor) + .OnGetMenuContent_Lambda([this]() -> TSharedRef + { + return BuildFileMenuContent(); + }) + [ + SNew(SButton) + .ButtonStyle(StyleFlatButton.Get()) + .ContentPadding(FMargin(8.0f, 4.0f)) + .OnClicked_Lambda([this]() -> FReply + { + if (FileMenuAnchor.IsValid()) + { + FileMenuAnchor->SetIsOpen(!FileMenuAnchor->IsOpen()); + } + return FReply::Handled(); + }) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0.0f, 0.0f, 6.0f, 0.0f) + [ + FPS_Editor_UIStyle::MakeIconFollowForeground(StyleAsset, EPS_Editor_Icon::Folder, 12.0f) + ] + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("File"))) + .Font(FPS_Editor_UIStyle::BodyFont(StyleAsset)) + .ColorAndOpacity(FPS_Editor_UIStyle::TextPrimary(StyleAsset)) + ] + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(4.0f, 0.0f, 0.0f, 0.0f) + [ + FPS_Editor_UIStyle::MakeIconFollowForeground(StyleAsset, EPS_Editor_Icon::ChevronDown, 10.0f) + ] + ] + ] + ] + // Keep ModeText hidden (still used internally to display mode; kept for compat) + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center) + [ + SAssignNew(ModeText, STextBlock) + .Text(FText::GetEmpty()) + .Visibility(EVisibility::Collapsed) + ] + // Spacer + + SHorizontalBox::Slot().FillWidth(1.0f) + [ + SNullWidget::NullWidget + ] + // Scene center pill: status dot + scene name + run controls + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(4.0f, 0.0f) + [ + SNew(SBorder) + .BorderImage(BrushPill.Get()) + .BorderBackgroundColor(FLinearColor(0.0f, 0.0f, 0.0f, 0.32f)) + .Padding(FMargin(12.0f, 3.0f)) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0.0f, 0.0f, 8.0f, 0.0f) + [ + SNew(SBox).WidthOverride(8.0f).HeightOverride(8.0f) + [ + SAssignNew(TopbarStatusDot, SBorder) + .BorderImage(BrushPill.Get()) + .BorderBackgroundColor(FPS_Editor_UIStyle::Accent(S)) + ] + ] + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center) + [ + SAssignNew(TopbarSceneName, STextBlock) + .Text(FText::FromString(TEXT("scenario"))) + .Font(FPS_Editor_UIStyle::BodyFont(S)) + .ColorAndOpacity(FPS_Editor_UIStyle::TextPrimary(S)) + ] + // Vertical divider + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(10.0f, 2.0f) + [ + SNew(SBox).WidthOverride(1.0f).HeightOverride(14.0f) + [ + SNew(SBorder) + .BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush")) + .BorderBackgroundColor(FPS_Editor_UIStyle::GlassBorder(S)) + ] + ] + // Play button + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center) + [ + SNew(SButton) + .ButtonStyle(StyleFlatButton.Get()) + .ContentPadding(FMargin(8.0f, 2.0f)) + .ToolTipText(FText::FromString(TEXT("Play scenario"))) + .OnClicked_Lambda([this]() -> FReply + { + if (APS_Editor_PlayerController* PC = Cast(GetOwningPlayer())) + { + if (PC->IsSimulating()) PC->StopSimulation(); + else PC->StartSimulation(); + } + return FReply::Handled(); + }) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0.0f, 0.0f, 4.0f, 0.0f) + [ + FPS_Editor_UIStyle::MakeIconFollowForeground(S, EPS_Editor_Icon::Play, 11.0f) + ] + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Play"))) + .Font(FPS_Editor_UIStyle::SmallFont(S)) + .ColorAndOpacity(FPS_Editor_UIStyle::TextPrimary(S)) + ] + ] + ] + // Separator + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(2.0f, 4.0f) + [ + SNew(SBox).WidthOverride(1.0f).HeightOverride(12.0f) + [ + SNew(SBorder) + .BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush")) + .BorderBackgroundColor(FPS_Editor_UIStyle::GlassBorder(S)) + ] + ] + // Simulate toggle + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center) + [ + SAssignNew(TopbarSimulateButton, SButton) + .ButtonStyle(StyleFlatButton.Get()) + .ContentPadding(FMargin(8.0f, 2.0f)) + .ToolTipText(FText::FromString(TEXT("Toggle simulation"))) + .OnClicked_Lambda([this]() -> FReply + { + if (APS_Editor_PlayerController* PC = Cast(GetOwningPlayer())) + { + if (PC->IsSimulating()) PC->StopSimulation(); + else PC->StartSimulation(); + } + return FReply::Handled(); + }) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0.0f, 0.0f, 4.0f, 0.0f) + [ + FPS_Editor_UIStyle::MakeIcon(S, EPS_Editor_Icon::Simulate, 11.0f, FPS_Editor_UIStyle::Warn(S)) + ] + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center) + [ + SAssignNew(TopbarSimulateText, STextBlock) + .Text(FText::FromString(TEXT("Simulate"))) + .Font(FPS_Editor_UIStyle::SmallFont(S)) + .ColorAndOpacity(FPS_Editor_UIStyle::TextPrimary(S)) + ] + ] + ] + ] + ] + // Spacer + + SHorizontalBox::Slot().FillWidth(1.0f) + [ + SNullWidget::NullWidget + ] + // Command palette hint button + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(2.0f, 0.0f) + [ + SNew(SButton) + .ButtonStyle(StyleFlatButton.Get()) + .ContentPadding(FMargin(6.0f, 3.0f)) + .ToolTipText(FText::FromString(TEXT("Command palette (Ctrl+K)"))) + .OnClicked_Lambda([this]() -> FReply { ToggleCommandPalette(); return FReply::Handled(); }) + [ + FPS_Editor_UIStyle::MakeIconFollowForeground(S, EPS_Editor_Icon::Command, 14.0f) + ] + ] + // Help + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(2.0f, 0.0f) + [ + SNew(SButton) + .ButtonStyle(StyleFlatButton.Get()) + .ContentPadding(FMargin(6.0f, 3.0f)) + .ToolTipText(FText::FromString(TEXT("Help"))) + .OnClicked_Lambda([this]() -> FReply { ShowToast(TEXT("Z/E/R: tool | F: focus | Del: delete | Ctrl+K: palette"), FPS_Editor_UIStyle::Info(StyleAsset)); return FReply::Handled(); }) + [ + FPS_Editor_UIStyle::MakeIconFollowForeground(S, EPS_Editor_Icon::Help, 14.0f) + ] + ] + // Settings + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(2.0f, 0.0f) + [ + SNew(SButton) + .ButtonStyle(StyleFlatButton.Get()) + .ContentPadding(FMargin(6.0f, 3.0f)) + .ToolTipText(FText::FromString(TEXT("Settings"))) + [ + FPS_Editor_UIStyle::MakeIconFollowForeground(S, EPS_Editor_Icon::Settings, 14.0f) + ] + ] + // Vertical divider + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(6.0f, 6.0f) + [ + SNew(SBox).WidthOverride(1.0f).HeightOverride(16.0f) + [ + SNew(SBorder) + .BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush")) + .BorderBackgroundColor(FPS_Editor_UIStyle::GlassBorder(S)) + ] + ] + // Outliner (sidebar) toggle + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(2.0f, 0.0f) + [ + SNew(SButton) + .ButtonStyle(StyleFlatButton.Get()) + .ContentPadding(FMargin(6.0f, 3.0f)) + .ToolTipText(FText::FromString(TEXT("Toggle outliner"))) + [ + FPS_Editor_UIStyle::MakeIconFollowForeground(S, EPS_Editor_Icon::Sidebar, 14.0f) + ] + ] + // Timeline toggle + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(2.0f, 0.0f) + [ + SNew(SButton) + .ButtonStyle(StyleFlatButton.Get()) + .ContentPadding(FMargin(6.0f, 3.0f)) + .ToolTipText(FText::FromString(TEXT("Toggle timeline"))) + .OnClicked_Lambda([this]() -> FReply { ToggleTimelineVisible(); return FReply::Handled(); }) + [ + FPS_Editor_UIStyle::MakeIconFollowForeground(S, EPS_Editor_Icon::PanelBottom, 14.0f) + ] + ] + // Inspector (panel right) toggle + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(2.0f, 0.0f) + [ + SNew(SButton) + .ButtonStyle(StyleFlatButton.Get()) + .ContentPadding(FMargin(6.0f, 3.0f)) + .ToolTipText(FText::FromString(TEXT("Toggle inspector"))) + [ + FPS_Editor_UIStyle::MakeIconFollowForeground(S, EPS_Editor_Icon::PanelRight, 14.0f) + ] + ] + // Close X button + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(8.0f, 0.0f, 0.0f, 0.0f) + [ + SNew(SButton) + .ButtonStyle(StyleFlatButton.Get()) + .ContentPadding(FMargin(6.0f, 3.0f)) + .ToolTipText(FText::FromString(TEXT("Close editor"))) + .OnClicked_Lambda([this]() -> FReply { OnCloseClicked(); return FReply::Handled(); }) + [ + FPS_Editor_UIStyle::MakeIcon(S, EPS_Editor_Icon::Close, 14.0f, FPS_Editor_UIStyle::Err(S)) + ] + ] + ]; +} + +TSharedRef UPS_Editor_MainWidget::BuildFileMenuContent() +{ + const UPS_Editor_UIStyleAsset* S = StyleAsset; + + auto MakeMenuItem = [this, S](EPS_Editor_Icon Icon, const FString& Label, TFunction OnClick) -> TSharedRef + { + return SNew(SButton) + .ButtonStyle(StyleFlatButton.Get()) + .ContentPadding(FMargin(10.0f, 6.0f)) + .OnClicked_Lambda([this, OnClick]() -> FReply + { + OnClick(); + if (FileMenuAnchor.IsValid()) FileMenuAnchor->SetIsOpen(false); + return FReply::Handled(); + }) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0.0f, 0.0f, 10.0f, 0.0f) + [ + FPS_Editor_UIStyle::MakeIcon(S, Icon, 13.0f, FPS_Editor_UIStyle::TextMuted(S)) + ] + + SHorizontalBox::Slot().FillWidth(1.0f).VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(FText::FromString(Label)) + .Font(FPS_Editor_UIStyle::BodyFont(S)) + .ColorAndOpacity(FPS_Editor_UIStyle::TextPrimary(S)) + ] + ]; + }; + + return SNew(SBox).WidthOverride(200.0f) + [ + SNew(SBorder) + .BorderImage(BrushGlassStrong.Get()) + .Padding(FMargin(4.0f)) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot().AutoHeight() + [ MakeMenuItem(EPS_Editor_Icon::Plus, TEXT("New Scenario..."), [this]() { OpenBaseLevelPopup(); }) ] + + SVerticalBox::Slot().AutoHeight() + [ MakeMenuItem(EPS_Editor_Icon::Folder, TEXT("Load Scenario..."), [this]() { ShowToast(TEXT("Use the scene list in the Outliner."), FPS_Editor_UIStyle::Info(StyleAsset)); }) ] + // separator + + SVerticalBox::Slot().AutoHeight().Padding(FMargin(6.0f, 4.0f)) + [ + SNew(SBox).HeightOverride(1.0f) + [ + SNew(SBorder) + .BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush")) + .BorderBackgroundColor(FPS_Editor_UIStyle::GlassBorder(S)) + ] + ] + + SVerticalBox::Slot().AutoHeight() + [ MakeMenuItem(EPS_Editor_Icon::Save, TEXT("Save"), [this]() + { + if (SaveNameField.IsValid() && SceneSerializer.IsValid()) + { + FString Name = SaveNameField->GetText().ToString(); + if (!Name.IsEmpty()) + { + UPS_Editor_SpawnManager* SaveSM = nullptr; + if (APS_Editor_PlayerController* PC = Cast(GetOwningPlayer())) + SaveSM = PC->GetSpawnManager(); + if (!SaveSM) SaveSM = SpawnManager.Get(); + if (SaveSM) + { + SceneSerializer->SaveScene(Name, SaveSM); + bSceneDirty = false; + PopulateSceneList(); + ShowToast(FString::Printf(TEXT("Scene saved: %s"), *Name), FPS_Editor_UIStyle::Ok(StyleAsset)); + } + } + else { ShowToast(TEXT("Enter a scenario name first."), FPS_Editor_UIStyle::Warn(StyleAsset)); } + } + }) ] + + SVerticalBox::Slot().AutoHeight() + [ MakeMenuItem(EPS_Editor_Icon::Save, TEXT("Save As..."), [this]() { ShowToast(TEXT("Save As... (use the Save As button)"), FPS_Editor_UIStyle::Info(StyleAsset)); }) ] + ] + ]; +} + +TSharedRef UPS_Editor_MainWidget::BuildViewCube() +{ + const UPS_Editor_UIStyleAsset* S = StyleAsset; + + return SNew(SBox).WidthOverride(62.0f).HeightOverride(62.0f) + [ + SNew(SBorder) + .BorderImage(BrushGlassSoft.Get()) + .Padding(FMargin(4.0f)) + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + [ + SNew(SOverlay) + // X axis label (right) + + SOverlay::Slot().HAlign(HAlign_Right).VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("X"))) + .Font(FPS_Editor_UIStyle::BodyFontBold(S)) + .ColorAndOpacity(FPS_Editor_UIStyle::AxisX(S)) + ] + // Y axis label (top) + + SOverlay::Slot().HAlign(HAlign_Center).VAlign(VAlign_Top) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Y"))) + .Font(FPS_Editor_UIStyle::BodyFontBold(S)) + .ColorAndOpacity(FPS_Editor_UIStyle::AxisY(S)) + ] + // Z axis label (bottom-left) + + SOverlay::Slot().HAlign(HAlign_Left).VAlign(VAlign_Bottom) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Z"))) + .Font(FPS_Editor_UIStyle::BodyFontBold(S)) + .ColorAndOpacity(FPS_Editor_UIStyle::AxisZ(S)) + ] + // Center dot + + SOverlay::Slot().HAlign(HAlign_Center).VAlign(VAlign_Center) + [ + SNew(SBorder) + .BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush")) + .BorderBackgroundColor(FPS_Editor_UIStyle::TextDim(S)) + .Padding(FMargin(3.0f)) + [ + SNew(SBox).WidthOverride(2.0f).HeightOverride(2.0f) + ] + ] + ] + ]; +} + +TSharedRef UPS_Editor_MainWidget::BuildStatsHUD() +{ + const UPS_Editor_UIStyleAsset* S = StyleAsset; + const FSlateFontInfo Mono = FPS_Editor_UIStyle::MonoFont(S); + + return SNew(SBorder) + .BorderImage(BrushGlassSoft.Get()) + .Padding(FMargin(10.0f, 6.0f)) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0.0f, 0.0f, 12.0f, 0.0f) + [ + SAssignNew(StatsFpsText, STextBlock) + .Text(FText::FromString(TEXT("FPS 60"))) + .Font(Mono) + .ColorAndOpacity(FPS_Editor_UIStyle::Ok(S)) + ] + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0.0f, 0.0f, 12.0f, 0.0f) + [ + SAssignNew(StatsTriText, STextBlock) + .Text(FText::FromString(TEXT("Tri \u2014"))) + .Font(Mono) + .ColorAndOpacity(FPS_Editor_UIStyle::TextMuted(S)) + ] + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center) + [ + SAssignNew(StatsDrawText, STextBlock) + .Text(FText::FromString(TEXT("Draw \u2014"))) + .Font(Mono) + .ColorAndOpacity(FPS_Editor_UIStyle::TextMuted(S)) + ] + ]; +} + +TSharedRef UPS_Editor_MainWidget::BuildHintHUD() +{ + const UPS_Editor_UIStyleAsset* S = StyleAsset; + + return SNew(SBorder) + .BorderImage(BrushGlassSoft.Get()) + .Padding(FMargin(10.0f, 5.0f)) + [ + SAssignNew(HelpBarText, STextBlock) + .Text(FText::FromString(TEXT("LMB Select \u00B7 RMB Fly \u00B7 Alt+LMB Orbit \u00B7 Z/E/R Tool \u00B7 Del Delete \u00B7 S Spline"))) + .Font(FPS_Editor_UIStyle::SmallFont(S)) + .ColorAndOpacity(FPS_Editor_UIStyle::TextDim(S)) + ]; +} + +TSharedRef UPS_Editor_MainWidget::BuildSplineEditBar() +{ + const UPS_Editor_UIStyleAsset* S = StyleAsset; + + TSharedRef Bar = SNew(SBorder) + .BorderImage(BrushGlassStrong.Get()) + .Padding(FMargin(12.0f, 6.0f)) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0.0f, 0.0f, 10.0f, 0.0f) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Editing Spline"))) + .Font(FPS_Editor_UIStyle::BodyFontBold(S)) + .ColorAndOpacity(FPS_Editor_UIStyle::Accent(S)) + ] + // Edit Spline button + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(4.0f, 0.0f) + [ + SAssignNew(SplineEditButton, SButton) + .ButtonStyle(StyleFlatButton.Get()) + .Visibility(EVisibility::Collapsed) + .ContentPadding(FMargin(8.0f, 3.0f)) + .OnClicked_Lambda([this]() -> FReply + { + if (APS_Editor_PlayerController* EditorPC = Cast(GetOwningPlayer())) + { + if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement) + { + EditorPC->FinishSplineEditing(); + } + else if (UPS_Editor_SelectionManager* SelMgr = SelectionManager.Get()) + { + for (const TWeakObjectPtr& Weak : SelMgr->GetSelectedActors()) + { + if (Cast(Weak.Get())) + { + EditorPC->BeginSplineEditing(Weak.Get()); + break; + } + } + } + } + return FReply::Handled(); + }) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Edit Spline"))) + .Font(FPS_Editor_UIStyle::SmallFont(S)) + .ColorAndOpacity(FPS_Editor_UIStyle::Warn(S)) + ] + ] + // Edit Children button + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(4.0f, 0.0f) + [ + SAssignNew(ChildEditButton, SButton) + .ButtonStyle(StyleFlatButton.Get()) + .Visibility(EVisibility::Collapsed) + .ContentPadding(FMargin(8.0f, 3.0f)) + .OnClicked_Lambda([this]() -> FReply + { + if (APS_Editor_PlayerController* EditorPC = Cast(GetOwningPlayer())) + { + if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement) + { + EditorPC->FinishPointPlacement(); + } + else if (UPS_Editor_SelectionManager* SelMgr = SelectionManager.Get()) + { + for (const TWeakObjectPtr& Weak : SelMgr->GetSelectedActors()) + { + if (Weak.IsValid() && Weak->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass())) + { + EditorPC->BeginPointPlacementAppend(Weak.Get()); + break; + } + } + } + } + return FReply::Handled(); + }) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Edit Children"))) + .Font(FPS_Editor_UIStyle::SmallFont(S)) + .ColorAndOpacity(FPS_Editor_UIStyle::Info(S)) + ] + ] + // Delete Point button + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(4.0f, 0.0f) + [ + SAssignNew(SplineDeletePointButton, SButton) + .ButtonStyle(StyleFlatButton.Get()) + .Visibility(EVisibility::Collapsed) + .ContentPadding(FMargin(8.0f, 3.0f)) + .OnClicked_Lambda([this]() -> FReply + { + if (APS_Editor_Pawn* Pawn = Cast(GetOwningPlayerPawn())) + { + Pawn->HandleDelete(FInputActionValue()); + } + return FReply::Handled(); + }) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Delete Point"))) + .Font(FPS_Editor_UIStyle::SmallFont(S)) + .ColorAndOpacity(FPS_Editor_UIStyle::Err(S)) + ] + ] + // Done button + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(4.0f, 0.0f) + [ + SAssignNew(SplineFinishButton, SButton) + .ButtonStyle(StyleFlatButton.Get()) + .Visibility(EVisibility::Collapsed) + .ContentPadding(FMargin(10.0f, 3.0f)) + .OnClicked_Lambda([this]() -> FReply + { + if (APS_Editor_PlayerController* EditorPC = Cast(GetOwningPlayer())) + { + if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement) + EditorPC->FinishPointPlacement(); + else if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement) + EditorPC->FinishSplineEditing(); + } + return FReply::Handled(); + }) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Done"))) + .Font(FPS_Editor_UIStyle::SmallFont(S)) + .ColorAndOpacity(FPS_Editor_UIStyle::Ok(S)) + ] + ] + ]; + + SplineEditBar = Bar; + // Hidden by default — made visible when any of its child buttons becomes visible. + Bar->SetVisibility(EVisibility::Collapsed); + return Bar; +} + +TSharedRef UPS_Editor_MainWidget::BuildToast() +{ + const UPS_Editor_UIStyleAsset* S = StyleAsset; + + TSharedRef Toast = SNew(SBorder) + .BorderImage(BrushGlassStrong.Get()) + .Padding(FMargin(14.0f, 6.0f)) + [ + SAssignNew(ToastTextBlock, STextBlock) + .Text(FText::FromString(TEXT(""))) + .Font(FPS_Editor_UIStyle::BodyFont(S)) + .ColorAndOpacity(FPS_Editor_UIStyle::TextPrimary(S)) + ]; + + ToastPanel = Toast; + Toast->SetVisibility(EVisibility::Collapsed); + return Toast; +} + +TSharedRef UPS_Editor_MainWidget::BuildCommandPalette() +{ + const UPS_Editor_UIStyleAsset* S = StyleAsset; + + TSharedRef Palette = SNew(SBox).WidthOverride(520.0f) + [ + SNew(SBorder) + .BorderImage(BrushGlassStrong.Get()) + .Padding(FMargin(8.0f)) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot().AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(6.0f, 0.0f, 8.0f, 0.0f) + [ + FPS_Editor_UIStyle::MakeIconFollowForeground(S, EPS_Editor_Icon::Search, 14.0f) + ] + + SHorizontalBox::Slot().FillWidth(1.0f) + [ + SAssignNew(CmdPaletteInput, SEditableTextBox) + .Style(StyleTextBox.Get()) + .HintText(FText::FromString(TEXT("Type a command or search..."))) + .Font(FPS_Editor_UIStyle::BodyFont(S)) + .OnTextChanged_Lambda([this](const FText& NewText) + { + RefreshCommandPaletteList(NewText.ToString()); + }) + .OnTextCommitted_Lambda([this](const FText& Text, ETextCommit::Type CommitType) + { + if (CommitType == ETextCommit::OnEnter) + { + RefreshCommandPaletteList(Text.ToString()); + } + }) + ] + ] + + SVerticalBox::Slot().AutoHeight().Padding(0.0f, 6.0f, 0.0f, 0.0f) + [ + SNew(SBox).MaxDesiredHeight(360.0f) + [ + SNew(SScrollBox) + .ConsumeMouseWheel(EConsumeMouseWheel::Always) + + SScrollBox::Slot() + [ + SAssignNew(CmdPaletteList, SVerticalBox) + ] + ] + ] + ] + ]; + + CmdPalettePanel = Palette; + Palette->SetVisibility(EVisibility::Collapsed); + return Palette; +} + +void UPS_Editor_MainWidget::ShowToast(const FString& Message, FLinearColor Color) +{ + if (!ToastTextBlock.IsValid() || !ToastPanel.IsValid()) return; + + ToastTextBlock->SetText(FText::FromString(Message)); + ToastTextBlock->SetColorAndOpacity(FSlateColor(Color)); + ToastPanel->SetVisibility(EVisibility::Visible); + + if (UWorld* World = GetWorld()) + { + World->GetTimerManager().ClearTimer(ToastTimerHandle); + World->GetTimerManager().SetTimer(ToastTimerHandle, FTimerDelegate::CreateLambda([this]() + { + if (ToastPanel.IsValid()) + { + ToastPanel->SetVisibility(EVisibility::Collapsed); + } + }), 2.0f, false); + } +} + +void UPS_Editor_MainWidget::ToggleCommandPalette() +{ + if (!CmdPalettePanel.IsValid()) return; + bCmdPaletteOpen = !bCmdPaletteOpen; + CmdPalettePanel->SetVisibility(bCmdPaletteOpen ? EVisibility::Visible : EVisibility::Collapsed); + if (bCmdPaletteOpen) + { + RefreshCommandPaletteList(TEXT("")); + if (CmdPaletteInput.IsValid()) + { + CmdPaletteInput->SetText(FText::GetEmpty()); + FSlateApplication::Get().SetKeyboardFocus(CmdPaletteInput.ToSharedRef()); + } + } +} + +void UPS_Editor_MainWidget::RefreshCommandPaletteList(const FString& Filter) +{ + if (!CmdPaletteList.IsValid()) return; + CmdPaletteList->ClearChildren(); + + struct FCmd { const TCHAR* Id; const TCHAR* Label; const TCHAR* Cat; const TCHAR* Shortcut; EPS_Editor_Icon Icon; }; + static const FCmd Commands[] = { + { TEXT("tool_translate"), TEXT("Translate tool"), TEXT("Tool"), TEXT("Z"), EPS_Editor_Icon::Move }, + { TEXT("tool_rotate"), TEXT("Rotate tool"), TEXT("Tool"), TEXT("E"), EPS_Editor_Icon::Rotate }, + { TEXT("tool_scale"), TEXT("Scale tool"), TEXT("Tool"), TEXT("R"), EPS_Editor_Icon::Scale }, + { TEXT("snap_ground"), TEXT("Snap to ground"), TEXT("Tool"), TEXT(""), EPS_Editor_Icon::Snap }, + { TEXT("new_scenario"), TEXT("New scenario..."), TEXT("File"), TEXT(""), EPS_Editor_Icon::NewFile }, + { TEXT("save"), TEXT("Save scenario"), TEXT("File"), TEXT(""), EPS_Editor_Icon::Save }, + { TEXT("simulate"), TEXT("Toggle simulation"), TEXT("Run"), TEXT(""), EPS_Editor_Icon::Simulate }, + { TEXT("timeline"), TEXT("Toggle timeline"), TEXT("View"), TEXT(""), EPS_Editor_Icon::PanelBottom }, + { TEXT("close"), TEXT("Close editor"), TEXT("App"), TEXT(""), EPS_Editor_Icon::Close }, + }; + + const FString LowerFilter = Filter.ToLower(); + const UPS_Editor_UIStyleAsset* S = StyleAsset; + + for (const FCmd& Cmd : Commands) + { + const FString Label(Cmd.Label); + const FString Cat(Cmd.Cat); + if (!LowerFilter.IsEmpty()) + { + if (!Label.ToLower().Contains(LowerFilter) && !Cat.ToLower().Contains(LowerFilter)) continue; + } + + const FString CmdId(Cmd.Id); + CmdPaletteList->AddSlot().AutoHeight().Padding(0.0f, 1.0f) + [ + SNew(SButton) + .ButtonStyle(FCoreStyle::Get(), "NoBorder") + .ContentPadding(FMargin(6.0f, 4.0f)) + .OnClicked_Lambda([this, CmdId]() -> FReply + { + ExecuteCommand(CmdId); + ToggleCommandPalette(); + return FReply::Handled(); + }) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0.0f, 0.0f, 8.0f, 0.0f) + [ + FPS_Editor_UIStyle::MakeIcon(S, Cmd.Icon, 14.0f, FPS_Editor_UIStyle::TextMuted(S)) + ] + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(FText::FromString(FString::Printf(TEXT("%s \u00B7 %s"), *Cat, *Label))) + .Font(FPS_Editor_UIStyle::BodyFont(S)) + .ColorAndOpacity(FPS_Editor_UIStyle::TextPrimary(S)) + ] + + SHorizontalBox::Slot().FillWidth(1.0f) + [ + SNullWidget::NullWidget + ] + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(FText::FromString(Cmd.Shortcut)) + .Font(FPS_Editor_UIStyle::SmallFont(S)) + .ColorAndOpacity(FPS_Editor_UIStyle::TextDim(S)) + ] + ] + ]; + } +} + +void UPS_Editor_MainWidget::ExecuteCommand(const FString& CommandId) +{ + APS_Editor_PlayerController* PC = Cast(GetOwningPlayer()); + + if (CommandId == TEXT("tool_translate")) { OnModeButtonClicked(EPS_Editor_TransformMode::Translate); } + else if (CommandId == TEXT("tool_rotate")) { OnModeButtonClicked(EPS_Editor_TransformMode::Rotate); } + else if (CommandId == TEXT("tool_scale")) { OnModeButtonClicked(EPS_Editor_TransformMode::Scale); } + else if (CommandId == TEXT("snap_ground")) + { + if (UPS_Editor_SelectionManager* SM = SelectionManager.Get()) { SM->SnapSelectionToGround(); } + } + else if (CommandId == TEXT("new_scenario")) { OpenBaseLevelPopup(); } + else if (CommandId == TEXT("simulate")) + { + if (PC) + { + if (PC->IsSimulating()) PC->StopSimulation(); else PC->StartSimulation(); + } + } + else if (CommandId == TEXT("timeline")) { ToggleTimelineVisible(); } + else if (CommandId == TEXT("close")) { OnCloseClicked(); } + else if (CommandId == TEXT("save")) + { + // Trigger the Save button logic via toast message. + ShowToast(TEXT("Use the Save button in the Scene panel."), FPS_Editor_UIStyle::Warn(StyleAsset)); + } +} + +FReply UPS_Editor_MainWidget::NativeOnKeyDown(const FGeometry& InGeometry, const FKeyEvent& InKeyEvent) +{ + const bool bCtrl = InKeyEvent.IsControlDown() || InKeyEvent.IsCommandDown(); + + if (bCtrl && InKeyEvent.GetKey() == EKeys::K) + { + ToggleCommandPalette(); + return FReply::Handled(); + } + + if (bCmdPaletteOpen && InKeyEvent.GetKey() == EKeys::Escape) + { + ToggleCommandPalette(); + return FReply::Handled(); + } + + return Super::NativeOnKeyDown(InGeometry, InKeyEvent); +} diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.h index 042efa3..048c4ba 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget.h @@ -3,11 +3,13 @@ #include "CoreMinimal.h" #include "Blueprint/UserWidget.h" #include "PS_Editor_SelectionTypes.h" +#include "Styling/SlateTypes.h" #include "PS_Editor_MainWidget.generated.h" class UPS_Editor_SelectionManager; class UPS_Editor_SpawnManager; class UPS_Editor_SceneSerializer; +class UPS_Editor_UIStyleAsset; /** Widget state for a single dynamic property row in the property panel. */ struct FPS_Editor_PropertyRowWidgets @@ -63,6 +65,26 @@ private: UPROPERTY() TWeakObjectPtr SceneSerializer; + /** UI style configuration. Resolved once at RebuildWidget entry via soft path. + * Nullable — all FPS_Editor_UIStyle accessors fall back to hardcoded defaults. */ + UPROPERTY() + TObjectPtr StyleAsset; + + // Cached brushes / styles built from the style asset. Lifetime-owned by the widget so + // Slate can reference them via raw pointer in Build* methods. + TSharedPtr BrushGlassStrong; + TSharedPtr BrushGlassSoft; + TSharedPtr BrushPill; + TSharedPtr BrushField; + TSharedPtr BrushAccent; + TSharedPtr BrushAccentSoft; + TSharedPtr StyleFlatButton; + TSharedPtr StyleAccentButton; + TSharedPtr StyleTextBox; + + void EnsureStyleResolved(); + void EnsureStyleBrushes(); + // ---- Slate widget refs for dynamic updates ---- TSharedPtr ModeText; TSharedPtr SelectionInfoText; @@ -144,6 +166,30 @@ private: // Loading screen TSharedPtr LoadingOverlay; + // HUDs (new design) + TSharedPtr StatsFpsText; + TSharedPtr StatsTriText; + TSharedPtr StatsDrawText; + float SmoothedFps = 60.0f; + + TSharedPtr HintText; + TSharedPtr SplineEditBar; + + TSharedPtr ToastPanel; + TSharedPtr ToastTextBlock; + FTimerHandle ToastTimerHandle; + + TSharedPtr CmdPalettePanel; + TSharedPtr CmdPaletteInput; + TSharedPtr CmdPaletteList; + bool bCmdPaletteOpen = false; + + TSharedPtr TopbarSceneName; + TSharedPtr TopbarStatusDot; + TSharedPtr TopbarSimulateButton; + TSharedPtr TopbarSimulateText; + TSharedPtr FileMenuAnchor; + public: /** Show/hide the loading overlay. */ void ShowLoadingScreen(bool bShow); @@ -155,6 +201,26 @@ public: TSharedRef BuildSceneButtons(); TSharedRef BuildOutliner(); TSharedRef BuildTimelinePanel(); + TSharedRef BuildTopbar(); + TSharedRef BuildFileMenuContent(); + TSharedRef BuildViewCube(); + TSharedRef BuildStatsHUD(); + TSharedRef BuildHintHUD(); + TSharedRef BuildSplineEditBar(); + TSharedRef BuildToast(); + TSharedRef BuildCommandPalette(); + + /** Show a transient toast message. Hidden automatically after ~1.8s. */ + void ShowToast(const FString& Message, FLinearColor Color = FLinearColor::White); + + /** Toggle visibility of the command palette overlay. */ + void ToggleCommandPalette(); + void RefreshCommandPaletteList(const FString& Filter); + void ExecuteCommand(const FString& CommandId); + + /** Keyboard handler (captures Ctrl+K). */ + virtual FReply NativeOnKeyDown(const FGeometry& InGeometry, const FKeyEvent& InKeyEvent) override; + virtual bool NativeSupportsKeyboardFocus() const override { return true; } void RefreshOutliner(); void PopulateSceneList(); void PopulateSpawnCatalog(); diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget_Legacy.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget_Legacy.cpp new file mode 100644 index 0000000..ac92e41 --- /dev/null +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget_Legacy.cpp @@ -0,0 +1,3684 @@ +#include "PS_Editor_MainWidget_Legacy.h" +#include "PS_Editor_SelectionManager.h" +#include "PS_Editor_SpawnManager.h" +#include "PS_Editor_SceneSerializer.h" +#include "PS_Editor_SceneLoader.h" +#include "PS_Editor_UndoManager.h" +#include "PS_Editor_PlayerController.h" +#include "PS_Editor_EditableComponent.h" +#include "PS_Editor_EditableInterface.h" +#include "PS_Editor_SpawnableComponent.h" +#include "PS_Editor_SplineActor.h" +#include "PS_Editor_SplineEditable.h" +#include "PS_Editor_ChildMovable.h" +#include "PS_Editor_Pawn.h" +#include "PS_Editor_PointPlaceable.h" +#include "Widgets/SBoxPanel.h" +#include "Widgets/Text/STextBlock.h" +#include "Widgets/Layout/SBorder.h" +#include "Widgets/Layout/SScrollBox.h" +#include "Widgets/Input/SButton.h" +#include "Widgets/Images/SImage.h" +#include "Widgets/Input/SCheckBox.h" +#include "Widgets/Input/SComboBox.h" +#include "Engine/Texture2D.h" +#include "Widgets/Input/SEditableTextBox.h" +#include "Widgets/Input/SSlider.h" +#include "Widgets/Layout/SConstraintCanvas.h" +#include "Widgets/SOverlay.h" +#include "UObject/UnrealType.h" +#include "Kismet/GameplayStatics.h" +#include "DesktopPlatformModule.h" +#include "PS_Editor_GameInstanceSubsystem.h" +#include "PS_Editor_TimelineSubsystem.h" +#include "Engine/World.h" + +// Forward declaration — the real definition lives further down with the timeline UI block, +// but the toolbar Simulate button + NativeTick both need to look up the subsystem too. +static UPS_Editor_TimelineSubsystem* GetTimelineSubsystemFromWidget(UPS_Editor_MainWidget_Legacy* Widget); + +void UPS_Editor_MainWidget_Legacy::SetSelectionManager(UPS_Editor_SelectionManager* InSelectionManager) +{ + SelectionManager = InSelectionManager; +} + +void UPS_Editor_MainWidget_Legacy::SetSpawnManager(UPS_Editor_SpawnManager* InSpawnManager) +{ + SpawnManager = InSpawnManager; +} + +void UPS_Editor_MainWidget_Legacy::SetSceneSerializer(UPS_Editor_SceneSerializer* InSerializer) +{ + SceneSerializer = InSerializer; +} + +TSharedRef UPS_Editor_MainWidget_Legacy::RebuildWidget() +{ + return SNew(SOverlay) + // Loading screen (behind everything — first slot = bottom layer) + + SOverlay::Slot() + .HAlign(HAlign_Fill) + .VAlign(VAlign_Fill) + [ + SAssignNew(LoadingOverlay, SBorder) + .Visibility(EVisibility::Visible) + .BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush")) + .BorderBackgroundColor(FLinearColor(0.0f, 0.0f, 0.0f, 1.0f)) + [ + SNew(SBox) + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Loading..."))) + .Font(FCoreStyle::GetDefaultFontStyle("Bold", 18)) + .ColorAndOpacity(FLinearColor(0.7f, 0.7f, 0.7f, 1.0f)) + ] + ] + ] + // Top-left: Title + Toolbar + + SOverlay::Slot() + .HAlign(HAlign_Left) + .VAlign(VAlign_Top) + .Padding(16.0f) + [ + SNew(SVerticalBox) + // Title bar + + SVerticalBox::Slot() + .AutoHeight() + [ + SNew(SBorder) + .BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush")) + .BorderBackgroundColor(FLinearColor(0.05f, 0.05f, 0.05f, 1.0f)) + .Padding(FMargin(12.0f, 8.0f)) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("PS Editor"))) + .ColorAndOpacity(FLinearColor(0.2f, 0.7f, 1.0f, 1.0f)) + .Font(FCoreStyle::GetDefaultFontStyle("Bold", 14)) + ] + + SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Center) + .Padding(12.0f, 0.0f, 0.0f, 0.0f) + [ + SAssignNew(ModeText, STextBlock) + .Text(FText::FromString(TEXT("Translate (Z)"))) + .ColorAndOpacity(FLinearColor(0.7f, 0.7f, 0.7f, 1.0f)) + .Font(FCoreStyle::GetDefaultFontStyle("Regular", 11)) + ] + + SHorizontalBox::Slot() + .FillWidth(1.0f) + [ + SNullWidget::NullWidget + ] + + SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Center) + .Padding(8.0f, 0.0f, 0.0f, 0.0f) + [ + SNew(SButton) + .OnClicked_Lambda([this]() -> FReply + { + OnCloseClicked(); + return FReply::Handled(); + }) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("X"))) + .Font(FCoreStyle::GetDefaultFontStyle("Bold", 12)) + .ColorAndOpacity(FLinearColor(1.0f, 0.3f, 0.3f)) + ] + ] + ] + ] + // Toolbar + + SVerticalBox::Slot() + .AutoHeight() + .Padding(0.0f, 4.0f, 0.0f, 0.0f) + [ + BuildToolbar() + ] + // Scene save/load + + SVerticalBox::Slot() + .AutoHeight() + .Padding(0.0f, 4.0f, 0.0f, 0.0f) + [ + BuildSceneButtons() + ] + ] + // Left panel: Spawn Catalogue + + SOverlay::Slot() + .HAlign(HAlign_Left) + .VAlign(VAlign_Center) + .Padding(16.0f, 80.0f, 0.0f, 60.0f) + [ + BuildSpawnCatalog() + ] + // Right panel: Properties + Outliner + + SOverlay::Slot() + .HAlign(HAlign_Right) + .VAlign(VAlign_Top) + .Padding(16.0f, 16.0f, 16.0f, 60.0f) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + .AutoHeight() + [ + BuildPropertiesPanel() + ] + + SVerticalBox::Slot() + .FillHeight(1.0f) + .Padding(0.0f, 4.0f, 0.0f, 0.0f) + [ + BuildOutliner() + ] + ] + // Timeline panel (hidden by default; toggled via toolbar button) + + SOverlay::Slot() + .HAlign(HAlign_Fill) + .VAlign(VAlign_Bottom) + .Padding(16.0f, 0.0f, 16.0f, 60.0f) + [ + BuildTimelinePanel() + ] + // Bottom notification / help bar + + SOverlay::Slot() + .HAlign(HAlign_Center) + .VAlign(VAlign_Bottom) + .Padding(16.0f) + [ + SNew(SBorder) + .BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush")) + .BorderBackgroundColor(FLinearColor(0.05f, 0.05f, 0.05f, 1.0f)) + .Padding(FMargin(16.0f, 6.0f)) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot().FillWidth(1.0f).VAlign(VAlign_Center) + [ + SAssignNew(HelpBarText, STextBlock) + .Text(FText::FromString(TEXT("LMB: Select/Pan | RMB: Fly | Alt+LMB/MMB: Orbit | Z/E/R: Translate/Rotate/Scale | End: Snap | Del: Delete | S: Spline"))) + .ColorAndOpacity(FLinearColor(0.6f, 0.6f, 0.6f, 1.0f)) + .Font(FCoreStyle::GetDefaultFontStyle("Regular", 9)) + ] + + SHorizontalBox::Slot().AutoWidth().Padding(12.0f, 0.0f, 0.0f, 0.0f).VAlign(VAlign_Center) + [ + SAssignNew(SplineEditButton, SButton) + .Visibility(EVisibility::Collapsed) + .OnClicked_Lambda([this]() -> FReply + { + if (APS_Editor_PlayerController* EditorPC = Cast(GetOwningPlayer())) + { + if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement) + { + EditorPC->FinishSplineEditing(); + } + else if (UPS_Editor_SelectionManager* SelMgr = SelectionManager.Get()) + { + for (const TWeakObjectPtr& Weak : SelMgr->GetSelectedActors()) + { + if (Cast(Weak.Get())) + { + EditorPC->BeginSplineEditing(Weak.Get()); + break; + } + } + } + } + return FReply::Handled(); + }) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Edit Spline"))) + .Font(FCoreStyle::GetDefaultFontStyle("Bold", 10)) + .ColorAndOpacity(FLinearColor(1.0f, 0.6f, 0.0f)) + ] + ] + + SHorizontalBox::Slot().AutoWidth().Padding(8.0f, 0.0f, 0.0f, 0.0f).VAlign(VAlign_Center) + [ + SAssignNew(ChildEditButton, SButton) + .Visibility(EVisibility::Collapsed) + .OnClicked_Lambda([this]() -> FReply + { + if (APS_Editor_PlayerController* EditorPC = Cast(GetOwningPlayer())) + { + if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement) + { + EditorPC->FinishPointPlacement(); + } + else if (UPS_Editor_SelectionManager* SelMgr = SelectionManager.Get()) + { + for (const TWeakObjectPtr& Weak : SelMgr->GetSelectedActors()) + { + if (Weak.IsValid() && Weak->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass())) + { + EditorPC->BeginPointPlacementAppend(Weak.Get()); + break; + } + } + } + } + return FReply::Handled(); + }) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Edit Children"))) + .Font(FCoreStyle::GetDefaultFontStyle("Bold", 10)) + .ColorAndOpacity(FLinearColor(0.3f, 0.8f, 1.0f)) + ] + ] + + SHorizontalBox::Slot().AutoWidth().Padding(8.0f, 0.0f, 0.0f, 0.0f).VAlign(VAlign_Center) + [ + SAssignNew(SplineDeletePointButton, SButton) + .Visibility(EVisibility::Collapsed) + .OnClicked_Lambda([this]() -> FReply + { + // Simulate Delete key press for the selected spline point + if (APS_Editor_Pawn* Pawn = Cast(GetOwningPlayerPawn())) + { + Pawn->HandleDelete(FInputActionValue()); + } + return FReply::Handled(); + }) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Delete Point"))) + .Font(FCoreStyle::GetDefaultFontStyle("Bold", 10)) + .ColorAndOpacity(FLinearColor(1.0f, 0.3f, 0.2f)) + ] + ] + + SHorizontalBox::Slot().AutoWidth().Padding(8.0f, 0.0f, 0.0f, 0.0f).VAlign(VAlign_Center) + [ + SAssignNew(SplineFinishButton, SButton) + .Visibility(EVisibility::Collapsed) + .OnClicked_Lambda([this]() -> FReply + { + if (APS_Editor_PlayerController* EditorPC = Cast(GetOwningPlayer())) + { + if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement) + EditorPC->FinishPointPlacement(); + else if (EditorPC->GetEditorMode() == EPS_Editor_EditorMode::SplinePlacement) + EditorPC->FinishSplineEditing(); + } + return FReply::Handled(); + }) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Done"))) + .Font(FCoreStyle::GetDefaultFontStyle("Bold", 10)) + .ColorAndOpacity(FLinearColor(0.3f, 0.9f, 0.3f)) + ] + ] + ] + ] + // BaseLevel selection popup + + SOverlay::Slot() + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + [ + SAssignNew(BaseLevelPopup, SBorder) + .Visibility(EVisibility::Collapsed) + .BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush")) + .BorderBackgroundColor(FLinearColor(0.08f, 0.08f, 0.08f, 0.97f)) + .Padding(FMargin(24.0f, 16.0f)) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot().AutoHeight().HAlign(HAlign_Center).Padding(0.0f, 0.0f, 0.0f, 12.0f) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Select Base Level"))) + .Font(FCoreStyle::GetDefaultFontStyle("Bold", 14)) + .ColorAndOpacity(FLinearColor::White) + ] + + SVerticalBox::Slot().FillHeight(1.0f).MaxHeight(400.0f) + [ + SNew(SScrollBox) + .ConsumeMouseWheel(EConsumeMouseWheel::Always) + + SScrollBox::Slot() + [ + SAssignNew(BaseLevelGridContainer, SVerticalBox) + ] + ] + + SVerticalBox::Slot().AutoHeight().HAlign(HAlign_Center).Padding(0.0f, 12.0f, 0.0f, 0.0f) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot().AutoWidth().Padding(8.0f, 0.0f) + [ + SNew(SButton) + .OnClicked_Lambda([this]() -> FReply + { + ConfirmBaseLevelSelection(); + return FReply::Handled(); + }) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Validate"))) + .Font(FCoreStyle::GetDefaultFontStyle("Bold", 11)) + .ColorAndOpacity(FLinearColor(0.2f, 0.9f, 0.2f)) + ] + ] + + SHorizontalBox::Slot().AutoWidth().Padding(8.0f, 0.0f) + [ + SNew(SButton) + .OnClicked_Lambda([this]() -> FReply + { + CancelBaseLevelPopup(); + return FReply::Handled(); + }) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Cancel"))) + .Font(FCoreStyle::GetDefaultFontStyle("Regular", 11)) + ] + ] + ] + ] + ] + // Close confirmation overlay + + SOverlay::Slot() + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + [ + SAssignNew(CloseConfirmOverlay, SBorder) + .Visibility(EVisibility::Collapsed) + .BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush")) + .BorderBackgroundColor(FLinearColor(0.08f, 0.08f, 0.08f, 0.97f)) + .Padding(FMargin(32.0f, 20.0f)) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot().AutoHeight().HAlign(HAlign_Center).Padding(0.0f, 0.0f, 0.0f, 16.0f) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Save changes before closing?"))) + .Font(FCoreStyle::GetDefaultFontStyle("Bold", 13)) + .ColorAndOpacity(FLinearColor::White) + ] + + SVerticalBox::Slot().AutoHeight().HAlign(HAlign_Center) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot().AutoWidth().Padding(6.0f, 0.0f) + [ + SNew(SButton) + .OnClicked_Lambda([this]() -> FReply + { + ExecuteClose(true); + return FReply::Handled(); + }) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Save & Close"))) + .Font(FCoreStyle::GetDefaultFontStyle("Bold", 11)) + .ColorAndOpacity(FLinearColor(0.2f, 0.9f, 0.2f)) + ] + ] + + SHorizontalBox::Slot().AutoWidth().Padding(6.0f, 0.0f) + [ + SNew(SButton) + .OnClicked_Lambda([this]() -> FReply + { + ExecuteClose(false); + return FReply::Handled(); + }) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Close without saving"))) + .Font(FCoreStyle::GetDefaultFontStyle("Regular", 11)) + .ColorAndOpacity(FLinearColor(1.0f, 0.5f, 0.3f)) + ] + ] + + SHorizontalBox::Slot().AutoWidth().Padding(6.0f, 0.0f) + [ + SNew(SButton) + .OnClicked_Lambda([this]() -> FReply + { + if (CloseConfirmOverlay.IsValid()) + CloseConfirmOverlay->SetVisibility(EVisibility::Collapsed); + return FReply::Handled(); + }) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Cancel"))) + .Font(FCoreStyle::GetDefaultFontStyle("Regular", 11)) + ] + ] + ] + ] + ]; +} + +TSharedRef UPS_Editor_MainWidget_Legacy::BuildToolbar() +{ + auto MakeButton = [this](const FText& Label, EPS_Editor_TransformMode Mode) -> TSharedRef + { + return SNew(SButton) + .OnClicked_Lambda([this, Mode]() -> FReply + { + OnModeButtonClicked(Mode); + return FReply::Handled(); + }) + [ + SNew(STextBlock) + .Text(Label) + .Font(FCoreStyle::GetDefaultFontStyle("Regular", 10)) + .Justification(ETextJustify::Center) + ]; + }; + + return SNew(SBorder) + .BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush")) + .BorderBackgroundColor(FLinearColor(0.05f, 0.05f, 0.05f, 1.0f)) + .Padding(FMargin(8.0f, 4.0f)) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot().AutoWidth().Padding(2.0f, 0.0f) + [ MakeButton(FText::FromString(TEXT("Translate (Z)")), EPS_Editor_TransformMode::Translate) ] + + SHorizontalBox::Slot().AutoWidth().Padding(2.0f, 0.0f) + [ MakeButton(FText::FromString(TEXT("Rotate (E)")), EPS_Editor_TransformMode::Rotate) ] + + SHorizontalBox::Slot().AutoWidth().Padding(2.0f, 0.0f) + [ MakeButton(FText::FromString(TEXT("Scale (R)")), EPS_Editor_TransformMode::Scale) ] + + SHorizontalBox::Slot().AutoWidth().Padding(8.0f, 0.0f, 2.0f, 0.0f) + [ + SNew(SButton) + .OnClicked_Lambda([this]() -> FReply + { + if (UPS_Editor_SelectionManager* SM = SelectionManager.Get()) + { + SM->SnapSelectionToGround(); + } + return FReply::Handled(); + }) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Snap Ground"))) + .Font(FCoreStyle::GetDefaultFontStyle("Regular", 10)) + ] + ] + // World/Local toggle + + SHorizontalBox::Slot().AutoWidth().Padding(8.0f, 0.0f, 2.0f, 0.0f) + [ + SNew(SButton) + .OnClicked_Lambda([this]() -> FReply + { + if (UPS_Editor_SelectionManager* SM = SelectionManager.Get()) + { + SM->ToggleTransformSpace(); + } + return FReply::Handled(); + }) + [ + SAssignNew(SpaceText, STextBlock) + .Text(FText::FromString(TEXT("World"))) + .Font(FCoreStyle::GetDefaultFontStyle("Regular", 10)) + ] + ] + // Snap toggle + value + + SHorizontalBox::Slot().AutoWidth().Padding(8.0f, 0.0f, 2.0f, 0.0f) + [ + SNew(SButton) + .OnClicked_Lambda([this]() -> FReply + { + if (UPS_Editor_SelectionManager* SM = SelectionManager.Get()) + { + SM->bSnapTranslation = !SM->bSnapTranslation; + SM->bSnapRotation = SM->bSnapTranslation; + SM->bSnapScale = SM->bSnapTranslation; + } + return FReply::Handled(); + }) + [ + SAssignNew(SnapText, STextBlock) + .Text(FText::FromString(TEXT("Snap: Off"))) + .Font(FCoreStyle::GetDefaultFontStyle("Regular", 10)) + ] + ] + + SHorizontalBox::Slot().AutoWidth().Padding(2.0f, 0.0f).VAlign(VAlign_Center) + [ + SNew(SBox).MinDesiredWidth(40.0f) + [ + SAssignNew(SnapSizeField, SEditableTextBox) + .Font(FCoreStyle::GetDefaultFontStyle("Regular", 9)) + .HintText(FText::FromString(TEXT("10"))) + .OnTextCommitted_Lambda([this](const FText& Text, ETextCommit::Type) + { + if (UPS_Editor_SelectionManager* SM = SelectionManager.Get()) + { + float Val = FCString::Atof(*Text.ToString()); + if (Val > 0.0f) + { + SM->TranslationSnapSize = Val; + SM->RotationSnapSize = Val; + SM->ScaleSnapSize = Val * 0.01f; + } + } + }) + ] + ] + ]; +} + +TSharedRef UPS_Editor_MainWidget_Legacy::BuildPropertiesPanel() +{ + const FMargin RowPadding(0.0f, 2.0f); + const FSlateFontInfo LabelFont = FCoreStyle::GetDefaultFontStyle("Regular", 9); + const FSlateFontInfo ValueFont = FCoreStyle::GetDefaultFontStyle("Regular", 9); + const float LabelWidth = 14.0f; + const float FieldWidth = 70.0f; + + auto MakeRow = [&](const FText& Label, TSharedPtr& X, TSharedPtr& Y, TSharedPtr& Z, const FLinearColor& ColorX, const FLinearColor& ColorY, const FLinearColor& ColorZ) -> TSharedRef + { + auto MakeField = [&](TSharedPtr& Field, const FLinearColor& Col) -> TSharedRef + { + return SNew(SHorizontalBox) + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(4.0f, 0.0f, 2.0f, 0.0f) + [ + SNew(SBorder) + .BorderBackgroundColor(Col) + .Padding(FMargin(3.0f, 1.0f)) + [ + SNew(STextBlock) + .Font(LabelFont) + .ColorAndOpacity(FLinearColor::White) + ] + ] + + SHorizontalBox::Slot() + .FillWidth(1.0f) + [ + SAssignNew(Field, SEditableTextBox) + .Font(ValueFont) + .MinDesiredWidth(FieldWidth) + .OnTextCommitted_Lambda([this](const FText&, ETextCommit::Type) + { + ApplyTransformFromUI(); + }) + ]; + }; + + return SNew(SHorizontalBox) + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(Label) + .Font(LabelFont) + .ColorAndOpacity(FLinearColor(0.7f, 0.7f, 0.7f)) + .MinDesiredWidth(LabelWidth) + ] + + SHorizontalBox::Slot().FillWidth(1.0f) + [ MakeField(X, ColorX) ] + + SHorizontalBox::Slot().FillWidth(1.0f) + [ MakeField(Y, ColorY) ] + + SHorizontalBox::Slot().FillWidth(1.0f) + [ MakeField(Z, ColorZ) ]; + }; + + const FLinearColor RedX(0.8f, 0.15f, 0.1f); + const FLinearColor GreenY(0.15f, 0.6f, 0.1f); + const FLinearColor BlueZ(0.1f, 0.3f, 0.8f); + + return SNew(SBorder) + .BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush")) + .BorderBackgroundColor(FLinearColor(0.05f, 0.05f, 0.05f, 1.0f)) + .Padding(FMargin(10.0f, 8.0f)) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot().AutoHeight() + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot().FillWidth(1.0f) + [ + SAssignNew(SelectionInfoText, STextBlock) + .Text(FText::FromString(TEXT("No selection"))) + .Font(FCoreStyle::GetDefaultFontStyle("Bold", 10)) + .ColorAndOpacity(FLinearColor(0.8f, 0.8f, 0.8f)) + ] + ] + + SVerticalBox::Slot().AutoHeight().Padding(RowPadding) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Location"))) + .Font(FCoreStyle::GetDefaultFontStyle("Regular", 9)) + .ColorAndOpacity(FLinearColor(0.5f, 0.5f, 0.5f)) + ] + + SVerticalBox::Slot().AutoHeight().Padding(RowPadding) + [ MakeRow(FText::GetEmpty(), LocX, LocY, LocZ, RedX, GreenY, BlueZ) ] + + SVerticalBox::Slot().AutoHeight().Padding(RowPadding) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Rotation"))) + .Font(FCoreStyle::GetDefaultFontStyle("Regular", 9)) + .ColorAndOpacity(FLinearColor(0.5f, 0.5f, 0.5f)) + ] + + SVerticalBox::Slot().AutoHeight().Padding(RowPadding) + [ MakeRow(FText::GetEmpty(), RotX, RotY, RotZ, RedX, GreenY, BlueZ) ] + + SVerticalBox::Slot().AutoHeight().Padding(RowPadding) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Scale"))) + .Font(FCoreStyle::GetDefaultFontStyle("Regular", 9)) + .ColorAndOpacity(FLinearColor(0.5f, 0.5f, 0.5f)) + ] + + SVerticalBox::Slot().AutoHeight().Padding(RowPadding) + [ MakeRow(FText::GetEmpty(), ScaX, ScaY, ScaZ, RedX, GreenY, BlueZ) ] + + SVerticalBox::Slot().AutoHeight() + [ + SAssignNew(DynamicPropertiesContainer, SVerticalBox) + ] + ]; +} + +TSharedRef UPS_Editor_MainWidget_Legacy::BuildSceneButtons() +{ + return SNew(SBorder) + .BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush")) + .BorderBackgroundColor(FLinearColor(0.05f, 0.05f, 0.05f, 1.0f)) + .Padding(FMargin(8.0f, 4.0f)) + [ + SNew(SVerticalBox) + // Save row: text field + save button + + SVerticalBox::Slot() + .AutoHeight() + .Padding(0.0f, 2.0f) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .FillWidth(1.0f) + .Padding(0.0f, 0.0f, 4.0f, 0.0f) + [ + SAssignNew(SaveNameField, SEditableTextBox) + .HintText(FText::FromString(TEXT("Scenario name..."))) + .Font(FCoreStyle::GetDefaultFontStyle("Regular", 10)) + ] + + SHorizontalBox::Slot() + .AutoWidth() + [ + SNew(SButton) + .OnClicked_Lambda([this]() -> FReply + { + // Block save during simulation + if (APS_Editor_PlayerController* SimPC = Cast(GetOwningPlayer())) + { + if (SimPC->IsSimulating()) + { + if (HelpBarText.IsValid()) + { + HelpBarText->SetText(FText::FromString(TEXT("Cannot save during simulation. Stop simulation first."))); + HelpBarText->SetColorAndOpacity(FLinearColor(1.0f, 0.4f, 0.2f)); + } + return FReply::Handled(); + } + } + if (SaveNameField.IsValid() && SceneSerializer.IsValid()) + { + FString Name = SaveNameField->GetText().ToString(); + if (!Name.IsEmpty()) + { + // Get SpawnManager fresh from PlayerController (widget weak ptr may be stale in packaged) + UPS_Editor_SpawnManager* SaveSM = nullptr; + if (APS_Editor_PlayerController* EditorPC = Cast(GetOwningPlayer())) + { + SaveSM = EditorPC->GetSpawnManager(); + } + if (!SaveSM) SaveSM = SpawnManager.Get(); + + if (SaveSM) + { + SceneSerializer->SaveScene(Name, SaveSM); + bSceneDirty = false; + PopulateSceneList(); + } + } + } + return FReply::Handled(); + }) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Save"))) + .Font(FCoreStyle::GetDefaultFontStyle("Regular", 10)) + ] + ] + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(4.0f, 0.0f, 0.0f, 0.0f) + [ + SNew(SButton) + .OnClicked_Lambda([this]() -> FReply + { + IDesktopPlatform* DesktopPlatform = FDesktopPlatformModule::Get(); + if (!DesktopPlatform) return FReply::Handled(); + + const FString DefaultPath = UPS_Editor_SceneLoader::GetScenesDirectory(); + TArray OutFiles; + + void* ParentWindowHandle = FSlateApplication::Get().GetActiveTopLevelWindow().IsValid() + ? FSlateApplication::Get().GetActiveTopLevelWindow()->GetNativeWindow()->GetOSWindowHandle() + : nullptr; + + bool bOpened = DesktopPlatform->SaveFileDialog( + ParentWindowHandle, + TEXT("Save Scenario As"), + DefaultPath, + SaveNameField.IsValid() ? SaveNameField->GetText().ToString() + TEXT(".pss") : TEXT(""), + TEXT("Scenario Files (*.pss)|*.pss"), + 0, + OutFiles); + + if (bOpened && OutFiles.Num() > 0) + { + FString FileName = FPaths::GetBaseFilename(OutFiles[0]); + if (!FileName.IsEmpty() && SceneSerializer.IsValid() && SpawnManager.IsValid()) + { + SceneSerializer->SaveScene(FileName, SpawnManager.Get()); + bSceneDirty = false; + if (SaveNameField.IsValid()) + { + SaveNameField->SetText(FText::FromString(FileName)); + } + PopulateSceneList(); + } + } + return FReply::Handled(); + }) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Save As"))) + .Font(FCoreStyle::GetDefaultFontStyle("Regular", 10)) + ] + ] + ] + // Action buttons: New + Load list + + SVerticalBox::Slot() + .AutoHeight() + .Padding(0.0f, 2.0f) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(0.0f, 0.0f, 4.0f, 0.0f) + [ + SNew(SButton) + .OnClicked_Lambda([this]() -> FReply + { + if (SceneSerializer.IsValid() && SpawnManager.IsValid()) + { + SceneSerializer->ClearScene(SpawnManager.Get()); + bSceneDirty = false; + LastKnownSpawnedCount = 0; + } + return FReply::Handled(); + }) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("New Scenario"))) + .Font(FCoreStyle::GetDefaultFontStyle("Regular", 10)) + ] + ] + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(4.0f, 0.0f, 0.0f, 0.0f) + [ + SNew(SButton) + .OnClicked_Lambda([this]() -> FReply + { + APS_Editor_PlayerController* EditorPC = Cast(GetOwningPlayer()); + if (!EditorPC) return FReply::Handled(); + + // Auto-save current scene before playing + UPS_Editor_SpawnManager* PlaySM = EditorPC->GetSpawnManager(); + UPS_Editor_SceneSerializer* PlaySer = EditorPC->GetSceneSerializer(); + if (SaveNameField.IsValid() && PlaySer && PlaySM) + { + FString SceneName = SaveNameField->GetText().ToString(); + if (!SceneName.IsEmpty()) + { + PlaySer->SaveScene(SceneName, PlaySM); + } + } + + // Store scene name in GameInstance subsystem (survives OpenLevel) + FString BaseLevel = EditorPC->CurrentBaseLevel; + FString SceneName = SaveNameField.IsValid() ? SaveNameField->GetText().ToString() : TEXT(""); + + // Resolve map path: BaseLevel from catalog, or current map if none selected + FString MapPath; + if (!BaseLevel.IsEmpty()) + { + if (FString* Found = BaseLevelPathMap.Find(BaseLevel)) + { + MapPath = *Found; + } + else + { + MapPath = FString::Printf(TEXT("/Game/%s"), *BaseLevel); + } + } + else + { + // No BaseLevel = play on the current editor map + MapPath = EditorPC->GetWorld()->GetMapName(); + MapPath.RemoveFromStart(EditorPC->GetWorld()->StreamingLevelsPrefix); + BaseLevel = MapPath; + } + + UE_LOG(LogTemp, Log, TEXT("PS_Editor Play: BaseLevel='%s', Map='%s', Scenario='%s'"), *BaseLevel, *MapPath, *SceneName); + + if (UGameInstance* GI = EditorPC->GetGameInstance()) + { + if (UPS_Editor_GameInstanceSubsystem* Sub = GI->GetSubsystem()) + { + Sub->SetPendingScenario(SceneName, BaseLevel); + Sub->bIsPlayMode = true; + if (Sub->EditorMapName.IsEmpty()) + { + FString EdMapName = EditorPC->GetWorld()->GetMapName(); + EdMapName.RemoveFromStart(EditorPC->GetWorld()->StreamingLevelsPrefix); + Sub->EditorMapName = EdMapName; + } + } + } + + // Notify all spawned actors: leaving editor mode + UPS_Editor_SceneLoader::bIsEditorModeActive = false; + if (UPS_Editor_SpawnManager* PlaySM2 = EditorPC->GetSpawnManager()) + { + PlaySM2->NotifyEditorModeChanged(false); + } + + // Unload sublevel before OpenLevel to avoid name conflict + EditorPC->LoadBaseLevelAsSublevel(TEXT("None")); + + // Reset input mode before transition + EditorPC->bShowMouseCursor = false; + EditorPC->SetInputMode(FInputModeGameOnly()); + + UGameplayStatics::OpenLevel(EditorPC, FName(*MapPath), true, TEXT("")); + return FReply::Handled(); + }) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Play"))) + .Font(FCoreStyle::GetDefaultFontStyle("Bold", 10)) + .ColorAndOpacity(FLinearColor(0.2f, 0.9f, 0.2f)) + ] + ] + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(4.0f, 0.0f, 0.0f, 0.0f) + [ + SNew(SButton) + .ToolTipText(FText::FromString(TEXT("Start/stop simulation. If a timeline exists, also plays/pauses it in sync."))) + .OnClicked_Lambda([this]() -> FReply + { + APS_Editor_PlayerController* EditorPC = Cast(GetOwningPlayer()); + if (!EditorPC) return FReply::Handled(); + + UPS_Editor_TimelineSubsystem* TS = GetTimelineSubsystemFromWidget(this); + + // If there's a timeline with at least one track, route through the timeline + // so simulate + animation are always in lockstep (Simulate is effectively + // the Play/Pause button of the timeline). + if (TS && TS->GetNumTracks() > 0) + { + if (TS->IsPlaying()) + { + TS->Pause(); + } + else + { + TS->Play(); + } + } + else + { + // No animation to sync with — just toggle simulation directly. + if (EditorPC->IsSimulating()) + { + EditorPC->StopSimulation(); + } + else + { + EditorPC->StartSimulation(); + } + } + return FReply::Handled(); + }) + [ + SAssignNew(SimulateButtonText, STextBlock) + .Text(FText::FromString(TEXT("Simulate"))) + .Font(FCoreStyle::GetDefaultFontStyle("Bold", 10)) + .ColorAndOpacity(FLinearColor(1.0f, 0.6f, 0.0f)) + ] + ] + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(4.0f, 0.0f, 0.0f, 0.0f) + [ + SNew(SButton) + .ToolTipText(FText::FromString(TEXT("Show/hide the timeline panel"))) + .OnClicked_Lambda([this]() -> FReply + { + ToggleTimelineVisible(); + return FReply::Handled(); + }) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Timeline"))) + .Font(FCoreStyle::GetDefaultFontStyle("Bold", 10)) + .ColorAndOpacity(FLinearColor(0.6f, 0.8f, 1.0f)) + ] + ] + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(4.0f, 0.0f, 0.0f, 0.0f) + [ + SNew(SButton) + .OnClicked_Lambda([this]() -> FReply + { + IDesktopPlatform* DesktopPlatform = FDesktopPlatformModule::Get(); + if (!DesktopPlatform) return FReply::Handled(); + + const FString DefaultPath = UPS_Editor_SceneLoader::GetScenesDirectory(); + TArray OutFiles; + + void* ParentWindowHandle = FSlateApplication::Get().GetActiveTopLevelWindow().IsValid() + ? FSlateApplication::Get().GetActiveTopLevelWindow()->GetNativeWindow()->GetOSWindowHandle() + : nullptr; + + bool bOpened = DesktopPlatform->OpenFileDialog( + ParentWindowHandle, + TEXT("Load Scenario"), + DefaultPath, + TEXT(""), + TEXT("Scenario Files (*.pss)|*.pss"), + 0, + OutFiles); + + if (bOpened && OutFiles.Num() > 0) + { + FString FileName = FPaths::GetBaseFilename(OutFiles[0]); + + if (SceneSerializer.IsValid() && SpawnManager.IsValid()) + { + // Same base-level sync logic as the scene-list click path: + // fall back to the MasterCatalog's default entry when the scenario + // has no base level; otherwise keep the current sublevel. + FString SceneLevelName = UPS_Editor_SceneLoader::GetSceneLevelName(FileName); + bool bHasValidBaseLevel = !SceneLevelName.IsEmpty() + && SceneLevelName != TEXT("Unfiled") + && BaseLevelPathMap.Contains(SceneLevelName); + if (APS_Editor_PlayerController* EditorPC = Cast(GetOwningPlayer())) + { + if (!bHasValidBaseLevel && EditorPC->MasterCatalogAsset) + { + for (const FPS_Editor_BaseLevelEntry& Entry : EditorPC->MasterCatalogAsset->BaseLevels) + { + if (Entry.bIsDefault && !Entry.DisplayName.IsEmpty()) + { + SceneLevelName = Entry.DisplayName; + bHasValidBaseLevel = BaseLevelPathMap.Contains(SceneLevelName); + break; + } + } + } + + if (bHasValidBaseLevel) + { + if (EditorPC->CurrentBaseLevel != SceneLevelName) + { + FString SceneMapPath = SceneLevelName; + if (FString* Found = BaseLevelPathMap.Find(SceneLevelName)) + { + SceneMapPath = *Found; + } + TArray SceneExtras; + if (TArray* FoundExtras = BaseLevelExtraSublevels.Find(SceneLevelName)) + { + SceneExtras = *FoundExtras; + } + EditorPC->LoadBaseLevelAsSublevel(SceneLevelName, SceneMapPath, SceneExtras); + CurrentLevelFilter = EditorPC->CurrentBaseLevel; + for (const TSharedPtr& Opt : BaseLevelOptions) + { + if (*Opt == SceneLevelName) + { + if (BaseLevelButtonText.IsValid()) BaseLevelButtonText->SetText(FText::FromString(SceneLevelName)); + break; + } + } + } + } + } + + SceneSerializer->LoadScene(FileName, SpawnManager.Get()); + bSceneDirty = false; + if (SaveNameField.IsValid()) + { + SaveNameField->SetText(FText::FromString(FileName)); + } + PopulateSceneList(); + LastOutlinerActorCount = -1; + } + } + return FReply::Handled(); + }) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Browse"))) + .Font(FCoreStyle::GetDefaultFontStyle("Regular", 10)) + ] + ] + ] + // BaseLevel selection button + + SVerticalBox::Slot() + .AutoHeight() + .Padding(0.0f, 4.0f, 0.0f, 2.0f) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0.0f, 0.0f, 4.0f, 0.0f) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("BaseLevel:"))) + .Font(FCoreStyle::GetDefaultFontStyle("Regular", 9)) + .ColorAndOpacity(FLinearColor(0.6f, 0.6f, 0.6f)) + ] + + SHorizontalBox::Slot().FillWidth(1.0f).VAlign(VAlign_Center) + [ + SNew(SButton) + .OnClicked_Lambda([this]() -> FReply + { + OpenBaseLevelPopup(); + return FReply::Handled(); + }) + [ + SAssignNew(BaseLevelButtonText, STextBlock) + .Text(FText::FromString(TEXT("Select..."))) + .Font(FCoreStyle::GetDefaultFontStyle("Bold", 9)) + .ColorAndOpacity(FLinearColor(0.4f, 0.8f, 0.4f)) + ] + ] + ] + // Saved scenes list + + SVerticalBox::Slot() + .AutoHeight() + .Padding(0.0f, 2.0f, 0.0f, 0.0f) + [ + SAssignNew(SceneListContainer, SVerticalBox) + ] + ]; +} + +TSharedRef UPS_Editor_MainWidget_Legacy::BuildSpawnCatalog() +{ + TSharedRef CatalogList = SNew(SVerticalBox); + + // Header + CatalogList->AddSlot() + .AutoHeight() + .Padding(0.0f, 0.0f, 0.0f, 4.0f) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Spawn Catalogue"))) + .Font(FCoreStyle::GetDefaultFontStyle("Bold", 11)) + .ColorAndOpacity(FLinearColor(0.8f, 0.8f, 0.8f)) + ]; + + // Note: entries will be populated in NativeConstruct when SpawnManager is set. + // For now, show a placeholder. The actual list is built dynamically. + CatalogList->AddSlot() + .AutoHeight() + [ + SAssignNew(SpawnListContainer, SVerticalBox) + ]; + + return SNew(SBorder) + .BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush")) + .BorderBackgroundColor(FLinearColor(0.05f, 0.05f, 0.05f, 1.0f)) + .Padding(FMargin(10.0f, 8.0f)) + [ + SNew(SBox) + .MinDesiredWidth(180.0f) + .MaxDesiredHeight(400.0f) + [ + SNew(SScrollBox) + .ConsumeMouseWheel(EConsumeMouseWheel::Always) + + SScrollBox::Slot() + [ + CatalogList + ] + ] + ]; +} + +void UPS_Editor_MainWidget_Legacy::NativeConstruct() +{ + Super::NativeConstruct(); + PopulateSpawnCatalog(); + + // Populate BaseLevel options from MasterCatalog. + // No hardcoded "None" entry: if the user wants an "empty" fallback they mark one of + // their catalog entries as the default (bIsDefault) — it appears in the popup like + // any other base level. + if (APS_Editor_PlayerController* EditorPC = Cast(GetOwningPlayer())) + { + if (UPS_Editor_MasterCatalog* Master = EditorPC->MasterCatalogAsset) + { + for (const FPS_Editor_BaseLevelEntry& Entry : Master->BaseLevels) + { + BaseLevelOptions.Add(MakeShared(Entry.DisplayName)); + if (!Entry.MapPath.IsEmpty()) + { + BaseLevelPathMap.Add(Entry.DisplayName, Entry.MapPath); + } + if (Entry.AdditionalSublevels.Num() > 0) + { + BaseLevelExtraSublevels.Add(Entry.DisplayName, Entry.AdditionalSublevels); + } + if (Entry.Thumbnail) + { + BaseLevelThumbnails.Add(Entry.DisplayName, Entry.Thumbnail); + } + } + } + + // If a BaseLevel was set (e.g. from subsystem), update the button text + CurrentLevelFilter = EditorPC->CurrentBaseLevel; + if (!CurrentLevelFilter.IsEmpty() && BaseLevelButtonText.IsValid()) + { + BaseLevelButtonText->SetText(FText::FromString(CurrentLevelFilter)); + } + + // Restore scenario name in save field (from subsystem LastEdited) + if (UGameInstance* GI = EditorPC->GetGameInstance()) + { + if (UPS_Editor_GameInstanceSubsystem* Sub = GI->GetSubsystem()) + { + if (!Sub->LastEditedScenarioName.IsEmpty() && SaveNameField.IsValid()) + { + SaveNameField->SetText(FText::FromString(Sub->LastEditedScenarioName)); + } + } + } + } + + PopulateSceneList(); + + // Hide loading overlay if no sublevel is being loaded + if (APS_Editor_PlayerController* PC = Cast(GetOwningPlayer())) + { + if (PC->CurrentBaseLevel.IsEmpty()) + { + ShowLoadingScreen(false); + } + } + else + { + ShowLoadingScreen(false); + } + + // Bind to the timeline subsystem change delegate to refresh the tracks UI when data mutates + if (UWorld* World = GetWorld()) + { + if (UPS_Editor_TimelineSubsystem* TS = World->GetSubsystem()) + { + TimelineChangedHandle = TS->OnTimelineChanged.AddLambda([this]() + { + RebuildTimelineTracksList(); + }); + + // Seed duration field + if (TimelineDurationBox.IsValid()) + { + TimelineDurationBox->SetText(FText::FromString(FString::SanitizeFloat(TS->GetDuration()))); + } + } + } + + RebuildTimelineTracksList(); +} + +void UPS_Editor_MainWidget_Legacy::NativeDestruct() +{ + if (TimelineChangedHandle.IsValid()) + { + if (UWorld* World = GetWorld()) + { + if (UPS_Editor_TimelineSubsystem* TS = World->GetSubsystem()) + { + TS->OnTimelineChanged.Remove(TimelineChangedHandle); + } + } + TimelineChangedHandle.Reset(); + } + Super::NativeDestruct(); +} + +void UPS_Editor_MainWidget_Legacy::PopulateSceneList() +{ + if (!SceneListContainer.IsValid()) return; + SceneListContainer->ClearChildren(); + + UPS_Editor_SceneSerializer* Ser = SceneSerializer.Get(); + if (!Ser) return; + + // Filter scenes by selected BaseLevel + TArray Scenes = UPS_Editor_SceneLoader::GetSavedSceneNames(CurrentLevelFilter); + + if (Scenes.Num() == 0) + { + SceneListContainer->AddSlot() + .AutoHeight() + [ + SNew(STextBlock) + .Text(FText::FromString(CurrentLevelFilter.IsEmpty() ? TEXT("No saved scenarios") : FString::Printf(TEXT("No scenarios for '%s'"), *CurrentLevelFilter))) + .Font(FCoreStyle::GetDefaultFontStyle("Italic", 9)) + .ColorAndOpacity(FLinearColor(0.5f, 0.5f, 0.5f)) + ]; + return; + } + + for (const FString& SceneName : Scenes) + { + FString Name = SceneName; + SceneListContainer->AddSlot() + .AutoHeight() + .Padding(0.0f, 1.0f) + [ + SNew(SButton) + .OnClicked_Lambda([this, Name]() -> FReply + { + if (SceneSerializer.IsValid() && SpawnManager.IsValid()) + { + // Sync the BaseLevel sublevel to match the scenario being loaded. + // "Unfiled" is the folder for scenes saved without a base level — in that case + // we fall back to the MasterCatalog entry marked bIsDefault, or if none is + // defined we keep whatever sublevel is currently loaded. + FString SceneLevelName = UPS_Editor_SceneLoader::GetSceneLevelName(Name); + bool bHasValidBaseLevel = !SceneLevelName.IsEmpty() + && SceneLevelName != TEXT("Unfiled") + && BaseLevelPathMap.Contains(SceneLevelName); + + if (APS_Editor_PlayerController* EditorPC = Cast(GetOwningPlayer())) + { + // No base level in the scenario file — try to resolve the default from the catalog. + if (!bHasValidBaseLevel && EditorPC->MasterCatalogAsset) + { + UE_LOG(LogTemp, Log, TEXT("PS_Editor: Scenario '%s' has no base level, searching for default in MasterCatalog (%d entries)"), + *Name, EditorPC->MasterCatalogAsset->BaseLevels.Num()); + for (const FPS_Editor_BaseLevelEntry& Entry : EditorPC->MasterCatalogAsset->BaseLevels) + { + UE_LOG(LogTemp, Log, TEXT(" BaseLevel entry: DisplayName='%s', MapPath='%s', bIsDefault=%d"), + *Entry.DisplayName, *Entry.MapPath, Entry.bIsDefault ? 1 : 0); + if (Entry.bIsDefault && !Entry.DisplayName.IsEmpty()) + { + SceneLevelName = Entry.DisplayName; + bHasValidBaseLevel = BaseLevelPathMap.Contains(SceneLevelName); + UE_LOG(LogTemp, Log, TEXT(" -> Picked default '%s' (in PathMap=%d)"), + *SceneLevelName, bHasValidBaseLevel ? 1 : 0); + break; + } + } + } + + if (bHasValidBaseLevel) + { + UE_LOG(LogTemp, Log, TEXT("PS_Editor: Loading scenario '%s' with base level '%s' (current='%s')"), + *Name, *SceneLevelName, *EditorPC->CurrentBaseLevel); + if (EditorPC->CurrentBaseLevel != SceneLevelName) + { + FString SceneMapPath = SceneLevelName; + if (FString* Found = BaseLevelPathMap.Find(SceneLevelName)) + { + SceneMapPath = *Found; + } + TArray ScnExtras; + if (TArray* FoundExtras = BaseLevelExtraSublevels.Find(SceneLevelName)) + { + ScnExtras = *FoundExtras; + } + EditorPC->LoadBaseLevelAsSublevel(SceneLevelName, SceneMapPath, ScnExtras); + CurrentLevelFilter = EditorPC->CurrentBaseLevel; + + if (BaseLevelButtonText.IsValid()) + { + BaseLevelButtonText->SetText(FText::FromString(SceneLevelName)); + } + } + } + // If still no valid base level and no default in the catalog, keep the + // current sublevel as-is — don't unload it (avoids empty scenes). + } + + SceneSerializer->LoadScene(Name, SpawnManager.Get()); + bSceneDirty = false; + if (SaveNameField.IsValid()) + { + SaveNameField->SetText(FText::FromString(Name)); + } + LastOutlinerActorCount = -1; + } + return FReply::Handled(); + }) + [ + SNew(STextBlock) + .Text(FText::FromString(SceneName)) + .Font(FCoreStyle::GetDefaultFontStyle("Regular", 10)) + ] + ]; + } +} + +void UPS_Editor_MainWidget_Legacy::PopulateSpawnCatalog() +{ + if (!SpawnListContainer.IsValid()) + { + return; + } + + SpawnListContainer->ClearChildren(); + + UPS_Editor_SpawnManager* SM = SpawnManager.Get(); + if (!SM) + { + SpawnListContainer->AddSlot() + .AutoHeight() + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("SpawnManager is null"))) + .Font(FCoreStyle::GetDefaultFontStyle("Italic", 9)) + .ColorAndOpacity(FLinearColor(1.0f, 0.3f, 0.3f)) + ]; + return; + } + + const TArray& Entries = SM->GetResolvedEntries(); + if (Entries.Num() == 0) + { + // Debug: show why catalog is empty + FString DebugInfo = TEXT("Catalog empty."); + if (APS_Editor_PlayerController* EditorPC = Cast(GetOwningPlayer())) + { + if (EditorPC->MasterCatalogAsset) + { + DebugInfo += FString::Printf(TEXT(" Master OK (%d sub-catalogs)."), + EditorPC->MasterCatalogAsset->Catalogs.Num()); + for (UPS_Editor_SpawnCatalog* Cat : EditorPC->MasterCatalogAsset->Catalogs) + { + DebugInfo += FString::Printf(TEXT(" [%s:%d]"), + Cat ? *Cat->GetName() : TEXT("NULL"), + Cat ? Cat->Entries.Num() : 0); + } + } + else + { + DebugInfo += TEXT(" MasterCatalog is NULL!"); + } + } + + SpawnListContainer->AddSlot() + .AutoHeight() + [ + SNew(STextBlock) + .Text(FText::FromString(DebugInfo)) + .Font(FCoreStyle::GetDefaultFontStyle("Italic", 8)) + .ColorAndOpacity(FLinearColor(1.0f, 0.5f, 0.3f)) + .AutoWrapText(true) + ]; + return; + } + + // Group by category + FString CurrentCategory; + for (int32 i = 0; i < Entries.Num(); ++i) + { + const FPS_Editor_ResolvedEntry& Entry = Entries[i]; + + // Category header + if (Entry.Category != CurrentCategory) + { + CurrentCategory = Entry.Category; + SpawnListContainer->AddSlot() + .AutoHeight() + .Padding(0.0f, 6.0f, 0.0f, 2.0f) + [ + SNew(STextBlock) + .Text(FText::FromString(CurrentCategory)) + .Font(FCoreStyle::GetDefaultFontStyle("Bold", 9)) + .ColorAndOpacity(FLinearColor(0.6f, 0.6f, 0.6f)) + ]; + } + + // Spawn card: thumbnail on top, name below + const int32 EntryIndex = i; + const float CardSize = 80.0f; + + TSharedRef CardContent = SNew(SVerticalBox); + + // Thumbnail + if (Entry.Thumbnail) + { + TSharedPtr Brush = MakeShared(); + Brush->SetResourceObject(Entry.Thumbnail); + Brush->ImageSize = FVector2D(CardSize, CardSize); + Brush->DrawAs = ESlateBrushDrawType::Image; + Brush->Tiling = ESlateBrushTileType::NoTile; + CachedBrushes.Add(Brush); + + TWeakPtr WeakBrush = Brush; + CardContent->AddSlot() + .AutoHeight() + .HAlign(HAlign_Center) + [ + SNew(SImage) + .Image_Lambda([WeakBrush]() -> const FSlateBrush* + { + return WeakBrush.IsValid() ? WeakBrush.Pin().Get() : nullptr; + }) + .DesiredSizeOverride(FVector2D(CardSize, CardSize)) + ]; + } + + // Name below thumbnail + CardContent->AddSlot() + .AutoHeight() + .HAlign(HAlign_Center) + .Padding(0.0f, 4.0f, 0.0f, 0.0f) + [ + SNew(STextBlock) + .Text(FText::FromString(Entry.DisplayName)) + .Font(FCoreStyle::GetDefaultFontStyle("Regular", 9)) + .Justification(ETextJustify::Center) + .ColorAndOpacity(FLinearColor::White) + ]; + + SpawnListContainer->AddSlot() + .AutoHeight() + .Padding(4.0f, 4.0f) + [ + SNew(SButton) + .OnClicked_Lambda([this, EntryIndex]() -> FReply + { + // Get SpawnManager fresh from PlayerController + UPS_Editor_SpawnManager* Mgr = nullptr; + if (APS_Editor_PlayerController* PC = Cast(GetOwningPlayer())) + { + Mgr = PC->GetSpawnManager(); + } + if (!Mgr) Mgr = SpawnManager.Get(); + if (!Mgr) return FReply::Handled(); + + // Check if this actor implements IPS_Editor_PointPlaceable → enter placement mode + const TArray& AllEntries = Mgr->GetResolvedEntries(); + if (AllEntries.IsValidIndex(EntryIndex) && AllEntries[EntryIndex].ActorClass) + { + if (AllEntries[EntryIndex].ActorClass->ImplementsInterface(UPS_Editor_PointPlaceable::StaticClass())) + { + if (APS_Editor_PlayerController* EditorPC = Cast(GetOwningPlayer())) + { + EditorPC->BeginPointPlacement(AllEntries[EntryIndex].ActorClass); + } + return FReply::Handled(); + } + } + + // Spawn and immediately select the new actor exclusively — deselects + // whatever was active before, so the gizmo and property panel switch to it. + if (AActor* Spawned = Mgr->SpawnFromCatalog(EntryIndex)) + { + if (UPS_Editor_SelectionManager* Sel = SelectionManager.Get()) + { + Sel->ClearSelection(); + Sel->SelectActor(Spawned); + } + } + bSceneDirty = true; + return FReply::Handled(); + }) + [ + SNew(SBox) + .MinDesiredWidth(CardSize + 8.0f) + [ + CardContent + ] + ] + ]; + } + +} + +void UPS_Editor_MainWidget_Legacy::NativeTick(const FGeometry& MyGeometry, float InDeltaTime) +{ + Super::NativeTick(MyGeometry, InDeltaTime); + + // Track dirty state: detect spawned actor count changes (spawn, delete, undo, paste) + if (APS_Editor_PlayerController* EditorPC = Cast(GetOwningPlayer())) + { + if (UPS_Editor_SpawnManager* SM = EditorPC->GetSpawnManager()) + { + int32 CurrentCount = SM->GetSpawnedActors().Num(); + if (CurrentCount != LastKnownSpawnedCount) + { + if (CurrentCount > 0) bSceneDirty = true; + LastKnownSpawnedCount = CurrentCount; + } + } + } + + // Update simulate button text / color. Keep the label "Simulate" / "Stop" regardless of + // the timeline state so it doesn't clash with the dedicated Play button in the timeline + // panel — a second "Play" on the toolbar was confusing. The button still drives the + // timeline under the hood (see BuildToolbar lambda). + if (SimulateButtonText.IsValid()) + { + if (APS_Editor_PlayerController* SimPC = Cast(GetOwningPlayer())) + { + UPS_Editor_TimelineSubsystem* TS = GetTimelineSubsystemFromWidget(this); + const bool bTimelinePlaying = TS && TS->GetNumTracks() > 0 && TS->IsPlaying(); + const bool bActive = SimPC->IsSimulating() || bTimelinePlaying; + + if (bActive) + { + SimulateButtonText->SetText(FText::FromString(TEXT("Stop"))); + SimulateButtonText->SetColorAndOpacity(FLinearColor(1.0f, 0.3f, 0.2f)); + } + else + { + SimulateButtonText->SetText(FText::FromString(TEXT("Simulate"))); + SimulateButtonText->SetColorAndOpacity(FLinearColor(1.0f, 0.6f, 0.0f)); + } + } + } + + RefreshPropertiesFromSelection(); + RefreshOutliner(); + RefreshTimelineUI(); +} + +void UPS_Editor_MainWidget_Legacy::RefreshPropertiesFromSelection() +{ + bIsRefreshing = true; + + UPS_Editor_SelectionManager* SM = SelectionManager.Get(); + if (!SM) + { + bIsRefreshing = false; + return; + } + + // Update mode text + help bar based on editor mode + EPS_Editor_EditorMode CurrentEditorMode = EPS_Editor_EditorMode::Normal; + if (APS_Editor_PlayerController* EditorPC = Cast(GetOwningPlayer())) + { + CurrentEditorMode = EditorPC->GetEditorMode(); + } + + if (ModeText.IsValid()) + { + if (CurrentEditorMode == EPS_Editor_EditorMode::SplinePlacement) + { + ModeText->SetText(FText::FromString(TEXT("Spline Placement"))); + } + else + { + FString ModeStr; + switch (SM->GetTransformMode()) + { + case EPS_Editor_TransformMode::Translate: ModeStr = TEXT("Translate (Z)"); break; + case EPS_Editor_TransformMode::Rotate: ModeStr = TEXT("Rotate (E)"); break; + case EPS_Editor_TransformMode::Scale: ModeStr = TEXT("Scale (R)"); break; + } + ModeText->SetText(FText::FromString(ModeStr)); + } + } + + // Determine if a spline or child-movable is selected (for Edit buttons) + bool bSplineSelected = false; + bool bChildMovableSelected = false; + if (CurrentEditorMode == EPS_Editor_EditorMode::Normal && SM->IsAnythingSelected()) + { + for (const TWeakObjectPtr& Weak : SM->GetSelectedActors()) + { + if (Cast(Weak.Get())) + { + bSplineSelected = true; + break; + } + if (Weak.IsValid() && Weak->GetClass()->ImplementsInterface(UPS_Editor_ChildMovable::StaticClass())) + { + bChildMovableSelected = true; + break; + } + } + } + + // Update help bar text and button visibility + if (CurrentEditorMode == EPS_Editor_EditorMode::SplinePlacement) + { + bool bPointSelected = false; + if (APS_Editor_Pawn* Pawn = Cast(GetOwningPlayerPawn())) + { + bPointSelected = (Pawn->ActiveSplinePointIndex >= 0); + } + + if (HelpBarText.IsValid()) + { + if (bPointSelected) + { + HelpBarText->SetText(FText::FromString(TEXT("SPLINE: Drag gizmo to move | Del: delete point | Click line: insert | Click outside: add | Esc/Done: finish"))); + } + else + { + HelpBarText->SetText(FText::FromString(TEXT("SPLINE: Click to add points | Click handle to select | Click line to insert | End: snap | Esc/Done: finish"))); + } + HelpBarText->SetColorAndOpacity(FLinearColor(1.0f, 0.6f, 0.0f)); + } + if (SplineEditButton.IsValid()) SplineEditButton->SetVisibility(EVisibility::Collapsed); + if (ChildEditButton.IsValid()) ChildEditButton->SetVisibility(EVisibility::Collapsed); + if (SplineDeletePointButton.IsValid()) SplineDeletePointButton->SetVisibility(bPointSelected ? EVisibility::Visible : EVisibility::Collapsed); + if (SplineFinishButton.IsValid()) SplineFinishButton->SetVisibility(EVisibility::Visible); + } + else if (bChildMovableSelected) + { + if (HelpBarText.IsValid()) + { + HelpBarText->SetText(FText::FromString(TEXT("Child-movable selected — click Edit Children to move sub-elements"))); + HelpBarText->SetColorAndOpacity(FLinearColor(0.3f, 0.8f, 1.0f)); + } + if (SplineEditButton.IsValid()) SplineEditButton->SetVisibility(EVisibility::Collapsed); + if (ChildEditButton.IsValid()) ChildEditButton->SetVisibility(EVisibility::Visible); + if (SplineDeletePointButton.IsValid()) SplineDeletePointButton->SetVisibility(EVisibility::Collapsed); + if (SplineFinishButton.IsValid()) SplineFinishButton->SetVisibility(EVisibility::Collapsed); + } + else if (bSplineSelected) + { + if (HelpBarText.IsValid()) + { + HelpBarText->SetText(FText::FromString(TEXT("Spline selected — click Edit to modify points | S: add points"))); + HelpBarText->SetColorAndOpacity(FLinearColor(0.8f, 0.8f, 0.5f)); + } + if (SplineEditButton.IsValid()) SplineEditButton->SetVisibility(EVisibility::Visible); + if (SplineDeletePointButton.IsValid()) SplineDeletePointButton->SetVisibility(EVisibility::Collapsed); + if (SplineFinishButton.IsValid()) SplineFinishButton->SetVisibility(EVisibility::Collapsed); + } + else + { + if (HelpBarText.IsValid()) + { + HelpBarText->SetText(FText::FromString(TEXT("LMB: Select/Pan | RMB: Fly | Alt+LMB/MMB: Orbit | Z/E/R: Translate/Rotate/Scale | End: Snap | Del: Delete | S: Spline"))); + HelpBarText->SetColorAndOpacity(FLinearColor(0.6f, 0.6f, 0.6f)); + } + if (SplineEditButton.IsValid()) SplineEditButton->SetVisibility(EVisibility::Collapsed); + if (ChildEditButton.IsValid()) ChildEditButton->SetVisibility(EVisibility::Collapsed); + if (SplineDeletePointButton.IsValid()) SplineDeletePointButton->SetVisibility(EVisibility::Collapsed); + if (SplineFinishButton.IsValid()) SplineFinishButton->SetVisibility(EVisibility::Collapsed); + } + + // Update space button text + if (SpaceText.IsValid()) + { + SpaceText->SetText(FText::FromString( + SM->GetTransformSpace() == EPS_Editor_Space::World ? TEXT("World") : TEXT("Local"))); + } + + // Update snap button text + if (SnapText.IsValid()) + { + SnapText->SetText(FText::FromString( + SM->bSnapTranslation ? TEXT("Snap: On") : TEXT("Snap: Off"))); + } + + if (!SM->IsAnythingSelected()) + { + if (SelectionInfoText.IsValid()) + { + SelectionInfoText->SetText(FText::FromString(TEXT("No selection"))); + } + auto ClearField = [](TSharedPtr& Field) + { + if (Field.IsValid()) Field->SetText(FText::FromString(TEXT("--"))); + }; + ClearField(LocX); ClearField(LocY); ClearField(LocZ); + ClearField(RotX); ClearField(RotY); ClearField(RotZ); + ClearField(ScaX); ClearField(ScaY); ClearField(ScaZ); + + if (LastSelectedActor.IsValid()) + { + LastSelectedActor = nullptr; + RebuildDynamicProperties(); + LastOutlinerActorCount = -1; // Force outliner refresh + } + bIsRefreshing = false; + return; + } + + // Show first selected actor info + const TArray>& Selected = SM->GetSelectedActors(); + AActor* Actor = Selected[0].Get(); + if (!Actor) + { + bIsRefreshing = false; + return; + } + + if (SelectionInfoText.IsValid()) + { + FString ActorName = Actor->GetClass()->GetName(); + ActorName.RemoveFromEnd(TEXT("_C")); + FString Info = FString::Printf(TEXT("%s (%d)"), *ActorName, Selected.Num()); + SelectionInfoText->SetText(FText::FromString(Info)); + } + + const FVector Loc = Actor->GetActorLocation(); + const FRotator Rot = Actor->GetActorRotation(); + const FVector Sca = Actor->GetActorScale3D(); + + auto SetField = [this](TSharedPtr& Field, float Val) + { + if (Field.IsValid() && !Field->HasKeyboardFocus()) + { + Field->SetText(FText::FromString(FloatToString(Val))); + } + }; + + SetField(LocX, Loc.X); SetField(LocY, Loc.Y); SetField(LocZ, Loc.Z); + SetField(RotX, Rot.Roll); SetField(RotY, Rot.Pitch); SetField(RotZ, Rot.Yaw); + SetField(ScaX, Sca.X); SetField(ScaY, Sca.Y); SetField(ScaZ, Sca.Z); + + // Detect selection change → rebuild dynamic properties + refresh outliner highlight + if (Actor != LastSelectedActor.Get()) + { + LastSelectedActor = Actor; + RebuildDynamicProperties(); + LastOutlinerActorCount = -1; // Force outliner refresh for highlight update + } + + RefreshDynamicPropertyValues(); + + bIsRefreshing = false; +} + +void UPS_Editor_MainWidget_Legacy::ApplyTransformFromUI() +{ + if (bIsRefreshing) return; + bSceneDirty = true; + + UPS_Editor_SelectionManager* SM = SelectionManager.Get(); + if (!SM || !SM->IsAnythingSelected()) return; + + AActor* Actor = SM->GetSelectedActors()[0].Get(); + if (!Actor) return; + + auto GetFloat = [](TSharedPtr& Field, float Default) -> float + { + if (!Field.IsValid()) return Default; + return FCString::Atof(*Field->GetText().ToString()); + }; + + // Capture old transform for undo + const FTransform OldTransform = Actor->GetActorTransform(); + + const FVector NewLoc(GetFloat(LocX, 0), GetFloat(LocY, 0), GetFloat(LocZ, 0)); + const FRotator NewRot(GetFloat(RotY, 0), GetFloat(RotZ, 0), GetFloat(RotX, 0)); + const FVector NewScale(GetFloat(ScaX, 1), GetFloat(ScaY, 1), GetFloat(ScaZ, 1)); + + Actor->SetActorLocation(NewLoc); + Actor->SetActorRotation(NewRot); + Actor->SetActorScale3D(NewScale); + + // Record undo action + if (APlayerController* PC = GetOwningPlayer()) + { + if (APS_Editor_PlayerController* EditorPC = Cast(PC)) + { + if (UPS_Editor_UndoManager* UM = EditorPC->GetUndoManager()) + { + TSharedPtr Action = MakeShared(); + Action->Actors.Add(Actor); + Action->OldTransforms.Add(OldTransform); + Action->NewTransforms.Add(Actor->GetActorTransform()); + Action->Description = TEXT("UI Transform Edit"); + UM->RecordAction(Action); + } + } + } +} + +TSharedRef UPS_Editor_MainWidget_Legacy::BuildOutliner() +{ + return SNew(SBorder) + .BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush")) + .BorderBackgroundColor(FLinearColor(0.05f, 0.05f, 0.05f, 1.0f)) + .Padding(FMargin(10.0f, 8.0f)) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot().AutoHeight() + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Scene Objects"))) + .Font(FCoreStyle::GetDefaultFontStyle("Bold", 10)) + .ColorAndOpacity(FLinearColor(0.8f, 0.8f, 0.8f)) + ] + + SVerticalBox::Slot().FillHeight(1.0f).Padding(0.0f, 4.0f, 0.0f, 0.0f) + [ + SNew(SScrollBox) + .ConsumeMouseWheel(EConsumeMouseWheel::Always) + + SScrollBox::Slot() + [ + SAssignNew(OutlinerListContainer, SVerticalBox) + ] + ] + ]; +} + +void UPS_Editor_MainWidget_Legacy::RefreshOutliner() +{ + UPS_Editor_SpawnManager* SM = SpawnManager.Get(); + if (!SM || !OutlinerListContainer.IsValid()) return; + + // Count visible (non-hidden) spawned actors + const TArray>& SpawnedActors = SM->GetSpawnedActors(); + int32 VisibleCount = 0; + for (const TWeakObjectPtr& Weak : SpawnedActors) + { + if (AActor* Actor = Weak.Get()) + { + if (!Actor->IsHidden()) VisibleCount++; + } + } + + // Only rebuild if count changed (avoid per-tick rebuild) + if (VisibleCount == LastOutlinerActorCount) return; + LastOutlinerActorCount = VisibleCount; + + OutlinerListContainer->ClearChildren(); + + UPS_Editor_SelectionManager* SelMgr = SelectionManager.Get(); + const FSlateFontInfo ItemFont = FCoreStyle::GetDefaultFontStyle("Regular", 9); + + for (int32 i = 0; i < SpawnedActors.Num(); i++) + { + AActor* Actor = SpawnedActors[i].Get(); + if (!Actor || Actor->IsHidden()) continue; + + // Build display name + FString DisplayName; + if (UPS_Editor_SpawnableComponent* SC = Actor->FindComponentByClass()) + { + if (!SC->DisplayName.IsEmpty()) + { + DisplayName = SC->DisplayName; + } + } + if (DisplayName.IsEmpty()) + { + DisplayName = Actor->GetClass()->GetName(); + DisplayName.RemoveFromEnd(TEXT("_C")); + } + + // Append index for disambiguation + DisplayName = FString::Printf(TEXT("%s #%d"), *DisplayName, i + 1); + + const bool bIsSelected = SelMgr && SelMgr->IsActorSelected(Actor); + const FLinearColor TextColor = bIsSelected ? FLinearColor(0.2f, 0.7f, 1.0f) : FLinearColor(0.7f, 0.7f, 0.7f); + const FLinearColor BgColor = bIsSelected ? FLinearColor(0.15f, 0.25f, 0.35f, 0.8f) : FLinearColor(0.0f, 0.0f, 0.0f, 0.0f); + + TWeakObjectPtr WeakActor = Actor; + + OutlinerListContainer->AddSlot().AutoHeight().Padding(0.0f, 1.0f) + [ + SNew(SButton) + .ButtonStyle(FCoreStyle::Get(), "NoBorder") + .ContentPadding(FMargin(6.0f, 3.0f)) + .OnClicked_Lambda([this, WeakActor]() -> FReply + { + AActor* Act = WeakActor.Get(); + UPS_Editor_SelectionManager* SM = SelectionManager.Get(); + if (Act && SM) + { + SM->ClearSelection(); + SM->SelectActor(Act); + } + return FReply::Handled(); + }) + [ + SNew(SBorder) + .BorderBackgroundColor(BgColor) + .Padding(FMargin(4.0f, 2.0f)) + [ + SNew(STextBlock) + .Text(FText::FromString(DisplayName)) + .Font(ItemFont) + .ColorAndOpacity(TextColor) + ] + ] + ]; + } +} + +UObject* UPS_Editor_MainWidget_Legacy::FindPropertyTarget(AActor* Actor, const FString& ComponentName) +{ + if (!Actor) return nullptr; + if (ComponentName.IsEmpty()) return Actor; + + TArray Components; + Actor->GetComponents(Components); + for (UActorComponent* Comp : Components) + { + if (Comp && Comp->GetName() == ComponentName) + { + return Comp; + } + } + return nullptr; +} + +void UPS_Editor_MainWidget_Legacy::RebuildDynamicProperties() +{ + DynamicPropertyRows.Empty(); + if (!DynamicPropertiesContainer.IsValid()) return; + DynamicPropertiesContainer->ClearChildren(); + + UPS_Editor_SelectionManager* SM = SelectionManager.Get(); + if (!SM || !SM->IsAnythingSelected()) return; + + AActor* Actor = SM->GetSelectedActors()[0].Get(); + if (!Actor) return; + + UPS_Editor_EditableComponent* EditComp = Actor->FindComponentByClass(); + if (!EditComp) + { + UE_LOG(LogTemp, Warning, TEXT("PS_Editor: No EditableComponent on %s"), *Actor->GetName()); + return; + } + if (EditComp->EditableProperties.Num() == 0) + { + UE_LOG(LogTemp, Warning, TEXT("PS_Editor: EditableComponent has 0 properties on %s"), *Actor->GetName()); + return; + } + UE_LOG(LogTemp, Log, TEXT("PS_Editor: Building %d dynamic property rows for %s"), EditComp->EditableProperties.Num(), *Actor->GetName()); + + const FSlateFontInfo LabelFont = FCoreStyle::GetDefaultFontStyle("Regular", 9); + const FSlateFontInfo ValueFont = FCoreStyle::GetDefaultFontStyle("Regular", 9); + + // Separator line + DynamicPropertiesContainer->AddSlot().AutoHeight().Padding(0.0f, 6.0f, 0.0f, 2.0f) + [ + SNew(SBorder) + .BorderBackgroundColor(FLinearColor(0.3f, 0.3f, 0.3f, 0.5f)) + .Padding(FMargin(0.0f, 1.0f)) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Properties"))) + .Font(FCoreStyle::GetDefaultFontStyle("Bold", 9)) + .ColorAndOpacity(FLinearColor(0.6f, 0.6f, 0.6f)) + ] + ]; + + for (int32 i = 0; i < EditComp->EditableProperties.Num(); i++) + { + const FPS_Editor_EditablePropertyEntry& Entry = EditComp->EditableProperties[i]; + + FPS_Editor_PropertyRowWidgetsLegacy Row; + UPS_Editor_EditableComponent::ParsePropertyPath(Entry.Path, Row.ComponentName, Row.PropertyName); + Row.DisplayName = EditComp->GetDisplayName(Entry.Path); + Row.Key = Entry.Path.ToString(); + + UE_LOG(LogTemp, Log, TEXT("PS_Editor: Resolving [%s] comp='%s' prop='%s'"), *Row.Key, *Row.ComponentName, *Row.PropertyName); + + UObject* Target = FindPropertyTarget(Actor, Row.ComponentName); + if (!Target) + { + // Log available components for debugging + TArray AllComps; + Actor->GetComponents(AllComps); + for (UActorComponent* C : AllComps) + { + UE_LOG(LogTemp, Warning, TEXT("PS_Editor: Available component: '%s' (%s)"), *C->GetName(), *C->GetClass()->GetName()); + } + UE_LOG(LogTemp, Warning, TEXT("PS_Editor: Component '%s' not found on %s"), *Row.ComponentName, *Actor->GetName()); + continue; + } + + Row.CachedProperty = FindFProperty(Target->GetClass(), *Row.PropertyName); + if (!Row.CachedProperty) + { + UE_LOG(LogTemp, Warning, TEXT("PS_Editor: Property '%s' not found on %s (%s)"), *Row.PropertyName, *Target->GetName(), *Target->GetClass()->GetName()); + continue; + } + UE_LOG(LogTemp, Log, TEXT("PS_Editor: Resolved OK → %s on %s"), *Row.CachedProperty->GetName(), *Target->GetClass()->GetName()); + + const int32 RowIndex = DynamicPropertyRows.Num(); + + // Label + DynamicPropertiesContainer->AddSlot().AutoHeight().Padding(0.0f, 3.0f, 0.0f, 0.0f) + [ + SNew(STextBlock) + .Text(FText::FromString(Row.DisplayName)) + .Font(LabelFont) + .ColorAndOpacity(FLinearColor(0.5f, 0.5f, 0.5f)) + ]; + + // Value widget depends on property type + TSharedRef ValueWidget = SNullWidget::NullWidget; + + // Check for custom dropdown options from the interface + TArray CustomOptions; + if (Actor->GetClass()->ImplementsInterface(UPS_Editor_EditableInterface::StaticClass())) + { + CustomOptions = IPS_Editor_EditableInterface::Execute_GetCustomOptionsForProperty(Actor, Row.Key); + } + + if (CustomOptions.Num() > 0) + { + Row.EnumOptions = MakeShared>>(); + for (const FString& Opt : CustomOptions) + { + Row.EnumOptions->Add(MakeShared(Opt)); + } + + ValueWidget = SAssignNew(Row.EnumCombo, SComboBox>) + .OptionsSource(Row.EnumOptions.Get()) + .Method(EPopupMethod::UseCurrentWindow) + .IsFocusable(true) + .OnSelectionChanged_Lambda([this, RowIndex](TSharedPtr Selected, ESelectInfo::Type SelectInfo) + { + if (!Selected.IsValid() || SelectInfo == ESelectInfo::Direct) return; + FPS_Editor_PropertyRowWidgetsLegacy& R = DynamicPropertyRows[RowIndex]; + if (R.EnumComboText.IsValid()) + { + R.EnumComboText->SetText(FText::FromString(*Selected)); + } + ApplyPropertyFromUI(RowIndex); + }) + .OnGenerateWidget_Lambda([](TSharedPtr Item) -> TSharedRef + { + return SNew(STextBlock) + .Text(FText::FromString(Item.IsValid() ? *Item : TEXT(""))) + .Font(FCoreStyle::GetDefaultFontStyle("Regular", 9)); + }) + [ + SAssignNew(Row.EnumComboText, STextBlock) + .Text(FText::FromString(TEXT("Select..."))) + .Font(ValueFont) + ]; + } + // Detect enum type (FEnumProperty for C++ enum class, FByteProperty with UEnum for legacy) + else + { + UEnum* FoundEnum = nullptr; + if (FEnumProperty* EnumProp = CastField(Row.CachedProperty)) + { + FoundEnum = EnumProp->GetEnum(); + } + else if (FByteProperty* ByteProp = CastField(Row.CachedProperty)) + { + FoundEnum = ByteProp->GetIntPropertyEnum(); + } + + if (FoundEnum) + { + Row.CachedEnum = FoundEnum; + Row.EnumOptions = MakeShared>>(); + for (int32 Idx = 0; Idx < FoundEnum->NumEnums() - 1; ++Idx) // -1 to skip _MAX + { + FString Name = FoundEnum->GetDisplayNameTextByIndex(Idx).ToString(); + Row.EnumOptions->Add(MakeShared(Name)); + } + + UE_LOG(LogTemp, Warning, TEXT("PS_Editor: Enum '%s' for property '%s' — %d options, OptionsPtr=%p"), + *FoundEnum->GetName(), *Row.PropertyName, Row.EnumOptions->Num(), Row.EnumOptions.Get()); + for (int32 Dbg = 0; Dbg < Row.EnumOptions->Num(); ++Dbg) + { + UE_LOG(LogTemp, Warning, TEXT(" [%d] %s"), Dbg, **(*Row.EnumOptions)[Dbg]); + } + + ValueWidget = SAssignNew(Row.EnumCombo, SComboBox>) + .OptionsSource(Row.EnumOptions.Get()) + .Method(EPopupMethod::UseCurrentWindow) + .IsFocusable(true) + .OnSelectionChanged_Lambda([this, RowIndex](TSharedPtr Selected, ESelectInfo::Type SelectInfo) + { + if (!Selected.IsValid() || SelectInfo == ESelectInfo::Direct) return; + FPS_Editor_PropertyRowWidgetsLegacy& R = DynamicPropertyRows[RowIndex]; + if (R.EnumComboText.IsValid()) + { + R.EnumComboText->SetText(FText::FromString(*Selected)); + } + ApplyPropertyFromUI(RowIndex); + }) + .OnGenerateWidget_Lambda([](TSharedPtr Item) -> TSharedRef + { + return SNew(STextBlock) + .Text(FText::FromString(Item.IsValid() ? *Item : TEXT(""))) + .Font(FCoreStyle::GetDefaultFontStyle("Regular", 9)); + }) + [ + SAssignNew(Row.EnumComboText, STextBlock) + .Text(FText::FromString(TEXT("Select..."))) + .Font(ValueFont) + ]; + } + else if (CastField(Row.CachedProperty)) + { + ValueWidget = SNew(SHorizontalBox) + + SHorizontalBox::Slot().AutoWidth() + [ + SAssignNew(Row.BoolField, SCheckBox) + .OnCheckStateChanged_Lambda([this, RowIndex](ECheckBoxState) + { + ApplyPropertyFromUI(RowIndex); + }) + ]; + } + else if (FStructProperty* StructProp = CastField(Row.CachedProperty)) + { + auto MakeTextField = [&](TSharedPtr& Field) -> TSharedRef + { + return SAssignNew(Field, SEditableTextBox) + .Font(ValueFont) + .MinDesiredWidth(55.0f) + .OnTextCommitted_Lambda([this, RowIndex](const FText&, ETextCommit::Type) + { + ApplyPropertyFromUI(RowIndex); + }); + }; + + if (StructProp->Struct == TBaseStructure::Get() + || StructProp->Struct == TBaseStructure::Get()) + { + ValueWidget = SNew(SHorizontalBox) + + SHorizontalBox::Slot().FillWidth(1.0f).Padding(2.0f, 0.0f) + [ MakeTextField(Row.VecX) ] + + SHorizontalBox::Slot().FillWidth(1.0f).Padding(2.0f, 0.0f) + [ MakeTextField(Row.VecY) ] + + SHorizontalBox::Slot().FillWidth(1.0f).Padding(2.0f, 0.0f) + [ MakeTextField(Row.VecZ) ]; + } + else if (StructProp->Struct == TBaseStructure::Get()) + { + ValueWidget = SNew(SHorizontalBox) + + SHorizontalBox::Slot().FillWidth(1.0f).Padding(2.0f, 0.0f) + [ MakeTextField(Row.VecX) ] + + SHorizontalBox::Slot().FillWidth(1.0f).Padding(2.0f, 0.0f) + [ MakeTextField(Row.VecY) ]; + } + else if (StructProp->Struct == TBaseStructure::Get()) + { + ValueWidget = SNew(SHorizontalBox) + + SHorizontalBox::Slot().FillWidth(1.0f).Padding(2.0f, 0.0f) + [ MakeTextField(Row.ColR) ] + + SHorizontalBox::Slot().FillWidth(1.0f).Padding(2.0f, 0.0f) + [ MakeTextField(Row.ColG) ] + + SHorizontalBox::Slot().FillWidth(1.0f).Padding(2.0f, 0.0f) + [ MakeTextField(Row.ColB) ] + + SHorizontalBox::Slot().FillWidth(1.0f).Padding(2.0f, 0.0f) + [ MakeTextField(Row.ColA) ]; + } + else + { + // Unsupported struct: single text field with ExportText format + ValueWidget = SAssignNew(Row.SingleField, SEditableTextBox) + .Font(ValueFont) + .MinDesiredWidth(70.0f) + .OnTextCommitted_Lambda([this, RowIndex](const FText&, ETextCommit::Type) + { + ApplyPropertyFromUI(RowIndex); + }); + } + } + else + { + // Default: single text field (float, int, string, etc.) + ValueWidget = SAssignNew(Row.SingleField, SEditableTextBox) + .Font(ValueFont) + .MinDesiredWidth(70.0f) + .OnTextCommitted_Lambda([this, RowIndex](const FText&, ETextCommit::Type) + { + ApplyPropertyFromUI(RowIndex); + }); + } + + } // end else (enum/struct/default type detection) + + const bool bAnimatable = EditComp->IsAnimatable(Entry.Path); + + if (bAnimatable) + { + const int32 CapturedRowIndex = RowIndex; + DynamicPropertiesContainer->AddSlot().AutoHeight().Padding(0.0f, 2.0f) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot().FillWidth(1.0f) + [ ValueWidget ] + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(4.0f, 0.0f, 0.0f, 0.0f) + [ + SNew(SButton) + .ToolTipText(FText::FromString(TEXT("Add a keyframe at the current timeline time"))) + .ContentPadding(FMargin(4.0f, 0.0f)) + .OnClicked_Lambda([this, CapturedRowIndex]() -> FReply + { + AddKeyForRow(CapturedRowIndex); + return FReply::Handled(); + }) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("+"))) + .Font(FCoreStyle::GetDefaultFontStyle("Bold", 10)) + .ColorAndOpacity(FLinearColor(0.6f, 0.8f, 1.0f)) + ] + ] + ]; + } + else + { + DynamicPropertiesContainer->AddSlot().AutoHeight().Padding(0.0f, 2.0f) + [ ValueWidget ]; + } + + DynamicPropertyRows.Add(Row); + } + + UE_LOG(LogTemp, Log, TEXT("PS_Editor: Built %d dynamic property row(s)"), DynamicPropertyRows.Num()); +} + +void UPS_Editor_MainWidget_Legacy::RefreshDynamicPropertyValues() +{ + if (DynamicPropertyRows.Num() == 0) return; + + UPS_Editor_SelectionManager* SM = SelectionManager.Get(); + if (!SM || !SM->IsAnythingSelected()) return; + + AActor* Actor = SM->GetSelectedActors()[0].Get(); + if (!Actor) return; + + for (FPS_Editor_PropertyRowWidgetsLegacy& Row : DynamicPropertyRows) + { + if (!Row.CachedProperty) continue; + + UObject* Target = FindPropertyTarget(Actor, Row.ComponentName); + if (!Target) continue; + + // Custom options combo (no CachedEnum, has EnumCombo) + if (!Row.CachedEnum && Row.EnumCombo.IsValid() && !Row.EnumCombo->IsOpen()) + { + // Read current property value as text + FString CurrentVal; + Row.CachedProperty->ExportText_InContainer(0, CurrentVal, Target, Target, nullptr, PPF_None); + + if (Row.EnumComboText.IsValid() && Row.EnumComboText->GetText().ToString() != CurrentVal) + { + // Find matching option + if (Row.EnumOptions.IsValid()) + { + for (const TSharedPtr& Opt : *Row.EnumOptions) + { + if (*Opt == CurrentVal) + { + Row.EnumCombo->SetSelectedItem(Opt); + Row.EnumComboText->SetText(FText::FromString(CurrentVal)); + break; + } + } + } + } + } + else if (Row.CachedEnum && Row.EnumCombo.IsValid() && !Row.EnumCombo->IsOpen()) + { + // Read current enum value index + int64 EnumValue = 0; + if (FEnumProperty* EnumProp = CastField(Row.CachedProperty)) + { + FNumericProperty* UnderlyingProp = EnumProp->GetUnderlyingProperty(); + const void* ValuePtr = Row.CachedProperty->ContainerPtrToValuePtr(Target); + EnumValue = UnderlyingProp->GetSignedIntPropertyValue(ValuePtr); + } + else if (FByteProperty* ByteProp = CastField(Row.CachedProperty)) + { + EnumValue = ByteProp->GetPropertyValue_InContainer(Target); + } + + int32 DisplayIdx = Row.CachedEnum->GetIndexByValue(EnumValue); + if (Row.EnumOptions.IsValid() && Row.EnumOptions->IsValidIndex(DisplayIdx)) + { + // Only update if value actually changed + FString NewText = *(*Row.EnumOptions)[DisplayIdx]; + if (Row.EnumComboText.IsValid() && Row.EnumComboText->GetText().ToString() != NewText) + { + Row.EnumCombo->SetSelectedItem((*Row.EnumOptions)[DisplayIdx]); + Row.EnumComboText->SetText(FText::FromString(NewText)); + } + } + } + else if (FBoolProperty* BoolProp = CastField(Row.CachedProperty)) + { + if (Row.BoolField.IsValid()) + { + const bool Val = BoolProp->GetPropertyValue_InContainer(Target); + Row.BoolField->SetIsChecked(Val ? ECheckBoxState::Checked : ECheckBoxState::Unchecked); + } + } + else if (FStructProperty* StructProp = CastField(Row.CachedProperty)) + { + void* ValuePtr = Row.CachedProperty->ContainerPtrToValuePtr(Target); + if (!ValuePtr) continue; + + auto SetIfNoFocus = [this](TSharedPtr& Field, float Val) + { + if (Field.IsValid() && !Field->HasKeyboardFocus()) + { + Field->SetText(FText::FromString(FloatToString(Val))); + } + }; + + if (StructProp->Struct == TBaseStructure::Get()) + { + const FVector& Vec = *static_cast(ValuePtr); + SetIfNoFocus(Row.VecX, Vec.X); + SetIfNoFocus(Row.VecY, Vec.Y); + SetIfNoFocus(Row.VecZ, Vec.Z); + } + else if (StructProp->Struct == TBaseStructure::Get()) + { + const FRotator& Rot = *static_cast(ValuePtr); + SetIfNoFocus(Row.VecX, Rot.Pitch); + SetIfNoFocus(Row.VecY, Rot.Yaw); + SetIfNoFocus(Row.VecZ, Rot.Roll); + } + else if (StructProp->Struct == TBaseStructure::Get()) + { + const FVector2D& Vec2 = *static_cast(ValuePtr); + SetIfNoFocus(Row.VecX, Vec2.X); + SetIfNoFocus(Row.VecY, Vec2.Y); + } + else if (StructProp->Struct == TBaseStructure::Get()) + { + const FLinearColor& Col = *static_cast(ValuePtr); + SetIfNoFocus(Row.ColR, Col.R); + SetIfNoFocus(Row.ColG, Col.G); + SetIfNoFocus(Row.ColB, Col.B); + SetIfNoFocus(Row.ColA, Col.A); + } + else if (Row.SingleField.IsValid() && !Row.SingleField->HasKeyboardFocus()) + { + FString ExportedVal; + Row.CachedProperty->ExportText_InContainer(0, ExportedVal, Target, Target, nullptr, PPF_None); + Row.SingleField->SetText(FText::FromString(ExportedVal)); + } + } + else if (Row.SingleField.IsValid() && !Row.SingleField->HasKeyboardFocus()) + { + // float, double, int, string, etc. + if (CastField(Row.CachedProperty)) + { + const float Val = *Row.CachedProperty->ContainerPtrToValuePtr(Target); + Row.SingleField->SetText(FText::FromString(FloatToString(Val))); + } + else if (CastField(Row.CachedProperty)) + { + const double Val = *Row.CachedProperty->ContainerPtrToValuePtr(Target); + Row.SingleField->SetText(FText::FromString(FloatToString(static_cast(Val)))); + } + else if (CastField(Row.CachedProperty)) + { + const int32 Val = *Row.CachedProperty->ContainerPtrToValuePtr(Target); + Row.SingleField->SetText(FText::FromString(FString::FromInt(Val))); + } + else + { + // Generic: ExportText + FString ExportedVal; + Row.CachedProperty->ExportText_InContainer(0, ExportedVal, Target, Target, nullptr, PPF_None); + Row.SingleField->SetText(FText::FromString(ExportedVal)); + } + } + } +} + +void UPS_Editor_MainWidget_Legacy::ApplyPropertyFromUI(int32 RowIndex) +{ + if (bIsRefreshing) return; + bSceneDirty = true; + if (!DynamicPropertyRows.IsValidIndex(RowIndex)) return; + + FPS_Editor_PropertyRowWidgetsLegacy& Row = DynamicPropertyRows[RowIndex]; + if (!Row.CachedProperty) return; + + UPS_Editor_SelectionManager* SM = SelectionManager.Get(); + if (!SM || !SM->IsAnythingSelected()) return; + + AActor* Actor = SM->GetSelectedActors()[0].Get(); + if (!Actor) return; + + UObject* Target = FindPropertyTarget(Actor, Row.ComponentName); + if (!Target) return; + + // Export old value for undo + FString OldValue; + Row.CachedProperty->ExportText_InContainer(0, OldValue, Target, Target, nullptr, PPF_None); + + // Build new value text from UI and import + FString NewValueText; + + if (!Row.CachedEnum && Row.EnumComboText.IsValid()) + { + // Custom options combo: use selected text directly as property value + NewValueText = Row.EnumComboText->GetText().ToString(); + } + else if (Row.CachedEnum && Row.EnumComboText.IsValid()) + { + FString DisplayName = Row.EnumComboText->GetText().ToString(); + for (int32 Idx = 0; Idx < Row.CachedEnum->NumEnums() - 1; ++Idx) + { + if (Row.CachedEnum->GetDisplayNameTextByIndex(Idx).ToString() == DisplayName) + { + NewValueText = Row.CachedEnum->GetNameStringByIndex(Idx); + break; + } + } + } + else if (CastField(Row.CachedProperty)) + { + NewValueText = Row.BoolField.IsValid() && Row.BoolField->IsChecked() ? TEXT("True") : TEXT("False"); + } + else if (FStructProperty* StructProp = CastField(Row.CachedProperty)) + { + auto GetText = [](TSharedPtr& F) -> FString + { + return F.IsValid() ? F->GetText().ToString() : TEXT("0"); + }; + + if (StructProp->Struct == TBaseStructure::Get()) + { + NewValueText = FString::Printf(TEXT("(X=%s,Y=%s,Z=%s)"), *GetText(Row.VecX), *GetText(Row.VecY), *GetText(Row.VecZ)); + } + else if (StructProp->Struct == TBaseStructure::Get()) + { + NewValueText = FString::Printf(TEXT("(X=%s,Y=%s)"), *GetText(Row.VecX), *GetText(Row.VecY)); + } + else if (StructProp->Struct == TBaseStructure::Get()) + { + NewValueText = FString::Printf(TEXT("(Pitch=%s,Yaw=%s,Roll=%s)"), *GetText(Row.VecX), *GetText(Row.VecY), *GetText(Row.VecZ)); + } + else if (StructProp->Struct == TBaseStructure::Get()) + { + NewValueText = FString::Printf(TEXT("(R=%s,G=%s,B=%s,A=%s)"), *GetText(Row.ColR), *GetText(Row.ColG), *GetText(Row.ColB), *GetText(Row.ColA)); + } + else if (Row.SingleField.IsValid()) + { + NewValueText = Row.SingleField->GetText().ToString(); + } + } + else if (Row.SingleField.IsValid()) + { + NewValueText = Row.SingleField->GetText().ToString(); + } + + if (NewValueText.IsEmpty()) + { + UE_LOG(LogTemp, Warning, TEXT("PS_Editor: ApplyProperty '%s' - NewValueText is EMPTY, aborting"), *Row.Key); + return; + } + + // Apply the new value + const TCHAR* ImportResult = Row.CachedProperty->ImportText_InContainer(*NewValueText, Target, Target, PPF_None); + UE_LOG(LogTemp, Warning, TEXT("PS_Editor: ApplyProperty '%s' = '%s' -> Import %s"), + *Row.Key, *NewValueText, ImportResult ? TEXT("OK") : TEXT("FAILED")); + + // Refresh rendering + force network replication (triggers OnRep on clients) + if (UActorComponent* Comp = Cast(Target)) + { + Comp->MarkRenderStateDirty(); + if (AActor* OwnerActor = Comp->GetOwner()) + { + OwnerActor->ForceNetUpdate(); + } + } + else + { + Actor->ForceNetUpdate(); + } + + // Auto-call OnRep on the server (ImportText bypasses replication notify) + if (!Row.CachedProperty->RepNotifyFunc.IsNone()) + { + if (UFunction* RepFunc = Target->FindFunction(Row.CachedProperty->RepNotifyFunc)) + { + Target->ProcessEvent(RepFunc, nullptr); + UE_LOG(LogTemp, Log, TEXT("PS_Editor: Called %s on %s"), *Row.CachedProperty->RepNotifyFunc.ToString(), *Target->GetName()); + } + } + + // Export new value after import (normalized form for undo) + FString NewValue; + Row.CachedProperty->ExportText_InContainer(0, NewValue, Target, Target, nullptr, PPF_None); + + // Record undo + if (APlayerController* PC = GetOwningPlayer()) + { + if (APS_Editor_PlayerController* EditorPC = Cast(PC)) + { + if (UPS_Editor_UndoManager* UM = EditorPC->GetUndoManager()) + { + TSharedPtr Action = MakeShared(); + Action->Actor = Actor; + Action->ComponentName = Row.ComponentName; + Action->PropertyName = Row.PropertyName; + Action->OldValue = OldValue; + Action->NewValue = NewValue; + Action->Description = FString::Printf(TEXT("Edit %s"), *Row.DisplayName); + UM->RecordAction(Action); + } + } + } + + UE_LOG(LogTemp, Log, TEXT("PS_Editor: Property %s changed: %s → %s"), *Row.Key, *OldValue, *NewValue); + + // Always notify property changed + const bool bImplementsInterface = Actor->GetClass()->ImplementsInterface(UPS_Editor_EditableInterface::StaticClass()); + if (bImplementsInterface) + { + IPS_Editor_EditableInterface::Execute_OnEditorPropertyChanged(Actor, Row.Key); + } + + // Check if this property requires reinit or respawn + UPS_Editor_EditableComponent* EditComp = Actor->FindComponentByClass(); + if (!EditComp) return; + + const FName PropertyPath(*Row.Key); + const bool bRespawn = EditComp->RequiresRespawn(PropertyPath); + const bool bReinit = EditComp->RequiresReinit(PropertyPath); + + if (bRespawn) + { + APS_Editor_PlayerController* EditorPC = Cast(GetOwningPlayer()); + if (!EditorPC) return; + UPS_Editor_SpawnManager* SpawnMgr = EditorPC->GetSpawnManager(); + if (!SpawnMgr) return; + + // Collect ALL current editable property values (including the one just changed) + TMap AllProps; + for (const FPS_Editor_PropertyRowWidgetsLegacy& PropRow : DynamicPropertyRows) + { + if (!PropRow.CachedProperty) continue; + UObject* PropTarget = FindPropertyTarget(Actor, PropRow.ComponentName); + if (!PropTarget) continue; + + FString ExportedVal; + PropRow.CachedProperty->ExportText_InContainer(0, ExportedVal, PropTarget, PropTarget, nullptr, PPF_None); + AllProps.Add(PropRow.Key, ExportedVal); + } + + // Deselect before destroying + UPS_Editor_SelectionManager* SelMgr = SelectionManager.Get(); + if (SelMgr) SelMgr->ClearSelection(); + + // Respawn: destroy + spawn + apply all props + AActor* NewActor = SpawnMgr->RespawnActorWithProperties(Actor, AllProps); + if (NewActor && SelMgr) + { + SelMgr->SelectActor(NewActor); + } + UE_LOG(LogTemp, Log, TEXT("PS_Editor: Respawned actor for property '%s'"), *Row.Key); + } + else if (bReinit) + { + if (bImplementsInterface) + { + IPS_Editor_EditableInterface::Execute_OnEditorNeedsReinit(Actor); + } + } +} + +FString UPS_Editor_MainWidget_Legacy::FloatToString(float Value) const +{ + return FString::Printf(TEXT("%.2f"), Value); +} + +void UPS_Editor_MainWidget_Legacy::OnModeButtonClicked(EPS_Editor_TransformMode Mode) +{ + if (UPS_Editor_SelectionManager* SM = SelectionManager.Get()) + { + SM->SetTransformMode(Mode); + } +} + +void UPS_Editor_MainWidget_Legacy::ShowLoadingScreen(bool bShow) +{ + if (LoadingOverlay.IsValid()) + { + LoadingOverlay->SetVisibility(bShow ? EVisibility::Visible : EVisibility::Collapsed); + } +} + +void UPS_Editor_MainWidget_Legacy::OpenBaseLevelPopup() +{ + if (!BaseLevelGridContainer.IsValid() || !BaseLevelPopup.IsValid()) return; + + BaseLevelGridContainer->ClearChildren(); + BaseLevelCardBorders.Empty(); + PendingBaseLevelSelection.Empty(); + + const float CardWidth = 160.0f; + const float CardHeight = 80.0f; // 2:1 ratio + const int32 Columns = 3; + TSharedPtr CurrentRow; + int32 ColIndex = 0; + + for (const TSharedPtr& Option : BaseLevelOptions) + { + if (!Option.IsValid()) continue; + const FString Name = *Option; + + // Start new row if needed + if (ColIndex % Columns == 0) + { + CurrentRow = SNew(SHorizontalBox); + BaseLevelGridContainer->AddSlot().AutoHeight().Padding(0.0f, 4.0f) + [ + CurrentRow.ToSharedRef() + ]; + } + + TSharedRef Card = SNew(SVerticalBox); + + // Thumbnail + TObjectPtr* FoundThumb = BaseLevelThumbnails.Find(Name); + if (FoundThumb && *FoundThumb) + { + TSharedPtr Brush = MakeShared(); + Brush->SetResourceObject(*FoundThumb); + Brush->ImageSize = FVector2D(CardWidth, CardHeight); + Brush->DrawAs = ESlateBrushDrawType::Image; + Brush->Tiling = ESlateBrushTileType::NoTile; + CachedBrushes.Add(Brush); + + TWeakPtr WeakBrush = Brush; + Card->AddSlot().AutoHeight().HAlign(HAlign_Center) + [ + SNew(SImage) + .Image_Lambda([WeakBrush]() -> const FSlateBrush* + { + return WeakBrush.IsValid() ? WeakBrush.Pin().Get() : nullptr; + }) + .DesiredSizeOverride(FVector2D(CardWidth, CardHeight)) + ]; + } + else + { + Card->AddSlot().AutoHeight().HAlign(HAlign_Center) + [ + SNew(SBorder) + .BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush")) + .BorderBackgroundColor(FLinearColor(0.15f, 0.15f, 0.15f)) + [ + SNew(SBox) + .WidthOverride(CardWidth) + .HeightOverride(CardHeight) + .HAlign(HAlign_Center) + .VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(FText::FromString(Name)) + .Font(FCoreStyle::GetDefaultFontStyle("Italic", 9)) + .ColorAndOpacity(FLinearColor(0.5f, 0.5f, 0.5f)) + ] + ] + ]; + } + + // Name label + Card->AddSlot().AutoHeight().HAlign(HAlign_Center).Padding(0.0f, 4.0f, 0.0f, 0.0f) + [ + SNew(STextBlock) + .Text(FText::FromString(Name)) + .Font(FCoreStyle::GetDefaultFontStyle("Regular", 9)) + .ColorAndOpacity(FLinearColor::White) + ]; + + // Add card to current row with highlight border + TSharedPtr CardBorder; + CurrentRow->AddSlot().AutoWidth().Padding(8.0f, 0.0f) + [ + SNew(SBox) + .MinDesiredWidth(CardWidth + 16.0f) + [ + SAssignNew(CardBorder, SBorder) + .BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush")) + .BorderBackgroundColor(FLinearColor(0.1f, 0.1f, 0.1f, 0.0f)) + .Padding(FMargin(2.0f)) + [ + SNew(SButton) + .OnClicked_Lambda([this, Name]() -> FReply + { + PendingBaseLevelSelection = Name; + UpdateBaseLevelHighlight(); + return FReply::Handled(); + }) + [ + Card + ] + ] + ] + ]; + BaseLevelCardBorders.Add(Name, CardBorder); + + ColIndex++; + } + + BaseLevelPopup->SetVisibility(EVisibility::Visible); +} + +void UPS_Editor_MainWidget_Legacy::ConfirmBaseLevelSelection() +{ + if (BaseLevelPopup.IsValid()) + { + BaseLevelPopup->SetVisibility(EVisibility::Collapsed); + } + + if (PendingBaseLevelSelection.IsEmpty()) return; + + APS_Editor_PlayerController* EditorPC = Cast(GetOwningPlayer()); + if (!EditorPC) return; + + // Clear current scenario before switching BaseLevel + if (UPS_Editor_SceneSerializer* Ser = EditorPC->GetSceneSerializer()) + { + if (UPS_Editor_SpawnManager* SM = EditorPC->GetSpawnManager()) + { + Ser->ClearScene(SM); + } + } + if (SaveNameField.IsValid()) + { + SaveNameField->SetText(FText::GetEmpty()); + } + + // Load the selected BaseLevel + FString SublevelPath = PendingBaseLevelSelection; + if (FString* Found = BaseLevelPathMap.Find(PendingBaseLevelSelection)) + { + SublevelPath = *Found; + } + + if (PendingBaseLevelSelection == TEXT("None")) + { + EditorPC->LoadBaseLevelAsSublevel(TEXT("None")); + } + else + { + TArray Extras; + if (TArray* Found = BaseLevelExtraSublevels.Find(PendingBaseLevelSelection)) + { + Extras = *Found; + } + EditorPC->LoadBaseLevelAsSublevel(PendingBaseLevelSelection, SublevelPath, Extras); + } + + CurrentLevelFilter = EditorPC->CurrentBaseLevel; + + if (BaseLevelButtonText.IsValid()) + { + BaseLevelButtonText->SetText(FText::FromString( + PendingBaseLevelSelection == TEXT("None") ? TEXT("Select...") : PendingBaseLevelSelection)); + } + + PopulateSceneList(); + PopulateSpawnCatalog(); + LastOutlinerActorCount = -1; + PendingBaseLevelSelection.Empty(); +} + +void UPS_Editor_MainWidget_Legacy::UpdateBaseLevelHighlight() +{ + for (auto& Pair : BaseLevelCardBorders) + { + if (Pair.Value.IsValid()) + { + const bool bSelected = (Pair.Key == PendingBaseLevelSelection); + Pair.Value->SetBorderBackgroundColor(bSelected + ? FLinearColor(0.2f, 0.6f, 1.0f, 0.8f) + : FLinearColor(0.1f, 0.1f, 0.1f, 0.0f)); + } + } +} + +void UPS_Editor_MainWidget_Legacy::CancelBaseLevelPopup() +{ + if (BaseLevelPopup.IsValid()) + { + BaseLevelPopup->SetVisibility(EVisibility::Collapsed); + } + PendingBaseLevelSelection.Empty(); +} + +void UPS_Editor_MainWidget_Legacy::OnCloseClicked() +{ + if (CloseConfirmOverlay.IsValid()) + { + CloseConfirmOverlay->SetVisibility(EVisibility::Visible); + } +} + +void UPS_Editor_MainWidget_Legacy::ExecuteClose(bool bSave) +{ + if (CloseConfirmOverlay.IsValid()) + { + CloseConfirmOverlay->SetVisibility(EVisibility::Collapsed); + } + + APS_Editor_PlayerController* EditorPC = Cast(GetOwningPlayer()); + if (!EditorPC) return; + + if (bSave) + { + UPS_Editor_SpawnManager* SM = EditorPC->GetSpawnManager(); + UPS_Editor_SceneSerializer* Ser = EditorPC->GetSceneSerializer(); + if (SM && Ser) + { + FString SceneName = SaveNameField.IsValid() ? SaveNameField->GetText().ToString() : TEXT(""); + if (SceneName.IsEmpty()) + { + if (HelpBarText.IsValid()) + { + HelpBarText->SetText(FText::FromString(TEXT("Enter a scenario name before saving."))); + HelpBarText->SetColorAndOpacity(FLinearColor(1.0f, 0.4f, 0.2f)); + } + return; + } + Ser->SaveScene(SceneName, SM); + bSceneDirty = false; + } + } + + // Delegate cleanup + transition to PlayerController + EditorPC->CloseEditor(); +} + +// ============================================================================ +// Timeline UI +// ============================================================================ + +static UPS_Editor_TimelineSubsystem* GetTimelineSubsystemFromWidget(UPS_Editor_MainWidget_Legacy* Widget) +{ + if (!Widget) return nullptr; + UWorld* World = Widget->GetWorld(); + return World ? World->GetSubsystem() : nullptr; +} + +/** + * A single keyframe dot on the timeline track strip. Supports: + * - left click (no drag): fires OnClickedAction + * - left drag: updates a shared VisualTimeRef each frame so the parent canvas + * re-anchors the dot live; fires OnDragFinished with the final time on release + * + * Deletion is handled externally via the global Delete key, targeting whichever + * key MainWidget has marked as selected (set by OnClickedAction). + * + * The widget must live inside a parent whose cached geometry spans the timeline's + * full width — the horizontal cursor position within that parent maps linearly to time. + */ +class SPS_Editor_TimelineKeyDot : public SCompoundWidget +{ +public: + DECLARE_DELEGATE(FOnSimpleAction); + DECLARE_DELEGATE_OneParam(FOnTimeChanged, float); + + SLATE_BEGIN_ARGS(SPS_Editor_TimelineKeyDot) + : _Duration(10.f) + , _InitialTime(0.f) + , _DotColor(FLinearColor::White) + {} + SLATE_ARGUMENT(float, Duration) + SLATE_ARGUMENT(float, InitialTime) + SLATE_ATTRIBUTE(FSlateColor, DotColor) + SLATE_ARGUMENT(TSharedPtr, VisualTimeRef) + SLATE_EVENT(FOnSimpleAction, OnClickedAction) + SLATE_EVENT(FOnTimeChanged, OnDragFinished) + SLATE_ATTRIBUTE(FText, ToolTipText) + SLATE_END_ARGS() + + void Construct(const FArguments& InArgs) + { + Duration = FMath::Max(InArgs._Duration, 0.0001f); + VisualTimeRef = InArgs._VisualTimeRef; + if (VisualTimeRef.IsValid()) + { + *VisualTimeRef = InArgs._InitialTime; + } + OnClickedAction = InArgs._OnClickedAction; + OnDragFinished = InArgs._OnDragFinished; + + ChildSlot + [ + SNew(SBox).WidthOverride(10.f).HeightOverride(10.f) + [ + SNew(SBorder) + .BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush")) + .BorderBackgroundColor(InArgs._DotColor) + .ToolTipText(InArgs._ToolTipText) + ] + ]; + } + + virtual FReply OnMouseButtonDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override + { + if (MouseEvent.GetEffectingButton() == EKeys::LeftMouseButton) + { + MouseDownScreenPos = MouseEvent.GetScreenSpacePosition(); + bDragging = false; + return FReply::Handled().CaptureMouse(SharedThis(this)); + } + return FReply::Unhandled(); + } + + virtual FReply OnMouseMove(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override + { + if (HasMouseCapture()) + { + const float DistMoved = (MouseEvent.GetScreenSpacePosition() - MouseDownScreenPos).Size(); + if (!bDragging && DistMoved > 3.f) + { + bDragging = true; + } + if (bDragging && VisualTimeRef.IsValid()) + { + *VisualTimeRef = ComputeTimeAtCursor(MouseEvent.GetScreenSpacePosition()); + } + return FReply::Handled(); + } + return FReply::Unhandled(); + } + + virtual FReply OnMouseButtonUp(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override + { + if (HasMouseCapture()) + { + if (!bDragging) + { + OnClickedAction.ExecuteIfBound(); + } + else if (VisualTimeRef.IsValid()) + { + OnDragFinished.ExecuteIfBound(*VisualTimeRef); + } + bDragging = false; + return FReply::Handled().ReleaseMouseCapture(); + } + return FReply::Unhandled(); + } + +private: + float ComputeTimeAtCursor(const FVector2D& ScreenPos) const + { + TSharedPtr Parent = GetParentWidget(); + if (!Parent.IsValid()) return 0.f; + const FGeometry ParentGeo = Parent->GetCachedGeometry(); + const FVector2D LocalInParent = ParentGeo.AbsoluteToLocal(ScreenPos); + const float Norm = FMath::Clamp(LocalInParent.X / FMath::Max(ParentGeo.GetLocalSize().X, 1.f), 0.f, 1.f); + return Norm * Duration; + } + + float Duration = 10.f; + TSharedPtr VisualTimeRef; + FVector2D MouseDownScreenPos = FVector2D::ZeroVector; + bool bDragging = false; + + FOnSimpleAction OnClickedAction; + FOnTimeChanged OnDragFinished; +}; + +/** + * Horizontal scrub bar that drives the timeline cursor. Replaces SSlider because + * SSlider insets the handle by (HandleSize/2) on each side, which breaks pixel-perfect + * alignment with the keyframe dots (which use pure percentage anchors). + * + * Layout is the same as the key strips below: full-width track + a single handle anchored + * at (CurrentTime / Duration). Click or drag anywhere on the bar to seek. + */ +class SPS_Editor_TimelineScrubBar : public SCompoundWidget +{ +public: + DECLARE_DELEGATE_OneParam(FOnTimeSeek, float); + + SLATE_BEGIN_ARGS(SPS_Editor_TimelineScrubBar) {} + SLATE_ATTRIBUTE(float, Duration) + SLATE_ATTRIBUTE(float, CurrentTime) + SLATE_EVENT(FOnTimeSeek, OnSeek) + SLATE_END_ARGS() + + void Construct(const FArguments& InArgs) + { + DurationAttr = InArgs._Duration; + CurrentTimeAttr = InArgs._CurrentTime; + OnSeek = InArgs._OnSeek; + + ChildSlot + [ + SNew(SBox).HeightOverride(18.f) + [ + SNew(SOverlay) + // Track background (thin horizontal bar) + + SOverlay::Slot().HAlign(HAlign_Fill).VAlign(VAlign_Center) + [ + SNew(SBox).HeightOverride(4.f) + [ + SNew(SBorder) + .BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush")) + .BorderBackgroundColor(FLinearColor(0.25f, 0.25f, 0.25f, 1.0f)) + .Padding(FMargin(0.f)) + ] + ] + // Handle — anchored via percentage just like the keyframes below + + SOverlay::Slot().HAlign(HAlign_Fill).VAlign(VAlign_Fill) + [ + SNew(SConstraintCanvas) + + SConstraintCanvas::Slot() + .Anchors_Lambda([this]() -> FAnchors + { + const float Dur = FMath::Max(DurationAttr.Get(10.f), 0.0001f); + const float Now = CurrentTimeAttr.Get(0.f); + const float N = FMath::Clamp(Now / Dur, 0.f, 1.f); + return FAnchors(N, 0.5f, N, 0.5f); + }) + .Alignment(FVector2D(0.5f, 0.5f)) + .AutoSize(true) + [ + SNew(SBox).WidthOverride(12.f).HeightOverride(16.f) + [ + SNew(SBorder) + .BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush")) + .BorderBackgroundColor(FLinearColor(0.95f, 0.95f, 0.95f)) + ] + ] + ] + ] + ]; + } + + virtual FReply OnMouseButtonDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override + { + if (MouseEvent.GetEffectingButton() == EKeys::LeftMouseButton) + { + DoSeek(MyGeometry, MouseEvent.GetScreenSpacePosition()); + return FReply::Handled().CaptureMouse(SharedThis(this)); + } + return FReply::Unhandled(); + } + + virtual FReply OnMouseMove(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override + { + if (HasMouseCapture()) + { + DoSeek(MyGeometry, MouseEvent.GetScreenSpacePosition()); + return FReply::Handled(); + } + return FReply::Unhandled(); + } + + virtual FReply OnMouseButtonUp(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override + { + if (HasMouseCapture()) + { + return FReply::Handled().ReleaseMouseCapture(); + } + return FReply::Unhandled(); + } + +private: + void DoSeek(const FGeometry& Geo, const FVector2D& ScreenPos) + { + const FVector2D LocalPos = Geo.AbsoluteToLocal(ScreenPos); + const float Norm = FMath::Clamp(LocalPos.X / FMath::Max(Geo.GetLocalSize().X, 1.f), 0.f, 1.f); + const float Dur = FMath::Max(DurationAttr.Get(10.f), 0.0001f); + if (OnSeek.IsBound()) + { + OnSeek.Execute(Norm * Dur); + } + } + + TAttribute DurationAttr; + TAttribute CurrentTimeAttr; + FOnTimeSeek OnSeek; +}; + +TSharedRef UPS_Editor_MainWidget_Legacy::BuildTimelinePanel() +{ + const FSlateFontInfo Small = FCoreStyle::GetDefaultFontStyle("Regular", 9); + const FSlateFontInfo Bold = FCoreStyle::GetDefaultFontStyle("Bold", 10); + + return SAssignNew(TimelinePanel, SBorder) + .Visibility(EVisibility::Collapsed) + .BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush")) + .BorderBackgroundColor(FLinearColor(0.05f, 0.05f, 0.05f, 0.95f)) + .Padding(FMargin(12.0f, 8.0f)) + [ + SNew(SVerticalBox) + // Header: Play / Pause / Stop + current time + duration + + SVerticalBox::Slot().AutoHeight().Padding(0.0f, 0.0f, 0.0f, 6.0f) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Timeline"))) + .Font(FCoreStyle::GetDefaultFontStyle("Bold", 12)) + .ColorAndOpacity(FLinearColor(0.6f, 0.8f, 1.0f)) + ] + + SHorizontalBox::Slot().AutoWidth().Padding(12.0f, 0.0f, 0.0f, 0.0f).VAlign(VAlign_Center) + [ + SNew(SButton) + .OnClicked_Lambda([this]() -> FReply + { + if (UPS_Editor_TimelineSubsystem* TS = GetTimelineSubsystemFromWidget(this)) + { + if (TS->IsPlaying()) TS->Pause(); else TS->Play(); + } + return FReply::Handled(); + }) + [ + SAssignNew(TimelinePlayButtonText, STextBlock) + .Text(FText::FromString(TEXT("Play"))) + .Font(Bold) + .ColorAndOpacity(FLinearColor(0.2f, 0.9f, 0.2f)) + ] + ] + + SHorizontalBox::Slot().AutoWidth().Padding(4.0f, 0.0f, 0.0f, 0.0f).VAlign(VAlign_Center) + [ + SNew(SButton) + .ToolTipText(FText::FromString(TEXT("Rewind to t=0 and replay from the start"))) + .OnClicked_Lambda([this]() -> FReply + { + if (UPS_Editor_TimelineSubsystem* TS = GetTimelineSubsystemFromWidget(this)) + { + TS->Restart(); + } + return FReply::Handled(); + }) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Restart"))) + .Font(Bold) + .ColorAndOpacity(FLinearColor(0.6f, 0.8f, 1.0f)) + ] + ] + + SHorizontalBox::Slot().AutoWidth().Padding(4.0f, 0.0f, 0.0f, 0.0f).VAlign(VAlign_Center) + [ + SNew(SButton) + .ToolTipText(FText::FromString(TEXT("Stop: rewind + restore actor transforms"))) + .OnClicked_Lambda([this]() -> FReply + { + if (UPS_Editor_TimelineSubsystem* TS = GetTimelineSubsystemFromWidget(this)) + { + TS->Stop(); + } + return FReply::Handled(); + }) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Stop"))) + .Font(Bold) + .ColorAndOpacity(FLinearColor(1.0f, 0.4f, 0.2f)) + ] + ] + + SHorizontalBox::Slot().AutoWidth().Padding(12.0f, 0.0f, 0.0f, 0.0f).VAlign(VAlign_Center) + [ + SAssignNew(TimelineTimeLabel, STextBlock) + .Text(FText::FromString(TEXT("0.00 / 10.00 s"))) + .Font(Small) + .ColorAndOpacity(FLinearColor(0.8f, 0.8f, 0.8f)) + ] + + SHorizontalBox::Slot().FillWidth(1.0f) + [ + SNullWidget::NullWidget + ] + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(8.0f, 0.0f, 4.0f, 0.0f) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Duration:"))) + .Font(Small) + .ColorAndOpacity(FLinearColor(0.7f, 0.7f, 0.7f)) + ] + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center) + [ + SNew(SBox).MinDesiredWidth(60.0f) + [ + SAssignNew(TimelineDurationBox, SEditableTextBox) + .Font(Small) + .HintText(FText::FromString(TEXT("10"))) + .OnTextCommitted_Lambda([this](const FText& Text, ETextCommit::Type) + { + if (UPS_Editor_TimelineSubsystem* TS = GetTimelineSubsystemFromWidget(this)) + { + float Val = FCString::Atof(*Text.ToString()); + if (Val > 0.0f) + { + TS->SetDuration(Val); + } + } + }) + ] + ] + ] + // Scrub slider — aligned with the per-track key strips below so that + // the slider handle and the key dots share the same time axis. + + SVerticalBox::Slot().AutoHeight().Padding(0.0f, 0.0f, 0.0f, 6.0f) + [ + SNew(SHorizontalBox) + // Spacer matching the per-track X button width (+ its padding) + + SHorizontalBox::Slot().AutoWidth() + [ + SNew(SBox).WidthOverride(24.0f) + ] + // Spacer matching the per-track label width + + SHorizontalBox::Slot().AutoWidth().Padding(0.0f, 0.0f, 12.0f, 0.0f) + [ + SNew(SBox).MinDesiredWidth(180.0f) + ] + // The scrub bar fills the exact same area as the key strips below. Using a + // custom SConstraintCanvas-based widget (same math as the key anchors) to avoid + // the handle-inset that SSlider applies by default. + + SHorizontalBox::Slot().FillWidth(1.0f).VAlign(VAlign_Center) + [ + SNew(SPS_Editor_TimelineScrubBar) + .Duration_Lambda([this]() + { + if (UPS_Editor_TimelineSubsystem* TS = GetTimelineSubsystemFromWidget(this)) + { + return TS->GetDuration(); + } + return 10.f; + }) + .CurrentTime_Lambda([this]() + { + if (UPS_Editor_TimelineSubsystem* TS = GetTimelineSubsystemFromWidget(this)) + { + return TS->GetCurrentTime(); + } + return 0.f; + }) + .OnSeek_Lambda([this](float NewTime) + { + if (UPS_Editor_TimelineSubsystem* TS = GetTimelineSubsystemFromWidget(this)) + { + TS->Seek(NewTime); + } + }) + ] + // Spacer matching the per-track Interp button width + + SHorizontalBox::Slot().AutoWidth().Padding(6.0f, 0.0f, 0.0f, 0.0f) + [ + SNew(SBox).WidthOverride(50.0f) + ] + ] + // Track list (scrollable) + + SVerticalBox::Slot().AutoHeight().MaxHeight(220.0f) + [ + SNew(SScrollBox) + .ConsumeMouseWheel(EConsumeMouseWheel::Always) + + SScrollBox::Slot() + [ + SAssignNew(TimelineTracksContainer, SVerticalBox) + ] + ] + ]; +} + +void UPS_Editor_MainWidget_Legacy::ToggleTimelineVisible() +{ + bTimelineVisible = !bTimelineVisible; + if (TimelinePanel.IsValid()) + { + TimelinePanel->SetVisibility(bTimelineVisible ? EVisibility::Visible : EVisibility::Collapsed); + } + if (bTimelineVisible) + { + RebuildTimelineTracksList(); + } +} + +void UPS_Editor_MainWidget_Legacy::RebuildTimelineTracksList() +{ + if (!TimelineTracksContainer.IsValid()) return; + + // Validate the stored key selection against the current data. Any mutation that reshuffles + // keys (SetKeyTime, RemoveKey, clear, ...) triggers a rebuild; if the indices no longer + // address a real key, we drop the selection. Otherwise we keep it so a Seek-induced rebuild + // doesn't blow away the selection the user just made. + if (SelectedTimelineTrackIdx >= 0) + { + UPS_Editor_TimelineSubsystem* TS = GetTimelineSubsystemFromWidget(this); + if (!TS + || !TS->GetTracks().IsValidIndex(SelectedTimelineTrackIdx) + || !TS->GetTracks()[SelectedTimelineTrackIdx].Keys.IsValidIndex(SelectedTimelineKeyIdx)) + { + SelectedTimelineTrackIdx = -1; + SelectedTimelineKeyIdx = -1; + } + } + + TimelineTracksContainer->ClearChildren(); + + UPS_Editor_TimelineSubsystem* TS = GetTimelineSubsystemFromWidget(this); + if (!TS) return; + + const FSlateFontInfo Small = FCoreStyle::GetDefaultFontStyle("Regular", 9); + const float Duration = FMath::Max(TS->GetDuration(), 0.0001f); + + const TArray& Tracks = TS->GetTracks(); + if (Tracks.Num() == 0) + { + TimelineTracksContainer->AddSlot().AutoHeight().Padding(0.0f, 8.0f) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("No animated properties yet. Click + next to an animatable property to add a keyframe."))) + .Font(FCoreStyle::GetDefaultFontStyle("Italic", 9)) + .ColorAndOpacity(FLinearColor(0.5f, 0.5f, 0.5f)) + ]; + return; + } + + for (int32 TrackIdx = 0; TrackIdx < Tracks.Num(); ++TrackIdx) + { + const FPS_Editor_TimelineTrack& Track = Tracks[TrackIdx]; + + // Resolve actor display name for the label (fallback to GUID if not found) + FString ActorName = TEXT("(missing)"); + if (AActor* Actor = TS->FindActorById(Track.ActorId)) + { + ActorName = Actor->GetName(); + if (UPS_Editor_SpawnableComponent* Sp = Actor->FindComponentByClass()) + { + if (!Sp->DisplayName.IsEmpty()) ActorName = Sp->DisplayName; + } + } + const FString Label = FString::Printf(TEXT("%s . %s"), *ActorName, *Track.PropertyPath.ToString()); + + // Build the keyframe strip with absolute-percentage positioning so each dot's + // horizontal center sits exactly at (Time / Duration) of the strip width — + // the slider handle uses the same math, so visual alignment is pixel-perfect. + TSharedRef KeysCanvas = SNew(SConstraintCanvas); + for (int32 KeyIdx = 0; KeyIdx < Track.Keys.Num(); ++KeyIdx) + { + const FPS_Editor_TimelineKey& Key = Track.Keys[KeyIdx]; + // Saturated base colors so the dots pop against the dark track. + const FLinearColor BaseDotColor = Key.bInterpolate + ? FLinearColor(0.0f, 0.6f, 1.0f) // vivid blue = interpolated + : FLinearColor(1.0f, 0.5f, 0.0f); // vivid orange = stepped + const int32 CapturedKey = KeyIdx; + const int32 CapturedTrack = TrackIdx; + const bool bKeyInterp = Key.bInterpolate; + + // Shared live time value: updated by the widget during drag, read by the canvas anchor + // lambda each frame so the dot moves under the cursor without a full UI rebuild. + TSharedPtr VisualTime = MakeShared(Key.Time); + + KeysCanvas->AddSlot() + .Anchors_Lambda([VisualTime, Duration]() + { + const float N = FMath::Clamp(*VisualTime / Duration, 0.f, 1.f); + return FAnchors(N, 0.5f, N, 0.5f); + }) + .Alignment(FVector2D(0.5f, 0.5f)) + .AutoSize(true) + .Offset(FMargin(0.f, 0.f, 0.f, 0.f)) + [ + SNew(SPS_Editor_TimelineKeyDot) + .Duration(Duration) + .InitialTime(Key.Time) + .VisualTimeRef(VisualTime) + .DotColor_Lambda([this, CapturedTrack, CapturedKey, BaseDotColor]() -> FSlateColor + { + // Priority 1: selected by the user (click) — bright yellow + if (SelectedTimelineTrackIdx == CapturedTrack && SelectedTimelineKeyIdx == CapturedKey) + { + return FSlateColor(FLinearColor(1.0f, 1.0f, 0.0f)); + } + + // Priority 2: "active" — the last key whose time is <= CurrentTime for this track. + // That's the key currently providing the base value during playback/scrub. + if (UPS_Editor_TimelineSubsystem* TS2 = GetTimelineSubsystemFromWidget(const_cast(this))) + { + if (TS2->GetTracks().IsValidIndex(CapturedTrack)) + { + const FPS_Editor_TimelineTrack& Tr = TS2->GetTracks()[CapturedTrack]; + if (Tr.Keys.IsValidIndex(CapturedKey)) + { + const float Now = TS2->GetCurrentTime(); + int32 ActiveIdx = INDEX_NONE; + for (int32 k = 0; k < Tr.Keys.Num(); ++k) + { + if (Tr.Keys[k].Time <= Now + 0.0001f) ActiveIdx = k; else break; + } + if (ActiveIdx == CapturedKey) + { + return FSlateColor(FLinearColor(0.0f, 1.0f, 0.0f)); // pure green + } + } + } + } + + // Priority 3: base color (vivid blue for interp, vivid orange for step) + return FSlateColor(BaseDotColor); + }) + .ToolTipText(FText::FromString(FString::Printf(TEXT("t=%.2fs %s — click to select/seek, drag to move, Del to delete"), Key.Time, bKeyInterp ? TEXT("(interp)") : TEXT("(step)")))) + .OnClickedAction_Lambda([this, CapturedTrack, CapturedKey]() + { + UPS_Editor_TimelineSubsystem* TS2 = GetTimelineSubsystemFromWidget(this); + if (!TS2) return; + if (!TS2->GetTracks().IsValidIndex(CapturedTrack)) return; + const FPS_Editor_TimelineTrack& Tr = TS2->GetTracks()[CapturedTrack]; + if (!Tr.Keys.IsValidIndex(CapturedKey)) return; + + // Mark this key as the selected one so Del can target it + SelectedTimelineTrackIdx = CapturedTrack; + SelectedTimelineKeyIdx = CapturedKey; + + // Select the animated actor exclusively — ClearSelection first so any + // previously-selected actor (e.g. a spline still in overlay-visible state) + // is properly released and its handles/highlight go away. + AActor* TrackActor = TS2->FindActorById(Tr.ActorId); + UE_LOG(LogTemp, Log, TEXT("PS_Editor: Key click — track=%d key=%d ActorId=%s resolved=%s"), + CapturedTrack, CapturedKey, *Tr.ActorId.ToString(), TrackActor ? *TrackActor->GetName() : TEXT("null")); + if (TrackActor) + { + if (UPS_Editor_SelectionManager* Sel = SelectionManager.Get()) + { + Sel->ClearSelection(); + Sel->SelectActor(TrackActor); + } + } + + // Seek to the key time — ApplyValuesAtTime writes the key value onto the actor + TS2->Seek(Tr.Keys[CapturedKey].Time); + }) + .OnDragFinished_Lambda([this, CapturedTrack, CapturedKey](float NewTime) + { + if (UPS_Editor_TimelineSubsystem* TS2 = GetTimelineSubsystemFromWidget(this)) + { + // SetKeyTime re-sorts the keys, so indices may shift afterwards; + // clear the key selection to avoid pointing at the wrong one. + SelectedTimelineTrackIdx = -1; + SelectedTimelineKeyIdx = -1; + TS2->SetKeyTime(CapturedTrack, CapturedKey, NewTime); + } + }) + ]; + } + + TimelineTracksContainer->AddSlot().AutoHeight().Padding(0.0f, 2.0f) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center) + [ + SNew(SBox).WidthOverride(24.0f) + [ + SNew(SButton) + .ToolTipText(FText::FromString(TEXT("Remove this track"))) + .ContentPadding(FMargin(2.0f, 0.0f)) + .OnClicked_Lambda([this, TrackIdx]() -> FReply + { + if (UPS_Editor_TimelineSubsystem* TS2 = GetTimelineSubsystemFromWidget(this)) + { + TS2->RemoveTrack(TrackIdx); + } + return FReply::Handled(); + }) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("X"))) + .Font(FCoreStyle::GetDefaultFontStyle("Bold", 9)) + .ColorAndOpacity(FLinearColor(1.0f, 0.3f, 0.3f)) + ] + ] + ] + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(0.0f, 0.0f, 12.0f, 0.0f) + [ + SNew(SBox).MinDesiredWidth(180.0f) + [ + SNew(STextBlock) + .Text(FText::FromString(Label)) + .Font(Small) + .ColorAndOpacity(FLinearColor(0.85f, 0.85f, 0.85f)) + ] + ] + // Key strip: horizontal bar with small buttons positioned by Time/Duration + + SHorizontalBox::Slot().FillWidth(1.0f).VAlign(VAlign_Center) + [ + SNew(SBox).HeightOverride(18.0f) + [ + SNew(SOverlay) + + SOverlay::Slot() + .HAlign(HAlign_Fill).VAlign(VAlign_Center) + [ + SNew(SBorder) + .BorderImage(FCoreStyle::Get().GetBrush("WhiteBrush")) + .BorderBackgroundColor(FLinearColor(0.15f, 0.15f, 0.15f, 1.0f)) + .Padding(FMargin(0.0f, 2.0f)) + ] + + SOverlay::Slot() + .HAlign(HAlign_Fill).VAlign(VAlign_Fill) + [ + KeysCanvas + ] + ] + ] + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(6.0f, 0.0f, 0.0f, 0.0f) + [ + SNew(SBox).WidthOverride(50.0f) + [ + SNew(SButton) + .ToolTipText(FText::FromString(TEXT("Toggle interpolation on the key at (or before) the current time"))) + .ContentPadding(FMargin(2.0f, 0.0f)) + .OnClicked_Lambda([this, TrackIdx]() -> FReply + { + if (UPS_Editor_TimelineSubsystem* TS2 = GetTimelineSubsystemFromWidget(this)) + { + if (TS2->GetTracks().IsValidIndex(TrackIdx)) + { + const FPS_Editor_TimelineTrack& Tr = TS2->GetTracks()[TrackIdx]; + if (Tr.Keys.Num() > 0) + { + // Pick the key AT or BEFORE the current time (cursor-oriented) + const float Now = TS2->GetCurrentTime(); + int32 PickedKey = 0; + for (int32 k = 0; k < Tr.Keys.Num(); ++k) + { + if (Tr.Keys[k].Time <= Now) PickedKey = k; else break; + } + TS2->SetKeyInterpolate(TrackIdx, PickedKey, !Tr.Keys[PickedKey].bInterpolate); + } + } + } + return FReply::Handled(); + }) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Interp"))) + .Font(Small) + .ColorAndOpacity(FLinearColor(0.8f, 0.8f, 0.8f)) + ] + ] + ] + ]; + } +} + +void UPS_Editor_MainWidget_Legacy::RefreshTimelineUI() +{ + if (!TimelinePanel.IsValid() || !bTimelineVisible) return; + + UPS_Editor_TimelineSubsystem* TS = GetTimelineSubsystemFromWidget(this); + if (!TS) return; + + const float Duration = FMath::Max(TS->GetDuration(), 0.0001f); + const float Time = TS->GetCurrentTime(); + + // Auto-deselect a keyframe when the timeline cursor moves away from it + // (slider scrub, playback, or clicking a different key). The selection is tied to + // "cursor parked on this key", so if the cursor leaves, so does the selection. + if (SelectedTimelineTrackIdx >= 0 + && TS->GetTracks().IsValidIndex(SelectedTimelineTrackIdx) + && TS->GetTracks()[SelectedTimelineTrackIdx].Keys.IsValidIndex(SelectedTimelineKeyIdx)) + { + const float SelectedKeyTime = TS->GetTracks()[SelectedTimelineTrackIdx].Keys[SelectedTimelineKeyIdx].Time; + if (!FMath::IsNearlyEqual(Time, SelectedKeyTime, 0.05f)) + { + SelectedTimelineTrackIdx = -1; + SelectedTimelineKeyIdx = -1; + } + } + + // The scrub bar handle position is driven by CurrentTimeAttr (TAttribute) — no manual SetValue needed. + if (TimelineTimeLabel.IsValid()) + { + TimelineTimeLabel->SetText(FText::FromString(FString::Printf(TEXT("%.2f / %.2f s"), Time, Duration))); + } + if (TimelinePlayButtonText.IsValid()) + { + if (TS->IsPlaying()) + { + TimelinePlayButtonText->SetText(FText::FromString(TEXT("Pause"))); + TimelinePlayButtonText->SetColorAndOpacity(FLinearColor(1.0f, 0.8f, 0.2f)); + } + else + { + TimelinePlayButtonText->SetText(FText::FromString(TEXT("Play"))); + TimelinePlayButtonText->SetColorAndOpacity(FLinearColor(0.2f, 0.9f, 0.2f)); + } + } +} + +void UPS_Editor_MainWidget_Legacy::AddKeyForRow(int32 RowIndex) +{ + if (!DynamicPropertyRows.IsValidIndex(RowIndex)) return; + + UPS_Editor_SelectionManager* SM = SelectionManager.Get(); + if (!SM || !SM->IsAnythingSelected()) return; + + AActor* Actor = SM->GetSelectedActors()[0].Get(); + if (!Actor) return; + + UPS_Editor_TimelineSubsystem* TS = GetTimelineSubsystemFromWidget(this); + if (!TS) return; + + const FPS_Editor_PropertyRowWidgetsLegacy& Row = DynamicPropertyRows[RowIndex]; + TS->AddOrUpdateKeyAtCurrentTime(Actor, FName(*Row.Key)); + bSceneDirty = true; +} + +bool UPS_Editor_MainWidget_Legacy::TryDeleteSelectedTimelineKey() +{ + if (SelectedTimelineTrackIdx < 0 || SelectedTimelineKeyIdx < 0) return false; + UPS_Editor_TimelineSubsystem* TS = GetTimelineSubsystemFromWidget(this); + if (!TS) return false; + if (!TS->GetTracks().IsValidIndex(SelectedTimelineTrackIdx)) return false; + if (!TS->GetTracks()[SelectedTimelineTrackIdx].Keys.IsValidIndex(SelectedTimelineKeyIdx)) return false; + + TS->RemoveKey(SelectedTimelineTrackIdx, SelectedTimelineKeyIdx); + SelectedTimelineTrackIdx = -1; + SelectedTimelineKeyIdx = -1; + bSceneDirty = true; + return true; +} diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget_Legacy.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget_Legacy.h new file mode 100644 index 0000000..62881c6 --- /dev/null +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget_Legacy.h @@ -0,0 +1,183 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Blueprint/UserWidget.h" +#include "PS_Editor_SelectionTypes.h" +#include "PS_Editor_MainWidget_Legacy.generated.h" + +class UPS_Editor_SelectionManager; +class UPS_Editor_SpawnManager; +class UPS_Editor_SceneSerializer; + +/** Widget state for a single dynamic property row in the property panel. */ +struct FPS_Editor_PropertyRowWidgetsLegacy +{ + FString ComponentName; + FString PropertyName; + FString DisplayName; + FString Key; // "ComponentName.PropertyName" + FProperty* CachedProperty = nullptr; + + // Widget refs (only relevant ones are used based on property type) + TSharedPtr SingleField; // float, double, int32, FString + TSharedPtr BoolField; // bool + TSharedPtr VecX, VecY, VecZ; // FVector, FRotator + TSharedPtr ColR, ColG, ColB, ColA; // FLinearColor + + // Enum support + TSharedPtr>> EnumCombo; + TSharedPtr EnumComboText; + TSharedPtr>> EnumOptions; // Heap-allocated so OptionsSource pointer survives Row copy + UEnum* CachedEnum = nullptr; +}; + +/** + * Main editor widget for PS_Editor. + * Contains: title bar, transform mode toolbar, properties panel, help bar. + * Built entirely in C++ using Slate. + */ +UCLASS() +class PS_EDITOR_API UPS_Editor_MainWidget_Legacy : public UUserWidget +{ + GENERATED_BODY() + +public: + /** Set manager references. Must be called after construction. */ + void SetSelectionManager(UPS_Editor_SelectionManager* InSelectionManager); + void SetSpawnManager(UPS_Editor_SpawnManager* InSpawnManager); + void SetSceneSerializer(UPS_Editor_SceneSerializer* InSerializer); + +protected: + virtual TSharedRef RebuildWidget() override; + virtual void NativeConstruct() override; + virtual void NativeDestruct() override; + virtual void NativeTick(const FGeometry& MyGeometry, float InDeltaTime) override; + +private: + UPROPERTY() + TWeakObjectPtr SelectionManager; + + UPROPERTY() + TWeakObjectPtr SpawnManager; + + UPROPERTY() + TWeakObjectPtr SceneSerializer; + + // ---- Slate widget refs for dynamic updates ---- + TSharedPtr ModeText; + TSharedPtr SelectionInfoText; + + // Toolbar refs + TSharedPtr SpaceText; + TSharedPtr SnapText; + TSharedPtr SnapSizeField; + + // Spawn catalog + TSharedPtr SpawnListContainer; + + // Scene save/load + TSharedPtr SaveNameField; + TSharedPtr SceneListContainer; + TArray> BaseLevelOptions; + TSharedPtr BaseLevelButtonText; + FString CurrentLevelFilter; + /** Maps DisplayName → full MapPath for base levels (from MasterCatalog). */ + TMap BaseLevelPathMap; + /** Maps DisplayName → additional sublevels to load (from MasterCatalog). */ + TMap> BaseLevelExtraSublevels; + /** Maps DisplayName → Thumbnail texture for base levels. */ + TMap> BaseLevelThumbnails; + TArray> CachedBrushes; + + // BaseLevel selection popup + TSharedPtr BaseLevelPopup; + TSharedPtr BaseLevelGridContainer; + FString PendingBaseLevelSelection; + TMap> BaseLevelCardBorders; + void OpenBaseLevelPopup(); + void UpdateBaseLevelHighlight(); + void ConfirmBaseLevelSelection(); + void CancelBaseLevelPopup(); + + // Transform fields + TSharedPtr LocX, LocY, LocZ; + TSharedPtr RotX, RotY, RotZ; + TSharedPtr ScaX, ScaY, ScaZ; + + // Dynamic property editing + TSharedPtr DynamicPropertiesContainer; + TArray DynamicPropertyRows; + TWeakObjectPtr LastSelectedActor; + + // Outliner + TSharedPtr OutlinerListContainer; + int32 LastOutlinerActorCount = -1; + + // Bottom notification bar + TSharedPtr HelpBarText; + TSharedPtr SplineFinishButton; + TSharedPtr SplineEditButton; + TSharedPtr SplineDeletePointButton; + TSharedPtr ChildEditButton; + + // Simulate button + TSharedPtr SimulateButtonText; + + // Timeline panel + TSharedPtr TimelinePanel; + TSharedPtr TimelineSlider; + TSharedPtr TimelineTimeLabel; + TSharedPtr TimelinePlayButtonText; + TSharedPtr TimelineDurationBox; + TSharedPtr TimelineTracksContainer; + bool bTimelineVisible = false; + FDelegateHandle TimelineChangedHandle; + /** Currently selected timeline key (updated on click). -1 means no selection. */ + int32 SelectedTimelineTrackIdx = -1; + int32 SelectedTimelineKeyIdx = -1; + + // Close confirmation + TSharedPtr CloseConfirmOverlay; + bool bSceneDirty = false; + int32 LastKnownSpawnedCount = 0; + + // Loading screen + TSharedPtr LoadingOverlay; + +public: + /** Show/hide the loading overlay. */ + void ShowLoadingScreen(bool bShow); + + // ---- Helpers ---- + TSharedRef BuildToolbar(); + TSharedRef BuildPropertiesPanel(); + TSharedRef BuildSpawnCatalog(); + TSharedRef BuildSceneButtons(); + TSharedRef BuildOutliner(); + TSharedRef BuildTimelinePanel(); + void RefreshOutliner(); + void PopulateSceneList(); + void PopulateSpawnCatalog(); + void RefreshPropertiesFromSelection(); + void ApplyTransformFromUI(); + void RebuildDynamicProperties(); + void RefreshDynamicPropertyValues(); + void ApplyPropertyFromUI(int32 RowIndex); + void AddKeyForRow(int32 RowIndex); + void ToggleTimelineVisible(); + void RebuildTimelineTracksList(); + void RefreshTimelineUI(); + + /** Try to delete the currently selected timeline key. Returns true if one was deleted. + * Called by the Pawn's global Delete action before it falls back to actor deletion. */ + bool TryDeleteSelectedTimelineKey(); + static UObject* FindPropertyTarget(AActor* Actor, const FString& ComponentName); + FString FloatToString(float Value) const; + + void OnModeButtonClicked(EPS_Editor_TransformMode Mode); + void OnCloseClicked(); + void ExecuteClose(bool bSave); + + /** Whether we are currently refreshing UI from selection (avoid feedback loop). */ + bool bIsRefreshing = false; +}; diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget_UMG.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget_UMG.cpp new file mode 100644 index 0000000..1a5b010 --- /dev/null +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget_UMG.cpp @@ -0,0 +1,237 @@ +#include "PS_Editor_MainWidget_UMG.h" + +#include "PS_Editor_SelectionManager.h" +#include "PS_Editor_SpawnManager.h" +#include "PS_Editor_SceneSerializer.h" +#include "PS_Editor_PlayerController.h" +#include "GameFramework/PlayerController.h" + +void UPS_Editor_MainWidget_UMG::SetSelectionManager(UPS_Editor_SelectionManager* InSelectionManager) +{ + SelectionManager = InSelectionManager; +} + +void UPS_Editor_MainWidget_UMG::SetSpawnManager(UPS_Editor_SpawnManager* InSpawnManager) +{ + SpawnManager = InSpawnManager; +} + +void UPS_Editor_MainWidget_UMG::SetSceneSerializer(UPS_Editor_SceneSerializer* InSerializer) +{ + SceneSerializer = InSerializer; +} + +void UPS_Editor_MainWidget_UMG::NativeConstruct() +{ + Super::NativeConstruct(); + + if (UPS_Editor_SelectionManager* SM = SelectionManager.Get()) + { + SelectionChangedHandle = SM->OnSelectionChanged.AddLambda([this]() + { + OnSelectionChanged.Broadcast(); + }); + } +} + +void UPS_Editor_MainWidget_UMG::NativeDestruct() +{ + if (SelectionChangedHandle.IsValid()) + { + if (UPS_Editor_SelectionManager* SM = SelectionManager.Get()) + { + SM->OnSelectionChanged.Remove(SelectionChangedHandle); + } + SelectionChangedHandle.Reset(); + } + Super::NativeDestruct(); +} + +// -------------------- Actions -------------------- + +void UPS_Editor_MainWidget_UMG::SetTransformMode(EPS_Editor_TransformMode Mode) +{ + if (UPS_Editor_SelectionManager* SM = SelectionManager.Get()) + { + SM->SetTransformMode(Mode); + OnToolChanged.Broadcast(); + } +} + +void UPS_Editor_MainWidget_UMG::ToggleTransformSpace() +{ + if (UPS_Editor_SelectionManager* SM = SelectionManager.Get()) + { + SM->ToggleTransformSpace(); + OnToolChanged.Broadcast(); + } +} + +void UPS_Editor_MainWidget_UMG::ToggleSnap() +{ + if (UPS_Editor_SelectionManager* SM = SelectionManager.Get()) + { + SM->bSnapTranslation = !SM->bSnapTranslation; + SM->bSnapRotation = SM->bSnapTranslation; + SM->bSnapScale = SM->bSnapTranslation; + OnToolChanged.Broadcast(); + } +} + +void UPS_Editor_MainWidget_UMG::SnapSelectionToGround() +{ + if (UPS_Editor_SelectionManager* SM = SelectionManager.Get()) + { + SM->SnapSelectionToGround(); + } +} + +void UPS_Editor_MainWidget_UMG::StartSimulation() +{ + if (APS_Editor_PlayerController* PC = Cast(GetOwningPlayer())) + { + PC->StartSimulation(); + OnSimulateChanged.Broadcast(); + } +} + +void UPS_Editor_MainWidget_UMG::StopSimulation() +{ + if (APS_Editor_PlayerController* PC = Cast(GetOwningPlayer())) + { + PC->StopSimulation(); + OnSimulateChanged.Broadcast(); + } +} + +bool UPS_Editor_MainWidget_UMG::SaveScenario(const FString& Name) +{ + UPS_Editor_SpawnManager* SM = nullptr; + if (APS_Editor_PlayerController* PC = Cast(GetOwningPlayer())) + { + SM = PC->GetSpawnManager(); + } + if (!SM) SM = SpawnManager.Get(); + UPS_Editor_SceneSerializer* Ser = SceneSerializer.Get(); + if (!Ser || !SM || Name.IsEmpty()) return false; + Ser->SaveScene(Name, SM); + OnSceneDirty.Broadcast(); + return true; +} + +bool UPS_Editor_MainWidget_UMG::LoadScenario(const FString& Name) +{ + UPS_Editor_SceneSerializer* Ser = SceneSerializer.Get(); + UPS_Editor_SpawnManager* SM = nullptr; + if (APS_Editor_PlayerController* PC = Cast(GetOwningPlayer())) + { + SM = PC->GetSpawnManager(); + } + if (!SM) SM = SpawnManager.Get(); + if (!Ser || !SM || Name.IsEmpty()) return false; + Ser->LoadScene(Name, SM); + OnSceneDirty.Broadcast(); + return true; +} + +void UPS_Editor_MainWidget_UMG::RequestClose() +{ + if (APS_Editor_PlayerController* PC = Cast(GetOwningPlayer())) + { + PC->CloseEditor(); + } +} + +void UPS_Editor_MainWidget_UMG::SelectActorByPtr(AActor* Actor) +{ + if (!Actor) return; + if (UPS_Editor_SelectionManager* SM = SelectionManager.Get()) + { + SM->ClearSelection(); + SM->SelectActor(Actor); + } +} + +void UPS_Editor_MainWidget_UMG::ClearSelection() +{ + if (UPS_Editor_SelectionManager* SM = SelectionManager.Get()) + { + SM->ClearSelection(); + } +} + +// -------------------- Getters -------------------- + +EPS_Editor_TransformMode UPS_Editor_MainWidget_UMG::GetCurrentTransformMode() const +{ + if (UPS_Editor_SelectionManager* SM = SelectionManager.Get()) return SM->GetTransformMode(); + return EPS_Editor_TransformMode::Translate; +} + +bool UPS_Editor_MainWidget_UMG::IsSnapEnabled() const +{ + if (UPS_Editor_SelectionManager* SM = SelectionManager.Get()) return SM->bSnapTranslation; + return false; +} + +bool UPS_Editor_MainWidget_UMG::IsWorldSpace() const +{ + if (UPS_Editor_SelectionManager* SM = SelectionManager.Get()) return SM->GetTransformSpace() == EPS_Editor_Space::World; + return true; +} + +bool UPS_Editor_MainWidget_UMG::IsSimulating() const +{ + if (APS_Editor_PlayerController* PC = Cast(GetOwningPlayer())) return PC->IsSimulating(); + return false; +} + +TArray UPS_Editor_MainWidget_UMG::GetSpawnedActors() const +{ + TArray Result; + UPS_Editor_SpawnManager* SM = nullptr; + if (APS_Editor_PlayerController* PC = Cast(GetOwningPlayer())) + { + SM = PC->GetSpawnManager(); + } + if (!SM) SM = SpawnManager.Get(); + if (SM) + { + for (const TWeakObjectPtr& Weak : SM->GetSpawnedActors()) + { + if (AActor* A = Weak.Get()) Result.Add(A); + } + } + return Result; +} + +TArray UPS_Editor_MainWidget_UMG::GetSavedScenarioNames() const +{ + if (UPS_Editor_SceneSerializer* Ser = SceneSerializer.Get()) return Ser->GetSavedSceneNames(); + return {}; +} + +AActor* UPS_Editor_MainWidget_UMG::GetFirstSelectedActor() const +{ + if (UPS_Editor_SelectionManager* SM = SelectionManager.Get()) + { + if (SM->IsAnythingSelected()) + { + return SM->GetSelectedActors()[0].Get(); + } + } + return nullptr; +} + +FString UPS_Editor_MainWidget_UMG::GetCurrentScenarioName() const +{ + if (APS_Editor_PlayerController* PC = Cast(GetOwningPlayer())) + { + if (UGameInstance* GI = PC->GetGameInstance()) + { + // Name is kept on the subsystem — look it up via header chain if available. + // Fallback to empty string. + } + } + return FString(); +} diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget_UMG.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget_UMG.h new file mode 100644 index 0000000..e7ff9be --- /dev/null +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/UI/Widgets/PS_Editor_MainWidget_UMG.h @@ -0,0 +1,130 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Blueprint/UserWidget.h" +#include "PS_Editor_SelectionTypes.h" +#include "PS_Editor_MainWidget_UMG.generated.h" + +class UPS_Editor_SelectionManager; +class UPS_Editor_SpawnManager; +class UPS_Editor_SceneSerializer; + +/** + * UMG-based PS_Editor main widget. This class is intentionally minimal in C++ — + * the full UI is authored as Widget Blueprints that derive from this class, and + * call into the Blueprint-exposed API below to drive the editor. + * + * Workflow: + * 1. Create a Widget Blueprint "WBP_PSEditor_UMG" that inherits from + * UPS_Editor_MainWidget_UMG. + * 2. Design the visual layout (floating HUDs, panels, etc.) in the UMG designer. + * 3. In event graphs, call the BlueprintCallable functions (SetTool, ToggleSimulate, + * SaveScenario, etc.) from button click events. + * 4. Subscribe to the BlueprintAssignable delegates (OnToolChanged, OnSelectionChanged…) + * to refresh visual state. + * 5. Switch APS_Editor_HUD::UIMode to EPS_Editor_UIMode::UMG and set UMGWidgetClass + * to WBP_PSEditor_UMG. + */ +UCLASS(Abstract, Blueprintable) +class PS_EDITOR_API UPS_Editor_MainWidget_UMG : public UUserWidget +{ + GENERATED_BODY() + +public: + /** Manager refs — set by the HUD after construction. */ + void SetSelectionManager(UPS_Editor_SelectionManager* InSelectionManager); + void SetSpawnManager(UPS_Editor_SpawnManager* InSpawnManager); + void SetSceneSerializer(UPS_Editor_SceneSerializer* InSerializer); + + // ==================== BP-CALLABLE ACTIONS ==================== + UFUNCTION(BlueprintCallable, Category = "PS Editor|Tools") + void SetTransformMode(EPS_Editor_TransformMode Mode); + + UFUNCTION(BlueprintCallable, Category = "PS Editor|Tools") + void ToggleTransformSpace(); + + UFUNCTION(BlueprintCallable, Category = "PS Editor|Tools") + void ToggleSnap(); + + UFUNCTION(BlueprintCallable, Category = "PS Editor|Tools") + void SnapSelectionToGround(); + + UFUNCTION(BlueprintCallable, Category = "PS Editor|Simulation") + void StartSimulation(); + + UFUNCTION(BlueprintCallable, Category = "PS Editor|Simulation") + void StopSimulation(); + + UFUNCTION(BlueprintCallable, Category = "PS Editor|Scene") + bool SaveScenario(const FString& Name); + + UFUNCTION(BlueprintCallable, Category = "PS Editor|Scene") + bool LoadScenario(const FString& Name); + + UFUNCTION(BlueprintCallable, Category = "PS Editor|Scene") + void RequestClose(); + + UFUNCTION(BlueprintCallable, Category = "PS Editor|Selection") + void SelectActorByPtr(AActor* Actor); + + UFUNCTION(BlueprintCallable, Category = "PS Editor|Selection") + void ClearSelection(); + + // ==================== BP-PURE GETTERS ==================== + UFUNCTION(BlueprintPure, Category = "PS Editor|Tools") + EPS_Editor_TransformMode GetCurrentTransformMode() const; + + UFUNCTION(BlueprintPure, Category = "PS Editor|Tools") + bool IsSnapEnabled() const; + + UFUNCTION(BlueprintPure, Category = "PS Editor|Tools") + bool IsWorldSpace() const; + + UFUNCTION(BlueprintPure, Category = "PS Editor|Simulation") + bool IsSimulating() const; + + UFUNCTION(BlueprintPure, Category = "PS Editor|Scene") + TArray GetSpawnedActors() const; + + UFUNCTION(BlueprintPure, Category = "PS Editor|Scene") + TArray GetSavedScenarioNames() const; + + UFUNCTION(BlueprintPure, Category = "PS Editor|Selection") + AActor* GetFirstSelectedActor() const; + + UFUNCTION(BlueprintPure, Category = "PS Editor|Scene") + FString GetCurrentScenarioName() const; + + // ==================== EVENTS (BP delegates) ==================== + DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnPSE_ToolChanged); + DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnPSE_SelectionChanged); + DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnPSE_SimulateChanged); + DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnPSE_SceneDirty); + + UPROPERTY(BlueprintAssignable, Category = "PS Editor|Events") + FOnPSE_ToolChanged OnToolChanged; + + UPROPERTY(BlueprintAssignable, Category = "PS Editor|Events") + FOnPSE_SelectionChanged OnSelectionChanged; + + UPROPERTY(BlueprintAssignable, Category = "PS Editor|Events") + FOnPSE_SimulateChanged OnSimulateChanged; + + UPROPERTY(BlueprintAssignable, Category = "PS Editor|Events") + FOnPSE_SceneDirty OnSceneDirty; + +protected: + virtual void NativeConstruct() override; + virtual void NativeDestruct() override; + + UPROPERTY() + TWeakObjectPtr SelectionManager; + + UPROPERTY() + TWeakObjectPtr SpawnManager; + + UPROPERTY() + TWeakObjectPtr SceneSerializer; + + FDelegateHandle SelectionChangedHandle; +};