CARLA
Loading...
Searching...
No Matches
LightDefaultsEditorPanel.cpp
Go to the documentation of this file.
1// Copyright (c) 2026 Computer Vision Center (CVC) at the Universitat Autonoma
2// de Barcelona (UAB).
3//
4// This work is licensed under the terms of the MIT license.
5// For a copy, see <https://opensource.org/licenses/MIT>.
6
8
9#if WITH_EDITOR
10
11#include <util/ue-header-guard-begin.h>
12#include "Editor.h"
13#include "Engine/World.h"
14#include "TimerManager.h"
15#include "Framework/Application/SlateApplication.h"
16#include "Framework/Docking/TabManager.h"
17#include "Styling/AppStyle.h"
18#include "Widgets/Colors/SColorBlock.h"
19#include "Widgets/Colors/SColorPicker.h"
20#include "Widgets/Docking/SDockTab.h"
21#include "Widgets/Input/SButton.h"
22#include "Widgets/Input/SCheckBox.h"
23#include "Widgets/Input/SEditableTextBox.h"
24#include "Widgets/Layout/SBox.h"
25#include "Widgets/SBoxPanel.h"
26#include "Widgets/Text/STextBlock.h"
27#include "Widgets/Views/STableRow.h"
28#include <util/ue-header-guard-end.h>
29
30#define LOCTEXT_NAMESPACE "SLightDefaultsPanel"
31
32namespace
33{
34 // One row: a class summary plus its editable fields (Count, Intensity,
35 // Emissive, Color) -- LightGroup itself isn't repeated per row since the
36 // panel's group tabs already filter to one group at a time. Every edit
37 // pushes live onto the level immediately (OnLiveChanged); nothing here
38 // writes Defaults.json -- that's the panel's single "Save All" button,
39 // once an artist is happy with what they see.
40 class SLightDefaultsRow : public STableRow<TSharedPtr<FLightAssetSummary>>
41 {
42 public:
43 SLATE_BEGIN_ARGS(SLightDefaultsRow) {}
44 SLATE_END_ARGS()
45
46 void Construct(const FArguments& InArgs, const TSharedRef<STableViewBase>& OwnerTable, TSharedPtr<FLightAssetSummary> InItem,
47 TFunction<void(TSharedPtr<FLightAssetSummary>)> InOnLiveChanged, TFunction<void(const FString&, bool)> InOnToggleSelect,
48 TFunction<void(const FString&, bool)> InOnTogglePreviewOn = nullptr)
49 {
50 Item = InItem;
51 OnLiveChanged = MoveTemp(InOnLiveChanged);
52 OnToggleSelect = MoveTemp(InOnToggleSelect);
53 OnTogglePreviewOn = MoveTemp(InOnTogglePreviewOn);
54 // Slate's SNew required-args plumbing caps at 5 positional
55 // arguments (RequiredArgs::MakeRequiredArgs), already spent on
56 // (OwnerTable, Item, OnLiveChanged, OnToggleSelect,
57 // OnTogglePreviewOn) -- so a row's display mode is detected from
58 // data already in hand instead of extra constructor arguments.
59 // Traffic lights (OnTogglePreviewOn set) hide Intensity and Color
60 // entirely -- neither is meaningful for a signal lens. Buildings
61 // (fixed synthetic ClassName) repurpose their Intensity slot as
62 // "Percent Lit"; their Color is the windows' emissive colour (see
63 // ULightDefaultsJsonUtils::ApplyProceduralBuildingsEmissiveLive).
64 const bool bIsTrafficRow = static_cast<bool>(OnTogglePreviewOn);
65 const bool bIsBuildingsRow = Item.IsValid() && Item->ClassName == TEXT("ProceduralBuildings");
66 const bool bIsLegacyBuildingsRow = Item.IsValid() && Item->ClassName == TEXT("LegacyBuildings");
67 const bool bIsAnyBuildingsRow = bIsBuildingsRow || bIsLegacyBuildingsRow;
68 const bool bShowIntensity = !bIsTrafficRow;
69 const bool bShowColor = !bIsTrafficRow;
70
71 TSharedRef<SHorizontalBox> RowBox = SNew(SHorizontalBox)
72
73 + SHorizontalBox::Slot().AutoWidth().Padding(2.0f).VAlign(VAlign_Center)
74 [
75 SNew(SBox).WidthOverride(240.0f)
76 [
77 SNew(STextBlock).Text(FText::FromString(Item->ClassName))
78 ]
79 ]
80
81 + SHorizontalBox::Slot().AutoWidth().Padding(2.0f).VAlign(VAlign_Center)
82 [
83 SNew(SBox).WidthOverride(50.0f)
84 [
85 SNew(STextBlock).Text(FText::FromString(FString::FromInt(Item->InstanceCount)))
86 ]
87 ];
88
89 if (bShowIntensity)
90 {
91 const TCHAR* IntensityHint = bIsAnyBuildingsRow ? TEXT("Percent Lit") : TEXT("Intensity");
92 RowBox->AddSlot().AutoWidth().Padding(2.0f)
93 [ MakeNumberBox(IntensityHint, Item->Current.Intensity, [this](float V) { Item->Current.Intensity = V; NotifyLiveChanged(); }) ];
94 }
95
96 RowBox->AddSlot().AutoWidth().Padding(2.0f)
97 [ MakeNumberBox(TEXT("Emissive"), Item->Current.EmissiveIntensity, [this](float V) { Item->Current.EmissiveIntensity = V; NotifyLiveChanged(); }) ];
98
99 if (bShowColor)
100 {
101 RowBox->AddSlot().AutoWidth().Padding(2.0f).VAlign(VAlign_Center)
102 [ MakeColorSwatch(this) ];
103 }
104
105 RowBox->AddSlot().AutoWidth().Padding(6.0f, 2.0f).VAlign(VAlign_Center)
106 [
107 SNew(SCheckBox)
108 .Style(FAppStyle::Get(), "ToggleButtonCheckbox")
109 .Padding(FMargin(8.0f, 2.0f))
110 .IsChecked_Lambda([this]() { return bIsSelected ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; })
111 .OnCheckStateChanged_Lambda([this](ECheckBoxState NewState)
112 {
113 bIsSelected = (NewState == ECheckBoxState::Checked);
114 if (OnToggleSelect)
115 {
116 OnToggleSelect(Item->ClassName, bIsSelected);
117 }
118 })
119 [
120 SNew(STextBlock).Text(LOCTEXT("SelectButton", "Select"))
121 ]
122 ];
123
124 if (OnTogglePreviewOn)
125 {
126 RowBox->AddSlot().AutoWidth().Padding(2.0f, 2.0f).VAlign(VAlign_Center)
127 [
128 SNew(SCheckBox)
129 .Style(FAppStyle::Get(), "ToggleButtonCheckbox")
130 .Padding(FMargin(8.0f, 2.0f))
131 .IsChecked_Lambda([this]() { return bIsPreviewOn ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; })
132 .OnCheckStateChanged_Lambda([this](ECheckBoxState NewState)
133 {
134 bIsPreviewOn = (NewState == ECheckBoxState::Checked);
135 if (OnTogglePreviewOn)
136 {
137 OnTogglePreviewOn(Item->ClassName, bIsPreviewOn);
138 }
139 })
140 [
141 SNew(STextBlock).Text(LOCTEXT("PreviewOnButton", "Preview On"))
142 ]
143 ];
144 }
145
146 STableRow<TSharedPtr<FLightAssetSummary>>::Construct(
147 STableRow<TSharedPtr<FLightAssetSummary>>::FArguments()
148 .Padding(FMargin(4.0f, 2.0f))
149 [ RowBox ],
150 OwnerTable);
151 }
152
153 private:
154 TSharedPtr<FLightAssetSummary> Item;
155 TFunction<void(TSharedPtr<FLightAssetSummary>)> OnLiveChanged;
156 TFunction<void(const FString&, bool)> OnToggleSelect;
157 TFunction<void(const FString&, bool)> OnTogglePreviewOn;
158 bool bIsSelected = false;
159 bool bIsPreviewOn = false;
160
161 void NotifyLiveChanged()
162 {
163 if (OnLiveChanged)
164 {
165 OnLiveChanged(Item);
166 }
167 }
168
169 static TSharedRef<SWidget> MakeNumberBox(const TCHAR* Hint, float InitialValue, TFunction<void(float)> OnChanged)
170 {
171 return SNew(SBox).WidthOverride(70.0f)
172 [
173 SNew(SEditableTextBox)
174 .HintText(FText::FromString(Hint))
175 .ToolTipText(FText::FromString(Hint))
176 .Text(FText::FromString(FString::SanitizeFloat(InitialValue)))
177 .OnTextCommitted_Lambda([OnChanged](const FText& NewText, ETextCommit::Type)
178 {
179 OnChanged(FCString::Atof(*NewText.ToString()));
180 })
181 ];
182 }
183
184 // A clickable color swatch that opens UE's own color picker -- no
185 // separate R/G/B text fields.
186 static TSharedRef<SWidget> MakeColorSwatch(SLightDefaultsRow* Row)
187 {
188 TSharedPtr<FLightAssetSummary> InItem = Row->Item;
189 return SNew(SBox).WidthOverride(40.0f).HeightOverride(20.0f)
190 [
191 SNew(SColorBlock)
192 .Color_Lambda([InItem]() { return InItem->Current.Color; })
193 .ShowBackgroundForAlpha(false)
194 .OnMouseButtonDown_Lambda([InItem, Row](const FGeometry&, const FPointerEvent&) -> FReply
195 {
196 FColorPickerArgs PickerArgs;
197 PickerArgs.bUseAlpha = false;
198 PickerArgs.InitialColor = InItem->Current.Color;
199 PickerArgs.OnColorCommitted = FOnLinearColorValueChanged::CreateLambda(
200 [InItem, Row](FLinearColor NewColor) { InItem->Current.Color = NewColor; Row->NotifyLiveChanged(); });
201 OpenColorPicker(PickerArgs);
202 return FReply::Handled();
203 })
204 ];
205 }
206 };
207
208 // Vehicles row: a fixed global "Intensity" field plus one narrow field
209 // per canonical light group (ULightDefaultsJsonUtils::GetVehicleLightGroupNames),
210 // disabled/greyed when Item->GroupPresent says this particular vehicle's
211 // material doesn't expose that parameter -- rather than showing a
212 // control that would silently do nothing. No Color (vehicles' Emissive
213 // material has no such concept) and no Emissive column (Intensity here
214 // IS that column, see FVehicleLightSummary). "Preview" replaces Select:
215 // most rows have no placed instance in the level to jump to, since this
216 // tab lists the full catalog, not a level scan.
217 class SVehicleLightRow : public STableRow<TSharedPtr<FVehicleLightSummary>>
218 {
219 public:
220 SLATE_BEGIN_ARGS(SVehicleLightRow) {}
221 SLATE_END_ARGS()
222
223 void Construct(const FArguments& InArgs, const TSharedRef<STableViewBase>& OwnerTable, TSharedPtr<FVehicleLightSummary> InItem,
224 TFunction<void(TSharedPtr<FVehicleLightSummary>)> InOnLiveChanged, TFunction<void(const FString&, bool)> InOnTogglePreview)
225 {
226 Item = InItem;
227 OnLiveChanged = MoveTemp(InOnLiveChanged);
228 OnTogglePreview = MoveTemp(InOnTogglePreview);
229
230 TSharedRef<SHorizontalBox> RowBox = SNew(SHorizontalBox)
231
232 + SHorizontalBox::Slot().AutoWidth().Padding(2.0f).VAlign(VAlign_Center)
233 [
234 SNew(SBox).WidthOverride(200.0f)
235 [
236 SNew(STextBlock).Text(FText::FromString(Item->DisplayName)).ToolTipText(FText::FromString(Item->ClassName))
237 ]
238 ]
239
240 + SHorizontalBox::Slot().AutoWidth().Padding(2.0f).VAlign(VAlign_Center)
241 [
242 SNew(SBox).WidthOverride(40.0f)
243 [
244 SNew(STextBlock).Text(FText::FromString(FString::FromInt(Item->InstanceCount)))
245 ]
246 ]
247
248 + SHorizontalBox::Slot().AutoWidth().Padding(2.0f)
249 [ MakeNumberBox(TEXT("Intensity"), 55.0f, Item->Intensity, [this](float V) { Item->Intensity = V; NotifyLiveChanged(); }) ];
250
251 for (const FString& GroupName : ULightDefaultsJsonUtils::GetVehicleLightGroupNames())
252 {
253 const bool bPresent = Item->GroupPresent.Contains(GroupName) && Item->GroupPresent[GroupName];
254 const float InitialValue = Item->GroupIntensity.Contains(GroupName) ? Item->GroupIntensity[GroupName] : 0.0f;
255 TSharedRef<SWidget> Field = MakeNumberBox(*GroupName, 55.0f, InitialValue, [this, GroupName](float V)
256 {
257 Item->GroupIntensity.Add(GroupName, V);
258 NotifyLiveChanged();
259 });
260 Field->SetEnabled(bPresent);
261 RowBox->AddSlot().AutoWidth().Padding(1.0f)[ Field ];
262 }
263
264 RowBox->AddSlot().AutoWidth().Padding(6.0f, 2.0f).VAlign(VAlign_Center)
265 [
266 SNew(SCheckBox)
267 .Style(FAppStyle::Get(), "ToggleButtonCheckbox")
268 .Padding(FMargin(8.0f, 2.0f))
269 .IsChecked_Lambda([this]() { return bIsPreviewOn ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; })
270 .OnCheckStateChanged_Lambda([this](ECheckBoxState NewState)
271 {
272 bIsPreviewOn = (NewState == ECheckBoxState::Checked);
273 if (OnTogglePreview)
274 {
275 OnTogglePreview(Item->ClassName, bIsPreviewOn);
276 }
277 if (bIsPreviewOn)
278 {
279 // The freshly spawned actor still has the asset's raw
280 // authored material values -- push whatever's
281 // currently in this row onto it immediately, instead
282 // of waiting for the next field edit.
283 NotifyLiveChanged();
284 }
285 })
286 [
287 SNew(STextBlock).Text(LOCTEXT("PreviewVehicleButton", "Preview"))
288 ]
289 ];
290
291 STableRow<TSharedPtr<FVehicleLightSummary>>::Construct(
292 STableRow<TSharedPtr<FVehicleLightSummary>>::FArguments()
293 .Padding(FMargin(4.0f, 2.0f))
294 [ RowBox ],
295 OwnerTable);
296 }
297
298 private:
299 TSharedPtr<FVehicleLightSummary> Item;
300 TFunction<void(TSharedPtr<FVehicleLightSummary>)> OnLiveChanged;
301 TFunction<void(const FString&, bool)> OnTogglePreview;
302 bool bIsPreviewOn = false;
303
304 void NotifyLiveChanged()
305 {
306 if (OnLiveChanged)
307 {
308 OnLiveChanged(Item);
309 }
310 }
311
312 static TSharedRef<SWidget> MakeNumberBox(const TCHAR* Hint, float Width, float InitialValue, TFunction<void(float)> OnChanged)
313 {
314 return SNew(SBox).WidthOverride(Width)
315 [
316 SNew(SEditableTextBox)
317 .HintText(FText::FromString(Hint))
318 .ToolTipText(FText::FromString(Hint))
319 .Text(FText::FromString(FString::SanitizeFloat(InitialValue)))
320 .OnTextCommitted_Lambda([OnChanged](const FText& NewText, ETextCommit::Type)
321 {
322 OnChanged(FCString::Atof(*NewText.ToString()));
323 })
324 ];
325 }
326 };
327}
328
329TSharedRef<SWidget> SLightDefaultsPanel::MakeGroupTab(ELightType Group, const FText& Label)
330{
331 return SNew(SCheckBox)
332 .Style(FAppStyle::Get(), "ToggleButtonCheckbox")
333 .Padding(FMargin(10.0f, 4.0f))
334 .IsChecked_Lambda([this, Group]()
335 {
336 return (!bShowingTrafficLights && !bShowingBuildings && !bShowingVehicles && ActiveFilter == Group) ? ECheckBoxState::Checked : ECheckBoxState::Unchecked;
337 })
338 .OnCheckStateChanged_Lambda([this, Group](ECheckBoxState)
339 {
340 bShowingTrafficLights = false;
341 bShowingBuildings = false;
342 bShowingVehicles = false;
343 ActiveFilter = Group;
344 ApplyFilter();
345 })
346 [
347 SNew(STextBlock).Text(Label)
348 ];
349}
350
351TSharedRef<SWidget> SLightDefaultsPanel::MakeTrafficTab()
352{
353 return SNew(SCheckBox)
354 .Style(FAppStyle::Get(), "ToggleButtonCheckbox")
355 .Padding(FMargin(10.0f, 4.0f))
356 .IsChecked_Lambda([this]()
357 {
358 return bShowingTrafficLights ? ECheckBoxState::Checked : ECheckBoxState::Unchecked;
359 })
360 .OnCheckStateChanged_Lambda([this](ECheckBoxState)
361 {
362 bShowingTrafficLights = true;
363 bShowingBuildings = false;
364 bShowingVehicles = false;
365 ApplyFilter();
366 })
367 [
368 SNew(STextBlock).Text(LOCTEXT("TabTrafficLight", "Traffic Lights"))
369 ];
370}
371
372TSharedRef<SWidget> SLightDefaultsPanel::MakeBuildingsTab()
373{
374 return SNew(SCheckBox)
375 .Style(FAppStyle::Get(), "ToggleButtonCheckbox")
376 .Padding(FMargin(10.0f, 4.0f))
377 .IsChecked_Lambda([this]()
378 {
379 return bShowingBuildings ? ECheckBoxState::Checked : ECheckBoxState::Unchecked;
380 })
381 .OnCheckStateChanged_Lambda([this](ECheckBoxState)
382 {
383 bShowingBuildings = true;
384 bShowingTrafficLights = false;
385 bShowingVehicles = false;
386 ApplyFilter();
387 })
388 [
389 SNew(STextBlock).Text(LOCTEXT("TabBuildings", "Buildings"))
390 ];
391}
392
393TSharedRef<SWidget> SLightDefaultsPanel::MakeVehiclesTab()
394{
395 return SNew(SCheckBox)
396 .Style(FAppStyle::Get(), "ToggleButtonCheckbox")
397 .Padding(FMargin(10.0f, 4.0f))
398 .IsChecked_Lambda([this]()
399 {
400 return bShowingVehicles ? ECheckBoxState::Checked : ECheckBoxState::Unchecked;
401 })
402 .OnCheckStateChanged_Lambda([this](ECheckBoxState)
403 {
404 bShowingVehicles = true;
405 bShowingTrafficLights = false;
406 bShowingBuildings = false;
407 ApplyFilter();
408 })
409 [
410 SNew(STextBlock).Text(LOCTEXT("TabVehicles", "Vehicles"))
411 ];
412}
413
414void SLightDefaultsPanel::Construct(const FArguments& InArgs)
415{
416 ChildSlot
417 [
418 SNew(SVerticalBox)
419
420 + SVerticalBox::Slot().AutoHeight().Padding(4.0f)
421 [
422 SNew(SHorizontalBox)
423
424 + SHorizontalBox::Slot().AutoWidth().Padding(2.0f)
425 [
426 SNew(SButton)
427 .Text(LOCTEXT("ScanButton", "Scan Level"))
428 .OnClicked(this, &SLightDefaultsPanel::OnScanClicked)
429 ]
430
431 + SHorizontalBox::Slot().AutoWidth().Padding(2.0f)
432 [
433 SNew(SButton)
434 .Text(LOCTEXT("ReapplyButton", "Reapply To Level"))
435 .ToolTipText(LOCTEXT("ReapplyTooltip", "Reverts any live, unsaved preview edits back to the last saved catalog on disk."))
436 .OnClicked(this, &SLightDefaultsPanel::OnReapplyClicked)
437 ]
438
439 + SHorizontalBox::Slot().AutoWidth().Padding(2.0f)
440 [
441 SNew(SButton)
442 .Text(LOCTEXT("SaveAllButton", "Save All"))
443 .ToolTipText(LOCTEXT("SaveAllTooltip", "Commits every row's current (already live-previewed) values to Config/Lights/Defaults.json."))
444 .OnClicked(this, &SLightDefaultsPanel::OnSaveAllClicked)
445 ]
446 ]
447
448 + SVerticalBox::Slot().AutoHeight().Padding(4.0f, 0.0f)
449 [
450 SNew(SHorizontalBox)
451
452 + SHorizontalBox::Slot().AutoWidth().Padding(2.0f)
453 [ MakeGroupTab(ELightType::Street, LOCTEXT("TabStreet", "Street")) ]
454
455 + SHorizontalBox::Slot().AutoWidth().Padding(2.0f)
456 [ MakeTrafficTab() ]
457
458 + SHorizontalBox::Slot().AutoWidth().Padding(2.0f)
459 [ MakeBuildingsTab() ]
460
461 + SHorizontalBox::Slot().AutoWidth().Padding(2.0f)
462 [ MakeGroupTab(ELightType::Other, LOCTEXT("TabOther", "Other")) ]
463
464 + SHorizontalBox::Slot().AutoWidth().Padding(2.0f)
465 [ MakeVehiclesTab() ]
466 ]
467
468 + SVerticalBox::Slot().AutoHeight().Padding(6.0f, 4.0f, 4.0f, 0.0f)
469 [
470 SNew(SHorizontalBox)
471 .Visibility_Lambda([this]() { return bShowingVehicles ? EVisibility::Collapsed : EVisibility::Visible; })
472
473 + SHorizontalBox::Slot().AutoWidth().Padding(2.0f)
474 [ SNew(SBox).WidthOverride(240.0f) [ SNew(STextBlock).Text(LOCTEXT("HeaderClass", "Class")) ] ]
475
476 + SHorizontalBox::Slot().AutoWidth().Padding(2.0f)
477 [ SNew(SBox).WidthOverride(50.0f) [ SNew(STextBlock).Text(LOCTEXT("HeaderCount", "Count")) ] ]
478
479 + SHorizontalBox::Slot().AutoWidth().Padding(2.0f)
480 [
481 SNew(SBox).WidthOverride(70.0f)
482 [
483 SNew(STextBlock).Text_Lambda([this]() -> FText
484 {
485 return bShowingBuildings
486 ? LOCTEXT("HeaderPercentLit", "Percent Lit")
487 : LOCTEXT("HeaderIntensity", "Intensity");
488 })
489 ]
490 ]
491
492 + SHorizontalBox::Slot().AutoWidth().Padding(2.0f)
493 [ SNew(SBox).WidthOverride(70.0f) [ SNew(STextBlock).Text(LOCTEXT("HeaderEmissive", "Emissive")) ] ]
494
495 + SHorizontalBox::Slot().AutoWidth().Padding(2.0f)
496 [ SNew(SBox).WidthOverride(40.0f) [ SNew(STextBlock).Text(LOCTEXT("HeaderColor", "Color")) ] ]
497
498 + SHorizontalBox::Slot().AutoWidth().Padding(6.0f, 2.0f)
499 [ SNew(STextBlock).Text(LOCTEXT("HeaderSelect", "Select")) ]
500 ]
501
502 + SVerticalBox::Slot().FillHeight(1.0f).Padding(4.0f)
503 [
504 SAssignNew(RowsListView, SListView<TSharedPtr<FLightAssetSummary>>)
505 .Visibility_Lambda([this]() { return bShowingVehicles ? EVisibility::Collapsed : EVisibility::Visible; })
506 .ListItemsSource(&Rows)
507 .SelectionMode(ESelectionMode::None)
508 .OnGenerateRow(this, &SLightDefaultsPanel::OnGenerateRow)
509 ]
510
511 // Vehicles: a wider, differently-shaped header/row (see
512 // SVehicleLightRow) -- a second header row + ListView, occupying the
513 // same area but Collapsed (not just hidden) whenever this tab isn't
514 // active, same trick as above in reverse.
515 + SVerticalBox::Slot().AutoHeight().Padding(6.0f, 4.0f, 4.0f, 0.0f)
516 [
517 SNew(SHorizontalBox)
518 .Visibility_Lambda([this]() { return bShowingVehicles ? EVisibility::Visible : EVisibility::Collapsed; })
519
520 + SHorizontalBox::Slot().AutoWidth().Padding(2.0f)
521 [ SNew(SBox).WidthOverride(200.0f) [ SNew(STextBlock).Text(LOCTEXT("HeaderVehicleClass", "Vehicle")) ] ]
522
523 + SHorizontalBox::Slot().AutoWidth().Padding(2.0f)
524 [ SNew(SBox).WidthOverride(40.0f) [ SNew(STextBlock).Text(LOCTEXT("HeaderCount2", "Count")) ] ]
525
526 + SHorizontalBox::Slot().AutoWidth().Padding(2.0f)
527 [ SNew(SBox).WidthOverride(55.0f) [ SNew(STextBlock).Text(LOCTEXT("HeaderVehicleIntensity", "Intensity")) ] ]
528
529 + SHorizontalBox::Slot().AutoWidth().Padding(1.0f)
530 [
531 SNew(SHorizontalBox)
532 + SHorizontalBox::Slot().AutoWidth().Padding(1.0f)
533 [ SNew(SBox).WidthOverride(55.0f) [ SNew(STextBlock).Text(LOCTEXT("HeaderPos", "Position")).ToolTipText(LOCTEXT("HeaderPosTT", "Position")) ] ]
534 + SHorizontalBox::Slot().AutoWidth().Padding(1.0f)
535 [ SNew(SBox).WidthOverride(55.0f) [ SNew(STextBlock).Text(LOCTEXT("HeaderLow", "Low Beam")).ToolTipText(LOCTEXT("HeaderLowTT", "Low Beam")) ] ]
536 + SHorizontalBox::Slot().AutoWidth().Padding(1.0f)
537 [ SNew(SBox).WidthOverride(55.0f) [ SNew(STextBlock).Text(LOCTEXT("HeaderHigh", "High Beam")).ToolTipText(LOCTEXT("HeaderHighTT", "High Beam")) ] ]
538 + SHorizontalBox::Slot().AutoWidth().Padding(1.0f)
539 [ SNew(SBox).WidthOverride(55.0f) [ SNew(STextBlock).Text(LOCTEXT("HeaderFog", "Fog")).ToolTipText(LOCTEXT("HeaderFogTT", "Fog")) ] ]
540 + SHorizontalBox::Slot().AutoWidth().Padding(1.0f)
541 [ SNew(SBox).WidthOverride(55.0f) [ SNew(STextBlock).Text(LOCTEXT("HeaderBrake", "Brake")).ToolTipText(LOCTEXT("HeaderBrakeTT", "Brake")) ] ]
542 + SHorizontalBox::Slot().AutoWidth().Padding(1.0f)
543 [ SNew(SBox).WidthOverride(55.0f) [ SNew(STextBlock).Text(LOCTEXT("HeaderReverse", "Reverse")).ToolTipText(LOCTEXT("HeaderReverseTT", "Reverse")) ] ]
544 + SHorizontalBox::Slot().AutoWidth().Padding(1.0f)
545 [ SNew(SBox).WidthOverride(55.0f) [ SNew(STextBlock).Text(LOCTEXT("HeaderLBlink", "L Blink")).ToolTipText(LOCTEXT("HeaderLBlinkTT", "Left Blinker")) ] ]
546 + SHorizontalBox::Slot().AutoWidth().Padding(1.0f)
547 [ SNew(SBox).WidthOverride(55.0f) [ SNew(STextBlock).Text(LOCTEXT("HeaderRBlink", "R Blink")).ToolTipText(LOCTEXT("HeaderRBlinkTT", "Right Blinker")) ] ]
548 + SHorizontalBox::Slot().AutoWidth().Padding(1.0f)
549 [ SNew(SBox).WidthOverride(55.0f) [ SNew(STextBlock).Text(LOCTEXT("HeaderSpecial1", "Special1")).ToolTipText(LOCTEXT("HeaderSpecial1TT", "Siren/lightbar")) ] ]
550 ]
551
552 + SHorizontalBox::Slot().AutoWidth().Padding(6.0f, 2.0f)
553 [ SNew(STextBlock).Text(LOCTEXT("HeaderPreview", "Preview")) ]
554 ]
555
556 + SVerticalBox::Slot().FillHeight(1.0f).Padding(4.0f)
557 [
558 SAssignNew(VehicleRowsListView, SListView<TSharedPtr<FVehicleLightSummary>>)
559 .Visibility_Lambda([this]() { return bShowingVehicles ? EVisibility::Visible : EVisibility::Collapsed; })
560 .ListItemsSource(&AllVehicleRows)
561 .SelectionMode(ESelectionMode::None)
562 .OnGenerateRow(this, &SLightDefaultsPanel::OnGenerateVehicleRow)
563 ]
564 ];
565}
566
567FReply SLightDefaultsPanel::OnScanClicked()
568{
569 UWorld* World = GEditor ? GEditor->GetEditorWorldContext().World() : nullptr;
570
571 AllRows.Reset();
572 for (const FLightAssetSummary& Summary : ULightDefaultsJsonUtils::ScanLightAssetsInLevel(World))
573 {
574 AllRows.Add(MakeShared<FLightAssetSummary>(Summary));
575 }
576
577 AllTrafficRows.Reset();
578 for (const FLightAssetSummary& Summary : ULightDefaultsJsonUtils::ScanTrafficLightAssetsInLevel(World))
579 {
580 AllTrafficRows.Add(MakeShared<FLightAssetSummary>(Summary));
581 }
582
583 AllBuildingRows.Reset();
584 for (const FLightAssetSummary& Summary : ULightDefaultsJsonUtils::ScanProceduralBuildingsInLevel(World))
585 {
586 AllBuildingRows.Add(MakeShared<FLightAssetSummary>(Summary));
587 }
588
589 AllLegacyBuildingRows.Reset();
590 for (const FLightAssetSummary& Summary : ULightDefaultsJsonUtils::ScanLegacyBuildingsInLevel(World))
591 {
592 AllLegacyBuildingRows.Add(MakeShared<FLightAssetSummary>(Summary));
593 }
594
595 AllVehicleRows.Reset();
596 for (const FVehicleLightSummary& Summary : ULightDefaultsJsonUtils::ScanVehicleCatalog(World))
597 {
598 AllVehicleRows.Add(MakeShared<FVehicleLightSummary>(Summary));
599 }
600 if (VehicleRowsListView.IsValid())
601 {
602 VehicleRowsListView->RequestListRefresh();
603 }
604
605 ApplyFilter();
606 return FReply::Handled();
607}
608
609void SLightDefaultsPanel::ApplyFilter()
610{
611 if (bShowingVehicles)
612 {
613 // Vehicles has its own array/ListView (AllVehicleRows/VehicleRowsListView),
614 // not the Rows subset this function otherwise builds -- see OnScanClicked.
615 return;
616 }
617
618 Rows.Reset();
619 if (bShowingTrafficLights)
620 {
621 Rows = AllTrafficRows;
622 }
623 else if (bShowingBuildings)
624 {
625 // Baked (HISM, per-window) and legacy (non-HISM, per-building) rows
626 // use Percent Lit -- see SLightDefaultsRow's bIsAnyBuildingsRow.
627 // AllRows items with bIsBuildingClass are real UCarlaLight-driven
628 // buildings (BP_Apt*, BP_Factory*, ...), routed here regardless of
629 // their artist-authored LightGroup because they live under
630 // Static/Building/ -- they use the normal Intensity/Color UI, not
631 // Percent Lit, same as any Street/Vehicle/Other row.
632 Rows = AllBuildingRows;
633 Rows.Append(AllLegacyBuildingRows);
634 for (const TSharedPtr<FLightAssetSummary>& Item : AllRows)
635 {
636 if (Item->bIsBuildingClass)
637 {
638 Rows.Add(Item);
639 }
640 }
641 }
642 else
643 {
644 for (const TSharedPtr<FLightAssetSummary>& Item : AllRows)
645 {
646 if (Item->bIsBuildingClass)
647 {
648 continue;
649 }
650 // No dedicated tab for ELightType::Building -- folded into
651 // Other, since with baked/legacy buildings covered by their own
652 // material-based tab, this group saw little standalone use.
653 const bool bMatchesGroup = (Item->LightGroup == ActiveFilter) ||
654 (ActiveFilter == ELightType::Other && Item->LightGroup == ELightType::Building);
655 if (bMatchesGroup)
656 {
657 Rows.Add(Item);
658 }
659 }
660 }
661
662 if (RowsListView.IsValid())
663 {
664 RowsListView->RequestListRefresh();
665 }
666}
667
668FReply SLightDefaultsPanel::OnReapplyClicked()
669{
670 UWorld* World = GEditor ? GEditor->GetEditorWorldContext().World() : nullptr;
676 return FReply::Handled();
677}
678
679FReply SLightDefaultsPanel::OnSaveAllClicked()
680{
681 // Saves every scanned row (AllRows/AllTrafficRows, not just the currently
682 // filtered group's Rows) so switching tabs doesn't lose an edit made
683 // before Save All was clicked. Same FLightAssetDefault/JSON format for
684 // both -- traffic rows just leave Intensity/Color at their unused
685 // defaults.
686 for (const TSharedPtr<FLightAssetSummary>& Item : AllRows)
687 {
688 if (Item.IsValid())
689 {
690 ULightDefaultsJsonUtils::SaveClassDefault(Item->ClassName, Item->Current);
691 }
692 }
693 for (const TSharedPtr<FLightAssetSummary>& Item : AllTrafficRows)
694 {
695 if (Item.IsValid())
696 {
697 ULightDefaultsJsonUtils::SaveClassDefault(Item->ClassName, Item->Current);
698 }
699 }
700 for (const TSharedPtr<FLightAssetSummary>& Item : AllBuildingRows)
701 {
702 if (Item.IsValid())
703 {
704 ULightDefaultsJsonUtils::SaveClassDefault(Item->ClassName, Item->Current);
705 }
706 }
707 for (const TSharedPtr<FLightAssetSummary>& Item : AllLegacyBuildingRows)
708 {
709 if (Item.IsValid())
710 {
711 ULightDefaultsJsonUtils::SaveClassDefault(Item->ClassName, Item->Current);
712 }
713 }
714 for (const TSharedPtr<FVehicleLightSummary>& Item : AllVehicleRows)
715 {
716 if (Item.IsValid())
717 {
718 ULightDefaultsJsonUtils::SaveVehicleLightDefault(Item->ClassName, Item->Intensity, Item->GroupIntensity);
719 }
720 }
721 return FReply::Handled();
722}
723
724void SLightDefaultsPanel::ApplyRowLive(TSharedPtr<FLightAssetSummary> Item)
725{
726 if (!Item.IsValid())
727 {
728 return;
729 }
730 UWorld* World = GEditor ? GEditor->GetEditorWorldContext().World() : nullptr;
731 ULightDefaultsJsonUtils::ApplyLightDefaultToClassLive(World, Item->ClassName, Item->Current);
732}
733
734void SLightDefaultsPanel::SetClassSelected(const FString& ClassName, bool bSelected)
735{
736 UWorld* World = GEditor ? GEditor->GetEditorWorldContext().World() : nullptr;
737 ULightDefaultsJsonUtils::SetClassActorsSelected(World, ClassName, bSelected);
738}
739
740void SLightDefaultsPanel::ApplyTrafficRowLive(TSharedPtr<FLightAssetSummary> Item)
741{
742 if (!Item.IsValid())
743 {
744 return;
745 }
746 UWorld* World = GEditor ? GEditor->GetEditorWorldContext().World() : nullptr;
747 ULightDefaultsJsonUtils::ApplyTrafficLightDefaultToClassLive(World, Item->ClassName, Item->Current.EmissiveIntensity);
748}
749
750void SLightDefaultsPanel::SetTrafficClassSelected(const FString& ClassName, bool bSelected)
751{
752 UWorld* World = GEditor ? GEditor->GetEditorWorldContext().World() : nullptr;
754}
755
756void SLightDefaultsPanel::SetTrafficClassPreviewOn(const FString& ClassName, bool bOn)
757{
758 UWorld* World = GEditor ? GEditor->GetEditorWorldContext().World() : nullptr;
760}
761
762void SLightDefaultsPanel::ApplyBuildingRowLive(TSharedPtr<FLightAssetSummary> Item)
763{
764 if (!Item.IsValid())
765 {
766 return;
767 }
768 UWorld* World = GEditor ? GEditor->GetEditorWorldContext().World() : nullptr;
769 ULightDefaultsJsonUtils::ApplyProceduralBuildingsEmissiveLive(World, Item->Current.EmissiveIntensity, Item->Current.Intensity, Item->Current.Color);
770}
771
772void SLightDefaultsPanel::SetBuildingSelected(bool bSelected)
773{
774 UWorld* World = GEditor ? GEditor->GetEditorWorldContext().World() : nullptr;
776}
777
778void SLightDefaultsPanel::ApplyLegacyBuildingRowLive(TSharedPtr<FLightAssetSummary> Item)
779{
780 if (!Item.IsValid())
781 {
782 return;
783 }
784 UWorld* World = GEditor ? GEditor->GetEditorWorldContext().World() : nullptr;
785 ULightDefaultsJsonUtils::ApplyLegacyBuildingsEmissiveLive(World, Item->Current.EmissiveIntensity, Item->Current.Intensity, Item->Current.Color);
786}
787
788void SLightDefaultsPanel::SetLegacyBuildingSelected(bool bSelected)
789{
790 UWorld* World = GEditor ? GEditor->GetEditorWorldContext().World() : nullptr;
792}
793
794void SLightDefaultsPanel::ApplyVehicleRowLive(TSharedPtr<FVehicleLightSummary> Item)
795{
796 if (!Item.IsValid())
797 {
798 return;
799 }
800 UWorld* World = GEditor ? GEditor->GetEditorWorldContext().World() : nullptr;
801 ULightDefaultsJsonUtils::ApplyVehicleLightsLive(World, Item->ClassName, Item->Intensity, Item->GroupIntensity);
802}
803
804void SLightDefaultsPanel::ToggleVehiclePreview(const FString& ClassName, bool bPreviewOn)
805{
806 UWorld* World = GEditor ? GEditor->GetEditorWorldContext().World() : nullptr;
807 if (bPreviewOn)
808 {
810 // Blinkers are gated by Sine('VisualTime') in the vehicle lights
811 // material -- that clock only advances during Play, so it can sit on
812 // a dark half of the wave in a static viewport. Pin it to a
813 // confirmed-visible value so Left/Right Blinker actually shows on
814 // the freshly spawned preview without running the simulation.
816 }
817 else
818 {
820 }
821}
822
823TSharedRef<ITableRow> SLightDefaultsPanel::OnGenerateRow(
824 TSharedPtr<FLightAssetSummary> Item,
825 const TSharedRef<STableViewBase>& OwnerTable)
826{
827 // bShowingTrafficLights/bShowingBuildings are stable for the whole
828 // refresh that triggers this (set before ApplyFilter runs), so it's safe
829 // to branch once here rather than re-checking per row at click time.
830 if (bShowingTrafficLights)
831 {
832 return SNew(SLightDefaultsRow, OwnerTable, Item,
833 [this](TSharedPtr<FLightAssetSummary> RowItem) { ApplyTrafficRowLive(RowItem); },
834 [this](const FString& ClassName, bool bSelected) { SetTrafficClassSelected(ClassName, bSelected); },
835 [this](const FString& ClassName, bool bOn) { SetTrafficClassPreviewOn(ClassName, bOn); });
836 }
837 if (bShowingBuildings)
838 {
839 // Baked and legacy rows share this tab now -- tell them apart by
840 // ClassName so each wires to its own apply/select target. A row
841 // that's neither is a real UCarlaLight-driven building
842 // (bIsBuildingClass, e.g. an Apt-family row) -- wire it exactly
843 // like a normal Street/Vehicle/Other row.
844 const bool bIsLegacyRow = Item.IsValid() && Item->ClassName == TEXT("LegacyBuildings");
845 const bool bIsBakedRow = Item.IsValid() && Item->ClassName == TEXT("ProceduralBuildings");
846 if (bIsLegacyRow)
847 {
848 return SNew(SLightDefaultsRow, OwnerTable, Item,
849 [this](TSharedPtr<FLightAssetSummary> RowItem) { ApplyLegacyBuildingRowLive(RowItem); },
850 [this](const FString&, bool bSelected) { SetLegacyBuildingSelected(bSelected); });
851 }
852 if (bIsBakedRow)
853 {
854 return SNew(SLightDefaultsRow, OwnerTable, Item,
855 [this](TSharedPtr<FLightAssetSummary> RowItem) { ApplyBuildingRowLive(RowItem); },
856 [this](const FString&, bool bSelected) { SetBuildingSelected(bSelected); });
857 }
858 return SNew(SLightDefaultsRow, OwnerTable, Item,
859 [this](TSharedPtr<FLightAssetSummary> RowItem) { ApplyRowLive(RowItem); },
860 [this](const FString& ClassName, bool bSelected) { SetClassSelected(ClassName, bSelected); });
861 }
862 return SNew(SLightDefaultsRow, OwnerTable, Item,
863 [this](TSharedPtr<FLightAssetSummary> RowItem) { ApplyRowLive(RowItem); },
864 [this](const FString& ClassName, bool bSelected) { SetClassSelected(ClassName, bSelected); });
865}
866
867TSharedRef<ITableRow> SLightDefaultsPanel::OnGenerateVehicleRow(
868 TSharedPtr<FVehicleLightSummary> Item,
869 const TSharedRef<STableViewBase>& OwnerTable)
870{
871 return SNew(SVehicleLightRow, OwnerTable, Item,
872 [this](TSharedPtr<FVehicleLightSummary> RowItem) { ApplyVehicleRowLive(RowItem); },
873 [this](const FString& ClassName, bool bPreviewOn) { ToggleVehiclePreview(ClassName, bPreviewOn); });
874}
875
876namespace
877{
878 const FName LightDefaultsTabId(TEXT("CarlaLightDefaultsPanel"));
879
880 TSharedRef<SDockTab> SpawnLightDefaultsTab(const FSpawnTabArgs&)
881 {
882 return SNew(SDockTab)
883 .TabRole(ETabRole::NomadTab)
884 [
885 SNew(SLightDefaultsPanel)
886 ];
887 }
888}
889
890void RegisterLightDefaultsEditorTab()
891{
892 FGlobalTabmanager::Get()->RegisterNomadTabSpawner(LightDefaultsTabId, FOnSpawnTab::CreateStatic(&SpawnLightDefaultsTab))
893 .SetDisplayName(LOCTEXT("LightDefaultsTabTitle", "Light Defaults"))
894 .SetTooltipText(LOCTEXT("LightDefaultsTabTooltip", "Edit per-class light color/intensity/emissive defaults"))
895 .SetMenuType(ETabSpawnerMenuType::Enabled);
896}
897
898void UnregisterLightDefaultsEditorTab()
899{
900 if (FSlateApplication::IsInitialized())
901 {
902 FGlobalTabmanager::Get()->UnregisterNomadTabSpawner(LightDefaultsTabId);
903 }
904}
905
906namespace
907{
908 FDelegateHandle MapOpenedHandle;
909}
910
911void RegisterLightDefaultsMapOpenedHook()
912{
913 MapOpenedHandle = FEditorDelegates::OnMapOpened.AddLambda([](const FString&, bool)
914 {
915 UWorld* World = GEditor ? GEditor->GetEditorWorldContext().World() : nullptr;
916 if (World == nullptr)
917 {
918 return;
919 }
920 TWeakObjectPtr<UWorld> WeakWorld(World);
921 FTimerHandle Unused;
922 World->GetTimerManager().SetTimer(Unused, FTimerDelegate::CreateLambda([WeakWorld]()
923 {
924 if (WeakWorld.IsValid())
925 {
926 ULightDefaultsJsonUtils::ReapplyLightDefaultsToLevel(WeakWorld.Get());
927 ULightDefaultsJsonUtils::ReapplyTrafficLightDefaultsToLevel(WeakWorld.Get());
928 ULightDefaultsJsonUtils::ReapplyProceduralBuildingsToLevel(WeakWorld.Get());
929 ULightDefaultsJsonUtils::ReapplyLegacyBuildingsToLevel(WeakWorld.Get());
930 ULightDefaultsJsonUtils::ReapplyVehicleLightsToLevel(WeakWorld.Get());
931 }
932 }), 1.0f, false);
933 });
934}
935
936void UnregisterLightDefaultsMapOpenedHook()
937{
938 FEditorDelegates::OnMapOpened.Remove(MapOpenedHandle);
939}
940
941#undef LOCTEXT_NAMESPACE
942
943#endif // WITH_EDITOR
ELightType
Definition CarlaLight.h:30
static void ReapplyLegacyBuildingsToLevel(const UObject *WorldContextObject)
static void PreviewVehicleBlinkers(const UObject *WorldContextObject)
static AActor * SpawnVehiclePreview(const UObject *WorldContextObject, const FString &ClassName)
static void SetLegacyBuildingsSelected(const UObject *WorldContextObject, bool bSelected)
static void SetTrafficLightClassPreviewOn(const UObject *WorldContextObject, const FString &ClassName, bool bOn)
static bool SaveClassDefault(const FString &ClassName, const FLightAssetDefault &Default)
static void ApplyLightDefaultToClassLive(const UObject *WorldContextObject, const FString &ClassName, const FLightAssetDefault &Value)
static void ReapplyProceduralBuildingsToLevel(const UObject *WorldContextObject)
static void ApplyProceduralBuildingsEmissiveLive(const UObject *WorldContextObject, float EmissiveIntensity, float PercentLit, const FLinearColor &Color)
static void ApplyTrafficLightDefaultToClassLive(const UObject *WorldContextObject, const FString &ClassName, float EmissiveIntensity)
static void SetTrafficLightClassActorsSelected(const UObject *WorldContextObject, const FString &ClassName, bool bSelected)
static void ReapplyLightDefaultsToLevel(const UObject *WorldContextObject)
static void ReapplyTrafficLightDefaultsToLevel(const UObject *WorldContextObject)
static void ClearVehiclePreview(const UObject *WorldContextObject)
static void ApplyLegacyBuildingsEmissiveLive(const UObject *WorldContextObject, float EmissiveIntensity, float PercentLit, const FLinearColor &Color)
static void ReapplyVehicleLightsToLevel(const UObject *WorldContextObject)
static void SetProceduralBuildingsSelected(const UObject *WorldContextObject, bool bSelected)
static void ApplyVehicleLightsLive(const UObject *WorldContextObject, const FString &ClassName, float Intensity, const TMap< FString, float > &GroupIntensity)
static void SetClassActorsSelected(const UObject *WorldContextObject, const FString &ClassName, bool bSelected)
static bool SaveVehicleLightDefault(const FString &ClassName, float Intensity, const TMap< FString, float > &GroupIntensity)