// Copyright Epic Games, Inc. All Rights Reserved. #include "SSequencerGroupManager.h" #include "MVVM/ViewModels/SequencerEditorViewModel.h" #include "MVVM/Extensions/IOutlinerExtension.h" #include "MVVM/Extensions/IGroupableExtension.h" #include "MVVM/Selection/Selection.h" #include "Sequencer.h" #include "MovieSceneSequence.h" #include "MovieScene.h" #include "SequencerOutlinerItemDragDropOp.h" #include "SequencerUtilities.h" #include "SlateOptMacros.h" #include "Widgets/SNullWidget.h" #include "Widgets/SBoxPanel.h" #include "Widgets/Layout/SBox.h" #include "Widgets/Layout/SScrollBorder.h" #include "Widgets/Images/SImage.h" #include "Widgets/SOverlay.h" #include "Widgets/Text/STextBlock.h" #include "Widgets/Input/SEditableTextBox.h" #include "Widgets/Input/SButton.h" #include "Widgets/Input/SComboButton.h" #include "Widgets/Text/SInlineEditableTextBlock.h" #include "EditorFontGlyphs.h" #include "ScopedTransaction.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #define LOCTEXT_NAMESPACE "SSequencerGroupManager" class SSequencerGroupNodeRow; struct FSequencerNodeGroupTreeNode { enum Type { /* Base Node Type */ BaseNode, /* Group */ GroupNode, /* Item Node*/ ItemNode }; FSequencerNodeGroupTreeNode(const FText& InDisplayText) : DisplayText(InDisplayText) { } virtual ~FSequencerNodeGroupTreeNode() {} virtual Type GetType() const { return BaseNode; } FText DisplayText; TArray> Children; }; struct FSequencerGroupItemNode : public FSequencerNodeGroupTreeNode { FSequencerGroupItemNode(const FText& InDisplayText, const FString& InPath, TSharedPtr InGroup) : FSequencerNodeGroupTreeNode(InDisplayText), Path(InPath), Group(InGroup) { } virtual ~FSequencerGroupItemNode() override {} virtual Type GetType() const override { return ItemNode; } FString Path; TSharedPtr Group; }; struct FSequencerNodeGroupNode : public FSequencerNodeGroupTreeNode { FSequencerNodeGroupNode(const FText& InDisplayText, UMovieSceneNodeGroup* InGroup, TWeakPtr InGroupManager) : FSequencerNodeGroupTreeNode(InDisplayText), WeakGroupManager(InGroupManager), Group(InGroup) { } virtual ~FSequencerNodeGroupNode() override {} virtual Type GetType() const override { return GroupNode; } FReply OnEnableFilterClicked() { if (IsValid(Group)) { Group->SetEnableFilter(!Group->GetEnableFilter()); } return FReply::Handled(); } bool IsFilterEnabled() const { return Group->GetEnableFilter(); } bool VerifyNodeTextChanged(const FText& NewLabel, FText& OutErrorMessage) { return !NewLabel.IsEmptyOrWhitespace(); } void HandleNodeLabelTextCommitted(const FText& NewLabel, ETextCommit::Type CommitType) { TSharedPtr GroupManager = WeakGroupManager.Pin(); UMovieScene* MovieScene = GroupManager ? GroupManager->GetMovieScene() : nullptr; if (MovieScene) { const FScopedTransaction Transaction(LOCTEXT("RenameGroupTransaction", "Rename Group")); Group->SetName(FName(*FText::TrimPrecedingAndTrailing(NewLabel).ToString())); } } void OnRenameRequested() { if (InlineEditableTextBlock) { InlineEditableTextBlock->EnterEditingMode(); } } TWeakPtr WeakGroupManager; UMovieSceneNodeGroup* Group; TSharedPtr InlineEditableTextBlock; }; class SSequencerGroupNodeRow : public STableRow> { SLATE_BEGIN_ARGS(SSequencerGroupNodeRow) {} SLATE_END_ARGS() public: void Construct(const FArguments& InArgs, const TSharedRef& InOwnerTableView, TWeakPtr InWeakSequencerGroupTreeNode, TWeakPtr InWeakSequencerGroupManager) { WeakSequencerGroupManager = InWeakSequencerGroupManager; WeakSequencerGroupTreeNode = InWeakSequencerGroupTreeNode; STableRow>::ConstructInternal(STableRow::FArguments() .Padding(5.f) .OnCanAcceptDrop(this, &SSequencerGroupNodeRow::OnCanAcceptDrop) .OnAcceptDrop(this, &SSequencerGroupNodeRow::OnAcceptDrop) , InOwnerTableView); TSharedPtr SequencerGroupTreeNode = WeakSequencerGroupTreeNode.Pin(); if (!SequencerGroupTreeNode) { return; } TSharedPtr SequencerGroupManager = WeakSequencerGroupManager.Pin(); const FSlateBrush* IconBrush = SequencerGroupManager ? SequencerGroupManager->GetIconBrush(SequencerGroupTreeNode) : nullptr; if(SequencerGroupTreeNode) { if(SequencerGroupTreeNode->GetType() == FSequencerNodeGroupTreeNode::Type::ItemNode) { TSharedPtr SequencerGroupItemNode = StaticCastSharedPtr(SequencerGroupTreeNode); this->ChildSlot [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .VAlign(VAlign_Center) .AutoWidth() [ SNew(SExpanderArrow, SharedThis(this)) ] + SHorizontalBox::Slot() .VAlign(VAlign_Center) .Padding(FMargin(5.f, 0.f, 5.f, 0.f)) .AutoWidth() [ SNew(SOverlay) + SOverlay::Slot() [ SNew(SImage) .Image(IconBrush ? IconBrush : FCoreStyle::Get().GetDefaultBrush()) .ColorAndOpacity(IconBrush ? FLinearColor::White : FLinearColor::Transparent) ] ] + SHorizontalBox::Slot() .VAlign(VAlign_Center) [ SNew(STextBlock) .Text(SequencerGroupTreeNode->DisplayText) .ToolTipText(FText::FromString(SequencerGroupItemNode->Path)) ] ]; } else if (SequencerGroupTreeNode->GetType() == FSequencerNodeGroupTreeNode::Type::GroupNode) { TSharedPtr NodeGroupNode = StaticCastSharedPtr(SequencerGroupTreeNode); this->ChildSlot [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .VAlign(VAlign_Center) .AutoWidth() [ SNew(SExpanderArrow, SharedThis(this)) ] + SHorizontalBox::Slot() .VAlign(VAlign_Center) .Padding(FMargin(5.f, 0.f, 5.f, 0.f)) .AutoWidth() [ SNew(SOverlay) + SOverlay::Slot() [ SNew(SButton) .OnClicked(FOnClicked::CreateSP(NodeGroupNode.ToSharedRef(), &FSequencerNodeGroupNode::OnEnableFilterClicked)) .ButtonStyle( FAppStyle::Get(), "NoBorder" ) .Content() [ SNew(STextBlock) .TextStyle(FAppStyle::Get(), "GenericFilters.TextStyle") .Font(FAppStyle::Get().GetFontStyle("FontAwesome.11")) .Text(FEditorFontGlyphs::Filter) .ColorAndOpacity(NodeGroupNode->Group->GetEnableFilter() ? FLinearColor::White : FLinearColor(0.66f, 0.66f, 0.66f, 0.66f)) ] ] ] + SHorizontalBox::Slot() .VAlign(VAlign_Center) [ SAssignNew(NodeGroupNode->InlineEditableTextBlock, SInlineEditableTextBlock) .OnVerifyTextChanged(NodeGroupNode.ToSharedRef(), &FSequencerNodeGroupNode::VerifyNodeTextChanged) .OnTextCommitted(NodeGroupNode.ToSharedRef(), &FSequencerNodeGroupNode::HandleNodeLabelTextCommitted) .Text(NodeGroupNode->DisplayText) .Clipping(EWidgetClipping::ClipToBounds) ] ]; } } } private: TOptional OnCanAcceptDrop(const FDragDropEvent& DragDropEvent, EItemDropZone InItemDropZone, TSharedPtr SequencerGroupTreeNode) { using namespace UE::Sequencer; TSharedPtr DragDropOp = DragDropEvent.GetOperationAs(); if (DragDropOp.IsValid()) { DragDropOp->ResetToDefaultToolTip(); TOptional AllowedDropZone; if (SequencerGroupTreeNode->GetType() == FSequencerNodeGroupTreeNode::Type::GroupNode) { AllowedDropZone = EItemDropZone::OntoItem; DragDropOp->CurrentHoverText = FText::Format(LOCTEXT("DragDropAddItemsFormat", "Add {0} item(s)"), FText::AsNumber(DragDropOp->GetDraggedViewModels().Num())); } if (AllowedDropZone.IsSet() == false) { DragDropOp->CurrentIconBrush = FAppStyle::GetBrush(TEXT("Graph.ConnectorFeedback.Error")); } return AllowedDropZone; } return TOptional(); } FReply OnAcceptDrop(const FDragDropEvent& DragDropEvent, EItemDropZone InItemDropZone, TSharedPtr SequencerGroupTreeNode) { using namespace UE::Sequencer; TSharedPtr DragDropOp = DragDropEvent.GetOperationAs(); if (DragDropOp.IsValid()) { TSharedPtr SequencerGroupManager = WeakSequencerGroupManager.Pin(); TSharedPtr Sequencer = SequencerGroupManager ? SequencerGroupManager->GetSequencer() : nullptr; if (Sequencer.IsValid()) { if (SequencerGroupTreeNode->GetType() == FSequencerNodeGroupTreeNode::Type::GroupNode) { TSharedPtr NodeGroupNode = StaticCastSharedPtr(SequencerGroupTreeNode); Sequencer->AddNodesToExistingNodeGroup(DragDropOp->GetDraggedViewModels(), NodeGroupNode->Group); return FReply::Handled(); } } } return FReply::Unhandled(); } TWeakPtr WeakSequencerGroupManager; TWeakPtr WeakSequencerGroupTreeNode; }; UMovieScene* SSequencerGroupManager::GetMovieScene() const { TSharedPtr Sequencer = WeakSequencer.Pin(); UMovieSceneSequence* Sequence = Sequencer ? Sequencer->GetFocusedMovieSceneSequence() : nullptr; UMovieScene* MovieScene = Sequence ? Sequence->GetMovieScene() : nullptr; return MovieScene; } void SSequencerGroupManager::Construct(const FArguments& InArgs, TWeakPtr InWeakSequencer) { WeakSequencer = InWeakSequencer; UMovieScene* MovieScene = GetMovieScene(); if (!ensure(MovieScene)) { return; } TWeakPtr WeakTabManager = SharedThis(this); auto HandleGenerateRow = [WeakTabManager](TSharedPtr InNode, const TSharedRef& InOwnerTableView) -> TSharedRef { return SNew(SSequencerGroupNodeRow, InOwnerTableView, InNode, WeakTabManager); }; auto HandleGetChildren = [](TSharedPtr InParent, TArray>& OutChildren) { OutChildren.Append(InParent->Children); }; TreeView = SNew(STreeView>) .OnGenerateRow_Lambda(HandleGenerateRow) .OnGetChildren_Lambda(HandleGetChildren) .TreeItemsSource(&NodeGroupsTree) .OnSelectionChanged(this, &SSequencerGroupManager::HandleTreeSelectionChanged) .OnContextMenuOpening(this, &SSequencerGroupManager::OnContextMenuOpening); ChildSlot [ SNew(SBorder) .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) [ SNew(SVerticalBox) + SVerticalBox::Slot() [ SNew(SScrollBorder, TreeView.ToSharedRef()) [ TreeView.ToSharedRef() ] ] ] ]; UpdateTree(); } SSequencerGroupManager::~SSequencerGroupManager() { } void SSequencerGroupManager::UpdateTree() { using namespace UE::Sequencer; TSharedPtr Sequencer = WeakSequencer.Pin(); UMovieScene* MovieScene = GetMovieScene(); if (!ensure(Sequencer) || !ensure(MovieScene)) { return; } TSharedRef NodeTree = Sequencer->GetNodeTree(); TArray ExpandedSets; TSet> ExpandedTreeItems; TreeView->GetExpandedItems(ExpandedTreeItems); for(TSharedPtr ExpandedTreeItem : ExpandedTreeItems) { if (ExpandedTreeItem->GetType() == FSequencerNodeGroupTreeNode::Type::GroupNode) { TSharedPtr NodeGroupNode = StaticCastSharedPtr(ExpandedTreeItem); ExpandedSets.Add(NodeGroupNode->Group); } } NodeGroupsTree.Empty(); AllNodeGroupItems.Empty(); for (UMovieSceneNodeGroup* NodeGroup : MovieScene->GetNodeGroups()) { TSharedPtr SequencerGroupNode = MakeShared(FText::FromName(NodeGroup->GetName()), NodeGroup, SharedThis(this)); NodeGroupsTree.Add(SequencerGroupNode); for (const FString& NodePath : NodeGroup->GetNodes()) { TViewModelPtr Node = NodeTree->GetNodeAtPath(NodePath); if (Node) { TSharedPtr SequencerGroupItemNode = MakeShared(Node->GetLabel(), NodePath, SequencerGroupNode); SequencerGroupNode->Children.Add(SequencerGroupItemNode); AllNodeGroupItems.Add(NodePath); } } SequencerGroupNode->Children.Sort([](const TSharedPtr& A, const TSharedPtr& B) { return A->DisplayText.CompareTo(B->DisplayText) < 0; }); } NodeGroupsTree.Sort([](const TSharedPtr& A, const TSharedPtr& B) { return A->DisplayText.CompareTo(B->DisplayText) < 0; }); TreeView->SetTreeItemsSource(&NodeGroupsTree); for (TSharedPtr NodeGroupTreeNode : NodeGroupsTree) { if (NodeGroupTreeNode->GetType() == FSequencerNodeGroupTreeNode::Type::GroupNode) { TSharedPtr NodeGroupNode = StaticCastSharedPtr(NodeGroupTreeNode); if (ExpandedSets.Contains(NodeGroupNode->Group)) { TreeView->SetItemExpansion(NodeGroupTreeNode,true); } } } TreeView->RequestTreeRefresh(); bNodeGroupsDirty = false; } void SSequencerGroupManager::HandleTreeSelectionChanged(TSharedPtr InSelectedNode, ESelectInfo::Type SelectionType) { SelectSelectedItemsInSequencer(); } void SSequencerGroupManager::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) { if (bNodeGroupsDirty) { UpdateTree(); } if (RequestedRenameNodeGroup && !TreeView->IsPendingRefresh()) { for (const TSharedPtr& Node : NodeGroupsTree) { if (Node->GetType() == FSequencerNodeGroupTreeNode::Type::GroupNode) { TSharedPtr SequencerGroupItemNode = StaticCastSharedPtr(Node); if (SequencerGroupItemNode->Group == RequestedRenameNodeGroup) { SequencerGroupItemNode->OnRenameRequested(); break; } } } RequestedRenameNodeGroup = nullptr; } } const FSlateBrush* SSequencerGroupManager::GetIconBrush(TSharedPtr NodeGroupTreeNode) const { using namespace UE::Sequencer; if (NodeGroupTreeNode->GetType() == FSequencerNodeGroupTreeNode::Type::ItemNode) { TSharedPtr SequencerGroupItemNode = StaticCastSharedPtr(NodeGroupTreeNode); TSharedPtr Sequencer = WeakSequencer.Pin(); if (!Sequencer) { return nullptr; } TSharedRef NodeTree = Sequencer->GetNodeTree(); // @todo_sequencer_mvvm: This is literally walking the entire tree to find a node by its path. // Worse still, it is doing so every frame, for every group node :/ TViewModelPtr OutlinerNode = NodeTree->GetNodeAtPath(SequencerGroupItemNode->Path); if (OutlinerNode) { return OutlinerNode->GetIconBrush(); } } return nullptr; } void SSequencerGroupManager::SelectItemsInGroup(FSequencerNodeGroupNode* Node) { TreeView->ClearSelection(); for (TSharedPtr ChildNode : Node->Children) { TreeView->SetItemSelection(ChildNode, true); } } void SSequencerGroupManager::RequestDeleteNodeGroup(FSequencerNodeGroupNode * NodeGroupNode) { UMovieScene* MovieScene = GetMovieScene(); if (!ensure(MovieScene) || !ensure(NodeGroupNode)) { return; } if (MovieScene->IsReadOnly()) { return; } const FScopedTransaction Transaction(LOCTEXT("DeleteGroupTransaction", "Delete Group")); MovieScene->GetNodeGroups().RemoveNodeGroup(NodeGroupNode->Group); } void SSequencerGroupManager::RemoveSelectedItemsFromNodeGroup() { UMovieScene* MovieScene = GetMovieScene(); if (!ensure(MovieScene)) { return; } if (MovieScene->IsReadOnly()) { return; } TArray> ItemsToRemove; TArray> SelectedNodes = TreeView->GetSelectedItems(); for (const TSharedPtr& Node : SelectedNodes) { if (Node->GetType() == FSequencerNodeGroupTreeNode::Type::ItemNode) { TSharedPtr ItemNode = StaticCastSharedPtr(Node); ItemsToRemove.Add(TPair(ItemNode->Group->Group,ItemNode->Path)); } } if (ItemsToRemove.Num() < 1) { return; } const FScopedTransaction Transaction(LOCTEXT("RemoveItemFromGroupTransaction", "Remove Items From Group")); for (const TPair& Item : ItemsToRemove) { Item.Key->RemoveNode(Item.Value); } RefreshNodeGroups(); } void SSequencerGroupManager::CreateNodeGroup() { UMovieScene* MovieScene = GetMovieScene(); if (!ensure(MovieScene)) { return; } if (MovieScene->IsReadOnly()) { return; } TArray ExistingGroupNames; for (const UMovieSceneNodeGroup* NodeGroup : MovieScene->GetNodeGroups()) { ExistingGroupNames.Add(NodeGroup->GetName()); } const FScopedTransaction Transaction(LOCTEXT("CreateNewGroupTransaction", "Create New Group")); MovieScene->Modify(); UMovieSceneNodeGroup* NewNodeGroup = NewObject(&MovieScene->GetNodeGroups(), NAME_None, RF_Transactional); NewNodeGroup->SetName(FSequencerUtilities::GetUniqueName(FName("Group"), ExistingGroupNames)); TSet SelectedNodePaths; GetSelectedItemsNodePaths(SelectedNodePaths); for (const FString& NodeToAdd : SelectedNodePaths) { NewNodeGroup->AddNode(NodeToAdd); } MovieScene->GetNodeGroups().AddNodeGroup(NewNodeGroup); RequestRenameNodeGroup(NewNodeGroup); } void SSequencerGroupManager::GetSelectedItemsNodePaths(TSet& OutSelectedNodePaths) const { TArray> SelectedNodes = TreeView->GetSelectedItems(); for (const TSharedPtr& Node : SelectedNodes) { if (Node->GetType() == FSequencerNodeGroupTreeNode::Type::ItemNode) { TSharedPtr ItemNode = StaticCastSharedPtr(Node); OutSelectedNodePaths.Add(ItemNode->Path); } } } void SSequencerGroupManager::SelectSelectedItemsInSequencer() { if (bSynchronizingSelection) { return; } // When selection changes in the group manager tree, select the corresponding Sequencer items first { TGuardValue Guard(bSynchronizingSelection, true); TSharedPtr Sequencer = WeakSequencer.Pin(); if (!ensure(Sequencer)) { return; } TSet SelectedNodePaths; GetSelectedItemsNodePaths(SelectedNodePaths); Sequencer->SelectNodesByPath(SelectedNodePaths); } } void SSequencerGroupManager::SelectItemsSelectedInSequencer() { using namespace UE::Sequencer; if (bSynchronizingSelection) { return; } TGuardValue Guard(bSynchronizingSelection, true); TSharedPtr Sequencer = WeakSequencer.Pin(); if (!ensure(Sequencer)) { return; } TStringBuilder<128> TempString; // Build a list of the nodepaths that we want to consider for selection TSet NodesPathsToSelect; for (FViewModelPtr Model : Sequencer->GetViewModel()->GetSelection()->Outliner) { TViewModelPtr Groupable = Model->FindAncestorOfType(true); if (Groupable) { TempString.Reset(); Groupable->GetIdentifierForGrouping(TempString); for (const FString& NodeGroupPath : AllNodeGroupItems) { // AllNodeGroupItems path is the full path (including folder) if (NodeGroupPath.Contains(TempString.ToString())) { NodesPathsToSelect.Add(NodeGroupPath); break; } } } } TreeView->ClearSelection(); // Build a list of the treenodes which match a nodepath we want to select for (const TSharedPtr& Node : NodeGroupsTree) { if (Node->GetType() == FSequencerNodeGroupTreeNode::Type::ItemNode) { TSharedPtr ItemNode = StaticCastSharedPtr(Node); if (NodesPathsToSelect.Contains(ItemNode->Path)) { TreeView->SetItemSelection(Node, true); } } else if (Node->GetType() == FSequencerNodeGroupTreeNode::Type::GroupNode) { for (TSharedPtr ChildNode : Node->Children) { // Note: Currently, children of a set can only be item nodes, but that may change in the future. if (ChildNode->GetType() == FSequencerNodeGroupTreeNode::Type::ItemNode) { TSharedPtr ItemNode = StaticCastSharedPtr(ChildNode); if (NodesPathsToSelect.Contains(ItemNode->Path)) { TreeView->SetItemSelection(ChildNode, true); } } } } } } TSharedPtr SSequencerGroupManager::OnContextMenuOpening() { TArray> SelectedNodes = TreeView->GetSelectedItems(); FMenuBuilder MenuBuilder(true, nullptr); MenuBuilder.AddMenuEntry( LOCTEXT("CreateNodeGroup", "Create Group"), LOCTEXT("CreateNodeGroupTooltip", "Create a new group and add any selected items to it"), FSlateIcon(), FUIAction(FExecuteAction::CreateSP(this, &SSequencerGroupManager::CreateNodeGroup))); UMovieScene* MovieScene = GetMovieScene(); bool bIsReadOnly = MovieScene? MovieScene->IsReadOnly() : true; for (const TSharedPtr& Node : SelectedNodes) { if (Node->GetType() == FSequencerNodeGroupTreeNode::Type::GroupNode) { TSharedPtr NodeGroupNode = StaticCastSharedPtr(Node); MenuBuilder.AddMenuEntry( LOCTEXT("NodeGroupToggleFilter", "Toggle Filter"), LOCTEXT("NodeGroupToggleFilterTooltip", "Toggle whether this group should be used to filter items"), FSlateIcon(), FUIAction( FExecuteAction::CreateLambda([NodeGroupNode]() { NodeGroupNode->OnEnableFilterClicked(); }), FCanExecuteAction::CreateLambda([bIsReadOnly]() { return !bIsReadOnly; }), FIsActionChecked::CreateLambda([NodeGroupNode]() { return NodeGroupNode->IsFilterEnabled(); })), NAME_None, EUserInterfaceActionType::ToggleButton ); MenuBuilder.AddMenuEntry( LOCTEXT("SelectItemsInGroup", "Select Items in Group"), LOCTEXT("SelectItemsInGroupTooltip", "Select items in group"), FSlateIcon(), FUIAction(FExecuteAction::CreateSP(this, &SSequencerGroupManager::SelectItemsInGroup, NodeGroupNode.Get()))); MenuBuilder.AddMenuEntry( FText::Format(LOCTEXT("RenameNodeGroupFormat", "Rename {0}"), NodeGroupNode->DisplayText), FText(), FSlateIcon(), FUIAction(FExecuteAction::CreateLambda([NodeGroupNode]() { NodeGroupNode->OnRenameRequested(); }))); MenuBuilder.AddMenuEntry( LOCTEXT("DeleteNodeGroup", "Delete Group"), FText(), FSlateIcon(), FUIAction(FExecuteAction::CreateSP(this, &SSequencerGroupManager::RequestDeleteNodeGroup, NodeGroupNode.Get()))); break; } } bool bAnyItemSelected = false; for (const TSharedPtr& Node : SelectedNodes) { if (Node->GetType() == FSequencerNodeGroupTreeNode::Type::ItemNode) { bAnyItemSelected = true; break; } } if (bAnyItemSelected) { MenuBuilder.AddMenuEntry( LOCTEXT("RemoveItemsFromNodeGropu", "Remove Items From Group"), FText(), FSlateIcon(), FUIAction(FExecuteAction::CreateSP(this, &SSequencerGroupManager::RemoveSelectedItemsFromNodeGroup))); } return MenuBuilder.MakeWidget(); } #undef LOCTEXT_NAMESPACE