git mv Code\Sandbox\Plugins Code/Editor/Plugins

Signed-off-by: Esteban Papp <81431996+amznestebanpapp@users.noreply.github.com>
This commit is contained in:
Esteban Papp
2021-06-29 12:42:54 -07:00
parent e34e36cb35
commit 1696680240
215 changed files with 0 additions and 0 deletions
@@ -0,0 +1,76 @@
#
# Copyright (c) Contributors to the Open 3D Engine Project
#
# SPDX-License-Identifier: Apache-2.0 OR MIT
#
#
if(NOT PAL_TRAIT_BUILD_HOST_TOOLS)
return()
endif()
ly_add_target(
NAME ComponentEntityEditorPlugin MODULE
NAMESPACE Legacy
OUTPUT_SUBDIRECTORY EditorPlugins
AUTOMOC
AUTOUIC
AUTORCC
FILES_CMAKE
componententityeditorplugin_files.cmake
COMPILE_DEFINITIONS
PRIVATE
PLUGIN_EXPORTS
INCLUDE_DIRECTORIES
PUBLIC
.
BUILD_DEPENDENCIES
PRIVATE
3rdParty::Qt::Core
AZ::AzCore
AZ::AzToolsFramework
Legacy::CryCommon
Legacy::EditorLib
AZ::AtomCore
Gem::Atom_RPI.Public
Gem::AtomToolsFramework.Static
Gem::LmbrCentral.Editor
RUNTIME_DEPENDENCIES
Gem::LmbrCentral.Editor
)
ly_add_dependencies(Editor ComponentEntityEditorPlugin)
set_property(GLOBAL APPEND PROPERTY LY_EDITOR_PLUGINS $<TARGET_FILE_NAME:Legacy::ComponentEntityEditorPlugin>)
################################################################################
# Tests
################################################################################
if(PAL_TRAIT_BUILD_TESTS_SUPPORTED)
ly_add_target(
NAME ComponentEntityEditorPlugin.Tests ${PAL_TRAIT_TEST_TARGET_TYPE}
NAMESPACE Legacy
AUTOMOC
FILES_CMAKE
componententityeditorplugin_test_files.cmake
INCLUDE_DIRECTORIES
PRIVATE
.
BUILD_DEPENDENCIES
PRIVATE
3rdParty::Qt::Core
3rdParty::Qt::Gui
3rdParty::Qt::Widgets
AZ::AzTest
AZ::AzToolsFramework
AZ::AzToolsFrameworkTestCommon
Legacy::CryCommon
Legacy::EditorLib
Gem::LmbrCentral.Editor
RUNTIME_DEPENDENCIES
Gem::LmbrCentral.Editor
)
ly_add_googletest(
NAME Legacy::ComponentEntityEditorPlugin.Tests
)
endif()
@@ -0,0 +1,210 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include "ComponentEntityEditorPlugin_precompiled.h"
#include "ComponentEntityEditorPlugin.h"
#include <LyViewPaneNames.h>
#include "IResourceSelectorHost.h"
#include "UI/QComponentEntityEditorMainWindow.h"
#include "UI/QComponentEntityEditorOutlinerWindow.h"
#include "UI/QComponentLevelEntityEditorMainWindow.h"
#include "UI/ComponentPalette/ComponentPaletteSettings.h"
#include "UI/ComponentPalette/ComponentPaletteWindow.h"
#include <AzCore/Component/ComponentApplicationBus.h>
#include <AzCore/Component/EntityUtils.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/std/containers/vector.h>
#include <AzFramework/API/ApplicationAPI.h>
#include <AzToolsFramework/API/EntityCompositionRequestBus.h>
#include <AzToolsFramework/API/ToolsApplicationAPI.h>
#include <AzToolsFramework/UI/Slice/SliceRelationshipWidget.hxx>
#include "SandboxIntegration.h"
#include "Objects/ComponentEntityObject.h"
namespace ComponentEntityEditorPluginInternal
{
void RegisterSandboxObjects()
{
GetIEditor()->GetClassFactory()->RegisterClass(new CTemplateObjectClassDesc<CComponentEntityObject>("ComponentEntity", "", "", OBJTYPE_AZENTITY, 201, "*.entity"));
AZ::SerializeContext* serializeContext = nullptr;
EBUS_EVENT_RESULT(serializeContext, AZ::ComponentApplicationBus, GetSerializeContext);
AZ_Assert(serializeContext, "Serialization context not available");
if (serializeContext)
{
ComponentPaletteSettings::Reflect(serializeContext);
}
}
void UnregisterSandboxObjects()
{
GetIEditor()->GetClassFactory()->UnregisterClass("ComponentEntity");
}
void CheckComponentDeclarations()
{
AZ::SerializeContext* serializeContext = nullptr;
AZ::ComponentApplicationBus::BroadcastResult(serializeContext, &AZ::ComponentApplicationBus::Events::GetSerializeContext);
if (!serializeContext)
{
return;
}
// Catch the common mistake of reflecting a Component to SerializeContext
// without declaring how it inherits from AZ::Component.
// Collect violators so we can list them all in one message, rather than raising N popups.
AZStd::vector<AZ::ComponentDescriptor*> componentsLackingBaseClass;
AZ::EBusAggregateResults<AZ::ComponentDescriptor*> allComponentDescriptors;
AZ::ComponentDescriptorBus::BroadcastResult(allComponentDescriptors, &AZ::ComponentDescriptorBus::Events::GetDescriptor);
for (AZ::ComponentDescriptor* componentDescriptor : allComponentDescriptors.values)
{
const AZ::TypeId& componentTypeId = componentDescriptor->GetUuid();
const AZ::TypeId& typeOfAZComponent = azrtti_typeid<AZ::Component>();
if (const AZ::SerializeContext::ClassData* serializeData = serializeContext->FindClassData(componentTypeId))
{
if (!AZ::EntityUtils::CheckIfClassIsDeprecated(serializeContext, componentTypeId)
&& !AZ::EntityUtils::CheckDeclaresSerializeBaseClass(serializeContext, typeOfAZComponent, componentTypeId))
{
componentsLackingBaseClass.push_back(componentDescriptor);
}
}
}
AZStd::string message;
for (AZ::ComponentDescriptor* componentDescriptor : componentsLackingBaseClass)
{
message.append(AZStd::string::format("- %s %s\n",
componentDescriptor->GetName(),
componentDescriptor->GetUuid().ToString<AZStd::string>().c_str()));
}
if (!message.empty())
{
message.insert(0, "Programmer error:\nClasses deriving from AZ::Component are not declaring their base class to SerializeContext.\n"
"This will cause unexpected behavior such as components shifting around, or duplicating themselves.\n"
"Affected components:\n");
message.append("\nReflection code should look something like this:\n"
"serializeContext->Class<MyComponent, AZ::Component, ... (other base classes, if any) ...>()"
"\nMake sure the Reflect function is called for all base classes as well.");
// this happens during startup, and its a programmer error - so during startup, make it an error, so it shows as a pretty noisy
// popup box. Its important that programmers fix this, before they submit their code, so that data corruption / data loss does
// not occur.
AZ_Error("Serialize", false, message.c_str());
}
}
} // end namespace ComponentEntityEditorPluginInternal
ComponentEntityEditorPlugin::ComponentEntityEditorPlugin([[maybe_unused]] IEditor* editor)
: m_registered(false)
{
m_appListener = new SandboxIntegrationManager();
m_appListener->Setup();
using namespace AzToolsFramework;
ViewPaneOptions inspectorOptions;
inspectorOptions.canHaveMultipleInstances = true;
inspectorOptions.preferedDockingArea = Qt::RightDockWidgetArea;
RegisterViewPane<QComponentEntityEditorInspectorWindow>(
LyViewPane::EntityInspector,
LyViewPane::CategoryTools,
inspectorOptions);
ViewPaneOptions pinnedInspectorOptions;
pinnedInspectorOptions.canHaveMultipleInstances = true;
pinnedInspectorOptions.preferedDockingArea = Qt::NoDockWidgetArea;
pinnedInspectorOptions.paneRect = QRect(50, 50, 400, 700);
pinnedInspectorOptions.showInMenu = false;
RegisterViewPane<QComponentEntityEditorInspectorWindow>(
LyViewPane::EntityInspectorPinned,
LyViewPane::CategoryTools,
pinnedInspectorOptions);
bool prefabSystemEnabled = false;
AzFramework::ApplicationRequests::Bus::BroadcastResult(prefabSystemEnabled, &AzFramework::ApplicationRequests::IsPrefabSystemEnabled);
if (prefabSystemEnabled)
{
// Add the new Outliner to the Tools Menu
ViewPaneOptions outlinerOptions;
outlinerOptions.canHaveMultipleInstances = true;
outlinerOptions.preferedDockingArea = Qt::LeftDockWidgetArea;
RegisterViewPane<QEntityOutlinerWindow>(
LyViewPane::EntityOutliner,
LyViewPane::CategoryTools,
outlinerOptions);
}
else
{
ViewPaneOptions levelInspectorOptions;
levelInspectorOptions.canHaveMultipleInstances = false;
levelInspectorOptions.preferedDockingArea = Qt::RightDockWidgetArea;
levelInspectorOptions.paneRect = QRect(50, 50, 400, 700);
RegisterViewPane<QComponentLevelEntityEditorInspectorWindow>(
LyViewPane::LevelInspector, LyViewPane::CategoryTools, levelInspectorOptions);
// Add the Legacy Outliner to the Tools Menu
ViewPaneOptions outlinerOptions;
outlinerOptions.canHaveMultipleInstances = true;
outlinerOptions.preferedDockingArea = Qt::LeftDockWidgetArea;
// this pane was originally introduced with this name, so layout settings are all saved with that name, despite the preview label being removed.
outlinerOptions.saveKeyName = "Entity Outliner (PREVIEW)";
RegisterViewPane<QComponentEntityEditorOutlinerWindow>(
LyViewPane::EntityOutliner,
LyViewPane::CategoryTools,
outlinerOptions);
AzToolsFramework::ViewPaneOptions options;
options.preferedDockingArea = Qt::NoDockWidgetArea;
RegisterViewPane<SliceRelationshipWidget>(LyViewPane::SliceRelationships, LyViewPane::CategoryTools, options);
}
RegisterModuleResourceSelectors(GetIEditor()->GetResourceSelectorHost());
ComponentEntityEditorPluginInternal::RegisterSandboxObjects();
// Check for common mistakes in component declarations
ComponentEntityEditorPluginInternal::CheckComponentDeclarations();
m_registered = true;
}
void ComponentEntityEditorPlugin::Release()
{
if (m_registered)
{
using namespace AzToolsFramework;
UnregisterViewPane(LyViewPane::EntityInspector);
UnregisterViewPane(LyViewPane::EntityOutliner);
UnregisterViewPane(LyViewPane::EntityInspectorPinned);
ComponentEntityEditorPluginInternal::UnregisterSandboxObjects();
}
m_appListener->Teardown();
delete m_appListener;
delete this;
}
@@ -0,0 +1,30 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#include <IEditor.h>
#include <Include/IPlugin.h>
//------------------------------------------------------------------
class ComponentEntityEditorPlugin
: public IPlugin
{
public:
ComponentEntityEditorPlugin(IEditor* editor);
virtual ~ComponentEntityEditorPlugin() = default;
void Release() override;
void ShowAbout() override {}
const char* GetPluginGUID() override { return "{11B0041C-BC34-4827-A3E4-AB7458FFF678}"; }
DWORD GetPluginVersion() override { return 1; }
const char* GetPluginName() override { return "ComponentEntityEditor"; }
bool CanExitNow() override { return true; }
void OnEditorNotify([[maybe_unused]] EEditorNotifyEvent aEventId) override {}
private:
bool m_registered;
class SandboxIntegrationManager* m_appListener;
};
@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<Manifest>
</Manifest>
@@ -0,0 +1,32 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#include <AzCore/PlatformDef.h>
/////////////////////////////////////////////////////////////////////////////
// Engine
/////////////////////////////////////////////////////////////////////////////
#include <Cry_Math.h>
#include <ISystem.h>
#include <ISerialize.h>
#include <CryName.h>
#include <EditorDefs.h>
#include <Resource.h>
/////////////////////////////////////////////////////////////////////////////
// STL
/////////////////////////////////////////////////////////////////////////////
#include <vector>
#include <list>
#include <map>
#include <set>
#include <algorithm>
#ifdef CreateDirectory
#undef CreateDirectory
#endif
@@ -0,0 +1,257 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#ifndef CRYINCLUDE_COMPONENTENTITYEDITORPLUGIN_COMPONENTENTITYOBJECT_H
#define CRYINCLUDE_COMPONENTENTITYEDITORPLUGIN_COMPONENTENTITYOBJECT_H
#pragma once
#if !defined(Q_MOC_RUN)
#include <AzCore/Component/TransformBus.h>
#include <AzCore/Component/EntityBus.h>
#include <AzFramework/Entity/EntityDebugDisplayBus.h>
#include <AzToolsFramework/API/ToolsApplicationAPI.h>
#include <AzToolsFramework/API/ComponentEntityObjectBus.h>
#include <AzToolsFramework/ToolsComponents/EditorLockComponentBus.h>
#include <AzToolsFramework/ToolsComponents/EditorVisibilityBus.h>
#include <AzToolsFramework/ToolsComponents/EditorEntityIconComponentBus.h>
#include <AzToolsFramework/UI/PropertyEditor/PropertyEditorAPI.h>
#include <QtViewPane.h>
#include "../Editor/Objects/EntityObject.h"
#include <LmbrCentral/Rendering/RenderBoundsBus.h>
#endif
class QMenu;
/**
* Sandbox representation of component entities (AZ::Entity).
*/
class CComponentEntityObject
: public CEntityObject
, private AzToolsFramework::EditorLockComponentNotificationBus::Handler
, private AzToolsFramework::EditorVisibilityNotificationBus::Handler
, private AzToolsFramework::EditorEntityIconComponentNotificationBus::Handler
, private AZ::TransformNotificationBus::Handler
, private LmbrCentral::RenderBoundsNotificationBus::Handler
, private AzToolsFramework::ComponentEntityEditorRequestBus::Handler
, private AzToolsFramework::ComponentEntityObjectRequestBus::Handler
, private AZ::EntityBus::Handler
{
public:
CComponentEntityObject();
~CComponentEntityObject();
//////////////////////////////////////////////////////////////////////////
// Overrides from CEntityObject/CBaseObject.
//////////////////////////////////////////////////////////////////////////
bool Init(IEditor* ie, CBaseObject* prev, const QString& file) override;
void InitVariables() override {};
bool SetPos(const Vec3& pos, int flags = 0) override;
bool SetRotation(const Quat& rotate, int flags) override;
bool SetScale(const Vec3& scale, int flags) override;
void InvalidateTM(int nWhyFlags) override;
void Display(DisplayContext& disp) override;
void OnContextMenu(QMenu* pMenu) override;
int MouseCreateCallback(CViewport* view, EMouseEvent event, QPoint& point, int flags) override;
bool HitHelperTest(HitContext& hc) override;
bool HitTest(HitContext& hc) override;
void GetLocalBounds(AABB& box) override;
void GetBoundBox(AABB& box) override;
void SetName(const QString& name) override;
bool IsFrozen() const override;
void SetFrozen(bool bFrozen) override;
void SetHidden(bool bHidden, uint64 hiddenId = CBaseObject::s_invalidHiddenID, bool bAnimated = false) override;
void SetSelected(bool bSelect) override;
void SetHighlight(bool bHighlight) override;
IRenderNode* GetEngineNode() const override;
void AttachChild(CBaseObject* child, bool bKeepPos = true) override;
void DetachAll(bool bKeepPos = true) override;
void DetachThis(bool bKeepPos = true) override;
CBaseObject* GetLinkParent() const override;
XmlNodeRef Export(const QString& levelPath, XmlNodeRef& xmlNode) override;
void DeleteEntity() override;
void DrawDefault(DisplayContext& dc, const QColor& labelColor = QColor(255, 255, 255)) override;
IStatObj* GetIStatObj() override;
bool IsIsolated() const override;
bool IsSelected() const override;
bool IsSelectable() const override;
void SetWorldPos(const Vec3& pos, int flags = 0) override;
// Always returns false as Component entity highlighting (accenting) is taken care of elsewhere
bool IsHighlighted() { return false; }
// Component entity highlighting (accenting) is taken care of elsewhere
void DrawHighlight(DisplayContext& /*dc*/) {};
// Don't auto-clone children. Cloning happens in groups with reference fixups,
// and individually selected objercts should be cloned as individuals.
bool ShouldCloneChildren() const override { return false; }
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
// AZ::EntityBus::Handler
void OnEntityNameChanged(const AZStd::string& name) override;
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
// AzToolsFramework::EditorLockComponentNotificationBus::Handler
void OnEntityLockChanged(bool locked) override;
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
// AzToolsFramework::EditorVisibilityNotificationBus::Handler
void OnEntityVisibilityChanged(bool flag) override;
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
// AzToolsFramework::EditorEntityIconComponentNotificationBus::Handler
void OnEntityIconChanged(const AZ::Data::AssetId& entityIconAssetId) override;
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
//! AZ::TransformNotificationBus::Handler
void OnTransformChanged(const AZ::Transform& local, const AZ::Transform& world) override;
void OnParentChanged(AZ::EntityId oldParent, AZ::EntityId newParent) override;
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
//! RenderBoundsNotificationBus
void OnRenderBoundsReset() override;
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
//! ComponentEntityEditorRequestBus
CEntityObject* GetSandboxObject() override { return this; }
bool IsSandboxObjectHighlighted() override { return IsHighlighted(); }
void SetSandboxObjectAccent(AzToolsFramework::EntityAccentType accent) override;
void SetSandBoxObjectIsolated(bool isIsolated) override;
bool IsSandBoxObjectIsolated() override;
void RefreshVisibilityAndLock() override;
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
//! ComponentEntityObjectRequestBus
AZ::EntityId GetAssociatedEntityId() override { return m_entityId; }
void UpdatePreemptiveUndoCache() override;
//////////////////////////////////////////////////////////////////////////
void AssignEntity(AZ::Entity* entity, bool destroyOld = true);
bool IsEntityIconVisible() const { return m_entityIconVisible; }
static CComponentEntityObject* FindObjectForEntity(AZ::EntityId id);
protected:
friend class CTemplateObjectClassDesc<CComponentEntityObject>;
friend class SandboxIntegrationManager;
static const GUID& GetClassID()
{
// {70650EB8-B1BD-4DC8-AC28-7CD767D7BB30}
static const GUID guid = {
0x70650EB8, 0xB1BD, 0x4DC8, { 0xac, 0x28, 0x7c, 0xd7, 0x67, 0xd7, 0xbb, 0x30 }
};
return guid;
}
float GetRadius();
void DeleteThis() { delete this; };
bool IsNonLayerAncestorSelected() const;
bool IsLayer() const;
bool IsAncestorIconDrawingAtSameLocation() const;
bool IsDescendantSelectedAtSameLocation() const;
void SetupEntityIcon();
void DrawAccent(DisplayContext& dc);
class EditorActionGuard
{
public:
EditorActionGuard()
: m_count(0) {}
void Enter() { ++m_count; }
void Exit() { --m_count; }
//! \return true if the guard passes.
operator bool() const {
return m_count <= 0;
}
private:
int m_count;
};
class EditorActionScope
{
public:
EditorActionScope(EditorActionGuard& guard)
: m_guard(guard)
{
m_guard.Enter();
}
~EditorActionScope()
{
m_guard.Exit();
}
private:
EditorActionGuard& m_guard;
};
EditorActionGuard m_lockedReentryGuard;
EditorActionGuard m_nameReentryGuard;
EditorActionGuard m_selectionReentryGuard;
EditorActionGuard m_visibilityFlagReentryGuard;
EditorActionGuard m_transformReentryGuard;
EditorActionGuard m_parentingReentryGuard;
AzToolsFramework::EntityAccentType m_accentType;
//! Whether we have have a valid icon path in \ref m_icon
bool m_hasIcon;
//! Whether this component entity icon is visible
bool m_entityIconVisible;
//! Whether to only use this object's icon for hit tests. When enabled, we ignore hit tests
//! against the geometry of the object
bool m_iconOnlyHitTest;
//! Whether to draw accents for this object (accents include selection wireframe bounding boxes)
bool m_drawAccents;
//! Indicate if an entity is isolated when the editor is in Isolation Mode.
bool m_isIsolated;
//! EntityId that this editor object represents/is tied to
AZ::EntityId m_entityId;
//! Path to component entity icon for this object
AZStd::string m_icon;
ITexture* m_iconTexture;
//! Displays viewport icon for this entity.
//! \returns whether an icon is being displayed
bool DisplayEntityIcon(
DisplayContext& dc, AzFramework::DebugDisplayRequests& debugDisplay);
};
#endif // CRYINCLUDE_COMPONENTENTITYEDITORPLUGIN_COMPONENTENTITYOBJECT_H
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,371 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#ifndef CRYINCLUDE_COMPONENTENTITYEDITORPLUGIN_SANDBOXINTEGRATION_H
#define CRYINCLUDE_COMPONENTENTITYEDITORPLUGIN_SANDBOXINTEGRATION_H
#include <AzCore/Component/ComponentApplicationBus.h>
#include <AzCore/Slice/SliceBus.h>
#include <AzCore/Slice/SliceComponent.h>
#include <AzCore/Math/Uuid.h>
#include <AzCore/std/string/conversions.h>
#include <AzFramework/Asset/AssetCatalogBus.h>
#include <AzFramework/Entity/EntityDebugDisplayBus.h>
#include <AzFramework/Viewport/DisplayContextRequestBus.h>
#include <AzToolsFramework/API/EditorWindowRequestBus.h>
#include <AzToolsFramework/API/ToolsApplicationAPI.h>
#include <AzToolsFramework/Editor/EditorContextMenuBus.h>
#include <AzToolsFramework/ToolsComponents/EditorLayerComponentBus.h>
#include <AzToolsFramework/UI/PropertyEditor/PropertyEditorAPI.h>
#include <AzToolsFramework/UI/Layer/LayerUiHandler.h>
#include <AzToolsFramework/UI/Prefab/PrefabIntegrationManager.h>
#include <AzToolsFramework/UI/Slice/SliceOverridesNotificationWindowManager.hxx>
#include <AzToolsFramework/UI/Slice/SliceOverridesNotificationWindow.hxx>
#include <AzToolsFramework/Entity/EditorEntityContextBus.h>
#include <AzToolsFramework/Entity/SliceEditorEntityOwnershipServiceBus.h>
#include <AzToolsFramework/Viewport/ViewportMessages.h>
// Sandbox imports.
#include "../Editor/ViewManager.h"
#include "../Editor/Viewport.h"
#include "../Editor/Undo/IUndoManagerListener.h"
#include "../Editor/Undo/IUndoObject.h"
#include <QApplication>
#include <QPointer>
/**
* Integration of ToolsApplication behavior and Cry undo/redo and selection systems
* with respect to component entity operations.
*
* Undo/Redo
* - CToolsApplicationUndoLink represents a component application undo operation within
* the Sandbox undo system. When an undo-able component operation is performed, we
* intercept ToolsApplicationEventBus::OnBeginUndo()/OnEndUndo() events, and in turn
* create and register a link instance.
* - When the user attempts to undo/redo a CToolsApplicationUndoLink event in Sandbox,
* CToolsApplicationUndoLink::Undo()/Redo() is invoked, and the request is passed
* to the component application via ToolsApplicationRequestBus::OnUndoPressed/OnRedoPressed,
* where restoration of the previous entity snapshot is handled.
*
* AzToolsFramework::ToolsApplication Extensions
* - Provides engine UI customizations, such as using the engine's built in asset browser
* when assigning asset references to component properties.
* - Handles component edit-time display requests (using the editor's drawing context).
* - Handles source control requests from AZ components or component-related UI.
*/
namespace AZ::Data
{
class AssetInfo;
}
class CToolsApplicationUndoLink;
class QMenu;
class QWidget;
class CComponentEntityObject;
class CHyperGraph;
namespace AzToolsFramework
{
class EditorEntityAPI;
class EditorEntityUiInterface;
namespace AssetBrowser
{
class AssetSelectionModel;
}
namespace Prefab
{
class PrefabIntegrationInterface;
}
}
//////////////////////////////////////////////////////////////////////////
class SandboxIntegrationManager
: private AzToolsFramework::ToolsApplicationEvents::Bus::Handler
, private AzToolsFramework::EditorRequests::Bus::Handler
, private AzToolsFramework::EditorPickModeNotificationBus::Handler
, private AzToolsFramework::EditorContextMenuBus::Handler
, private AzToolsFramework::EditorWindowRequests::Bus::Handler
, private AzFramework::AssetCatalogEventBus::Handler
, private AzFramework::DisplayContextRequestBus::Handler
, private AzToolsFramework::EditorEntityContextNotificationBus::Handler
, private AzToolsFramework::SliceEditorEntityOwnershipServiceNotificationBus::Handler
, private IUndoManagerListener
, private AzToolsFramework::Layers::EditorLayerComponentNotificationBus::Handler
{
public:
SandboxIntegrationManager();
~SandboxIntegrationManager();
void Setup();
void Teardown();
private:
//////////////////////////////////////////////////////////////////////////
// AssetCatalogEventBus::Handler
void OnCatalogAssetAdded(const AZ::Data::AssetId& assetId) override;
void OnCatalogAssetRemoved(const AZ::Data::AssetId& assetId, const AZ::Data::AssetInfo& assetInfo) override;
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
// AzToolsFramework::ToolsApplicationEvents::Bus::Handler overrides
void OnBeginUndo(const char* label) override;
void OnEndUndo(const char* label, bool changed) override;
void EntityParentChanged(
AZ::EntityId entityId,
AZ::EntityId newParentId,
AZ::EntityId oldParentId) override;
void OnSaveLevel() override;
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
// AzToolsFramework::EditorRequests::Bus::Handler overrides
void RegisterViewPane(const char* name, const char* category, const AzToolsFramework::ViewPaneOptions& viewOptions, const WidgetCreationFunc& widgetCreationFunc) override;
void RegisterCustomViewPane(const char* name, const char* category, const AzToolsFramework::ViewPaneOptions& viewOptions) override;
void UnregisterViewPane(const char* name) override;
QWidget* GetViewPaneWidget(const char* viewPaneName) override;
void OpenViewPane(const char* paneName) override;
QDockWidget* InstanceViewPane(const char* paneName) override;
void CloseViewPane(const char* paneName) override;
void BrowseForAssets(AzToolsFramework::AssetBrowser::AssetSelectionModel& selection) override;
void HandleObjectModeSelection(const AZ::Vector2& point, int flags, bool& handled) override;
void UpdateObjectModeCursor(AZ::u32& cursorId, AZStd::string& cursorStr) override;
void CreateEditorRepresentation(AZ::Entity* entity) override;
bool DestroyEditorRepresentation(AZ::EntityId entityId, bool deleteAZEntity) override;
void CloneSelection(bool& handled) override;
void DeleteSelectedEntities(bool includeDescendants) override;
AZ::EntityId CreateNewEntity(AZ::EntityId parentId = AZ::EntityId()) override;
AZ::EntityId CreateNewEntityAsChild(AZ::EntityId parentId) override;
AZ::EntityId CreateNewEntityAtPosition(const AZ::Vector3& /*pos*/, AZ::EntityId parentId = AZ::EntityId()) override;
AzFramework::EntityContextId GetEntityContextId() override;
QWidget* GetMainWindow() override;
IEditor* GetEditor() override;
bool GetUndoSliceOverrideSaveValue() override;
bool GetShowCircularDependencyError() override;
void SetShowCircularDependencyError(const bool& showCircularDependencyError) override;
void LaunchLuaEditor(const char* files) override;
bool IsLevelDocumentOpen() override;
AZStd::string GetLevelName() override;
AZStd::string SelectResource(const AZStd::string& resourceType, const AZStd::string& previousValue) override;
void OpenPinnedInspector(const AzToolsFramework::EntityIdSet& entities) override;
void ClosePinnedInspector(AzToolsFramework::EntityPropertyEditor* editor) override;
void GoToSelectedOrHighlightedEntitiesInViewports() override;
void GoToSelectedEntitiesInViewports() override;
bool CanGoToSelectedEntitiesInViewports() override;
AZ::Vector3 GetWorldPositionAtViewportCenter() override;
void InstantiateSliceFromAssetId(const AZ::Data::AssetId& assetId) override;
void ClearRedoStack() override;
int GetIconTextureIdFromEntityIconPath(const AZStd::string& entityIconPath) override;
bool DisplayHelpersVisible() override;
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
// AzToolsFramework::EditorWindowRequests::Bus::Handler
QWidget* GetAppMainWindow() override;
//////////////////////////////////////////////////////////////////////////
// EditorPickModeNotificationBus
void OnEntityPickModeStarted() override;
void OnEntityPickModeStopped() override;
//////////////////////////////////////////////////////////////////////////
// AzToolsFramework::EditorContextMenu::Bus::Handler overrides
void PopulateEditorGlobalContextMenu(QMenu* menu, const AZ::Vector2& point, int flags) override;
int GetMenuPosition() const;
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
// AzToolsFramework::EditorEntityContextNotificationBus::Handler
void OnContextReset() override;
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
/// AzToolsFramework::SliceEditorEntityOwnershipServiceNotificationBus::Handler
void OnSliceInstantiated(
const AZ::Data::AssetId& sliceAssetId,
AZ::SliceComponent::SliceInstanceAddress& sliceAddress,
const AzFramework::SliceInstantiationTicket& ticket) override;
//////////////////////////////////////////////////////////////////////////
// AzFramework::DisplayContextRequestBus (and @deprecated EntityDebugDisplayRequestBus)
// AzFramework::DisplayContextRequestBus
void SetDC(DisplayContext* dc) override;
DisplayContext* GetDC() override;
// Context menu handlers.
void ContextMenu_NewEntity();
AZ::EntityId ContextMenu_NewLayer();
void ContextMenu_SaveLayers(const AZStd::unordered_set<AZ::EntityId>& layers);
void ContextMenu_MakeSlice(AzToolsFramework::EntityIdList entities);
void ContextMenu_InheritSlice(AzToolsFramework::EntityIdList entities);
void ContextMenu_InstantiateSlice();
void ContextMenu_SelectSlice();
void ContextMenu_PushEntitiesToSlice(AzToolsFramework::EntityIdList entities,
AZ::SliceComponent::EntityAncestorList ancestors,
AZ::Data::AssetId targetAncestorId,
bool affectEntireHierarchy);
void ContextMenu_Duplicate();
void ContextMenu_DeleteSelected();
void ContextMenu_ResetToSliceDefaults(AzToolsFramework::EntityIdList entities);
void MakeSliceFromEntities(const AzToolsFramework::EntityIdList& entities, bool inheritSlices, bool setAsDynamic);
void GetSelectedEntities(AzToolsFramework::EntityIdList& entities);
void GetSelectedOrHighlightedEntities(AzToolsFramework::EntityIdList& entities);
AZStd::string GetDefaultComponentViewportIcon() override
{
return m_defaultComponentViewportIconLocation;
}
AZStd::string GetDefaultComponentEditorIcon() override
{
return m_defaultComponentIconLocation;
}
AZStd::string GetDefaultEntityIcon() override
{
return m_defaultEntityIconLocation;
}
AZStd::string GetComponentEditorIcon(const AZ::Uuid& componentType, AZ::Component* component) override;
AZStd::string GetComponentIconPath(const AZ::Uuid& componentType, AZ::Crc32 componentIconAttrib, AZ::Component* component) override;
//////////////////////////////////////////////////////////////////////////
// IUndoManagerListener
// Listens for Cry Undo System events.
void UndoStackFlushed() override;
// EditorLayerRequestBus...
void OnLayerComponentActivated(AZ::EntityId entityId) override;
void OnLayerComponentDeactivated(AZ::EntityId entityId) override;
private:
// Right click context menu when a layer is included in the selection.
void SetupLayerContextMenu(QMenu* menu);
void SetupSliceContextMenu(QMenu* menu);
void SetupSliceContextMenu_Modify(QMenu* menu, const AzToolsFramework::EntityIdList& selectedEntities, const AZ::u32 numEntitiesInSlices);
void SaveSlice(const bool& QuickPushToFirstLevel);
void GetEntitiesInSlices(const AzToolsFramework::EntityIdList& selectedEntities, AZ::u32& entitiesInSlices, AZStd::vector<AZ::SliceComponent::SliceInstanceAddress>& sliceInstances);
void GoToEntitiesInViewports(const AzToolsFramework::EntityIdList& entityIds);
bool CanGoToEntityOrChildren(const AZ::EntityId& entityId) const;
// This struct exists to help handle the error case where slice assets are
// accidentally deleted from disk but their instances are still in the editing level.
struct SliceAssetDeletionErrorInfo
{
SliceAssetDeletionErrorInfo() = default;
SliceAssetDeletionErrorInfo(AZ::Data::AssetId assetId, AZStd::vector<AZStd::pair<AZ::EntityId, AZ::SliceComponent::EntityRestoreInfo>>&& entityRestoreInfos)
: m_assetId(assetId)
, m_entityRestoreInfos(AZStd::move(entityRestoreInfos))
{ }
AZ::Data::AssetId m_assetId;
AZStd::vector<AZStd::pair<AZ::EntityId, AZ::SliceComponent::EntityRestoreInfo>> m_entityRestoreInfos;
};
private:
AZ::Vector2 m_contextMenuViewPoint;
AZ::Vector3 m_sliceWorldPos;
int m_inObjectPickMode;
short m_startedUndoRecordingNestingLevel; // used in OnBegin/EndUndo to ensure we only accept undo's we started recording
AzToolsFramework::SliceOverridesNotificationWindowManager* m_notificationWindowManager;
DisplayContext* m_dc;
AZStd::vector<SliceAssetDeletionErrorInfo> m_sliceAssetDeletionErrorRestoreInfos;
// Tracks new entities that have not yet been saved.
AZStd::unordered_set<AZ::EntityId> m_unsavedEntities;
const AZStd::string m_defaultComponentIconLocation = "Icons/Components/Component_Placeholder.svg";
const AZStd::string m_defaultComponentViewportIconLocation = "Icons/Components/Viewport/Component_Placeholder.png";
const AZStd::string m_defaultEntityIconLocation = "Icons/Components/Viewport/Transform.png";
bool m_debugDisplayBusImplementationActive = false;
AzToolsFramework::Prefab::PrefabIntegrationManager* m_prefabIntegrationManager = nullptr;
AzToolsFramework::EditorEntityUiInterface* m_editorEntityUiInterface = nullptr;
AzToolsFramework::Prefab::PrefabIntegrationInterface* m_prefabIntegrationInterface = nullptr;
AzToolsFramework::EditorEntityAPI* m_editorEntityAPI = nullptr;
// Overrides UI styling and behavior for Layer Entities
AzToolsFramework::LayerUiHandler m_layerUiOverrideHandler;
};
//////////////////////////////////////////////////////////////////////////
class CToolsApplicationUndoLink
: public IUndoObject
{
public:
CToolsApplicationUndoLink(const char* description)
: m_description(description)
{
}
int GetSize() override
{
return 0;
}
QString GetDescription() override
{
return m_description.c_str();
}
void Undo(bool bUndo = true) override
{
// Always run the undo even if the flag was set to false, that just means that undo wasn't expressly desired, but can be used in cases of canceling the current super undo.
// Restore previous focus after applying the undo.
QPointer<QWidget> w = QApplication::focusWidget();
AzToolsFramework::ToolsApplicationRequestBus::Broadcast(&AzToolsFramework::ToolsApplicationRequestBus::Events::UndoPressed);
// Slice the redo stack if this wasn't due to explicit undo command
if (!bUndo)
{
AzToolsFramework::ToolsApplicationRequestBus::Broadcast(&AzToolsFramework::ToolsApplicationRequestBus::Events::FlushRedo);
}
if (!w.isNull())
{
w->setFocus(Qt::OtherFocusReason);
}
}
void Redo() override
{
// Restore previous focus after applying the undo.
QPointer<QWidget> w = QApplication::focusWidget();
AzToolsFramework::ToolsApplicationRequestBus::Broadcast(&AzToolsFramework::ToolsApplicationRequestBus::Events::RedoPressed);
if (!w.isNull())
{
w->setFocus(Qt::OtherFocusReason);
}
}
AZStd::string m_description;
};
#endif // CRYINCLUDE_COMPONENTENTITYEDITORPLUGIN_SANDBOXINTEGRATION_H
@@ -0,0 +1,265 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include "ComponentEntityEditorPlugin_precompiled.h"
#include <AzCore/Component/TransformBus.h>
#include <AzCore/UnitTest/TestTypes.h>
#include <AzTest/AzTest.h>
#include <AzToolsFramework/Entity/EditorEntityHelpers.h>
#include <AzToolsFramework/Entity/EditorEntityInfoBus.h>
#include <AzToolsFramework/ToolsComponents/EditorVisibilityBus.h>
#include <AzToolsFramework/UnitTest/AzToolsFrameworkTestHelpers.h>
#include <Plugins/ComponentEntityEditorPlugin/Objects/ComponentEntityObject.h>
using namespace AzToolsFramework;
namespace UnitTest
{
class ComponentEntityObjectVisibilityFixture
: public ToolsApplicationFixture
, private EditorEntityVisibilityNotificationBus::Router
, private EditorEntityInfoNotificationBus::Handler
{
public:
void SetUpEditorFixtureImpl() override
{
EditorEntityVisibilityNotificationBus::Router::BusRouterConnect();
EditorEntityInfoNotificationBus::Handler::BusConnect();
}
void TearDownEditorFixtureImpl() override
{
EditorEntityInfoNotificationBus::Handler::BusDisconnect();
EditorEntityVisibilityNotificationBus::Router::BusRouterDisconnect();
}
TestEditorActions m_editorActions;
AZ::EntityId m_layerId;
private:
// EditorEntityVisibilityNotificationBus ...
void OnEntityVisibilityChanged(bool /*visibility*/) override
{
}
// EditorEntityInfoNotificationBus ...
void OnEntityInfoUpdatedVisibility(AZ::EntityId /*entityId*/, bool /*visible*/) override
{
}
};
TEST_F(ComponentEntityObjectVisibilityFixture, ViewportComponentEntityObjectRespectsLayerVisibility)
{
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Given
AZ::Entity* entityA = nullptr;
const AZ::EntityId a = CreateDefaultEditorEntity("A", &entityA);
AZ::Entity* entityB = nullptr;
const AZ::EntityId b = CreateDefaultEditorEntity("B", &entityB);
AZ::Entity* entityC = nullptr;
const AZ::EntityId c = CreateDefaultEditorEntity("C", &entityC);
m_layerId = CreateEditorLayerEntity("Layer");
entityA->Deactivate();
entityB->Deactivate();
entityC->Deactivate();
CComponentEntityObject componentEntityObjectA;
componentEntityObjectA.AssignEntity(entityA);
CComponentEntityObject componentEntityObjectB;
componentEntityObjectB.AssignEntity(entityB);
CComponentEntityObject componentEntityObjectC;
componentEntityObjectC.AssignEntity(entityC);
entityC->Activate();
entityB->Activate();
entityA->Activate();
AZ::TransformBus::Event(a, &AZ::TransformBus::Events::SetParent, m_layerId);
AZ::TransformBus::Event(b, &AZ::TransformBus::Events::SetParent, m_layerId);
AZ::TransformBus::Event(c, &AZ::TransformBus::Events::SetParent, m_layerId);
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
// When
SetEntityVisibility(a, false);
SetEntityVisibility(b, false);
SetEntityVisibility(c, false);
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Then
EXPECT_TRUE(componentEntityObjectA.IsHidden());
EXPECT_TRUE(componentEntityObjectB.IsHidden());
EXPECT_TRUE(componentEntityObjectC.IsHidden());
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
// When
SetEntityVisibility(a, true);
SetEntityVisibility(b, true);
SetEntityVisibility(c, true);
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Then
EXPECT_FALSE(componentEntityObjectA.IsHidden());
EXPECT_FALSE(componentEntityObjectB.IsHidden());
EXPECT_FALSE(componentEntityObjectC.IsHidden());
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
// When
SetEntityVisibility(m_layerId, false);
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Then
EXPECT_TRUE(componentEntityObjectA.IsHidden());
EXPECT_TRUE(componentEntityObjectB.IsHidden());
EXPECT_TRUE(componentEntityObjectC.IsHidden());
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
}
TEST_F(ComponentEntityObjectVisibilityFixture, ComponentEntityObjectDoesNotOverrideVisibility)
{
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Given
AZ::Entity* entityA = nullptr;
const AZ::EntityId a = CreateDefaultEditorEntity("A", &entityA);
AZ::Entity* entityB = nullptr;
const AZ::EntityId b = CreateDefaultEditorEntity("B", &entityB);
AZ::Entity* entityC = nullptr;
const AZ::EntityId c = CreateDefaultEditorEntity("C", &entityC);
m_layerId = CreateEditorLayerEntity("Layer");
entityA->Deactivate();
entityB->Deactivate();
entityC->Deactivate();
CComponentEntityObject componentEntityObjectA;
componentEntityObjectA.AssignEntity(entityA);
CComponentEntityObject componentEntityObjectB;
componentEntityObjectB.AssignEntity(entityB);
CComponentEntityObject componentEntityObjectC;
componentEntityObjectC.AssignEntity(entityC);
entityC->Activate();
entityB->Activate();
entityA->Activate();
AZ::TransformBus::Event(a, &AZ::TransformBus::Events::SetParent, m_layerId);
AZ::TransformBus::Event(b, &AZ::TransformBus::Events::SetParent, m_layerId);
AZ::TransformBus::Event(c, &AZ::TransformBus::Events::SetParent, m_layerId);
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
// When
SetEntityVisibility(m_layerId, false);
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Then
EXPECT_TRUE(componentEntityObjectA.IsHidden());
EXPECT_TRUE(componentEntityObjectB.IsHidden());
EXPECT_TRUE(componentEntityObjectC.IsHidden());
EXPECT_TRUE(IsEntitySetToBeVisible(a));
EXPECT_FALSE(IsEntityVisible(a));
EXPECT_TRUE(IsEntitySetToBeVisible(a));
EXPECT_FALSE(IsEntityVisible(b));
EXPECT_TRUE(IsEntitySetToBeVisible(a));
EXPECT_FALSE(IsEntityVisible(c));
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
}
TEST_F(ComponentEntityObjectVisibilityFixture, ViewportComponentEntityObjectRespectsLayerLock)
{
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Given
AZ::Entity* entityA = nullptr;
const AZ::EntityId a = CreateDefaultEditorEntity("A", &entityA);
AZ::Entity* entityB = nullptr;
const AZ::EntityId b = CreateDefaultEditorEntity("B", &entityB);
AZ::Entity* entityC = nullptr;
const AZ::EntityId c = CreateDefaultEditorEntity("C", &entityC);
m_layerId = CreateEditorLayerEntity("Layer");
entityA->Deactivate();
entityB->Deactivate();
entityC->Deactivate();
CComponentEntityObject componentEntityObjectA;
componentEntityObjectA.AssignEntity(entityA);
CComponentEntityObject componentEntityObjectB;
componentEntityObjectB.AssignEntity(entityB);
CComponentEntityObject componentEntityObjectC;
componentEntityObjectC.AssignEntity(entityC);
entityC->Activate();
entityB->Activate();
entityA->Activate();
AZ::TransformBus::Event(a, &AZ::TransformBus::Events::SetParent, m_layerId);
AZ::TransformBus::Event(b, &AZ::TransformBus::Events::SetParent, m_layerId);
AZ::TransformBus::Event(c, &AZ::TransformBus::Events::SetParent, m_layerId);
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
// When
SetEntityLockState(a, true);
SetEntityLockState(b, true);
SetEntityLockState(c, true);
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Then
EXPECT_TRUE(componentEntityObjectA.IsFrozen());
EXPECT_TRUE(componentEntityObjectB.IsFrozen());
EXPECT_TRUE(componentEntityObjectC.IsFrozen());
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
// When
SetEntityLockState(a, false);
SetEntityLockState(b, false);
SetEntityLockState(c, false);
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Then
EXPECT_FALSE(componentEntityObjectA.IsFrozen());
EXPECT_FALSE(componentEntityObjectB.IsFrozen());
EXPECT_FALSE(componentEntityObjectC.IsFrozen());
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
// When
SetEntityLockState(m_layerId, true);
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Then
EXPECT_TRUE(componentEntityObjectA.IsFrozen());
EXPECT_TRUE(componentEntityObjectB.IsFrozen());
EXPECT_TRUE(componentEntityObjectC.IsFrozen());
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
}
} // namespace UnitTest
@@ -0,0 +1,43 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include "ComponentEntityEditorPlugin_precompiled.h"
#include <AzTest/AzTest.h>
#include <AzCore/Memory/SystemAllocator.h>
#include <QApplication>
using namespace AZ;
// Handle asserts
class ToolsFrameworkHook
: public AZ::Test::ITestEnvironment
{
public:
void SetupEnvironment() override
{
AllocatorInstance<SystemAllocator>::Create();
}
void TeardownEnvironment() override
{
AllocatorInstance<SystemAllocator>::Destroy();
}
};
AZTEST_EXPORT int AZ_UNIT_TEST_HOOK_NAME(int argc, char** argv)
{
::testing::InitGoogleMock(&argc, argv);
QApplication app(argc, argv);
AZ::Test::printUnusedParametersWarning(argc, argv);
AZ::Test::addTestEnvironments({ new ToolsFrameworkHook });
int result = RUN_ALL_TESTS();
return result;
}
IMPLEMENT_TEST_EXECUTABLE_MAIN();
@@ -0,0 +1,758 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include "ComponentEntityEditorPlugin_precompiled.h"
#include "CryEdit.h"
#include "AssetCatalogModel.h"
#include "Objects/ComponentEntityObject.h"
#include <ISourceControl.h>
#include <IEditor.h>
#include <qevent.h>
#include <qmimedata.h>
#include <LmbrCentral/Rendering/LensFlareAsset.h>
#include <LmbrCentral/Rendering/MeshAsset.h>
#include <LmbrCentral/Rendering/MaterialAsset.h>
#include <AzCore/Memory/Memory.h>
#include <AzCore/RTTI/TypeInfo.h>
#include <AzCore/Component/ComponentApplicationBus.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/Settings/SettingsRegistryMergeUtils.h>
#include <AzCore/Slice/SliceAsset.h>
#include <AzCore/Asset/AssetManager.h>
#include <AzCore/Asset/AssetManagerBus.h>
#include <AzCore/Asset/AssetTypeInfoBus.h>
#include <AzFramework/API/ApplicationAPI.h>
#include <AzToolsFramework/API/ToolsApplicationAPI.h>
#include <AzToolsFramework/Entity/EditorEntityContextBus.h>
#include <AzToolsFramework/ToolsComponents/EditorAssetMimeDataContainer.h>
#include <AzToolsFramework/ToolsComponents/ComponentAssetMimeDataContainer.h>
#include <AzToolsFramework/ToolsComponents/ScriptEditorComponent.h>
#include <AzToolsFramework/ToolsComponents/TransformComponent.h>
#include <AzToolsFramework/Commands/EntityStateCommand.h>
#include <QTimer>
///////////////////////////////////////////////////////////////////////////////
// AssetCatalogModelWorkerThread
///////////////////////////////////////////////////////////////////////////////
AssetCatalogModelWorkerThread::AssetCatalogModelWorkerThread(AssetCatalogModel* catalog, QThread* returnThread)
: m_catalog(catalog)
, m_returnThread(returnThread)
{
connect(this, &QThread::started, this, &AssetCatalogModelWorkerThread::startJob);
connect(m_catalog, &AssetCatalogModel::LoadComplete, this, &AssetCatalogModelWorkerThread::ReturnToThread);
}
void AssetCatalogModelWorkerThread::ReturnToThread()
{
quit();
}
void AssetCatalogModelWorkerThread::startJob()
{
disconnect(this, &QThread::started, this, &AssetCatalogModelWorkerThread::startJob);
m_catalog->StartProcessingAssets();
QTimer::singleShot(0, m_catalog, &AssetCatalogModel::ProcessAssets);
}
void AssetCatalogModelWorkerThread::run()
{
exec();
disconnect(m_catalog, &AssetCatalogModel::LoadComplete, this, &AssetCatalogModelWorkerThread::ReturnToThread);
m_catalog->moveToThread(m_returnThread);
}
///////////////////////////////////////////////////////////////////////////////
// AssetCatalogEntry
///////////////////////////////////////////////////////////////////////////////
bool AssetCatalogEntry::operator<(const QStandardItem& other) const
{
// Set directories as always less than files.
bool leftIsDir = data(FolderRole).toBool();
bool rightIsDir = other.data(FolderRole).toBool();
if (leftIsDir != rightIsDir)
{
return leftIsDir;
}
QVariant leftName = data(Qt::DisplayRole);
QVariant rightName = other.data(Qt::DisplayRole);
return leftName.toString().compare(rightName.toString(), Qt::CaseInsensitive) < 0;
}
///////////////////////////////////////////////////////////////////////////////
// AssetCatalogModel
///////////////////////////////////////////////////////////////////////////////
AssetCatalogModel::AssetCatalogModel(QObject* parent)
: QStandardItemModel(parent)
, m_canProcessAssets(true)
{
AZStd::string allExtensions;
AZStd::vector<AZ::Data::AssetType> assetTypes;
// Discover all types that the Asset system recognizes.
// Create a one-to-many map that associates extensions with AssetTypes.
EBUS_EVENT(AZ::Data::AssetCatalogRequestBus, GetHandledAssetTypes, assetTypes);
for (auto type : assetTypes)
{
AZStd::vector<AZStd::string> extensions;
allExtensions.clear();
EBUS_EVENT_ID(type, AZ::AssetTypeInfoBus, GetAssetTypeExtensions, extensions);
for (int i = 0; i < extensions.size(); i++)
{
if (i > 0)
{
allExtensions += ";";
}
allExtensions += "."; // Adding dots to all extensions to be able to separate full extensions from substrings, i.e. "bin" and input"bin"dings.
allExtensions += extensions[i].c_str();
}
if (!allExtensions.empty())
{
auto existingEntry = m_extensionToAssetType.find(allExtensions);
if (existingEntry != m_extensionToAssetType.end())
{
existingEntry->second.push_back(type);
}
else
{
m_extensionToAssetType.insert(AZStd::make_pair(allExtensions, AZStd::vector<AZ::Uuid> {type}));
}
}
}
// Special cases for SimpleAssets. If these get full-fledged AssetData types, these cases can be removed.
QString textureExtensions = LmbrCentral::TextureAsset::GetFileFilter();
m_extensionToAssetType.insert(AZStd::make_pair(textureExtensions.replace("*", "").replace(" ", "").toStdString().c_str(), AZStd::vector<AZ::Uuid> { AZ::AzTypeInfo<LmbrCentral::TextureAsset>::Uuid() }));
QString materialExtensions = LmbrCentral::MaterialAsset::GetFileFilter();
m_extensionToAssetType.insert(AZStd::make_pair(materialExtensions.replace("*", "").replace(" ", "").toStdString().c_str(), AZStd::vector<AZ::Uuid> { AZ::AzTypeInfo<LmbrCentral::MaterialAsset>::Uuid() }));
QString dccMaterialExtensions = LmbrCentral::DccMaterialAsset::GetFileFilter();
m_extensionToAssetType.insert(AZStd::make_pair(dccMaterialExtensions.replace("*", "").replace(" ", "").toStdString().c_str(), AZStd::vector<AZ::Uuid> { AZ::AzTypeInfo<LmbrCentral::DccMaterialAsset>::Uuid() }));
AZ::SerializeContext* serializeContext = nullptr;
EBUS_EVENT_RESULT(serializeContext, AZ::ComponentApplicationBus, GetSerializeContext);
AZ_Assert(serializeContext, "Failed to acquire application serialize context.");
serializeContext->EnumerateDerived<AZ::Component>([this](const AZ::SerializeContext::ClassData* classData, const AZ::Uuid&) -> bool
{
if (classData->m_editData)
{
AZ::Data::AssetType assetType;
const AZ::Edit::ElementData* element = classData->m_editData->FindElementData(AZ::Edit::ClassElements::EditorData);
if (element)
{
const AZ::Edit::Attribute* assetTypeAttribute = element->FindAttribute(AZ::Edit::Attributes::PrimaryAssetType);
if (assetTypeAttribute)
{
auto* assetTypeData = azdynamic_cast<const AZ::Edit::AttributeData<AZ::Uuid>*>(assetTypeAttribute);
if (assetTypeData)
{
assetType = assetTypeData->Get(nullptr);
m_assetTypeToComponent[assetType] = classData->m_azRtti->GetTypeId();
}
}
else
{
assetType = AZ::Data::AssetType::CreateNull();
}
if (!assetType.IsNull())
{
const AZ::Edit::Attribute* iconAttribute = element->FindAttribute(AZ_CRC("Icon"));
if (iconAttribute)
{
auto* iconAttributeData = azdynamic_cast<const AZ::Edit::AttributeData<const char*>*>(iconAttribute);
if (iconAttributeData)
{
QIcon icon(iconAttributeData->Get(nullptr));
if (!icon.isNull())
{
m_assetTypeToIcon[assetType] = icon;
}
}
}
}
}
}
return true;
});
}
AssetCatalogModel::~AssetCatalogModel()
{
AzFramework::AssetCatalogEventBus::Handler::BusDisconnect();
}
AZ::Data::AssetType AssetCatalogModel::GetAssetType(QString filename) const
{
AZ::Data::AssetType returnType = AZ::Uuid::CreateNull();
// Compare file extensions with the map created from the asset database.
int dotIndex = filename.lastIndexOf('.');
if (dotIndex >= 0)
{
QString extension = filename.mid(dotIndex);
for (auto pair : m_extensionToAssetType)
{
QString qExtensions = pair.first.c_str();
if (qExtensions.indexOf(extension) >= 0)
{
if (pair.second.size() > 1)
{
// There are multiple types with this extension. Check each handler to see if they can handle this data type.
AZStd::string azFilename = filename.toStdString().c_str();
EBUS_EVENT(AzFramework::ApplicationRequests::Bus, MakePathAssetRootRelative, azFilename);
AZ::Data::AssetId assetId;
EBUS_EVENT_RESULT(assetId, AZ::Data::AssetCatalogRequestBus, GetAssetIdByPath, azFilename.c_str(), AZ::Data::s_invalidAssetType, false);
for (AZ::Uuid type : pair.second)
{
const AZ::Data::AssetHandler* handler = AZ::Data::AssetManager::Instance().GetHandler(type);
if (handler && handler->CanHandleAsset(assetId))
{
returnType = type;
break;
}
}
}
else
{
returnType = pair.second[0];
break;
}
}
}
}
return returnType;
}
QStandardItem* AssetCatalogModel::GetPath(QString& path, bool createIfNeeded, QStandardItem* parent)
{
if (!parent)
{
parent = invisibleRootItem();
}
QString cleanPath = path.replace("\\", "/");
while (cleanPath.startsWith("/"))
{
cleanPath = cleanPath.mid(1);
}
while (cleanPath.endsWith("/"))
{
cleanPath.chop(1);
}
QString currentFolder;
QString restOfPath;
int slashIdx = cleanPath.indexOf('/', 1);
if (slashIdx < 0)
{
currentFolder = cleanPath;
restOfPath.clear();
}
else
{
currentFolder = cleanPath.left(slashIdx);
restOfPath = cleanPath.mid(slashIdx + 1);
}
if (currentFolder.isEmpty())
{
return parent;
}
for (int i = 0; i < parent->rowCount(); i++)
{
QString name = parent->child(i)->data(Qt::DisplayRole).toString();
bool isFolder = parent->child(i)->data(AssetCatalogEntry::FolderRole).toBool();
if (currentFolder == name && isFolder)
{
if (restOfPath.isEmpty())
{
return parent->child(i);
}
else
{
return GetPath(restOfPath, createIfNeeded, parent->child(i));
}
}
}
if (createIfNeeded)
{
QString fullpath = parent->data(AssetCatalogEntry::FilePathRole).toString();
fullpath += currentFolder + "/";
AssetCatalogEntry* folder = new AssetCatalogEntry();
folder->setData(currentFolder, Qt::DisplayRole);
folder->setData(fullpath, AssetCatalogEntry::FilePathRole);
folder->setData(true, AssetCatalogEntry::FolderRole);
folder->setData(true, AssetCatalogEntry::VisibilityRole);
parent->appendRow(folder);
if (restOfPath.isEmpty())
{
return folder;
}
else
{
return GetPath(restOfPath, createIfNeeded, folder);
}
}
else
{
return nullptr;
}
}
AssetCatalogEntry* AssetCatalogModel::FindAsset(QString assetPath)
{
QString path;
QString asset;
// Separate file name and folder name.
int slashIdx = assetPath.lastIndexOf('/');
if (slashIdx < 0)
{
asset = assetPath;
path.clear();
}
else
{
path = assetPath.left(slashIdx);
asset = assetPath.mid(slashIdx + 1);
}
QStandardItem* folder = GetPath(path, false);
if (folder)
{
for (int i = 0; i < folder->rowCount(); i++)
{
QString name = folder->child(i)->data(Qt::DisplayRole).toString();
if (name == asset)
{
AssetCatalogEntry* entry = static_cast<AssetCatalogEntry*>(folder->child(i));
return entry;
}
}
}
return nullptr;
}
AssetCatalogEntry* AssetCatalogModel::AddAsset(QString assetPath, AZ::Data::AssetId id)
{
QString path;
QString asset;
// Separate file name and folder name.
int slashIdx = assetPath.lastIndexOf('/');
if (slashIdx < 0)
{
asset = assetPath;
path.clear();
}
else
{
path = assetPath.left(slashIdx);
asset = assetPath.mid(slashIdx + 1);
}
QRegExp mipMapExtension("\\.dds\\.\\d+a?$"); // Files that end with ".dds.#", with an optional "a"
if (asset.contains(mipMapExtension))
{
// Mip map files should be ignored by the file browser.
// This is a temporary solution until texture streams are refactored.
return nullptr;
}
QStandardItem* folder = GetPath(path, true);
QString fullPath = folder->data(AssetCatalogEntry::FilePathRole).toString() + asset;
AZ::Data::AssetType assetType = GetAssetType(fullPath);
AZ::Uuid classId = AZ::Uuid::CreateNull();
auto it = m_assetTypeToComponent.find(assetType);
if (it != m_assetTypeToComponent.end())
{
classId = it->second;
}
AssetCatalogEntry* entry = new AssetCatalogEntry();
entry->setData(asset, Qt::DisplayRole);
entry->setData(fullPath, AssetCatalogEntry::FilePathRole);
entry->setData(false, AssetCatalogEntry::FolderRole);
entry->setData(true, AssetCatalogEntry::VisibilityRole);
entry->m_assetId = id;
entry->m_assetType = assetType;
entry->m_classId = classId;
if (!assetType.IsNull())
{
auto iconIt = m_assetTypeToIcon.find(assetType);
if (iconIt == m_assetTypeToIcon.end())
{
// The m_assetTypeToIcon map was seeded with icons for known asset types.
// If we come across an asset type that is not associated with a component,
// we'll get its icon from OS if we can. This will help users recognize files more easily.
QFileInfo fileInfo(m_rootPath + fullPath);
QIcon fileIcon = m_iconProvider.icon(fileInfo);
// Now, make a deep copy for OS-provided icons. On Windows 10, there seems to be an issue with
// icons' memory being reclaimed and crashing the Editor.
QSize size = fileIcon.actualSize(QSize(16, 16));
QIcon deepCopy = fileIcon.pixmap(size).copy(0, 0, size.width(), size.height());
if (!fileIcon.isNull())
{
m_assetTypeToIcon[assetType] = deepCopy;
}
}
}
folder->appendRow(entry);
return entry;
}
AssetCatalogEntry* AssetCatalogModel::RemoveAsset(QString assetPath)
{
AssetCatalogEntry* entry = FindAsset(assetPath);
if (entry)
{
QStandardItem* parent = entry->parent();
if (parent)
{
parent->removeRow(entry->row());
AssetCatalogEntry* folder = static_cast<AssetCatalogEntry*>(parent);
return folder;
}
}
return nullptr;
}
void AssetCatalogModel::LoadDatabase()
{
clear();
AZStd::string assetRootFolder;
if (auto settingsRegistry = AZ::SettingsRegistry::Get(); settingsRegistry != nullptr)
{
settingsRegistry->Get(assetRootFolder, AZ::SettingsRegistryMergeUtils::FilePathKey_CacheRootFolder);
}
m_rootPath = assetRootFolder.c_str();
auto startCB = []() {};
auto enumerateCB = [this](const AZ::Data::AssetId id, const AZ::Data::AssetInfo& assetInfo)
{
DatabaseEntry* entry = new DatabaseEntry(id, assetInfo.m_relativePath.c_str());
m_fileCache.push_back(entry);
};
auto endCB = [this]()
{
m_fileCacheCurrentIndex = 0;
Q_EMIT UpdateProgress(0);
Q_EMIT SetTotalProgress(m_fileCache.size());
};
EBUS_EVENT(AZ::Data::AssetCatalogRequestBus, EnumerateAssets, startCB, enumerateCB, endCB);
AzFramework::AssetCatalogEventBus::Handler::BusConnect();
m_canProcessAssets = true;
}
void AssetCatalogModel::ProcessAssets()
{
if (m_fileCacheCurrentIndex >= m_fileCache.size())
{
sort(0);
m_fileCache.clear();
Q_EMIT LoadComplete();
}
else
{
for (int i = 0; m_canProcessAssets && i < ASSET_CATALOG_BATCH_SIZE && m_fileCacheCurrentIndex < m_fileCache.size(); i++, m_fileCacheCurrentIndex++)
{
AddAsset(m_fileCache[m_fileCacheCurrentIndex]->m_path, m_fileCache[m_fileCacheCurrentIndex]->m_id);
}
Q_EMIT UpdateProgress(m_fileCacheCurrentIndex);
if (m_canProcessAssets)
{
QTimer::singleShot(1, this, &AssetCatalogModel::ProcessAssets);
}
}
}
void AssetCatalogModel::StartProcessingAssets()
{
m_canProcessAssets = true;
}
void AssetCatalogModel::StopProcessingAssets()
{
m_canProcessAssets = false;
}
void AssetCatalogModel::OnCatalogAssetAdded(const AZ::Data::AssetId& assetId)
{
AZ::Data::AssetInfo assetInfo;
EBUS_EVENT_RESULT(assetInfo, AZ::Data::AssetCatalogRequestBus, GetAssetInfoById, assetId);
// note that this will get called twice, once with the real assetId and once with legacy assetId.
// we only want to add the real asset to the list, in which the assetId passed in is equal to the final assetId returned
// otherwise, you look up assetId (and its a legacy assetId) and the actual asset will be different.
if ((assetInfo.m_assetId.IsValid()) && (assetInfo.m_assetId == assetId))
{
AssetCatalogEntry* asset = AddAsset(assetInfo.m_relativePath.c_str(), assetInfo.m_assetId);
if (asset)
{
Q_EMIT itemChanged(asset);
}
}
}
void AssetCatalogModel::OnCatalogAssetRemoved(const AZ::Data::AssetId& /*assetId*/, const AZ::Data::AssetInfo& assetInfo)
{
AssetCatalogEntry* asset = RemoveAsset(assetInfo.m_relativePath.c_str());
if (asset)
{
Q_EMIT itemChanged(asset);
}
}
QVariant AssetCatalogModel::data(const QModelIndex& index, int role) const
{
QStandardItem* item = itemFromIndex(index);
if (item && role == Qt::DecorationRole)
{
AssetCatalogEntry* entry = static_cast<AssetCatalogEntry*>(item);
auto it = m_assetTypeToIcon.find(entry->m_assetType);
if (it != m_assetTypeToIcon.end())
{
return it->second;
}
bool isFolder = item->data(AssetCatalogEntry::FolderRole).toBool();
return isFolder ? m_iconProvider.icon(QFileIconProvider::Folder) : m_iconProvider.icon(QFileIconProvider::File);
}
return QStandardItemModel::data(index, role);
}
QMimeData* AssetCatalogModel::mimeData(const QModelIndexList& indexes) const
{
AssetCatalogEntry* item = static_cast<AssetCatalogEntry*>(itemFromIndex(indexes[0]));
bool isFolder = item ? item->data(AssetCatalogEntry::FolderRole).toBool() : true;
if (isFolder)
{
return new QMimeData();
}
QString fullPath = item->data(AssetCatalogEntry::FilePathRole).toString();
QMimeData* mimeData = new QMimeData;
if (!item->m_assetType.IsNull() && item->m_assetId.IsValid())
{
// This mime data is used to drag into PropertyAssetCtrl fields.
AzToolsFramework::EditorAssetMimeDataContainer mimeDataContainer;
mimeDataContainer.AddEditorAsset(item->m_assetId, item->m_assetType);
mimeDataContainer.AddToMimeData(mimeData);
// This mime data is used for spawning of entities with components and the adding of components through assets.
AzToolsFramework::ComponentAssetMimeDataContainer componentContainer;
componentContainer.AddComponentAsset(item->m_classId, item->m_assetId);
componentContainer.AddToMimeData(mimeData);
}
// Also, add the filename, for untyped fields.
QList<QUrl> urls;
urls << QUrl::fromLocalFile(fullPath);
mimeData->setUrls(urls);
return mimeData;
}
QVariant AssetCatalogModel::headerData(int section, Qt::Orientation orientation, int role) const
{
if (role == Qt::DisplayRole && section == 0 && orientation == Qt::Horizontal)
{
return tr("Assets");
}
return QAbstractItemModel::headerData(section, orientation, role);
}
void AssetCatalogModel::SearchCriteriaChanged(QStringList& criteriaList, AzToolsFramework::FilterOperatorType filterOperator)
{
BuildFilter(criteriaList, filterOperator);
InvalidateFilter();
}
void AssetCatalogModel::BuildFilter(QStringList& criteriaList, AzToolsFramework::FilterOperatorType filterOperator)
{
ClearFilterRegExp();
if (criteriaList.size() > 0)
{
QString filter, tag, text;
for (int i = 0; i < criteriaList.size(); i++)
{
AzToolsFramework::SearchCriteriaButton::SplitTagAndText(criteriaList[i], tag, text);
if (tag.isEmpty())
{
tag = "null";
}
filter = m_filtersRegExp[tag.toStdString().c_str()].pattern();
if (filterOperator == AzToolsFramework::FilterOperatorType::Or)
{
if (filter.isEmpty())
{
filter = text;
}
else
{
filter += "|" + text;
}
}
else if (filterOperator == AzToolsFramework::FilterOperatorType::And)
{
filter += "(?=.*" + text + ")"; // Using Lookaheads to produce an "and" effect.
}
SetFilterRegExp(tag.toStdString().c_str(), QRegExp(filter, Qt::CaseInsensitive));
}
}
}
void AssetCatalogModel::SetFilterRegExp(const AZStd::string& filterType, const QRegExp& regExp)
{
m_filtersRegExp[filterType] = regExp;
}
void AssetCatalogModel::ClearFilterRegExp(const AZStd::string& filterType)
{
if (filterType.empty())
{
for (auto& it : m_filtersRegExp)
{
it.second = QRegExp();
}
}
else
{
m_filtersRegExp[filterType] = QRegExp();
}
}
void AssetCatalogModel::InvalidateFilter()
{
ApplyFilter(invisibleRootItem());
}
void AssetCatalogModel::ApplyFilter(QStandardItem* parent)
{
// Set the visibility as a breadth-first search of the tree.
// This will allow us to also set our parents visible if we are visible
// without a later search overriding us.
for (int i = 0; i < parent->rowCount(); i++)
{
QStandardItem* child = parent->child(i);
if (m_filtersRegExp["name"].isEmpty())
{
child->setData(true, AssetCatalogEntry::VisibilityRole);
}
else
{
QString assetname = child->data(Qt::DisplayRole).toString();
bool matchesFilter = assetname.contains(m_filtersRegExp["name"]);
child->setData(matchesFilter, AssetCatalogEntry::VisibilityRole);
if (matchesFilter)
{
// Set all parents to visible.
QStandardItem* visiblityParent = parent;
bool isVisible = visiblityParent->data(AssetCatalogEntry::VisibilityRole).toBool();
while (!isVisible) // Checking isVisible gives us a short circuit for already visible folders.
{
visiblityParent->setData(true, AssetCatalogEntry::VisibilityRole);
visiblityParent = visiblityParent->parent();
isVisible = visiblityParent ? visiblityParent->data(AssetCatalogEntry::VisibilityRole).toBool() : true;
}
}
}
}
// Recurse through the children that are folders
for (int i = 0; i < parent->rowCount(); i++)
{
QStandardItem* child = parent->child(i);
bool isFolder = child->data(AssetCatalogEntry::FolderRole).toBool();
if (isFolder)
{
ApplyFilter(child);
}
}
}
QString AssetCatalogModel::FileName(const QModelIndex& index) const
{
QStandardItem* item = itemFromIndex(index);
if (item)
{
return item->data(Qt::DisplayRole).toString();
}
return QString();
}
QString AssetCatalogModel::FilePath(const QModelIndex& index) const
{
// filePath contains the name of the file.
QStandardItem* item = itemFromIndex(index);
if (item)
{
QString fullPath = RootPath();
fullPath += item->data(AssetCatalogEntry::FilePathRole).toString();
return fullPath;
}
return QString();
}
AssetCatalogEntry* AssetCatalogModel::AssetData(const QModelIndex& index) const
{
return static_cast<AssetCatalogEntry*>(itemFromIndex(index));
}
///////////////////////////////////////////////////////////////////////////////
// End of context menu handling
///////////////////////////////////////////////////////////////////////////////
#include <UI/moc_AssetCatalogModel.cpp>
@@ -0,0 +1,153 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#if !defined(Q_MOC_RUN)
#include <QtGui/qstandarditemmodel.h>
#include <QFileIconProvider>
#include <QThread>
#include <AzCore/Asset/AssetCommon.h>
#include <AzCore/std/containers/unordered_map.h>
#include <AzFramework/Asset/AssetCatalogBus.h>
#include <AzToolsFramework/UI/SearchWidget/SearchCriteriaWidget.hxx>
#endif
///////////////////////////////////////////////////////////////////////////////
struct DatabaseEntry
{
public:
DatabaseEntry(AZ::Data::AssetId assetID, const char *assetPath)
: m_id(assetID)
, m_path(assetPath)
{}
AZ::Data::AssetId m_id;
QString m_path;
};
///////////////////////////////////////////////////////////////////////////////
class AssetCatalogEntry
: public QStandardItem
{
public:
// This will be easier to store in the data, so that filters don't have to cast the item to get to it.
enum Roles
{
FileIconRole = Qt::DecorationRole,
FilePathRole = Qt::UserRole + 1,
VisibilityRole = Qt::UserRole + 2,
FolderRole = Qt::UserRole + 3
};
AssetCatalogEntry() {}
AZ_CLASS_ALLOCATOR(AssetCatalogEntry, AZ::SystemAllocator, 0);
bool operator<(const QStandardItem& other) const override;
public:
AZ::Data::AssetId m_assetId; ///< The unique ID of the asset in the asset database.
AZ::Data::AssetType m_assetType; ///< The type of the asset is used to validate on certain drop targets, like the PropertyAssetCtrl.
AZ::Uuid m_classId; ///< If valid, the component that should be created when this asset is dragged onto creation-capable windows.
};
///////////////////////////////////////////////////////////////////////////////
class AssetCatalogModel
: public QStandardItemModel
, public AzFramework::AssetCatalogEventBus::Handler
{
Q_OBJECT
public:
AZ_CLASS_ALLOCATOR(AssetCatalogModel, AZ::SystemAllocator, 0);
AssetCatalogModel(QObject* parent = 0);
~AssetCatalogModel() override;
QString RootPath() const { return m_rootPath; }
void LoadDatabase();
QString FileName(const QModelIndex& index) const;
QString FilePath(const QModelIndex& index) const;
AssetCatalogEntry* AssetData(const QModelIndex& index) const;
//! Finds an asset. On success, returns a pointer to the item.
//! \retrun A valid pointer on success, nullptr on fail.
AssetCatalogEntry* FindAsset(QString assetPath);
// QAbstractItemModel overrides
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
QMimeData* mimeData(const QModelIndexList& indexes) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
// AzFramework::AssetCatalogEventBus::Handler
void OnCatalogAssetAdded(const AZ::Data::AssetId& assetId) override;
void OnCatalogAssetRemoved(const AZ::Data::AssetId& assetId, const AZ::Data::AssetInfo& assetInfo) override;
Q_SIGNALS:
void LoadComplete();
void SetTotalProgress(int value);
void UpdateProgress(int value);
public Q_SLOTS:
void SearchCriteriaChanged(QStringList& criteriaList, AzToolsFramework::FilterOperatorType filterOperator);
void ProcessAssets();
void StartProcessingAssets();
void StopProcessingAssets();
protected:
//! Adds an asset and returns a pointer to that new asset.
AssetCatalogEntry* AddAsset(QString assetPath, AZ::Data::AssetId id);
//! Removes an asset. On success, returns a pointer to the parent item. On failure, returns nullptr.
AssetCatalogEntry* RemoveAsset(QString assetPath);
void BuildFilter(QStringList& criteriaList, AzToolsFramework::FilterOperatorType filterOperator);
void InvalidateFilter();
void SetFilterRegExp(const AZStd::string& filterType, const QRegExp& regExp);
void ClearFilterRegExp(const AZStd::string& filterType = AZStd::string());
AZ::Data::AssetType GetAssetType(QString filename) const;
QStandardItem* GetPath(QString& path, bool createIfNeeded, QStandardItem* parent = nullptr);
void ApplyFilter(QStandardItem* parent);
AZStd::unordered_map<AZ::Data::AssetType, QIcon> m_assetTypeToIcon;
AZStd::unordered_map<AZ::Uuid, AZ::Uuid> m_assetTypeToComponent;
AZStd::unordered_map<AZStd::string, AZStd::vector<AZ::Uuid>> m_extensionToAssetType;
QFileIconProvider m_iconProvider;
QString m_rootPath;
AzToolsFramework::FilterByCategoryMap m_filtersRegExp;
static const int ASSET_CATALOG_BATCH_SIZE = 50;
AZStd::vector<DatabaseEntry*> m_fileCache; // scratch space to get the registry data out of the AssetDatabase in quick fashion.
int m_fileCacheCurrentIndex;
bool m_canProcessAssets;
};
///////////////////////////////////////////////////////////////////////////////
class AssetCatalogModelWorkerThread
: public QThread
{
Q_OBJECT
public:
AssetCatalogModelWorkerThread(AssetCatalogModel* catalog, QThread* returnThread);
void startJob();
public Q_SLOTS:
void ReturnToThread();
protected:
void run() override;
// These are pointers that this object will not own.
QThread* m_returnThread;
AssetCatalogModel* m_catalog;
};
@@ -0,0 +1,21 @@
<RCC>
<qresource prefix="/">
<file alias="sort_a_to_z.svg">Icons/sort_a_to_z.svg</file>
<file alias="sort_manually.svg">Icons/sort_manually.svg</file>
<file alias="sort_z_to_a.svg">Icons/sort_z_to_a.svg</file>
<file alias="visibility_default.svg">Icons/visibility_default.svg</file>
<file alias="visibility_default_hover.svg">Icons/visibility_default_hover.svg</file>
<file alias="visibility_default_transparent.svg">Icons/visibility_default_transparent.svg</file>
<file alias="visibility_on.svg">Icons/visibility_on.svg</file>
<file alias="visibility_on_hover.svg">Icons/visibility_on_hover.svg</file>
<file alias="visibility_on_transparent.svg">Icons/visibility_on_transparent.svg</file>
<file alias="lock_default.svg">Icons/lock_default.svg</file>
<file alias="lock_default_hover.svg">Icons/lock_default_hover.svg</file>
<file alias="lock_default_transparent.svg">Icons/lock_default_transparent.svg</file>
<file alias="lock_on.svg">Icons/lock_on.svg</file>
<file alias="lock_on_hover.svg">Icons/lock_on_hover.svg</file>
<file alias="lock_on_transparent.svg">Icons/lock_on_transparent.svg</file>
</qresource>
</RCC>
@@ -0,0 +1,98 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include "ComponentEntityEditorPlugin_precompiled.h"
#include "CategoriesList.h"
ComponentCategoryList::ComponentCategoryList(QWidget* parent /*= nullptr*/)
: QTreeWidget(parent)
{
}
void ComponentCategoryList::Init()
{
setColumnCount(1);
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
setDragDropMode(QAbstractItemView::DragDropMode::DragOnly);
setDragEnabled(true);
setSelectionMode(QAbstractItemView::ExtendedSelection);
setAllColumnsShowFocus(true);
setStyleSheet("QTreeWidget { selection-background-color: rgba(255,255,255,0.2); }");
QStringList headers;
headers << tr("Categories");
setHeaderLabels(headers);
const QString parentCategoryIconPath = QString("Icons/PropertyEditor/Browse_on.png");
const QString categoryIconPath = QString("Icons/PropertyEditor/Browse.png");
QTreeWidgetItem* allCategory = new QTreeWidgetItem(this);
allCategory->setText(0, "All");
allCategory->setIcon(0, QIcon(categoryIconPath));
// Need this briefly to collect the list of available categories.
ComponentDataModel dataModel(this);
for (const auto& cat : dataModel.GetCategories())
{
QString categoryString = QString(cat.c_str());
QStringList categories = categoryString.split('/', Qt::SkipEmptyParts);
QTreeWidgetItem* parent = nullptr;
QTreeWidgetItem* categoryWidget = nullptr;
for (const auto& categoryName : categories)
{
if (parent)
{
categoryWidget = new QTreeWidgetItem(parent);
categoryWidget->setIcon(0, QIcon(categoryIconPath));
// Store the full category path in a user role because we'll need it to locate the actual category
categoryWidget->setData(0, Qt::UserRole, QVariant::fromValue(categoryString));
}
else
{
auto existingCategory = findItems(categoryName, Qt::MatchExactly);
if (existingCategory.empty())
{
categoryWidget = new QTreeWidgetItem(this);
categoryWidget->setIcon(0, QIcon(parentCategoryIconPath));
}
else
{
categoryWidget = static_cast<QTreeWidgetItem*>(existingCategory.first());
categoryWidget->setIcon(0, QIcon(parentCategoryIconPath));
}
}
parent = categoryWidget;
categoryWidget->setText(0, categoryName);
}
}
expandAll();
connect(this, &QTreeWidget::itemClicked, this, &ComponentCategoryList::OnItemClicked);
}
void ComponentCategoryList::OnItemClicked(QTreeWidgetItem* item, int /*column*/)
{
QVariant userData = item->data(0, Qt::UserRole);
if (userData.isValid())
{
// Send in the full category path, not just the child category name
emit OnCategoryChange(userData.value<QString>().toStdString().c_str());
}
else
{
emit OnCategoryChange(item->text(0).toStdString().c_str());
}
}
#include <UI/ComponentPalette/moc_CategoriesList.cpp>
@@ -0,0 +1,38 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#if !defined(Q_MOC_RUN)
#include "ComponentDataModel.h"
#include <QTreeWidget>
#endif
//! ComponentCategoryList
//! Provides a list of all reflected categories that users can select for quick
//! filtering the filtered component list.
class ComponentCategoryList : public QTreeWidget
{
Q_OBJECT
public:
AZ_CLASS_ALLOCATOR(ComponentCategoryList, AZ::SystemAllocator, 0);
explicit ComponentCategoryList(QWidget* parent = nullptr);
void Init();
Q_SIGNALS:
void OnCategoryChange(const char* category);
protected:
// Will emit OnCategoryChange signal
void OnItemClicked(QTreeWidgetItem* item, int column);
};
@@ -0,0 +1,548 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include "ComponentEntityEditorPlugin_precompiled.h"
#include "ComponentDataModel.h"
#include "Include/IObjectManager.h"
#include "Objects/SelectionGroup.h"
#include <AzCore/Component/Entity.h>
#include <AzCore/Component/ComponentApplicationBus.h>
#include <AzCore/Component/Component.h>
#include <AzCore/Serialization/EditContext.h>
#include <AzToolsFramework/API/ToolsApplicationAPI.h>
#include <AzToolsFramework/ToolsComponents/ComponentMimeData.h>
#include <AzToolsFramework/Entity/EditorEntityContextBus.h>
#include <AzToolsFramework/Commands/EntityStateCommand.h>
#include <AzToolsFramework/ToolsComponents/TransformComponent.h>
#include <AzToolsFramework/API/EntityCompositionRequestBus.h>
#include <AzToolsFramework/Entity/EditorEntityHelpers.h>
#include <Editor/IEditor.h>
#include <Editor/Viewport.h>
#include <Editor/ViewManager.h>
#include <CryCommon/MathConversion.h>
#include <AzQtComponents/DragAndDrop/ViewportDragAndDrop.h>
#include <QMimeData>
namespace
{
// This is a helper function that given an object that derives from QAbstractItemModel,
// it will request the model's "ClassDataRole" class data for an entry and use that
// information to create a new entity with the selected components.
AZ::EntityId CreateEntityFromSelection(const QModelIndexList& selection, QAbstractItemModel* model)
{
AZ::Vector3 position = AZ::Vector3::CreateZero();
CViewport *view = GetIEditor()->GetViewManager()->GetGameViewport();
int width, height;
view->GetDimensions(&width, &height);
position = LYVec3ToAZVec3(view->ViewToWorld(QPoint(width / 2, height / 2)));
AZ::EntityId newEntityId;
EBUS_EVENT_RESULT(newEntityId, AzToolsFramework::EditorRequests::Bus, CreateNewEntityAtPosition, position, AZ::EntityId());
if (newEntityId.IsValid())
{
// Add all the selected components.
AZ::ComponentTypeList componentsToAdd;
for (auto index : selection)
{
// We only need to consider the first column, it's important that the data() function that
// returns ComponentDataModel::ClassDataRole also does so for the first column.
if (index.column() != 0)
{
continue;
}
QVariant classDataVariant = model->data(index, ComponentDataModel::ClassDataRole);
if (classDataVariant.isValid())
{
const AZ::SerializeContext::ClassData* classData = reinterpret_cast<const AZ::SerializeContext::ClassData*>(classDataVariant.value<void*>());
componentsToAdd.push_back(classData->m_typeId);
}
}
AzToolsFramework::EntityCompositionRequestBus::Broadcast(&AzToolsFramework::EntityCompositionRequests::AddComponentsToEntities, AzToolsFramework::EntityIdList{ newEntityId }, componentsToAdd);
return newEntityId;
}
return AZ::EntityId();
}
}
namespace ComponentDataUtilities
{
// This is a helper function to add the specified components to the selected entities, it relies on the provided
// QAbstractItemModel to determine the appropriate ClassData to use to create the components (given that some widgets
// may provide proxy models that alter the order).
void AddComponentsToSelectedEntities(const QModelIndexList& selectedComponents, QAbstractItemModel* model)
{
AzToolsFramework::EntityIdList selectedEntities;
AzToolsFramework::ToolsApplicationRequestBus::BroadcastResult(selectedEntities, &AzToolsFramework::ToolsApplicationRequests::GetSelectedEntities);
if (selectedEntities.empty())
{
return;
}
// Add all the selected components.
AZ::ComponentTypeList componentsToAdd;
for (auto index : selectedComponents)
{
// We only need to consider the first column, it's important that the data() function that
// returns ComponentDataModel::ClassDataRole also does so for the first column.
if (index.column() != 0)
{
continue;
}
QVariant classDataVariant = model->data(index, ComponentDataModel::ClassDataRole);
if (classDataVariant.isValid())
{
const AZ::SerializeContext::ClassData* classData = reinterpret_cast<const AZ::SerializeContext::ClassData*>(classDataVariant.value<void*>());
componentsToAdd.push_back(classData->m_typeId);
}
}
AzToolsFramework::EntityCompositionRequestBus::Broadcast(&AzToolsFramework::EntityCompositionRequests::AddComponentsToEntities, selectedEntities, componentsToAdd);
}
}
// ComponentDataModel
//////////////////////////////////////////////////////////////////////////
ComponentDataModel::ComponentDataModel(QObject* parent)
: QAbstractTableModel(parent)
{
AZ::SerializeContext* serializeContext = nullptr;
EBUS_EVENT_RESULT(serializeContext, AZ::ComponentApplicationBus, GetSerializeContext);
AZ_Assert(serializeContext, "Failed to acquire application serialize context.");
serializeContext->EnumerateDerived<AZ::Component>([this](const AZ::SerializeContext::ClassData* classData, const AZ::Uuid&) -> bool
{
bool allowed = false;
bool hidden = false;
AZStd::string category = "Miscellaneous";
if (classData->m_editData)
{
for (const AZ::Edit::ElementData& element : classData->m_editData->m_elements)
{
if (element.m_elementId == AZ::Edit::ClassElements::EditorData)
{
AZStd::string iconPath;
EBUS_EVENT_RESULT(iconPath, AzToolsFramework::EditorRequests::Bus, GetComponentEditorIcon, classData->m_typeId, nullptr);
if (!iconPath.empty())
{
m_componentIcons[classData->m_typeId] = QIcon(iconPath.c_str());
}
for (const AZ::Edit::AttributePair& attribPair : element.m_attributes)
{
if (attribPair.first == AZ::Edit::Attributes::AppearsInAddComponentMenu)
{
if (auto data = azdynamic_cast<AZ::Edit::AttributeData<AZ::Crc32>*>(attribPair.second))
{
if (data->Get(nullptr) == AZ_CRC("Game"))
{
allowed = true;
}
}
}
else if (attribPair.first == AZ::Edit::Attributes::AddableByUser)
{
// skip this component if user is not allowed to add it directly
if (auto data = azdynamic_cast<AZ::Edit::AttributeData<bool>*>(attribPair.second))
{
if (!data->Get(nullptr))
{
hidden = true;
}
}
}
else if (attribPair.first == AZ::Edit::Attributes::Category)
{
if (auto data = azdynamic_cast<AZ::Edit::AttributeData<const char*>*>(attribPair.second))
{
category = data->Get(nullptr);
}
}
}
break;
}
}
}
if (allowed && !hidden)
{
m_componentList.push_back(classData);
m_componentMap[category].push_back(classData);
m_categories.insert(category);
}
return true;
});
// we'd like viewport events
AzQtComponents::DragAndDropEventsBus::Handler::BusConnect(AzQtComponents::DragAndDropContexts::EditorViewport);
}
ComponentDataModel::~ComponentDataModel()
{
AzQtComponents::DragAndDropEventsBus::Handler::BusDisconnect();
}
Qt::ItemFlags ComponentDataModel::flags([[maybe_unused]] const QModelIndex &index) const
{
return Qt::ItemFlags(
Qt::ItemIsEnabled |
Qt::ItemIsDragEnabled |
Qt::ItemIsDropEnabled |
Qt::ItemIsSelectable);
}
const AZ::SerializeContext::ClassData* ComponentDataModel::GetClassData(const QModelIndex& index) const
{
int row = index.row();
if (row < 0 || row >= m_componentList.size())
{
return nullptr;
}
return m_componentList[row];
}
const char* ComponentDataModel::GetCategory(const AZ::SerializeContext::ClassData* classData)
{
if (classData)
{
if (auto editorDataElement = classData->m_editData->FindElementData(AZ::Edit::ClassElements::EditorData))
{
if (auto categoryAttribute = editorDataElement->FindAttribute(AZ::Edit::Attributes::Category))
{
if (auto categoryData = azdynamic_cast<AZ::Edit::AttributeData<const char*>*>(categoryAttribute))
{
const char* result = categoryData->Get(nullptr);
if (result)
{
return result;
}
}
}
}
}
return "";
}
QModelIndex ComponentDataModel::index(int row, int column, const QModelIndex &parent /*= QModelIndex()*/) const
{
if (row >= rowCount(parent) || column >= columnCount(parent))
{
return QModelIndex();
}
return createIndex(row, column, (void*)(m_componentList[row]));
}
QModelIndex ComponentDataModel::parent([[maybe_unused]] const QModelIndex &child) const
{
return QModelIndex();
}
int ComponentDataModel::rowCount([[maybe_unused]] const QModelIndex &parent /*= QModelIndex()*/) const
{
return m_componentList.size();
}
int ComponentDataModel::columnCount([[maybe_unused]] const QModelIndex &parent /*= QModelIndex()*/) const
{
return ColumnIndex::Count;
}
QVariant ComponentDataModel::data(const QModelIndex &index, int role /*= Qt::DisplayRole*/) const
{
if (index.isValid())
{
const AZ::SerializeContext::ClassData* classData = m_componentList[index.row()];
if (!classData)
{
return QVariant();
}
switch (role)
{
case ClassDataRole:
if (index.column() == 0) // Only get data for one column
{
return QVariant::fromValue<void*>(reinterpret_cast<void*>(const_cast<AZ::SerializeContext::ClassData*>(classData)));
}
break;
case Qt::DisplayRole:
{
if (index.column() == ColumnIndex::Name)
{
return QVariant(classData->m_editData->m_name);
}
else
if (index.column() == ColumnIndex::Category)
{
if (auto editorDataElement = classData->m_editData->FindElementData(AZ::Edit::ClassElements::EditorData))
{
if (auto categoryAttribute = editorDataElement->FindAttribute(AZ::Edit::Attributes::Category))
{
if (auto categoryData = azdynamic_cast<const AZ::Edit::AttributeData<const char*>*>(categoryAttribute))
{
return QVariant(categoryData->Get(nullptr));
}
}
}
}
}
break;
case Qt::ToolTipRole:
{
return QVariant(classData->m_editData->m_description);
}
case Qt::DecorationRole:
{
if (index.column() == ColumnIndex::Icon)
{
auto iconIterator = m_componentIcons.find(classData->m_typeId);
if (iconIterator != m_componentIcons.end())
{
return iconIterator->second;
}
}
}
break;
default:
break;
}
}
return QVariant();
}
QMimeData* ComponentDataModel::mimeData(const QModelIndexList& indices) const
{
QModelIndexList list;
// Filter out columns we are not interested in.
for (const QModelIndex& index : indices)
{
if (index.column() == 0)
{
list.push_back(index);
}
}
AZStd::vector<const AZ::SerializeContext::ClassData*> sortedList;
for (QModelIndex index : list)
{
QVariant classDataVariant = index.data(ComponentDataModel::ClassDataRole);
if (classDataVariant.isValid())
{
const AZ::SerializeContext::ClassData* classData = reinterpret_cast<const AZ::SerializeContext::ClassData*>(classDataVariant.value<void*>());
sortedList.push_back(classData);
}
}
QMimeData* mimeData = nullptr;
if (!sortedList.empty())
{
mimeData = AzToolsFramework::ComponentTypeMimeData::Create(sortedList).release();
}
return mimeData;
}
bool ComponentDataModel::CanAcceptDragAndDropEvent(QDropEvent* event, AzQtComponents::DragAndDropContextBase& context) const
{
using namespace AzToolsFramework;
using namespace AzQtComponents;
// if a listener with a higher priority already claimed this event, do not touch it.
if ((!event) || (event->isAccepted()) || (!event->mimeData()))
{
return false;
}
ViewportDragContext* contextVP = azrtti_cast<ViewportDragContext*>(&context);
if (!contextVP)
{
// not a viewport event. This is for some other GUI such as the main window itself.
return false;
}
AZStd::vector<const AZ::SerializeContext::ClassData*> componentClassDataList;
return AzToolsFramework::ComponentTypeMimeData::Get(event->mimeData(), componentClassDataList);
}
void ComponentDataModel::DragEnter(QDragEnterEvent* event, AzQtComponents::DragAndDropContextBase& context)
{
if (CanAcceptDragAndDropEvent(event, context))
{
event->setDropAction(Qt::CopyAction);
event->setAccepted(true);
// opportunities to show special highlights, or ghosted entities or previews here.
}
}
void ComponentDataModel::DragMove(QDragMoveEvent* event, AzQtComponents::DragAndDropContextBase& context)
{
if (CanAcceptDragAndDropEvent(event, context))
{
event->setDropAction(Qt::CopyAction);
event->setAccepted(true);
// opportunities to update special highlights, or ghosted entities or previews here.
}
}
void ComponentDataModel::DragLeave(QDragLeaveEvent* /*event*/)
{
// opportunities to remove ghosted entities or previews here.
}
void ComponentDataModel::Drop(QDropEvent* event, AzQtComponents::DragAndDropContextBase& context)
{
using namespace AzToolsFramework;
using namespace AzQtComponents;
// ALWAYS CHECK - you are not the only one connected to this bus, and someone else may have already
// handled the event or accepted the drop - it might not contain types relevant to you.
// you still get informed about the drop event in case you did some stuff in your gui and need to clean it up.
if (!CanAcceptDragAndDropEvent(event, context))
{
return;
}
// note that the above call already checks all the pointers such as event, or whether context is a VP context, mimetype, etc
ViewportDragContext* contextVP = azrtti_cast<ViewportDragContext*>(&context);
// we don't get given this action by Qt unless we already returned accepted from one of the other ones (such as drag move of drag enter)
event->setDropAction(Qt::CopyAction);
event->setAccepted(true);
AzToolsFramework::ScopedUndoBatch undo("Create entity from components");
const AZStd::string name = AZStd::string::format("Entity%d", GetIEditor()->GetObjectManager()->GetObjectCount());
AZ::Entity* newEntity = aznew AZ::Entity(name.c_str());
if (newEntity)
{
AzToolsFramework::EditorEntityContextRequestBus::Broadcast(&AzToolsFramework::EditorEntityContextRequests::AddRequiredComponents, *newEntity);
auto* transformComponent = newEntity->FindComponent<AzToolsFramework::Components::TransformComponent>();
if (transformComponent)
{
transformComponent->SetWorldTM(AZ::Transform::CreateTranslation(contextVP->m_hitLocation));
}
// Add the entity to the editor context, which activates it and creates the sandbox object.
AzToolsFramework::EditorEntityContextRequestBus::Broadcast(&AzToolsFramework::EditorEntityContextRequests::AddEditorEntity, newEntity);
// Prepare undo command last so it captures the final state of the entity.
AzToolsFramework::EntityCreateCommand* command = aznew AzToolsFramework::EntityCreateCommand(static_cast<AZ::u64>(newEntity->GetId()));
command->Capture(newEntity);
command->SetParent(undo.GetUndoBatch());
// Only need to add components to the new entity
AzToolsFramework::EntityIdList entities = { newEntity->GetId() };
AZStd::vector<const AZ::SerializeContext::ClassData*> componentClassDataList;
AzToolsFramework::ComponentTypeMimeData::Get(event->mimeData(), componentClassDataList);
AZ::ComponentTypeList componentsToAdd;
for (auto classData : componentClassDataList)
{
if (!classData)
{
continue;
}
componentsToAdd.push_back(classData->m_typeId);
}
AzToolsFramework::EntityCompositionRequests::AddComponentsOutcome addedComponentsResult = AZ::Failure(AZStd::string("Failed to call AddComponentsToEntities on EntityCompositionRequestBus"));
AzToolsFramework::EntityCompositionRequestBus::BroadcastResult(addedComponentsResult, &AzToolsFramework::EntityCompositionRequests::AddComponentsToEntities, entities, componentsToAdd);
ToolsApplicationRequests::Bus::Broadcast(&ToolsApplicationRequests::AddDirtyEntity, newEntity->GetId());
AzToolsFramework::ToolsApplicationRequestBus::Broadcast(&AzToolsFramework::ToolsApplicationRequests::SetSelectedEntities, entities);
}
}
AZ::EntityId ComponentDataProxyModel::NewEntityFromSelection(const QModelIndexList& selection)
{
return CreateEntityFromSelection(selection, this);
}
AZ::EntityId ComponentDataModel::NewEntityFromSelection(const QModelIndexList& selection)
{
return CreateEntityFromSelection(selection, this);
}
bool ComponentDataProxyModel::filterAcceptsRow(int sourceRow, [[maybe_unused]] const QModelIndex &sourceParent) const
{
if (m_selectedCategory.empty() && !filterRegExp().isValid())
return true;
ComponentDataModel* dataModel = static_cast<ComponentDataModel*>(sourceModel());
if (sourceRow < 0 || sourceRow >= dataModel->GetComponents().size())
{
return false;
}
const AZ::SerializeContext::ClassData* classData = dataModel->GetComponents()[sourceRow];
if (!classData)
{
return false;
}
// Get Category
if (!m_selectedCategory.empty())
{
AZStd::string currentCateogry = ComponentDataModel::GetCategory(classData);
if (AzFramework::StringFunc::Find(currentCateogry.c_str(), m_selectedCategory.c_str()))
{
return false;
}
}
if (filterRegExp().isValid())
{
QString componentName = QString::fromUtf8(classData->m_editData->m_name);
return componentName.contains(filterRegExp());
}
return true;
}
void ComponentDataProxyModel::SetSelectedCategory(const AZStd::string& category)
{
m_selectedCategory = category;
invalidate();
}
void ComponentDataProxyModel::ClearSelectedCategory()
{
m_selectedCategory.clear();
invalidate();
}
#include "UI/ComponentPalette/moc_ComponentDataModel.cpp"
@@ -0,0 +1,127 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#if !defined(Q_MOC_RUN)
#include <QAbstractTableModel>
#include <QSortFilterProxyModel>
#include <AzCore/EBus/EBus.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/Component/EntityId.h>
#include <AzCore/std/containers/set.h>
#include <AzCore/std/containers/vector.h>
#include <AzQtComponents/Buses/DragAndDrop.h>
#endif
namespace ComponentDataUtilities
{
// Given a list of selected components, use the provided model to get the components to add to any selected entities.
void AddComponentsToSelectedEntities(const QModelIndexList& selectedComponents, QAbstractItemModel* model);
}
class CViewport;
//! ComponentDataModel
//! Holds the data required to display components in a table, this includes component name, categories, icons.
class ComponentDataModel
: public QAbstractTableModel
, protected AzQtComponents::DragAndDropEventsBus::Handler // its okay if more than one of these is installed, the first one gets it.
{
Q_OBJECT
public:
AZ_CLASS_ALLOCATOR(ComponentDataModel, AZ::SystemAllocator, 0);
using ComponentClassList = AZStd::vector<const AZ::SerializeContext::ClassData*>;
using ComponentCategorySet = AZStd::set<AZStd::string>;
using ComponentClassMap = AZStd::unordered_map<AZStd::string, AZStd::vector<const AZ::SerializeContext::ClassData*>>;
using ComponentIconMap = AZStd::unordered_map<AZ::Uuid, QIcon>;
enum ColumnIndex
{
Icon,
Category,
Name,
Count
};
enum CustomRoles
{
ClassDataRole = Qt::UserRole + 1
};
ComponentDataModel(QObject* parent = nullptr);
~ComponentDataModel() override;
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
QModelIndex parent(const QModelIndex &child) const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
Qt::ItemFlags flags(const QModelIndex &index) const override;
QMimeData* mimeData(const QModelIndexList& indexes) const override;
const AZ::SerializeContext::ClassData* GetClassData(const QModelIndex&) const;
AZ::EntityId NewEntityFromSelection(const QModelIndexList& selection);
static const char* GetCategory(const AZ::SerializeContext::ClassData* classData);
ComponentClassList& GetComponents() { return m_componentList; }
ComponentCategorySet& GetCategories() { return m_categories; }
protected:
//////////////////////////////////////////////////////////////////////////
// AzQtComponents::DragAndDropEventsBus::Handler
//////////////////////////////////////////////////////////////////////////
void DragEnter(QDragEnterEvent* event, AzQtComponents::DragAndDropContextBase& context) override;
void DragMove(QDragMoveEvent* event, AzQtComponents::DragAndDropContextBase& context) override;
void DragLeave(QDragLeaveEvent* event) override;
void Drop(QDropEvent* event, AzQtComponents::DragAndDropContextBase& context) override;
bool CanAcceptDragAndDropEvent(QDropEvent* event, AzQtComponents::DragAndDropContextBase& context) const;
ComponentClassList m_componentList;
ComponentClassMap m_componentMap;
ComponentIconMap m_componentIcons;
ComponentCategorySet m_categories;
};
//! ComponentDataProxyModel
//! FilterProxy for the ComponentDataModel is used along with the search criteria to filter the
//! list of components based on tags and/or selected category.
class ComponentDataProxyModel : public QSortFilterProxyModel
{
Q_OBJECT
public:
AZ_CLASS_ALLOCATOR(ComponentDataProxyModel, AZ::SystemAllocator, 0);
ComponentDataProxyModel(QObject* parent = nullptr)
: QSortFilterProxyModel(parent)
{}
// Creates a new entity and adds the selected components to it.
// It is specialized here to ensure it uses the correct indices according to the sorted data.
AZ::EntityId NewEntityFromSelection(const QModelIndexList& selection);
// Filters rows according to the specifed tags and/or selected category
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
// Set the category to filter by.
void SetSelectedCategory(const AZStd::string& category);
void ClearSelectedCategory();
protected:
AZStd::string m_selectedCategory;
};
@@ -0,0 +1,65 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#include <AzCore/base.h>
#include <AzCore/Memory/SystemAllocator.h>
#include <AzCore/RTTI/ReflectContext.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/UserSettings/UserSettings.h>
//=============================================================================
class ComponentPaletteSettings
: public AZ::UserSettings
{
public:
AZ_CLASS_ALLOCATOR(ComponentPaletteSettings, AZ::SystemAllocator, 0);
AZ_RTTI(ComponentPaletteSettings, "{BAC3BABA-6DF1-4EEE-AFF1-6A84AD1820A1}", AZ::UserSettings);
AZStd::vector<AZ::Uuid> m_favorites;
void SetFavorites(AZStd::vector<AZ::Uuid>&& componentIds)
{
m_favorites = AZStd::move(componentIds);
}
void RemoveFavorites(const AZStd::vector<AZ::Uuid>& componentIds)
{
for (const AZ::Uuid& componentId : componentIds)
{
auto favoriteIterator = AZStd::find(m_favorites.begin(), m_favorites.end(), componentId);
AZ_Assert(favoriteIterator != m_favorites.end(), "Component Palette Favorite not found.");
if (favoriteIterator != m_favorites.end())
{
m_favorites.erase(favoriteIterator);
}
}
}
static const char* GetSettingsFile()
{
static const char* settingsFile("@user@/editor/componentpalette.usersettings");
return settingsFile;
}
static void Reflect(AZ::ReflectContext* context)
{
AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context);
if (serializeContext)
{
serializeContext->Class<ComponentPaletteSettings>()
->Version(1)
->Field("m_favorites", &ComponentPaletteSettings::m_favorites)
;
}
}
};
@@ -0,0 +1,116 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include "ComponentEntityEditorPlugin_precompiled.h"
#include "ComponentPaletteWindow.h"
#include "ComponentDataModel.h"
#include "FavoriteComponentList.h"
#include "FilteredComponentList.h"
#include "CategoriesList.h"
#include <LyViewPaneNames.h>
#include <AzCore/Component/Component.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/Serialization/EditContext.h>
#include <AzToolsFramework/UI/SearchWidget/SearchCriteriaWidget.hxx>
#include <AzToolsFramework/ToolsComponents/ComponentMimeData.h>
#include <AzCore/Component/ComponentApplicationBus.h>
#include <AzToolsFramework/API/ToolsApplicationAPI.h>
#include <AzToolsFramework/API/ViewPaneOptions.h>
#include <QLabel>
ComponentPaletteWindow::ComponentPaletteWindow(QWidget* parent)
: QMainWindow(parent)
{
Init();
}
void ComponentPaletteWindow::Init()
{
layout()->setSizeConstraint(QLayout::SetMinimumSize);
QVBoxLayout* layout = new QVBoxLayout();
layout->setSizeConstraint(QLayout::SetMinimumSize);
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(0);
QHBoxLayout* gridLayout = new QHBoxLayout(NULL);
gridLayout->setSizeConstraint(QLayout::SetMaximumSize);
gridLayout->setContentsMargins(0, 0, 0, 0);
gridLayout->setSpacing(0);
m_filterWidget = new AzToolsFramework::SearchCriteriaWidget(this);
QStringList tags;
tags << tr("name");
m_filterWidget->SetAcceptedTags(tags, tags[0]);
layout->addLayout(gridLayout, 1);
// Left Panel
QVBoxLayout* leftPaneLayout = new QVBoxLayout(this);
// Favorites
leftPaneLayout->addWidget(new QLabel(tr("Favorites")));
leftPaneLayout->addWidget(new QLabel(tr("Drag components here to add favorites.")));
FavoritesList* favorites = new FavoritesList();
favorites->Init();
leftPaneLayout->addWidget(favorites);
// Categories
m_categoryListWidget = new ComponentCategoryList();
m_categoryListWidget->Init();
leftPaneLayout->addWidget(m_categoryListWidget);
gridLayout->addLayout(leftPaneLayout);
// Right Panel
QVBoxLayout* rightPanelLayout = new QVBoxLayout(this);
gridLayout->addLayout(rightPanelLayout);
// Component list
m_componentListWidget = new FilteredComponentList(this);
m_componentListWidget->Init();
rightPanelLayout->addWidget(new QLabel(tr("Components")));
rightPanelLayout->addWidget(m_filterWidget, 0, Qt::AlignTop);
rightPanelLayout->addWidget(m_componentListWidget);
// The main window
QWidget* window = new QWidget();
window->setLayout(layout);
setCentralWidget(window);
connect(m_categoryListWidget, &ComponentCategoryList::OnCategoryChange, m_componentListWidget, &FilteredComponentList::SetCategory);
connect(m_filterWidget, &AzToolsFramework::SearchCriteriaWidget::SearchCriteriaChanged, m_componentListWidget, &FilteredComponentList::SearchCriteriaChanged);
}
void ComponentPaletteWindow::keyPressEvent(QKeyEvent* event)
{
if (event->modifiers().testFlag(Qt::ControlModifier) && event->key() == Qt::Key_F)
{
m_filterWidget->SelectTextEntryBox();
}
else
{
QMainWindow::keyPressEvent(event);
}
}
void ComponentPaletteWindow::RegisterViewClass()
{
using namespace AzToolsFramework;
ViewPaneOptions options;
options.canHaveMultipleInstances = true;
RegisterViewPane<ComponentPaletteWindow>("Component Palette", LyViewPane::CategoryOther, options);
}
#include <UI/ComponentPalette/moc_ComponentPaletteWindow.cpp>
@@ -0,0 +1,56 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#if !defined(Q_MOC_RUN)
#include <QMainWindow>
#endif
namespace AzToolsFramework
{
class SearchCriteriaWidget;
}
class ComponentCategoryList;
class FilteredComponentList;
class ComponentDataModel;
//! ComponentPaletteWindow
//! Provides a window with controls related to the Component Entity system. It provides an intuitive and organized
//! set of controls to display, sort, filter components. It provides mechanisms for creating entities by dragging
//! and dropping components into the viewport as well as from context menus.
class ComponentPaletteWindow
: public QMainWindow
{
Q_OBJECT
public:
explicit ComponentPaletteWindow(QWidget* parent = 0);
void Init();
static const GUID& GetClassID()
{
// {4236998F-1138-466D-9DF5-6533BFA1DFCA}
static const GUID guid =
{
0x4236998F, 0x1138, 0x466D, { 0x9D, 0xF5, 0x65, 0x33, 0xBF, 0xA1, 0xDF, 0xCA }
};
return guid;
}
static void RegisterViewClass();
protected:
ComponentCategoryList* m_categoryListWidget;
FilteredComponentList* m_componentListWidget;
AzToolsFramework::SearchCriteriaWidget* m_filterWidget;
void keyPressEvent(QKeyEvent* event) override;
};
@@ -0,0 +1,393 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include "ComponentEntityEditorPlugin_precompiled.h"
#include "FavoriteComponentList.h"
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/Serialization/EditContext.h>
#include <AzToolsFramework/API/ToolsApplicationAPI.h>
#include <Editor/CryEditDoc.h>
#include <Editor/ViewManager.h>
#include <QHeaderView>
#include <QMimeData>
// FavoritesList
//////////////////////////////////////////////////////////////////////////
FavoritesList::FavoritesList(QWidget* parent /*= nullptr*/)
: FilteredComponentList(parent)
{
}
FavoritesList::~FavoritesList()
{
FavoriteComponentListRequestBus::Handler::BusDisconnect();
}
void FavoritesList::Init()
{
FavoriteComponentListRequestBus::Handler::BusConnect();
FavoritesDataModel* favoritesDataModel = new FavoritesDataModel(this);
setModel(favoritesDataModel);
horizontalHeader()->setSectionResizeMode(ComponentDataModel::ColumnIndex::Name, QHeaderView::Stretch);
setShowGrid(false);
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
setSelectionMode(QAbstractItemView::SelectionMode::ExtendedSelection);
setStyleSheet("QTableView { selection-background-color: rgba(255,255,255,0.2); }");
setGridStyle(Qt::PenStyle::NoPen);
verticalHeader()->hide();
horizontalHeader()->hide();
setSelectionBehavior(QAbstractItemView::SelectionBehavior::SelectRows);
setShowGrid(false);
setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel);
setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
setColumnWidth(ComponentDataModel::ColumnIndex::Icon, 32);
hideColumn(ComponentDataModel::ColumnIndex::Category);
setDragDropMode(QAbstractItemView::DragDrop);
setAcceptDrops(true);
horizontalHeader()->setSectionResizeMode(ComponentDataModel::ColumnIndex::Icon, QHeaderView::ResizeToContents);
setColumnWidth(ComponentDataModel::ColumnIndex::Icon, 32);
horizontalHeader()->setSectionResizeMode(ComponentDataModel::ColumnIndex::Category, QHeaderView::Stretch);
setColumnWidth(ComponentDataModel::ColumnIndex::Category, 90);
// Context menu
setContextMenuPolicy(Qt::CustomContextMenu);
connect(this, &QWidget::customContextMenuRequested, this, &FavoritesList::ShowContextMenu);
}
void FavoritesList::ShowContextMenu(const QPoint& pos)
{
// Only show if a level is loaded
if (!GetIEditor() || GetIEditor()->IsInGameMode())
{
return;
}
if ( model()->rowCount() == 0)
{
return;
}
QMenu contextMenu(tr("Context menu"), this);
QAction actionNewEntity(tr("Make entity with selected favorites"), this);
QAction actionAddToSelection(this);
if (GetIEditor()->GetDocument()->IsDocumentReady())
{
QObject::connect(&actionNewEntity, &QAction::triggered, this, [&] { ContextMenu_NewEntity(); });
contextMenu.addAction(&actionNewEntity);
AzToolsFramework::EntityIdList selectedEntities;
EBUS_EVENT_RESULT(selectedEntities, AzToolsFramework::ToolsApplicationRequests::Bus, GetSelectedEntities);
if (!selectedEntities.empty())
{
QString addToSelection = selectedEntities.size() > 1 ? tr("Add to selected entities") : tr("Add to selected entity");
actionAddToSelection.setText(addToSelection);
QObject::connect(&actionAddToSelection, &QAction::triggered, this, [&] { ContextMenu_AddToSelectedEntities(); });
contextMenu.addAction(&actionAddToSelection);
}
contextMenu.addSeparator();
}
QAction action(tr("Remove"), this);
QObject::connect(&action, &QAction::triggered, this, [&] { ContextMenu_RemoveSelectedFavorites(); });
contextMenu.addAction(&action);
contextMenu.exec(mapToGlobal(pos));
}
void FavoritesList::ContextMenu_RemoveSelectedFavorites()
{
FavoritesDataModel* dataModel = qobject_cast<FavoritesDataModel*>(model());
if (!selectedIndexes().empty())
{
dataModel->Remove(selectedIndexes());
}
}
void FavoritesList::rowsInserted([[maybe_unused]] const QModelIndex& parent, [[maybe_unused]] int start, [[maybe_unused]] int end)
{
resizeRowToContents(0);
}
void FavoritesList::AddFavorites(const AZStd::vector<const AZ::SerializeContext::ClassData*>& classDataContainer)
{
for (const AZ::SerializeContext::ClassData* classData : classDataContainer)
{
if (classData)
{
FavoritesDataModel* dataModel = qobject_cast<FavoritesDataModel*>(model());
dataModel->AddFavorite(classData);
}
}
}
void FavoritesList::dragEnterEvent(QDragEnterEvent* event)
{
if (event->mimeData()->hasFormat(AzToolsFramework::ComponentTypeMimeData::GetMimeType()))
{
event->acceptProposedAction();
}
}
void FavoritesList::dragMoveEvent(QDragMoveEvent* event)
{
if (event->source() == this)
{
event->ignore();
}
else
{
event->accept();
}
}
// FavoritesDataModel
//////////////////////////////////////////////////////////////////////////
int FavoritesDataModel::rowCount([[maybe_unused]] const QModelIndex &parent /*= QModelIndex()*/) const
{
return m_favorites.size();
}
int FavoritesDataModel::columnCount([[maybe_unused]] const QModelIndex &parent /*= QModelIndex()*/) const
{
return ColumnIndex::Count;
}
void FavoritesDataModel::SaveState()
{
AZStd::vector<AZ::Uuid> favorites;
for (const AZ::SerializeContext::ClassData* classData : m_favorites)
{
favorites.push_back(classData->m_typeId);
}
m_settings->SetFavorites(AZStd::move(favorites));
// Write the settings to file...
AZ::SerializeContext* serializeContext = nullptr;
EBUS_EVENT_RESULT(serializeContext, AZ::ComponentApplicationBus, GetSerializeContext);
AZ_Assert(serializeContext, "Serialize Context is null!");
char settingsPath[AZ_MAX_PATH_LEN] = { 0 };
AZ::IO::FileIOBase::GetInstance()->ResolvePath(ComponentPaletteSettings::GetSettingsFile(), settingsPath, AZ_MAX_PATH_LEN);
bool result = m_provider.Save(settingsPath, serializeContext);
(void)result;
AZ_Warning("ComponentPaletteSettings", result, "Failed to Save the Component Palette Settings!");
}
void FavoritesDataModel::LoadState()
{
// It is necessary to Load the settings file *before* you call UserSettings::CreateFind!
AZ::SerializeContext* serializeContext = nullptr;
EBUS_EVENT_RESULT(serializeContext, AZ::ComponentApplicationBus, GetSerializeContext);
AZ_Assert(serializeContext, "Serialize Context is null!");
char settingsPath[AZ_MAX_PATH_LEN] = { 0 };
AZ::IO::FileIOBase::GetInstance()->ResolvePath(ComponentPaletteSettings::GetSettingsFile(), settingsPath, AZ_MAX_PATH_LEN);
bool result = m_provider.Load(settingsPath, serializeContext);
(void)result;
// Create (if no file was found) or find the settings, this will populate the m_settings->m_favorites list.
m_settings = AZ::UserSettings::CreateFind<ComponentPaletteSettings>(AZ_CRC("ComponentPaletteSettings", 0x481d355b), m_providerId);
// Add favorites to the data model from loaded settings
for (const AZ::Uuid& favorite : m_settings->m_favorites)
{
const AZ::SerializeContext::ClassData* classData = serializeContext->FindClassData(favorite);
if (classData)
{
AddFavorite(classData, false);
}
}
}
void FavoritesDataModel::Remove(const QModelIndexList& indices)
{
beginResetModel();
auto newFavorites = m_favorites;
// swap here
for (auto index : indices)
{
// we're only dealing with columns and they're the only thing with class data anyways
if (index.column() == 0)
{
QVariant classDataVariant = index.data(ComponentDataModel::ClassDataRole);
if (classDataVariant.isValid())
{
const AZ::SerializeContext::ClassData* classData = reinterpret_cast<const AZ::SerializeContext::ClassData*>(classDataVariant.value<void*>());
newFavorites.removeAll(classData);
AZ_TracePrintf("Debug", "Removing: %s\n", classData->m_editData->m_name);
}
}
}
m_favorites.swap(newFavorites);
endResetModel();
SaveState();
}
QModelIndex FavoritesDataModel::index(int row, int column, const QModelIndex &parent) const
{
if (!hasIndex(row, column, parent))
{
return QModelIndex();
}
if (row >= rowCount(parent) || column >= columnCount(parent))
{
return QModelIndex();
}
return createIndex(row, column, (void*)(m_favorites[row]));
}
QVariant FavoritesDataModel::data(const QModelIndex &index, int role /*= Qt::DisplayRole*/) const
{
if (!index.isValid())
{
return QVariant();
}
const AZ::SerializeContext::ClassData* classData = m_favorites[index.row()];
if (!classData)
{
return QVariant();
}
switch (role)
{
case Qt::DisplayRole:
{
if (index.column() == ComponentDataModel::ColumnIndex::Name)
{
if (m_favorites.empty())
{
return QVariant(tr("You have 0 favorites.\nDrag some components here."));
}
return QVariant(classData->m_editData->m_name);
}
}
break;
case Qt::DecorationRole:
{
if (index.column() == ColumnIndex::Icon)
{
const AZ::SerializeContext::ClassData* iconClassData = m_favorites[index.row()];
auto iconIterator = m_componentIcons.find(iconClassData->m_typeId);
if (iconIterator != m_componentIcons.end())
{
return iconIterator->second;
}
return QVariant();
}
}
break;
case ClassDataRole:
if (index.column() == 0) // Only get data for one column
{
return QVariant::fromValue<void*>(reinterpret_cast<void*>(const_cast<AZ::SerializeContext::ClassData*>(classData)));
}
break;
default:
break;
}
return ComponentDataModel::data(index, role);
}
void FavoritesDataModel::SetSavedStateKey([[maybe_unused]] AZ::u32 key)
{
}
FavoritesDataModel::FavoritesDataModel(QWidget* parent /*= nullptr*/)
: ComponentDataModel(parent)
, m_providerId(AZ_CRC("ComponentPaletteSettingsProviderId"))
{
m_provider.Activate(m_providerId);
LoadState();
}
FavoritesDataModel::~FavoritesDataModel()
{
m_provider.Deactivate();
}
void FavoritesDataModel::AddFavorite(const AZ::SerializeContext::ClassData* classData, bool updateSettings)
{
beginResetModel();
if (m_favorites.indexOf(classData) < 0)
{
m_favorites.push_back(classData);
}
endResetModel();
if (updateSettings)
{
SaveState();
}
}
bool FavoritesDataModel::dropMimeData(const QMimeData *data, Qt::DropAction action, [[maybe_unused]] int row, [[maybe_unused]] int column, [[maybe_unused]] const QModelIndex &parent)
{
if (action == Qt::IgnoreAction)
{
return true;
}
if (data && data->hasFormat(AzToolsFramework::ComponentTypeMimeData::GetMimeType()))
{
AzToolsFramework::ComponentTypeMimeData::ClassDataContainer classDataContainer;
AzToolsFramework::ComponentTypeMimeData::Get(data, classDataContainer);
for (const AZ::SerializeContext::ClassData* classData : classDataContainer)
{
if (classData)
{
AddFavorite(classData);
}
}
return true;
}
return false;
}
#include <UI/ComponentPalette/moc_FavoriteComponentList.cpp>
@@ -0,0 +1,116 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#if !defined(Q_MOC_RUN)
#include "ComponentDataModel.h"
#include "FilteredComponentList.h"
#include "ComponentPaletteSettings.h"
#include <AzCore/Component/ComponentApplicationBus.h>
#include <AzCore/UserSettings/UserSettingsProvider.h>
#include <AzToolsFramework/ToolsComponents/ComponentMimeData.h>
#include <AzToolsFramework/ToolsComponents/ComponentAssetMimeDataContainer.h>
#endif
//! FavoriteComponentListRequest
//! Bus that provides a way for external features to record favorites
class FavoriteComponentListRequest : public AZ::EBusTraits
{
public:
virtual void AddFavorites(const AZStd::vector<const AZ::SerializeContext::ClassData*>&) = 0;
};
using FavoriteComponentListRequestBus = AZ::EBus<FavoriteComponentListRequest>;
//! FavoritesDataModel
//! Stores the list of component class data to display in the favorites control, offers persistence through user settings.
class FavoritesDataModel
: public ComponentDataModel
{
Q_OBJECT
public:
AZ_CLASS_ALLOCATOR(FavoritesDataModel, AZ::SystemAllocator, 0);
FavoritesDataModel(QWidget* parent = nullptr);
~FavoritesDataModel() override;
//! Add a favorite component
//! \param classData The ClassData information for the component to store as favorite
//! \param updateSettings Optional parameter used to determine if the persistent settings need to be updated.
void AddFavorite(const AZ::SerializeContext::ClassData* classData, bool updateSettings = true);
//! Remove all the specified items from the table
//! \param indices List of indices to remove from favorites
void Remove(const QModelIndexList& indices);
//! Save the list of favorite components to user settings
void SaveState();
//! Load the list of favorite components from user settings
void LoadState();
protected:
void SetSavedStateKey(AZ::u32 key);
// Qt handlers
QModelIndex index(int row, int column, const QModelIndex &parent) const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override;
// List of component class data
QList<const AZ::SerializeContext::ClassData*> m_favorites;
// The Palette settings and provider information for saving out the Favorites list
AZStd::intrusive_ptr<ComponentPaletteSettings> m_settings;
AZ::UserSettingsProvider m_provider;
AZ::u32 m_providerId;
};
//! FavoritesList
//! User customized list of favorite components, provides persistence.
class FavoritesList
: public FilteredComponentList
, FavoriteComponentListRequestBus::Handler
{
Q_OBJECT
public:
explicit FavoritesList(QWidget* parent = nullptr);
~FavoritesList() override;
void Init() override;
protected:
//////////////////////////////////////////////////////////////////////////
// FavoriteComponentListRequestBus
void AddFavorites(const AZStd::vector<const AZ::SerializeContext::ClassData*>& classDataContainer) override;
//////////////////////////////////////////////////////////////////////////
void rowsInserted(const QModelIndex& parent, int start, int end);
// Context menu handlers
void ShowContextMenu(const QPoint&);
void ContextMenu_RemoveSelectedFavorites();
// Validate data being dragged in
void dragEnterEvent(QDragEnterEvent * event) override;
void dragMoveEvent(QDragMoveEvent* event) override;
//! Handler used when dropping PaletteItems into the Viewport.
static void DragDropHandler(CViewport* viewport, int ptx, int pty, void* custom);
};
@@ -0,0 +1,265 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include "ComponentEntityEditorPlugin_precompiled.h"
#include "ComponentDataModel.h"
#include "FavoriteComponentList.h"
#include "FilteredComponentList.h"
#include "CryCommon/MathConversion.h"
#include "Editor/IEditor.h"
#include "Editor/ViewManager.h"
#include <Editor/CryEditDoc.h>
#include <AzToolsFramework/API/ToolsApplicationAPI.h>
#include <QHeaderView>
void FilteredComponentList::Init()
{
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
setDragDropMode(QAbstractItemView::DragDropMode::DragOnly);
setDragEnabled(true);
setSelectionMode(QAbstractItemView::SelectionMode::ExtendedSelection);
setStyleSheet("QTreeWidget { selection-background-color: rgba(255,255,255,0.2); }");
setGridStyle(Qt::PenStyle::NoPen);
verticalHeader()->hide();
horizontalHeader()->hide();
setSelectionBehavior(QAbstractItemView::SelectionBehavior::SelectRows);
setAcceptDrops(false);
m_componentDataModel = new ComponentDataModel(this);
ComponentDataProxyModel* componentDataProxyModel = new ComponentDataProxyModel(this);
componentDataProxyModel->setSourceModel(m_componentDataModel);
setModel(componentDataProxyModel);
QHeaderView* horizontalHeaderView = horizontalHeader();
horizontalHeaderView->setSectionResizeMode(ComponentDataModel::ColumnIndex::Icon, QHeaderView::ResizeToContents);
horizontalHeaderView->setSectionResizeMode(ComponentDataModel::ColumnIndex::Name, QHeaderView::Stretch);
setColumnWidth(ComponentDataModel::ColumnIndex::Icon, 32);
setShowGrid(false);
setColumnWidth(ComponentDataModel::ColumnIndex::Name, 90);
setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel);
sortByColumn(ComponentDataModel::ColumnIndex::Name, Qt::AscendingOrder);
hideColumn(ComponentDataModel::ColumnIndex::Category);
connect(model(), &QAbstractItemModel::rowsInserted, this, &FilteredComponentList::rowsInserted);
connect(model(), &QAbstractItemModel::rowsRemoved, this, &FilteredComponentList::rowsAboutToBeRemoved);
connect(model(), SIGNAL(modelReset()), SLOT(modelReset()));
// Context menu
setContextMenuPolicy(Qt::CustomContextMenu);
connect(this, &QWidget::customContextMenuRequested, this, &FilteredComponentList::ShowContextMenu);
}
void FilteredComponentList::ContextMenu_NewEntity()
{
AZ::EntityId entityId;
auto proxyDataModel = qobject_cast<ComponentDataProxyModel*>(model());
if (proxyDataModel)
{
entityId = proxyDataModel->NewEntityFromSelection(selectedIndexes());
}
else
{
auto dataModel = qobject_cast<ComponentDataModel*>(model());
if (dataModel)
{
entityId = dataModel->NewEntityFromSelection(selectedIndexes());
}
}
}
void FilteredComponentList::ContextMenu_AddToFavorites()
{
AZStd::vector<const AZ::SerializeContext::ClassData*> componentsToAdd;
for (auto index : selectedIndexes())
{
QVariant classDataVariant = index.data(ComponentDataModel::ClassDataRole);
if (classDataVariant.isValid())
{
auto classData = reinterpret_cast<const AZ::SerializeContext::ClassData*>(classDataVariant.value<void*>());
componentsToAdd.push_back(classData);
}
}
if (!componentsToAdd.empty())
{
EBUS_EVENT(FavoriteComponentListRequestBus, AddFavorites, componentsToAdd);
}
}
void FilteredComponentList::ContextMenu_AddToSelectedEntities()
{
ComponentDataUtilities::AddComponentsToSelectedEntities(selectedIndexes(), model());
}
void FilteredComponentList::ShowContextMenu(const QPoint& pos)
{
QMenu contextMenu(tr("Context menu"), this);
QAction actionNewEntity(tr("Create new entity with selected components"), this);
if (GetIEditor()->GetDocument()->IsDocumentReady())
{
QObject::connect(&actionNewEntity, &QAction::triggered, this, [this] { ContextMenu_NewEntity(); });
contextMenu.addAction(&actionNewEntity);
}
QAction actionAddFavorite(tr("Add to favorites"), this);
QObject::connect(&actionAddFavorite, &QAction::triggered, this, [this] { ContextMenu_AddToFavorites(); });
contextMenu.addAction(&actionAddFavorite);
QAction actionAddToSelection(this);
if (GetIEditor()->GetDocument()->IsDocumentReady())
{
AzToolsFramework::EntityIdList selectedEntities;
EBUS_EVENT_RESULT(selectedEntities, AzToolsFramework::ToolsApplicationRequests::Bus, GetSelectedEntities);
if (!selectedEntities.empty())
{
QString addToSelection = selectedEntities.size() > 1 ? tr("Add to selected entities") : tr("Add to selected entity");
actionAddToSelection.setText(addToSelection);
QObject::connect(&actionAddToSelection, &QAction::triggered, this, [this] { ContextMenu_AddToSelectedEntities(); });
contextMenu.addAction(&actionAddToSelection);
}
}
// TODO: Requires information panel implementation LMBR-28174
//QAction actionHelp(tr("Help"), this);
//QObject::connect(&actionHelp, &QAction::triggered, this, [&] {});
//contextMenu.addAction(&actionHelp);
contextMenu.exec(mapToGlobal(pos));
}
void FilteredComponentList::modelReset()
{
// Ensure that the category column is hidden
hideColumn(ComponentDataModel::ColumnIndex::Category);
}
FilteredComponentList::FilteredComponentList(QWidget* parent /*= nullptr*/)
: QTableView(parent)
{
}
FilteredComponentList::~FilteredComponentList()
{
}
void FilteredComponentList::SearchCriteriaChanged(QStringList& criteriaList, AzToolsFramework::FilterOperatorType filterOperator)
{
setUpdatesEnabled(false);
auto dataModel = qobject_cast<ComponentDataProxyModel*>(model());
if (dataModel)
{
// Go through the list of items and show/hide as needed due to filter.
QString filter;
for (const auto& criteria : criteriaList)
{
QString tag, text;
AzToolsFramework::SearchCriteriaButton::SplitTagAndText(criteria, tag, text);
AppendFilter(filter, text, filterOperator);
}
dataModel->setFilterRegExp(QRegExp(filter, Qt::CaseSensitivity::CaseInsensitive));
}
setUpdatesEnabled(true);
}
void FilteredComponentList::SetCategory(const char* category)
{
auto dataModel = qobject_cast<ComponentDataProxyModel*>(model());
if (dataModel)
{
if (!category || category[0] == 0 || azstricmp(category, "All") == 0)
{
dataModel->ClearSelectedCategory();
}
else
{
dataModel->SetSelectedCategory(category);
}
}
// Note: this ensures the category column remains hidden
hideColumn(ComponentDataModel::ColumnIndex::Category);
}
void FilteredComponentList::BuildFilter(QStringList& criteriaList, AzToolsFramework::FilterOperatorType filterOperator)
{
ClearFilterRegExp();
for (const auto& criteria : criteriaList)
{
QString tag, text;
AzToolsFramework::SearchCriteriaButton::SplitTagAndText(criteria, tag, text);
if (tag.isEmpty())
{
tag = "null";
}
QString filter = m_filtersRegExp[tag.toStdString().c_str()].pattern();
AppendFilter(filter, text, filterOperator);
SetFilterRegExp(tag.toStdString().c_str(), QRegExp(filter, Qt::CaseInsensitive));
}
}
void FilteredComponentList::AppendFilter(QString& filter, const QString& text, AzToolsFramework::FilterOperatorType filterOperator)
{
if (filterOperator == AzToolsFramework::FilterOperatorType::Or)
{
if (filter.isEmpty())
{
filter = text;
}
else
{
filter += "|" + text;
}
}
else if (filterOperator == AzToolsFramework::FilterOperatorType::And)
{
//using lookaheads to produce an "and" effect.
filter += "(?=.*" + text + ")";
}
}
void FilteredComponentList::SetFilterRegExp(const AZStd::string& filterType, const QRegExp& regExp)
{
m_filtersRegExp[filterType] = regExp;
}
void FilteredComponentList::ClearFilterRegExp(const AZStd::string& filterType /*= AZStd::string()*/)
{
if (filterType.empty())
{
for (auto& it : m_filtersRegExp)
{
it.second = QRegExp();
}
}
else
{
m_filtersRegExp[filterType] = QRegExp();
}
}
#include <UI/ComponentPalette/moc_FilteredComponentList.cpp>
@@ -0,0 +1,67 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#if !defined(Q_MOC_RUN)
#include <QTableView>
#include <QWidget>
#include <AzToolsFramework/UI/SearchWidget/SearchCriteriaWidget.hxx>
#include <AzToolsFramework/UI/SearchWidget/SearchWidgetTypes.hxx>
#include <AzCore/Serialization/SerializeContext.h>
#include "ComponentDataModel.h"
#endif
namespace AZ
{
class SerializeContext;
class ClassData;
}
class ComponentDataModel;
//! FilteredComponentList
//! Provides a list of components that can be filtered according to search criteria provided and/or from
//! a category selection control.
class FilteredComponentList
: public QTableView
{
Q_OBJECT
public:
explicit FilteredComponentList(QWidget* parent = nullptr);
~FilteredComponentList() override;
virtual void Init();
void SearchCriteriaChanged(QStringList& criteriaList, AzToolsFramework::FilterOperatorType filterOperator);
void SetCategory(const char* category);
protected:
// Filtering support
void BuildFilter(QStringList& criteriaList, AzToolsFramework::FilterOperatorType filterOperator);
void AppendFilter(QString& filter, const QString& text, AzToolsFramework::FilterOperatorType filterOperator);
void SetFilterRegExp(const AZStd::string& filterType, const QRegExp& regExp);
void ClearFilterRegExp(const AZStd::string& filterType = AZStd::string());
// Context menu handlers
void ShowContextMenu(const QPoint&);
void ContextMenu_NewEntity();
void ContextMenu_AddToFavorites();
void ContextMenu_AddToSelectedEntities();
void modelReset();
AzToolsFramework::FilterByCategoryMap m_filtersRegExp;
ComponentDataModel* m_componentDataModel;
};
@@ -0,0 +1,13 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include "ComponentEntityEditorPlugin_precompiled.h"
#include "InformationPanel.h"
// TODO: LMBR-28174
@@ -0,0 +1,10 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
// TODO: LMBR-28174
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 60.1 (88133) - https://sketch.com -->
<title>Outliner / vis &amp; lock / lock / Default</title>
<desc>Created with Sketch.</desc>
<g id="Outliner-/-vis-&amp;-lock-/-lock-/-Default" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" fill-opacity="0.35">
<circle id="Oval-3" fill="#FFFFFF" cx="8" cy="8" r="2.5"></circle>
</g>
</svg>

