Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 99 additions & 2 deletions src/mainwindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
#include <QSignalBlocker>
#include <QGridLayout>
#include <QToolBar>
#include <functional>

namespace {

Expand Down Expand Up @@ -830,6 +831,70 @@ void MainWindow::initToolBar()
}
)";

auto addRailButton = [this, topoBtnStyle](const QString& text,
const QString& tooltip,
const QString& actionObjectName,
const std::function<void()>& triggered) {
auto* button = new QToolButton(ui->objectsToolbar);
button->setText(text);
button->setToolTip(tooltip);
QFont font = button->font();
font.setPixelSize(text.size() > 1 ? 10 : 15);
font.setBold(true);
button->setFont(font);
button->setStyleSheet(topoBtnStyle);
connect(button, &QToolButton::clicked, this, triggered);

QAction* action = ui->objectsToolbar->addWidget(button);
action->setObjectName(actionObjectName);
return action;
};

addRailButton(
QStringLiteral("D"),
tr("Show Dope Sheet"),
QStringLiteral("modeAnimationDopeSheetAction"),
[this]() {
SentryReporter::addBreadcrumb("ui.action", "Rail: Dope Sheet");
showBottomToolDock(m_dopeSheetDock);
});

addRailButton(
QStringLiteral("C"),
tr("Show Curve Editor"),
QStringLiteral("modeAnimationCurveEditorAction"),
[this]() {
SentryReporter::addBreadcrumb("ui.action", "Rail: Curve Editor");
showBottomToolDock(m_curveEditorDock);
});

addRailButton(
QStringLiteral("\u21C4"),
tr("Merge animations"),
QStringLiteral("modeAnimationMergeAction"),
[this]() {
SentryReporter::addBreadcrumb("ui.action", "Rail: Merge Animations");
ui->actionMerge_Animations->trigger();
});

addRailButton(
QStringLiteral("M"),
tr("Open Material Editor"),
QStringLiteral("modeMaterialEditorAction"),
[this]() {
SentryReporter::addBreadcrumb("ui.action", "Rail: Material Editor");
ui->actionMaterial_Editor->trigger();
});

addRailButton(
QStringLiteral("\u2713"),
tr("Validate selected mesh"),
QStringLiteral("modeValidationRunAction"),
[]() {
SentryReporter::addBreadcrumb("ui.action", "Rail: Validate Mesh");
MeshValidator::instance()->validate();
});

// Extrude: face mode only.
auto extrudeButton = new QToolButton(ui->objectsToolbar);
extrudeButton->setText("\u2B06"); // ⬆ (extrude = push outward)
Expand Down Expand Up @@ -1335,6 +1400,10 @@ void MainWindow::initToolBar()
// Merge Animations button — enable/disable based on selection
connect(SelectionSet::getSingleton(), &SelectionSet::selectionChanged,
this, &MainWindow::updateMergeAnimationsButton);
connect(SelectionSet::getSingleton(), &SelectionSet::selectionChanged,
this, &MainWindow::updateToolRailForMode);
connect(MeshValidator::instance(), &MeshValidator::selectionChanged,
this, &MainWindow::updateToolRailForMode);

// Viewport
connect(ui->actionAdd_Viewport, SIGNAL(triggered()), this, SLOT(createEditorViewport()));
Expand Down Expand Up @@ -1737,16 +1806,38 @@ void MainWindow::updateToolRailForMode()
const bool editMode = mode == EditorModeController::EditMode;
const bool animationMode = mode == EditorModeController::AnimationMode;
const bool materialMode = mode == EditorModeController::MaterialMode;
const bool validationMode = mode == EditorModeController::ValidationMode;

// Keeps the QAction reachable in menus (so users can always find Material
// Editor / Merge Animations under their menu) while showing or hiding only
// the toolbar-button representation per the active mode. The QAction's
// own visibility is intentionally left unchanged.
auto setSharedToolbarButtonVisible = [this](QAction* action, bool showButton) {
action->setVisible(true);
if (QWidget* toolbarButton = ui->toolToolbar->widgetForAction(action))
toolbarButton->setVisible(showButton);
};

for (QAction* action : ui->objectsToolbar->actions()) {
const QString name = action->objectName();
if (name.startsWith(QStringLiteral("modeObject"))) {
action->setVisible(objectMode);
} else if (name.startsWith(QStringLiteral("modeEdit"))) {
action->setVisible(editMode && EditModeController::instance()->isEditModeActive());
} else if (name.startsWith(QStringLiteral("modeAnimation"))) {
action->setVisible(animationMode);
} else if (name.startsWith(QStringLiteral("modeMaterial"))) {
action->setVisible(materialMode);
} else if (name.startsWith(QStringLiteral("modeValidation"))) {
action->setVisible(validationMode);
} else if (name.startsWith(QStringLiteral("modeAny"))) {
action->setVisible(true);
}

if (name == QStringLiteral("modeAnimationMergeAction"))
action->setEnabled(ui->actionMerge_Animations->isEnabled());
else if (name == QStringLiteral("modeValidationRunAction"))
action->setEnabled(MeshValidator::instance()->hasSelection());
}

ui->actionSelect_Object->setVisible(objectMode || editMode);
Expand All @@ -1755,8 +1846,8 @@ void MainWindow::updateToolRailForMode()
ui->actionScale_Object->setVisible(objectMode || editMode);
ui->actionToggle_Transform_Space->setVisible(objectMode || editMode);
ui->actionRemove_Object->setVisible(objectMode);
ui->actionMaterial_Editor->setVisible(objectMode || materialMode);
ui->actionMerge_Animations->setVisible(objectMode || animationMode);
setSharedToolbarButtonVisible(ui->actionMaterial_Editor, objectMode || materialMode);
setSharedToolbarButtonVisible(ui->actionMerge_Animations, objectMode || animationMode);
}

