// Copyright Epic Games, Inc. All Rights Reserved. #include "SZenWorkspaces.h" #include "SZenServiceStatus.h" #include "Experimental/ZenServerInterface.h" #include "Internationalization/FastDecimalFormat.h" #include "Math/BasicMathExpressionEvaluator.h" #include "Math/UnitConversion.h" #include "Misc/ExpressionParser.h" #include "Styling/StyleColors.h" #include "Widgets/Images/SImage.h" #include "Widgets/Input/SButton.h" #include "Widgets/Layout/SGridPanel.h" #include "Widgets/Layout/SSeparator.h" #include "Widgets/SBoxPanel.h" #include "Widgets/Text/STextBlock.h" #include "Widgets/Input/SEditableTextBox.h" #include "Framework/Application/SlateApplication.h" #include "DesktopPlatformModule.h" #include "Misc/MonitoredProcess.h" #define LOCTEXT_NAMESPACE "ZenDashboard" DEFINE_LOG_CATEGORY_STATIC(LogZenDashboardWorkpaces, Log, All); namespace { bool RunZenExe(FString Cmd) { FString ZenExePath = UE::Zen::GetLocalInstallUtilityPath(); FMonitoredProcess ZenExeProcess(ZenExePath, Cmd, true); bool bLaunched = ZenExeProcess.Launch(); if (!bLaunched) { UE_LOG(LogZenDashboardWorkpaces, Warning, TEXT("Failed to launch zen utility: '%s'."), *ZenExePath); return false; } const uint64 StartTime = FPlatformTime::Cycles64(); while (ZenExeProcess.Update()) { double Duration = FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - StartTime); if (Duration > 10.f) { ZenExeProcess.Cancel(true); UE_LOG(LogZenDashboardWorkpaces, Warning, TEXT("Cancelled launch of zen utility: '%s %s' due to timeout."), *ZenExePath, *Cmd); while (ZenExeProcess.Update()) { Duration = FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - StartTime); if (Duration > 15.f) { UE_LOG(LogZenDashboardWorkpaces, Warning, TEXT("Cancelled launch of zen utility: '%s'. Failed waiting for termination."), *ZenExePath); break; } FPlatformProcess::Sleep(0.2f); } FString OutputString = ZenExeProcess.GetFullOutputWithoutDelegate(); UE_LOG(LogZenDashboardWorkpaces, Warning, TEXT("Launch of zen utility: '%s' failed. Output: '%s'"), *ZenExePath, *OutputString); return false; } FPlatformProcess::Sleep(0.1f); } return true; } } class SZenNewWorkspace : public SCompoundWidget { SLATE_BEGIN_ARGS(SZenNewWorkspace) {} SLATE_EVENT(FOnClicked, OnCloseClicked); SLATE_END_ARGS() void Construct(const FArguments& InArgs) { OnCloseClicked = InArgs._OnCloseClicked; ChildSlot [ SNew(SVerticalBox) +SVerticalBox::Slot() .AutoHeight() .HAlign(HAlign_Left) [ SNew(SButton) .OnClicked(this, &SZenNewWorkspace::OnGoBack) [ SNew(SImage) .Image(FAppStyle::Get().GetBrush("Icons.Back")) ] ] + SVerticalBox::Slot() .Padding(0.f, 4.f) .AutoHeight() [ SNew(SSeparator) ] +SVerticalBox::Slot() .AutoHeight() [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .Padding(2.f) .VAlign(VAlign_Center) .AutoWidth() [ SNew(STextBlock) .Text(LOCTEXT("NewWorkspacePath", "Workspace Path:")) ] + SHorizontalBox::Slot() .FillWidth(1) .VAlign(VAlign_Center) .Padding(0, 0, 0, 3) [ SNew(SEditableTextBox) .Text(this, &SZenNewWorkspace::GetCurrentPath) ] + SHorizontalBox::Slot() .VAlign(VAlign_Center) .AutoWidth() [ SNew(SButton) .ContentPadding(FMargin(6.f, 2.f)) .OnClicked(this, &SZenNewWorkspace::OnBrowseClick) [ SNew(SImage) .Image(FAppStyle::Get().GetBrush("Icons.FolderOpen")) ] ] ] + SVerticalBox::Slot() .Padding(0.f, 4.f) .AutoHeight() [ SNew(SSeparator) ] + SVerticalBox::Slot() .Padding(0.f, 4.f) .AutoHeight() .HAlign(HAlign_Right) [ SNew(SButton) .ContentPadding(FMargin(6.f, 2.f)) .OnClicked(this, &SZenNewWorkspace::OnGoBack) [ SNew(SImage) .Image(FAppStyle::Get().GetBrush("Icons.Check")) .ColorAndOpacity(FStyleColors::AccentGreen) ] ] ]; } private: FText GetCurrentPath() const { return FText::GetEmpty(); } FReply OnGoBack() const { if (OnCloseClicked.IsBound()) { OnCloseClicked.Execute(); } return FReply::Handled(); } FReply OnBrowseClick() { IDesktopPlatform* DesktopPlatform = FDesktopPlatformModule::Get(); if (DesktopPlatform) { TSharedPtr ParentWindow = FSlateApplication::Get().FindWidgetWindow(AsShared()); void* ParentWindowHandle = (ParentWindow.IsValid() && ParentWindow->GetNativeWindow().IsValid()) ? ParentWindow->GetNativeWindow()->GetOSWindowHandle() : nullptr; FString FolderName; const bool bFolderSelected = DesktopPlatform->OpenDirectoryDialog( ParentWindowHandle, LOCTEXT("FolderDialogTitle", "Choose a directory").ToString(), "", FolderName ); if (bFolderSelected) { if (!FolderName.EndsWith(TEXT("/"))) { FolderName += TEXT("/"); } SelectedDir = FolderName; } } return FReply::Handled(); } private: FOnClicked OnCloseClicked; FString SelectedDir; }; class SZenWorkspaceShareRow : public SCompoundWidget { SLATE_BEGIN_ARGS(SZenWorkspaceShareRow) {} SLATE_ATTRIBUTE(UE::Zen::FZenWorkspaces::Workspace, WorkspaceInfo); SLATE_ATTRIBUTE(UE::Zen::FZenWorkspaces::Share, ShareInfo); SLATE_END_ARGS() void Construct(const FArguments& InArgs) { WorkspaceInfo = InArgs._WorkspaceInfo.Get(); ShareInfo = InArgs._ShareInfo.Get(); const float RowMargin = 10.f; const float ColumnMargin = 0.f; const FSlateFontInfo TitleFont = FCoreStyle::GetDefaultFontStyle("Bold", 10); this->ChildSlot [ SNew(SBorder) //.Padding(10.f, 10.f) .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .FillWidth(2) [ SNew(SGridPanel) + SGridPanel::Slot(0, 0) [ SNew(SBox) [ SNew(STextBlock) .Font(TitleFont) .Margin(FMargin(RowMargin, ColumnMargin)) .ColorAndOpacity(FStyleColors::AccentWhite) .Text(LOCTEXT("SharesId", "ShareId")) ] ] + SGridPanel::Slot(1, 0) [ SNew(STextBlock) .Margin(FMargin(RowMargin, ColumnMargin)) .Text_Lambda([this]() { return FText::FromString(ShareInfo.Id); }) ] + SGridPanel::Slot(0, 1) [ SNew(STextBlock) .Margin(FMargin(RowMargin, ColumnMargin)) .Font(TitleFont) .ColorAndOpacity(FStyleColors::AccentWhite) .Text(LOCTEXT("SharePath", "Path")) ] + SGridPanel::Slot(1, 1) [ SNew(STextBlock) .Margin(FMargin(RowMargin, ColumnMargin)) .Text_Lambda([this]() { return FText::FromString(ShareInfo.Dir); }) ] + SGridPanel::Slot(0, 2) [ SNew(STextBlock) .Margin(FMargin(RowMargin, ColumnMargin)) .Font(TitleFont) .ColorAndOpacity(FStyleColors::AccentWhite) .Text(LOCTEXT("ShareAlias", "Alias")) ] + SGridPanel::Slot(1, 2) [ SNew(STextBlock) .Margin(FMargin(RowMargin, ColumnMargin)) .Text_Lambda([this]() { return FText::FromString(ShareInfo.Alias); }) ] ] + SHorizontalBox::Slot() .AutoWidth() [ SNew(SBox) [ SNew(SButton) .VAlign(VAlign_Center) .HAlign(HAlign_Center) .ButtonStyle(&FAppStyle::Get().GetWidgetStyle("Button")) .OnClicked(this, &SZenWorkspaceShareRow::OnDeleteClicked) [ SNew(SImage) .Image(FAppStyle::Get().GetBrush("Icons.XCircle")) .ColorAndOpacity(FStyleColors::AccentRed) ] ] ] ] ]; } FReply OnDeleteClicked() { TStringBuilder<128> ShareRemoveCommand; ShareRemoveCommand << "workspace-share remove " << WorkspaceInfo.Id << " " << ShareInfo.Id; if (RunZenExe(ShareRemoveCommand.ToString())) { SetVisibility(EVisibility::Collapsed); } return FReply::Handled(); } private: UE::Zen::FZenWorkspaces::Workspace WorkspaceInfo; UE::Zen::FZenWorkspaces::Share ShareInfo; }; class SZenWorkspaceRow : public SCompoundWidget { SLATE_BEGIN_ARGS(SZenWorkspaceRow) {} SLATE_ATTRIBUTE(UE::Zen::FZenWorkspaces::Workspace, WorkspaceInfo); SLATE_ATTRIBUTE(int32, IndexNumber) SLATE_END_ARGS() void Construct(const FArguments& InArgs) { WorkspaceInfo = InArgs._WorkspaceInfo.Get(); IndexNumber = InArgs._IndexNumber.Get(); const float RowMargin = 0.f; const float ColumnMargin = 10.f; const FSlateFontInfo TitleFont = FCoreStyle::GetDefaultFontStyle("Bold", 10); this->ChildSlot [ SNew(SBorder) .BorderImage(FAppStyle::GetBrush("Brushes.Secondary")) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() .FillWidth(1.0) [ SNew(SVerticalBox) + SVerticalBox::Slot() [ SNew(SHorizontalBox) + SHorizontalBox::Slot() [ SNew(SGridPanel) // base path row + SGridPanel::Slot(0, 0) [ SNew(STextBlock) .Margin(FMargin(ColumnMargin, RowMargin)) .ColorAndOpacity(FStyleColors::AccentWhite) .Font(TitleFont) .Text(LOCTEXT("ZenWorkspaces_Path", "Root dir")) ] + SGridPanel::Slot(1, 0) [ SNew(STextBlock) .Margin(FMargin(ColumnMargin, RowMargin)) .Text_Lambda([this] { return FText::FromString(WorkspaceInfo.BaseDir); }) ] // id row + SGridPanel::Slot(0, 1) [ SNew(STextBlock) .Margin(FMargin(ColumnMargin, RowMargin)) .ColorAndOpacity(FStyleColors::AccentWhite) .Font(TitleFont) .Text(LOCTEXT("ZenWorkspaces_Id", "Id")) ] + SGridPanel::Slot(1, 1) [ SNew(STextBlock) .Margin(FMargin(ColumnMargin, RowMargin)) .Text_Lambda([this] { return FText::FromString(WorkspaceInfo.Id); }) ] + SGridPanel::Slot(0, 2) [ SNew(STextBlock) .Margin(FMargin(ColumnMargin, RowMargin)) .ColorAndOpacity(FStyleColors::AccentWhite) .Font(TitleFont) .Text(LOCTEXT("ZenWorkspaces_Dynamic", "Dynamic shares")) .ToolTipText(LOCTEXT("WorkspacesDynamic_Tooltip", "Does this workspace allow for creating shares using the /ws http endpoint")) ] + SGridPanel::Slot(1, 2) [ SNew(STextBlock) .Margin(FMargin(ColumnMargin, RowMargin)) .Text_Lambda([this] { FText DynamicStatus = LOCTEXT("WorkspaceStatus_Disabled", "Disabled"); if (WorkspaceInfo.bDynamicShare) { DynamicStatus = LOCTEXT("WorkspaceStatus_Enabled", "Enabled"); } return DynamicStatus; }) ] ] + SHorizontalBox::Slot() .AutoWidth() .Padding(0.f, 0.f, 10.f, 0.f) [ SNew(SBox) [ SNew(SButton) .VAlign(VAlign_Center) .HAlign(HAlign_Center) .OnClicked(this, &SZenWorkspaceRow::OnDeleteClicked) .ButtonStyle(&FAppStyle::Get().GetWidgetStyle("Button")) [ SNew(SImage) .Image(FAppStyle::Get().GetBrush("GenericCommands.Delete")) .ColorAndOpacity(FStyleColors::AccentRed) ] ] ] ] + SVerticalBox::Slot() .AutoHeight() .Padding(8.f, 8.f) [ ConstructShareList() ] ] ] ]; } FReply OnDeleteClicked() { TStringBuilder<128> WorkspaceRemoveCommand; WorkspaceRemoveCommand << "workspace remove " << WorkspaceInfo.Id; for (int Idx = 0; Idx < WorkspaceInfo.WorkspaceShares.Num(); ++Idx) { TStringBuilder<128> ShareRemoveCommand; ShareRemoveCommand << "workspace-share remove " << WorkspaceInfo.Id << " " << WorkspaceInfo.WorkspaceShares[Idx].Id; RunZenExe(ShareRemoveCommand.ToString()); } if (RunZenExe(WorkspaceRemoveCommand.ToString())) { SetVisibility(EVisibility::Collapsed); } return FReply::Handled(); } private: TSharedRef ConstructShareList() { const float RowMargin = 0.f; const float ColumnMargin = 10.f; const FSlateFontInfo TitleFont = FCoreStyle::GetDefaultFontStyle("Bold", 10); TSharedRef Widget = SNew(SVerticalBox); if (WorkspaceInfo.WorkspaceShares.IsEmpty()) { return Widget; } for (int32 Idx = 0; Idx < WorkspaceInfo.WorkspaceShares.Num(); ++Idx) { Widget->AddSlot() .AutoHeight() .Padding(0.f, 1.f) [ SNew(SZenWorkspaceShareRow) .WorkspaceInfo(WorkspaceInfo) .ShareInfo(WorkspaceInfo.WorkspaceShares[Idx]) ]; } TSharedRef Result = SNew(SBorder) .BorderImage(FAppStyle::GetBrush("Brushes.Header")) [ SNew(SVerticalBox) + SVerticalBox::Slot() .AutoHeight() .HAlign(HAlign_Center) [ SNew(STextBlock) .Font(TitleFont) .Margin(FMargin(RowMargin, ColumnMargin)) .ColorAndOpacity(FStyleColors::AccentWhite) .Text(LOCTEXT("Shares", "Shared folders:")) ] +SVerticalBox::Slot() [ Widget ] ]; return Result; } private: UE::Zen::FZenWorkspaces::Workspace WorkspaceInfo; int32 IndexNumber; bool bVisible = true; }; void SZenWorkspaces::Construct(const FArguments& InArgs) { ZenServiceInstance = InArgs._ZenServiceInstance; UpdateFrequency = InArgs._UpdateFrequency.Get(); const FSlateColor TitleColor = FStyleColors::AccentWhite; this->ChildSlot [ SNew(SVerticalBox) + SVerticalBox::Slot() .AutoHeight() .Padding(0.f, 0.f) .HAlign(HAlign_Fill) [ SNew(SButton) .ButtonStyle(&FAppStyle::Get().GetWidgetStyle("SimpleButton")) .ToolTipText(LOCTEXT("ZenWorkspacesExpand", "Workspaces")) .OnClicked(this, &SZenWorkspaces::ZenWorkspacesArea_Toggle) [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) [ SNew(SImage) .Image(this, &SZenWorkspaces::ZenWorkspacesArea_Icon) .ColorAndOpacity(FStyleColors::White) ] +SHorizontalBox::Slot() .VAlign(VAlign_Center) .Padding(4.0f, 0.f) [ SNew(STextBlock) .ColorAndOpacity(FStyleColors::White) .Text(LOCTEXT("ZenWorkspaces", "Workspaces")) ] ] ] + SVerticalBox::Slot() .MaxHeight(400) .Padding(0.f, 8.f) [ SAssignNew(WidgetSwitcher, SWidgetSwitcher) .Visibility(EVisibility::Collapsed) + SWidgetSwitcher::Slot() [ SAssignNew(WorkspaceArea, SVerticalBox) + SVerticalBox::Slot() .Expose(GridSlot) [ GetWorkspaceList() ] ] + SWidgetSwitcher::Slot() [ SNew(SZenNewWorkspace) .OnCloseClicked(this, &SZenWorkspaces::OnBackToMainWidget) ] ] ]; UpdateWorkspaces(0, true); } TSharedRef SZenWorkspaces::GetWorkspaceList() { TSharedPtr WorkspaceItems = SNew(SScrollBox); const float RowMargin = 0.f; const float ColumnMargin = 10.f; const FSlateFontInfo TitleFont = FCoreStyle::GetDefaultFontStyle("Bold", 10); int32 Idx = 1; for (UE::Zen::FZenWorkspaces::Workspace& Workspace : Workspaces.ZenWorkspaces) { WorkspaceItems->AddSlot() .Padding(16.f, 4.f) .VAlign(VAlign_Center) [ SNew(SZenWorkspaceRow) .WorkspaceInfo(Workspace) .IndexNumber(Idx++) ]; } return WorkspaceItems.ToSharedRef(); } FReply SZenWorkspaces::ZenWorkspacesArea_Toggle() const { if (WidgetSwitcher) { if (WidgetSwitcher->GetVisibility() == EVisibility::Visible) { WidgetSwitcher->SetVisibility(EVisibility::Collapsed); } else { WidgetSwitcher->SetVisibility(EVisibility::Visible); } } return FReply::Handled(); } FReply SZenWorkspaces::OnBackToMainWidget() { WidgetSwitcher->SetActiveWidgetIndex(0); return FReply::Handled(); } FReply SZenWorkspaces::OnCreateNewWorkspaceClicked() { WidgetSwitcher->SetActiveWidgetIndex(1); return FReply::Handled(); } const FSlateBrush* SZenWorkspaces::ZenWorkspacesArea_Icon() const { return (WidgetSwitcher && WidgetSwitcher->GetVisibility() == EVisibility::Visible) ? FAppStyle::Get().GetBrush("Icons.ChevronDown") : FAppStyle::Get().GetBrush("Icons.ChevronRight"); } void SZenWorkspaces::UpdateWorkspaces(float InDeltaTime, bool bForce) { static double AccumulatedDeltaTime = 0.f; AccumulatedDeltaTime += InDeltaTime; if (AccumulatedDeltaTime >= UpdateFrequency || bForce) { AccumulatedDeltaTime = 0.f; UE::Zen::FZenWorkspaces RecentWorkspaces; if (TSharedPtr ServiceInstance = ZenServiceInstance.Get()) { ServiceInstance->GetWorkspaces(RecentWorkspaces); } if (RecentWorkspaces == Workspaces) { return; } Workspaces = RecentWorkspaces; if (GridSlot) { (*GridSlot) [ GetWorkspaceList() ]; } SlatePrepass(GetPrepassLayoutScaleMultiplier()); } } #undef LOCTEXT_NAMESPACE