// Fill out your copyright notice in the Description page of Project Settings. #include "Widget/DTFluxAssetModelDetailsWidget.h" #include "DTFluxAssetsEditorModule.h" #include "Widgets/Layout/SBorder.h" #include "Widgets/Layout/SBox.h" #include "Widgets/Text/STextBlock.h" #include "Widgets/Input/SButton.h" #include "Widgets/Views/STableRow.h" #include "Widgets/Views/SHeaderRow.h" void SHierarchicalTreeItemRow::Construct(const FArguments& InArgs, const TSharedRef& InOwnerTableView) { Item = InArgs._Item; ParentWidget = InArgs._ParentWidget; SMultiColumnTableRow>::Construct( SMultiColumnTableRow>::FArguments(), InOwnerTableView ); } TSharedRef SHierarchicalTreeItemRow::GenerateWidgetForColumn(const FName& ColumnName) { if (!Item.IsValid()) { // UE_LOG(logDTFluxAssetEditor, Warning, TEXT("GenerateWidgetForColumn: Invalid item for column %s"), *ColumnName.ToString()); return SNew(STextBlock).Text(FText::FromString("Invalid Item")); } if (!ParentWidget) { // UE_LOG(logDTFluxAssetEditor, Warning, TEXT("GenerateWidgetForColumn: Invalid ParentWidget for column %s"), // *ColumnName.ToString()); return SNew(STextBlock).Text(FText::FromString("Invalid Parent")); } // UE_LOG(logDTFluxAssetEditor, VeryVerbose, TEXT("GenerateWidgetForColumn: %s for item %s"), // *ColumnName.ToString(), *Item->Name); if (ColumnName == "Name") { return SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) .Padding(0, 0, 5, 0) [ SNew(SImage) .Image(ParentWidget->GetItemIcon(Item->Type)) .ColorAndOpacity(ParentWidget->GetItemTypeColor(Item->Type)) ] + SHorizontalBox::Slot() .FillWidth(1.0f) .VAlign(VAlign_Center) [ SNew(STextBlock) .Text(FText::FromString(Item->Name)) .ColorAndOpacity(ParentWidget->GetItemTypeColor(Item->Type)) .Font_Lambda([this]() -> FSlateFontInfo { return Item->Type == FHierarchicalTreeItem::EItemType::Contest ? FAppStyle::GetFontStyle("HeadingText") : FAppStyle::GetFontStyle("NormalText"); }) ]; } else if (ColumnName == "ID") { return SNew(STextBlock) .Text(FText::FromString(Item->ID)) .Font(FAppStyle::GetFontStyle("NormalText")) .Justification(ETextJustify::Center); } else if (ColumnName == "Details") { return SNew(STextBlock) .Text(FText::FromString(Item->Details)) .Font(FAppStyle::GetFontStyle("NormalText")) .OverflowPolicy(ETextOverflowPolicy::Ellipsis); } else if (ColumnName == "Status") { return SNew(STextBlock) .Text(FText::FromString(Item->Status)) .Font(FAppStyle::GetFontStyle("NormalText")) .OverflowPolicy(ETextOverflowPolicy::Ellipsis); } else if (ColumnName == "Extra") { return SNew(STextBlock) .Text(FText::FromString(Item->Extra)) .Font(FAppStyle::GetFontStyle("NormalText")) .OverflowPolicy(ETextOverflowPolicy::Ellipsis); } UE_LOG(logDTFluxAssetEditor, Warning, TEXT("GenerateWidgetForColumn: Unknown column %s"), *ColumnName.ToString()); return SNew(STextBlock).Text(FText::FromString(FString::Printf(TEXT("Unknown: %s"), *ColumnName.ToString()))); } void SDTFluxAssetModelDetailsWidget::Construct(const FArguments& InArgs) { ModelAsset = InArgs._ModelAsset; ChildSlot [ SNew(SBorder) .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) [ SNew(SVerticalBox) // === SECTION STATISTIQUES === + SVerticalBox::Slot() .AutoHeight() .Padding(5) [ SNew(SBorder) .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) .Padding(10) [ SAssignNew(StatsText, STextBlock) .Text(this, &SDTFluxAssetModelDetailsWidget::GetStatsText) .Font(FAppStyle::GetFontStyle("HeadingText")) .Justification(ETextJustify::Center) ] ] // === SECTION BOUTONS DE NAVIGATION === + SVerticalBox::Slot() .AutoHeight() .Padding(5) [ SNew(SBorder) .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() .Padding(0, 0, 5, 0) [ SNew(SButton) .ButtonStyle(FAppStyle::Get(), "SimpleButton") .Text(FText::FromString("Expand All Contests")) .OnClicked(this, &SDTFluxAssetModelDetailsWidget::OnExpandAllClicked) ] + SHorizontalBox::Slot() .AutoWidth() .Padding(0, 0, 10, 0) [ SNew(SButton) .ButtonStyle(FAppStyle::Get(), "SimpleButton") .Text(FText::FromString("Collapse All Contests")) .OnClicked(this, &SDTFluxAssetModelDetailsWidget::OnCollapseAllClicked) ] + SHorizontalBox::Slot() .FillWidth(1.0f) [ SNew(SSpacer) ] + SHorizontalBox::Slot() .AutoWidth() [ SNew(SButton) .ButtonStyle(FAppStyle::Get(), "PrimaryButton") .Text(FText::FromString("Refresh")) .OnClicked(this, &SDTFluxAssetModelDetailsWidget::OnRefreshClicked) ] ] ] #pragma region ListView + SVerticalBox::Slot() .FillHeight(1.0f) .Padding(5) [ SNew(SBorder) .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) [ SNew(SBorder) .BorderImage(FAppStyle::GetBrush("DetailsView.CategoryTop")) .Padding(0) [ SNew(SBox) [ #pragma region ScrollBox SNew(SScrollBox) .Orientation(Orient_Vertical) .ScrollBarVisibility(EVisibility::Visible) .ConsumeMouseWheel(EConsumeMouseWheel::WhenScrollingPossible) .ScrollBarAlwaysVisible(true) // Force la scrollbar à être toujours visible #pragma region ListView.Contest + SScrollBox::Slot() .Padding(0, 0, 0, 10) [ SNew(SBorder) .BorderImage(FAppStyle::GetBrush("DetailsView.CategoryTop")) [ SNew(SVerticalBox) + SVerticalBox::Slot() .AutoHeight() .Padding(5) [ SNew(SBorder) .BorderImage(FAppStyle::GetBrush("DetailsView.CategoryTop")) .Padding(10, 5) [ SNew(STextBlock) .Text(FText::FromString("CONTESTS HIERARCHY")) .Font(FAppStyle::GetFontStyle("HeadingText")) .ColorAndOpacity(FLinearColor(0.2f, 0.6f, 1.0f)) ] ] // TreeView Contests + SVerticalBox::Slot() .AutoHeight() [ SNew(SBox) [ SAssignNew(ContestTreeView, STreeView) .TreeItemsSource(&RootItems) .OnGenerateRow(this, &SDTFluxAssetModelDetailsWidget::OnGenerateRowForTree) .OnGetChildren(this, &SDTFluxAssetModelDetailsWidget::OnGetChildrenForTree) .OnSelectionChanged( this, &SDTFluxAssetModelDetailsWidget::OnTreeSelectionChanged) .OnSetExpansionRecursive( this, &SDTFluxAssetModelDetailsWidget::OnSetExpansionRecursive) .SelectionMode(ESelectionMode::Single) .ConsumeMouseWheel(EConsumeMouseWheel::WhenScrollingPossible) .HeaderRow ( SNew(SHeaderRow) .CanSelectGeneratedColumn(true) + SHeaderRow::Column("Name") .DefaultLabel(FText::FromString("Contest / Stage / Split")) .SortMode(EColumnSortMode::None) + SHeaderRow::Column("ID") .DefaultLabel(FText::FromString("ID")) .SortMode(EColumnSortMode::None) .FillWidth(.02f) + SHeaderRow::Column("Details") .DefaultLabel(FText::FromString("Details")) .SortMode(EColumnSortMode::None) .FillWidth(.3f) + SHeaderRow::Column("Status") .DefaultLabel(FText::FromString("Status / Time")) .SortMode(EColumnSortMode::None) .FillWidth(.1f) + SHeaderRow::Column("Extra") .DefaultLabel(FText::FromString("Extra Info")) .FillWidth(.2f) .SortMode(EColumnSortMode::None) ) ] ] ] ] #pragma endregion #pragma region ListView.Participant + SScrollBox::Slot() .Padding(0, 10, 0, 0) [ SNew(SBorder) .BorderImage(FAppStyle::GetBrush("DetailsView.CategoryTop")) [ SNew(SVerticalBox) // Header "Participants" + SVerticalBox::Slot() .AutoHeight() .Padding(5) [ SNew(SBorder) .BorderImage(FAppStyle::GetBrush("DetailsView.CategoryTop")) .Padding(10, 5) [ SNew(STextBlock) .Text(FText::FromString("PARTICIPANTS LIST")) .Font(FAppStyle::GetFontStyle("HeadingText")) .ColorAndOpacity(FLinearColor(0.2f, 0.8f, 0.8f)) ] ] // TreeView Participants + SVerticalBox::Slot() .AutoHeight() [ SNew(SBox) [ SAssignNew(ParticipantTreeView, STreeView) .TreeItemsSource(&ParticipantItems) .OnGenerateRow(this, &SDTFluxAssetModelDetailsWidget::OnGenerateRowForTree) .OnGetChildren(this, &SDTFluxAssetModelDetailsWidget::OnGetChildrenForTree) .OnSelectionChanged( this, &SDTFluxAssetModelDetailsWidget::OnTreeSelectionChanged) .OnSetExpansionRecursive( this, &SDTFluxAssetModelDetailsWidget::OnSetExpansionRecursive) .SelectionMode(ESelectionMode::Single) .ConsumeMouseWheel(EConsumeMouseWheel::WhenScrollingPossible) .HeaderRow ( SNew(SHeaderRow) .CanSelectGeneratedColumn(true) + SHeaderRow::Column("Name") .DefaultLabel(FText::FromString("Participant")) .SortMode(EColumnSortMode::None) + SHeaderRow::Column("ID") .DefaultLabel(FText::FromString("Bib")) .SortMode(EColumnSortMode::None) .FillWidth(.08f) + SHeaderRow::Column("Details") .DefaultLabel(FText::FromString("Category & Teammates")) .SortMode(EColumnSortMode::None) .FillWidth(.2f) + SHeaderRow::Column("Status") .DefaultLabel(FText::FromString("Status")) .FillWidth(.1f) .SortMode(EColumnSortMode::None) + SHeaderRow::Column("Extra") .DefaultLabel(FText::FromString("Club")) .FillWidth(.2f) .SortMode(EColumnSortMode::None) ) ] ] ] ] #pragma endregion ] #pragma endregion ] #pragma endregion ] ] #pragma region DetailView.Participant + SVerticalBox::Slot() .AutoHeight() .Padding(5) [ SNew(SBorder) .BorderImage(FAppStyle::GetBrush("DetailsView.CategoryTop")) .Padding(10) [ SAssignNew(SelectionText, STextBlock) .Text(FText::FromString("Select an item to see details. Use expand/collapse arrows in the tree.")) .Font(FAppStyle::GetFontStyle("NormalText")) .AutoWrapText(true) ] ] #pragma endregion ] ]; RefreshData(); RegisterActiveTimer( 0.1f, FWidgetActiveTimerDelegate::CreateSP(this, &SDTFluxAssetModelDetailsWidget::ForceInitialLayout)); } // ===== CONSTRUCTION DE LA HIÉRARCHIE ===== void SDTFluxAssetModelDetailsWidget::BuildContestHierarchy() { RootItems.Empty(); if (!ModelAsset) return; // Construire la hiérarchie Contest → Stages → Splits for (const auto& ContestPair : ModelAsset->Contests) { const FString& ContestName = ContestPair.Key; const FDTFluxContest& Contest = ContestPair.Value; // Créer l'élément Contest racine auto ContestItem = FHierarchicalTreeItem::CreateContest(ContestName, Contest); // Ajouter les Stages comme enfants for (const FDTFluxStage& Stage : Contest.Stages) { auto StageItem = FHierarchicalTreeItem::CreateStage(Stage, Contest.ContestId); ContestItem->AddChild(StageItem); } // Ajouter les Splits comme enfants directs du Contest for (const FDTFluxSplit& Split : Contest.Splits) { auto SplitItem = FHierarchicalTreeItem::CreateSplit(Split, Contest.ContestId); ContestItem->AddChild(SplitItem); } RootItems.Add(ContestItem); } // UE_LOG(logDTFluxAssetEditor, Log, TEXT("Built contest hierarchy with %d root contests"), RootItems.Num()); } void SDTFluxAssetModelDetailsWidget::BuildParticipantList() { ParticipantItems.Empty(); if (!ModelAsset) { UE_LOG(logDTFluxAssetEditor, Warning, TEXT("BuildParticipantList: ModelAsset is null!")); // return; } // UE_LOG(logDTFluxAssetEditor, Log, TEXT("BuildParticipantList: ModelAsset has %d participants"), ModelAsset->Participants.Num()); // Créer la liste des participants (pas de hiérarchie pour les participants) for (const auto& ParticipantPair : ModelAsset->Participants) { const FDTFluxParticipant& Participant = ParticipantPair.Value; auto ParticipantItem = FHierarchicalTreeItem::CreateParticipant(Participant); ParticipantItems.Add(ParticipantItem); // UE_LOG(logDTFluxAssetEditor, Log, TEXT("BuildParticipantList: Added participant %s (Bib: %d)"), // *ParticipantItem->Name, ParticipantItem->Bib); } // UE_LOG(logDTFluxAssetEditor, Log, TEXT("BuildParticipantList: Built participant list with %d participants"), // ParticipantItems.Num()); } // ===== CALLBACKS TREEVIEW ===== TSharedRef SDTFluxAssetModelDetailsWidget::OnGenerateRowForTree( FHierarchicalTreeItemPtr Item, const TSharedRef& OwnerTable) { if (!Item.IsValid()) { UE_LOG(logDTFluxAssetEditor, Warning, TEXT("OnGenerateRowForTree: Invalid item!")); return SNew(STableRow, OwnerTable); } // UE_LOG(logDTFluxAssetEditor, Log, TEXT("OnGenerateRowForTree: Generating row for %s (Type: %d)"), // *Item->Name, (int32)Item->Type); return SNew(SHierarchicalTreeItemRow, OwnerTable) .Item(Item) .ParentWidget(this); } void SDTFluxAssetModelDetailsWidget::OnGetChildrenForTree(FHierarchicalTreeItemPtr Item, TArray& OutChildren) { if (Item.IsValid()) { OutChildren = Item->Children; } } void SDTFluxAssetModelDetailsWidget::OnTreeSelectionChanged(FHierarchicalTreeItemPtr SelectedItem, ESelectInfo::Type SelectInfo) { if (SelectionText.IsValid()) { if (SelectedItem.IsValid()) { FString TypeString; switch (SelectedItem->Type) { case FHierarchicalTreeItem::EItemType::Contest: TypeString = "Contest"; break; case FHierarchicalTreeItem::EItemType::Stage: TypeString = "Stage"; break; case FHierarchicalTreeItem::EItemType::Split: TypeString = "Split"; break; case FHierarchicalTreeItem::EItemType::Participant: TypeString = "Participant"; break; } FString SelectionInfo = FString::Printf( TEXT("📋 Selected: %s (%s)\n🔢 ID: %s\n📄 Details: %s\n📊 Status: %s\n➕ Extra: %s\n🌟 Children: %d"), *SelectedItem->Name, *TypeString, *SelectedItem->ID, *SelectedItem->Details, *SelectedItem->Status, *SelectedItem->Extra, SelectedItem->Children.Num()); SelectionText->SetText(FText::FromString(SelectionInfo)); } else { SelectionText->SetText(FText::FromString("Select an item to see details")); } } } void SDTFluxAssetModelDetailsWidget::OnSetExpansionRecursive(FHierarchicalTreeItemPtr Item, bool bIsExpanded) { if (Item.IsValid() && ContestTreeView.IsValid()) { ContestTreeView->SetItemExpansion(Item, bIsExpanded); // Expansion récursive des enfants for (auto Child : Item->Children) { OnSetExpansionRecursive(Child, bIsExpanded); } } } // ===== CALLBACKS DES BOUTONS ===== FReply SDTFluxAssetModelDetailsWidget::OnExpandAllClicked() { if (ContestTreeView.IsValid()) { for (auto& RootItem : RootItems) { OnSetExpansionRecursive(RootItem, true); } } // UE_LOG(logDTFluxAssetEditor, Log, TEXT("Expanded all contests")); return FReply::Handled(); } FReply SDTFluxAssetModelDetailsWidget::OnCollapseAllClicked() { if (ContestTreeView.IsValid()) { for (auto& RootItem : RootItems) { OnSetExpansionRecursive(RootItem, false); } } // UE_LOG(logDTFluxAssetEditor, Log, TEXT("Collapsed all contests")); return FReply::Handled(); } FReply SDTFluxAssetModelDetailsWidget::OnRefreshClicked() { RefreshData(); // UE_LOG(logDTFluxAssetEditor, Log, TEXT("Data refreshed")); return FReply::Handled(); } // ===== REFRESHDATA ===== void SDTFluxAssetModelDetailsWidget::RefreshData() { if (!ModelAsset) { UE_LOG(logDTFluxAssetEditor, Warning, TEXT("ModelAsset is null!")); return; } // UE_LOG(logDTFluxAssetEditor, Log, TEXT("RefreshData: Starting refresh for ModelAsset %s"), *ModelAsset->GetName()); RootItems.Empty(); ParticipantItems.Empty(); BuildContestHierarchy(); BuildParticipantList(); if (ContestTreeView.IsValid()) { ContestTreeView->RequestTreeRefresh(); } if (ParticipantTreeView.IsValid()) { ParticipantTreeView->RequestTreeRefresh(); } // UE_LOG(logDTFluxAssetEditor, Log, TEXT("RefreshData: Completed successfully - %d contests, %d participants"), RootItems.Num(), // ParticipantItems.Num()); } // ===== MÉTHODES UTILITAIRES ===== FSlateColor SDTFluxAssetModelDetailsWidget::GetItemTypeColor(FHierarchicalTreeItem::EItemType Type) const { switch (Type) { case FHierarchicalTreeItem::EItemType::Contest: return FSlateColor(FLinearColor(0.2f, 0.6f, 1.0f)); case FHierarchicalTreeItem::EItemType::Stage: return FSlateColor(FLinearColor(0.2f, 0.8f, 0.2f)); case FHierarchicalTreeItem::EItemType::Split: return FSlateColor(FLinearColor(1.0f, 0.8f, 0.2f)); case FHierarchicalTreeItem::EItemType::Participant: return FSlateColor(FLinearColor(0.2f, 0.8f, 0.8f)); default: return FSlateColor(FLinearColor::White); } } const FSlateBrush* SDTFluxAssetModelDetailsWidget::GetItemIcon(FHierarchicalTreeItem::EItemType Type) const { switch (Type) { case FHierarchicalTreeItem::EItemType::Contest: return FAppStyle::GetBrush("TreeArrow_Collapsed"); case FHierarchicalTreeItem::EItemType::Stage: case FHierarchicalTreeItem::EItemType::Split: return FAppStyle::GetBrush("TreeArrow_Expanded"); case FHierarchicalTreeItem::EItemType::Participant: return FAppStyle::GetBrush("Icons.User"); default: return FAppStyle::GetBrush("Icons.Help"); } } FText SDTFluxAssetModelDetailsWidget::GetStatsText() const { if (!ModelAsset) return FText::FromString("No data"); return FText::FromString(FString::Printf( TEXT("Contests: [%d] Participants: [%d] Persons: [%d]"), ModelAsset->Contests.Num(), ModelAsset->Participants.Num(), ModelAsset->Persons.Num() )); }