After

Width:  |  Height:  |  Size: 579 B

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 60.1 (88133) - https://sketch.com -->
<title>Outliner / vis &amp; lock / lock / Default - hover</title>
<desc>Created with Sketch.</desc>
<g id="Outliner-/-vis-&amp;-lock-/-lock-/-Default---hover" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M7.62646385,1.48235749 C9.42626113,1.48235749 10.8852851,2.94138146 10.8852851,4.74117875 L10.8843498,7.34735749 L12.8405779,7.34823575 L12.8405779,14.5176425 L2.41234985,14.5176425 L2.41234985,7.34823575 L9.58134985,7.34735749 L9.5817566,4.74117875 C9.5817566,3.66130038 8.70634222,2.78588599 7.62646385,2.78588599 C7.21874475,2.78588599 6.62971236,2.90054702 6.14480925,3.34844343 C6.01584142,3.46756874 5.88452781,3.71832946 5.75086844,4.10072562 L4.38428828,4.10072562 C4.43323512,3.87757947 4.47506118,3.7202004 4.50976646,3.62858842 C4.59003595,3.41669999 4.80061604,3.0404071 4.86756375,2.94801197 C5.78155489,1.68660458 6.73091609,1.48235749 7.62646385,1.48235749 Z M11.5370494,8.65176425 L3.71587835,8.65176425 L3.71587835,13.214114 L11.5370494,13.214114 L11.5370494,8.65176425 Z M8.2782281,9.95529275 L8.2782281,11.9105855 L6.9746996,11.9105855 L6.9746996,9.95529275 L8.2782281,9.95529275 Z" id="Combined-Shape-Copy-4" fill="#17A3CD"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 60.1 (88133) - https://sketch.com -->
<title>Outliner / vis &amp; lock / lock / Default</title>
<desc>Created with Sketch.</desc>
<g id="Outliner-/-vis-&amp;-lock-/-lock-/-Default" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" fill-opacity="0.1">
<circle id="Oval-3" fill="#FFFFFF" cx="8" cy="8" r="2.5"></circle>
</g>
</svg>

