diff --git a/editor/styling/dark.qss b/editor/styling/dark.qss index 7e940acc..6be8300e 100644 --- a/editor/styling/dark.qss +++ b/editor/styling/dark.qss @@ -426,6 +426,88 @@ QTableWidget::item:selected { color: #FFFFFF; } +#panelToolbar { + background-color: #222526; + border: none; +} + +#panelAddButton { + background-color: #2C3235; + border-color: #414A4F; + padding: 4px 10px; + min-height: 22px; + font-weight: 600; +} + +#panelMoreButton, +#browserRevealButton, +#browserNavigationButton { + background-color: transparent; + border-color: transparent; + padding: 4px; + min-width: 24px; + min-height: 22px; +} + +#panelMoreButton:hover, +#browserRevealButton:hover, +#browserNavigationButton:hover { + background-color: #30373A; + border-color: #424B50; +} + +#contentPathField { + background-color: #191D1F; + border-color: #30373B; + color: #AEB8BE; + padding: 4px 8px; + min-height: 22px; +} + +#contentSearchField { + padding: 4px 8px; + min-height: 22px; +} + +QTreeView#hierarchyTree { + background-color: #1B1F20; + border: none; + border-radius: 4px; + padding: 3px; + show-decoration-selected: 1; +} + +QTreeView#hierarchyTree::item { + min-height: 23px; + padding: 3px 5px; +} + +QListView#contentGrid { + background-color: #1B1F20; + border: none; + border-radius: 4px; + padding: 8px; +} + +QListView#contentGrid::item { + min-height: 88px; + padding: 6px; + margin: 2px; + border: 1px solid transparent; + border-radius: 7px; +} + +QListView#contentGrid::item:hover { + background-color: #272E31; + border-color: #353F44; +} + +QListView#contentGrid::item:selected { + background-color: #284E73; + border-color: #579FE3; + color: #FFFFFF; +} + QHeaderView { background-color: #252A2C; border: none; diff --git a/editor/views/editor/editor.cpp b/editor/views/editor/editor.cpp index 498381f2..92d38c0c 100644 --- a/editor/views/editor/editor.cpp +++ b/editor/views/editor/editor.cpp @@ -1,16 +1,17 @@ /* -* editor.cpp -* As part of the Atlas project -* Created by Max Van den Eynde in 2026 -* -------------------------------------- -* Description: Editor view and window -* Copyright (c) 2026 Max Van den Eynde -*/ + * editor.cpp + * As part of the Atlas project + * Created by Max Van den Eynde in 2026 + * -------------------------------------- + * Description: Editor view and window + * Copyright (c) 2026 Max Van den Eynde + */ #include #include +#include #include #include #include @@ -23,7 +24,7 @@ #include "editor/views/inspectorView.h" #include "editor/views/viewport.h" -EditorWindow::EditorWindow(const QString& projectFile, QWidget* parent) +EditorWindow::EditorWindow(const QString &projectFile, QWidget *parent) : QMainWindow(parent), projectFile(projectFile) { setupWindow(); setupMenus(); @@ -40,12 +41,18 @@ void EditorWindow::setupWindow() { resize(1280, 720); menuBar()->setNativeMenuBar(false); - ads::CDockManager::setConfigFlag(ads::CDockManager::OpaqueSplitterResize, true); - ads::CDockManager::setConfigFlag(ads::CDockManager::FocusHighlighting, true); - ads::CDockManager::setConfigFlag(ads::CDockManager::DisableStylesheet, true); - ads::CDockManager::setConfigFlag(ads::CDockManager::DockAreaHasTabsMenuButton, false); - ads::CDockManager::setConfigFlag(ads::CDockManager::DockAreaHasUndockButton, false); - ads::CDockManager::setConfigFlag(ads::CDockManager::DockAreaHasCloseButton, false); + ads::CDockManager::setConfigFlag(ads::CDockManager::OpaqueSplitterResize, + true); + ads::CDockManager::setConfigFlag(ads::CDockManager::FocusHighlighting, + true); + ads::CDockManager::setConfigFlag(ads::CDockManager::DisableStylesheet, + true); + ads::CDockManager::setConfigFlag( + ads::CDockManager::DockAreaHasTabsMenuButton, false); + ads::CDockManager::setConfigFlag(ads::CDockManager::DockAreaHasUndockButton, + false); + ads::CDockManager::setConfigFlag(ads::CDockManager::DockAreaHasCloseButton, + false); coreManager = new ads::CDockManager(this); setCentralWidget(coreManager); @@ -53,63 +60,72 @@ void EditorWindow::setupWindow() { } void EditorWindow::setupMenus() { - auto* fileMenu = menuBar()->addMenu("File"); + auto *fileMenu = menuBar()->addMenu("File"); fileMenu->addAction(style()->standardIcon(QStyle::SP_FileIcon), "New"); - fileMenu->addAction(style()->standardIcon(QStyle::SP_DialogOpenButton), "Open"); - fileMenu->addAction(style()->standardIcon(QStyle::SP_DialogSaveButton), "Save"); + fileMenu->addAction(style()->standardIcon(QStyle::SP_DialogOpenButton), + "Open"); + auto *saveAction = fileMenu->addAction( + style()->standardIcon(QStyle::SP_DialogSaveButton), "Save Scene"); + connect(saveAction, &QAction::triggered, this, [this] { + if (viewportPanel != nullptr) { + viewportPanel->saveRuntimeScene(); + } + }); fileMenu->addSeparator(); - fileMenu->addAction(style()->standardIcon(QStyle::SP_DialogCloseButton), "Quit", this, &QWidget::close); + fileMenu->addAction(style()->standardIcon(QStyle::SP_DialogCloseButton), + "Quit", this, &QWidget::close); - auto* editMenu = menuBar()->addMenu("Edit"); + auto *editMenu = menuBar()->addMenu("Edit"); editMenu->addAction(style()->standardIcon(QStyle::SP_ArrowBack), "Undo"); editMenu->addAction(style()->standardIcon(QStyle::SP_ArrowForward), "Redo"); editMenu->addSeparator(); editMenu->addAction("Preferences"); - auto* viewMenu = menuBar()->addMenu("View"); - viewMenu->addAction(style()->standardIcon(QStyle::SP_FileDialogDetailedView), "Reset Layout"); + auto *viewMenu = menuBar()->addMenu("View"); + viewMenu->addAction( + style()->standardIcon(QStyle::SP_FileDialogDetailedView), + "Reset Layout"); - auto* windowMenu = menuBar()->addMenu("Window"); - windowMenu->addAction(style()->standardIcon(QStyle::SP_TitleBarNormalButton), "Minimize"); - windowMenu->addAction(style()->standardIcon(QStyle::SP_TitleBarMaxButton), "Zoom"); + auto *windowMenu = menuBar()->addMenu("Window"); + windowMenu->addAction( + style()->standardIcon(QStyle::SP_TitleBarNormalButton), "Minimize"); + windowMenu->addAction(style()->standardIcon(QStyle::SP_TitleBarMaxButton), + "Zoom"); - auto* helpMenu = menuBar()->addMenu("Help"); - helpMenu->addAction(style()->standardIcon(QStyle::SP_MessageBoxQuestion), "About Atlas"); + auto *helpMenu = menuBar()->addMenu("Help"); + helpMenu->addAction(style()->standardIcon(QStyle::SP_MessageBoxQuestion), + "About Atlas"); } void EditorWindow::setupDocks() { - dockManager->addPanel({ - .id = "viewport", - .title = "Viewport", - .widget = new ViewportPanel(projectFile), - .area = EditorDockArea::Center, - .icon = style()->standardIcon(QStyle::SP_DirOpenIcon) - }); - - dockManager->addPanel({ - .id = "hierarchy", - .title = "Hierarchy Panel", - .widget = new HierarchyPanel(), - .area = EditorDockArea::Left, - .icon = style()->standardIcon(QStyle::SP_DirOpenIcon) - }); - - dockManager->addPanel({ - .id = "inspector", - .title = "Inspector", - .widget = new InspectorPanel(), - .area = EditorDockArea::Right, - .icon = style()->standardIcon(QStyle::SP_DirOpenIcon) - }); - - - dockManager->addPanel({ - .id = "fileExplorer", - .title = "Content Browser", - .widget = new ContentBrowserPanel(), - .area = EditorDockArea::Bottom, - .icon = style()->standardIcon(QStyle::SP_DirOpenIcon) - }); + viewportPanel = new ViewportPanel(projectFile); + dockManager->addPanel( + {.id = "viewport", + .title = "Viewport", + .widget = viewportPanel, + .area = EditorDockArea::Center, + .icon = style()->standardIcon(QStyle::SP_DirOpenIcon)}); + + dockManager->addPanel( + {.id = "hierarchy", + .title = "Hierarchy Panel", + .widget = new HierarchyPanel(viewportPanel), + .area = EditorDockArea::Left, + .icon = style()->standardIcon(QStyle::SP_DirOpenIcon)}); + + dockManager->addPanel( + {.id = "inspector", + .title = "Inspector", + .widget = new InspectorPanel(), + .area = EditorDockArea::Right, + .icon = style()->standardIcon(QStyle::SP_DirOpenIcon)}); + + dockManager->addPanel( + {.id = "fileExplorer", + .title = "Content Browser", + .widget = new ContentBrowserPanel(projectFile), + .area = EditorDockArea::Bottom, + .icon = style()->standardIcon(QStyle::SP_DirOpenIcon)}); } void EditorWindow::saveLayout() { @@ -134,14 +150,14 @@ void EditorWindow::restoreLayout() { } } -void EditorWindow::closeEvent(QCloseEvent* event) { +void EditorWindow::closeEvent(QCloseEvent *event) { if (closing) { event->accept(); return; } closing = true; saveLayout(); - for (auto* viewport : findChildren()) { + for (auto *viewport : findChildren()) { viewport->shutdownRuntime(); } delete dockManager; diff --git a/editor/views/editor/hierarchy.cpp b/editor/views/editor/hierarchy.cpp index 63b25c4b..1bf6f582 100644 --- a/editor/views/editor/hierarchy.cpp +++ b/editor/views/editor/hierarchy.cpp @@ -1,64 +1,389 @@ /* -* hierarchy.cpp -* As part of the Atlas project -* Created by Max Van den Eynde in 2026 -* -------------------------------------- -* Description: Hierarchy panel definition -* Copyright (c) 2026 Max Van den Eynde -*/ + * hierarchy.cpp + * As part of the Atlas project + * Created by Max Van den Eynde in 2026 + * -------------------------------------- + * Description: Hierarchy panel definition + * Copyright (c) 2026 Max Van den Eynde + */ #include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include #include -#include -#include +#include -#include "editor/application/styling.h" +#include "editor/views/viewport.h" -HierarchyPanel::HierarchyPanel(QWidget* parent) : QWidget(parent) { - auto* layout = new QVBoxLayout(this); - constexpr int margins = 4; - layout->setContentsMargins(margins, margins, margins, margins); +namespace { +constexpr int ObjectIdRole = Qt::UserRole + 1; +constexpr int ObjectTypeRole = Qt::UserRole + 2; - treeView = new QTreeView(this); - model = new QStandardItemModel(this); +QIcon hierarchyIcon(QWidget *widget, const QString &type) { + const QString normalized = type.toLower(); + QStyle::StandardPixmap fallback = QStyle::SP_FileIcon; + QString themeName = "application-x-executable"; + + if (normalized == "scene") { + fallback = QStyle::SP_DesktopIcon; + themeName = "view-grid"; + } else if (normalized == "compound" || normalized == "group") { + fallback = QStyle::SP_DirClosedIcon; + themeName = "folder"; + } else if (normalized == "camera") { + fallback = QStyle::SP_ComputerIcon; + themeName = "camera-photo"; + } else if (normalized.contains("light") || normalized == "sun") { + fallback = QStyle::SP_MessageBoxInformation; + themeName = "weather-clear"; + } else if (normalized == "terrain" || normalized == "landscape") { + fallback = QStyle::SP_DriveHDIcon; + themeName = "applications-graphics"; + } else if (normalized == "particleemitter" || normalized == "particles") { + fallback = QStyle::SP_BrowserReload; + themeName = "weather-showers-scattered"; + } else if (normalized == "model") { + fallback = QStyle::SP_FileDialogContentsView; + themeName = "model"; + } else if (normalized == "cube" || normalized == "sphere" || + normalized == "plane" || normalized == "pyramid" || + normalized == "capsule" || normalized == "solid") { + fallback = QStyle::SP_DirIcon; + themeName = "applications-games"; + } + + return QIcon::fromTheme(themeName, widget->style()->standardIcon(fallback)); +} + +QString objectSignature(const QJsonArray &objects) { + QString signature; + for (const QJsonValue &value : objects) { + const QJsonObject object = value.toObject(); + signature += QStringLiteral("%1:%2:%3[") + .arg(object.value("id").toInt()) + .arg(object.value("name").toString(), + object.value("type").toString()); + signature += objectSignature(object.value("children").toArray()); + signature += ']'; + } + return signature; +} +} - model->setHorizontalHeaderLabels({"Object"}); +HierarchyPanel::HierarchyPanel(ViewportPanel *viewport, QWidget *parent) + : QWidget(parent), viewport(viewport) { + setObjectName("hierarchyPanel"); + auto *layout = new QVBoxLayout(this); + layout->setContentsMargins(4, 4, 4, 4); + layout->setSpacing(4); + auto *toolbar = new QWidget(this); + toolbar->setObjectName("panelToolbar"); + auto *toolbarLayout = new QHBoxLayout(toolbar); + toolbarLayout->setContentsMargins(0, 0, 0, 0); + toolbarLayout->setSpacing(4); + + addButton = new QToolButton(toolbar); + addButton->setObjectName("panelAddButton"); + addButton->setText("+ Add Object"); + addButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + addButton->setPopupMode(QToolButton::InstantPopup); + + moreButton = new QToolButton(toolbar); + moreButton->setObjectName("panelMoreButton"); + moreButton->setText("•••"); + moreButton->setPopupMode(QToolButton::InstantPopup); + + toolbarLayout->addWidget(addButton); + toolbarLayout->addStretch(); + toolbarLayout->addWidget(moreButton); + layout->addWidget(toolbar); + + treeView = new QTreeView(this); + treeView->setObjectName("hierarchyTree"); + model = new QStandardItemModel(this); treeView->setModel(model); treeView->setHeaderHidden(true); - treeView->setAlternatingRowColors(false); treeView->setAnimated(true); treeView->setEditTriggers(QAbstractItemView::NoEditTriggers); - + treeView->setSelectionMode(QAbstractItemView::SingleSelection); + treeView->setContextMenuPolicy(Qt::CustomContextMenu); + treeView->setUniformRowHeights(true); layout->addWidget(treeView); - populateTemporaryScene(); + auto *addMenu = new QMenu(addButton); + auto *objectsMenu = addMenu->addMenu("3D Object"); + const QList> objects = { + {"Cube", "cube"}, {"Sphere", "sphere"}, {"Plane", "plane"}, + {"Pyramid", "pyramid"}, {"Capsule", "capsule"}, {"Terrain", "terrain"}}; + for (const auto &[label, type] : objects) { + objectsMenu->addAction( + hierarchyIcon(this, type), label, this, + [this, type, label] { createObject(type, label); }); + } + + auto *lightsMenu = addMenu->addMenu("Light"); + const QList> lights = { + {"Point Light", "pointLight"}, + {"Spot Light", "spotLight"}, + {"Directional Light", "directionalLight"}, + {"Area Light", "areaLight"}, + {"Ambient Light", "ambientLight"}}; + for (const auto &[label, type] : lights) { + lightsMenu->addAction( + hierarchyIcon(this, type), label, this, + [this, type, label] { createObject(type, label); }); + } + addMenu->addSeparator(); + addMenu->addAction(hierarchyIcon(this, "group"), "Empty Object", this, + [this] { createObject("group", "Empty Object"); }); + addMenu->addAction(hierarchyIcon(this, "camera"), "Camera", this, + [this] { createObject("camera", "Camera"); }); + addMenu->addAction( + hierarchyIcon(this, "particleEmitter"), "Particle Emitter", this, + [this] { createObject("particleEmitter", "Particle Emitter"); }); + addButton->setMenu(addMenu); + + auto *moreMenu = new QMenu(moreButton); + moreMenu->addAction("Focus Selection", this, + &HierarchyPanel::focusSelectedObject); + moreMenu->addAction("Rename", this, &HierarchyPanel::renameSelectedObject); + moreMenu->addAction("Move to Scene Root", this, + &HierarchyPanel::moveSelectedObjectToRoot); + moreMenu->addSeparator(); + moreMenu->addAction("Save Scene", this, [this] { + if (this->viewport != nullptr) { + this->viewport->saveRuntimeScene(); + } + }); + moreButton->setMenu(moreMenu); + + connect(treeView, &QTreeView::clicked, this, + [this](const QModelIndex &) { focusSelectedObject(); }); + connect(treeView, &QTreeView::doubleClicked, this, + [this](const QModelIndex &) { renameSelectedObject(); }); + connect(treeView, &QTreeView::customContextMenuRequested, this, + &HierarchyPanel::showContextMenu); + + auto *deleteAction = new QAction(this); + deleteAction->setShortcut(QKeySequence::Delete); + deleteAction->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(deleteAction, &QAction::triggered, this, + &HierarchyPanel::deleteSelectedObject); + addAction(deleteAction); + + auto *renameAction = new QAction(this); + renameAction->setShortcut(Qt::Key_F2); + renameAction->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameAction, &QAction::triggered, this, + &HierarchyPanel::renameSelectedObject); + addAction(renameAction); + + if (viewport != nullptr) { + addButton->setEnabled(false); + moreButton->setEnabled(false); + connect(viewport, &ViewportPanel::sceneSnapshotChanged, this, + &HierarchyPanel::applySceneSnapshot); + connect(viewport, &ViewportPanel::runtimeAvailabilityChanged, this, + [this](bool available) { + addButton->setEnabled(available); + moreButton->setEnabled(available); + treeView->setEnabled(available); + }); + } } -void HierarchyPanel::populateTemporaryScene() { - auto* root = new QStandardItem("Scene"); - root->setIcon(THEME_ICON(QStyle::SP_DesktopIcon)); +void HierarchyPanel::applySceneSnapshot(const QString &snapshot) { + QJsonParseError error; + const QJsonDocument document = + QJsonDocument::fromJson(snapshot.toUtf8(), &error); + if (error.error != QJsonParseError::NoError || !document.isObject()) { + return; + } - auto* camera = new QStandardItem("Main Camera"); - camera->setIcon(THEME_ICON(QStyle::SP_FileIcon)); - auto* light = new QStandardItem("Directional Light"); - light->setIcon(THEME_ICON(QStyle::SP_FileIcon)); + const QJsonObject scene = document.object(); + const QString sceneName = scene.value("name").toString("Scene"); + const QJsonArray objects = scene.value("objects").toArray(); + const int selectedId = scene.value("selectedId").toInt(-1); + const QString signature = sceneSignature(sceneName, objects); - auto* player = new QStandardItem("Player"); - player->appendRow(new QStandardItem("Mesh Renderer")); - player->appendRow(new QStandardItem("Rigidbody")); - player->appendRow(new QStandardItem("Player Controller")); + if (signature != lastStructureSignature) { + rebuildScene(sceneName, objects, selectedId); + lastStructureSignature = signature; + return; + } - auto* world = new QStandardItem("World"); - world->appendRow(new QStandardItem("Terrain")); - world->appendRow(new QStandardItem("Sky")); + applyingSnapshot = true; + const QSignalBlocker blocker(treeView->selectionModel()); + treeView->clearSelection(); + treeView->setCurrentIndex(QModelIndex()); + if (itemsById.contains(selectedId)) { + const QModelIndex index = itemsById.value(selectedId)->index(); + treeView->setCurrentIndex(index); + treeView->scrollTo(index, QAbstractItemView::EnsureVisible); + } else { + treeView->setCurrentIndex(QModelIndex()); + } + applyingSnapshot = false; +} - root->appendRow(camera); - root->appendRow(light); - root->appendRow(player); - root->appendRow(world); +void HierarchyPanel::rebuildScene(const QString &sceneName, + const QJsonArray &objects, int selectedId) { + applyingSnapshot = true; + model->clear(); + itemsById.clear(); + auto *root = new QStandardItem(hierarchyIcon(this, "scene"), sceneName); + root->setData(-1, ObjectIdRole); + root->setData("scene", ObjectTypeRole); + root->setEditable(false); + appendObjects(root, objects); model->appendRow(root); treeView->expandAll(); + + if (itemsById.contains(selectedId)) { + const QModelIndex index = itemsById.value(selectedId)->index(); + treeView->setCurrentIndex(index); + treeView->scrollTo(index, QAbstractItemView::EnsureVisible); + } + applyingSnapshot = false; +} + +void HierarchyPanel::appendObjects(QStandardItem *parent, + const QJsonArray &objects) { + for (const QJsonValue &value : objects) { + const QJsonObject object = value.toObject(); + const int id = object.value("id").toInt(-1); + const QString name = object.value("name").toString("Object"); + const QString type = object.value("type").toString("gameObject"); + auto *item = new QStandardItem(hierarchyIcon(this, type), name); + item->setData(id, ObjectIdRole); + item->setData(type, ObjectTypeRole); + item->setToolTip(type); + item->setEditable(false); + itemsById.insert(id, item); + parent->appendRow(item); + appendObjects(item, object.value("children").toArray()); + } +} + +void HierarchyPanel::showAddObjectMenu(const QPoint &position) { + if (addButton->menu() != nullptr) { + addButton->menu()->popup(position); + } +} + +void HierarchyPanel::showContextMenu(const QPoint &position) { + const QModelIndex index = treeView->indexAt(position); + if (index.isValid()) { + treeView->setCurrentIndex(index); + } + + QMenu menu(this); + auto *addMenu = menu.addMenu("Add Object"); + for (QAction *action : addButton->menu()->actions()) { + addMenu->addAction(action); + } + if (index.isValid() && selectedObjectId() >= 0) { + menu.addSeparator(); + menu.addAction("Focus", this, &HierarchyPanel::focusSelectedObject); + menu.addAction("Rename", this, &HierarchyPanel::renameSelectedObject); + menu.addAction("Move to Scene Root", this, + &HierarchyPanel::moveSelectedObjectToRoot); + menu.addSeparator(); + menu.addAction("Delete", this, &HierarchyPanel::deleteSelectedObject); + } + menu.exec(treeView->viewport()->mapToGlobal(position)); +} + +void HierarchyPanel::createObject(const QString &type, + const QString &displayName) { + if (viewport == nullptr) { + return; + } + const int parentId = selectedObjectId(); + const int id = viewport->createRuntimeObject(type, displayName); + if (id >= 0 && parentId >= 0) { + viewport->setRuntimeObjectParent(id, parentId); + } +} + +void HierarchyPanel::renameSelectedObject() { + const int id = selectedObjectId(); + if (id < 0 || viewport == nullptr) { + return; + } + QStandardItem *item = itemsById.value(id, nullptr); + if (item == nullptr) { + return; + } + bool accepted = false; + const QString name = + QInputDialog::getText(this, "Rename Object", "Name", QLineEdit::Normal, + item->text(), &accepted); + if (accepted && !name.trimmed().isEmpty()) { + viewport->renameRuntimeObject(id, name.trimmed()); + } +} + +void HierarchyPanel::deleteSelectedObject() { + const int id = selectedObjectId(); + if (id < 0 || viewport == nullptr) { + return; + } + const QString name = itemsById.value(id)->text(); + if (QMessageBox::question( + this, "Delete Object", + QStringLiteral("Delete “%1” and its children?").arg(name)) == + QMessageBox::Yes) { + viewport->deleteRuntimeObject(id); + } +} + +void HierarchyPanel::focusSelectedObject() { + if (applyingSnapshot || viewport == nullptr) { + return; + } + const int id = selectedObjectId(); + if (id >= 0) { + viewport->selectRuntimeObject(id, true); + } +} + +void HierarchyPanel::moveSelectedObjectToRoot() { + const int id = selectedObjectId(); + if (id >= 0 && viewport != nullptr) { + viewport->setRuntimeObjectParent(id, -1); + } +} + +int HierarchyPanel::selectedObjectId() const { + if (treeView == nullptr || !treeView->currentIndex().isValid()) { + return -1; + } + return treeView->currentIndex().data(ObjectIdRole).toInt(); +} + +QString HierarchyPanel::sceneSignature(const QString &sceneName, + const QJsonArray &objects) const { + return sceneName + ':' + objectSignature(objects); } diff --git a/editor/views/editor/viewport.cpp b/editor/views/editor/viewport.cpp index b69c101d..e377de09 100644 --- a/editor/views/editor/viewport.cpp +++ b/editor/views/editor/viewport.cpp @@ -1,11 +1,11 @@ /* -* viewport.cpp -* As part of the Atlas project -* Created by Max Van den Eynde in 2026 -* -------------------------------------- -* Description: Viewport definitions -* Copyright (c) 2026 Max Van den Eynde -*/ + * viewport.cpp + * As part of the Atlas project + * Created by Max Van den Eynde in 2026 + * -------------------------------------- + * Description: Viewport definitions + * Copyright (c) 2026 Max Van den Eynde + */ #include @@ -100,13 +100,13 @@ int editorCameraKey(int key) { } } -float widgetScale(QWidget* widget) { +float widgetScale(QWidget *widget) { const qreal scale = widget != nullptr ? widget->devicePixelRatioF() : 1.0; return scale > 0.0 ? static_cast(scale) : 1.0f; } } -ViewportPanel::ViewportPanel(const QString& projectFile, QWidget* parent) +ViewportPanel::ViewportPanel(const QString &projectFile, QWidget *parent) : QWidget(parent), projectFile(projectFile) { setAttribute(Qt::WA_NativeWindow); setAttribute(Qt::WA_NoSystemBackground); @@ -120,35 +120,24 @@ ViewportPanel::ViewportPanel(const QString& projectFile, QWidget* parent) frameTimer = new QTimer(this); frameTimer->setTimerType(Qt::PreciseTimer); - connect(frameTimer, &QTimer::timeout, this, [this] { - stepRuntime(); - }); - if (auto* app = QCoreApplication::instance()) { - connect(app, &QCoreApplication::aboutToQuit, this, [this] { - shutdownRuntime(); - }); + connect(frameTimer, &QTimer::timeout, this, [this] { stepRuntime(); }); + if (auto *app = QCoreApplication::instance()) { + connect(app, &QCoreApplication::aboutToQuit, this, + [this] { shutdownRuntime(); }); } winId(); } -ViewportPanel::~ViewportPanel() { - shutdownRuntime(); -} +ViewportPanel::~ViewportPanel() { shutdownRuntime(); } -QSize ViewportPanel::sizeHint() const { - return QSize(640, 360); -} +QSize ViewportPanel::sizeHint() const { return QSize(640, 360); } -QSize ViewportPanel::minimumSizeHint() const { - return QSize(1, 1); -} +QSize ViewportPanel::minimumSizeHint() const { return QSize(1, 1); } -QPaintEngine* ViewportPanel::paintEngine() const { - return nullptr; -} +QPaintEngine *ViewportPanel::paintEngine() const { return nullptr; } -void ViewportPanel::showEvent(QShowEvent* event) { +void ViewportPanel::showEvent(QShowEvent *event) { QWidget::showEvent(event); if (runtimeContext != nullptr) { frameTimer->start(16); @@ -157,19 +146,19 @@ void ViewportPanel::showEvent(QShowEvent* event) { scheduleRuntimeStart(); } -void ViewportPanel::hideEvent(QHideEvent* event) { +void ViewportPanel::hideEvent(QHideEvent *event) { if (frameTimer != nullptr) { frameTimer->stop(); } QWidget::hideEvent(event); } -void ViewportPanel::closeEvent(QCloseEvent* event) { +void ViewportPanel::closeEvent(QCloseEvent *event) { shutdownRuntime(); QWidget::closeEvent(event); } -void ViewportPanel::resizeEvent(QResizeEvent* event) { +void ViewportPanel::resizeEvent(QResizeEvent *event) { QWidget::resizeEvent(event); if (runtimeContext == nullptr) { scheduleRuntimeStart(); @@ -202,7 +191,7 @@ void ViewportPanel::shutdownRuntime() { stopRuntime(); } -void ViewportPanel::mousePressEvent(QMouseEvent* event) { +void ViewportPanel::mousePressEvent(QMouseEvent *event) { setFocus(Qt::MouseFocusReason); sendPointerEvent(0, static_cast(event->position().x()), static_cast(event->position().y()), @@ -210,21 +199,21 @@ void ViewportPanel::mousePressEvent(QMouseEvent* event) { event->accept(); } -void ViewportPanel::mouseMoveEvent(QMouseEvent* event) { +void ViewportPanel::mouseMoveEvent(QMouseEvent *event) { sendPointerEvent(1, static_cast(event->position().x()), static_cast(event->position().y()), activeRuntimeMouseButton(event->buttons())); event->accept(); } -void ViewportPanel::mouseReleaseEvent(QMouseEvent* event) { +void ViewportPanel::mouseReleaseEvent(QMouseEvent *event) { sendPointerEvent(2, static_cast(event->position().x()), static_cast(event->position().y()), runtimeMouseButton(event->button())); event->accept(); } -void ViewportPanel::wheelEvent(QWheelEvent* event) { +void ViewportPanel::wheelEvent(QWheelEvent *event) { if (runtimeContext == nullptr) { QWidget::wheelEvent(event); return; @@ -236,7 +225,7 @@ void ViewportPanel::wheelEvent(QWheelEvent* event) { event->accept(); } -void ViewportPanel::keyPressEvent(QKeyEvent* event) { +void ViewportPanel::keyPressEvent(QKeyEvent *event) { const int key = editorCameraKey(event->key()); if (event->isAutoRepeat()) { if (key >= 0) { @@ -254,7 +243,7 @@ void ViewportPanel::keyPressEvent(QKeyEvent* event) { QWidget::keyPressEvent(event); } -void ViewportPanel::keyReleaseEvent(QKeyEvent* event) { +void ViewportPanel::keyReleaseEvent(QKeyEvent *event) { const int key = editorCameraKey(event->key()); if (event->isAutoRepeat()) { if (key >= 0) { @@ -284,23 +273,23 @@ void ViewportPanel::startRuntime() { return; } - void* metalView = - reinterpret_cast(static_cast(winId())); + void *metalView = reinterpret_cast(static_cast(winId())); if (metalView == nullptr) { qWarning() << "Atlas viewport could not resolve a native Metal view"; return; } try { - runtimeContext = - runtime::makeContextForMetalViewNonBlocking(runtimeProjectFile, - metalView); + runtimeContext = runtime::makeContextForMetalViewNonBlocking( + runtimeProjectFile, metalView); runtimeContext->setEditorControlsEnabled(true); runtimeContext->setEditorSimulationEnabled(false); runtimeContext->setEditorControlMode(1); resizeRuntime(); + refreshSceneSnapshot(); + emit runtimeAvailabilityChanged(true); frameTimer->start(16); - } catch (const std::exception& error) { + } catch (const std::exception &error) { qWarning().noquote() << QStringLiteral("Failed to start Atlas viewport runtime: %1") .arg(QString::fromUtf8(error.what())); @@ -322,9 +311,11 @@ void ViewportPanel::stopRuntime() { return; } auto context = std::move(runtimeContext); + lastSceneSnapshot.clear(); + emit runtimeAvailabilityChanged(false); try { context->end(); - } catch (const std::exception& error) { + } catch (const std::exception &error) { qWarning().noquote() << QStringLiteral("Failed to stop Atlas viewport runtime: %1") .arg(QString::fromUtf8(error.what())); @@ -343,8 +334,10 @@ void ViewportPanel::stepRuntime() { try { if (!runtimeContext->stepFrame()) { stopRuntime(); + return; } - } catch (const std::exception& error) { + refreshSceneSnapshot(); + } catch (const std::exception &error) { qWarning().noquote() << QStringLiteral("Atlas viewport runtime frame failed: %1") .arg(QString::fromUtf8(error.what())); @@ -380,3 +373,69 @@ void ViewportPanel::sendPointerEvent(int action, float x, float y, int button) { runtimeContext->editorPointerEvent(action, x, flippedY, button, widgetScale(this)); } + +bool ViewportPanel::selectRuntimeObject(int id, bool focusCamera) { + if (runtimeContext == nullptr || + !runtimeContext->selectObject(id, focusCamera)) { + return false; + } + refreshSceneSnapshot(); + setFocus(Qt::OtherFocusReason); + return true; +} + +bool ViewportPanel::renameRuntimeObject(int id, const QString &name) { + if (runtimeContext == nullptr || + !runtimeContext->renameObject(id, name.toUtf8().toStdString())) { + return false; + } + refreshSceneSnapshot(); + return true; +} + +bool ViewportPanel::setRuntimeObjectParent(int childId, int parentId) { + if (runtimeContext == nullptr || + !runtimeContext->setObjectParent(childId, parentId)) { + return false; + } + refreshSceneSnapshot(); + return true; +} + +bool ViewportPanel::deleteRuntimeObject(int id) { + if (runtimeContext == nullptr || !runtimeContext->deleteObject(id)) { + return false; + } + refreshSceneSnapshot(); + return true; +} + +int ViewportPanel::createRuntimeObject(const QString &type, + const QString &name) { + if (runtimeContext == nullptr) { + return -1; + } + const int id = runtimeContext->createObject(type.toUtf8().toStdString(), + name.toUtf8().toStdString()); + if (id >= 0) { + refreshSceneSnapshot(); + } + return id; +} + +bool ViewportPanel::saveRuntimeScene() { + return runtimeContext != nullptr && runtimeContext->saveCurrentScene(); +} + +void ViewportPanel::refreshSceneSnapshot() { + if (runtimeContext == nullptr) { + return; + } + const QString snapshot = + QString::fromStdString(runtimeContext->sceneObjectsJson()); + if (snapshot == lastSceneSnapshot) { + return; + } + lastSceneSnapshot = snapshot; + emit sceneSnapshotChanged(snapshot); +} diff --git a/editor/views/general/contentBrowser.cpp b/editor/views/general/contentBrowser.cpp index 7e004a8c..208a9995 100644 --- a/editor/views/general/contentBrowser.cpp +++ b/editor/views/general/contentBrowser.cpp @@ -1,49 +1,485 @@ /* -* contentBrowser.cpp -* As part of the Atlas project -* Created by Max Van den Eynde in 2026 -* -------------------------------------- -* Description: Content Browser / File Explorer Declaration -* Copyright (c) 2026 Max Van den Eynde -*/ + * contentBrowser.cpp + * As part of the Atlas project + * Created by Max Van den Eynde in 2026 + * -------------------------------------- + * Description: Content Browser / File Explorer Declaration + * Copyright (c) 2026 Max Van den Eynde + */ #include "editor/views/fileExplorer.h" -#include -#include -#include +#include +#include +#include +#include +#include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { +const QByteArray EmptyScene = R"({ + "name": "New Scene", + "id": "new_scene", + "objects": [], + "lights": [ + { + "type": "ambient", + "intensity": 0.25 + } + ], + "camera": { + "position": [0.0, 1.5, -5.0], + "target": [0.0, 0.0, 0.0], + "fov": 60.0 + }, + "targets": [ + { + "name": "Main Target", + "type": "scene", + "render": true, + "display": true + } + ] +} +)"; + +bool writeNewFile(const QString &path, const QByteArray &contents) { + QSaveFile file(path); + return file.open(QIODevice::WriteOnly) && + file.write(contents) == contents.size() && file.commit(); +} + +bool isValidEntryName(const QString &name) { + return !name.isEmpty() && name != "." && name != ".." && + !name.contains('/') && !name.contains('\\'); +} +} -ContentBrowserPanel::ContentBrowserPanel(QWidget* parent) : QWidget(parent) { - auto* layout = new QVBoxLayout(this); +ContentBrowserPanel::ContentBrowserPanel(const QString &projectFile, + QWidget *parent) + : QWidget(parent) { + setObjectName("contentBrowserPanel"); + const QFileInfo projectInfo(projectFile); + projectRoot = projectInfo.absoluteDir().canonicalPath(); + if (projectRoot.isEmpty()) { + projectRoot = projectInfo.absoluteDir().absolutePath(); + } + + auto *layout = new QVBoxLayout(this); layout->setContentsMargins(4, 4, 4, 4); + layout->setSpacing(4); + + auto *toolbar = new QWidget(this); + toolbar->setObjectName("panelToolbar"); + auto *toolbarLayout = new QHBoxLayout(toolbar); + toolbarLayout->setContentsMargins(0, 0, 0, 0); + toolbarLayout->setSpacing(4); + + backButton = new QToolButton(toolbar); + backButton->setObjectName("browserNavigationButton"); + backButton->setIcon(style()->standardIcon(QStyle::SP_ArrowBack)); + backButton->setToolTip("Back"); + forwardButton = new QToolButton(toolbar); + forwardButton->setObjectName("browserNavigationButton"); + forwardButton->setIcon(style()->standardIcon(QStyle::SP_ArrowForward)); + forwardButton->setToolTip("Forward"); + upButton = new QToolButton(toolbar); + upButton->setObjectName("browserNavigationButton"); + upButton->setIcon(style()->standardIcon(QStyle::SP_ArrowUp)); + upButton->setToolTip("Parent Folder"); + + pathField = new QLineEdit(toolbar); + pathField->setObjectName("contentPathField"); + pathField->setReadOnly(true); + + searchField = new QLineEdit(toolbar); + searchField->setObjectName("contentSearchField"); + searchField->setPlaceholderText("Search"); + searchField->setClearButtonEnabled(true); + searchField->setMaximumWidth(180); + + createButton = new QToolButton(toolbar); + createButton->setObjectName("panelAddButton"); + createButton->setText("+ Create"); + createButton->setPopupMode(QToolButton::InstantPopup); + + revealButton = new QToolButton(toolbar); + revealButton->setObjectName("browserRevealButton"); + revealButton->setIcon(style()->standardIcon(QStyle::SP_DirOpenIcon)); + revealButton->setText("Reveal"); + revealButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + revealButton->setToolTip("Reveal in Finder"); + + moreButton = new QToolButton(toolbar); + moreButton->setObjectName("panelMoreButton"); + moreButton->setText("•••"); + moreButton->setPopupMode(QToolButton::InstantPopup); + + toolbarLayout->addWidget(backButton); + toolbarLayout->addWidget(forwardButton); + toolbarLayout->addWidget(upButton); + toolbarLayout->addWidget(pathField, 1); + toolbarLayout->addWidget(searchField); + toolbarLayout->addWidget(createButton); + toolbarLayout->addWidget(revealButton); + toolbarLayout->addWidget(moreButton); + layout->addWidget(toolbar); model = new QFileSystemModel(this); - model->setFilter( - QDir::AllDirs | - QDir::Files | - QDir::NoDotAndDotDot - ); + model->setFilter(QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot); + model->setReadOnly(false); + model->setRootPath(projectRoot); + model->sort(0, Qt::AscendingOrder); + + gridView = new QListView(this); + gridView->setObjectName("contentGrid"); + gridView->setModel(model); + gridView->setViewMode(QListView::IconMode); + gridView->setFlow(QListView::LeftToRight); + gridView->setWrapping(true); + gridView->setResizeMode(QListView::Adjust); + gridView->setMovement(QListView::Static); + gridView->setGridSize(QSize(112, 104)); + gridView->setIconSize(QSize(56, 56)); + gridView->setWordWrap(true); + gridView->setTextElideMode(Qt::ElideMiddle); + gridView->setSelectionMode(QAbstractItemView::ExtendedSelection); + gridView->setContextMenuPolicy(Qt::CustomContextMenu); + gridView->setUniformItemSizes(true); + gridView->setSpacing(4); + layout->addWidget(gridView, 1); - const QString rootPath = QDir::currentPath(); - model->setRootPath(rootPath); + auto *createMenu = new QMenu(createButton); + createMenu->addAction(style()->standardIcon(QStyle::SP_DirIcon), "Folder", + this, &ContentBrowserPanel::createFolder); + createMenu->addSeparator(); + createMenu->addAction(style()->standardIcon(QStyle::SP_FileIcon), "Scene", + this, &ContentBrowserPanel::createScene); + createMenu->addAction(style()->standardIcon(QStyle::SP_FileIcon), + "TypeScript Script", this, + &ContentBrowserPanel::createScript); + createButton->setMenu(createMenu); - treeView = new QTreeView(this); - treeView->setModel(model); - treeView->setRootIndex(model->index(rootPath)); + auto *moreMenu = new QMenu(moreButton); + moreMenu->addAction("Open", this, [this] { + if (gridView->currentIndex().isValid()) { + openIndex(gridView->currentIndex()); + } + }); + moreMenu->addAction("Rename", this, &ContentBrowserPanel::renameSelection); + moreMenu->addAction("Delete", this, &ContentBrowserPanel::deleteSelection); + moreMenu->addSeparator(); + moreMenu->addAction("Reveal in Finder", this, + &ContentBrowserPanel::revealSelection); + moreMenu->addAction("Copy Path", this, + &ContentBrowserPanel::copySelectionPath); + moreButton->setMenu(moreMenu); + + connect(gridView, &QListView::doubleClicked, this, + &ContentBrowserPanel::openIndex); + connect(gridView, &QListView::customContextMenuRequested, this, + &ContentBrowserPanel::showContextMenu); + connect(revealButton, &QToolButton::clicked, this, + &ContentBrowserPanel::revealSelection); + connect(backButton, &QToolButton::clicked, this, [this] { + if (historyIndex > 0) { + --historyIndex; + navigateTo(history.at(historyIndex), false); + } + }); + connect(forwardButton, &QToolButton::clicked, this, [this] { + if (historyIndex + 1 < history.size()) { + ++historyIndex; + navigateTo(history.at(historyIndex), false); + } + }); + connect(upButton, &QToolButton::clicked, this, [this] { + navigateTo(QFileInfo(currentPath).absoluteDir().absolutePath()); + }); + connect(searchField, &QLineEdit::textChanged, this, + [this](const QString &search) { + model->setNameFilters(search.isEmpty() + ? QStringList() + : QStringList{"*" + search + "*"}); + model->setNameFilterDisables(false); + }); + connect(gridView->selectionModel(), &QItemSelectionModel::selectionChanged, + this, [this] { updateNavigationState(); }); + + auto *deleteAction = new QAction(this); + deleteAction->setShortcut(QKeySequence::Delete); + deleteAction->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(deleteAction, &QAction::triggered, this, + &ContentBrowserPanel::deleteSelection); + addAction(deleteAction); + + auto *renameAction = new QAction(this); + renameAction->setShortcut(Qt::Key_F2); + renameAction->setShortcutContext(Qt::WidgetWithChildrenShortcut); + connect(renameAction, &QAction::triggered, this, + &ContentBrowserPanel::renameSelection); + addAction(renameAction); + + navigateTo(projectRoot); +} + +void ContentBrowserPanel::setRootPath(const QString &path) { + const QFileInfo info(path); + projectRoot = info.isDir() ? info.canonicalFilePath() + : info.absoluteDir().canonicalPath(); + if (projectRoot.isEmpty()) { + projectRoot = info.isDir() ? info.absoluteFilePath() + : info.absoluteDir().absolutePath(); + } + model->setRootPath(projectRoot); + history.clear(); + historyIndex = -1; + navigateTo(projectRoot); +} - treeView->setAnimated(true); - treeView->setSortingEnabled(true); - treeView->sortByColumn(0, Qt::AscendingOrder); +void ContentBrowserPanel::navigateTo(const QString &path, bool recordHistory) { + const QFileInfo info(path); + const QString target = info.canonicalFilePath().isEmpty() + ? info.absoluteFilePath() + : info.canonicalFilePath(); + if (!info.isDir() || !isInsideProject(target)) { + return; + } - treeView->hideColumn(1); - treeView->hideColumn(2); - treeView->hideColumn(3); + currentPath = target; + gridView->setRootIndex(model->index(currentPath)); + gridView->clearSelection(); + searchField->clear(); + + if (recordHistory) { + while (history.size() > historyIndex + 1) { + history.removeLast(); + } + if (history.isEmpty() || history.constLast() != currentPath) { + history.append(currentPath); + } + historyIndex = history.size() - 1; + } + updateNavigationState(); +} + +void ContentBrowserPanel::openIndex(const QModelIndex &index) { + const QFileInfo info = model->fileInfo(index); + if (info.isDir()) { + navigateTo(info.absoluteFilePath()); + return; + } + QDesktopServices::openUrl(QUrl::fromLocalFile(info.absoluteFilePath())); +} + +void ContentBrowserPanel::showContextMenu(const QPoint &position) { + const QModelIndex index = gridView->indexAt(position); + if (index.isValid()) { + gridView->setCurrentIndex(index); + if (!gridView->selectionModel()->isSelected(index)) { + gridView->selectionModel()->select( + index, QItemSelectionModel::ClearAndSelect); + } + } + + QMenu menu(this); + auto *createMenu = menu.addMenu("Create"); + for (QAction *action : createButton->menu()->actions()) { + createMenu->addAction(action); + } + if (index.isValid()) { + menu.addSeparator(); + menu.addAction("Open", this, [this, index] { openIndex(index); }); + menu.addAction("Rename", this, &ContentBrowserPanel::renameSelection); + menu.addAction("Delete", this, &ContentBrowserPanel::deleteSelection); + menu.addSeparator(); + menu.addAction("Reveal in Finder", this, + &ContentBrowserPanel::revealSelection); + menu.addAction("Copy Path", this, + &ContentBrowserPanel::copySelectionPath); + } else { + menu.addSeparator(); + menu.addAction("Reveal This Folder in Finder", this, [this] { +#ifdef Q_OS_MACOS + QProcess::startDetached("/usr/bin/open", {currentPath}); +#else + QDesktopServices::openUrl(QUrl::fromLocalFile(currentPath)); +#endif + }); + } + menu.exec(gridView->viewport()->mapToGlobal(position)); +} + +void ContentBrowserPanel::showCreateMenu(const QPoint &position) { + if (createButton->menu() != nullptr) { + createButton->menu()->popup(position); + } +} + +void ContentBrowserPanel::createFolder() { + bool accepted = false; + const QString name = + QInputDialog::getText(this, "New Folder", "Folder name", + QLineEdit::Normal, "New Folder", &accepted); + const QString entryName = name.trimmed(); + if (!accepted) { + return; + } + if (!isValidEntryName(entryName)) { + QMessageBox::warning(this, "New Folder", "Enter a valid folder name."); + return; + } + if (!QDir(currentPath).mkdir(entryName)) { + QMessageBox::warning(this, "New Folder", + "The folder could not be created."); + } +} + +void ContentBrowserPanel::createScene() { + const QString path = uniquePath("New Scene.ascene"); + if (writeNewFile(path, EmptyScene)) { + gridView->setCurrentIndex(model->index(path)); + } +} + +void ContentBrowserPanel::createScript() { + const QString path = uniquePath("NewScript.ts"); + const QByteArray script = "import { Component } from \"atlas\";\n\n" + "export class NewScript extends Component {\n" + " init() {}\n\n" + " update(deltaTime: number) {}\n" + "}\n"; + if (writeNewFile(path, script)) { + gridView->setCurrentIndex(model->index(path)); + } +} + +void ContentBrowserPanel::renameSelection() { + const QString path = selectedPath(); + if (path.isEmpty()) { + return; + } + const QFileInfo info(path); + bool accepted = false; + const QString name = QInputDialog::getText( + this, "Rename", "Name", QLineEdit::Normal, info.fileName(), &accepted); + const QString entryName = name.trimmed(); + if (!accepted || entryName == info.fileName()) { + return; + } + if (!isValidEntryName(entryName)) { + QMessageBox::warning(this, "Rename", "Enter a valid file name."); + return; + } + if (!QDir(info.absolutePath()).rename(info.fileName(), entryName)) { + QMessageBox::warning(this, "Rename", "The item could not be renamed."); + return; + } + gridView->setCurrentIndex( + model->index(QDir(info.absolutePath()).filePath(entryName))); +} + +void ContentBrowserPanel::deleteSelection() { + const QModelIndexList selected = + gridView->selectionModel()->selectedIndexes(); + if (selected.isEmpty()) { + return; + } + const QString prompt = + selected.size() == 1 + ? QStringLiteral("Delete “%1”? This cannot be undone.") + .arg(model->fileName(selected.first())) + : QStringLiteral("Delete %1 items? This cannot be undone.") + .arg(selected.size()); + if (QMessageBox::warning(this, "Delete Assets", prompt, + QMessageBox::Cancel | QMessageBox::Yes, + QMessageBox::Cancel) != QMessageBox::Yes) { + return; + } + for (const QModelIndex &index : selected) { + const QFileInfo info = model->fileInfo(index); + if (info.isSymLink()) { + QFile::remove(info.absoluteFilePath()); + } else if (info.isDir()) { + QDir(info.absoluteFilePath()).removeRecursively(); + } else { + QFile::remove(info.absoluteFilePath()); + } + } +} + +void ContentBrowserPanel::revealSelection() const { + const QString path = + selectedPath().isEmpty() ? currentPath : selectedPath(); +#ifdef Q_OS_MACOS + QProcess::startDetached("/usr/bin/open", {"-R", path}); +#elif defined(Q_OS_WIN) + QProcess::startDetached("explorer.exe", + {"/select,", QDir::toNativeSeparators(path)}); +#else + const QFileInfo info(path); + QDesktopServices::openUrl( + QUrl::fromLocalFile(info.isDir() ? path : info.absolutePath())); +#endif +} + +void ContentBrowserPanel::copySelectionPath() const { + const QString path = + selectedPath().isEmpty() ? currentPath : selectedPath(); + QApplication::clipboard()->setText(path); +} + +QString ContentBrowserPanel::selectedPath() const { + const QModelIndex index = gridView->currentIndex(); + return index.isValid() ? model->filePath(index) : QString(); +} + +QString ContentBrowserPanel::uniquePath(const QString &baseName) const { + const QFileInfo base(baseName); + QString path = QDir(currentPath).filePath(baseName); + int suffix = 2; + while (QFileInfo::exists(path)) { + const QString name = + base.completeBaseName() + ' ' + QString::number(suffix++) + + (base.suffix().isEmpty() ? QString() : '.' + base.suffix()); + path = QDir(currentPath).filePath(name); + } + return path; +} - layout->addWidget(treeView); +bool ContentBrowserPanel::isInsideProject(const QString &path) const { + const QString root = QDir::cleanPath(projectRoot); + const QString candidate = QDir::cleanPath(path); + return candidate == root || candidate.startsWith(root + QDir::separator()); } -void ContentBrowserPanel::setRootPath(const QString& path) { - model->setRootPath(path); - treeView->setRootIndex(model->index(path)); +void ContentBrowserPanel::updateNavigationState() { + backButton->setEnabled(historyIndex > 0); + forwardButton->setEnabled(historyIndex + 1 < history.size()); + upButton->setEnabled(currentPath != projectRoot); + pathField->setText( + QDir(projectRoot).relativeFilePath(currentPath) == "." + ? QFileInfo(projectRoot).fileName() + : QFileInfo(projectRoot).fileName() + '/' + + QDir(projectRoot).relativeFilePath(currentPath)); + const bool hasSelection = !selectedPath().isEmpty(); + moreButton->setEnabled(hasSelection); } diff --git a/include/editor/application/dockManager.h b/include/editor/application/dockManager.h index 751b4ffe..51e81141 100644 --- a/include/editor/application/dockManager.h +++ b/include/editor/application/dockManager.h @@ -10,7 +10,7 @@ #ifndef ATLAS_DOCKMANAGER_H #define ATLAS_DOCKMANAGER_H -#include "DockManager.h" +#include #include "DockWidget.h" class QWidget; diff --git a/include/editor/core/themes.h b/include/editor/core/themes.h index 6e067a9b..554ebf79 100644 --- a/include/editor/core/themes.h +++ b/include/editor/core/themes.h @@ -3,7 +3,7 @@ // This file is generated. Do not edit manually. // Generated from .qss theme files. -// Source: /Users/maxvdec/Coding/Projects/Atlas/editor/styling/dark.qss +// Source: editor/styling/dark.qss inline constexpr const char* DARK_THEME = "* {\n" " font-family: \"Helvetica Neue\", \"Segoe UI\", Arial, sans-serif;\n" @@ -433,6 +433,88 @@ inline constexpr const char* DARK_THEME = " color: #FFFFFF;\n" "}\n" "\n" +"#panelToolbar {\n" +" background-color: #222526;\n" +" border: none;\n" +"}\n" +"\n" +"#panelAddButton {\n" +" background-color: #2C3235;\n" +" border-color: #414A4F;\n" +" padding: 4px 10px;\n" +" min-height: 22px;\n" +" font-weight: 600;\n" +"}\n" +"\n" +"#panelMoreButton,\n" +"#browserRevealButton,\n" +"#browserNavigationButton {\n" +" background-color: transparent;\n" +" border-color: transparent;\n" +" padding: 4px;\n" +" min-width: 24px;\n" +" min-height: 22px;\n" +"}\n" +"\n" +"#panelMoreButton:hover,\n" +"#browserRevealButton:hover,\n" +"#browserNavigationButton:hover {\n" +" background-color: #30373A;\n" +" border-color: #424B50;\n" +"}\n" +"\n" +"#contentPathField {\n" +" background-color: #191D1F;\n" +" border-color: #30373B;\n" +" color: #AEB8BE;\n" +" padding: 4px 8px;\n" +" min-height: 22px;\n" +"}\n" +"\n" +"#contentSearchField {\n" +" padding: 4px 8px;\n" +" min-height: 22px;\n" +"}\n" +"\n" +"QTreeView#hierarchyTree {\n" +" background-color: #1B1F20;\n" +" border: none;\n" +" border-radius: 4px;\n" +" padding: 3px;\n" +" show-decoration-selected: 1;\n" +"}\n" +"\n" +"QTreeView#hierarchyTree::item {\n" +" min-height: 23px;\n" +" padding: 3px 5px;\n" +"}\n" +"\n" +"QListView#contentGrid {\n" +" background-color: #1B1F20;\n" +" border: none;\n" +" border-radius: 4px;\n" +" padding: 8px;\n" +"}\n" +"\n" +"QListView#contentGrid::item {\n" +" min-height: 88px;\n" +" padding: 6px;\n" +" margin: 2px;\n" +" border: 1px solid transparent;\n" +" border-radius: 7px;\n" +"}\n" +"\n" +"QListView#contentGrid::item:hover {\n" +" background-color: #272E31;\n" +" border-color: #353F44;\n" +"}\n" +"\n" +"QListView#contentGrid::item:selected {\n" +" background-color: #284E73;\n" +" border-color: #579FE3;\n" +" color: #FFFFFF;\n" +"}\n" +"\n" "QHeaderView {\n" " background-color: #252A2C;\n" " border: none;\n" diff --git a/include/editor/views/editorWindow.h b/include/editor/views/editorWindow.h index 079e5d1a..06395eed 100644 --- a/include/editor/views/editorWindow.h +++ b/include/editor/views/editorWindow.h @@ -20,6 +20,8 @@ namespace ads { class CDockWidget; } +class ViewportPanel; + class EditorWindow : public QMainWindow { Q_OBJECT @@ -37,6 +39,7 @@ class EditorWindow : public QMainWindow { EditorDockManager* dockManager = nullptr; ads::CDockManager* coreManager = nullptr; + ViewportPanel* viewportPanel = nullptr; QString projectFile; bool closing = false; diff --git a/include/editor/views/fileExplorer.h b/include/editor/views/fileExplorer.h index 079c036a..664548bc 100644 --- a/include/editor/views/fileExplorer.h +++ b/include/editor/views/fileExplorer.h @@ -1,31 +1,65 @@ /* -* fileExplorer.h -* As part of the Atlas project -* Created by Max Van den Eynde in 2026 -* -------------------------------------- -* Description: Content Browser panel -* Copyright (c) 2026 Max Van den Eynde -*/ + * fileExplorer.h + * As part of the Atlas project + * Created by Max Van den Eynde in 2026 + * -------------------------------------- + * Description: Content Browser panel + * Copyright (c) 2026 Max Van den Eynde + */ #ifndef ATLAS_FILEEXPLORER_H #define ATLAS_FILEEXPLORER_H +#include #include -class QTreeView; class QFileSystemModel; +class QLineEdit; +class QListView; +class QModelIndex; +class QPoint; +class QToolButton; class ContentBrowserPanel : public QWidget { Q_OBJECT -public: - explicit ContentBrowserPanel(QWidget* parent = nullptr); + public: + explicit ContentBrowserPanel(const QString &projectFile, + QWidget *parent = nullptr); - void setRootPath(const QString& path); + void setRootPath(const QString &path); -private: - QTreeView* treeView = nullptr; - QFileSystemModel* model = nullptr; + private: + void navigateTo(const QString &path, bool recordHistory = true); + void openIndex(const QModelIndex &index); + void showContextMenu(const QPoint &position); + void showCreateMenu(const QPoint &position); + void createFolder(); + void createScene(); + void createScript(); + void renameSelection(); + void deleteSelection(); + void revealSelection() const; + void copySelectionPath() const; + QString selectedPath() const; + QString uniquePath(const QString &baseName) const; + bool isInsideProject(const QString &path) const; + void updateNavigationState(); + + QListView *gridView = nullptr; + QFileSystemModel *model = nullptr; + QToolButton *backButton = nullptr; + QToolButton *forwardButton = nullptr; + QToolButton *upButton = nullptr; + QToolButton *createButton = nullptr; + QToolButton *revealButton = nullptr; + QToolButton *moreButton = nullptr; + QLineEdit *pathField = nullptr; + QLineEdit *searchField = nullptr; + QString projectRoot; + QString currentPath; + QStringList history; + int historyIndex = -1; }; -#endif //ATLAS_FILEEXPLORER_H +#endif diff --git a/include/editor/views/hierarchyPanel.h b/include/editor/views/hierarchyPanel.h index 6467c22d..aac5243a 100644 --- a/include/editor/views/hierarchyPanel.h +++ b/include/editor/views/hierarchyPanel.h @@ -1,31 +1,58 @@ /* -* hierarchyPanel.h -* As part of the Atlas project -* Created by Max Van den Eynde in 2026 -* -------------------------------------- -* Description: Hierarchy panel definition -* Copyright (c) 2026 Max Van den Eynde -*/ + * hierarchyPanel.h + * As part of the Atlas project + * Created by Max Van den Eynde in 2026 + * -------------------------------------- + * Description: Hierarchy panel definition + * Copyright (c) 2026 Max Van den Eynde + */ #ifndef ATLAS_HIERARCHYPANEL_H #define ATLAS_HIERARCHYPANEL_H +#include +#include #include -class QTreeView; +class QJsonArray; +class QMenu; +class QPoint; +class QStandardItem; class QStandardItemModel; +class QToolButton; +class QTreeView; +class ViewportPanel; class HierarchyPanel : public QWidget { Q_OBJECT -public: - explicit HierarchyPanel(QWidget* parent = nullptr); - -private: - void populateTemporaryScene(); - - QTreeView* treeView = nullptr; - QStandardItemModel* model = nullptr; + public: + explicit HierarchyPanel(ViewportPanel *viewport, QWidget *parent = nullptr); + + private: + void applySceneSnapshot(const QString &snapshot); + void rebuildScene(const QString &sceneName, const QJsonArray &objects, + int selectedId); + void appendObjects(QStandardItem *parent, const QJsonArray &objects); + void showAddObjectMenu(const QPoint &position); + void showContextMenu(const QPoint &position); + void createObject(const QString &type, const QString &displayName); + void renameSelectedObject(); + void deleteSelectedObject(); + void focusSelectedObject(); + void moveSelectedObjectToRoot(); + int selectedObjectId() const; + QString sceneSignature(const QString &sceneName, + const QJsonArray &objects) const; + + ViewportPanel *viewport = nullptr; + QTreeView *treeView = nullptr; + QStandardItemModel *model = nullptr; + QToolButton *addButton = nullptr; + QToolButton *moreButton = nullptr; + QHash itemsById; + QString lastStructureSignature; + bool applyingSnapshot = false; }; -#endif //ATLAS_HIERARCHYPANEL_H +#endif diff --git a/include/editor/views/viewport.h b/include/editor/views/viewport.h index b08b92b4..2b59dadb 100644 --- a/include/editor/views/viewport.h +++ b/include/editor/views/viewport.h @@ -1,11 +1,11 @@ /* -* viewport.h -* As part of the Atlas project -* Created by Max Van den Eynde in 2026 -* -------------------------------------- -* Description: Viewport declaration -* Copyright (c) 2026 Max Van den Eynde -*/ + * viewport.h + * As part of the Atlas project + * Created by Max Van den Eynde in 2026 + * -------------------------------------- + * Description: Viewport declaration + * Copyright (c) 2026 Max Van den Eynde + */ #ifndef ATLAS_VIEWPORT_H #define ATLAS_VIEWPORT_H @@ -30,43 +30,55 @@ class QWheelEvent; class ViewportPanel : public QWidget { Q_OBJECT -public: - explicit ViewportPanel(const QString& projectFile, - QWidget* parent = nullptr); + public: + explicit ViewportPanel(const QString &projectFile, + QWidget *parent = nullptr); ~ViewportPanel() override; QSize sizeHint() const override; QSize minimumSizeHint() const override; void shutdownRuntime(); + bool selectRuntimeObject(int id, bool focusCamera = true); + bool renameRuntimeObject(int id, const QString &name); + bool setRuntimeObjectParent(int childId, int parentId); + bool deleteRuntimeObject(int id); + int createRuntimeObject(const QString &type, const QString &name = {}); + bool saveRuntimeScene(); -protected: - QPaintEngine* paintEngine() const override; - void showEvent(QShowEvent* event) override; - void hideEvent(QHideEvent* event) override; - void closeEvent(QCloseEvent* event) override; - void resizeEvent(QResizeEvent* event) override; - void mousePressEvent(QMouseEvent* event) override; - void mouseMoveEvent(QMouseEvent* event) override; - void mouseReleaseEvent(QMouseEvent* event) override; - void wheelEvent(QWheelEvent* event) override; - void keyPressEvent(QKeyEvent* event) override; - void keyReleaseEvent(QKeyEvent* event) override; + signals: + void sceneSnapshotChanged(const QString &snapshot); + void runtimeAvailabilityChanged(bool available); -private: + protected: + QPaintEngine *paintEngine() const override; + void showEvent(QShowEvent *event) override; + void hideEvent(QHideEvent *event) override; + void closeEvent(QCloseEvent *event) override; + void resizeEvent(QResizeEvent *event) override; + void mousePressEvent(QMouseEvent *event) override; + void mouseMoveEvent(QMouseEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override; + void wheelEvent(QWheelEvent *event) override; + void keyPressEvent(QKeyEvent *event) override; + void keyReleaseEvent(QKeyEvent *event) override; + + private: void scheduleRuntimeStart(); void startRuntime(); void stopRuntime(); void stepRuntime(); void resizeRuntime(); void sendPointerEvent(int action, float x, float y, int button); + void refreshSceneSnapshot(); - QTimer* frameTimer = nullptr; + QTimer *frameTimer = nullptr; QString projectFile; std::shared_ptr runtimeContext; int runtimeWidth = 0; int runtimeHeight = 0; float runtimeScale = 0.0f; + QString lastSceneSnapshot; bool runtimeStartQueued = false; bool shuttingDown = false; }; -#endif //ATLAS_VIEWPORT_H +#endif // ATLAS_VIEWPORT_H