// LCOV_EXCL_START — Ogre frame listener requires render loop
Expand Down Expand Up @@ -2497,6 +2588,12 @@ void MainWindow::on_actionMaterial_Editor_triggered()

void MainWindow::updateMergeAnimationsButton()
{
// SelectionSet::selectionChanged is wired to updateMergeAnimationsButton
// first and updateToolRailForMode immediately after (see the connect()
// pair where these slots are registered). Qt fires connections in
// registration order, so the tool rail is always refreshed for free
// after this slot completes — calling updateToolRailForMode() here
// would just iterate every toolbar action twice per selection event.
auto entities = SelectionSet::getSingleton()->getResolvedEntities();

// Filter to entities with skeletons
Expand Down
86 changes: 86 additions & 0 deletions src/mainwindow_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,92 @@ TEST_F(MainWindowTest, ModeBarLoadsAndModeChangeUpdatesStatusIndicator)
EXPECT_EQ(window->m_editModeLabel->text(), QStringLiteral("Material mode"));
}

TEST_F(MainWindowTest, ContextualToolRailSwitchesActionsByMode)
{
QAction* primitiveAction = findActionByObjectName("modeObjectPrimitiveAction");
QAction* editExtrudeAction = findActionByObjectName("modeEditExtrudeAction");
QAction* dopeAction = findActionByObjectName("modeAnimationDopeSheetAction");
QAction* curveAction = findActionByObjectName("modeAnimationCurveEditorAction");
QAction* mergeAction = findActionByObjectName("modeAnimationMergeAction");
QAction* materialAction = findActionByObjectName("modeMaterialEditorAction");
QAction* validationAction = findActionByObjectName("modeValidationRunAction");
ASSERT_NE(primitiveAction, nullptr);
ASSERT_NE(editExtrudeAction, nullptr);
ASSERT_NE(dopeAction, nullptr);
ASSERT_NE(curveAction, nullptr);
ASSERT_NE(mergeAction, nullptr);
ASSERT_NE(materialAction, nullptr);
ASSERT_NE(validationAction, nullptr);

auto* modeController = EditorModeController::instance();

modeController->requestMode(EditorModeController::ObjectMode);
app->processEvents();
EXPECT_TRUE(primitiveAction->isVisible());
EXPECT_FALSE(editExtrudeAction->isVisible());
EXPECT_FALSE(dopeAction->isVisible());
EXPECT_FALSE(materialAction->isVisible());
EXPECT_FALSE(validationAction->isVisible());

modeController->requestMode(EditorModeController::AnimationMode);
app->processEvents();
EXPECT_FALSE(primitiveAction->isVisible());
EXPECT_TRUE(dopeAction->isVisible());
EXPECT_TRUE(curveAction->isVisible());
EXPECT_TRUE(mergeAction->isVisible());
EXPECT_FALSE(materialAction->isVisible());
EXPECT_FALSE(validationAction->isVisible());

modeController->requestMode(EditorModeController::MaterialMode);
app->processEvents();
EXPECT_FALSE(dopeAction->isVisible());
EXPECT_TRUE(materialAction->isVisible());
EXPECT_FALSE(validationAction->isVisible());

modeController->requestMode(EditorModeController::ValidationMode);
app->processEvents();
EXPECT_FALSE(materialAction->isVisible());
EXPECT_TRUE(validationAction->isVisible());
EXPECT_FALSE(validationAction->isEnabled());
}

TEST_F(MainWindowTest, ContextualToolRailKeepsSharedMenuActionsReachable)
{
auto* modeController = EditorModeController::instance();
modeController->requestMode(EditorModeController::ValidationMode);
app->processEvents();

EXPECT_TRUE(window->ui->actionMaterial_Editor->isVisible());
EXPECT_TRUE(window->ui->actionMerge_Animations->isVisible());

QWidget* materialToolbarButton =
window->ui->toolToolbar->widgetForAction(window->ui->actionMaterial_Editor);
QWidget* mergeToolbarButton =
window->ui->toolToolbar->widgetForAction(window->ui->actionMerge_Animations);
ASSERT_NE(materialToolbarButton, nullptr);
ASSERT_NE(mergeToolbarButton, nullptr);
EXPECT_TRUE(materialToolbarButton->isHidden());
EXPECT_TRUE(mergeToolbarButton->isHidden());
}

TEST_F(MainWindowTest, ContextualToolRailButtonsTriggerExistingBottomTools)
{
QAction* dopeAction = findActionByObjectName("modeAnimationDopeSheetAction");
ASSERT_NE(dopeAction, nullptr);
auto* dopeButton = qobject_cast<QToolButton*>(window->ui->objectsToolbar->widgetForAction(dopeAction));
ASSERT_NE(dopeButton, nullptr);
ASSERT_NE(window->m_dopeSheetDock, nullptr);

window->m_dopeSheetDock->hide();
EditorModeController::instance()->requestMode(EditorModeController::AnimationMode);
app->processEvents();

dopeButton->click();
app->processEvents();
EXPECT_FALSE(window->m_dopeSheetDock->isHidden());
EXPECT_EQ(window->dockWidgetArea(window->m_dopeSheetDock), Qt::BottomDockWidgetArea);
}

// ---- Cycle all transform states via keyboard ----

TEST_F(MainWindowTest, CycleAllStatesViaKeyboard) {
Expand Down
Loading