After

Width:  |  Height:  |  Size: 578 B

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 60.1 (88133) - https://sketch.com -->
<title>Outliner / vis &amp; lock / lock / on</title>
<desc>Created with Sketch.</desc>
<g id="Outliner-/-vis-&amp;-lock-/-lock-/-on" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M8,1.53182564 C9.78613703,1.53182564 11.2340872,2.97977579 11.2340872,4.76591282 L11.2340872,7.35318256 L13.1745395,7.35318256 L13.1745395,14.4681744 L2.82546052,14.4681744 L2.82546052,7.35318256 L4.76591282,7.35318256 L4.76591282,4.76591282 C4.76591282,2.97977579 6.21386297,1.53182564 8,1.53182564 Z M8.64681744,9.94045231 L7.35318256,9.94045231 L7.35318256,11.8809046 L8.64681744,11.8809046 L8.64681744,9.94045231 Z M8,2.82546052 C6.92831778,2.82546052 6.05954769,3.69423061 6.05954769,4.76591282 L6.05954769,4.76591282 L6.05954769,7.35318256 L9.94045231,7.35318256 L9.94045231,4.76591282 C9.94045231,3.69423061 9.07168222,2.82546052 8,2.82546052 Z" id="Combined-Shape-Copy" fill="#E9E9E9"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 60.1 (88133) - https://sketch.com -->
<title>Outliner / vis &amp; lock /lock / on - hover</title>
<desc>Created with Sketch.</desc>
<g id="Outliner-/-vis-&amp;-lock-/lock-/-on---hover" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M8,1.53182564 C9.78613703,1.53182564 11.2340872,2.97977579 11.2340872,4.76591282 L11.2340872,7.35318256 L13.1745395,7.35318256 L13.1745395,14.4681744 L2.82546052,14.4681744 L2.82546052,7.35318256 L4.76591282,7.35318256 L4.76591282,4.76591282 C4.76591282,2.97977579 6.21386297,1.53182564 8,1.53182564 Z M8.64681744,9.94045231 L7.35318256,9.94045231 L7.35318256,11.8809046 L8.64681744,11.8809046 L8.64681744,9.94045231 Z M8,2.82546052 C6.92831778,2.82546052 6.05954769,3.69423061 6.05954769,4.76591282 L6.05954769,4.76591282 L6.05954769,7.35318256 L9.94045231,7.35318256 L9.94045231,4.76591282 C9.94045231,3.69423061 9.07168222,2.82546052 8,2.82546052 Z" id="Combined-Shape" fill="#17A3CD"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 60.1 (88133) - https://sketch.com -->
<title>Outliner / vis &amp; lock / lock / on</title>
<desc>Created with Sketch.</desc>
<g id="Outliner-/-vis-&amp;-lock-/-lock-/-on" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" fill-opacity="0.2">
<path d="M8,1.53182564 C9.78613703,1.53182564 11.2340872,2.97977579 11.2340872,4.76591282 L11.2340872,7.35318256 L13.1745395,7.35318256 L13.1745395,14.4681744 L2.82546052,14.4681744 L2.82546052,7.35318256 L4.76591282,7.35318256 L4.76591282,4.76591282 C4.76591282,2.97977579 6.21386297,1.53182564 8,1.53182564 Z M8.64681744,9.94045231 L7.35318256,9.94045231 L7.35318256,11.8809046 L8.64681744,11.8809046 L8.64681744,9.94045231 Z M8,2.82546052 C6.92831778,2.82546052 6.05954769,3.69423061 6.05954769,4.76591282 L6.05954769,4.76591282 L6.05954769,7.35318256 L9.94045231,7.35318256 L9.94045231,4.76591282 C9.94045231,3.69423061 9.07168222,2.82546052 8,2.82546052 Z" id="Combined-Shape-Copy" fill="#E9E9E9"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="12px" height="10px" viewBox="0 0 12 10" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 61 (89581) - https://sketch.com -->
<title>a to z sort</title>
<desc>Created with Sketch.</desc>
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Menu-/-Outliner-/-menu" transform="translate(-186.000000, -32.000000)" fill="#FFFFFF">
<g id="Group-14">
<g id="icon-/-outliner-/-menu-/-sort-A-to-Z" transform="translate(184.000000, 29.000000)">
<g id="Group" transform="translate(2.000000, 3.000000)">
<path d="M6.48182562,2.90763871 C6.48182562,3.11168285 6.48182562,4.18637111 6.48182562,4.32322247 C6.48182562,4.46007383 6.3017789,4.6288924 6.11455497,4.43691302 C5.98973902,4.30892676 5.44321422,3.77841492 4.47498059,2.84537751 C4.47498059,7.17586292 4.47498059,9.42262347 4.47498059,9.58565917 C4.47498059,9.83021273 4.33443054,10 4.11176088,10 C3.88909121,10 3.58218839,10 3.30773713,10 C3.03328587,10 2.92923833,9.76271172 2.92923833,9.58565917 C2.92923833,9.46762414 2.92923833,7.22086359 2.92923833,2.84537751 C1.96975238,3.80404153 1.43442638,4.33455337 1.32326034,4.43691302 C1.15651127,4.59045249 0.963651247,4.53541253 0.963651247,4.32322247 C0.963651247,4.11103242 0.963651247,3.19983486 0.963651247,2.90763871 C0.963651247,2.61544257 1.02930291,2.55216863 1.14422635,2.41197084 C1.25914979,2.27177305 3.13689309,0.413447847 3.37510604,0.178801486 C3.61331899,-0.0558448745 3.8068395,-0.0645632043 4.04157392,0.178801486 C4.27630835,0.422166177 6.07430975,2.17371386 6.24960102,2.34970964 C6.42489229,2.52570542 6.48182562,2.70359457 6.48182562,2.90763871 Z" id="Path-6-Copy" transform="translate(3.722738, 4.999766) rotate(-180.000000) translate(-3.722738, -4.999766) "></path>
<path d="M10.8281874,4.68625528 L10.4759248,3.61770952 L8.77212367,3.61770952 L8.43423907,4.68625528 L7.30556071,4.68625528 L9.05968504,-0.000467562775 L10.2458757,-0.000467562775 L12,4.68625528 L10.8281874,4.68625528 Z M9.6204297,0.926056289 L9.00217276,2.88731116 L10.2530647,2.88731116 L9.6204297,0.926056289 Z" id="A"></path>
<polygon id="Z" points="7.83462135 10 7.83462135 9.29224882 10.3215285 6.21877381 7.96001163 6.21877381 7.96001163 5.45859663 11.450041 5.45859663 11.450041 6.1663478 8.97706609 9.23982281 11.4709394 9.23982281 11.4709394 10"></polygon>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="13px" height="12px" viewBox="0 0 13 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 61 (89581) - https://sketch.com -->
<title>sort manually</title>
<desc>Created with Sketch.</desc>
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Menu-/-Outliner-/-menu" transform="translate(-186.000000, -6.000000)" fill-rule="nonzero">
<g id="Group-14">
<g id="sort-manually" transform="translate(184.000000, 4.000000)">
<rect id="Rectangle-2" fill-opacity="0" fill="#D8D8D8" x="0" y="0" width="16" height="16"></rect>
<g id="manual-sort" transform="translate(1.000000, 2.000000)" fill="#FFFFFF">
<g id="Group-3" transform="translate(0.863227, 0.500000)">
<path d="M3.78314642,0.285229 L3.99871527,0.501168483 C4.54521539,1.04243432 5.84355982,2.30793018 5.99117352,2.45613715 C6.16646479,2.63213293 6.22339812,2.81002208 6.22339812,3.01406623 L6.22339812,4.42964999 L6.22339812,4.42964999 C6.22339812,4.56650134 6.0433514,4.73531991 5.85612747,4.54334053 C5.7331288,4.41721771 5.20061175,3.90020525 4.25857636,2.99230315 L4.25781117,8.41304244 L4.25781117,8.41304244 L4.7527864,7.9189506 C5.4031325,7.27048871 5.77346675,6.90467415 5.86378917,6.82150693 C6.03053823,6.66796746 6.22339825,6.72300742 6.22339825,6.93519747 L6.22339825,8.35078124 L6.22339825,8.35078124 C6.22339825,8.64297738 6.15774659,8.70625132 6.04282315,8.84644911 C5.92789971,8.98664689 4.05015641,10.8449721 3.81194346,11.0796185 C3.57373051,11.3142648 3.38021,11.3229832 3.14547558,11.0796185 L2.92990673,10.863679 C2.38340661,10.3224131 1.08506218,9.05691728 0.93744848,8.90871031 C0.76215721,8.73271453 0.70522388,8.55482538 0.70522388,8.35078124 L0.70522388,6.93519747 L0.70522388,6.93519747 C0.70522388,6.79834612 0.8852706,6.62952755 1.07249453,6.82150693 C1.1954932,6.94762975 1.72801025,7.46464221 2.67004564,8.37254431 L2.67081083,2.95180502 L2.67081083,2.95180502 L2.3308861,3.29123439 C1.58412088,4.03640712 1.16210312,4.45377584 1.06483283,4.54334053 C0.89808377,4.69688 0.705223747,4.64184004 0.705223747,4.42964999 L0.705223747,3.01406623 L0.705223747,3.01406623 C0.705223747,2.72187008 0.770875412,2.65859614 0.88579885,2.51839835 C1.00072229,2.37820057 2.87846559,0.519875361 3.11667854,0.285229 C3.35489149,0.0505826389 3.548412,0.0418643091 3.78314642,0.285229 Z M12.8441776,7.75 L12.8441776,9.25 L7.32600318,9.25 L7.32600318,7.75 L12.8441776,7.75 Z M12.8441776,5 L12.8441776,6.5 L7.32600318,6.5 L7.32600318,5 L12.8441776,5 Z M12.8441776,2.25 L12.8441776,3.75 L7.32600318,3.75 L7.32600318,2.25 L12.8441776,2.25 Z" id="Combined-Shape"></path>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="12px" height="11px" viewBox="0 0 12 11" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 61 (89581) - https://sketch.com -->
<title>z to A sort</title>
<desc>Created with Sketch.</desc>
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Menu-/-Outliner-/-menu" transform="translate(-186.000000, -55.000000)" fill="#FFFFFF">
<g id="Group-14">
<g id="icon-/-outliner-/-menu-/-sort-Z-to-A" transform="translate(184.000000, 52.000000)">
<g id="Group-Copy" transform="translate(1.500000, 2.000000)">
<path d="M6.48182562,3.90763871 C6.48182562,4.11168285 6.48182562,5.18637111 6.48182562,5.32322247 C6.48182562,5.46007383 6.3017789,5.6288924 6.11455497,5.43691302 C5.98973902,5.30892676 5.44321422,4.77841492 4.47498059,3.84537751 C4.47498059,8.17586292 4.47498059,10.4226235 4.47498059,10.5856592 C4.47498059,10.8302127 4.33443054,11 4.11176088,11 C3.88909121,11 3.58218839,11 3.30773713,11 C3.03328587,11 2.92923833,10.7627117 2.92923833,10.5856592 C2.92923833,10.4676241 2.92923833,8.22086359 2.92923833,3.84537751 C1.96975238,4.80404153 1.43442638,5.33455337 1.32326034,5.43691302 C1.15651127,5.59045249 0.963651247,5.53541253 0.963651247,5.32322247 C0.963651247,5.11103242 0.963651247,4.19983486 0.963651247,3.90763871 C0.963651247,3.61544257 1.02930291,3.55216863 1.14422635,3.41197084 C1.25914979,3.27177305 3.13689309,1.41344785 3.37510604,1.17880149 C3.61331899,0.944155126 3.8068395,0.935436796 4.04157392,1.17880149 C4.27630835,1.42216618 6.07430975,3.17371386 6.24960102,3.34970964 C6.42489229,3.52570542 6.48182562,3.70359457 6.48182562,3.90763871 Z" id="Path-6-Copy" transform="translate(3.722738, 5.999766) rotate(-1.000000) translate(-3.722738, -5.999766) "></path>
<path d="M10.8281874,11.1453195 L10.4759248,10.0767737 L8.77212367,10.0767737 L8.43423907,11.1453195 L7.30556071,11.1453195 L9.05968504,6.45859663 L10.2458757,6.45859663 L12,11.1453195 L10.8281874,11.1453195 Z M9.6204297,7.38512048 L9.00217276,9.34637535 L10.2530647,9.34637535 L9.6204297,7.38512048 Z" id="A"></path>
<polygon id="Z" points="7.83462135 5.54093581 7.83462135 4.83318464 10.3215285 1.75970963 7.96001163 1.75970963 7.96001163 0.999532437 11.450041 0.999532437 11.450041 1.70728361 8.97706609 4.78075862 11.4709394 4.78075862 11.4709394 5.54093581"></polygon>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 60.1 (88133) - https://sketch.com -->
<title>Outliner / vis &amp; lock / visible / Default</title>
<desc>Created with Sketch.</desc>
<g id="Outliner-/-vis-&amp;-lock-/-visible-/-Default" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" fill-opacity="0.35">
<circle id="Oval-3" fill="#FFFFFF" cx="8" cy="8" r="2.5"></circle>
</g>
</svg>

