From 28d796fccb02c1877a37a85b060ef17327326bea Mon Sep 17 00:00:00 2001 From: Max Van den Eynde Date: Sun, 2 Aug 2026 19:58:27 +0200 Subject: [PATCH 1/5] Added input actions dialog --- atlas/application/window.cpp | 73 +++- editor/views/editor/editor.cpp | 28 +- editor/views/editor/inspector.cpp | 155 ++++++- editor/views/editor/viewport.cpp | 278 +++++++++++++ editor/views/general/inputActionsDialog.cpp | 430 +++++++++++++++----- include/atlas/runtime/context.h | 5 + include/atlas/window.h | 10 + include/editor/views/editorWindow.h | 2 + include/editor/views/inputActionsDialog.h | 20 +- include/editor/views/inspectorView.h | 1 + include/editor/views/viewport.h | 6 + runtime/docs/other.md | 13 +- runtime/lib/context.cpp | 117 +++++- 13 files changed, 1012 insertions(+), 126 deletions(-) diff --git a/atlas/application/window.cpp b/atlas/application/window.cpp index f150110c..1f5cefb8 100644 --- a/atlas/application/window.cpp +++ b/atlas/application/window.cpp @@ -1463,9 +1463,13 @@ bool Window::stepFrame() { constexpr float MAX_DELTA_TIME = 1.0f / 30.0f; currentFrame++; - this->relativeMousePos = {.x = 0.0f, .y = 0.0f}; - this->keysPressedThisFrame.fill(false); - this->mouseButtonsPressedThisFrame.fill(false); + this->relativeMousePos = this->editorRuntimeRelativeMousePending; + this->editorRuntimeRelativeMousePending = {.x = 0.0f, .y = 0.0f}; + this->keysPressedThisFrame = this->editorRuntimeKeysPressedPending; + this->editorRuntimeKeysPressedPending.fill(false); + this->mouseButtonsPressedThisFrame = + this->editorRuntimeMouseButtonsPressedPending; + this->editorRuntimeMouseButtonsPressedPending.fill(false); this->textInputBuffer.clear(); this->pollEvents(); @@ -2118,6 +2122,8 @@ void Window::setEditorCameraFocused(bool focused) { void Window::setEditorSimulationEnabled(bool enabled) { editorSimulationEnabled = enabled; + if (!enabled) + clearEditorRuntimeInput(); editorDragging = false; editorKeyboardTransform = false; editorActiveGizmoAxis = 0; @@ -2473,6 +2479,52 @@ void Window::editorKeyEvent(int key, bool pressed) { editorCameraKeys[static_cast(key)] = pressed; } +void Window::editorRuntimeKeyEvent(int key, bool pressed) { + if (key < 0 || key >= static_cast(editorRuntimeKeysActive.size())) + return; + const std::size_t index = static_cast(key); + if (pressed && !editorRuntimeKeysActive[index]) + editorRuntimeKeysPressedPending[index] = true; + editorRuntimeKeysActive[index] = pressed; +} + +void Window::editorRuntimeMouseMove(float x, float y, float deltaX, + float deltaY) { + editorRuntimeRelativeMousePending.x += deltaX; + editorRuntimeRelativeMousePending.y += deltaY; + lastMouseX = x; + lastMouseY = y; + if (editorSimulationEnabled && currentScene != nullptr) + currentScene->onMouseMove(*this, {.x = deltaX, .y = deltaY}); +} + +void Window::editorRuntimeMouseButtonEvent(int action, int button) { + if (button <= 0 || + button >= static_cast(editorRuntimeMouseButtonsActive.size())) + return; + const std::size_t index = static_cast(button); + if (action == 0) { + if (!editorRuntimeMouseButtonsActive[index]) + editorRuntimeMouseButtonsPressedPending[index] = true; + editorRuntimeMouseButtonsActive[index] = true; + } else if (action == 2) { + editorRuntimeMouseButtonsActive[index] = false; + } +} + +void Window::editorRuntimeScrollEvent(float x, float y) { + if (editorSimulationEnabled && currentScene != nullptr) + currentScene->onMouseScroll(*this, {.x = x, .y = y}); +} + +void Window::clearEditorRuntimeInput() { + editorRuntimeKeysActive.fill(false); + editorRuntimeKeysPressedPending.fill(false); + editorRuntimeMouseButtonsActive.fill(false); + editorRuntimeMouseButtonsPressedPending.fill(false); + editorRuntimeRelativeMousePending = {.x = 0.0f, .y = 0.0f}; +} + void Window::selectEditorObjectAt(float x, float y, float scale) { if (camera == nullptr) { selectedEditorObject = nullptr; @@ -4187,8 +4239,12 @@ bool Window::isKeyActive(Key key) { int keyCount = 0; const bool *state = SDL_GetKeyboardState(&keyCount); const int scancode = static_cast(key); - return state != nullptr && scancode >= 0 && scancode < keyCount && - state[scancode]; + const bool editorActive = + scancode >= 0 && + scancode < static_cast(editorRuntimeKeysActive.size()) && + editorRuntimeKeysActive[static_cast(scancode)]; + return editorActive || (state != nullptr && scancode >= 0 && + scancode < keyCount && state[scancode]); } bool Window::isKeyPressed(Key key) { @@ -4200,7 +4256,12 @@ bool Window::isKeyPressed(Key key) { bool Window::isMouseButtonActive(MouseButton button) { const SDL_MouseButtonFlags state = SDL_GetMouseState(nullptr, nullptr); - return (state & SDL_BUTTON_MASK(static_cast(button))) != 0; + const int index = static_cast(button); + const bool editorActive = + index >= 0 && + index < static_cast(editorRuntimeMouseButtonsActive.size()) && + editorRuntimeMouseButtonsActive[static_cast(index)]; + return editorActive || (state & SDL_BUTTON_MASK(index)) != 0; } bool Window::isMouseButtonPressed(MouseButton button) { diff --git a/editor/views/editor/editor.cpp b/editor/views/editor/editor.cpp index a1fc678e..512c1ce5 100644 --- a/editor/views/editor/editor.cpp +++ b/editor/views/editor/editor.cpp @@ -577,8 +577,6 @@ void EditorWindow::setupMenus() { auto *toolsSettings = addCommand(toolsMenu, "Project Settings…", QString(), [this] { showProjectSettings(); }); toolsSettings->setMenuRole(QAction::NoRole); - addCommand(toolsMenu, "Input Actions…", QString(), - [this] { showInputActions(); }); addCommand(toolsMenu, "Install Atlas Toolchain…", QString(), [this] { ToolchainInstaller::install(this); }); addCommand(toolsMenu, "Command Palette…", "Meta+Shift+P", @@ -589,6 +587,9 @@ void EditorWindow::setupMenus() { windowMenu->addAction("Zoom", this, [this] { isMaximized() ? showNormal() : showMaximized(); }); + windowMenu->addSeparator(); + windowMenu->addAction("Controller Actions", this, + [this] { showInputActions(); }); auto *helpMenu = menuBar()->addMenu("Help"); auto *aboutAction = helpMenu->addAction("About Atlas Engine", this, [this] { @@ -602,9 +603,24 @@ void EditorWindow::setupMenus() { } void EditorWindow::showInputActions() { - InputActionsDialog dialog(projectFile, this); - if (dialog.exec() == QDialog::Accepted && viewportPanel != nullptr) - viewportPanel->reloadRuntime(); + if (inputActionsDialog != nullptr) { + inputActionsDialog->showNormal(); + inputActionsDialog->raise(); + inputActionsDialog->activateWindow(); + return; + } + inputActionsDialog = new InputActionsDialog(projectFile, this); + inputActionsDialog->setAttribute(Qt::WA_DeleteOnClose); + connect(inputActionsDialog, &InputActionsDialog::actionsSaved, this, + [this] { + if (viewportPanel != nullptr) + viewportPanel->reloadRuntime(); + }); + connect(inputActionsDialog, &QObject::destroyed, this, + [this] { inputActionsDialog = nullptr; }); + inputActionsDialog->show(); + inputActionsDialog->raise(); + inputActionsDialog->activateWindow(); } void EditorWindow::setupDocks() { @@ -2194,6 +2210,8 @@ bool EditorWindow::eventFilter(QObject *watched, QEvent *event) { } } if (event->type() == QEvent::KeyPress) { + if (viewportPanel != nullptr && viewportPanel->isRuntimePlaying()) + return QMainWindow::eventFilter(watched, event); auto *key = static_cast(event); if (!key->isAutoRepeat() && key->matches(QKeySequence::Undo)) { undoActiveEditor(); diff --git a/editor/views/editor/inspector.cpp b/editor/views/editor/inspector.cpp index f64e4be5..ce969e48 100644 --- a/editor/views/editor/inspector.cpp +++ b/editor/views/editor/inspector.cpp @@ -58,6 +58,7 @@ #include #include "editor/views/viewport.h" +#include "editor/views/inputActionsDialog.h" #include "editor/widgets/scrubbableSpinBox.h" namespace { @@ -607,6 +608,18 @@ void refreshTaggedEditors(QFrame *card, const QJsonObject &properties) { } const QSignalBlocker blocker(field); field->setText(entries.join(", ")); + } else if (kind == "action") { + auto *field = qobject_cast(editor); + if (field == nullptr || !value.isArray()) + continue; + const QJsonArray actions = value.toArray(); + const int index = + editor->property("inspectorValueIndex").toInt(); + const QString action = index >= 0 && index < actions.size() + ? actions.at(index).toString() + : QString(); + field->setProperty("actionValue", action); + field->setText(action.isEmpty() ? "Select Action" : action); } else if (kind == "color") { const QJsonArray array = value.toArray(); if (array.size() < 3) @@ -1311,6 +1324,134 @@ QFrame *componentCard(const QString &title, const QJsonObject &properties, return card; } +QFrame *controllerActionsCard(const QJsonArray &actions, + const QString &projectFile, + const PropertyChanged &changed, + QWidget *parent) { + auto *card = new QFrame(parent); + card->setObjectName("inspectorComponent"); + card->setProperty("inspectorScope", "camera:actions"); + auto *layout = new QVBoxLayout(card); + layout->setContentsMargins(0, 0, 0, 7); + layout->setSpacing(2); + auto *header = new QToolButton(card); + header->setObjectName("inspectorComponentHeader"); + header->setText("Controller Actions"); + header->setCheckable(true); + header->setChecked(true); + header->setIcon(styling::icon(styling::Icon::CaretDown, "#8490A4")); + header->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + layout->addWidget(header); + auto *body = new QWidget(card); + body->setObjectName("inspectorComponentBody"); + auto *bodyLayout = new QVBoxLayout(body); + bodyLayout->setContentsMargins(0, 2, 0, 0); + bodyLayout->setSpacing(1); + const QStringList labels{"Movement", "Look", "Vertical"}; + QList pickers; + for (int index = 0; index < labels.size(); ++index) { + auto *picker = new QToolButton(body); + picker->setObjectName("inspectorActionPicker"); + picker->setPopupMode(QToolButton::InstantPopup); + picker->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + picker->setIcon(styling::icon(styling::Icon::CaretDown, "#8490A4")); + const QString current = + index < actions.size() ? actions.at(index).toString() : QString(); + picker->setProperty("actionValue", current); + picker->setProperty("actionIndex", index); + picker->setText(current.isEmpty() ? "Select Action" : current); + tagEditor(picker, "/actions", "action", index); + pickers.append(picker); + bodyLayout->addWidget(propertyRow(labels.at(index), picker, body)); + } + auto commit = [pickers, changed] { + QJsonArray result; + for (QToolButton *picker : pickers) + result.append(picker->property("actionValue").toString()); + while (!result.isEmpty() && result.last().toString().isEmpty()) + result.removeAt(result.size() - 1); + changed("/actions", result); + }; + for (QToolButton *picker : pickers) { + auto *menu = new QMenu(picker); + auto *searchAction = new QWidgetAction(menu); + auto *search = new PickerSearchField(menu); + search->setPlaceholderText("Search actions"); + search->setClearButtonEnabled(true); + search->setMinimumWidth(260); + searchAction->setDefaultWidget(search); + menu->addAction(searchAction); + menu->addSeparator(); + QObject::connect( + menu, &QMenu::aboutToShow, menu, + [menu, search, picker, projectFile, commit] { + const QList existing = menu->actions(); + for (QAction *action : existing) { + if (action->property("actionChoice").toBool()) { + menu->removeAction(action); + delete action; + } + } + QAction *none = menu->addAction("Unassigned"); + none->setProperty("actionChoice", true); + none->setProperty("searchText", "unassigned none"); + QObject::connect(none, &QAction::triggered, picker, + [picker, commit] { + picker->setProperty("actionValue", + QString()); + picker->setText("Select Action"); + commit(); + }); + const QStringList names = + InputActionsDialog::actionNamesForProject(projectFile); + for (const QString &name : names) { + QAction *choice = menu->addAction(name); + choice->setProperty("actionChoice", true); + choice->setProperty("searchText", name.toLower()); + QObject::connect(choice, &QAction::triggered, picker, + [picker, name, commit] { + picker->setProperty("actionValue", + name); + picker->setText(name); + commit(); + }); + } + if (names.isEmpty()) { + QAction *empty = menu->addAction( + "No actions yet — use Window → Controller Actions"); + empty->setEnabled(false); + empty->setProperty("actionChoice", true); + } + search->clear(); + search->setFocus(); + }); + QObject::connect(search, &QLineEdit::textChanged, menu, + [menu](const QString &text) { + const QString query = text.trimmed().toLower(); + for (QAction *action : menu->actions()) { + if (!action->property("actionChoice").toBool() || + !action->isEnabled()) + continue; + action->setVisible( + query.isEmpty() || + action->property("searchText") + .toString() + .contains(query)); + } + }); + picker->setMenu(menu); + } + layout->addWidget(body); + QObject::connect( + header, &QToolButton::toggled, card, [header, body](bool expanded) { + body->setVisible(expanded); + header->setIcon(styling::icon(expanded ? styling::Icon::CaretDown + : styling::Icon::CaretRight, + "#8490A4")); + }); + return card; +} + QJsonObject findObjectInArray(const QJsonArray &objects, int id) { for (const QJsonValue &value : objects) { const QJsonObject object = value.toObject(); @@ -1327,7 +1468,7 @@ QJsonObject findObjectInArray(const QJsonArray &objects, int id) { InspectorPanel::InspectorPanel(ViewportPanel *viewport, const QString &projectFile, QWidget *parent) - : QWidget(parent), viewport(viewport) { + : QWidget(parent), viewport(viewport), projectFile(projectFile) { setObjectName("inspectorPanel"); setMinimumWidth(400); setAcceptDrops(true); @@ -1436,6 +1577,10 @@ void InspectorPanel::applySceneSnapshot(const QString &snapshot) { refreshTaggedEditors(card, focus); else if (scope == "camera:controls") refreshTaggedEditors(card, controls); + else if (scope == "camera:actions") + refreshTaggedEditors( + card, QJsonObject{{"actions", + inspectedCamera.value("actions")}}); } return; } @@ -1962,10 +2107,7 @@ void InspectorPanel::showCamera() { {"controllerLookSensitivity", inspectedCamera.value("controllerLookSensitivity")}, {"lookSmoothness", inspectedCamera.value("lookSmoothness")}, - {"automaticMoving", inspectedCamera.value("automaticMoving")}, - {"actions", inspectedCamera.value("actions").isArray() - ? inspectedCamera.value("actions") - : QJsonValue(QJsonArray{})}}; + {"automaticMoving", inspectedCamera.value("automaticMoving")}}; SyncOptions syncOptions; collectSyncOptions("Camera", inspectedCamera, QJsonObject{{"section", "camera"}}, QString(), @@ -1991,6 +2133,9 @@ void InspectorPanel::showCamera() { bindSyncProvider(syncProvider, viewport, &scene, QJsonObject{{"section", "camera"}}), {}, "camera:controls")); + contentLayout->addWidget(controllerActionsCard( + inspectedCamera.value("actions").toArray(), projectFile, update, + content)); contentLayout->addStretch(); } diff --git a/editor/views/editor/viewport.cpp b/editor/views/editor/viewport.cpp index b4113ad1..11c39549 100644 --- a/editor/views/editor/viewport.cpp +++ b/editor/views/editor/viewport.cpp @@ -115,6 +115,197 @@ int editorCameraKey(int key) { } } +int runtimeKey(const QKeyEvent *event) { + const int key = event->key(); + if (event->modifiers().testFlag(Qt::KeypadModifier)) { + switch (key) { + case Qt::Key_0: + return static_cast(Key::KP0); + case Qt::Key_1: + return static_cast(Key::KP1); + case Qt::Key_2: + return static_cast(Key::KP2); + case Qt::Key_3: + return static_cast(Key::KP3); + case Qt::Key_4: + return static_cast(Key::KP4); + case Qt::Key_5: + return static_cast(Key::KP5); + case Qt::Key_6: + return static_cast(Key::KP6); + case Qt::Key_7: + return static_cast(Key::KP7); + case Qt::Key_8: + return static_cast(Key::KP8); + case Qt::Key_9: + return static_cast(Key::KP9); + case Qt::Key_Period: + case Qt::Key_Comma: + return static_cast(Key::KPDecimal); + case Qt::Key_Slash: + return static_cast(Key::KPDivide); + case Qt::Key_Asterisk: + return static_cast(Key::KPMultiply); + case Qt::Key_Minus: + return static_cast(Key::KPSubtract); + case Qt::Key_Plus: + return static_cast(Key::KPAdd); + case Qt::Key_Equal: + return static_cast(Key::KPEqual); + default: + break; + } + } + if (key >= Qt::Key_A && key <= Qt::Key_Z) + return static_cast(Key::A) + key - Qt::Key_A; + switch (key) { + case Qt::Key_0: + case Qt::Key_ParenRight: + return static_cast(Key::Key0); + case Qt::Key_1: + case Qt::Key_Exclam: + return static_cast(Key::Key1); + case Qt::Key_2: + case Qt::Key_At: + return static_cast(Key::Key2); + case Qt::Key_3: + case Qt::Key_NumberSign: + return static_cast(Key::Key3); + case Qt::Key_4: + case Qt::Key_Dollar: + return static_cast(Key::Key4); + case Qt::Key_5: + case Qt::Key_Percent: + return static_cast(Key::Key5); + case Qt::Key_6: + case Qt::Key_AsciiCircum: + return static_cast(Key::Key6); + case Qt::Key_7: + case Qt::Key_Ampersand: + return static_cast(Key::Key7); + case Qt::Key_8: + case Qt::Key_Asterisk: + return static_cast(Key::Key8); + case Qt::Key_9: + case Qt::Key_ParenLeft: + return static_cast(Key::Key9); + case Qt::Key_Space: + return static_cast(Key::Space); + case Qt::Key_Apostrophe: + case Qt::Key_QuoteDbl: + return static_cast(Key::Apostrophe); + case Qt::Key_Comma: + case Qt::Key_Less: + return static_cast(Key::Comma); + case Qt::Key_Minus: + case Qt::Key_Underscore: + return static_cast(Key::Minus); + case Qt::Key_Period: + case Qt::Key_Greater: + return static_cast(Key::Period); + case Qt::Key_Slash: + case Qt::Key_Question: + return static_cast(Key::Slash); + case Qt::Key_Semicolon: + case Qt::Key_Colon: + return static_cast(Key::Semicolon); + case Qt::Key_Equal: + case Qt::Key_Plus: + return static_cast(Key::Equal); + case Qt::Key_BracketLeft: + case Qt::Key_BraceLeft: + return static_cast(Key::LeftBracket); + case Qt::Key_Backslash: + case Qt::Key_Bar: + return static_cast(Key::Backslash); + case Qt::Key_BracketRight: + case Qt::Key_BraceRight: + return static_cast(Key::RightBracket); + case Qt::Key_QuoteLeft: + case Qt::Key_AsciiTilde: + return static_cast(Key::GraveAccent); + case Qt::Key_Return: + case Qt::Key_Enter: + return event->modifiers().testFlag(Qt::KeypadModifier) + ? static_cast(Key::KPEnter) + : static_cast(Key::Enter); + case Qt::Key_Tab: + case Qt::Key_Backtab: + return static_cast(Key::Tab); + case Qt::Key_Backspace: + return static_cast(Key::Backspace); + case Qt::Key_Insert: + return static_cast(Key::Insert); + case Qt::Key_Delete: + return static_cast(Key::Delete); + case Qt::Key_Right: + return static_cast(Key::Right); + case Qt::Key_Left: + return static_cast(Key::Left); + case Qt::Key_Down: + return static_cast(Key::Down); + case Qt::Key_Up: + return static_cast(Key::Up); + case Qt::Key_PageUp: + return static_cast(Key::PageUp); + case Qt::Key_PageDown: + return static_cast(Key::PageDown); + case Qt::Key_Home: + return static_cast(Key::Home); + case Qt::Key_End: + return static_cast(Key::End); + case Qt::Key_CapsLock: + return static_cast(Key::CapsLock); + case Qt::Key_ScrollLock: + return static_cast(Key::ScrollLock); + case Qt::Key_NumLock: + return static_cast(Key::NumLock); + case Qt::Key_Print: + return static_cast(Key::PrintScreen); + case Qt::Key_Pause: + return static_cast(Key::Pause); + case Qt::Key_Shift: +#ifdef Q_OS_MACOS + return event->nativeScanCode() == 60 ? static_cast(Key::RightShift) + : static_cast(Key::LeftShift); +#else + return static_cast(Key::LeftShift); +#endif + case Qt::Key_Control: +#ifdef Q_OS_MACOS + return event->nativeScanCode() == 62 + ? static_cast(Key::RightControl) + : static_cast(Key::LeftControl); +#else + return static_cast(Key::LeftControl); +#endif + case Qt::Key_Alt: +#ifdef Q_OS_MACOS + return event->nativeScanCode() == 61 ? static_cast(Key::RightAlt) + : static_cast(Key::LeftAlt); +#else + return static_cast(Key::LeftAlt); +#endif + case Qt::Key_Meta: +#ifdef Q_OS_MACOS + return event->nativeScanCode() == 54 + ? static_cast(Key::RightSuper) + : static_cast(Key::LeftSuper); +#else + return static_cast(Key::LeftSuper); +#endif + case Qt::Key_Menu: + return static_cast(Key::Menu); + default: + break; + } + if (key >= Qt::Key_F1 && key <= Qt::Key_F12) + return static_cast(Key::F1) + key - Qt::Key_F1; + if (key >= Qt::Key_F13 && key <= Qt::Key_F24) + return static_cast(Key::F13) + key - Qt::Key_F13; + return -1; +} + float widgetScale(QWidget *widget) { const qreal scale = widget != nullptr ? widget->devicePixelRatioF() : 1.0; return scale > 0.0 ? static_cast(scale) : 1.0f; @@ -284,6 +475,14 @@ ViewportPanel::ViewportPanel(const QString &projectFile, QWidget *parent) } } +bool ViewportPanel::event(QEvent *event) { + if (playbackState == 1 && event->type() == QEvent::ShortcutOverride) { + event->accept(); + return true; + } + return QWidget::event(event); +} + ViewportPanel::~ViewportPanel() { shutdownRuntime(); } QSize ViewportPanel::sizeHint() const { return QSize(640, 360); } @@ -300,6 +499,8 @@ void ViewportPanel::setRuntimeStartupEnabled(bool enabled) { void ViewportPanel::showEvent(QShowEvent *event) { QWidget::showEvent(event); + if (playbackState == 1) + captureRuntimeInput(); if (runtimeContext != nullptr) { frameTimer->start(RuntimeFrameIntervalMs); return; @@ -309,6 +510,7 @@ void ViewportPanel::showEvent(QShowEvent *event) { } void ViewportPanel::hideEvent(QHideEvent *event) { + releaseRuntimeInput(); frameTimer->stop(); QWidget::hideEvent(event); } @@ -399,6 +601,12 @@ void ViewportPanel::shutdownRuntime() { void ViewportPanel::mousePressEvent(QMouseEvent *event) { setFocus(Qt::MouseFocusReason); + if (playbackState == 1 && runtimeContext != nullptr) { + runtimeContext->editorRuntimeMouseButtonEvent( + 0, runtimeMouseButton(event->button())); + event->accept(); + return; + } if (keyboardTransformActive && (event->button() == Qt::LeftButton || event->button() == Qt::RightButton)) { finishKeyboardTransform(event->button() == Qt::LeftButton); @@ -436,6 +644,20 @@ void ViewportPanel::mousePressEvent(QMouseEvent *event) { } void ViewportPanel::mouseMoveEvent(QMouseEvent *event) { + if (playbackState == 1 && runtimeContext != nullptr) { + const QPoint center(width() / 2, height() / 2); + const QPointF delta = event->position() - QPointF(center); + if (!qFuzzyIsNull(delta.x()) || !qFuzzyIsNull(delta.y())) { + runtimeContext->editorRuntimeMouseMove( + static_cast(event->position().x()), + static_cast(height() - event->position().y()), + static_cast(delta.x()), + static_cast(-delta.y())); + QCursor::setPos(mapToGlobal(center)); + } + event->accept(); + return; + } if (event->buttons().testFlag(Qt::LeftButton)) leftPointerMoved = true; sendPointerEvent(1, static_cast(event->position().x()), @@ -468,6 +690,12 @@ void ViewportPanel::mouseMoveEvent(QMouseEvent *event) { } void ViewportPanel::mouseReleaseEvent(QMouseEvent *event) { + if (playbackState == 1 && runtimeContext != nullptr) { + runtimeContext->editorRuntimeMouseButtonEvent( + 2, runtimeMouseButton(event->button())); + event->accept(); + return; + } const int pointerButton = event->button() == Qt::RightButton ? rightDragRuntimeButton : event->button() == Qt::MiddleButton @@ -498,7 +726,13 @@ void ViewportPanel::wheelEvent(QWheelEvent *event) { QWidget::wheelEvent(event); return; } + const float deltaX = static_cast(event->angleDelta().x()) / 120.0f; const float delta = static_cast(event->angleDelta().y()) / 120.0f; + if (playbackState == 1) { + runtimeContext->editorRuntimeScrollEvent(deltaX, delta); + event->accept(); + return; + } if (std::abs(delta) > 0.0f) { runtimeContext->editorScrollEvent(delta, widgetScale(this)); } @@ -506,6 +740,18 @@ void ViewportPanel::wheelEvent(QWheelEvent *event) { } void ViewportPanel::keyPressEvent(QKeyEvent *event) { + if (playbackState == 1 && runtimeContext != nullptr) { + if (event->key() == Qt::Key_Escape && !event->isAutoRepeat()) { + stopRuntimePlayback(); + event->accept(); + return; + } + const int key = runtimeKey(event); + if (key >= 0 && !event->isAutoRepeat()) + runtimeContext->editorRuntimeKeyEvent(key, true); + event->accept(); + return; + } if (!event->isAutoRepeat() && runtimeContext != nullptr && playbackState == 0) { if (event->key() == Qt::Key_0 && @@ -571,6 +817,13 @@ void ViewportPanel::keyPressEvent(QKeyEvent *event) { } void ViewportPanel::keyReleaseEvent(QKeyEvent *event) { + if (playbackState == 1 && runtimeContext != nullptr) { + const int key = runtimeKey(event); + if (key >= 0 && !event->isAutoRepeat()) + runtimeContext->editorRuntimeKeyEvent(key, false); + event->accept(); + return; + } const int key = editorCameraKey(event->key()); if (event->isAutoRepeat()) { if (key >= 0) { @@ -680,6 +933,7 @@ void ViewportPanel::startRuntime() { runtimeContext->setEditorSimulationEnabled(true); playbackState = 1; emit playbackStateChanged(playbackState); + captureRuntimeInput(); } } catch (const std::exception &error) { const QString message = QString::fromUtf8(error.what()); @@ -708,6 +962,7 @@ void ViewportPanel::startRuntime() { } void ViewportPanel::stopRuntime() { + releaseRuntimeInput(); if (frameTimer != nullptr) { frameTimer->stop(); } @@ -1376,6 +1631,7 @@ void ViewportPanel::pauseRuntime() { return; } runtimeContext->setEditorSimulationEnabled(false); + releaseRuntimeInput(); refreshSceneSnapshot(); playbackState = 2; emit playbackStateChanged(playbackState); @@ -1402,6 +1658,28 @@ void ViewportPanel::stopRuntimePlayback() { reloadRuntime(); } +void ViewportPanel::captureRuntimeInput() { + if (runtimeInputCaptured || playbackState != 1 || !isVisible()) + return; + setFocus(Qt::OtherFocusReason); + grabKeyboard(); + grabMouse(); + setCursor(Qt::BlankCursor); + runtimeInputCaptured = true; + QCursor::setPos(mapToGlobal(QPoint(width() / 2, height() / 2))); +} + +void ViewportPanel::releaseRuntimeInput() { + if (runtimeContext != nullptr) + runtimeContext->clearEditorRuntimeInput(); + if (!runtimeInputCaptured) + return; + releaseKeyboard(); + releaseMouse(); + unsetCursor(); + runtimeInputCaptured = false; +} + void ViewportPanel::reloadRuntime() { if (shuttingDown) { return; diff --git a/editor/views/general/inputActionsDialog.cpp b/editor/views/general/inputActionsDialog.cpp index 144d11dc..bb06ccf8 100644 --- a/editor/views/general/inputActionsDialog.cpp +++ b/editor/views/general/inputActionsDialog.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -25,10 +26,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include @@ -37,11 +40,17 @@ QStringList bindingValues() { QStringList values; for (char letter = 'A'; letter <= 'Z'; ++letter) values.append(QString(QChar(letter))); + for (char digit = '0'; digit <= '9'; ++digit) + values.append(QString(QChar(digit))); + for (int functionKey = 1; functionKey <= 12; ++functionKey) + values.append(QStringLiteral("F%1").arg(functionKey)); values << "Space" << "Enter" << "Escape" << "Tab" << "Backspace" - << "Up" << "Down" << "Left" << "Right" << "Left Shift" - << "Right Shift" << "Left Control" << "Right Control" - << "Left Alt" << "Right Alt" << "MouseLeft" << "MouseRight" - << "MouseMiddle" << "Mouse4" << "Mouse5"; + << "Insert" << "Delete" << "Home" << "End" << "Page Up" + << "Page Down" << "Up" << "Down" << "Left" << "Right" + << "Left Shift" << "Right Shift" << "Left Control" + << "Right Control" << "Left Alt" << "Right Alt" << "Left Super" + << "Right Super" << "MouseLeft" << "MouseRight" << "MouseMiddle" + << "Mouse4" << "Mouse5"; return values; } @@ -52,6 +61,47 @@ QComboBox *bindingCombo(QWidget *parent) { return combo; } +QStringList controllerButtonNames() { + return {"A", "B", "X", "Y", + "Left Bumper", "Right Bumper", "Back", "Start", + "Guide", "Left Thumb", "Right Thumb", "D-Pad Up", + "D-Pad Right", "D-Pad Down", "D-Pad Left"}; +} + +QStringList controllerAxisNames() { + return {"Left Stick X", "Left Stick Y", "Right Stick X", + "Right Stick Y", "Left Trigger", "Right Trigger"}; +} + +QString controllerNameForIndex(const QStringList &names, int index) { + return index >= 0 && index < names.size() ? names.at(index) + : QString::number(index); +} + +int controllerIndexForName(const QStringList &names, const QString &name) { + for (int index = 0; index < names.size(); ++index) { + if (names.at(index).compare(name, Qt::CaseInsensitive) == 0) + return index; + } + bool valid = false; + const int index = name.toInt(&valid); + return valid ? index : -1; +} + +QJsonValue controllerValue(const QStringList &names, const QString &name) { + return controllerIndexForName(names, name) >= 0 && + names.contains(name, Qt::CaseInsensitive) + ? QJsonValue(name) + : QJsonValue(controllerIndexForName(names, name)); +} + +QComboBox *controllerCombo(const QStringList &names, QWidget *parent) { + auto *combo = new QComboBox(parent); + combo->setEditable(true); + combo->addItems(names); + return combo; +} + QString kindName(InputActionsDialog::ActionKind kind) { if (kind == InputActionsDialog::ActionKind::Axis1D) return "1D Axis"; @@ -59,28 +109,75 @@ QString kindName(InputActionsDialog::ActionKind kind) { return "2D Axis"; return "Button"; } + +QString configuredActionsPath(const QString &projectFile) { + QFile file(projectFile); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) + return {}; + const QString contents = QString::fromUtf8(file.readAll()); + const QRegularExpression gameExpression( + QStringLiteral(R"((?ms)^[ \t]*\[game\][ \t]*\r?\n(.*?)(?=^[ \t]*\[[^\]]+\][ \t]*\r?$|\z))")); + const QRegularExpressionMatch gameMatch = gameExpression.match(contents); + if (!gameMatch.hasMatch()) + return {}; + const QRegularExpression pathExpression( + QStringLiteral(R"((?m)^[ \t]*input_actions[ \t]*=[ \t]*["']([^"']+)["'][ \t]*$)")); + const QRegularExpressionMatch pathMatch = + pathExpression.match(gameMatch.captured(1)); + return pathMatch.hasMatch() ? pathMatch.captured(1).trimmed() : QString(); +} +} + +QString InputActionsDialog::actionsFileForProject(const QString &projectFile) { + const QFileInfo projectInfo(projectFile); + const QString configured = configuredActionsPath(projectFile); + if (configured.isEmpty()) + return projectInfo.absoluteDir().filePath("input-actions.json"); + const QFileInfo configuredInfo(configured); + return configuredInfo.isAbsolute() + ? configuredInfo.absoluteFilePath() + : projectInfo.absoluteDir().absoluteFilePath(configured); +} + +QStringList +InputActionsDialog::actionNamesForProject(const QString &projectFile) { + QFile file(actionsFileForProject(projectFile)); + if (!file.open(QIODevice::ReadOnly)) + return {}; + const QJsonDocument document = QJsonDocument::fromJson(file.readAll()); + if (!document.isObject()) + return {}; + QStringList names; + for (const QJsonValue &value : + document.object().value("actions").toArray()) { + const QString name = value.toObject().value("name").toString().trimmed(); + if (!name.isEmpty() && !names.contains(name, Qt::CaseInsensitive)) + names.append(name); + } + names.sort(Qt::CaseInsensitive); + return names; } InputActionsDialog::InputActionsDialog(const QString &projectFile, QWidget *parent) : QDialog(parent), projectFile(QFileInfo(projectFile).absoluteFilePath()), - actionsFile( - QFileInfo(projectFile).absoluteDir().filePath("input-actions.json")) { + actionsFile(actionsFileForProject(projectFile)) { setupUi(); load(); } void InputActionsDialog::setupUi() { - setWindowTitle("Project Input Actions"); + setWindowTitle("Actions"); + setWindowFlag(Qt::Window, true); setWindowFlag(Qt::WindowContextHelpButtonHint, false); - resize(940, 680); - setMinimumSize(780, 560); + resize(1120, 760); + setMinimumSize(900, 620); auto *root = new QVBoxLayout(this); root->setContentsMargins(16, 16, 16, 16); root->setSpacing(12); - auto *heading = new QLabel("Input Actions", this); + auto *heading = new QLabel("Controller Actions", this); QFont headingFont = heading->font(); headingFont.setPointSizeF(18); headingFont.setWeight(QFont::DemiBold); @@ -126,10 +223,14 @@ void InputActionsDialog::setupUi() { sidebarButtons->addWidget(removeButton); sidebarLayout->addLayout(sidebarButtons); - auto *editor = new QWidget(splitter); + auto *editorScroll = new QScrollArea(splitter); + editorScroll->setWidgetResizable(true); + editorScroll->setFrameShape(QFrame::NoFrame); + auto *editor = new QWidget(editorScroll); auto *editorLayout = new QVBoxLayout(editor); - editorLayout->setContentsMargins(12, 0, 0, 0); + editorLayout->setContentsMargins(16, 0, 8, 0); editorLayout->setSpacing(12); + editorScroll->setWidget(editor); auto *identity = new QFormLayout(); nameField = new QLineEdit(editor); @@ -146,9 +247,15 @@ void InputActionsDialog::setupUi() { buttonLayout->setContentsMargins(0, 0, 0, 0); buttonBindings = new QTableWidget(0, 4, buttonPage); buttonBindings->setHorizontalHeaderLabels( - {"Source", "Key / Mouse", "Controller", "Button"}); + {"Source", "Key / Mouse", "Controller", "Controller Button"}); + buttonBindings->horizontalHeader()->setSectionResizeMode( + 0, QHeaderView::ResizeToContents); buttonBindings->horizontalHeader()->setSectionResizeMode( 1, QHeaderView::Stretch); + buttonBindings->horizontalHeader()->setSectionResizeMode( + 2, QHeaderView::ResizeToContents); + buttonBindings->horizontalHeader()->setSectionResizeMode( + 3, QHeaderView::Stretch); buttonBindings->verticalHeader()->setVisible(false); buttonBindings->setSelectionBehavior(QAbstractItemView::SelectRows); buttonBindings->setSelectionMode(QAbstractItemView::SingleSelection); @@ -165,58 +272,74 @@ void InputActionsDialog::setupUi() { auto *axisPage = new QWidget(bindingPages); auto *axisLayout = new QVBoxLayout(axisPage); axisLayout->setContentsMargins(0, 0, 0, 0); + auto *keyboardGroup = new QGroupBox("Keyboard", axisPage); + auto *keyboardLayout = new QVBoxLayout(keyboardGroup); + keyboardAxisField = new QCheckBox("Use keyboard bindings", keyboardGroup); + keyboardLayout->addWidget(keyboardAxisField); auto *directionForm = new QFormLayout(); - positiveXField = bindingCombo(axisPage); - negativeXField = bindingCombo(axisPage); - positiveYField = bindingCombo(axisPage); - negativeYField = bindingCombo(axisPage); + positiveXField = bindingCombo(keyboardGroup); + negativeXField = bindingCombo(keyboardGroup); + positiveYField = bindingCombo(keyboardGroup); + negativeYField = bindingCombo(keyboardGroup); directionForm->addRow("Positive X", positiveXField); directionForm->addRow("Negative X", negativeXField); positiveYLabel = new QLabel("Positive Y", axisPage); negativeYLabel = new QLabel("Negative Y", axisPage); directionForm->addRow(positiveYLabel, positiveYField); directionForm->addRow(negativeYLabel, negativeYField); - axisLayout->addLayout(directionForm); + keyboardLayout->addLayout(directionForm); + axisLayout->addWidget(keyboardGroup); + + auto *mouseGroup = new QGroupBox("Mouse", axisPage); + auto *mouseLayout = new QVBoxLayout(mouseGroup); + mouseAxisField = new QCheckBox("Use mouse movement", mouseGroup); + mouseLayout->addWidget(mouseAxisField); + axisLayout->addWidget(mouseGroup); - mouseAxisField = new QCheckBox("Include mouse movement", axisPage); - controllerAxisField = new QCheckBox("Include controller axis", axisPage); - axisLayout->addWidget(mouseAxisField); - axisLayout->addWidget(controllerAxisField); + auto *controllerGroup = new QGroupBox("Controller", axisPage); + auto *controllerLayout = new QVBoxLayout(controllerGroup); + controllerAxisField = + new QCheckBox("Use a named controller axis", controllerGroup); + controllerLayout->addWidget(controllerAxisField); auto *controllerForm = new QFormLayout(); - controllerIdField = new QSpinBox(axisPage); + controllerIdField = new QSpinBox(controllerGroup); controllerIdField->setRange(-1, 15); controllerIdField->setSpecialValueText("Any"); - controllerAxisXField = new QSpinBox(axisPage); - controllerAxisXField->setRange(0, 31); - controllerAxisYField = new QSpinBox(axisPage); - controllerAxisYField->setRange(0, 31); - controllerAxisYLabel = new QLabel("Controller Y axis", axisPage); + controllerAxisXField = + controllerCombo(controllerAxisNames(), controllerGroup); + controllerAxisYField = + controllerCombo(controllerAxisNames(), controllerGroup); + controllerAxisYLabel = new QLabel("Y axis", controllerGroup); controllerForm->addRow("Controller", controllerIdField); - controllerForm->addRow("Controller X axis", controllerAxisXField); + controllerForm->addRow("X axis", controllerAxisXField); controllerForm->addRow(controllerAxisYLabel, controllerAxisYField); - axisLayout->addLayout(controllerForm); + controllerLayout->addLayout(controllerForm); + axisLayout->addWidget(controllerGroup); + auto *processingGroup = new QGroupBox("Processing", axisPage); + auto *processingLayout = new QVBoxLayout(processingGroup); auto *processingForm = new QFormLayout(); - deadzoneField = new QDoubleSpinBox(axisPage); + deadzoneField = new QDoubleSpinBox(processingGroup); deadzoneField->setRange(0.0, 1.0); deadzoneField->setSingleStep(0.05); - scaleXField = new QDoubleSpinBox(axisPage); + scaleXField = new QDoubleSpinBox(processingGroup); scaleXField->setRange(-100.0, 100.0); scaleXField->setSingleStep(0.1); - scaleYField = new QDoubleSpinBox(axisPage); + scaleYField = new QDoubleSpinBox(processingGroup); scaleYField->setRange(-100.0, 100.0); scaleYField->setSingleStep(0.1); - scaleYLabel = new QLabel("Y scale", axisPage); + scaleYLabel = new QLabel("Y scale", processingGroup); processingForm->addRow("Controller deadzone", deadzoneField); processingForm->addRow("X scale", scaleXField); processingForm->addRow(scaleYLabel, scaleYField); - axisLayout->addLayout(processingForm); - normalizeField = new QCheckBox("Normalize 2D value", axisPage); - invertYField = new QCheckBox("Invert controller Y", axisPage); - clampField = new QCheckBox("Clamp values to -1…1", axisPage); - axisLayout->addWidget(normalizeField); - axisLayout->addWidget(invertYField); - axisLayout->addWidget(clampField); + processingLayout->addLayout(processingForm); + normalizeField = new QCheckBox("Normalize 2D value", processingGroup); + invertYField = new QCheckBox("Invert controller Y", processingGroup); + clampField = new QCheckBox("Clamp values to -1…1", processingGroup); + processingLayout->addWidget(normalizeField); + processingLayout->addWidget(invertYField); + processingLayout->addWidget(clampField); + axisLayout->addWidget(processingGroup); axisLayout->addStretch(); bindingPages->addWidget(axisPage); editorLayout->addWidget(bindingPages, 1); @@ -233,11 +356,11 @@ void InputActionsDialog::setupUi() { editorLayout->addWidget(scriptExample); splitter->addWidget(sidebar); - splitter->addWidget(editor); - splitter->setSizes({270, 670}); + splitter->addWidget(editorScroll); + splitter->setSizes({290, 830}); auto *buttons = new QDialogButtonBox( - QDialogButtonBox::Cancel | QDialogButtonBox::Save, this); + QDialogButtonBox::Close | QDialogButtonBox::Save, this); buttons->button(QDialogButtonBox::Save)->setText("Save Actions"); root->addWidget(buttons); @@ -273,19 +396,23 @@ void InputActionsDialog::setupUi() { for (QComboBox *field : {positiveXField, negativeXField, positiveYField, negativeYField}) connect(field, &QComboBox::currentTextChanged, this, markChanged); - for (QCheckBox *field : {mouseAxisField, controllerAxisField, + for (QCheckBox *field : {keyboardAxisField, mouseAxisField, + controllerAxisField, normalizeField, invertYField, clampField}) connect(field, &QCheckBox::toggled, this, markChanged); - for (QSpinBox *field : - {controllerIdField, controllerAxisXField, controllerAxisYField}) + connect(controllerAxisXField, &QComboBox::currentTextChanged, this, + markChanged); + connect(controllerAxisYField, &QComboBox::currentTextChanged, this, + markChanged); + for (QSpinBox *field : {controllerIdField}) connect(field, &QSpinBox::valueChanged, this, markChanged); for (QDoubleSpinBox *field : {deadzoneField, scaleXField, scaleYField}) connect(field, &QDoubleSpinBox::valueChanged, this, markChanged); - connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); + connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::close); connect(buttons, &QDialogButtonBox::accepted, this, [this] { storeCurrentAction(); if (save()) - accept(); + emit actionsSaved(); }); } @@ -297,9 +424,11 @@ void InputActionsDialog::load() { QJsonDocument::fromJson(file.readAll(), &parseError); if (parseError.error != QJsonParseError::NoError || !document.isObject()) { - QMessageBox::warning(this, "Input Actions", - "The existing input-actions.json is not valid " - "JSON and could not be opened."); + QMessageBox::warning( + this, "Input Actions", + QStringLiteral("The existing %1 file is not valid JSON and " + "could not be opened.") + .arg(QFileInfo(actionsFile).fileName())); } else { const QJsonArray entries = document.object().value("actions").toArray(); @@ -316,6 +445,7 @@ void InputActionsDialog::load() { action.kind = object.value("singleAxis").toBool(false) ? ActionKind::Axis1D : ActionKind::Axis2D; + action.keyboardAxis = false; for (const QJsonValue &trigger : object.value("triggerAxes").toArray()) { if (trigger.isString() && @@ -332,14 +462,28 @@ void InputActionsDialog::load() { action.controllerId = axis.value("id").toInt(-1); const QJsonArray indexes = axis.value("indexes").toArray(); + const QJsonValue x = indexes.isEmpty() + ? axis.value("index") + : indexes.at(0); + const QJsonValue y = indexes.size() > 1 + ? indexes.at(1) + : axis.contains("indexY") + ? axis.value( + "indexY") + : x; action.controllerAxisX = - indexes.isEmpty() ? axis.value("index").toInt(0) - : indexes.at(0).toInt(0); + x.isString() + ? x.toString() + : controllerNameForIndex( + controllerAxisNames(), x.toInt(0)); action.controllerAxisY = - indexes.size() > 1 ? indexes.at(1).toInt(1) - : action.controllerAxisX; + y.isString() + ? y.toString() + : controllerNameForIndex( + controllerAxisNames(), y.toInt(1)); } else if (type.compare("custom", Qt::CaseInsensitive) == 0) { + action.keyboardAxis = true; const QJsonArray directions = axis.value("triggers").toArray(); action.positiveX = @@ -451,11 +595,12 @@ void InputActionsDialog::storeCurrentAction() { action.negativeX = negativeXField->currentText().trimmed(); action.positiveY = positiveYField->currentText().trimmed(); action.negativeY = negativeYField->currentText().trimmed(); + action.keyboardAxis = keyboardAxisField->isChecked(); action.mouseAxis = mouseAxisField->isChecked(); action.controllerAxis = controllerAxisField->isChecked(); action.controllerId = controllerIdField->value(); - action.controllerAxisX = controllerAxisXField->value(); - action.controllerAxisY = controllerAxisYField->value(); + action.controllerAxisX = controllerAxisXField->currentText().trimmed(); + action.controllerAxisY = controllerAxisYField->currentText().trimmed(); action.deadzone = deadzoneField->value(); action.scaleX = scaleXField->value(); action.scaleY = scaleYField->value(); @@ -470,13 +615,16 @@ void InputActionsDialog::storeCurrentAction() { binding.source = qobject_cast(buttonBindings->cellWidget(row, 0)) ->currentText(); - binding.value = buttonBindings->item(row, 1)->text().trimmed(); + binding.value = + qobject_cast(buttonBindings->cellWidget(row, 1)) + ->currentText() + .trimmed(); binding.controllerId = qobject_cast(buttonBindings->cellWidget(row, 2)) ->value(); binding.controllerButton = - qobject_cast(buttonBindings->cellWidget(row, 3)) - ->value(); + qobject_cast(buttonBindings->cellWidget(row, 3)) + ->currentText(); } } bindingPages->setCurrentIndex(action.kind == ActionKind::Button ? 0 : 1); @@ -491,6 +639,12 @@ void InputActionsDialog::storeCurrentAction() { scaleYField->setVisible(is2D); normalizeField->setVisible(is2D); invertYField->setVisible(is2D); + for (QComboBox *field : + {positiveXField, negativeXField, positiveYField, negativeYField}) + field->setEnabled(action.keyboardAxis); + controllerIdField->setEnabled(action.controllerAxis); + controllerAxisXField->setEnabled(action.controllerAxis); + controllerAxisYField->setEnabled(action.controllerAxis); refreshScriptExample(); } @@ -540,11 +694,12 @@ void InputActionsDialog::refreshEditor() { negativeXField->setCurrentText(action.negativeX); positiveYField->setCurrentText(action.positiveY); negativeYField->setCurrentText(action.negativeY); + keyboardAxisField->setChecked(action.keyboardAxis); mouseAxisField->setChecked(action.mouseAxis); controllerAxisField->setChecked(action.controllerAxis); controllerIdField->setValue(action.controllerId); - controllerAxisXField->setValue(action.controllerAxisX); - controllerAxisYField->setValue(action.controllerAxisY); + controllerAxisXField->setCurrentText(action.controllerAxisX); + controllerAxisYField->setCurrentText(action.controllerAxisY); deadzoneField->setValue(action.deadzone); scaleXField->setValue(action.scaleX); scaleYField->setValue(action.scaleY); @@ -562,6 +717,12 @@ void InputActionsDialog::refreshEditor() { scaleYField->setVisible(is2D); normalizeField->setVisible(is2D); invertYField->setVisible(is2D); + for (QComboBox *field : + {positiveXField, negativeXField, positiveYField, negativeYField}) + field->setEnabled(action.keyboardAxis); + controllerIdField->setEnabled(action.controllerAxis); + controllerAxisXField->setEnabled(action.controllerAxis); + controllerAxisYField->setEnabled(action.controllerAxis); updating = false; refreshBindingTable(); refreshScriptExample(); @@ -579,22 +740,43 @@ void InputActionsDialog::refreshBindingTable() { source->addItems({"Keyboard", "Mouse", "Controller"}); source->setCurrentText(binding.source); buttonBindings->setCellWidget(row, 0, source); - buttonBindings->setItem(row, 1, - new QTableWidgetItem(binding.value)); + auto *value = bindingCombo(buttonBindings); + value->setCurrentText(binding.value); + buttonBindings->setCellWidget(row, 1, value); auto *controllerId = new QSpinBox(buttonBindings); controllerId->setRange(-1, 15); controllerId->setSpecialValueText("Any"); controllerId->setValue(binding.controllerId); buttonBindings->setCellWidget(row, 2, controllerId); - auto *controllerButton = new QSpinBox(buttonBindings); - controllerButton->setRange(0, 255); - controllerButton->setValue(binding.controllerButton); + auto *controllerButton = + controllerCombo(controllerButtonNames(), buttonBindings); + controllerButton->setCurrentText(binding.controllerButton); buttonBindings->setCellWidget(row, 3, controllerButton); + auto updateRow = [source, value, controllerId, controllerButton] { + const bool controller = source->currentText() == "Controller"; + value->setEnabled(!controller); + controllerId->setEnabled(controller); + controllerButton->setEnabled(controller); + }; + updateRow(); connect(source, &QComboBox::currentTextChanged, this, - [this] { storeCurrentAction(); }); + [this, source, value, updateRow] { + if (source->currentText() == "Mouse" && + !value->currentText().startsWith( + "Mouse", Qt::CaseInsensitive)) + value->setCurrentText("MouseLeft"); + else if (source->currentText() == "Keyboard" && + value->currentText().startsWith( + "Mouse", Qt::CaseInsensitive)) + value->setCurrentText("Space"); + updateRow(); + storeCurrentAction(); + }); connect(controllerId, &QSpinBox::valueChanged, this, [this] { storeCurrentAction(); }); - connect(controllerButton, &QSpinBox::valueChanged, this, + connect(controllerButton, &QComboBox::currentTextChanged, this, + [this] { storeCurrentAction(); }); + connect(value, &QComboBox::currentTextChanged, this, [this] { storeCurrentAction(); }); } } @@ -633,7 +815,8 @@ InputActionsDialog::serializeButtonBinding(const ButtonBinding &binding) const { if (binding.source.compare("Controller", Qt::CaseInsensitive) == 0) { return QJsonObject{{"type", "controller"}, {"id", binding.controllerId}, - {"button", binding.controllerButton}}; + {"button", controllerValue(controllerButtonNames(), + binding.controllerButton)}}; } if (binding.source.compare("Mouse", Qt::CaseInsensitive) == 0) { return QJsonObject{{"type", "mouse"}, {"button", binding.value}}; @@ -656,7 +839,12 @@ InputActionsDialog::parseButtonBinding(const QJsonValue &value) const { if (type.compare("controller", Qt::CaseInsensitive) == 0) { binding.source = "Controller"; binding.controllerId = object.value("id").toInt(-1); - binding.controllerButton = object.value("button").toInt(0); + const QJsonValue button = object.value("button"); + binding.controllerButton = + button.isString() + ? button.toString() + : controllerNameForIndex(controllerButtonNames(), + button.toInt(0)); } else if (type.compare("mouse", Qt::CaseInsensitive) == 0) { binding.source = "Mouse"; binding.value = object.value("button").toString("MouseLeft"); @@ -722,39 +910,78 @@ bool InputActionsDialog::save() { .arg(action.name)); return false; } + if (binding.source == "Controller" && + controllerIndexForName(controllerButtonNames(), + binding.controllerButton) < 0) { + QMessageBox::warning( + this, "Input Actions", + QString("Choose a valid named controller button for " + "“%1”.") + .arg(action.name)); + return false; + } bindings.append(serializeButtonBinding(binding)); } entry.insert("triggerButtons", bindings); } else { - if (action.positiveX.isEmpty() || action.negativeX.isEmpty() || - (action.kind == ActionKind::Axis2D && - (action.positiveY.isEmpty() || action.negativeY.isEmpty()))) { + if (!action.keyboardAxis && !action.mouseAxis && + !action.controllerAxis) { QMessageBox::warning( this, "Input Actions", - QString("Complete the directional bindings for “%1”.") + QString("Choose at least one input source for “%1”.") + .arg(action.name)); + return false; + } + if (action.keyboardAxis && + (action.positiveX.isEmpty() || action.negativeX.isEmpty() || + (action.kind == ActionKind::Axis2D && + (action.positiveY.isEmpty() || + action.negativeY.isEmpty())))) { + QMessageBox::warning( + this, "Input Actions", + QString("Complete the keyboard bindings for “%1”.") + .arg(action.name)); + return false; + } + if (action.controllerAxis && + (controllerIndexForName(controllerAxisNames(), + action.controllerAxisX) < 0 || + (action.kind == ActionKind::Axis2D && + controllerIndexForName(controllerAxisNames(), + action.controllerAxisY) < 0))) { + QMessageBox::warning( + this, "Input Actions", + QString("Choose valid named controller axes for “%1”.") .arg(action.name)); return false; } QJsonArray triggers; - QJsonObject custom{{"type", "custom"}, - {"positiveX", action.positiveX}, - {"negativeX", action.negativeX}}; - if (action.kind == ActionKind::Axis2D) { - custom.insert("positiveY", action.positiveY); - custom.insert("negativeY", action.negativeY); + if (action.keyboardAxis) { + QJsonObject custom{{"type", "custom"}, + {"positiveX", action.positiveX}, + {"negativeX", action.negativeX}}; + if (action.kind == ActionKind::Axis2D) { + custom.insert("positiveY", action.positiveY); + custom.insert("negativeY", action.negativeY); + } + triggers.append(custom); } - triggers.append(custom); if (action.mouseAxis) triggers.append("mouse"); if (action.controllerAxis) { QJsonObject controller{{"type", "controller"}, {"id", action.controllerId}}; if (action.kind == ActionKind::Axis2D) - controller.insert("indexes", - QJsonArray{action.controllerAxisX, - action.controllerAxisY}); + controller.insert( + "indexes", + QJsonArray{controllerValue(controllerAxisNames(), + action.controllerAxisX), + controllerValue(controllerAxisNames(), + action.controllerAxisY)}); else - controller.insert("index", action.controllerAxisX); + controller.insert( + "index", controllerValue(controllerAxisNames(), + action.controllerAxisX)); triggers.append(controller); } entry.insert("triggerAxes", triggers); @@ -769,13 +996,22 @@ bool InputActionsDialog::save() { entries.append(entry); } + if (!QDir().mkpath(QFileInfo(actionsFile).absolutePath())) { + QMessageBox::critical( + this, "Input Actions", + QStringLiteral("Atlas could not create the folder for %1.") + .arg(QFileInfo(actionsFile).fileName())); + return false; + } QSaveFile file(actionsFile); if (!file.open(QIODevice::WriteOnly) || file.write(QJsonDocument(QJsonObject{{"actions", entries}}) .toJson(QJsonDocument::Indented)) < 0 || !file.commit()) { - QMessageBox::critical(this, "Input Actions", - "Atlas could not save input-actions.json."); + QMessageBox::critical( + this, "Input Actions", + QStringLiteral("Atlas could not save %1.") + .arg(QFileInfo(actionsFile).fileName())); return false; } QString manifestError; @@ -795,6 +1031,15 @@ bool InputActionsDialog::updateProjectManifest(QString *errorMessage) { QString contents = QString::fromUtf8(file.readAll()); file.close(); + QString manifestPath = QDir(QFileInfo(projectFile).absolutePath()) + .relativeFilePath(actionsFile); + if (manifestPath.startsWith("../")) + manifestPath = actionsFile; + manifestPath = QDir::fromNativeSeparators(manifestPath); + manifestPath.replace('\\', "\\\\").replace('"', "\\\""); + const QString inputLine = + QStringLiteral("input_actions = \"%1\"").arg(manifestPath); + const QRegularExpression sectionExpression( QStringLiteral(R"((?m)^\[game\][ \t]*$)")); const QRegularExpressionMatch sectionMatch = @@ -802,7 +1047,7 @@ bool InputActionsDialog::updateProjectManifest(QString *errorMessage) { if (!sectionMatch.hasMatch()) { if (!contents.endsWith('\n')) contents.append('\n'); - contents.append("\n[game]\ninput_actions = \"input-actions.json\"\n"); + contents.append("\n[game]\n" + inputLine + '\n'); } else { const int sectionStart = sectionMatch.capturedEnd(); const QRegularExpression nextSectionExpression( @@ -817,10 +1062,9 @@ bool InputActionsDialog::updateProjectManifest(QString *errorMessage) { const QRegularExpression valueExpression( QStringLiteral(R"((?m)^[ \t]*input_actions[ \t]*=.*$)")); if (gameSection.contains(valueExpression)) - gameSection.replace(valueExpression, - "\ninput_actions = \"input-actions.json\""); + gameSection.replace(valueExpression, inputLine); else - gameSection.prepend("\ninput_actions = \"input-actions.json\""); + gameSection.prepend('\n' + inputLine); contents.replace(sectionStart, sectionEnd - sectionStart, gameSection); } diff --git a/include/atlas/runtime/context.h b/include/atlas/runtime/context.h index 422801d7..a5f789f0 100644 --- a/include/atlas/runtime/context.h +++ b/include/atlas/runtime/context.h @@ -144,6 +144,11 @@ class Context { float scale); bool editorScrollEvent(float delta, float scale); bool editorKeyEvent(int key, bool pressed); + bool editorRuntimeKeyEvent(int key, bool pressed); + bool editorRuntimeMouseMove(float x, float y, float deltaX, float deltaY); + bool editorRuntimeMouseButtonEvent(int action, int button); + bool editorRuntimeScrollEvent(float x, float y); + bool clearEditorRuntimeInput(); bool beginEditorKeyboardTransform(int mode, float x, float y, float scale); bool setEditorKeyboardTransformAxes(int axes); bool finishEditorKeyboardTransform(bool commit); diff --git a/include/atlas/window.h b/include/atlas/window.h index 0e7ef920..fc648c75 100644 --- a/include/atlas/window.h +++ b/include/atlas/window.h @@ -453,6 +453,11 @@ class Window { float scale = 1.0f); void editorScrollEvent(float delta, float scale = 1.0f); void editorKeyEvent(int key, bool pressed); + void editorRuntimeKeyEvent(int key, bool pressed); + void editorRuntimeMouseMove(float x, float y, float deltaX, float deltaY); + void editorRuntimeMouseButtonEvent(int action, int button); + void editorRuntimeScrollEvent(float x, float y); + void clearEditorRuntimeInput(); bool beginEditorKeyboardTransform(EditorControlMode mode, float x, float y, float scale = 1.0f); void setEditorKeyboardTransformAxes(int axes); @@ -851,6 +856,11 @@ class Window { std::vector> inputActions; std::array keysPressedThisFrame{}; std::array mouseButtonsPressedThisFrame{}; + std::array editorRuntimeKeysActive{}; + std::array editorRuntimeKeysPressedPending{}; + std::array editorRuntimeMouseButtonsActive{}; + std::array editorRuntimeMouseButtonsPressedPending{}; + Position2d editorRuntimeRelativeMousePending{}; std::string textInputBuffer; bool textInputActive = false; std::shared_ptr activeCommandBuffer = nullptr; diff --git a/include/editor/views/editorWindow.h b/include/editor/views/editorWindow.h index a881f432..31b38a06 100644 --- a/include/editor/views/editorWindow.h +++ b/include/editor/views/editorWindow.h @@ -38,6 +38,7 @@ class QFileSystemWatcher; class QEvent; class SplashScreen; class QPlainTextEdit; +class InputActionsDialog; class EditorWindow : public QMainWindow { Q_OBJECT @@ -95,6 +96,7 @@ class EditorWindow : public QMainWindow { SplashScreen *assetLoadingSplash = nullptr; QFileSystemWatcher *scriptWatcher = nullptr; QPlainTextEdit *runtimeErrors = nullptr; + InputActionsDialog *inputActionsDialog = nullptr; QByteArray defaultDockState; QString projectFile; QString projectName; diff --git a/include/editor/views/inputActionsDialog.h b/include/editor/views/inputActionsDialog.h index ac652eb6..78baba50 100644 --- a/include/editor/views/inputActionsDialog.h +++ b/include/editor/views/inputActionsDialog.h @@ -5,6 +5,7 @@ #include #include #include +#include class QCheckBox; class QComboBox; @@ -19,18 +20,25 @@ class QStackedWidget; class QTableWidget; class InputActionsDialog : public QDialog { + Q_OBJECT + public: enum class ActionKind { Button, Axis1D, Axis2D }; explicit InputActionsDialog(const QString &projectFile, QWidget *parent = nullptr); + static QString actionsFileForProject(const QString &projectFile); + static QStringList actionNamesForProject(const QString &projectFile); + + signals: + void actionsSaved(); private: struct ButtonBinding { QString source = "Keyboard"; QString value = "Space"; int controllerId = -1; - int controllerButton = 0; + QString controllerButton = "A"; }; struct ActionDefinition { @@ -41,11 +49,12 @@ class InputActionsDialog : public QDialog { QString negativeX = "A"; QString positiveY = "W"; QString negativeY = "S"; + bool keyboardAxis = true; bool mouseAxis = false; bool controllerAxis = false; int controllerId = -1; - int controllerAxisX = 0; - int controllerAxisY = 1; + QString controllerAxisX = "Left Stick X"; + QString controllerAxisY = "Left Stick Y"; double deadzone = 0.2; double scaleX = 1.0; double scaleY = 1.0; @@ -92,11 +101,12 @@ class InputActionsDialog : public QDialog { QComboBox *negativeYField = nullptr; QLabel *positiveYLabel = nullptr; QLabel *negativeYLabel = nullptr; + QCheckBox *keyboardAxisField = nullptr; QCheckBox *mouseAxisField = nullptr; QCheckBox *controllerAxisField = nullptr; QSpinBox *controllerIdField = nullptr; - QSpinBox *controllerAxisXField = nullptr; - QSpinBox *controllerAxisYField = nullptr; + QComboBox *controllerAxisXField = nullptr; + QComboBox *controllerAxisYField = nullptr; QLabel *controllerAxisYLabel = nullptr; QDoubleSpinBox *deadzoneField = nullptr; QDoubleSpinBox *scaleXField = nullptr; diff --git a/include/editor/views/inspectorView.h b/include/editor/views/inspectorView.h index e66732cd..4118e413 100644 --- a/include/editor/views/inspectorView.h +++ b/include/editor/views/inspectorView.h @@ -64,6 +64,7 @@ class InspectorPanel : public QWidget { QJsonObject inspectedCamera; QString inspectedFile; QString projectRoot; + QString projectFile; int inspectedObjectId = -1; int lastRuntimeSelection = -1; bool fileTarget = false; diff --git a/include/editor/views/viewport.h b/include/editor/views/viewport.h index 453b9c0f..03538ee9 100644 --- a/include/editor/views/viewport.h +++ b/include/editor/views/viewport.h @@ -21,6 +21,7 @@ class Context; class QCloseEvent; +class QEvent; class QDragEnterEvent; class QDropEvent; class QHideEvent; @@ -107,6 +108,7 @@ class ViewportPanel : public QWidget { bool setCameraFocused(bool focused); void toggleCameraFocus(); bool isCameraFocused() const; + bool isRuntimePlaying() const { return playbackState == 1; } signals: void sceneSnapshotChanged(const QString &snapshot); @@ -127,6 +129,7 @@ class ViewportPanel : public QWidget { void cameraFocusChanged(bool focused); protected: + bool event(QEvent *event) override; QPaintEngine *paintEngine() const override; void showEvent(QShowEvent *event) override; void hideEvent(QHideEvent *event) override; @@ -148,6 +151,8 @@ class ViewportPanel : public QWidget { bool stepRuntime(); void resizeRuntime(); void sendPointerEvent(int action, float x, float y, int button); + void captureRuntimeInput(); + void releaseRuntimeInput(); void refreshSceneSnapshot(); void setSceneDirty(bool dirty); void beginKeyboardTransform(int mode); @@ -179,6 +184,7 @@ class ViewportPanel : public QWidget { bool playAfterRuntimeStart = false; bool leftPointerMoved = false; bool keyboardTransformActive = false; + bool runtimeInputCaptured = false; int keyboardTransformMode = 0; int keyboardTransformAxes = 7; int playbackState = 0; diff --git a/runtime/docs/other.md b/runtime/docs/other.md index 355499b6..0b4953a3 100644 --- a/runtime/docs/other.md +++ b/runtime/docs/other.md @@ -236,14 +236,15 @@ If both a manual directional light and `environment.atmosphere.globalLight` are ## Input Actions -Input Actions are defined in a separate file that can be referenced by the camera or other objects in the scene. They are defined as an array of input action objects, where each input action object has a `name` property that specifies the name of the input action, and an `inputs` property that defines the inputs for that action. These files are structured as following: +Input Actions are defined in the project-wide `input-actions.json` file and can be referenced by the camera, scripts, or other objects in the scene. * `name`: The name of the input action, which is a string that can be used to identify the input action. -* `triggerButtons`: An array of strings that specify the buttons that trigger the input action. These can be standard button names (e.g., "W", "A", "S", "D", "Space", etc.) or custom button names defined by the user. -* `triggerAxes`: An array of strings that specify the axes that trigger the input action. These axes are: - * `"type": "mouse"`: for mouse movement - * `"type": "controller"`: for controller stick movement but only for the controller id with field `id` and axis index for `index` - * `"type": "custom"`: for keyboard input, where the `triggers` property defines the keys that trigger the input action (e.g., "W", "A", "S", "D", etc.) +* `triggerButtons`: An array of keyboard names, mouse button objects, or controller button objects. Controller buttons accept readable names such as `A`, `Left Bumper`, or `D-Pad Up`, as well as legacy numeric indexes. +* `triggerAxes`: An array containing any combination of mouse movement, keyboard directions, and controller axes. An action may use only one source, such as mouse movement without keyboard bindings. + * `"mouse"` or `{"type": "mouse"}`: mouse movement. + * `{"type": "controller", "id": -1, "index": "Left Stick X"}`: a named 1D controller axis. + * `{"type": "controller", "id": -1, "indexes": ["Left Stick X", "Left Stick Y"]}`: a named 2D controller axis pair. + * `{"type": "custom", ...}`: keyboard or mouse-button directions. ## Terrain Generator Settings diff --git a/runtime/lib/context.cpp b/runtime/lib/context.cpp index 4a750286..dd02a7cc 100644 --- a/runtime/lib/context.cpp +++ b/runtime/lib/context.cpp @@ -3422,6 +3422,59 @@ MouseButton parseMouseButtonString(const std::string &value) { throw std::runtime_error("Unknown mouse trigger: " + value); } +int parseControllerButtonString(const std::string &value) { + const std::string token = normalizeToken(value); + if (token == "a" || token == "south" || token == "cross") + return static_cast(ControllerButton::A); + if (token == "b" || token == "east" || token == "circle") + return static_cast(ControllerButton::B); + if (token == "x" || token == "west" || token == "square") + return static_cast(ControllerButton::X); + if (token == "y" || token == "north" || token == "triangle") + return static_cast(ControllerButton::Y); + if (token == "leftbumper" || token == "leftshoulder" || token == "l1") + return static_cast(ControllerButton::LeftBumper); + if (token == "rightbumper" || token == "rightshoulder" || token == "r1") + return static_cast(ControllerButton::RightBumper); + if (token == "back" || token == "select" || token == "share" || + token == "minus") + return static_cast(ControllerButton::Back); + if (token == "start" || token == "options" || token == "plus") + return static_cast(ControllerButton::Start); + if (token == "guide" || token == "home") + return static_cast(ControllerButton::Guide); + if (token == "leftthumb" || token == "leftstick") + return static_cast(ControllerButton::LeftThumb); + if (token == "rightthumb" || token == "rightstick") + return static_cast(ControllerButton::RightThumb); + if (token == "dpadup") + return static_cast(ControllerButton::DPadUp); + if (token == "dpadright") + return static_cast(ControllerButton::DPadRight); + if (token == "dpaddown") + return static_cast(ControllerButton::DPadDown); + if (token == "dpadleft") + return static_cast(ControllerButton::DPadLeft); + throw std::runtime_error("Unknown controller button: " + value); +} + +int parseControllerAxisString(const std::string &value) { + const std::string token = normalizeToken(value); + if (token == "leftstickx" || token == "leftx") + return CONTROLLER_AXIS_LEFT_X; + if (token == "leftsticky" || token == "lefty") + return CONTROLLER_AXIS_LEFT_Y; + if (token == "rightstickx" || token == "rightx") + return CONTROLLER_AXIS_RIGHT_X; + if (token == "rightsticky" || token == "righty") + return CONTROLLER_AXIS_RIGHT_Y; + if (token == "lefttrigger" || token == "triggerleft" || token == "l2") + return CONTROLLER_AXIS_LEFT_TRIGGER; + if (token == "righttrigger" || token == "triggerright" || token == "r2") + return CONTROLLER_AXIS_RIGHT_TRIGGER; + throw std::runtime_error("Unknown controller axis: " + value); +} + Trigger parseTrigger(const json &triggerData) { if (triggerData.is_string()) { const std::string raw = triggerData.get(); @@ -3453,8 +3506,17 @@ Trigger parseTrigger(const json &triggerData) { int controllerId = -1; int buttonIndex = -1; JSON_READ_INT(triggerData, "id", controllerId); - JSON_READ_INT(triggerData, "button", buttonIndex); - JSON_READ_INT(triggerData, "buttonIndex", buttonIndex); + const auto button = triggerData.find("button"); + const auto legacyButton = triggerData.find("buttonIndex"); + const auto selected = button != triggerData.end() ? button + : legacyButton; + if (selected != triggerData.end()) { + if (selected->is_string()) + buttonIndex = + parseControllerButtonString(selected->get()); + else if (selected->is_number_integer()) + buttonIndex = selected->get(); + } if (buttonIndex < 0) { throw std::runtime_error("Controller trigger is missing button"); } @@ -3499,13 +3561,20 @@ AxisTrigger parseAxisTrigger(const json &triggerData) { int axisIndex = -1; int axisIndexY = -1; JSON_READ_INT(triggerData, "id", controllerId); - JSON_READ_INT(triggerData, "index", axisIndex); - JSON_READ_INT(triggerData, "indexY", axisIndexY); + auto readAxis = [](const json &value) { + return value.is_string() + ? parseControllerAxisString(value.get()) + : value.get(); + }; + if (triggerData.contains("index")) + axisIndex = readAxis(triggerData["index"]); + if (triggerData.contains("indexY")) + axisIndexY = readAxis(triggerData["indexY"]); if (axisIndex < 0 && triggerData.contains("indexes") && triggerData["indexes"].is_array() && triggerData["indexes"].size() == 2) { - axisIndex = triggerData["indexes"][0].get(); - axisIndexY = triggerData["indexes"][1].get(); + axisIndex = readAxis(triggerData["indexes"][0]); + axisIndexY = readAxis(triggerData["indexes"][1]); } if (axisIndex < 0) { throw std::runtime_error( @@ -4930,6 +4999,42 @@ bool Context::editorKeyEvent(int key, bool pressed) { return true; } +bool Context::editorRuntimeKeyEvent(int key, bool pressed) { + if (window == nullptr) + return false; + window->editorRuntimeKeyEvent(key, pressed); + return true; +} + +bool Context::editorRuntimeMouseMove(float x, float y, float deltaX, + float deltaY) { + if (window == nullptr) + return false; + window->editorRuntimeMouseMove(x, y, deltaX, deltaY); + return true; +} + +bool Context::editorRuntimeMouseButtonEvent(int action, int button) { + if (window == nullptr) + return false; + window->editorRuntimeMouseButtonEvent(action, button); + return true; +} + +bool Context::editorRuntimeScrollEvent(float x, float y) { + if (window == nullptr) + return false; + window->editorRuntimeScrollEvent(x, y); + return true; +} + +bool Context::clearEditorRuntimeInput() { + if (window == nullptr) + return false; + window->clearEditorRuntimeInput(); + return true; +} + bool Context::beginEditorKeyboardTransform(int mode, float x, float y, float scale) { if (window == nullptr || mode < 1 || mode > 3) From a0ff1590bbedb9d3525fa2d0706f8765560f1fa8 Mon Sep 17 00:00:00 2001 From: Max Van den Eynde Date: Sun, 2 Aug 2026 20:45:00 +0200 Subject: [PATCH 2/5] First round of fixes to the viewport --- editor/views/editor/editor.cpp | 5 +- editor/views/editor/viewport.cpp | 121 ++++++++++++++++++------------- include/editor/views/viewport.h | 1 + 3 files changed, 75 insertions(+), 52 deletions(-) diff --git a/editor/views/editor/editor.cpp b/editor/views/editor/editor.cpp index 512c1ce5..9cfabbe5 100644 --- a/editor/views/editor/editor.cpp +++ b/editor/views/editor/editor.cpp @@ -2209,9 +2209,10 @@ bool EditorWindow::eventFilter(QObject *watched, QEvent *event) { dialog->setAttribute(Qt::WA_TranslucentBackground); } } + if (viewportPanel != nullptr && + viewportPanel->routeRuntimeInputEvent(event)) + return true; if (event->type() == QEvent::KeyPress) { - if (viewportPanel != nullptr && viewportPanel->isRuntimePlaying()) - return QMainWindow::eventFilter(watched, event); auto *key = static_cast(event); if (!key->isAutoRepeat() && key->matches(QKeySequence::Undo)) { undoActiveEditor(); diff --git a/editor/views/editor/viewport.cpp b/editor/views/editor/viewport.cpp index 11c39549..2169d2bc 100644 --- a/editor/views/editor/viewport.cpp +++ b/editor/views/editor/viewport.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -476,11 +477,69 @@ ViewportPanel::ViewportPanel(const QString &projectFile, QWidget *parent) } bool ViewportPanel::event(QEvent *event) { - if (playbackState == 1 && event->type() == QEvent::ShortcutOverride) { + if (routeRuntimeInputEvent(event)) + return true; + return QWidget::event(event); +} + +bool ViewportPanel::routeRuntimeInputEvent(QEvent *event) { + if (playbackState != 1 || runtimeContext == nullptr || event == nullptr) + return false; + if (event->type() == QEvent::ShortcutOverride) { event->accept(); return true; } - return QWidget::event(event); + if (event->type() == QEvent::KeyPress || + event->type() == QEvent::KeyRelease) { + auto *keyEvent = static_cast(event); + if (event->type() == QEvent::KeyPress && + keyEvent->key() == Qt::Key_Escape && + !keyEvent->isAutoRepeat()) { + stopRuntimePlayback(); + event->accept(); + return true; + } + const int key = runtimeKey(keyEvent); + if (key >= 0 && !keyEvent->isAutoRepeat()) + runtimeContext->editorRuntimeKeyEvent( + key, event->type() == QEvent::KeyPress); + event->accept(); + return true; + } + if (event->type() == QEvent::MouseMove) { + auto *mouseEvent = static_cast(event); + const QPoint center = mapToGlobal(QPoint(width() / 2, height() / 2)); + const QPointF delta = mouseEvent->globalPosition() - QPointF(center); + if (!qFuzzyIsNull(delta.x()) || !qFuzzyIsNull(delta.y())) { + runtimeContext->editorRuntimeMouseMove( + static_cast(width()) * 0.5f, + static_cast(height()) * 0.5f, + static_cast(delta.x()), + static_cast(-delta.y())); + QCursor::setPos(center); + } + event->accept(); + return true; + } + if (event->type() == QEvent::MouseButtonPress || + event->type() == QEvent::MouseButtonDblClick || + event->type() == QEvent::MouseButtonRelease) { + auto *mouseEvent = static_cast(event); + runtimeContext->editorRuntimeMouseButtonEvent( + event->type() == QEvent::MouseButtonRelease ? 2 : 0, + runtimeMouseButton(mouseEvent->button())); + event->accept(); + return true; + } + if (event->type() == QEvent::Wheel) { + auto *wheelEvent = static_cast(event); + runtimeContext->editorRuntimeScrollEvent( + static_cast(wheelEvent->angleDelta().x()) / 120.0f, + static_cast(wheelEvent->angleDelta().y()) / 120.0f); + event->accept(); + return true; + } + return false; } ViewportPanel::~ViewportPanel() { shutdownRuntime(); } @@ -601,12 +660,8 @@ void ViewportPanel::shutdownRuntime() { void ViewportPanel::mousePressEvent(QMouseEvent *event) { setFocus(Qt::MouseFocusReason); - if (playbackState == 1 && runtimeContext != nullptr) { - runtimeContext->editorRuntimeMouseButtonEvent( - 0, runtimeMouseButton(event->button())); - event->accept(); + if (routeRuntimeInputEvent(event)) return; - } if (keyboardTransformActive && (event->button() == Qt::LeftButton || event->button() == Qt::RightButton)) { finishKeyboardTransform(event->button() == Qt::LeftButton); @@ -644,20 +699,8 @@ void ViewportPanel::mousePressEvent(QMouseEvent *event) { } void ViewportPanel::mouseMoveEvent(QMouseEvent *event) { - if (playbackState == 1 && runtimeContext != nullptr) { - const QPoint center(width() / 2, height() / 2); - const QPointF delta = event->position() - QPointF(center); - if (!qFuzzyIsNull(delta.x()) || !qFuzzyIsNull(delta.y())) { - runtimeContext->editorRuntimeMouseMove( - static_cast(event->position().x()), - static_cast(height() - event->position().y()), - static_cast(delta.x()), - static_cast(-delta.y())); - QCursor::setPos(mapToGlobal(center)); - } - event->accept(); + if (routeRuntimeInputEvent(event)) return; - } if (event->buttons().testFlag(Qt::LeftButton)) leftPointerMoved = true; sendPointerEvent(1, static_cast(event->position().x()), @@ -690,12 +733,8 @@ void ViewportPanel::mouseMoveEvent(QMouseEvent *event) { } void ViewportPanel::mouseReleaseEvent(QMouseEvent *event) { - if (playbackState == 1 && runtimeContext != nullptr) { - runtimeContext->editorRuntimeMouseButtonEvent( - 2, runtimeMouseButton(event->button())); - event->accept(); + if (routeRuntimeInputEvent(event)) return; - } const int pointerButton = event->button() == Qt::RightButton ? rightDragRuntimeButton : event->button() == Qt::MiddleButton @@ -722,17 +761,14 @@ void ViewportPanel::mouseReleaseEvent(QMouseEvent *event) { } void ViewportPanel::wheelEvent(QWheelEvent *event) { + if (routeRuntimeInputEvent(event)) + return; if (runtimeContext == nullptr) { QWidget::wheelEvent(event); return; } const float deltaX = static_cast(event->angleDelta().x()) / 120.0f; const float delta = static_cast(event->angleDelta().y()) / 120.0f; - if (playbackState == 1) { - runtimeContext->editorRuntimeScrollEvent(deltaX, delta); - event->accept(); - return; - } if (std::abs(delta) > 0.0f) { runtimeContext->editorScrollEvent(delta, widgetScale(this)); } @@ -740,18 +776,8 @@ void ViewportPanel::wheelEvent(QWheelEvent *event) { } void ViewportPanel::keyPressEvent(QKeyEvent *event) { - if (playbackState == 1 && runtimeContext != nullptr) { - if (event->key() == Qt::Key_Escape && !event->isAutoRepeat()) { - stopRuntimePlayback(); - event->accept(); - return; - } - const int key = runtimeKey(event); - if (key >= 0 && !event->isAutoRepeat()) - runtimeContext->editorRuntimeKeyEvent(key, true); - event->accept(); + if (routeRuntimeInputEvent(event)) return; - } if (!event->isAutoRepeat() && runtimeContext != nullptr && playbackState == 0) { if (event->key() == Qt::Key_0 && @@ -817,13 +843,8 @@ void ViewportPanel::keyPressEvent(QKeyEvent *event) { } void ViewportPanel::keyReleaseEvent(QKeyEvent *event) { - if (playbackState == 1 && runtimeContext != nullptr) { - const int key = runtimeKey(event); - if (key >= 0 && !event->isAutoRepeat()) - runtimeContext->editorRuntimeKeyEvent(key, false); - event->accept(); + if (routeRuntimeInputEvent(event)) return; - } const int key = editorCameraKey(event->key()); if (event->isAutoRepeat()) { if (key >= 0) { @@ -1659,12 +1680,12 @@ void ViewportPanel::stopRuntimePlayback() { } void ViewportPanel::captureRuntimeInput() { - if (runtimeInputCaptured || playbackState != 1 || !isVisible()) + if (runtimeInputCaptured || playbackState != 1) return; setFocus(Qt::OtherFocusReason); grabKeyboard(); grabMouse(); - setCursor(Qt::BlankCursor); + QApplication::setOverrideCursor(Qt::BlankCursor); runtimeInputCaptured = true; QCursor::setPos(mapToGlobal(QPoint(width() / 2, height() / 2))); } @@ -1676,7 +1697,7 @@ void ViewportPanel::releaseRuntimeInput() { return; releaseKeyboard(); releaseMouse(); - unsetCursor(); + QApplication::restoreOverrideCursor(); runtimeInputCaptured = false; } diff --git a/include/editor/views/viewport.h b/include/editor/views/viewport.h index 03538ee9..9ad8451d 100644 --- a/include/editor/views/viewport.h +++ b/include/editor/views/viewport.h @@ -109,6 +109,7 @@ class ViewportPanel : public QWidget { void toggleCameraFocus(); bool isCameraFocused() const; bool isRuntimePlaying() const { return playbackState == 1; } + bool routeRuntimeInputEvent(QEvent *event); signals: void sceneSnapshotChanged(const QString &snapshot); From 61c4dd3eb5c843fa32f68f5cd3c155f8ffbbe881 Mon Sep 17 00:00:00 2001 From: Max Van den Eynde Date: Sun, 2 Aug 2026 22:50:43 +0200 Subject: [PATCH 3/5] Fixed locking in the viewport --- atlas/application/window.cpp | 1 + editor/CMakeLists.txt | 1 + editor/views/editor/viewport.cpp | 307 +++++++++++++++++++++++++++++++ include/editor/views/viewport.h | 4 + 4 files changed, 313 insertions(+) diff --git a/atlas/application/window.cpp b/atlas/application/window.cpp index 1f5cefb8..ee711c5f 100644 --- a/atlas/application/window.cpp +++ b/atlas/application/window.cpp @@ -1304,6 +1304,7 @@ void Window::initializeRunLoop() { void Window::pollEvents() { if (this->renderToExternalMetalView) { + SDL_PumpEvents(); return; } SDL_Event event; diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 57b3f9f6..43423d52 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -202,6 +202,7 @@ if (APPLE) "-framework QuartzCore" "-framework Foundation" "-framework Cocoa" + "-framework CoreGraphics" "-framework IOKit" ) endif () diff --git a/editor/views/editor/viewport.cpp b/editor/views/editor/viewport.cpp index 2169d2bc..a302a471 100644 --- a/editor/views/editor/viewport.cpp +++ b/editor/views/editor/viewport.cpp @@ -48,6 +48,12 @@ #include #include +#ifdef Q_OS_MACOS +#include +#include +#include +#endif + #include #include #include @@ -307,6 +313,247 @@ int runtimeKey(const QKeyEvent *event) { return -1; } +#ifdef Q_OS_MACOS +int runtimeKeyFromMacVirtualKey(CGKeyCode key) { + switch (key) { + case 0: + return static_cast(Key::A); + case 1: + return static_cast(Key::S); + case 2: + return static_cast(Key::D); + case 3: + return static_cast(Key::F); + case 4: + return static_cast(Key::H); + case 5: + return static_cast(Key::G); + case 6: + return static_cast(Key::Z); + case 7: + return static_cast(Key::X); + case 8: + return static_cast(Key::C); + case 9: + return static_cast(Key::V); + case 11: + return static_cast(Key::B); + case 12: + return static_cast(Key::Q); + case 13: + return static_cast(Key::W); + case 14: + return static_cast(Key::E); + case 15: + return static_cast(Key::R); + case 16: + return static_cast(Key::Y); + case 17: + return static_cast(Key::T); + case 18: + return static_cast(Key::Key1); + case 19: + return static_cast(Key::Key2); + case 20: + return static_cast(Key::Key3); + case 21: + return static_cast(Key::Key4); + case 22: + return static_cast(Key::Key6); + case 23: + return static_cast(Key::Key5); + case 24: + return static_cast(Key::Equal); + case 25: + return static_cast(Key::Key9); + case 26: + return static_cast(Key::Key7); + case 27: + return static_cast(Key::Minus); + case 28: + return static_cast(Key::Key8); + case 29: + return static_cast(Key::Key0); + case 30: + return static_cast(Key::RightBracket); + case 31: + return static_cast(Key::O); + case 32: + return static_cast(Key::U); + case 33: + return static_cast(Key::LeftBracket); + case 34: + return static_cast(Key::I); + case 35: + return static_cast(Key::P); + case 36: + return static_cast(Key::Enter); + case 37: + return static_cast(Key::L); + case 38: + return static_cast(Key::J); + case 39: + return static_cast(Key::Apostrophe); + case 40: + return static_cast(Key::K); + case 41: + return static_cast(Key::Semicolon); + case 42: + return static_cast(Key::Backslash); + case 43: + return static_cast(Key::Comma); + case 44: + return static_cast(Key::Slash); + case 45: + return static_cast(Key::N); + case 46: + return static_cast(Key::M); + case 47: + return static_cast(Key::Period); + case 48: + return static_cast(Key::Tab); + case 49: + return static_cast(Key::Space); + case 50: + return static_cast(Key::GraveAccent); + case 51: + return static_cast(Key::Backspace); + case 53: + return static_cast(Key::Escape); + case 54: + return static_cast(Key::RightSuper); + case 55: + return static_cast(Key::LeftSuper); + case 56: + return static_cast(Key::LeftShift); + case 57: + return static_cast(Key::CapsLock); + case 58: + return static_cast(Key::LeftAlt); + case 59: + return static_cast(Key::LeftControl); + case 60: + return static_cast(Key::RightShift); + case 61: + return static_cast(Key::RightAlt); + case 62: + return static_cast(Key::RightControl); + case 64: + return static_cast(Key::F17); + case 65: + return static_cast(Key::KPDecimal); + case 67: + return static_cast(Key::KPMultiply); + case 69: + return static_cast(Key::KPAdd); + case 71: + return static_cast(Key::NumLock); + case 75: + return static_cast(Key::KPDivide); + case 76: + return static_cast(Key::KPEnter); + case 78: + return static_cast(Key::KPSubtract); + case 79: + return static_cast(Key::F18); + case 80: + return static_cast(Key::F19); + case 81: + return static_cast(Key::KPEqual); + case 82: + return static_cast(Key::KP0); + case 83: + return static_cast(Key::KP1); + case 84: + return static_cast(Key::KP2); + case 85: + return static_cast(Key::KP3); + case 86: + return static_cast(Key::KP4); + case 87: + return static_cast(Key::KP5); + case 88: + return static_cast(Key::KP6); + case 89: + return static_cast(Key::KP7); + case 90: + return static_cast(Key::F20); + case 91: + return static_cast(Key::KP8); + case 92: + return static_cast(Key::KP9); + case 96: + return static_cast(Key::F5); + case 97: + return static_cast(Key::F6); + case 98: + return static_cast(Key::F7); + case 99: + return static_cast(Key::F3); + case 100: + return static_cast(Key::F8); + case 101: + return static_cast(Key::F9); + case 103: + return static_cast(Key::F11); + case 105: + return static_cast(Key::F13); + case 106: + return static_cast(Key::F16); + case 107: + return static_cast(Key::F14); + case 109: + return static_cast(Key::F10); + case 111: + return static_cast(Key::F12); + case 113: + return static_cast(Key::F15); + case 114: + return static_cast(Key::Insert); + case 115: + return static_cast(Key::Home); + case 116: + return static_cast(Key::PageUp); + case 117: + return static_cast(Key::Delete); + case 118: + return static_cast(Key::F4); + case 119: + return static_cast(Key::End); + case 120: + return static_cast(Key::F2); + case 121: + return static_cast(Key::PageDown); + case 122: + return static_cast(Key::F1); + case 123: + return static_cast(Key::Left); + case 124: + return static_cast(Key::Right); + case 125: + return static_cast(Key::Down); + case 126: + return static_cast(Key::Up); + default: + return -1; + } +} + +std::array runtimeMouseEventTypes() { + return {kCGEventMouseMoved, kCGEventLeftMouseDragged, + kCGEventRightMouseDragged, kCGEventOtherMouseDragged}; +} + +std::array runtimeMouseCounters() { + std::array counters{}; + const auto types = runtimeMouseEventTypes(); + for (std::size_t index = 0; index < types.size(); ++index) + counters[index] = CGEventSourceCounterForEventType( + kCGEventSourceStateCombinedSessionState, types[index]); + return counters; +} +#endif + float widgetScale(QWidget *widget) { const qreal scale = widget != nullptr ? widget->devicePixelRatioF() : 1.0; return scale > 0.0 ? static_cast(scale) : 1.0f; @@ -507,6 +754,10 @@ bool ViewportPanel::routeRuntimeInputEvent(QEvent *event) { return true; } if (event->type() == QEvent::MouseMove) { +#ifdef Q_OS_MACOS + event->accept(); + return true; +#else auto *mouseEvent = static_cast(event); const QPoint center = mapToGlobal(QPoint(width() / 2, height() / 2)); const QPointF delta = mouseEvent->globalPosition() - QPointF(center); @@ -520,6 +771,7 @@ bool ViewportPanel::routeRuntimeInputEvent(QEvent *event) { } event->accept(); return true; +#endif } if (event->type() == QEvent::MouseButtonPress || event->type() == QEvent::MouseButtonDblClick || @@ -1018,6 +1270,8 @@ bool ViewportPanel::stepRuntime() { if (runtimeContext == nullptr) { return false; } + if (!pollCapturedRuntimeInput()) + return false; try { if (!runtimeContext->stepFrame()) { emit runtimeErrorOccurred( @@ -1052,6 +1306,50 @@ bool ViewportPanel::stepRuntime() { } } +bool ViewportPanel::pollCapturedRuntimeInput() { + if (!runtimeInputCaptured || playbackState != 1 || + runtimeContext == nullptr) + return true; +#ifdef Q_OS_MACOS + constexpr CGEventSourceStateID state = + kCGEventSourceStateCombinedSessionState; + if (CGEventSourceKeyState(state, 53)) { + stopRuntimePlayback(); + return false; + } + for (CGKeyCode keyCode = 0; keyCode < 128; ++keyCode) { + const int key = runtimeKeyFromMacVirtualKey(keyCode); + if (key < 0 || key == static_cast(Key::Escape)) + continue; + runtimeContext->editorRuntimeKeyEvent( + key, CGEventSourceKeyState(state, keyCode)); + } + runtimeContext->editorRuntimeMouseButtonEvent( + CGEventSourceButtonState(state, kCGMouseButtonLeft) ? 0 : 2, + static_cast(MouseButton::Left)); + runtimeContext->editorRuntimeMouseButtonEvent( + CGEventSourceButtonState(state, kCGMouseButtonRight) ? 0 : 2, + static_cast(MouseButton::Right)); + runtimeContext->editorRuntimeMouseButtonEvent( + CGEventSourceButtonState(state, kCGMouseButtonCenter) ? 0 : 2, + static_cast(MouseButton::Middle)); + const auto counters = runtimeMouseCounters(); + if (counters != runtimeMouseEventCounters) { + runtimeMouseEventCounters = counters; + std::int32_t deltaX = 0; + std::int32_t deltaY = 0; + CGGetLastMouseDelta(&deltaX, &deltaY); + if (deltaX != 0 || deltaY != 0) { + runtimeContext->editorRuntimeMouseMove( + static_cast(width()) * 0.5f, + static_cast(height()) * 0.5f, + static_cast(deltaX), static_cast(-deltaY)); + } + } +#endif + return true; +} + void ViewportPanel::resizeRuntime() { if (runtimeContext == nullptr) { return; @@ -1686,6 +1984,11 @@ void ViewportPanel::captureRuntimeInput() { grabKeyboard(); grabMouse(); QApplication::setOverrideCursor(Qt::BlankCursor); +#ifdef Q_OS_MACOS + runtimeMouseEventCounters = runtimeMouseCounters(); + CGAssociateMouseAndMouseCursorPosition(false); + CGDisplayHideCursor(CGMainDisplayID()); +#endif runtimeInputCaptured = true; QCursor::setPos(mapToGlobal(QPoint(width() / 2, height() / 2))); } @@ -1697,6 +2000,10 @@ void ViewportPanel::releaseRuntimeInput() { return; releaseKeyboard(); releaseMouse(); +#ifdef Q_OS_MACOS + CGAssociateMouseAndMouseCursorPosition(true); + CGDisplayShowCursor(CGMainDisplayID()); +#endif QApplication::restoreOverrideCursor(); runtimeInputCaptured = false; } diff --git a/include/editor/views/viewport.h b/include/editor/views/viewport.h index 9ad8451d..a834d32b 100644 --- a/include/editor/views/viewport.h +++ b/include/editor/views/viewport.h @@ -10,6 +10,8 @@ #ifndef ATLAS_VIEWPORT_H #define ATLAS_VIEWPORT_H +#include +#include #include #include @@ -150,6 +152,7 @@ class ViewportPanel : public QWidget { void startRuntime(); void stopRuntime(); bool stepRuntime(); + bool pollCapturedRuntimeInput(); void resizeRuntime(); void sendPointerEvent(int action, float x, float y, int button); void captureRuntimeInput(); @@ -186,6 +189,7 @@ class ViewportPanel : public QWidget { bool leftPointerMoved = false; bool keyboardTransformActive = false; bool runtimeInputCaptured = false; + std::array runtimeMouseEventCounters{}; int keyboardTransformMode = 0; int keyboardTransformAxes = 7; int playbackState = 0; From e252f0cfdf2d494dc1189d1700ae79515ed36661 Mon Sep 17 00:00:00 2001 From: Max Van den Eynde Date: Sun, 2 Aug 2026 22:57:58 +0200 Subject: [PATCH 4/5] Finished adding camera movement to options --- editor/views/editor/inspector.cpp | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/editor/views/editor/inspector.cpp b/editor/views/editor/inspector.cpp index ce969e48..5d4988db 100644 --- a/editor/views/editor/inspector.cpp +++ b/editor/views/editor/inspector.cpp @@ -1324,7 +1324,7 @@ QFrame *componentCard(const QString &title, const QJsonObject &properties, return card; } -QFrame *controllerActionsCard(const QJsonArray &actions, +QFrame *controllerActionsCard(const QJsonArray &actions, bool automaticMoving, const QString &projectFile, const PropertyChanged &changed, QWidget *parent) { @@ -1347,6 +1347,15 @@ QFrame *controllerActionsCard(const QJsonArray &actions, auto *bodyLayout = new QVBoxLayout(body); bodyLayout->setContentsMargins(0, 2, 0, 0); bodyLayout->setSpacing(1); + auto *automatic = new QCheckBox("Enable Automatic Movement", body); + automatic->setChecked(automaticMoving); + automatic->setCursor(Qt::PointingHandCursor); + tagEditor(automatic, "/automaticMoving", "bool"); + bodyLayout->addWidget(automatic); + QObject::connect(automatic, &QCheckBox::toggled, body, + [changed](bool checked) { + changed("/automaticMoving", checked); + }); const QStringList labels{"Movement", "Look", "Vertical"}; QList pickers; for (int index = 0; index < labels.size(); ++index) { @@ -1560,7 +1569,6 @@ void InspectorPanel::applySceneSnapshot(const QString &snapshot) { {"controllerLookSensitivity", inspectedCamera.value("controllerLookSensitivity")}, {"lookSmoothness", inspectedCamera.value("lookSmoothness")}, - {"automaticMoving", inspectedCamera.value("automaticMoving")}, {"actions", inspectedCamera.value("actions").isArray() ? inspectedCamera.value("actions") : QJsonValue(QJsonArray{})}}; @@ -1578,9 +1586,13 @@ void InspectorPanel::applySceneSnapshot(const QString &snapshot) { else if (scope == "camera:controls") refreshTaggedEditors(card, controls); else if (scope == "camera:actions") - refreshTaggedEditors( - card, QJsonObject{{"actions", - inspectedCamera.value("actions")}}); + refreshTaggedEditors(card, + QJsonObject{ + {"automaticMoving", + inspectedCamera.value( + "automaticMoving")}, + {"actions", inspectedCamera.value( + "actions")}}); } return; } @@ -2106,8 +2118,7 @@ void InspectorPanel::showCamera() { {"mouseSensitivity", inspectedCamera.value("mouseSensitivity")}, {"controllerLookSensitivity", inspectedCamera.value("controllerLookSensitivity")}, - {"lookSmoothness", inspectedCamera.value("lookSmoothness")}, - {"automaticMoving", inspectedCamera.value("automaticMoving")}}; + {"lookSmoothness", inspectedCamera.value("lookSmoothness")}}; SyncOptions syncOptions; collectSyncOptions("Camera", inspectedCamera, QJsonObject{{"section", "camera"}}, QString(), @@ -2134,8 +2145,9 @@ void InspectorPanel::showCamera() { QJsonObject{{"section", "camera"}}), {}, "camera:controls")); contentLayout->addWidget(controllerActionsCard( - inspectedCamera.value("actions").toArray(), projectFile, update, - content)); + inspectedCamera.value("actions").toArray(), + inspectedCamera.value("automaticMoving").toBool(false), projectFile, + update, content)); contentLayout->addStretch(); } From 253386963b522890740389de71042e4ccd646288 Mon Sep 17 00:00:00 2001 From: Max Van den Eynde Date: Sun, 2 Aug 2026 23:02:08 +0200 Subject: [PATCH 5/5] Locked Y when there's no action in the camera --- atlas/camera.cpp | 76 ++++++++++++++++++++--------------------- runtime/lib/context.cpp | 29 +++++----------- 2 files changed, 45 insertions(+), 60 deletions(-) diff --git a/atlas/camera.cpp b/atlas/camera.cpp index 484541b7..bddf4e8e 100644 --- a/atlas/camera.cpp +++ b/atlas/camera.cpp @@ -193,48 +193,51 @@ void Camera::update(Window &window) { void Camera::updateWithActions(Window &window, const std::string &moveAxis, const std::string &lookAction, const std::string &upAndDownAction) { - AxisPacket moveInput = window.getAxisActionValue(moveAxis); - AxisPacket lookInput = window.getAxisActionValue(lookAction); - AxisPacket upDownInput = window.getAxisActionValue(upAndDownAction); + AxisPacket moveInput = moveAxis.empty() + ? AxisPacket{} + : window.getAxisActionValue(moveAxis); + AxisPacket lookInput = lookAction.empty() + ? AxisPacket{} + : window.getAxisActionValue(lookAction); + AxisPacket upDownInput = + upAndDownAction.empty() + ? AxisPacket{} + : window.getAxisActionValue(upAndDownAction); float deltaTime = window.getDeltaTime(); - float xoffset = lookInput.inputDeltaX * mouseSensitivity; - float yoffset = lookInput.inputDeltaY * mouseSensitivity; - - if (lookInput.hasValueInput) { - glm::vec2 lookVector(lookInput.valueX, lookInput.valueY); - if (glm::length(lookVector) > 1.0f) { - lookVector = glm::normalize(lookVector); + if (!lookAction.empty()) { + float xoffset = lookInput.inputDeltaX * mouseSensitivity; + float yoffset = lookInput.inputDeltaY * mouseSensitivity; + + if (lookInput.hasValueInput) { + glm::vec2 lookVector(lookInput.valueX, lookInput.valueY); + if (glm::length(lookVector) > 1.0f) { + lookVector = glm::normalize(lookVector); + } + xoffset += + lookVector.x * controllerLookSensitivity * deltaTime; + yoffset += + lookVector.y * controllerLookSensitivity * deltaTime; } - xoffset += lookVector.x * controllerLookSensitivity * deltaTime; - yoffset += lookVector.y * controllerLookSensitivity * deltaTime; - } - - glm::vec2 fallbackLook = sampleControllerAxisPair( - window, CONTROLLER_AXIS_RIGHT_X, CONTROLLER_AXIS_RIGHT_Y, true); - if (glm::length(fallbackLook) > 0.0f && - glm::length(fallbackLook) > - glm::length(glm::vec2(lookInput.valueX, lookInput.valueY))) { - xoffset += fallbackLook.x * controllerLookSensitivity * deltaTime; - yoffset += fallbackLook.y * controllerLookSensitivity * deltaTime; - } - targetYaw += xoffset; - targetPitch += yoffset; + targetYaw += xoffset; + targetPitch += yoffset; - targetPitch = std::min(targetPitch, 89.0f); - targetPitch = std::max(targetPitch, -89.0f); + targetPitch = std::min(targetPitch, 89.0f); + targetPitch = std::max(targetPitch, -89.0f); - yaw += (targetYaw - yaw) * lookSmoothness; - pitch += (targetPitch - pitch) * lookSmoothness; + yaw += (targetYaw - yaw) * lookSmoothness; + pitch += (targetPitch - pitch) * lookSmoothness; - glm::vec3 front; - front.x = cos(glm::radians(yaw)) * cos(glm::radians(pitch)); - front.y = sin(glm::radians(pitch)); - front.z = sin(glm::radians(yaw)) * cos(glm::radians(pitch)); - front = glm::normalize(front); + glm::vec3 front; + front.x = cos(glm::radians(yaw)) * cos(glm::radians(pitch)); + front.y = sin(glm::radians(pitch)); + front.z = sin(glm::radians(yaw)) * cos(glm::radians(pitch)); + front = glm::normalize(front); - target = {position.x + front.x, position.y + front.y, position.z + front.z}; + target = {position.x + front.x, position.y + front.y, + position.z + front.z}; + } glm::vec3 camPos = glm::vec3(position.x, position.y, position.z); glm::vec3 camFront = @@ -244,11 +247,6 @@ void Camera::updateWithActions(Window &window, const std::string &moveAxis, glm::vec2 moveVector = moveInput.hasValueInput ? glm::vec2(moveInput.valueX, moveInput.valueY) : glm::vec2(moveInput.x, moveInput.y); - glm::vec2 fallbackMove = sampleControllerAxisPair( - window, CONTROLLER_AXIS_LEFT_X, CONTROLLER_AXIS_LEFT_Y, true); - if (glm::length(fallbackMove) > glm::length(moveVector)) { - moveVector = fallbackMove; - } if (glm::length(moveVector) > 1.0f) { moveVector = glm::normalize(moveVector); } diff --git a/runtime/lib/context.cpp b/runtime/lib/context.cpp index dd02a7cc..9f334c13 100644 --- a/runtime/lib/context.cpp +++ b/runtime/lib/context.cpp @@ -6867,13 +6867,14 @@ void RuntimeScene::update(Window &window) { return; } - if (runtimeContext->cameraActions.size() >= 3) { - runtimeContext->camera->updateWithActions( - window, runtimeContext->cameraActions[0], - runtimeContext->cameraActions[1], runtimeContext->cameraActions[2]); - } else { - runtimeContext->camera->update(window); - } + static const std::string emptyAction; + const auto actionAt = [&](std::size_t index) -> const std::string & { + return index < runtimeContext->cameraActions.size() + ? runtimeContext->cameraActions[index] + : emptyAction; + }; + runtimeContext->camera->updateWithActions(window, actionAt(0), actionAt(1), + actionAt(2)); if (runtimeContext->context != nullptr) { runtime::scripting::dispatchInteractiveFrame( @@ -6897,13 +6898,6 @@ void RuntimeScene::onMouseMove(Window &window, Movement2d movement) { runtimeContext->context, runtimeContext->scriptHost, window, packet, window.getDeltaTime()); } - - if (runtimeContext == nullptr || runtimeContext->camera == nullptr || - !runtimeContext->cameraAutomaticMoving || - runtimeContext->cameraActions.size() >= 3) { - return; - } - runtimeContext->camera->updateLook(window, movement); } void RuntimeScene::onMouseScroll(Window &window, Movement2d offset) { @@ -6914,13 +6908,6 @@ void RuntimeScene::onMouseScroll(Window &window, Movement2d offset) { runtimeContext->context, runtimeContext->scriptHost, packet, window.getDeltaTime()); } - - if (runtimeContext == nullptr || runtimeContext->camera == nullptr || - !runtimeContext->cameraAutomaticMoving || - runtimeContext->cameraActions.size() >= 3) { - return; - } - runtimeContext->camera->updateZoom(window, offset); } void Context::loadMainScene(Window &window) {