#include "CWorldEditor.h" #include "ui_CWorldEditor.h" #include "CConfirmUnlinkDialog.h" #include "CLayerEditor.h" #include "CTemplateMimeData.h" #include "WCreateTab.h" #include "WModifyTab.h" #include "WInstancesTab.h" #include "Editor/CAboutDialog.h" #include "Editor/CBasicViewport.h" #include "Editor/CExportGameDialog.h" #include "Editor/CNodeCopyMimeData.h" #include "Editor/CProjectSettingsDialog.h" #include "Editor/CQuickplayPropertyEditor.h" #include "Editor/CSelectionIterator.h" #include "Editor/UICommon.h" #include "Editor/PropertyEdit/CPropertyView.h" #include "Editor/ResourceBrowser/CResourceBrowser.h" #include "Editor/Widgets/WDraggableSpinBox.h" #include "Editor/Widgets/WVectorEditor.h" #include "Editor/Undo/UndoCommands.h" #include #include #include #include #include #include #include #include #include #include #include #include CWorldEditor::CWorldEditor(QWidget *parent) : INodeEditor(parent) , ui(new Ui::CWorldEditor) , mpArea(nullptr) , mpWorld(nullptr) , mpLinkDialog(new CLinkDialog(this, this)) , mpGeneratePropertyNamesDialog(new CGeneratePropertyNamesDialog(this)) , mpTweakEditor(new CTweakEditor(this)) , mIsMakingLink(false) , mpNewLinkSender(nullptr) , mpNewLinkReceiver(nullptr) { debugf("Creating World Editor"); ui->setupUi(this); UpdateWindowTitle(); mpSelection->SetAllowedNodeTypes(ENodeType::Script | ENodeType::Light); // Add resource browser to the layout QVBoxLayout *pLayout = new QVBoxLayout(); pLayout->setContentsMargins(0,0,0,0); CResourceBrowser *pResourceBrowser = gpEdApp->ResourceBrowser(); pResourceBrowser->setParent(this); pLayout->addWidget( pResourceBrowser ); ui->ResourceBrowserContainer->setLayout(pLayout); // Initialize splitter QList SplitterSizes; SplitterSizes << width() * 0.25 << width() * 0.53 << width() * 0.22; ui->splitter->setSizes(SplitterSizes); // Initialize UI stuff ResetCamera(); ui->MainViewport->SetScene(this, &mScene); ui->MainViewport->setAcceptDrops(true); ui->TransformSpinBox->SetOrientation(Qt::Horizontal); ui->TransformSpinBox->layout()->setContentsMargins(0,0,0,0); ui->CamSpeedSpinBox->SetDefaultValue(1.0); mpTransformCombo->setMinimumWidth(75); ui->MainToolBar->addActions(mGizmoActions); ui->MainToolBar->addWidget(mpTransformCombo); AddUndoActions(ui->menuEdit, ui->ActionCut); ui->menuEdit->insertSeparator(ui->ActionCut); // Initialize sidebar mpCurSidebar = nullptr; mpRightSidebarLayout = new QVBoxLayout(); mpRightSidebarLayout->setContentsMargins(0, 0, 0, 0); ui->RightSidebarFrame->setLayout(mpRightSidebarLayout); mpWorldInfoSidebar = new CWorldInfoSidebar(this); mpScriptSidebar = new CScriptEditSidebar(this); mpPoiMapSidebar = new CPoiMapSidebar(this); // Initialize edit mode toolbar mpEditModeButtonGroup = new QButtonGroup(this); connect(mpEditModeButtonGroup, SIGNAL(buttonClicked(int)), this, SLOT(ChangeEditMode(int))); AddEditModeButton( QIcon(":/icons/World.svg"), "Edit World Info",eWEM_EditWorldInfo ); AddEditModeButton( QIcon(":/icons/Modify.svg"), "Edit Script", eWEM_EditScript ); mpPoiMapAction = AddEditModeButton( QIcon(":/icons/PoiSymbol_24px.svg"), "Edit POI Mappings", eWEM_EditPOIMappings ); mpPoiMapAction->setVisible(false); ChangeEditMode(eWEM_EditWorldInfo); // Initialize actions addAction(ui->ActionIncrementGizmo); addAction(ui->ActionDecrementGizmo); AddUndoActions(ui->MainToolBar, ui->ActionLink); ui->MainToolBar->insertSeparator(ui->ActionLink); ui->ActionCut->setAutoRepeat(false); ui->ActionCut->setShortcut(QKeySequence::Cut); ui->ActionCopy->setAutoRepeat(false); ui->ActionCopy->setShortcut(QKeySequence::Copy); ui->ActionPaste->setAutoRepeat(false); ui->ActionPaste->setShortcut(QKeySequence::Paste); ui->ActionDelete->setAutoRepeat(false); ui->ActionDelete->setShortcut(QKeySequence::Delete); mpCollisionDialog = new CCollisionRenderSettingsDialog(this, this); // Quickplay buttons QToolButton* pQuickplayButton = new QToolButton(this); pQuickplayButton->setIcon( QIcon(":/icons/Play_32px.svg") ); pQuickplayButton->setPopupMode( QToolButton::MenuButtonPopup ); pQuickplayButton->setMenu( new CQuickplayPropertyEditor(mQuickplayParms, this) ); pQuickplayButton->setToolTip( "Quickplay" ); ui->MainToolBar->addSeparator(); mpQuickplayAction = ui->MainToolBar->addWidget(pQuickplayButton); mpQuickplayAction->setVisible(false); mpQuickplayAction->setEnabled(false); connect(pQuickplayButton, SIGNAL(pressed()), this, SLOT(LaunchQuickplay())); // "Open Recent" menu mpOpenRecentMenu = new QMenu(this); ui->ActionOpenRecent->setMenu(mpOpenRecentMenu); for (uint32 iAct = 0; iAct < mskMaxRecentProjects; iAct++) { QAction *pAction = new QAction(this); pAction->setVisible(false); pAction->setData((int) iAct); connect(pAction, SIGNAL(triggered(bool)), this, SLOT(OpenRecentProject())); mpOpenRecentMenu->addAction(pAction); mRecentProjectsActions[iAct] = pAction; } UpdateOpenRecentActions(); // Connect signals and slots connect(gpEdApp, SIGNAL(ActiveProjectChanged(CGameProject*)), this, SLOT(OnActiveProjectChanged(CGameProject*))); connect(gpEdApp->clipboard(), SIGNAL(dataChanged()), this, SLOT(OnClipboardDataModified())); connect(ui->MainViewport, SIGNAL(ViewportClick(SRayIntersection,QMouseEvent*)), this, SLOT(OnViewportClick(SRayIntersection,QMouseEvent*))); connect(ui->MainViewport, SIGNAL(InputProcessed(SRayIntersection,QMouseEvent*)), this, SLOT(OnViewportInputProcessed(SRayIntersection,QMouseEvent*))); connect(ui->MainViewport, SIGNAL(InputProcessed(SRayIntersection,QMouseEvent*)), this, SLOT(UpdateGizmoUI()) ); connect(ui->MainViewport, SIGNAL(InputProcessed(SRayIntersection,QMouseEvent*)), this, SLOT(UpdateStatusBar()) ); connect(ui->MainViewport, SIGNAL(InputProcessed(SRayIntersection,QMouseEvent*)), this, SLOT(UpdateCursor()) ); connect(ui->MainViewport, SIGNAL(GizmoMoved()), this, SLOT(OnGizmoMoved())); connect(ui->MainViewport, SIGNAL(CameraOrbit()), this, SLOT(UpdateCameraOrbit())); connect(this, SIGNAL(SelectionModified()), this, SLOT(OnSelectionModified())); connect(this, SIGNAL(SelectionTransformed()), this, SLOT(UpdateCameraOrbit())); connect(this, SIGNAL(PickModeEntered(QCursor)), this, SLOT(OnPickModeEnter(QCursor))); connect(this, SIGNAL(PickModeExited()), this, SLOT(OnPickModeExit())); connect(ui->TransformSpinBox, SIGNAL(ValueChanged(CVector3f)), this, SLOT(OnTransformSpinBoxModified(CVector3f))); connect(ui->TransformSpinBox, SIGNAL(EditingDone(CVector3f)), this, SLOT(OnTransformSpinBoxEdited(CVector3f))); connect(ui->CamSpeedSpinBox, SIGNAL(valueChanged(double)), this, SLOT(OnCameraSpeedChange(double))); connect(&UndoStack(), SIGNAL(indexChanged(int)), this, SLOT(OnUndoStackIndexChanged())); connect(ui->ActionOpenProject, SIGNAL(triggered()), this, SLOT(OpenProject())); connect(ui->ActionSave, SIGNAL(triggered()) , this, SLOT(Save())); connect(ui->ActionSaveAndRepack, SIGNAL(triggered()), this, SLOT(SaveAndRepack())); connect(ui->ActionExportGame, SIGNAL(triggered()), this, SLOT(ExportGame())); connect(ui->ActionProjectSettings, SIGNAL(triggered()), this, SLOT(OpenProjectSettings())); connect(ui->ActionCloseProject, SIGNAL(triggered()), this, SLOT(CloseProject())); connect(ui->ActionExit, SIGNAL(triggered()), this, SLOT(close())); connect(ui->ActionCut, SIGNAL(triggered()), this, SLOT(Cut())); connect(ui->ActionCopy, SIGNAL(triggered()), this, SLOT(Copy())); connect(ui->ActionPaste, SIGNAL(triggered()), this, SLOT(Paste())); connect(ui->ActionDelete, SIGNAL(triggered()), this, SLOT(DeleteSelection())); connect(ui->ActionSelectAll, SIGNAL(triggered()), this, SLOT(SelectAllTriggered())); connect(ui->ActionInvertSelection, SIGNAL(triggered()), this, SLOT(InvertSelectionTriggered())); connect(ui->ActionLink, SIGNAL(toggled(bool)), this, SLOT(OnLinkButtonToggled(bool))); connect(ui->ActionUnlink, SIGNAL(triggered()), this, SLOT(OnUnlinkClicked())); connect(ui->ActionEditTweaks, SIGNAL(triggered()), mpTweakEditor, SLOT(show())); connect(ui->ActionEditLayers, SIGNAL(triggered()), this, SLOT(EditLayers())); if (gTemplatesWritable) connect(ui->ActionGeneratePropertyNames, SIGNAL(triggered()), mpGeneratePropertyNamesDialog, SLOT(show())); else ui->ActionGeneratePropertyNames->setEnabled(false); connect(ui->ActionDrawWorld, SIGNAL(triggered()), this, SLOT(ToggleDrawWorld())); connect(ui->ActionDrawObjects, SIGNAL(triggered()), this, SLOT(ToggleDrawObjects())); connect(ui->ActionDrawCollision, SIGNAL(triggered()), this, SLOT(ToggleDrawCollision())); connect(ui->ActionDrawObjectCollision, SIGNAL(triggered()), this, SLOT(ToggleDrawObjectCollision())); connect(ui->ActionDrawLights, SIGNAL(triggered()), this, SLOT(ToggleDrawLights())); connect(ui->ActionDrawSky, SIGNAL(triggered()), this, SLOT(ToggleDrawSky())); connect(ui->ActionGameMode, SIGNAL(triggered()), this, SLOT(ToggleGameMode())); connect(ui->ActionDisableAlpha, SIGNAL(triggered()), this, SLOT(ToggleDisableAlpha())); connect(ui->ActionNoLighting, SIGNAL(triggered()), this, SLOT(SetNoLighting())); connect(ui->ActionBasicLighting, SIGNAL(triggered()), this, SLOT(SetBasicLighting())); connect(ui->ActionWorldLighting, SIGNAL(triggered()), this, SLOT(SetWorldLighting())); connect(ui->ActionNoBloom, SIGNAL(triggered()), this, SLOT(SetNoBloom())); connect(ui->ActionBloomMaps, SIGNAL(triggered()), this, SLOT(SetBloomMaps())); connect(ui->ActionFakeBloom, SIGNAL(triggered()), this, SLOT(SetFakeBloom())); connect(ui->ActionBloom, SIGNAL(triggered()), this, SLOT(SetBloom())); connect(ui->ActionIncrementGizmo, SIGNAL(triggered()), this, SLOT(IncrementGizmo())); connect(ui->ActionDecrementGizmo, SIGNAL(triggered()), this, SLOT(DecrementGizmo())); connect(ui->ActionCollisionRenderSettings, SIGNAL(triggered()), mpCollisionDialog, SLOT(show())); connect(ui->ActionAbout, SIGNAL(triggered(bool)), this, SLOT(About())); } CWorldEditor::~CWorldEditor() { mScene.ClearScene(); mpArea = nullptr; mpWorld = nullptr; if (gpResourceStore) gpResourceStore->DestroyUnreferencedResources(); // this should destroy the area! delete mpScriptSidebar; // For some reason WCreateTab filters an event during the viewport's destructor delete ui; } bool CWorldEditor::CloseWorld() { if (CheckUnsavedChanges()) { ExitPickMode(); ClearSelection(); ui->MainViewport->ResetHover(); mScene.ClearScene(); UndoStack().clear(); mpCollisionDialog->close(); mpLinkDialog->close(); mpQuickplayAction->setEnabled(false); mpArea = nullptr; mpWorld = nullptr; if (gpResourceStore) gpResourceStore->DestroyUnreferencedResources(); // this should destroy the area! UpdateWindowTitle(); ui->ActionSave->setEnabled(false); ui->ActionSaveAndRepack->setEnabled(false); ui->ActionEditLayers->setEnabled(false); emit MapChanged(mpWorld, mpArea); return true; } else return false; } bool CWorldEditor::SetArea(CWorld *pWorld, int AreaIndex) { if (!CloseWorld()) return false; ExitPickMode(); ui->MainViewport->ResetHover(); ClearSelection(); UndoStack().clear(); // Load new area mpWorld = pWorld; CAssetID AreaID = mpWorld->AreaResourceID(AreaIndex); CResourceEntry *pAreaEntry = gpResourceStore->FindEntry(AreaID); ASSERT(pAreaEntry); mpArea = pAreaEntry->Load(); ASSERT(mpArea); mpWorld->SetAreaLayerInfo(mpArea); mScene.SetActiveArea(mpWorld, mpArea); // Snap camera to new area CCamera *pCamera = &ui->MainViewport->Camera(); if (pCamera->MoveMode() == ECameraMoveMode::Free) { CTransform4f AreaTransform = mpArea->Transform(); CVector3f AreaPosition(AreaTransform[0][3], AreaTransform[1][3], AreaTransform[2][3]); pCamera->Snap(AreaPosition); } UpdateCameraOrbit(); // Update UI stuff UpdateWindowTitle(); CGameTemplate *pGame = NGameList::GetGameTemplate(mpArea->Game()); mpLinkDialog->SetGame(pGame); QString AreaName = TO_QSTRING(mpWorld->AreaInGameName(AreaIndex)); if (CurrentGame() < EGame::DKCReturns) debugf("Loaded area: %s (%s)", *mpArea->Entry()->Name(), *TO_TSTRING(AreaName)); else debugf("Loaded level: World %s / Area %s (%s)", *mpWorld->Entry()->Name(), *mpArea->Entry()->Name(), *TO_TSTRING(AreaName)); // Update paste action OnClipboardDataModified(); // Update toolbar actions ui->ActionSave->setEnabled(true); ui->ActionSaveAndRepack->setEnabled(true); ui->ActionEditLayers->setEnabled(true); mpQuickplayAction->setEnabled(true); // Emit signals emit MapChanged(mpWorld, mpArea); emit LayersModified(); return true; } void CWorldEditor::ResetCamera() { ui->MainViewport->Camera().Snap(CVector3f(0.f, 5.f, 1.f)); } bool CWorldEditor::HasAnyScriptNodesSelected() const { for (CSelectionIterator It(mpSelection); It; ++It) { if (It->NodeType() == ENodeType::Script) return true; } return false; } bool CWorldEditor::IsQuickplayEnabled() const { return mpQuickplayAction->isVisible() && mpQuickplayAction->isEnabled(); } CSceneViewport* CWorldEditor::Viewport() const { return ui->MainViewport; } // ************ PUBLIC SLOTS ************ void CWorldEditor::EditorTick(float) { // Update new link line UpdateNewLinkLine(); } void CWorldEditor::NotifyNodeAboutToBeDeleted(CSceneNode *pNode) { INodeEditor::NotifyNodeAboutToBeDeleted(pNode); if (ui->MainViewport->HoverNode() == pNode) ui->MainViewport->ResetHover(); } bool CWorldEditor::Save() { if (!mpArea) return true; bool SaveAreaSuccess = mpArea->Entry()->Save(); bool SaveEGMCSuccess = mpArea->PoiToWorldMap() ? mpArea->PoiToWorldMap()->Entry()->Save() : true; bool SaveWorldSuccess = mpWorld->Entry()->Save(); if (SaveAreaSuccess) mpArea->ClearExtraDependencies(); if (SaveAreaSuccess || SaveEGMCSuccess || SaveWorldSuccess) gpEdApp->NotifyAssetsModified(); if (SaveAreaSuccess && SaveEGMCSuccess && SaveWorldSuccess) { UndoStack().setClean(); setWindowModified(false); return true; } else { UICommon::ErrorMsg(this, "Area failed to save!"); return false; } } void CWorldEditor::Cut() { if (!mpSelection->IsEmpty()) { Copy(); UndoStack().push(new CDeleteSelectionCommand(this, "Cut")); } } void CWorldEditor::Copy() { if (!mpSelection->IsEmpty()) { CNodeCopyMimeData *pMimeData = new CNodeCopyMimeData(this); qApp->clipboard()->setMimeData(pMimeData); } } void CWorldEditor::Paste() { if (const CNodeCopyMimeData *pkMimeData = qobject_cast(qApp->clipboard()->mimeData())) { if (pkMimeData->Game() == CurrentGame()) { CVector3f PastePoint; if (ui->MainViewport->underMouse() && !ui->MainViewport->IsMouseInputActive()) PastePoint = ui->MainViewport->HoverPoint(); else { CRay Ray = ui->MainViewport->Camera().CastRay(CVector2f(0.f, 0.f)); SRayIntersection Intersect = ui->MainViewport->SceneRayCast(Ray); if (Intersect.Hit) PastePoint = Intersect.HitPoint; else PastePoint = Ray.PointOnRay(10.f); } CPasteNodesCommand *pCmd = new CPasteNodesCommand(this, mpScriptSidebar->CreateTab()->SpawnLayer(), PastePoint); UndoStack().push(pCmd); } } } void CWorldEditor::OpenProject() { UICommon::OpenProject(); } void CWorldEditor::OpenRecentProject() { QAction *pSender = qobject_cast(sender()); if (pSender) { QSettings Settings; QStringList RecentProjectsList = Settings.value("WorldEditor/RecentProjectsList").toStringList(); int ProjIndex = pSender->data().toInt(); QString ProjPath = RecentProjectsList[ProjIndex]; gpEdApp->OpenProject(ProjPath); } } void CWorldEditor::ExportGame() { QString IsoPath = UICommon::OpenFileDialog(this, "Select ISO", "*.iso *.gcm *.tgc *.wbfs"); if (IsoPath.isEmpty()) return; QString ExportDir = UICommon::OpenDirDialog(this, "Select output export directory"); if (ExportDir.isEmpty()) return; CExportGameDialog ExportDialog(IsoPath, ExportDir, this); if (ExportDialog.HasValidDisc()) ExportDialog.exec(); if (ExportDialog.ExportSucceeded()) { int OpenChoice = QMessageBox::information(this, "Export complete", "Export finished successfully! Open new project?", QMessageBox::Yes, QMessageBox::No); if (OpenChoice == QMessageBox::Yes) gpEdApp->OpenProject(ExportDialog.ProjectPath()); } } void CWorldEditor:: CloseProject() { gpEdApp->CloseProject(); SET_WINDOWTITLE_APPVARS( QString("%APP_FULL_NAME%") ); } void CWorldEditor::About() { CAboutDialog Dialog(this); Dialog.exec(); } void CWorldEditor::ChangeEditMode(int Mode) { // This function is connected to the edit mode QButtonGroup. ChangeEditMode((EWorldEditorMode) Mode); } void CWorldEditor::ChangeEditMode(EWorldEditorMode Mode) { mpEditModeButtonGroup->blockSignals(true); mpEditModeButtonGroup->button(Mode)->setChecked(true); mpEditModeButtonGroup->blockSignals(false); switch (Mode) { case eWEM_EditWorldInfo: SetSidebar(mpWorldInfoSidebar); break; case eWEM_EditScript: SetSidebar(mpScriptSidebar); break; case eWEM_EditPOIMappings: SetSidebar(mpPoiMapSidebar); break; default: ASSERT(false); break; } } void CWorldEditor::SetRenderingMergedWorld(bool RenderMerged) { Viewport()->SetRenderMergedWorld(RenderMerged); } void CWorldEditor::OpenProjectSettings() { CProjectSettingsDialog *pDialog = gpEdApp->ProjectDialog(); pDialog->show(); pDialog->raise(); } void CWorldEditor::OnActiveProjectChanged(CGameProject *pProj) { ui->ActionProjectSettings->setEnabled( pProj != nullptr ); ui->ActionCloseProject->setEnabled( pProj != nullptr ); mpPoiMapAction->setVisible( pProj != nullptr && pProj->Game() >= EGame::EchoesDemo && pProj->Game() <= EGame::Corruption ); mpQuickplayAction->setVisible( pProj != nullptr && NDolphinIntegration::IsQuickplaySupported(pProj) ); ResetCamera(); UpdateWindowTitle(); // Update tweak editor // We update this here to ensure we can update the menu item correctly without risking // that this function runs before the tweak editor has a chance to update its tweak list. mpTweakEditor->OnProjectChanged(pProj); ui->ActionEditTweaks->setEnabled( mpTweakEditor->HasTweaks() ); // Default bloom to Fake Bloom for Metroid Prime 3; disable for other games bool AllowBloom = (CurrentGame() == EGame::CorruptionProto || CurrentGame() == EGame::Corruption); AllowBloom ? SetFakeBloom() : SetNoBloom(); ui->menuBloom->setEnabled(AllowBloom); // Update recent projects list UpdateOpenRecentActions(); // Reset editor mode ChangeEditMode(eWEM_EditWorldInfo); } void CWorldEditor::OnLinksModified(const QList& rkInstances) { foreach (CScriptObject *pInstance, rkInstances) { CScriptNode *pNode = mScene.NodeForInstance(pInstance); pNode->LinksModified(); } if (!rkInstances.isEmpty()) emit InstanceLinksModified(rkInstances); } void CWorldEditor::OnPropertyModified(IProperty *pProp) { bool ShouldUpdateSelection = false; for (CSelectionIterator It(mpSelection); It; ++It) { CSceneNode* pNode = *It; if (pNode && pNode->NodeType() == ENodeType::Script) { CScriptNode* pScript = static_cast(pNode); pScript->PropertyModified(pProp); // If this is the name, update other parts of the UI to reflect the new value. if ( pProp->Name() == "Name" ) { UpdateStatusBar(); UpdateSelectionUI(); } else if (pProp->Name() == "Position" || pProp->Name() == "Rotation" || pProp->Name() == "Scale") { mpSelection->UpdateBounds(); } // Emit signal so other widgets can react to the property change emit PropertyModified(pProp, pScript->Instance()); } // If this is a model/character, then we'll treat this as a modified selection. This is to make sure the selection bounds updates. if (pProp->Type() == EPropertyType::Asset) { CAssetProperty *pAsset = TPropCast(pProp); const CResTypeFilter& rkFilter = pAsset->GetTypeFilter(); if (rkFilter.Accepts(EResourceType::Model) || rkFilter.Accepts(EResourceType::AnimSet) || rkFilter.Accepts(EResourceType::Character)) ShouldUpdateSelection = true; } else if (pProp->Type() == EPropertyType::AnimationSet) ShouldUpdateSelection = true; } if (ShouldUpdateSelection) { SelectionModified(); } } void CWorldEditor::SetSelectionActive(bool Active) { // Gather list of selected objects that actually have Active properties QVector Objects; for (CSelectionIterator It(mpSelection); It; ++It) { if (It->NodeType() == ENodeType::Script) { CScriptNode* pScript = static_cast(*It); CScriptObject* pInst = pScript->Instance(); Objects << pInst; } } if (!Objects.isEmpty()) { UndoStack().beginMacro("Toggle Active"); while (!Objects.isEmpty()) { QVector CommandObjects; CScriptTemplate* pTemplate = Objects[0]->Template(); CBoolProperty* pActiveProperty = pTemplate->ActiveProperty(); for (int ObjIdx = 0; ObjIdx < Objects.size(); ObjIdx++) { if (Objects[ObjIdx]->Template() == pTemplate) { CommandObjects << Objects[ObjIdx]; Objects.removeAt(ObjIdx); ObjIdx--; } } if (pActiveProperty) { CPropertyModel* pModel = qobject_cast( mpScriptSidebar->ModifyTab()->PropertyView()->model() ); CEditScriptPropertyCommand* pCommand = new CEditScriptPropertyCommand( pActiveProperty, CommandObjects, pModel ); pCommand->SaveOldData(); foreach (CScriptObject* pInstance, CommandObjects) pInstance->SetActive(Active); pCommand->SaveNewData(); UndoStack().push(pCommand); } } UndoStack().endMacro(); } } void CWorldEditor::SetSelectionInstanceNames(const QString& rkNewName, bool IsDone) { // todo: this only supports one node at a time because a macro prevents us from merging undo commands // this is fine right now because this function is only ever called with a selection of one node, but probably want to fix in the future /*if (mpSelection->Size() == 1 && mpSelection->Front()->NodeType() == eScriptNode) { CScriptNode *pNode = static_cast(mpSelection->Front()); CScriptObject *pInst = pNode->Instance(); if (pName) { TString NewName = TO_TSTRING(rkNewName); IPropertyValue *pOld = pName->RawValue()->Clone(); pInst->SetName(NewName); UndoStack().push(new CEditScriptPropertyCommand(pName, this, pOld, IsDone, "Edit Instance Name")); } }*/ } void CWorldEditor::SetSelectionLayer(CScriptLayer *pLayer) { QList ScriptNodes; for (CSelectionIterator It(mpSelection); It; ++It) { if (It->NodeType() == ENodeType::Script) ScriptNodes << static_cast(*It); } if (!ScriptNodes.isEmpty()) UndoStack().push(new CChangeLayerCommand(this, ScriptNodes, pLayer)); } void CWorldEditor::DeleteSelection() { if (HasAnyScriptNodesSelected()) { CDeleteSelectionCommand *pCmd = new CDeleteSelectionCommand(this); UndoStack().push(pCmd); } } void CWorldEditor::UpdateOpenRecentActions() { QSettings Settings; QStringList RecentProjectsList = Settings.value("WorldEditor/RecentProjectsList").toStringList(); // Bump the current project to the front CGameProject *pProj = gpEdApp->ActiveProject(); if (pProj) { QString ProjPath = TO_QSTRING(pProj->ProjectPath()); RecentProjectsList.removeAll(ProjPath); RecentProjectsList.prepend(ProjPath); } // Remove projects that don't exist anymore foreach (const QString& rkProj, RecentProjectsList) { if (!FileUtil::Exists( TO_TSTRING(rkProj) ) || rkProj.contains('\\') ) RecentProjectsList.removeAll(rkProj); } Settings.setValue("WorldEditor/RecentProjectsList", RecentProjectsList); // Set up the menu actions for (int iProj = 0; iProj < mskMaxRecentProjects; iProj++) { QAction *pAction = mRecentProjectsActions[iProj]; if (iProj < RecentProjectsList.size()) { QString ActionText = QString("&%1 %2").arg(iProj).arg(RecentProjectsList[iProj]); pAction->setText(ActionText); pAction->setVisible(true); } else pAction->setVisible(false); } } void CWorldEditor::UpdateWindowTitle() { QString WindowTitle = "%APP_FULL_NAME%"; CGameProject *pProj = gpEdApp->ActiveProject(); if (pProj) { WindowTitle += " - " + TO_QSTRING( pProj->Name() ); if (mpWorld) { WindowTitle += " - " + TO_QSTRING(mpWorld->InGameName()); if (mpArea && CurrentGame() < EGame::DKCReturns) WindowTitle += " - " + TO_QSTRING( mpWorld->AreaInGameName(mpArea->WorldIndex()) ); } } WindowTitle += "[*]"; SET_WINDOWTITLE_APPVARS(WindowTitle); } void CWorldEditor::UpdateStatusBar() { // Would be cool to do more frequent status bar updates with more info. Unfortunately, this causes lag. QString StatusText = ""; if (!mGizmoHovering) { if (ui->MainViewport->underMouse()) { CSceneNode *pHoverNode = ui->MainViewport->HoverNode(); if (pHoverNode && mpSelection->IsAllowedType(pHoverNode)) StatusText = TO_QSTRING(pHoverNode->Name()); } } if (ui->statusbar->currentMessage() != StatusText) ui->statusbar->showMessage(StatusText); } void CWorldEditor::UpdateGizmoUI() { // Update transform XYZ spin boxes if (!ui->TransformSpinBox->IsBeingEdited()) { CVector3f SpinBoxValue = CVector3f::skZero; // If the gizmo is transforming, use the total transform amount // Otherwise, use the first selected node transform, or 0 if no selection if (mShowGizmo) { switch (mGizmo.Mode()) { case CGizmo::EGizmoMode::Translate: if (mGizmoTransforming && mGizmo.HasTransformed()) SpinBoxValue = mGizmo.TotalTranslation(); else if (!mpSelection->IsEmpty()) SpinBoxValue = mpSelection->Front()->AbsolutePosition(); break; case CGizmo::EGizmoMode::Rotate: if (mGizmoTransforming && mGizmo.HasTransformed()) SpinBoxValue = mGizmo.TotalRotation(); else if (!mpSelection->IsEmpty()) SpinBoxValue = mpSelection->Front()->AbsoluteRotation().ToEuler(); break; case CGizmo::EGizmoMode::Scale: if (mGizmoTransforming && mGizmo.HasTransformed()) SpinBoxValue = mGizmo.TotalScale(); else if (!mpSelection->IsEmpty()) SpinBoxValue = mpSelection->Front()->AbsoluteScale(); break; default: break; } } else if (!mpSelection->IsEmpty()) SpinBoxValue = mpSelection->Front()->AbsolutePosition(); ui->TransformSpinBox->blockSignals(true); ui->TransformSpinBox->SetValue(SpinBoxValue); ui->TransformSpinBox->blockSignals(false); } // Update gizmo if (!mGizmoTransforming) { // Set gizmo transform if (!mpSelection->IsEmpty()) { mGizmo.SetPosition(mpSelection->Front()->AbsolutePosition()); mGizmo.SetLocalRotation(mpSelection->Front()->AbsoluteRotation()); } } } void CWorldEditor::UpdateSelectionUI() { // Update selection info text QString SelectionText; if (mpSelection->Size() == 1) SelectionText = TO_QSTRING(mpSelection->Front()->Name()); else if (mpSelection->Size() > 1) SelectionText = QString("%1 objects selected").arg(mpSelection->Size()); QFontMetrics Metrics(ui->SelectionInfoLabel->font()); SelectionText = Metrics.elidedText(SelectionText, Qt::ElideRight, ui->SelectionInfoFrame->width() - 10); if (ui->SelectionInfoLabel->text() != SelectionText) ui->SelectionInfoLabel->setText(SelectionText); // Update gizmo stuff UpdateGizmoUI(); } void CWorldEditor::UpdateCursor() { if (ui->MainViewport->IsCursorVisible() && !mPickMode) { CSceneNode *pHoverNode = ui->MainViewport->HoverNode(); if (ui->MainViewport->IsHoveringGizmo()) ui->MainViewport->SetCursorState(Qt::SizeAllCursor); else if ((pHoverNode) && mpSelection->IsAllowedType(pHoverNode)) ui->MainViewport->SetCursorState(Qt::PointingHandCursor); else ui->MainViewport->SetCursorState(Qt::ArrowCursor); } } void CWorldEditor::UpdateNewLinkLine() { // Check if there is a sender+receiver if (mpLinkDialog->isVisible() && mpLinkDialog->Sender() && mpLinkDialog->Receiver() && !mpLinkDialog->IsPicking()) { CVector3f Start = mScene.NodeForInstance(mpLinkDialog->Sender())->CenterPoint(); CVector3f End = mScene.NodeForInstance(mpLinkDialog->Receiver())->CenterPoint(); ui->MainViewport->SetLinkLineEnabled(true); ui->MainViewport->SetLinkLine(Start, End); } // Otherwise check whether there's just a sender or just a receiver else { CScriptObject *pSender = nullptr; CScriptObject *pReceiver = nullptr; if (mpLinkDialog->isVisible()) { if (mpLinkDialog->Sender() && !mpLinkDialog->IsPickingSender()) pSender = mpLinkDialog->Sender(); if (mpLinkDialog->Receiver() && !mpLinkDialog->IsPickingReceiver()) pReceiver = mpLinkDialog->Receiver(); } else if (mIsMakingLink && mpNewLinkSender) pSender = mpNewLinkSender; else if (mpScriptSidebar->ModifyTab()->IsPicking() && mpScriptSidebar->ModifyTab()->EditNode()->NodeType() == ENodeType::Script) pSender = static_cast(mpScriptSidebar->ModifyTab()->EditNode())->Instance(); // No sender and no receiver = no line if (!pSender && !pReceiver) ui->MainViewport->SetLinkLineEnabled(false); // Yes sender and yes receiver = yes line else if (pSender && pReceiver) { ui->MainViewport->SetLinkLineEnabled(true); ui->MainViewport->SetLinkLine( mScene.NodeForInstance(pSender)->CenterPoint(), mScene.NodeForInstance(pReceiver)->CenterPoint() ); } // Compensate for missing sender or missing receiver else { bool IsPicking = (mIsMakingLink || mpLinkDialog->IsPicking() || mpScriptSidebar->ModifyTab()->IsPicking()); if (ui->MainViewport->underMouse() && !ui->MainViewport->IsMouseInputActive() && IsPicking) { CSceneNode *pHoverNode = ui->MainViewport->HoverNode(); CScriptObject *pInst = (pSender ? pSender : pReceiver); CVector3f Start = mScene.NodeForInstance(pInst)->CenterPoint(); CVector3f End = (pHoverNode && pHoverNode->NodeType() == ENodeType::Script ? pHoverNode->CenterPoint() : ui->MainViewport->HoverPoint()); ui->MainViewport->SetLinkLineEnabled(true); ui->MainViewport->SetLinkLine(Start, End); } else ui->MainViewport->SetLinkLineEnabled(false); } } } void CWorldEditor::LaunchQuickplay() { CVector3f CameraPosition = Viewport()->Camera().Position(); LaunchQuickplayFromLocation(CameraPosition, false); } void CWorldEditor::LaunchQuickplayFromLocation(CVector3f Location, bool ForceAsSpawnPosition) { // This function should not be called if a level is not open in a project. ASSERT( gpEdApp->ActiveProject() != nullptr ); ASSERT( mpWorld && mpArea ); // Fill in parameters and start running SQuickplayParameters Parameters = mQuickplayParms; Parameters.BootWorldAssetID = mpWorld->ID().ToLong(); Parameters.BootAreaAssetID = mpArea->ID().ToLong(); Parameters.SpawnTransform = Viewport()->Camera().GetCameraTransform(); Parameters.SpawnTransform.SetTranslation(Location); if (ForceAsSpawnPosition) { Parameters.Features.SetFlag(EQuickplayFeature::JumpToArea); Parameters.Features.SetFlag(EQuickplayFeature::SetSpawnPosition); } NDolphinIntegration::LaunchQuickplay(this, gpEdApp->ActiveProject(), Parameters); } // ************ PROTECTED ************ QAction* CWorldEditor::AddEditModeButton(QIcon Icon, QString ToolTip, EWorldEditorMode Mode) { ASSERT(mpEditModeButtonGroup->button(Mode) == nullptr); QPushButton *pButton = new QPushButton(Icon, "", this); pButton->setCheckable(true); pButton->setToolTip(ToolTip); pButton->setIconSize(QSize(24, 24)); QAction *pAction = ui->EditModeToolBar->addWidget(pButton); mpEditModeButtonGroup->addButton(pButton, Mode); return pAction; } void CWorldEditor::SetSidebar(CWorldEditorSidebar *pSidebar) { if (mpCurSidebar == pSidebar) return; if (mpCurSidebar) { mpCurSidebar->SidebarClose(); mpRightSidebarLayout->removeWidget(mpCurSidebar); mpCurSidebar->setHidden(true); } mpCurSidebar = pSidebar; if (mpCurSidebar) { mpCurSidebar->SidebarOpen(); mpRightSidebarLayout->addWidget(mpCurSidebar); mpCurSidebar->setHidden(false); } } void CWorldEditor::GizmoModeChanged(CGizmo::EGizmoMode mode) { ui->TransformSpinBox->SetSingleStep( (mode == CGizmo::EGizmoMode::Rotate ? 1.0 : 0.1) ); ui->TransformSpinBox->SetDefaultValue( (mode == CGizmo::EGizmoMode::Scale ? 1.0 : 0.0) ); } // ************ PRIVATE SLOTS ************ void CWorldEditor::OnClipboardDataModified() { const QMimeData *pkClipboardMimeData = qApp->clipboard()->mimeData(); const CNodeCopyMimeData *pkMimeData = qobject_cast(pkClipboardMimeData); bool ValidMimeData = (pkMimeData && pkMimeData->Game() == CurrentGame()); ui->ActionPaste->setEnabled(ValidMimeData); } void CWorldEditor::OnSelectionModified() { ui->TransformSpinBox->setEnabled(!mpSelection->IsEmpty()); bool HasScriptNode = HasAnyScriptNodesSelected(); ui->ActionCut->setEnabled(HasScriptNode); ui->ActionCopy->setEnabled(HasScriptNode); ui->ActionDelete->setEnabled(HasScriptNode); UpdateCameraOrbit(); } void CWorldEditor::OnLinkButtonToggled(bool Enabled) { if (Enabled) { EnterPickMode(ENodeType::Script, true, false, false); connect(this, SIGNAL(PickModeClick(SRayIntersection,QMouseEvent*)), this, SLOT(OnLinkClick(SRayIntersection))); connect(this, SIGNAL(PickModeExited()), this, SLOT(OnLinkEnd())); mIsMakingLink = true; mpNewLinkSender = nullptr; mpNewLinkReceiver = nullptr; } else { if (mIsMakingLink) ExitPickMode(); } } void CWorldEditor::OnLinkClick(const SRayIntersection& rkIntersect) { if (!mpNewLinkSender) { mpNewLinkSender = static_cast(rkIntersect.pNode)->Instance(); } else { mpNewLinkReceiver = static_cast(rkIntersect.pNode)->Instance(); mpLinkDialog->NewLink(mpNewLinkSender, mpNewLinkReceiver); mpLinkDialog->show(); ExitPickMode(); } } void CWorldEditor::OnLinkEnd() { disconnect(this, SIGNAL(PickModeClick(SRayIntersection,QMouseEvent*)), this, SLOT(OnLinkClick(SRayIntersection))); disconnect(this, SIGNAL(PickModeExited()), this, SLOT(OnLinkEnd())); ui->ActionLink->setChecked(false); mIsMakingLink = false; mpNewLinkSender = nullptr; mpNewLinkReceiver = nullptr; } void CWorldEditor::OnUnlinkClicked() { QList SelectedScriptNodes; for (CSelectionIterator It(mpSelection); It; ++It) { if (It->NodeType() == ENodeType::Script) SelectedScriptNodes << static_cast(*It); } if (!SelectedScriptNodes.isEmpty()) { CConfirmUnlinkDialog Dialog(this); Dialog.exec(); if (Dialog.UserChoice() != CConfirmUnlinkDialog::EChoice::Cancel) { UndoStack().beginMacro("Unlink"); bool UnlinkIncoming = (Dialog.UserChoice() != CConfirmUnlinkDialog::EChoice::OutgoingOnly); bool UnlinkOutgoing = (Dialog.UserChoice() != CConfirmUnlinkDialog::EChoice::IncomingOnly); foreach (CScriptNode *pNode, SelectedScriptNodes) { CScriptObject *pInst = pNode->Instance(); if (UnlinkIncoming) { QVector LinkIndices; for (uint32 iLink = 0; iLink < pInst->NumLinks(ELinkType::Incoming); iLink++) LinkIndices << iLink; CDeleteLinksCommand *pCmd = new CDeleteLinksCommand(this, pInst, ELinkType::Incoming, LinkIndices); UndoStack().push(pCmd); } if (UnlinkOutgoing) { QVector LinkIndices; for (uint32 iLink = 0; iLink < pInst->NumLinks(ELinkType::Outgoing); iLink++) LinkIndices << iLink; CDeleteLinksCommand *pCmd = new CDeleteLinksCommand(this, pInst, ELinkType::Outgoing, LinkIndices); UndoStack().push(pCmd); } } UndoStack().endMacro(); } } } void CWorldEditor::OnPickModeEnter(QCursor Cursor) { ui->MainViewport->SetCursorState(Cursor); } void CWorldEditor::OnPickModeExit() { UpdateCursor(); } void CWorldEditor::UpdateCameraOrbit() { CCamera *pCamera = &ui->MainViewport->Camera(); if (!mpSelection->IsEmpty()) pCamera->SetOrbit(mpSelection->Bounds()); else if (mpArea) pCamera->SetOrbit(mpArea->AABox(), 1.2f); else pCamera->ResetOrbit(); } void CWorldEditor::OnCameraSpeedChange(double Speed) { static const double skDefaultSpeed = 1.0; ui->MainViewport->Camera().SetMoveSpeed(skDefaultSpeed * Speed); ui->CamSpeedSpinBox->blockSignals(true); ui->CamSpeedSpinBox->setValue(Speed); ui->CamSpeedSpinBox->blockSignals(false); } void CWorldEditor::OnTransformSpinBoxModified(CVector3f Value) { if (mpSelection->IsEmpty()) return; switch (mGizmo.Mode()) { // Use absolute position/rotation, but relative scale. (This way spinbox doesn't show preview multiplier) case CGizmo::EGizmoMode::Translate: { CVector3f Delta = Value - mpSelection->Front()->AbsolutePosition(); UndoStack().push(new CTranslateNodeCommand(this, mpSelection->SelectedNodeList(), Delta, mTranslateSpace)); break; } case CGizmo::EGizmoMode::Rotate: { CQuaternion Delta = CQuaternion::FromEuler(Value) * mpSelection->Front()->AbsoluteRotation().Inverse(); UndoStack().push(new CRotateNodeCommand(this, mpSelection->SelectedNodeList(), true, mGizmo.Position(), mGizmo.Rotation(), Delta, mRotateSpace)); break; } case CGizmo::EGizmoMode::Scale: { CVector3f Delta = Value / mpSelection->Front()->AbsoluteScale(); UndoStack().push(new CScaleNodeCommand(this, mpSelection->SelectedNodeList(), true, mGizmo.Position(), Delta)); break; } default: break; } UpdateGizmoUI(); } void CWorldEditor::OnTransformSpinBoxEdited(CVector3f) { if (mpSelection->IsEmpty()) return; if (mGizmo.Mode() == CGizmo::EGizmoMode::Translate) UndoStack().push(CTranslateNodeCommand::End()); else if (mGizmo.Mode() == CGizmo::EGizmoMode::Rotate) UndoStack().push(CRotateNodeCommand::End()); else if (mGizmo.Mode() == CGizmo::EGizmoMode::Scale) UndoStack().push(CScaleNodeCommand::End()); UpdateGizmoUI(); } void CWorldEditor::SelectAllTriggered() { FNodeFlags NodeFlags = CScene::NodeFlagsForShowFlags(ui->MainViewport->ShowFlags()); NodeFlags &= ~(ENodeType::Model | ENodeType::Static | ENodeType::Collision); SelectAll(NodeFlags); } void CWorldEditor::InvertSelectionTriggered() { FNodeFlags NodeFlags = CScene::NodeFlagsForShowFlags(ui->MainViewport->ShowFlags()); NodeFlags &= ~(ENodeType::Model | ENodeType::Static | ENodeType::Collision); InvertSelection(NodeFlags); } void CWorldEditor::ToggleDrawWorld() { ui->MainViewport->SetShowWorld(ui->ActionDrawWorld->isChecked()); } void CWorldEditor::ToggleDrawObjects() { ui->MainViewport->SetShowFlag(EShowFlag::ObjectGeometry, ui->ActionDrawObjects->isChecked()); } void CWorldEditor::ToggleDrawCollision() { ui->MainViewport->SetShowFlag(EShowFlag::WorldCollision, ui->ActionDrawCollision->isChecked()); } void CWorldEditor::ToggleDrawObjectCollision() { ui->MainViewport->SetShowFlag(EShowFlag::ObjectCollision, ui->ActionDrawObjectCollision->isChecked()); } void CWorldEditor::ToggleDrawLights() { ui->MainViewport->SetShowFlag(EShowFlag::Lights, ui->ActionDrawLights->isChecked()); } void CWorldEditor::ToggleDrawSky() { ui->MainViewport->SetShowFlag(EShowFlag::Sky, ui->ActionDrawSky->isChecked()); } void CWorldEditor::ToggleGameMode() { ui->MainViewport->SetGameMode(ui->ActionGameMode->isChecked()); } void CWorldEditor::ToggleDisableAlpha() { ui->MainViewport->Renderer()->ToggleAlphaDisabled(ui->ActionDisableAlpha->isChecked()); } void CWorldEditor::SetNoLighting() { CGraphics::sLightMode = CGraphics::ELightingMode::None; ui->ActionNoLighting->setChecked(true); ui->ActionBasicLighting->setChecked(false); ui->ActionWorldLighting->setChecked(false); } void CWorldEditor::SetBasicLighting() { CGraphics::sLightMode = CGraphics::ELightingMode::Basic; ui->ActionNoLighting->setChecked(false); ui->ActionBasicLighting->setChecked(true); ui->ActionWorldLighting->setChecked(false); } void CWorldEditor::SetWorldLighting() { CGraphics::sLightMode = CGraphics::ELightingMode::World; ui->ActionNoLighting->setChecked(false); ui->ActionBasicLighting->setChecked(false); ui->ActionWorldLighting->setChecked(true); } void CWorldEditor::SetNoBloom() { ui->MainViewport->Renderer()->SetBloom(EBloomMode::NoBloom); ui->ActionNoBloom->setChecked(true); ui->ActionBloomMaps->setChecked(false); ui->ActionFakeBloom->setChecked(false); ui->ActionBloom->setChecked(false); } void CWorldEditor::SetBloomMaps() { ui->MainViewport->Renderer()->SetBloom(EBloomMode::BloomMaps); ui->ActionNoBloom->setChecked(false); ui->ActionBloomMaps->setChecked(true); ui->ActionFakeBloom->setChecked(false); ui->ActionBloom->setChecked(false); } void CWorldEditor::SetFakeBloom() { ui->MainViewport->Renderer()->SetBloom(EBloomMode::FakeBloom); ui->ActionNoBloom->setChecked(false); ui->ActionBloomMaps->setChecked(false); ui->ActionFakeBloom->setChecked(true); ui->ActionBloom->setChecked(false); } void CWorldEditor::SetBloom() { ui->MainViewport->Renderer()->SetBloom(EBloomMode::Bloom); ui->ActionNoBloom->setChecked(false); ui->ActionBloomMaps->setChecked(false); ui->ActionFakeBloom->setChecked(false); ui->ActionBloom->setChecked(true); } void CWorldEditor::IncrementGizmo() { mGizmo.IncrementSize(); } void CWorldEditor::DecrementGizmo() { mGizmo.DecrementSize(); } void CWorldEditor::EditLayers() { // Launch layer editor CLayerEditor Editor(this); Editor.SetArea(mpArea); Editor.exec(); }