After

Width:  |  Height:  |  Size: 585 B

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 60.1 (88133) - https://sketch.com -->
<title>Outliner / vis &amp; lock /visible / Default - hover</title>
<desc>Created with Sketch.</desc>
<g id="Outliner-/-vis-&amp;-lock-/visible-/-Default---hover" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Group-9" transform="translate(0.500000, 3.500000)" fill="#17A3CD">
<path d="M7.31366902,0.423140577 C8.81176272,0.423140577 11.0671071,1.77422153 14.0797023,4.47638343 C11.0671071,7.17854533 8.81176272,8.52962629 7.31366902,8.52962629 C5.81557532,8.52962629 3.56731657,7.17854533 0.568892758,4.47638343 C3.56731657,1.77422153 5.81557532,0.423140577 7.31366902,0.423140577 Z M7.32429752,1.43645129 C5.64538935,1.43645129 4.28436537,2.79747527 4.28436537,4.47638343 C4.28436537,6.15529159 5.64538935,7.51631557 7.32429752,7.51631557 C9.00320568,7.51631557 10.3642297,6.15529159 10.3642297,4.47638343 C10.3642297,2.79747527 9.00320568,1.43645129 7.32429752,1.43645129 Z M7.32429752,2.449762 C8.44356962,2.449762 9.35091894,3.35711132 9.35091894,4.47638343 C9.35091894,5.59565554 8.44356962,6.50300486 7.32429752,6.50300486 C6.20502541,6.50300486 5.29767609,5.59565554 5.29767609,4.47638343 C5.29767609,3.35711132 6.20502541,2.449762 7.32429752,2.449762 Z" id="Combined-Shape-Copy-2"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 60.1 (88133) - https://sketch.com -->
<title>Outliner / vis &amp; lock / visible / Default</title>
<desc>Created with Sketch.</desc>
<g id="Outliner-/-vis-&amp;-lock-/-visible-/-Default" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" fill-opacity="0.1">
<circle id="Oval-3" fill="#FFFFFF" cx="8" cy="8" r="2.5"></circle>
</g>
</svg>

After

Width:  |  Height:  |  Size: 584 B

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 60.1 (88133) - https://sketch.com -->
<title>Outliner / vis &amp; lock /visible / on</title>
<desc>Created with Sketch.</desc>
<g id="Outliner-/-vis-&amp;-lock-/visible-/-on" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Icons-/-System-/--View---hidden" fill="#FFFFFF">
<path d="M14.6666667,8 L14.1911055,8.42074507 C11.4552099,10.806915 9.38801174,12 7.98951112,12 C7.51648635,12 6.96767553,11.8635059 6.34307866,11.5905178 L7.07749705,10.8554933 C7.36812933,10.9493157 7.67814569,11 8,11 C9.65685425,11 11,9.65685425 11,8 C11,7.67814569 10.9493157,7.36812933 10.8554933,7.07749705 L12.0655037,5.86660728 C12.8575187,6.44385359 13.724573,7.15498449 14.6666667,8 Z M7.98951112,4 C8.76671422,4 9.75044269,4.368481 10.9406965,5.10544299 L10.1451174,5.90274502 C9.60045101,5.34572695 8.84059993,5 8,5 C6.34314575,5 5,6.34314575 5,8 C5,8.84059993 5.34572695,9.60045101 5.90274502,10.1451174 L5.11176964,10.9363144 C4.02594566,10.270303 2.76646689,9.29153153 1.33333333,8 L1.80668794,7.57925493 C4.53006944,5.19308498 6.5910105,4 7.98951112,4 Z M10.0102404,8 C10.0102404,9.1045695 9.1125175,10 8.00512019,10 L7.93466667,9.99733333 L10.0088484,7.92477962 L10.0102404,8 L10.0102404,8 Z M8.00512019,6 C8.56870632,6 9.07798504,6.23192392 9.44226005,6.60530708 L6.61080237,9.43728937 C6.23415224,9.07368931 6,8.56411942 6,8 C6,6.8954305 6.89772289,6 8.00512019,6 Z" id="Shape"></path>
<polygon id="Rectangle-25" transform="translate(7.522750, 7.541445) rotate(45.000000) translate(-7.522750, -7.541445) " points="6.80375237 0.92856205 8.2417471 0.925883489 8.2197393 14.1570058 6.80375237 14.1278886"></polygon>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 60.1 (88133) - https://sketch.com -->
<title>Outliner / vis &amp; lock / visible / on - hover</title>
<desc>Created with Sketch.</desc>
<g id="Outliner-/-vis-&amp;-lock-/-visible-/-on---hover" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Sky-Icon-/-System-/-View" fill="#17A3CD">
<path d="M12.0662207,5.86712992 C12.858042,6.44428281 13.7248573,7.1552395 14.6666667,8 C11.6936446,10.6666667 9.46792606,12 7.98951112,12 C7.51688042,12 6.96858989,11.8637332 6.34463953,11.5911997 L7.07876759,10.8559032 C7.36903278,10.9494633 7.67861466,11 8,11 C9.65685425,11 11,9.65685425 11,8 C11,7.67861466 10.9494633,7.36903278 10.8559032,7.07876759 Z M7.98951112,4 C8.76679076,4 9.75063645,4.36855358 10.9410482,5.10566074 L10.1454186,5.90305312 C9.60073119,5.34585444 8.84075491,5 8,5 C6.34314575,5 5,6.34314575 5,8 C5,8.84036125 5.34553064,9.60001949 5.90227058,10.1446534 L5.11233104,10.9366587 C4.02637979,10.2706404 2.76671388,9.29175412 1.33333333,8 C4.29237025,5.33333333 6.51109618,4 7.98951112,4 Z M10.0073333,7.926 L10.0102404,8 C10.0102404,9.1045695 9.1125175,10 8.00512019,10 C7.98181012,10 7.95859295,9.99960325 7.9354751,9.99881616 L10.0073333,7.926 Z M8.00512019,6 C8.5686887,6 9.07795318,6.23190942 9.44222587,6.60527205 L6.6105304,9.43702678 C6.23403952,9.07344399 6,8.56398363 6,8 C6,6.8954305 6.89772289,6 8.00512019,6 Z" id="Combined-Shape"></path>
<polygon id="Rectangle-25" transform="translate(7.522750, 7.541445) rotate(45.000000) translate(-7.522750, -7.541445) " points="6.80375237 0.92856205 8.2417471 0.925883489 8.2197393 14.1570058 6.80375237 14.1278886"></polygon>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 60.1 (88133) - https://sketch.com -->
<title>Outliner / vis &amp; lock /visible / on</title>
<desc>Created with Sketch.</desc>
<g id="Outliner-/-vis-&amp;-lock-/visible-/-on" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Icons-/-System-/--View---hidden" fill="#FFFFFF" fill-opacity="0.2">
<path d="M14.6666667,8 L14.1911055,8.42074507 C11.4552099,10.806915 9.38801174,12 7.98951112,12 C7.51648635,12 6.96767553,11.8635059 6.34307866,11.5905178 L7.07749705,10.8554933 C7.36812933,10.9493157 7.67814569,11 8,11 C9.65685425,11 11,9.65685425 11,8 C11,7.67814569 10.9493157,7.36812933 10.8554933,7.07749705 L12.0655037,5.86660728 C12.8575187,6.44385359 13.724573,7.15498449 14.6666667,8 Z M7.98951112,4 C8.76671422,4 9.75044269,4.368481 10.9406965,5.10544299 L10.1451174,5.90274502 C9.60045101,5.34572695 8.84059993,5 8,5 C6.34314575,5 5,6.34314575 5,8 C5,8.84059993 5.34572695,9.60045101 5.90274502,10.1451174 L5.11176964,10.9363144 C4.02594566,10.270303 2.76646689,9.29153153 1.33333333,8 L1.80668794,7.57925493 C4.53006944,5.19308498 6.5910105,4 7.98951112,4 Z M10.0102404,8 C10.0102404,9.1045695 9.1125175,10 8.00512019,10 L7.93466667,9.99733333 L10.0088484,7.92477962 L10.0102404,8 L10.0102404,8 Z M8.00512019,6 C8.56870632,6 9.07798504,6.23192392 9.44226005,6.60530708 L6.61080237,9.43728937 C6.23415224,9.07368931 6,8.56411942 6,8 C6,6.8954305 6.89772289,6 8.00512019,6 Z" id="Shape"></path>
<polygon id="Rectangle-25" transform="translate(7.522750, 7.541445) rotate(45.000000) translate(-7.522750, -7.541445) " points="6.80375237 0.92856205 8.2417471 0.925883489 8.2197393 14.1570058 6.80375237 14.1278886"></polygon>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

@@ -0,0 +1,166 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
OutlinerWidget #m_display_options
{
qproperty-icon: url(:/stylesheet/img/UI20/menu-centered.svg);
qproperty-iconSize: 16px 16px;
qproperty-flat: true;
}
OutlinerWidget QWidget[PulseHighlight="true"]
{
background-color: #D9822E;
}
OutlinerWidget QTreeView
{
selection-background-color: transparent;
}
OutlinerWidget QTreeView::branch:hover
, OutlinerWidget QTreeView::item:hover
{
background: rgba(255, 255, 255, 30);
}
OutlinerWidget QTreeView::branch:selected
, OutlinerWidget QTreeView::item:selected
, OutlinerWidget QTreeView::branch:selected:active
, OutlinerWidget QTreeView::item:selected:active
{
background: rgba(255, 255, 255, 45);
}
/* --- VISIBILITY AND LOCK --- */
OutlinerCheckBox
{
border: 0px solid transparent;
border-radius: 0px;
spacing: 0px;
padding: 0px;
line-height: 0px;
font-size: 0px;
margin: 0px;
background-color: none;
max-height: 20px;
max-width: 18px;
}
OutlinerCheckBox::indicator
{
width: 16px;
height: 16px;
image-position:center;
border: 1px solid transparent;
image: none;
spacing: 0px;
padding: 0px;
line-height: 0px;
font-size: 0px;
margin: 0;
}
OutlinerCheckBox#VisibilityMixed::indicator:checked
, OutlinerCheckBox#VisibilityMixed::indicator:unchecked
, OutlinerCheckBox#VisibilityMixed::indicator:indeterminate
, OutlinerCheckBox#VisibilityMixedHover::indicator:checked
, OutlinerCheckBox#VisibilityMixedHover::indicator:unchecked
, OutlinerCheckBox#VisibilityMixedHover::indicator:indeterminate
, OutlinerCheckBox#LockMixed::indicator:checked
, OutlinerCheckBox#LockMixed::indicator:unchecked
, OutlinerCheckBox#LockMixed::indicator:indeterminate
, OutlinerCheckBox#LockMixedHover::indicator:checked
, OutlinerCheckBox#LockMixedHover::indicator:unchecked
, OutlinerCheckBox#LockMixedHover::indicator:indeterminate
{
background: rgba(0, 0, 0, 80);
border-radius: 5px;
padding-bottom: 1px;
}
/* --- VISIBILITY --- */
OutlinerCheckBox#Visibility::indicator:checked
, OutlinerCheckBox#VisibilityMixed::indicator:checked
{
image: url(:/visibility_default.svg);
}
OutlinerCheckBox#Visibility::indicator:unchecked
, OutlinerCheckBox#VisibilityMixed::indicator:unchecked
{
image: url(:/visibility_on.svg);
}
OutlinerCheckBox#VisibilityLayerOverride::indicator:checked
{
image: url(:/visibility_default_transparent.svg);
}
OutlinerCheckBox#VisibilityLayerOverride::indicator:unchecked
{
image: url(:/visibility_on_transparent.svg);
}
OutlinerCheckBox#VisibilityHover::indicator:checked
, OutlinerCheckBox#VisibilityMixedHover::indicator:checked
, OutlinerCheckBox#VisibilityLayerOverrideHover::indicator:checked
{
image: url(:/visibility_default_hover.svg);
}
OutlinerCheckBox#VisibilityHover::indicator:unchecked
, OutlinerCheckBox#VisibilityMixedHover::indicator:unchecked
, OutlinerCheckBox#VisibilityLayerOverrideHover::indicator:unchecked
{
image: url(:/visibility_on_hover.svg);
}
/* --- LOCK --- */
OutlinerCheckBox#Lock::indicator:checked
, OutlinerCheckBox#LockMixed::indicator:checked
{
image: url(:/lock_on.svg);
}
OutlinerCheckBox#Lock::indicator:unchecked
, OutlinerCheckBox#LockMixed::indicator:unchecked
{
image: url(:/lock_default.svg);
}
OutlinerCheckBox#LockLayerOverride::indicator:checked
{
image: url(:/lock_on_transparent.svg);
}
OutlinerCheckBox#LockLayerOverride::indicator:unchecked
{
image: url(:/lock_default_transparent.svg);
}
OutlinerCheckBox#LockHover::indicator:checked
, OutlinerCheckBox#LockMixedHover::indicator:checked
, OutlinerCheckBox#LockLayerOverrideHover::indicator:checked
{
image: url(:/lock_on_hover.svg);
}
OutlinerCheckBox#LockHover::indicator:unchecked
, OutlinerCheckBox#LockMixedHover::indicator:unchecked
, OutlinerCheckBox#LockLayerOverrideHover::indicator:unchecked
{
image: url(:/lock_default_hover.svg);
}
@@ -0,0 +1,67 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#include <QModelIndex>
#include <AzCore/Slice/SliceComponent.h>
#include <AzCore/EBus/EBus.h>
class OutlinerCacheRequests
: public AZ::EBusTraits
{
public:
/// Request selection of the item at the given cache index
virtual void SelectOutlinerCache( QModelIndex index ) = 0;
/// Request deselection of the item at the given cache index
virtual void DeselectOutlinerCache(QModelIndex index) = 0;
};
/// \ref EditorVisibilityRequests
using OutlinerCacheRequestBus = AZ::EBus<OutlinerCacheRequests>;
/**
* Messages dispatched when an entity has sustained changes that require
* it be redrawn in the outliner.
*/
class OutlinerCacheNotifications
: public AZ::EBusTraits
{
public:
/// The entity has changed in such a way that its outliner representation has changed and should be redrawn
/// invalidate now is true if the item needs to be redrawn immediately.
virtual void EntityCacheChanged(const AZ::EntityId& /*entityId*/) {}
/// The outliner cache item associated with the given entity has been selected
/// and is requesting that a notification be sent to the tree view.
/// These requests should be handled, considered, and either acted on or queued
virtual void EntityCacheSelectionRequest(const AZ::EntityId& /*entityId*/) {}
/// The outliner cache item associated with the given entity has been deselected
/// and is requesting that the a notification be sent to the tree view.
/// These requests should be handled, considered, and either acted on or queued
virtual void EntityCacheDeselectionRequest(const AZ::EntityId& /*entityId*/) {}
};
/// \ref EditorVisibilityNotifications
using OutlinerCacheNotificationBus = AZ::EBus<OutlinerCacheNotifications>;
class OutlinerModelNotifications
: public AZ::EBusTraits
{
public:
/// The outliner cache item associated with the given entity has been selected
/// and is requesting that a notification be sent to the tree view.
/// These requests should be handled, considered, and either acted on or queued
virtual void ModelEntitySelectionChanged(const AZStd::unordered_set<AZ::EntityId>& /*selectedEntityIdList*/, const AZStd::unordered_set<AZ::EntityId>& /*deselectedEntityIdList*/) {}
virtual void QueueScrollToNewContent(const AZ::EntityId& /*entityId*/) {}
};
/// \ref EditorVisibilityNotifications
using OutlinerModelNotificationBus = AZ::EBus<OutlinerModelNotifications>;
@@ -0,0 +1,68 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include "ComponentEntityEditorPlugin_precompiled.h"
#include "OutlinerDisplayOptionsMenu.h"
#include <QIcon>
namespace EntityOutliner
{
DisplayOptionsMenu::DisplayOptionsMenu(QWidget* parent)
: QMenu(parent)
{
auto sortManually = addAction(QIcon(QStringLiteral(":/sort_manually.svg")), tr("Sort: Manually"));
sortManually->setData(static_cast<int>(DisplaySortMode::Manually));
sortManually->setCheckable(true);
auto sortAtoZ = addAction(QIcon(QStringLiteral(":/sort_a_to_z.svg")), tr("Sort: A to Z"));
sortAtoZ->setData(static_cast<int>(DisplaySortMode::AtoZ));
sortAtoZ->setCheckable(true);
auto sortZtoA = addAction(QIcon(QStringLiteral(":/sort_z_to_a.svg")), tr("Sort: Z to A"));
sortZtoA->setData(static_cast<int>(DisplaySortMode::ZtoA));
sortZtoA->setCheckable(true);
addSeparator();
auto autoScroll = addAction(tr("Scroll to Selected"));
autoScroll->setCheckable(true);
auto autoExpand = addAction(tr("Expand Selected"));
autoExpand->setCheckable(true);
auto sortGroup = new QActionGroup(this);
sortGroup->addAction(sortManually);
sortGroup->addAction(sortAtoZ);
sortGroup->addAction(sortZtoA);
sortManually->setChecked(true);
autoScroll->setChecked(true);
autoExpand->setChecked(true);
connect(sortGroup, &QActionGroup::triggered, this, &DisplayOptionsMenu::OnSortModeSelected);
connect(autoScroll, &QAction::toggled, this, &DisplayOptionsMenu::OnAutoScrollToggle);
connect(autoExpand, &QAction::toggled, this, &DisplayOptionsMenu::OnAutoExpandToggle);
}
void DisplayOptionsMenu::OnSortModeSelected(QAction* action)
{
const auto sortMode = static_cast<DisplaySortMode>(action->data().toInt());
emit OnSortModeChanged(sortMode);
}
void DisplayOptionsMenu::OnAutoScrollToggle(bool checked)
{
emit OnOptionToggled(DisplayOption::AutoScroll, checked);
}
void DisplayOptionsMenu::OnAutoExpandToggle(bool checked)
{
emit OnOptionToggled(DisplayOption::AutoExpand, checked);
}
}
@@ -0,0 +1,55 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#if !defined(Q_MOC_RUN)
#include <QMenu>
#endif
class QAction;
namespace Ui
{
class OutlinerDisplayOptions;
}
namespace EntityOutliner
{
enum class DisplaySortMode : unsigned char
{
Manually,
AtoZ,
ZtoA
};
enum class DisplayOption : unsigned char
{
AutoScroll,
AutoExpand
};
class DisplayOptionsMenu
: public QMenu
{
Q_OBJECT // AUTOMOC
public:
DisplayOptionsMenu(QWidget* parent = nullptr);
~DisplayOptionsMenu() = default;
signals:
void OnSortModeChanged(DisplaySortMode sortMode);
void OnOptionToggled(DisplayOption option, bool enabled);
private:
void OnSortModeSelected(QAction* action);
void OnAutoScrollToggle(bool checked);
void OnAutoExpandToggle(bool checked);
};
}
@@ -0,0 +1,409 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#ifndef OUTLINER_VIEW_MODEL_H
#define OUTLINER_VIEW_MODEL_H
#if !defined(Q_MOC_RUN)
#include <AzCore/base.h>
#include <QtWidgets/QWidget>
#include <QtWidgets/QStyledItemDelegate>
#include <QtWidgets/QCheckBox>
#include <QtCore/QRect>
#include <AzCore/Memory/SystemAllocator.h>
#include <AzCore/Component/EntityBus.h>
#include <AzCore/Asset/AssetCommon.h>
#include <AzToolsFramework/API/ToolsApplicationAPI.h>
#include <AzToolsFramework/UI/SearchWidget/SearchCriteriaWidget.hxx>
#include "OutlinerSearchWidget.h"
#include <AzToolsFramework/ToolsComponents/EditorLockComponentBus.h>
#include <AzToolsFramework/ToolsComponents/EditorVisibilityBus.h>
#include <AzToolsFramework/Entity/EditorEntityContextBus.h>
#include <AzToolsFramework/Entity/EditorEntityInfoBus.h>
#include <AzToolsFramework/API/EntityCompositionNotificationBus.h>
#include <AzToolsFramework/Entity/EditorEntityRuntimeActivationBus.h>
#endif
#pragma once
namespace EntityOutliner
{
enum class DisplaySortMode : unsigned char;
}
//! Model for items in the OutlinerTreeView.
//! Each item represents an Entity.
//! Items are parented in the tree according to their transform hierarchy.
class OutlinerListModel
: public QAbstractItemModel
, private AzToolsFramework::EditorEntityContextNotificationBus::Handler
, private AzToolsFramework::EditorEntityInfoNotificationBus::Handler
, private AzToolsFramework::ToolsApplicationEvents::Bus::Handler
, private AzToolsFramework::EntityCompositionNotificationBus::Handler
, private AzToolsFramework::EditorEntityRuntimeActivationChangeNotificationBus::Handler
, private AZ::EntitySystemBus::Handler
{
Q_OBJECT;
public:
AZ_CLASS_ALLOCATOR(OutlinerListModel, AZ::SystemAllocator, 0);
//! Columns of data to display about each Entity.
enum Column
{
ColumnName, //!< Entity name
ColumnVisibilityToggle, //!< Visibility Icons
ColumnLockToggle, //!< Lock Icons
ColumnSortIndex, //!< Index of sort order
ColumnCount //!< Total number of columns
};
// Note: the ColumnSortIndex column isn't shown, hence the -1 and the need for a separate counter.
// A wrong column count number causes refresh issues and hover mismatch on model update.
static const int VisibleColumnCount = ColumnCount - 1;
enum EntryType
{
EntityType,
SliceEntityType,
SliceHandleType,
LayerType
};
enum Roles
{
VisibilityRole = Qt::UserRole + 1,
SliceBackgroundRole,
SliceEntityOverrideRole,
EntityIdRole,
EntityTypeRole,
LayerColorRole,
SelectedRole,
ChildSelectedRole,
PartiallyVisibleRole,
PartiallyLockedRole,
InLockedLayerRole,
InInvisibleLayerRole,
ChildCountRole,
ExpandedRole,
RoleCount
};
enum EntityIcon
{
SliceHandleIcon, // Icon used to decorate slice handles
BrokenSliceHandleIcon, // Icon used to decorate broken slice handles
SliceEntityIcon, // Icon used to decorate entities that are part of a slice instantiation
StandardEntityIcon // Icon used to decorate entities that are not part of a slice instantiation
};
enum class GlobalSearchCriteriaFlags : int
{
Unlocked = 1 << static_cast<int>(AzQtComponents::OutlinerSearchWidget::GlobalSearchCriteria::Unlocked),
Locked = 1 << static_cast<int>(AzQtComponents::OutlinerSearchWidget::GlobalSearchCriteria::Locked),
Visible = 1 << static_cast<int>(AzQtComponents::OutlinerSearchWidget::GlobalSearchCriteria::Visible),
Hidden = 1 << static_cast<int>(AzQtComponents::OutlinerSearchWidget::GlobalSearchCriteria::Hidden)
};
struct ComponentTypeValue
{
AZ::Uuid m_uuid;
int m_globalVal;
};
// Spacing is appropriate and matches the outliner concept work from the UI team.
static const int s_OutlinerSpacing = 5;
static bool s_paintingName;
OutlinerListModel(QObject* parent = nullptr);
~OutlinerListModel();
void Initialize();
// Qt overrides.
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
int columnCount(const QModelIndex&) const override;
QVariant data(const QModelIndex& index, int role) const override;
bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override;
QModelIndex index(int row, int column, const QModelIndex& parent = QModelIndex()) const override;
QModelIndex parent(const QModelIndex& index) const override;
Qt::ItemFlags flags(const QModelIndex& index) const override;
Qt::DropActions supportedDropActions() const override;
Qt::DropActions supportedDragActions() const override;
bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) override;
bool canDropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) const override;
QMimeData* mimeData(const QModelIndexList& indexes) const override;
QStringList mimeTypes() const override;
QString GetSliceAssetName(const AZ::EntityId& entityId) const;
QModelIndex GetIndexFromEntity(const AZ::EntityId& entityId, int column = 0) const;
AZ::EntityId GetEntityFromIndex(const QModelIndex& index) const;
bool FilterEntity(const AZ::EntityId& entityId);
void EnableAutoExpand(bool enable);
AZStd::string GetFilterString() const
{
return m_filterString;
}
static int GetLayerStripeWidth()
{
return 1;
}
void SetSortMode(EntityOutliner::DisplaySortMode sortMode) { m_sortMode = sortMode; }
void SetDropOperationInProgress(bool inProgress);
Q_SIGNALS:
void ExpandEntity(const AZ::EntityId& entityId, bool expand);
void SelectEntity(const AZ::EntityId& entityId, bool select);
void EnableSelectionUpdates(bool enable);
void ResetFilter();
void ReapplyFilter();
public Q_SLOTS:
void SearchStringChanged(const AZStd::string& filter);
void SearchFilterChanged(const AZStd::vector<ComponentTypeValue>& componentFilters);
void OnEntityExpanded(const AZ::EntityId& entityId);
void OnEntityCollapsed(const AZ::EntityId& entityId);
// Buffer Processing Slots - These are called using single-shot events when the buffers begin to fill.
bool CanReparentEntities(const AZ::EntityId& newParentId, const AzToolsFramework::EntityIdList& selectedEntityIds) const;
bool ReparentEntities(const AZ::EntityId& newParentId, const AzToolsFramework::EntityIdList& selectedEntityIds, const AZ::EntityId& beforeEntityId = AZ::EntityId());
//! Use the current filter setting and re-evaluate the filter.
void InvalidateFilter();
protected:
//! Editor entity context notification bus
void OnEditorEntityDuplicated(const AZ::EntityId& oldEntity, const AZ::EntityId& newEntity) override;
void OnContextReset() override;
void OnStartPlayInEditorBegin() override;
void OnStartPlayInEditor() override;
bool m_beginStartPlayInEditor = false;
void QueueEntityUpdate(AZ::EntityId entityId);
void QueueAncestorUpdate(AZ::EntityId entityId);
void QueueEntityToExpand(AZ::EntityId entityId, bool expand);
void ProcessEntityUpdates();
void ProcessEntityInfoResetEnd();
AZStd::unordered_set<AZ::EntityId> m_entitySelectQueue;
AZStd::unordered_set<AZ::EntityId> m_entityExpandQueue;
AZStd::unordered_set<AZ::EntityId> m_entityChangeQueue;
bool m_entityChangeQueued;
bool m_entityLayoutQueued;
bool m_dropOperationInProgress = false;
bool m_autoExpandEnabled = true;
bool m_layoutResetQueued = false;
AZStd::string m_filterString;
AZStd::vector<ComponentTypeValue> m_componentFilters;
bool m_isFilterDirty = true;
void OnEntityCompositionChanged(const AzToolsFramework::EntityIdList& entityIds) override;
void OnEntityInitialized(const AZ::EntityId& entityId) override;
void AfterEntitySelectionChanged(const AzToolsFramework::EntityIdList&, const AzToolsFramework::EntityIdList&) override;
//! AzToolsFramework::EditorEntityInfoNotificationBus::Handler
//! Get notifications when the EditorEntityInfo changes so we can update our model
void OnEntityInfoResetBegin() override;
void OnEntityInfoResetEnd() override;
void OnEntityInfoUpdatedAddChildBegin(AZ::EntityId parentId, AZ::EntityId childId) override;
void OnEntityInfoUpdatedAddChildEnd(AZ::EntityId parentId, AZ::EntityId childId) override;
void OnEntityInfoUpdatedRemoveChildBegin(AZ::EntityId parentId, AZ::EntityId childId) override;
void OnEntityInfoUpdatedRemoveChildEnd(AZ::EntityId parentId, AZ::EntityId childId) override;
void OnEntityInfoUpdatedOrderBegin(AZ::EntityId parentId, AZ::EntityId childId, AZ::u64 index) override;
void OnEntityInfoUpdatedOrderEnd(AZ::EntityId parentId, AZ::EntityId childId, AZ::u64 index) override;
void OnEntityInfoUpdatedSelection(AZ::EntityId entityId, bool selected) override;
void OnEntityInfoUpdatedLocked(AZ::EntityId entityId, bool locked) override;
void OnEntityInfoUpdatedVisibility(AZ::EntityId entityId, bool visible) override;
void OnEntityInfoUpdatedName(AZ::EntityId entityId, const AZStd::string& name) override;
void OnEntityInfoUpdateSliceOwnership(AZ::EntityId entityId) override;
void OnEntityInfoUpdatedUnsavedChanges(AZ::EntityId entityId) override;
// AzToolsFramework::EditorEntityRuntimeActivationChangeNotificationBus::Handler
void OnEntityRuntimeActivationChanged(AZ::EntityId entityId, bool activeOnStart) override;
// Drag/Drop of components from Component Palette.
bool dropMimeDataComponentPalette(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent);
// Drag/Drop of entities.
bool canDropMimeDataForEntityIds(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) const;
bool dropMimeDataEntities(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent);
// Drag/Drop of assets from asset browser.
using ComponentAssetPair = AZStd::pair<AZ::TypeId, AZ::Data::AssetId>;
using ComponentAssetPairs = AZStd::vector<ComponentAssetPair>;
using SliceAssetList = AZStd::vector<AZ::Data::AssetId>;
void DecodeAssetMimeData(const QMimeData* data, ComponentAssetPairs& componentAssetPairs, SliceAssetList& sliceAssets) const;
bool DropMimeDataAssets(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent);
bool CanDropMimeDataAssets(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) const;
QMap<int, QVariant> itemData(const QModelIndex &index) const;
QVariant dataForAll(const QModelIndex& index, int role) const;
QVariant dataForName(const QModelIndex& index, int role) const;
QVariant dataForVisibility(const QModelIndex& index, int role) const;
QVariant dataForLock(const QModelIndex& index, int role) const;
QVariant dataForSortIndex(const QModelIndex& index, int role) const;
//! Request a hierarchy expansion
void ExpandAncestors(const AZ::EntityId& entityId);
bool IsExpanded(const AZ::EntityId& entityId) const;
AZStd::unordered_map<AZ::EntityId, bool> m_entityExpansionState;
void RestoreDescendantExpansion(const AZ::EntityId& entityId);
void RestoreDescendantSelection(const AZ::EntityId& entityId);
bool IsFiltered(const AZ::EntityId& entityId) const;
AZStd::unordered_map<AZ::EntityId, bool> m_entityFilteredState;
bool HasSelectedDescendant(const AZ::EntityId& entityId) const;
bool AreAllDescendantsSameLockState(const AZ::EntityId& entityId) const;
bool AreAllDescendantsSameVisibleState(const AZ::EntityId& entityId) const;
enum LayerProperty
{
Locked,
Invisible
};
bool IsInLayerWithProperty(AZ::EntityId entityId, const LayerProperty& layerProperty) const;
// These are needed until we completely disassociated selection control from the outliner state to
// keep track of selection state before/during/after filtering and searching
AzToolsFramework::EntityIdList m_unfilteredSelectionEntityIds;
void CacheSelectionIfAppropriate();
void RestoreSelectionIfAppropriate();
bool ShouldOverrideUnfilteredSelection();
EntityOutliner::DisplaySortMode m_sortMode;
private:
QVariant GetEntityIcon(const AZ::EntityId& id) const;
QVariant GetEntityTooltip(const AZ::EntityId& id) const;
const char* circleIconColor = "#ff7b00";
const int circleIconDiameter = 5;
const int maskDiameter = 8;
};
class OutlinerCheckBox : public QCheckBox
{
Q_OBJECT
public:
explicit OutlinerCheckBox(QWidget* parent = nullptr);
void draw(QPainter* painter);
private:
const int m_toggleColumnWidth = 16;
};
/*!
* OutlinerItemDelegate exists to render custom item-types.
* Other item-types render in the default fashion.
*/
class OutlinerItemDelegate
: public QStyledItemDelegate
{
public:
AZ_CLASS_ALLOCATOR(OutlinerItemDelegate, AZ::SystemAllocator, 0);
OutlinerItemDelegate(QWidget* parent = nullptr);
// Qt overrides
void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override;
QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const override;
protected:
bool editorEvent(QEvent* event, QAbstractItemModel* model, const QStyleOptionViewItem& option, const QModelIndex& index) override;
private:
// The layer stripe is a continuous line from the layer's color box to the last entity in the layer.
// Two layer stripes are drawn, one in the color of the layer and other in the border box color.
void DrawLayerStripeAndBorder(QPainter* painter, int stripeX, int top, int bottom, QColor layerBorderColor, QColor layerColor) const;
// Draws all UI related to layers for the current row.
void DrawLayerUI(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index, const AZ::EntityId& entityId,
bool isSelected, bool isHovered) const;
// Layers with unsaved changes, and layers with errors, have additional text added to their strings.
QString GetLayerInfoString(const AZ::EntityId& entityId) const;
// Entity names are offset vertically if they are in a layer, and generally to better line up with icons.
int GetEntityNameVerticalOffset(const AZ::EntityId& entityId) const;
struct CheckboxGroup
{
OutlinerCheckBox m_default;
OutlinerCheckBox m_mixed;
OutlinerCheckBox m_layerOverride;
OutlinerCheckBox m_defaultHover;
OutlinerCheckBox m_mixedHover;
OutlinerCheckBox m_layerOverrideHover;
CheckboxGroup(QWidget* parent, AZStd::string prefix, OutlinerListModel::Roles mixed, OutlinerListModel::Roles layer);
OutlinerCheckBox* SelectCheckboxToRender(const QModelIndex& index, bool isHovered);
private:
OutlinerListModel::Roles m_mixedRole;
OutlinerListModel::Roles m_layerRole;
};
// Mutability added because these are being used ONLY as renderers
// for custom check boxes. The decision of whether or not to draw
// them checked is tracked by the individual entities and items in
// the hierarchy cache.
mutable CheckboxGroup m_visibilityCheckBoxes;
mutable CheckboxGroup m_lockCheckBoxes;
const int m_layerDividerLineHeight = 1;
const int m_lastEntityInLayerDividerLineHeight = 1;
const int m_toggleColumnWidth = 16;
// this is a cache, and is hence mutable
mutable QRect m_cachedBoundingRectOfTallCharacter;
struct OutlinerListModelColorConfig
{
QColor outlinerSelectionColor = QColor(255, 255, 255, 45);
QColor outlinerHoverColor = QColor(255, 255, 255, 30);
QColor layerBGColor = "#2F2F2F";
QColor layerChildBGColor = "#333333";
QColor layerBorderTopColor = "#515151";
QColor layerBorderBottomColor = "#252525";
QColor selectedLayerBGColor = "#676767";
QColor hoveredLayerBGColor = "#4B4B4B";
QColor sliceRootBackgroundColor = "#1E252F";
QColor sliceRootBorderColor = "#1E252F";
QColor selectedSliceRootBackgroundColor = "#47487B";
QColor selectedSliceRootBorderColor = "#2F306D";
QColor sliceEntityColor = "#4285F4";
QColor sliceOverrideColor = "#FF7B00";
int visibilityColumnWidth = 20;
int lockColumnWidth = 20;
};
OutlinerListModelColorConfig m_outlinerConfig;
};
Q_DECLARE_METATYPE(AZ::ComponentTypeList); // allows type to be stored by QVariable
#endif
@@ -0,0 +1,218 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include "ComponentEntityEditorPlugin_precompiled.h"
#include "OutlinerSearchWidget.h"
#include <AzQtComponents/Components/FlowLayout.h>
#include <AzQtComponents/Components/StyleManager.h>
#include <QLabel>
#include <QTreeView>
#include <QTextDocument>
#include <QPainter>
#include <QToolButton>
namespace AzQtComponents
{
OutlinerIcons::OutlinerIcons()
{
m_globalIcons[static_cast<int>(OutlinerSearchWidget::GlobalSearchCriteria::Unlocked)] = QIcon(QString(":Icons/unlocked.svg"));
m_globalIcons[static_cast<int>(OutlinerSearchWidget::GlobalSearchCriteria::Locked)] = QIcon(QString(":Icons/locked.svg"));
m_globalIcons[static_cast<int>(OutlinerSearchWidget::GlobalSearchCriteria::Visible)] = QIcon(QString(":Icons/visb.svg"));
m_globalIcons[static_cast<int>(OutlinerSearchWidget::GlobalSearchCriteria::Hidden)] = QIcon(QString(":Icons/visb_hidden.svg"));
m_globalIcons[static_cast<int>(OutlinerSearchWidget::GlobalSearchCriteria::Separator)] = QIcon();
}
OutlinerSearchTypeSelector::OutlinerSearchTypeSelector(QWidget* parent)
: SearchTypeSelector(parent)
{
}
bool OutlinerSearchTypeSelector::filterItemOut(int unfilteredDataIndex, bool itemMatchesFilter, bool categoryMatchesFilter)
{
bool unfilteredIndexInvalid = (unfilteredDataIndex >= static_cast<int>(OutlinerSearchWidget::GlobalSearchCriteria::FirstRealFilter));
return SearchTypeSelector::filterItemOut(unfilteredDataIndex, itemMatchesFilter, categoryMatchesFilter) && unfilteredIndexInvalid;
}
void OutlinerSearchTypeSelector::initItem(QStandardItem* item, const SearchTypeFilter& filter, int unfilteredDataIndex)
{
if (filter.displayName != "--------")
{
item->setCheckable(true);
item->setCheckState(filter.enabled ? Qt::Checked : Qt::Unchecked);
}
if (unfilteredDataIndex < static_cast<int>(OutlinerSearchWidget::GlobalSearchCriteria::FirstRealFilter))
{
item->setIcon(OutlinerIcons::GetInstance().GetIcon(unfilteredDataIndex));
}
}
int OutlinerSearchTypeSelector::GetNumFixedItems()
{
return static_cast<int>(OutlinerSearchWidget::GlobalSearchCriteria::FirstRealFilter);
}
OutlinerCriteriaButton::OutlinerCriteriaButton(QString labelText, QWidget* parent, int index)
: FilterCriteriaButton(labelText, parent)
{
if (index >= 0 && index < static_cast<int>(OutlinerSearchWidget::GlobalSearchCriteria::FirstRealFilter))
{
QLabel* icon = new QLabel(this);
icon->setStyleSheet(m_tagLabel->styleSheet() + "border: 0px; background-color: transparent;");
icon->setPixmap(OutlinerIcons::GetInstance().GetIcon(index).pixmap(10, 10));
m_frameLayout->insertWidget(0, icon);
}
}
OutlinerSearchWidget::OutlinerSearchWidget(QWidget* parent)
: FilteredSearchWidget(parent, true)
{
SetupOwnSelector(new OutlinerSearchTypeSelector(assetTypeSelectorButton()));
const SearchTypeFilterList globalMenu{
{"Global Settings", "Unlocked"},
{"Global Settings", "Locked"},
{"Global Settings", "Visible"},
{"Global Settings", "Hidden"},
{"Global Settings", "--------"}
};
int value = 0;
for (const SearchTypeFilter& filter : globalMenu)
{
AddTypeFilter(filter.category, filter.displayName, QVariant::fromValue<AZ::Uuid>(AZ::Uuid::Create()), value);
++value;
}
}
OutlinerSearchWidget::~OutlinerSearchWidget()
{
delete m_delegate;
m_delegate = nullptr;
delete m_selector;
m_selector = nullptr;
}
void OutlinerSearchWidget::SetupPaintDelegates()
{
m_delegate = new OutlinerSearchItemDelegate(m_selector->GetTree());
m_selector->GetTree()->setItemDelegate(m_delegate);
m_delegate->SetSelector(m_selector);
}
FilterCriteriaButton* OutlinerSearchWidget::createCriteriaButton(const SearchTypeFilter& filter, int filterIndex)
{
return new OutlinerCriteriaButton(filter.displayName, this, filterIndex);
}
OutlinerSearchItemDelegate::OutlinerSearchItemDelegate(QWidget* parent) : QStyledItemDelegate(parent)
{
}
void OutlinerSearchItemDelegate::PaintRichText(QPainter* painter, QStyleOptionViewItem& opt, QString& text) const
{
int textDocDrawYOffset = 3;
QPoint paintertextDocRenderOffset = QPoint(-2, -1);
QTextDocument textDoc;
textDoc.setDefaultFont(opt.font);
opt.palette.color(QPalette::Text);
textDoc.setDefaultStyleSheet("body {color: " + opt.palette.color(QPalette::Text).name() + "}");
textDoc.setHtml("<body>" + text + "</body>");
QRect textRect = opt.widget->style()->proxy()->subElementRect(QStyle::SE_ItemViewItemText, &opt);
painter->translate(textRect.topLeft() - paintertextDocRenderOffset);
textDoc.setTextWidth(textRect.width());
textDoc.drawContents(painter, QRectF(0, textDocDrawYOffset, textRect.width(), textRect.height() + textDocDrawYOffset));
}
void OutlinerSearchItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option,
const QModelIndex& index) const
{
bool isGlobalOption = false;
painter->save();
QStyleOptionViewItem opt = option;
initStyleOption(&opt, index);
const QWidget* widget = option.widget;
QStyle* style = widget ? widget->style() : QApplication::style();
if (!opt.icon.isNull())
{
// Draw the icon if there is one.
QRect r = style->subElementRect(QStyle::SubElement::SE_ItemViewItemDecoration, &opt, widget);
r.setX(-r.width());
QIcon::Mode mode = QIcon::Normal;
QIcon::State state = QIcon::On;
opt.icon.paint(painter, r, opt.decorationAlignment, mode, state);
opt.icon = QIcon();
opt.decorationSize = QSize(0, 0);
isGlobalOption = true;
}
// Handle the separator
if (opt.text.contains("-------"))
{
// Draw this item as a solid line.
painter->setPen(QColor(FilteredSearchWidget::GetSeparatorColor()));
painter->drawLine(0, opt.rect.center().y() + 3, opt.rect.right(), opt.rect.center().y() + 4);
}
else
{
if (m_selector->GetFilterString().length() > 0 && !isGlobalOption && opt.features & QStyleOptionViewItem::ViewItemFeature::HasCheckIndicator)
{
// Create rich text menu text to show filterstring
QString label{ opt.text };
opt.text = "";
style->drawControl(QStyle::CE_ItemViewItem, &opt, painter, widget);
int highlightTextIndex = 0;
do
{
// Find filter term within the text.
highlightTextIndex = label.lastIndexOf(m_selector->GetFilterString(), highlightTextIndex - 1, Qt::CaseInsensitive);
if (highlightTextIndex >= 0)
{
// Insert background-color terminator at appropriate place to return to normal text.
label.insert(highlightTextIndex + m_selector->GetFilterString().length(), "</span>");
// Insert background-color command at appropriate place to highlight filter term.
label.insert(highlightTextIndex, "<span style=\"background-color: " + FilteredSearchWidget::GetBackgroundColor() + "\">");
}
} while (highlightTextIndex > 0);// Repeat in case there are multiple occurrences.
PaintRichText(painter, opt, label);
}
else
{
// There's no filter to apply, just draw it.
QString label = opt.text;
opt.text = "";
style->drawControl(QStyle::CE_ItemViewItem, &opt, painter, widget);
PaintRichText(painter, opt, label);
}
}
painter->restore();
}
QSize OutlinerSearchItemDelegate::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const
{
QStyleOptionViewItem opt = option;
initStyleOption(&opt, index);
if (opt.text.contains("-------"))
{
return QSize(0, 6);
}
return QStyledItemDelegate::sizeHint(option, index);
}
}
#include <UI/Outliner/moc_OutlinerSearchWidget.cpp>
@@ -0,0 +1,113 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#if !defined(Q_MOC_RUN)
#include <AzQtComponents/AzQtComponentsAPI.h>
#include <AzQtComponents/Components/FilteredSearchWidget.h>
#include <QStyledItemDelegate>
#include <QStandardItem>
#endif
#if !defined(DEFINED_QMETATYPE_UUID)
#define DEFINED_QMETATYPE_UUID
Q_DECLARE_METATYPE(AZ::Uuid);
#endif
namespace Ui
{
class OutlinerSearchWidget;
}
namespace AzQtComponents
{
class OutlinerSearchItemDelegate;
class OutlinerSearchTypeSelector
: public SearchTypeSelector
{
public:
OutlinerSearchTypeSelector(QWidget* parent = nullptr);
protected:
// can be used to override the logic when adding items in RepopulateDataModel
bool filterItemOut(int unfilteredDataIndex, bool itemMatchesFilter, bool categoryMatchesFilter) override;
void initItem(QStandardItem* item, const SearchTypeFilter& filter, int unfilteredDataIndex) override;
int GetNumFixedItems() override;
};
class OutlinerCriteriaButton
: public FilterCriteriaButton
{
Q_OBJECT
public:
explicit OutlinerCriteriaButton(QString labelText, QWidget* parent = nullptr, int index = -1);
};
class OutlinerSearchWidget
: public FilteredSearchWidget
{
Q_OBJECT
public:
explicit OutlinerSearchWidget(QWidget* parent = nullptr);
~OutlinerSearchWidget() override;
FilterCriteriaButton* createCriteriaButton(const SearchTypeFilter& filter, int filterIndex) override;
enum class GlobalSearchCriteria : int
{
Unlocked,
Locked,
Visible,
Hidden,
Separator,
FirstRealFilter
};
protected:
void SetupPaintDelegates() override;
private:
OutlinerSearchItemDelegate* m_delegate = nullptr;
};
class OutlinerIcons
{
public:
static OutlinerIcons& GetInstance()
{
static OutlinerIcons instance;
return instance;
}
OutlinerIcons(OutlinerIcons const &) = delete;
void operator=(OutlinerIcons const &) = delete;
QIcon& GetIcon(int iconWanted) { return m_globalIcons[iconWanted]; }
private:
OutlinerIcons();
QIcon m_globalIcons[static_cast<int>(AzQtComponents::OutlinerSearchWidget::GlobalSearchCriteria::FirstRealFilter)];
};
class OutlinerSearchItemDelegate : public QStyledItemDelegate
{
public:
explicit OutlinerSearchItemDelegate(QWidget* parent = nullptr);
void PaintRichText(QPainter* painter, QStyleOptionViewItem& opt, QString& text) const;
void SetSelector(SearchTypeSelector* selector) { m_selector = selector; }
// QStyleItemDelegate overrides.
void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override;
QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const override;
private:
SearchTypeSelector* m_selector = nullptr;
};
}
@@ -0,0 +1,45 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include "ComponentEntityEditorPlugin_precompiled.h"
#include "OutlinerSortFilterProxyModel.hxx"
#include <AzCore/Component/ComponentApplication.h>
#include <AzCore/Component/Entity.h>
#include "OutlinerListModel.hxx"
OutlinerSortFilterProxyModel::OutlinerSortFilterProxyModel(QObject* pParent)
: QSortFilterProxyModel(pParent)
{
}
void OutlinerSortFilterProxyModel::UpdateFilter()
{
invalidateFilter();
}
bool OutlinerSortFilterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const
{
QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
QVariant visibilityData = sourceModel()->data(index, OutlinerListModel::VisibilityRole);
return visibilityData.isValid() ? visibilityData.toBool() : true;
}
bool OutlinerSortFilterProxyModel::lessThan(const QModelIndex& leftIndex, const QModelIndex& rightIndex) const
{
return sourceModel()->data(leftIndex).toString() < sourceModel()->data(rightIndex).toString();
}
void OutlinerSortFilterProxyModel::sort(int /*column*/, Qt::SortOrder /*order*/)
{
// override any attempts to change sort
QSortFilterProxyModel::sort(OutlinerListModel::ColumnSortIndex, Qt::SortOrder::AscendingOrder);
}
#include <UI/Outliner/moc_OutlinerSortFilterProxyModel.cpp>
@@ -0,0 +1,44 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#ifndef OUTLINER_SORT_FILTER_PROXY_MODEL_H
#define OUTLINER_SORT_FILTER_PROXY_MODEL_H
#if !defined(Q_MOC_RUN)
#include <AzCore/base.h>
#include <QtCore/QSortFilterProxyModel>
#include <AzCore/Memory/SystemAllocator.h>
#endif
#pragma once
/*!
* Enables the Outliner to filter entries based on search string.
* Enables the Outliner to do custom sorting on entries.
*/
class OutlinerSortFilterProxyModel
: public QSortFilterProxyModel
{
Q_OBJECT
public:
AZ_CLASS_ALLOCATOR(OutlinerSortFilterProxyModel, AZ::SystemAllocator, 0);
OutlinerSortFilterProxyModel(QObject* pParent = nullptr);
void UpdateFilter();
// Qt overrides
bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override;
bool lessThan(const QModelIndex& left, const QModelIndex& right) const override;
void sort(int column, Qt::SortOrder order) override;
private:
QString m_filterName;
};
#endif
@@ -0,0 +1,430 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include "ComponentEntityEditorPlugin_precompiled.h"
#include "OutlinerTreeView.hxx"
#include "OutlinerListModel.hxx"
#include <AzCore/Component/EntityId.h>
#include <AzCore/std/sort.h>
#include <AzCore/std/algorithm.h>
#include <AzToolsFramework/Entity/EditorEntityHelpers.h>
#include <AzQtComponents/Components/Style.h>
#include <QDrag>
#include <QPainter>
#include <QHeaderView>
OutlinerTreeView::OutlinerTreeView(QWidget* pParent)
: QTreeView(pParent)
, m_queuedMouseEvent(nullptr)
, m_draggingUnselectedItem(false)
{
setUniformRowHeights(true);
setHeaderHidden(true);
}
OutlinerTreeView::~OutlinerTreeView()
{
ClearQueuedMouseEvent();
}
void OutlinerTreeView::setAutoExpandDelay(int delay)
{
m_expandOnlyDelay = delay;
}
void OutlinerTreeView::ClearQueuedMouseEvent()
{
if (m_queuedMouseEvent)
{
delete m_queuedMouseEvent;
m_queuedMouseEvent = nullptr;
}
}
void OutlinerTreeView::mousePressEvent(QMouseEvent* event)
{
//postponing normal mouse pressed logic until mouse is released or dragged
//this means selection occurs on mouse released now
//this is to support drag/drop of non-selected items
ClearQueuedMouseEvent();
m_queuedMouseEvent = new QMouseEvent(*event);
}
void OutlinerTreeView::mouseReleaseEvent(QMouseEvent* event)
{
if (m_queuedMouseEvent && !m_draggingUnselectedItem)
{
// mouseMoveEvent will set the state to be DraggingState, which will make Qt ignore
// mousePressEvent in QTreeViewPrivate::expandOrCollapseItemAtPos. So we manually
// and temporarily set it to EditingState.
QAbstractItemView::State stateBefore = QAbstractItemView::state();
QAbstractItemView::setState(QAbstractItemView::State::EditingState);
//treat this as a mouse pressed event to process selection etc
processQueuedMousePressedEvent(m_queuedMouseEvent);
QAbstractItemView::setState(stateBefore);
}
ClearQueuedMouseEvent();
m_draggingUnselectedItem = false;
QTreeView::mouseReleaseEvent(event);
}
void OutlinerTreeView::mouseDoubleClickEvent(QMouseEvent* event)
{
//cancel pending mouse press
ClearQueuedMouseEvent();
QTreeView::mouseDoubleClickEvent(event);
}
void OutlinerTreeView::mouseMoveEvent(QMouseEvent* event)
{
// Store selection mode
QAbstractItemView::SelectionMode selectionModeBefore = selectionMode();
// Disable selection for the pending click so selection is maintained for dragging
setSelectionMode(QAbstractItemView::NoSelection);
// If a mouse event is queued, treat this as a mouse pressed event to process everything
// but selection, but use the position data from the mousePress message
if (m_queuedMouseEvent)
{
processQueuedMousePressedEvent(m_queuedMouseEvent);
}
// Process mouse movement as normal, potentially triggering drag and drop
QTreeView::mouseMoveEvent(event);
// Restore selection state
setSelectionMode(selectionModeBefore);
}
void OutlinerTreeView::focusInEvent(QFocusEvent* event)
{
//cancel pending mouse press
ClearQueuedMouseEvent();
QTreeView::focusInEvent(event);
}
void OutlinerTreeView::focusOutEvent(QFocusEvent* event)
{
//cancel pending mouse press
ClearQueuedMouseEvent();
QTreeView::focusOutEvent(event);
}
void OutlinerTreeView::startDrag(Qt::DropActions supportedActions)
{
//if we are attempting to drag an unselected item then we must special case drag and drop logic
//QAbstractItemView::startDrag only supports selected items
if (m_queuedMouseEvent)
{
QModelIndex index = indexAt(m_queuedMouseEvent->pos());
if (!index.isValid() || index.column() != 0)
{
return;
}
if (!selectionModel()->isSelected(index))
{
startCustomDrag({ index }, supportedActions);
return;
}
}
if (!selectionModel()->selectedIndexes().empty())
{
startCustomDrag(selectionModel()->selectedIndexes(), supportedActions);
return;
}
}
void OutlinerTreeView::dragMoveEvent(QDragMoveEvent* event)
{
if (m_expandOnlyDelay >= 0)
{
m_expandTimer.start(m_expandOnlyDelay, this);
}
QTreeView::dragMoveEvent(event);
}
void OutlinerTreeView::dropEvent(QDropEvent* event)
{
emit ItemDropped();
QTreeView::dropEvent(event);
m_draggingUnselectedItem = false;
}
QColor OutlinerTreeView::GetHierarchyLineColor(bool isSliceEntity, bool isSelected) const
{
if (isSliceEntity)
{
if (isSelected)
{
return m_colorConfig.hierarchyLinesSlicesSelected;
}
else
{
return m_colorConfig.hierarchyLinesSlices;
}
}
else
{
if (isSelected)
{
return m_colorConfig.hierarchyLinesNonSliceEntitiesSelected;
}
else
{
return m_colorConfig.hierarchyLinesNonSliceEntities;
}
}
}
void OutlinerTreeView::DrawLayerUI(QPainter* painter, const QRect& rect, const QModelIndex& index) const
{
bool isSelected = selectionModel()->isSelected(index);
QColor layerBranchesBGColor = isSelected ? m_colorConfig.layerChildBGSelectionColor : m_colorConfig.layerChildBackgroundColor;
painter->save();
painter->setRenderHint(QPainter::RenderHint::Antialiasing, false);
bool hasLayerAncestor = false;
for (QModelIndex ancestorIndex = index.parent(); ancestorIndex.isValid(); ancestorIndex = ancestorIndex.parent())
{
auto ancestorType = OutlinerListModel::EntryType(ancestorIndex.data(OutlinerListModel::EntityTypeRole).value<int>());
if (ancestorType == OutlinerListModel::LayerType)
{
hasLayerAncestor = true;
break;
}
}
if (hasLayerAncestor)
{
QPainterPath layerBGPath;
QRect layerBGRect(rect);
layerBGRect.setLeft(layerBGRect.left() + indentation());
layerBGPath.addRect(layerBGRect);
painter->fillPath(layerBGPath, layerBranchesBGColor);
}
OutlinerListModel::EntryType indexEntryType = OutlinerListModel::EntryType(index.data(OutlinerListModel::EntityTypeRole).value<int>());
if (indexEntryType == OutlinerListModel::LayerType)
{
QColor layerColor = index.data(OutlinerListModel::LayerColorRole).value<QColor>();
QPainterPath layerIconPath;
const int layerSquareSize = GetLayerSquareSize();
QPoint layerBoxOffset(1 + OutlinerListModel::GetLayerStripeWidth()*2, (rect.height() - layerSquareSize) / 2);
QRect layerIconRect(rect.topRight() + layerBoxOffset, QSize(layerSquareSize, layerSquareSize));
layerIconPath.addRect(layerIconRect);
painter->fillPath(layerIconPath, layerColor);
}
painter->restore();
}
void OutlinerTreeView::drawBranches(QPainter* painter, const QRect& rect, const QModelIndex& index) const
{
DrawLayerUI(painter, rect, index);
// Make sure the base class is called after the layer rect is drawn,
// so that the foldout arrow draws on top of the layer color.
QTreeView::drawBranches(painter, rect, index);
// No need to draw connecting lines if this has no parent.
if (!index.parent().isValid())
{
return;
}
QPen branchLinePen;
branchLinePen.setWidthF(m_branchLineWidth);
int lineBaseX = rect.right();
QModelIndex previousIndex = index;
for (QModelIndex ancestorIndex = index.parent(); ancestorIndex.isValid(); ancestorIndex = ancestorIndex.parent())
{
auto ancestorType = OutlinerListModel::EntryType(ancestorIndex.data(OutlinerListModel::EntityTypeRole).value<int>());
bool isSliceEntity = ancestorType == OutlinerListModel::SliceEntityType || ancestorType == OutlinerListModel::SliceHandleType;
bool isSelected = selectionModel()->isSelected(index);
branchLinePen.setColor(GetHierarchyLineColor(isSliceEntity, isSelected));
// Layers don't have connecting lines drawn.
if (ancestorType == OutlinerListModel::LayerType)
{
// Layers can only have other layers as parents, so once a layer is found in a hierarchy
// the line drawing can stop.
break;
}
painter->save();
painter->setRenderHint(QPainter::RenderHint::Antialiasing, false);
painter->setPen(branchLinePen);
int rectHalfHeight = rect.height() / 2;
if (previousIndex == index)
{
// draw a horizontal line from the parent branch to the item
// if the item has children offset the drawn line to compensate for drawn expander buttons
bool hasChildren = previousIndex.model()->index(0, 0, previousIndex).isValid();
int horizontalLineY = rect.top() + rectHalfHeight;
int horizontalLineLeft = rect.right() - indentation() * 1.5f;
int horizontalLineRight = hasChildren ? (lineBaseX - indentation()) : (lineBaseX - indentation() * 0.5f);
painter->drawLine(horizontalLineLeft, horizontalLineY, horizontalLineRight, horizontalLineY);
}
// draw a vertical line segment connecting parent to child and child to child
// if this is the last item, only draw half the line to terminate the segment
bool hasNext = previousIndex.sibling(previousIndex.row() + 1, previousIndex.column()).isValid();
if (hasNext || previousIndex == index)
{
int verticalLineX = lineBaseX - indentation() * 1.5f;
int verticalLineTop = rect.top();
int verticalLineBottom = hasNext ? rect.bottom() : rect.bottom() - rectHalfHeight;
painter->drawLine(verticalLineX, verticalLineTop, verticalLineX, verticalLineBottom);
}
painter->restore();
lineBaseX -= indentation();
previousIndex = ancestorIndex;
}
}
void OutlinerTreeView::timerEvent(QTimerEvent* event)
{
if (event->timerId() == m_expandTimer.timerId())
{
//duplicates functionality from QTreeView, but won't collapse an already expanded item
QPoint pos = this->viewport()->mapFromGlobal(QCursor::pos());
if (state() == QAbstractItemView::DraggingState && this->rect().contains(pos))
{
QModelIndex index = indexAt(pos);
if (!isExpanded(index))
{
setExpanded(index, true);
}
}
m_expandTimer.stop();
}
QTreeView::timerEvent(event);
}
void OutlinerTreeView::processQueuedMousePressedEvent(QMouseEvent* event)
{
//interpret the mouse event as a button press
QMouseEvent mousePressedEvent(
QEvent::MouseButtonPress,
event->localPos(),
event->windowPos(),
event->screenPos(),
event->button(),
event->buttons(),
event->modifiers(),
event->source());
QTreeView::mousePressEvent(&mousePressedEvent);
}
void OutlinerTreeView::startCustomDrag(const QModelIndexList& indexList, Qt::DropActions supportedActions)
{
m_draggingUnselectedItem = true;
//sort by container entity depth and order in hierarchy for proper drag image and drop order
QModelIndexList indexListSorted = indexList;
AZStd::unordered_map<AZ::EntityId, AZStd::list<AZ::u64>> locations;
for (auto index : indexListSorted)
{
AZ::EntityId entityId(index.data(OutlinerListModel::EntityIdRole).value<AZ::u64>());
AzToolsFramework::GetEntityLocationInHierarchy(entityId, locations[entityId]);
}
AZStd::sort(indexListSorted.begin(), indexListSorted.end(), [&locations](const QModelIndex& index1, const QModelIndex& index2) {
AZ::EntityId e1(index1.data(OutlinerListModel::EntityIdRole).value<AZ::u64>());
AZ::EntityId e2(index2.data(OutlinerListModel::EntityIdRole).value<AZ::u64>());
const auto& locationsE1 = locations[e1];
const auto& locationsE2 = locations[e2];
return AZStd::lexicographical_compare(locationsE1.begin(), locationsE1.end(), locationsE2.begin(), locationsE2.end());
});
//get the data for the unselected item(s)
QMimeData* mimeData = model()->mimeData(indexListSorted);
if (mimeData)
{
//initiate drag/drop for the item
QDrag* drag = new QDrag(this);
drag->setPixmap(QPixmap::fromImage(createDragImage(indexListSorted)));
drag->setMimeData(mimeData);
Qt::DropAction defDropAction = Qt::IgnoreAction;
if (defaultDropAction() != Qt::IgnoreAction && (supportedActions & defaultDropAction()))
{
defDropAction = defaultDropAction();
}
else if (supportedActions & Qt::CopyAction && dragDropMode() != QAbstractItemView::InternalMove)
{
defDropAction = Qt::CopyAction;
}
drag->exec(supportedActions, defDropAction);
}
}
QImage OutlinerTreeView::createDragImage(const QModelIndexList& indexList)
{
//generate a drag image of the item icon and text, normally done internally, and inaccessible
QRect rect(0, 0, 0, 0);
for (auto index : indexList)
{
if (index.column() != 0)
{
continue;
}
QRect itemRect = visualRect(index);
rect.setHeight(rect.height() + itemRect.height());
rect.setWidth(AZStd::GetMax(rect.width(), itemRect.width()));
}
QImage dragImage(rect.size(), QImage::Format_ARGB32_Premultiplied);
QPainter dragPainter(&dragImage);
dragPainter.setCompositionMode(QPainter::CompositionMode_Source);
dragPainter.fillRect(dragImage.rect(), Qt::transparent);
dragPainter.setCompositionMode(QPainter::CompositionMode_SourceOver);
dragPainter.setOpacity(0.35f);
dragPainter.fillRect(rect, QColor("#222222"));
dragPainter.setOpacity(1.0f);
int imageY = 0;
for (auto index : indexList)
{
if (index.column() != 0)
{
continue;
}
QRect itemRect = visualRect(index);
dragPainter.drawPixmap(QPoint(0, imageY),
model()->data(index, Qt::DecorationRole).value<QIcon>().pixmap(QSize(16, 16)));
dragPainter.setPen(
model()->data(index, Qt::ForegroundRole).value<QBrush>().color());
dragPainter.setFont(
font());
dragPainter.drawText(QRect(20, imageY, rect.width() - 20, rect.height()),
model()->data(index, Qt::DisplayRole).value<QString>());
imageY += itemRect.height();
}
dragPainter.end();
return dragImage;
}
#include <UI/Outliner/moc_OutlinerTreeView.cpp>
@@ -0,0 +1,98 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#ifndef OUTLINER_TREE_VIEW_H
#define OUTLINER_TREE_VIEW_H
#if !defined(Q_MOC_RUN)
#include <AzCore/base.h>
#include <AzCore/Memory/SystemAllocator.h>
#include <QBasicTimer>
#include <QEvent>
#include <QTreeView>
#endif
#pragma once
class QFocusEvent;
class QMouseEvent;
class OutlinerTreeViewModel;
//! This class largely exists to emit events for the OutlinerWidget to listen in on.
//! The logic for these events is best off not happening within the tree itself,
//! so it can be re-used in other interfaces.
//! The OutlinerWidget's need for these events is largely based on the concept of
//! delaying the Editor selection from updating with mouse interaction to
//! allow for dragging and dropping of entities from the outliner into the property editor
//! of other entities. If the selection updates instantly, this would never be possible.
class OutlinerTreeView
: public QTreeView
{
Q_OBJECT;
public:
AZ_CLASS_ALLOCATOR(OutlinerTreeView, AZ::SystemAllocator, 0);
OutlinerTreeView(QWidget* pParent = NULL);
virtual ~OutlinerTreeView();
void setAutoExpandDelay(int delay);
static int GetLayerSquareSize() { return 20; }
Q_SIGNALS:
void ItemDropped();
protected:
// Qt overrides
void mousePressEvent(QMouseEvent* event) override;
void mouseReleaseEvent(QMouseEvent* event) override;
void mouseDoubleClickEvent(QMouseEvent* event) override;
void mouseMoveEvent(QMouseEvent* event) override;
void focusInEvent(QFocusEvent* event) override;
void focusOutEvent(QFocusEvent* event) override;
void startDrag(Qt::DropActions supportedActions) override;
void dragMoveEvent(QDragMoveEvent* event) override;
void dropEvent(QDropEvent* event) override;
void drawBranches(QPainter* painter, const QRect& rect, const QModelIndex& index) const override;
void timerEvent(QTimerEvent* event) override;
private:
void ClearQueuedMouseEvent();
void processQueuedMousePressedEvent(QMouseEvent* event);
void startCustomDrag(const QModelIndexList& indexList, Qt::DropActions supportedActions);
QImage createDragImage(const QModelIndexList& indexList);
void DrawLayerUI(QPainter* painter, const QRect& rect, const QModelIndex& index) const;
QMouseEvent* m_queuedMouseEvent;
bool m_draggingUnselectedItem; // This is set when an item is dragged outside its bounding box.
int m_expandOnlyDelay = -1;
QBasicTimer m_expandTimer;
const int m_branchLineWidth = 1;
QColor GetHierarchyLineColor(bool isSliceEntity, bool isSelected) const;
struct OutlinerTreeViewColorConfig
{
QColor hierarchyLinesSlices = "#7B7B7B";
QColor hierarchyLinesSlicesSelected = "#7B7B7B";
QColor hierarchyLinesNonSliceEntities = "transparent";
QColor hierarchyLinesNonSliceEntitiesSelected = "transparent";
QColor layerChildBGSelectionColor = "#464747";
QColor layerChildBackgroundColor = "#333333";
};
OutlinerTreeViewColorConfig m_colorConfig;
};
#endif
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,210 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#ifndef OUTLINER_VIEW_H
#define OUTLINER_VIEW_H
#if !defined(Q_MOC_RUN)
#include "OutlinerCacheBus.h"
#include <AzCore/Memory/SystemAllocator.h>
#include <AzCore/base.h>
#include <AzToolsFramework/API/ToolsApplicationAPI.h>
#include <AzToolsFramework/ComponentMode/EditorComponentModeBus.h>
#include <AzToolsFramework/Entity/EditorEntityInfoBus.h>
#include <AzToolsFramework/Entity/EditorEntityContextBus.h>
#include <AzToolsFramework/Entity/SliceEditorEntityOwnershipServiceBus.h>
#include <AzToolsFramework/ToolsMessaging/EntityHighlightBus.h>
#include <AzToolsFramework/UI/SearchWidget/SearchWidgetTypes.hxx>
#include "OutlinerSearchWidget.h"
#include <QWidget>
#include <QtGui/QIcon>
#endif
#pragma once
class QAction;
namespace Ui
{
class OutlinerWidgetUI;
}
class QItemSelection;
class OutlinerListModel;
class OutlinerSortFilterProxyModel;
namespace EntityOutliner
{
class DisplayOptionsMenu;
enum class DisplaySortMode : unsigned char;
enum class DisplayOption : unsigned char;
}
class OutlinerWidget
: public QWidget
, private AzToolsFramework::EditorPickModeNotificationBus::Handler
, private AzToolsFramework::EntityHighlightMessages::Bus::Handler
, private OutlinerModelNotificationBus::Handler
, private AzToolsFramework::ToolsApplicationEvents::Bus::Handler
, private AzToolsFramework::EditorEntityContextNotificationBus::Handler
, private AzToolsFramework::SliceEditorEntityOwnershipServiceNotificationBus::Handler
, private AzToolsFramework::EditorEntityInfoNotificationBus::Handler
, private AzToolsFramework::ComponentModeFramework::EditorComponentModeNotificationBus::Handler
{
Q_OBJECT;
public:
AZ_CLASS_ALLOCATOR(OutlinerWidget, AZ::SystemAllocator, 0)
OutlinerWidget(QWidget* pParent = NULL, Qt::WindowFlags flags = Qt::WindowFlags());
virtual ~OutlinerWidget();
private Q_SLOTS:
void OnSelectionChanged(const QItemSelection&, const QItemSelection&);
void OnSearchTextChanged(const QString& activeTextFilter);
void OnFilterChanged(const AzQtComponents::SearchTypeFilterList& activeTypeFilters);
void OnSortModeChanged(EntityOutliner::DisplaySortMode sortMode);
void OnDisplayOptionChanged(EntityOutliner::DisplayOption displayOption, bool enable);
private:
void contextMenuEvent(QContextMenuEvent* event) override;
QString FindCommonSliceAssetName(const AZStd::vector<AZ::EntityId>& entityList) const;
AzFramework::EntityContextId GetPickModeEntityContextId();
// EntityHighlightMessages
virtual void EntityHighlightRequested(AZ::EntityId) override;
virtual void EntityStrongHighlightRequested(AZ::EntityId) override;
// EditorPickModeNotificationBus
void OnEntityPickModeStarted() override;
void OnEntityPickModeStopped() override;
// SliceEditorEntityOwnershipServiceNotificationBus
void OnSliceInstantiated(const AZ::Data::AssetId& /*sliceAssetId*/, AZ::SliceComponent::SliceInstanceAddress& /*sliceAddress*/, const AzFramework::SliceInstantiationTicket& /*ticket*/) override;
// EditorEntityContextNotificationBus
void OnEditorEntityCreated(const AZ::EntityId& entityId) override;
void OnStartPlayInEditor() override;
void OnStopPlayInEditor() override;
void OnFocusInEntityOutliner(const AzToolsFramework::EntityIdList& entityIdList) override;
/// AzToolsFramework::EditorEntityInfoNotificationBus implementation
void OnEntityInfoUpdatedAddChildEnd(AZ::EntityId /*parentId*/, AZ::EntityId /*childId*/) override;
void OnEntityInfoUpdatedName(AZ::EntityId entityId, const AZStd::string& /*name*/) override;
// EditorComponentModeNotificationBus
void EnteredComponentMode(const AZStd::vector<AZ::Uuid>& componentModeTypes) override;
void LeftComponentMode(const AZStd::vector<AZ::Uuid>& componentModeTypes) override;
// Build a selection object from the given entities. Entities already in the Widget's selection buffers are ignored.
template <class EntityIdCollection>
QItemSelection BuildSelectionFromEntities(const EntityIdCollection& entityIds);
Ui::OutlinerWidgetUI* m_gui;
OutlinerListModel* m_listModel;
OutlinerSortFilterProxyModel* m_proxyModel;
AZ::u64 m_selectionContextId;
AZStd::vector<AZ::EntityId> m_selectedEntityIds;
void PrepareSelection();
void DoCreateEntity();
void DoCreateEntityWithParent(const AZ::EntityId& parentId);
void DoShowSlice();
void DoDuplicateSelection();
void DoDeleteSelection();
void DoDeleteSelectionAndDescendants();
void DoRenameSelection();
void DoMoveEntityUp();
void DoMoveEntityDown();
void GoToEntitiesInViewport();
void DoSelectSliceRootAboveSelection();
void DoSelectSliceRootBelowSelection();
void DoSelectTopSliceRoot();
void DoSelectBottomSliceRoot();
void DoSelectSliceRootNextToSelection(bool above);
void DoSelectEdgeSliceRoot(bool top);
void SetIndexAsCurrentAndSelected(const QModelIndex& index);
void SetupActions();
void SelectSliceRoot();
QAction* m_actionToShowSlice;
QAction* m_actionToCreateEntity;
QAction* m_actionToDeleteSelection;
QAction* m_actionToDeleteSelectionAndDescendants;
QAction* m_actionToRenameSelection;
QAction* m_actionToReparentSelection;
QAction* m_actionToMoveEntityUp;
QAction* m_actionToMoveEntityDown;
QAction* m_actionGoToEntitiesInViewport;
QAction* m_actionToSelectSliceRootAboveSelection;
QAction* m_actionToSelectSliceRootBelowSelection;
QAction* m_actionToSelectTopSliceRoot;
QAction* m_actionToSelectBottomSliceRoot;
void OnTreeItemClicked(const QModelIndex &index);
void OnTreeItemExpanded(const QModelIndex &index);
void OnTreeItemCollapsed(const QModelIndex &index);
void OnExpandEntity(const AZ::EntityId& entityId, bool expand);
void OnSelectEntity(const AZ::EntityId& entityId, bool selected);
void OnEnableSelectionUpdates(bool enable);
void OnDropEvent();
bool m_inObjectPickMode;
void InvalidateFilter();
void ClearFilter();
AZ::EntityId GetEntityIdFromIndex(const QModelIndex& index) const;
QModelIndex GetIndexFromEntityId(const AZ::EntityId& entityId) const;
void ExtractEntityIdsFromSelection(const QItemSelection& selection, AzToolsFramework::EntityIdList& entityIdList) const;
// AzToolsFramework::OutlinerModelNotificationBus::Handler
// Receive notification from the outliner model that we should scroll
// to a given entity
void QueueScrollToNewContent(const AZ::EntityId& entityId) override;
void ScrollToNewContent();
bool m_scrollToNewContentQueued;
bool m_scrollToSelectedEntity;
bool m_dropOperationInProgress;
bool m_expandSelectedEntity;
bool m_focusInEntityOutliner;
AZ::EntityId m_scrollToEntityId;
void QueueUpdateSelection();
void UpdateSelection();
AzToolsFramework::EntityIdSet m_entitiesToSelect;
AzToolsFramework::EntityIdSet m_entitiesToDeselect;
AzToolsFramework::EntityIdSet m_entitiesSelectedByOutliner;
AzToolsFramework::EntityIdSet m_entitiesDeselectedByOutliner;
bool m_selectionChangeQueued;
bool m_selectionChangeInProgress;
bool m_enableSelectionUpdates;
QIcon m_emptyIcon;
QIcon m_clearIcon;
void QueueContentUpdateSort(const AZ::EntityId& entityId);
void SortContent();
EntityOutliner::DisplayOptionsMenu* m_displayOptionsMenu;
AzToolsFramework::EntityIdSet m_entitiesToSort;
EntityOutliner::DisplaySortMode m_sortMode;
bool m_sortContentQueued;
};
#endif
@@ -0,0 +1,121 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>OutlinerWidgetUI</class>
<widget class="QWidget" name="OutlinerWidgetUI">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>382</width>
<height>719</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="sizeConstraint">
<enum>QLayout::SetMinimumSize</enum>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<layout class="QHBoxLayout" name="m_horizontalLayout_search">
<property name="spacing">
<number>0</number>
</property>
<item>
<widget class="AzQtComponents::OutlinerSearchWidget" name="m_searchWidget" native="true"/>
</item>
</layout>
</item>
<item>
<widget class="QScrollArea" name="m_objectList">
<property name="focusPolicy">
<enum>Qt::ClickFocus</enum>
</property>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<property name="sizeAdjustPolicy">
<enum>QAbstractScrollArea::AdjustToContents</enum>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="m_objectList_Contents">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>382</width>
<height>705</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="OutlinerTreeView" name="m_objectTree" native="true"/>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>OutlinerTreeView</class>
<extends>QWidget</extends>
<header>UI/Outliner/OutlinerTreeView.hxx</header>
<container>1</container>
</customwidget>
<customwidget>
<class>AzQtComponents::OutlinerSearchWidget</class>
<extends>QWidget</extends>
<header>UI/Outliner/OutlinerSearchWidget.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>
@@ -0,0 +1,5 @@
<RCC>
<qresource prefix="/EntityOutliner">
<file>EntityOutliner.qss</file>
</qresource>
</RCC>
@@ -0,0 +1,56 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include "ComponentEntityEditorPlugin_precompiled.h"
#include "UI/QComponentEntityEditorMainWindow.h"
#include <AzCore/Component/Entity.h>
#include <AzCore/Component/ComponentApplicationBus.h>
#include <AzToolsFramework/API/ToolsApplicationAPI.h>
#include <AzToolsFramework/UI/PropertyEditor/ReflectedPropertyEditor.hxx>
#include <AzToolsFramework/UI/PropertyEditor/EntityPropertyEditor.hxx>
#include <QVBoxLayout>
QComponentEntityEditorInspectorWindow::QComponentEntityEditorInspectorWindow(QWidget* parent)
: QMainWindow(parent)
, m_propertyEditor(nullptr)
{
gEnv->pSystem->GetISystemEventDispatcher()->RegisterListener(this);
Init();
}
QComponentEntityEditorInspectorWindow::~QComponentEntityEditorInspectorWindow()
{
gEnv->pSystem->GetISystemEventDispatcher()->RemoveListener(this);
}
void QComponentEntityEditorInspectorWindow::OnSystemEvent([[maybe_unused]] ESystemEvent event, [[maybe_unused]] UINT_PTR wparam, [[maybe_unused]] UINT_PTR lparam)
{
}
void QComponentEntityEditorInspectorWindow::Init()
{
QVBoxLayout* layout = new QVBoxLayout();
m_propertyEditor = new AzToolsFramework::EntityPropertyEditor(nullptr);
layout->addWidget(m_propertyEditor);
QWidget* window = new QWidget();
window->setLayout(layout);
setCentralWidget(window);
}
///////////////////////////////////////////////////////////////////////////////
// End of context menu handling
///////////////////////////////////////////////////////////////////////////////
#include <UI/moc_QComponentEntityEditorMainWindow.cpp>
@@ -0,0 +1,63 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#if !defined(Q_MOC_RUN)
#include <QMainWindow>
#include <AzCore/Serialization/SerializeContext.h>
#endif
class QObjectPropertyModel;
class PropertyInfo;
namespace AZ
{
class Entity;
}
namespace AzToolsFramework
{
class ReflectedPropertyEditor;
class EntityPropertyEditor;
}
// This is the shell class to interface between Qt and the Sandbox. All Sandbox implementation is retained in an inherited class.
class QComponentEntityEditorInspectorWindow
: public QMainWindow
, public ISystemEventListener
{
Q_OBJECT
public:
explicit QComponentEntityEditorInspectorWindow(QWidget* parent = 0);
~QComponentEntityEditorInspectorWindow();
void Init();
// Used to receive events from widgets where SIGNALS aren't available or implemented yet.
// Required override.
void OnSystemEvent(ESystemEvent event, UINT_PTR wparam, UINT_PTR lparam) override;
// you are required to implement this to satisfy the unregister/registerclass requirements on "AzToolsFramework::RegisterViewPane"
// make sure you pick a unique GUID
static const GUID& GetClassID()
{
// {D7FEC1E3-8898-4D1F-8A9C-F8A161AF6746}
static const GUID guid =
{
0xD7FEC1E3, 0x8898, 0x4D1F, { 0x8a, 0x9c, 0xf8, 0xa1, 0x61, 0xaf, 0x67, 0x46 }
};
return guid;
}
AzToolsFramework::EntityPropertyEditor* GetPropertyEditor() { return m_propertyEditor; }
private:
AzToolsFramework::EntityPropertyEditor* m_propertyEditor;
};
@@ -0,0 +1,86 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include "ComponentEntityEditorPlugin_precompiled.h"
#include "CryEdit.h"
#include "UI/QComponentEntityEditorOutlinerWindow.h"
#include "UI/Outliner/OutlinerWidget.hxx"
#include <AzCore/Component/Entity.h>
#include <AzCore/Component/ComponentApplicationBus.h>
#include <AzToolsFramework/UI/Outliner/EntityOutlinerWidget.hxx>
#include <QVBoxLayout>
QComponentEntityEditorOutlinerWindow::QComponentEntityEditorOutlinerWindow(QWidget* parent)
: QMainWindow(parent)
, m_outlinerWidget(nullptr)
{
gEnv->pSystem->GetISystemEventDispatcher()->RegisterListener(this);
Init();
}
QComponentEntityEditorOutlinerWindow::~QComponentEntityEditorOutlinerWindow()
{
gEnv->pSystem->GetISystemEventDispatcher()->RemoveListener(this);
}
void QComponentEntityEditorOutlinerWindow::OnSystemEvent([[maybe_unused]] ESystemEvent event, [[maybe_unused]] UINT_PTR wparam, [[maybe_unused]] UINT_PTR lparam)
{
}
void QComponentEntityEditorOutlinerWindow::Init()
{
QVBoxLayout* layout = new QVBoxLayout();
m_outlinerWidget = new OutlinerWidget(nullptr);
layout->addWidget(m_outlinerWidget);
QWidget* window = new QWidget();
window->setLayout(layout);
setCentralWidget(window);
}
QEntityOutlinerWindow::QEntityOutlinerWindow(QWidget* parent)
: QMainWindow(parent)
, m_outlinerWidget(nullptr)
{
gEnv->pSystem->GetISystemEventDispatcher()->RegisterListener(this);
Init();
}
QEntityOutlinerWindow::~QEntityOutlinerWindow()
{
gEnv->pSystem->GetISystemEventDispatcher()->RemoveListener(this);
}
void QEntityOutlinerWindow::OnSystemEvent([[maybe_unused]] ESystemEvent event, [[maybe_unused]] UINT_PTR wparam, [[maybe_unused]] UINT_PTR lparam)
{
}
void QEntityOutlinerWindow::Init()
{
QVBoxLayout* layout = new QVBoxLayout();
m_outlinerWidget = new AzToolsFramework::EntityOutlinerWidget(nullptr);
layout->addWidget(m_outlinerWidget);
QWidget* window = new QWidget();
window->setLayout(layout);
setCentralWidget(window);
}
///////////////////////////////////////////////////////////////////////////////
// End of context menu handling
///////////////////////////////////////////////////////////////////////////////
#include <UI/moc_QComponentEntityEditorOutlinerWindow.cpp>
@@ -0,0 +1,95 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#if !defined(Q_MOC_RUN)
#include <QMainWindow>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzToolsFramework/Entity/EditorEntityContextBus.h>
#endif
class QObjectPropertyModel;
class PropertyInfo;
namespace AZ
{
class Entity;
}
namespace AzToolsFramework
{
class EntityOutlinerWidget;
}
class OutlinerWidget;
// This is the shell class to interface between Qt and the Sandbox. All Sandbox implementation is retained in an inherited class.
class QComponentEntityEditorOutlinerWindow
: public QMainWindow
, public ISystemEventListener
{
Q_OBJECT
public:
explicit QComponentEntityEditorOutlinerWindow(QWidget* parent = 0);
~QComponentEntityEditorOutlinerWindow();
void Init();
// Used to receive events from widgets where SIGNALS aren't available or implemented yet.
// Required override.
void OnSystemEvent(ESystemEvent event, UINT_PTR wparam, UINT_PTR lparam) override;
// you are required to implement this to satisfy the unregister/registerclass requirements on "AzToolsFramework::RegisterViewPane"
// make sure you pick a unique GUID
static const GUID& GetClassID()
{
// {A2B58C0B-811A-4773-A057-A02D4BB9A293}
static const GUID guid =
{
0xA2B58C0B, 0x811A, 0x4773, { 0xa0, 0x57, 0xa0, 0x2d, 0x4b, 0xb9, 0xa2, 0x93 }
};
return guid;
}
private:
OutlinerWidget* m_outlinerWidget;
};
// This is the shell class to interface between Qt and the Sandbox. All Sandbox implementation is retained in an inherited class.
class QEntityOutlinerWindow
: public QMainWindow
, public ISystemEventListener
{
Q_OBJECT
public:
explicit QEntityOutlinerWindow(QWidget* parent = 0);
~QEntityOutlinerWindow();
void Init();
// Used to receive events from widgets where SIGNALS aren't available or implemented yet.
// Required override.
void OnSystemEvent(ESystemEvent event, UINT_PTR wparam, UINT_PTR lparam) override;
// you are required to implement this to satisfy the unregister/registerclass requirements on "AzToolsFramework::RegisterViewPane"
// make sure you pick a unique GUID
static const GUID& GetClassID()
{
// {CEE50D0E-46A8-4CB4-9C5F-DCD374A78032}
static const GUID guid =
{
0xcee50d0e, 0x46a8, 0x4cb4, { 0x9c, 0x5f, 0xdc, 0xd3, 0x74, 0xa7, 0x80, 0x32 }
};
return guid;
}
private:
AzToolsFramework::EntityOutlinerWidget* m_outlinerWidget;
};
@@ -0,0 +1,99 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include "ComponentEntityEditorPlugin_precompiled.h"
#include "UI/QComponentLevelEntityEditorMainWindow.h"
#include <AzCore/Component/Entity.h>
#include <AzCore/Component/ComponentApplicationBus.h>
#include <AzToolsFramework/API/ToolsApplicationAPI.h>
#include <AzToolsFramework/UI/PropertyEditor/ReflectedPropertyEditor.hxx>
#include <AzToolsFramework/UI/PropertyEditor/EntityPropertyEditor.hxx>
#include <AzToolsFramework/Entity/EditorEntityContextBus.h>
#include <QVBoxLayout>
QComponentLevelEntityEditorInspectorWindow::QComponentLevelEntityEditorInspectorWindow(QWidget* parent)
: QMainWindow(parent)
, m_propertyEditor(nullptr)
{
GetIEditor()->RegisterNotifyListener(this);
AzToolsFramework::SliceMetadataEntityContextNotificationBus::Handler::BusConnect();
Init();
}
QComponentLevelEntityEditorInspectorWindow::~QComponentLevelEntityEditorInspectorWindow()
{
AzToolsFramework::SliceMetadataEntityContextNotificationBus::Handler::BusDisconnect();
GetIEditor()->UnregisterNotifyListener(this);
}
void QComponentLevelEntityEditorInspectorWindow::Init()
{
QVBoxLayout* layout = new QVBoxLayout();
m_propertyEditor = new AzToolsFramework::EntityPropertyEditor(nullptr, Qt::WindowFlags(), true);
layout->addWidget(m_propertyEditor);
// On initialization, notify our property editor about the root metadata entity if it exists
RefreshPropertyEditor();
QWidget* window = new QWidget();
window->setLayout(layout);
setCentralWidget(window);
}
void QComponentLevelEntityEditorInspectorWindow::OnMetadataEntityAdded(AZ::EntityId entityId)
{
AZ::EntityId rootSliceMetaDataEntity = GetRootMetaDataEntityId();
if (rootSliceMetaDataEntity == entityId)
{
AzToolsFramework::EntityIdSet entities;
entities.insert(rootSliceMetaDataEntity);
m_propertyEditor->SetOverrideEntityIds(entities);
}
}
void QComponentLevelEntityEditorInspectorWindow::RefreshPropertyEditor()
{
AZ::EntityId rootSliceMetaDataEntity = GetRootMetaDataEntityId();
OnMetadataEntityAdded(rootSliceMetaDataEntity);
}
AZ::EntityId QComponentLevelEntityEditorInspectorWindow::GetRootMetaDataEntityId() const
{
AZ::EntityId levelEntityId;
AzToolsFramework::ToolsApplicationRequestBus::BroadcastResult(levelEntityId, &AzToolsFramework::ToolsApplicationRequests::GetCurrentLevelEntityId);
return levelEntityId;
}
void QComponentLevelEntityEditorInspectorWindow::OnEditorNotifyEvent(EEditorNotifyEvent event)
{
switch (event)
{
// Refresh the Level Component Property Editor any time we start or end
// a level creation or load.
case eNotify_OnBeginLoad:
case eNotify_OnEndLoad:
case eNotify_OnBeginCreate:
case eNotify_OnEndCreate:
RefreshPropertyEditor();
break;
default:
break;
}
}
///////////////////////////////////////////////////////////////////////////////
// End of context menu handling
///////////////////////////////////////////////////////////////////////////////
#include <UI/moc_QComponentLevelEntityEditorMainWindow.cpp>
@@ -0,0 +1,71 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#if !defined(Q_MOC_RUN)
#include <QMainWindow>
#include <AzCore/Serialization/SerializeContext.h>
#include <Editor/IEditor.h>
#include <AzToolsFramework/Slice/SliceMetadataEntityContextBus.h>
#endif
class QObjectPropertyModel;
class PropertyInfo;
namespace AZ
{
class Entity;
}
namespace AzToolsFramework
{
class ReflectedPropertyEditor;
class EntityPropertyEditor;
}
// This is the shell class to interface between Qt and the Sandbox. All Sandbox implementation is retained in an inherited class.
class QComponentLevelEntityEditorInspectorWindow
: public QMainWindow
, public AzToolsFramework::SliceMetadataEntityContextNotificationBus::Handler
, private IEditorNotifyListener
{
Q_OBJECT
public:
explicit QComponentLevelEntityEditorInspectorWindow(QWidget* parent = 0);
~QComponentLevelEntityEditorInspectorWindow();
void Init();
// you are required to implement this to satisfy the unregister/registerclass requirements on "AzToolsFramework::RegisterViewPane"
// make sure you pick a unique GUID
static const GUID& GetClassID()
{
//{F539C646-7FC6-4AF4-BB58-F8A161AF6746}
static const GUID guid =
{
0xF539C646, 0x7FC6, 0x4AF4, { 0x8a, 0x9c, 0xf8, 0xa1, 0x61, 0xaf, 0x67, 0x46 }
};
return guid;
}
AzToolsFramework::EntityPropertyEditor* GetPropertyEditor() { return m_propertyEditor; }
private:
void OnMetadataEntityAdded(AZ::EntityId /*entityId*/) override;
void RefreshPropertyEditor();
AZ::EntityId GetRootMetaDataEntityId() const;
//////////////////////////////////////////////////////////////////////////
// IEditorEventListener
void OnEditorNotifyEvent(EEditorNotifyEvent event) override;
AzToolsFramework::EntityPropertyEditor* m_propertyEditor;
};
@@ -0,0 +1,55 @@
#
# Copyright (c) Contributors to the Open 3D Engine Project
#
# SPDX-License-Identifier: Apache-2.0 OR MIT
#
#
set(FILES
dllmain.cpp
ComponentEntityEditorPlugin.h
ComponentEntityEditorPlugin.cpp
SandboxIntegration.h
SandboxIntegration.cpp
ComponentEntityEditorPlugin_precompiled.h
UI/ComponentEntityEditorOutlinerWindow.qrc
UI/QComponentEntityEditorMainWindow.h
UI/QComponentEntityEditorMainWindow.cpp
UI/QComponentLevelEntityEditorMainWindow.h
UI/QComponentLevelEntityEditorMainWindow.cpp
UI/QComponentEntityEditorOutlinerWindow.h
UI/QComponentEntityEditorOutlinerWindow.cpp
UI/AssetCatalogModel.h
UI/AssetCatalogModel.cpp
UI/ComponentPalette/CategoriesList.h
UI/ComponentPalette/CategoriesList.cpp
UI/ComponentPalette/ComponentDataModel.h
UI/ComponentPalette/ComponentDataModel.cpp
UI/ComponentPalette/ComponentPaletteSettings.h
UI/ComponentPalette/ComponentPaletteWindow.h
UI/ComponentPalette/ComponentPaletteWindow.cpp
UI/ComponentPalette/FavoriteComponentList.h
UI/ComponentPalette/FavoriteComponentList.cpp
UI/ComponentPalette/FilteredComponentList.h
UI/ComponentPalette/FilteredComponentList.cpp
UI/ComponentPalette/InformationPanel.h
UI/ComponentPalette/InformationPanel.cpp
UI/Outliner/OutlinerDisplayOptionsMenu.h
UI/Outliner/OutlinerDisplayOptionsMenu.cpp
UI/Outliner/OutlinerTreeView.hxx
UI/Outliner/OutlinerTreeView.cpp
UI/Outliner/OutlinerWidget.hxx
UI/Outliner/OutlinerWidget.cpp
UI/Outliner/OutlinerCacheBus.h
UI/Outliner/OutlinerListModel.hxx
UI/Outliner/OutlinerListModel.cpp
UI/Outliner/OutlinerSearchWidget.h
UI/Outliner/OutlinerSearchWidget.cpp
UI/Outliner/OutlinerSortFilterProxyModel.hxx
UI/Outliner/OutlinerSortFilterProxyModel.cpp
UI/Outliner/OutlinerWidget.ui
UI/Outliner/resources.qrc
UI/Outliner/EntityOutliner.qss
Objects/ComponentEntityObject.h
Objects/ComponentEntityObject.cpp
)
@@ -0,0 +1,13 @@
#
# Copyright (c) Contributors to the Open 3D Engine Project
#
# SPDX-License-Identifier: Apache-2.0 OR MIT
#
#
set(FILES
Tests/test_Main.cpp
Tests/ComponentEntityObjectStateTests.cpp
Objects/ComponentEntityObject.h
Objects/ComponentEntityObject.cpp
)
@@ -0,0 +1,50 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include "ComponentEntityEditorPlugin_precompiled.h"
// All plugins suffer from the following warning:
// warning C4273: 'GetIEditor' : inconsistent dll linkage
// GetIEditor() is forward-declared using EDITOR_CORE_API, which without EDITOR_CORE set,
// results in dllimport rather than dllexport. This define ensure it's consistently and
// properly defined for export.
#define EDITOR_CORE
#include <platform.h>
#include <IEditor.h>
#include <Include/IPlugin.h>
#include <Include/IEditorClassFactory.h>
#include "ComponentEntityEditorPlugin.h"
#if AZ_TRAIT_OS_PLATFORM_APPLE || defined(AZ_PLATFORM_LINUX)
typedef HANDLE HINSTANCE;
#define DLL_PROCESS_ATTACH 1
#endif
IEditor* g_pEditor = nullptr;
//------------------------------------------------------------------
PLUGIN_API IPlugin* CreatePluginInstance(PLUGIN_INIT_PARAM* pInitParam)
{
g_pEditor = pInitParam->pIEditorInterface;
ISystem* pSystem = pInitParam->pIEditorInterface->GetSystem();
ModuleInitISystem(pSystem, "ComponentEntityEditorPlugin");
return new ComponentEntityEditorPlugin(g_pEditor);
}
//------------------------------------------------------------------
HINSTANCE g_hInstance = 0;
BOOL __stdcall DllMain(HINSTANCE hinstDLL, ULONG fdwReason, [[maybe_unused]] LPVOID lpvReserved)
{
if (fdwReason == DLL_PROCESS_ATTACH)
{
g_hInstance = hinstDLL;
}
return TRUE;
}