diff --git a/qtfred/help-src/doc/general/LayerManagerDialog.html b/qtfred/help-src/doc/general/LayerManagerDialog.html index 5b92bc38cd2..4dbc9ac63f1 100644 --- a/qtfred/help-src/doc/general/LayerManagerDialog.html +++ b/qtfred/help-src/doc/general/LayerManagerDialog.html @@ -17,8 +17,10 @@

Layers tab

Lists all layers in the mission. Each layer has a checkbox; uncheck it to hide all objects on that layer in the viewport, check it to show them again. Newly placed objects are automatically assigned to the Default layer.

-

Use Add to create a new layer. Use Delete to remove -the selected layer. The Default layer cannot be deleted.

+

Use Add Layer to create a new layer. Use Rename Layer +to rename the selected layer; every object on it is updated to the new name. Use +Delete Layer to remove the selected layer, reassigning its objects to the +Default layer. The Default layer cannot be renamed or deleted.

Filters tab

Works in conjunction with layers to provide additional visibility control. Allows diff --git a/qtfred/help-src/doc/general/SceneBrowserDialog.html b/qtfred/help-src/doc/general/SceneBrowserDialog.html index 07a6b9a477d..591ce11903c 100644 --- a/qtfred/help-src/doc/general/SceneBrowserDialog.html +++ b/qtfred/help-src/doc/general/SceneBrowserDialog.html @@ -38,6 +38,9 @@

Layer visibility

Each layer row in the list has a checkbox. Uncheck it to hide all objects on that layer in the viewport; check it to show them again. This is the same visibility toggle available in the Layer Manager.

+

Right-click a layer row for a menu with Rename Layer, +which renames the layer just as the Layer Manager does. The Default layer cannot be +renamed.

Hiding objects via the layer checkbox does not delete or disable them. Hidden objects remain in the mission file and function normally in-game.
diff --git a/qtfred/src/mission/EditorViewport.cpp b/qtfred/src/mission/EditorViewport.cpp index cee5ef70045..03489e6e943 100644 --- a/qtfred/src/mission/EditorViewport.cpp +++ b/qtfred/src/mission/EditorViewport.cpp @@ -946,6 +946,56 @@ bool EditorViewport::deleteLayer(const SCP_string& name, SCP_string* errorMessag return true; } +bool EditorViewport::renameLayer(const SCP_string& oldName, const SCP_string& newName, SCP_string* errorMessage) { + if (newName.empty()) { + if (errorMessage != nullptr) { + *errorMessage = "Layer name cannot be empty."; + } + return false; + } + + const auto layerIndex = getLayerIndex(oldName); + if (layerIndex == static_cast(-1)) { + if (errorMessage != nullptr) { + *errorMessage = "Layer does not exist."; + } + return false; + } + if (layerIndex == 0) { + if (errorMessage != nullptr) { + *errorMessage = "The default layer cannot be renamed."; + } + return false; + } + + // Reject collisions with a different layer; a case-only rename resolves to the same index and is allowed. + const auto existingIndex = getLayerIndex(newName); + if (existingIndex != static_cast(-1) && existingIndex != layerIndex) { + if (errorMessage != nullptr) { + *errorMessage = "Layer names must be unique."; + } + return false; + } + + _layerNames[layerIndex] = newName; + + // Rewrite the per-object fred_layer string on every object assigned to this layer. + std::vector toResync; + for (const auto& objectLayer : _objectLayers) { + if (objectLayer.second == layerIndex) { + toResync.push_back(objectLayer.first); + } + } + for (int objIdx : toResync) { + setObjectLayerByIndex(objIdx, layerIndex); + } + + syncMissionLayerNames(); + editor->notifyLayerStructureChanged(); + editor->notifyLayerListChanged(); + return true; +} + bool EditorViewport::setLayerVisibility(const SCP_string& name, bool visible, SCP_string* errorMessage) { const auto layerIndex = getLayerIndex(name); if (layerIndex == static_cast(-1)) { diff --git a/qtfred/src/mission/EditorViewport.h b/qtfred/src/mission/EditorViewport.h index 91dca1b071a..8d788b3b5fa 100644 --- a/qtfred/src/mission/EditorViewport.h +++ b/qtfred/src/mission/EditorViewport.h @@ -127,6 +127,7 @@ class EditorViewport { SCP_vector getLayerNames() const; bool addLayer(const SCP_string& name, SCP_string* errorMessage = nullptr); bool deleteLayer(const SCP_string& name, SCP_string* errorMessage = nullptr); + bool renameLayer(const SCP_string& oldName, const SCP_string& newName, SCP_string* errorMessage = nullptr); bool setLayerVisibility(const SCP_string& name, bool visible, SCP_string* errorMessage = nullptr); bool getLayerVisibility(const SCP_string& name, bool* visible, SCP_string* errorMessage = nullptr) const; void showAllLayers(); diff --git a/qtfred/src/mission/dialogs/LayerManagerDialogModel.cpp b/qtfred/src/mission/dialogs/LayerManagerDialogModel.cpp index c6ee021a655..d7e4b2e22e1 100644 --- a/qtfred/src/mission/dialogs/LayerManagerDialogModel.cpp +++ b/qtfred/src/mission/dialogs/LayerManagerDialogModel.cpp @@ -59,6 +59,14 @@ bool LayerManagerDialogModel::deleteLayer(const SCP_string& name, SCP_string* er return true; } +bool LayerManagerDialogModel::renameLayer(const SCP_string& oldName, const SCP_string& newName, SCP_string* error) { + if (!_viewport->renameLayer(oldName, newName, error)) { + return false; + } + modelChanged(); + return true; +} + bool LayerManagerDialogModel::isDefaultLayer(const SCP_string& name) { return name == EditorViewport::DefaultLayerName; } diff --git a/qtfred/src/mission/dialogs/LayerManagerDialogModel.h b/qtfred/src/mission/dialogs/LayerManagerDialogModel.h index 4409fd2e012..5891216630c 100644 --- a/qtfred/src/mission/dialogs/LayerManagerDialogModel.h +++ b/qtfred/src/mission/dialogs/LayerManagerDialogModel.h @@ -20,6 +20,7 @@ class LayerManagerDialogModel : public AbstractDialogModel { bool setLayerVisibility(const SCP_string& name, bool visible, SCP_string* error); bool addLayer(const SCP_string& name, SCP_string* error); bool deleteLayer(const SCP_string& name, SCP_string* error); + bool renameLayer(const SCP_string& oldName, const SCP_string& newName, SCP_string* error); static bool isDefaultLayer(const SCP_string& name); // Object type filters diff --git a/qtfred/src/mission/dialogs/SceneBrowserModel.cpp b/qtfred/src/mission/dialogs/SceneBrowserModel.cpp index 33e4ec58f39..cb3837f1504 100644 --- a/qtfred/src/mission/dialogs/SceneBrowserModel.cpp +++ b/qtfred/src/mission/dialogs/SceneBrowserModel.cpp @@ -195,6 +195,18 @@ void SceneBrowserModel::toggleLayerVisibility(const QString& layerName) // setLayerVisibility calls editor->notifyLayerVisibilityChanged() → onLayerVisibilityChanged() } +bool SceneBrowserModel::renameLayer(const QString& oldName, const QString& newName, SCP_string* error) +{ + // renameLayer fires editor->notifyLayerStructureChanged() → onLayerStructureChanged(), + // which rebuilds the tree, so there is nothing extra to do on success here. + return _viewport->renameLayer(oldName.toUtf8().constData(), newName.toUtf8().constData(), error); +} + +bool SceneBrowserModel::isDefaultLayer(const QString& name) +{ + return name.toUtf8().constData() == SCP_string(EditorViewport::DefaultLayerName); +} + void SceneBrowserModel::moveObjectToLayer(int objNum, const QString& layerName) { // Single inline call: temporary QByteArray lives until end of full expression — safe. diff --git a/qtfred/src/mission/dialogs/SceneBrowserModel.h b/qtfred/src/mission/dialogs/SceneBrowserModel.h index 5f055a99dc5..a54206e7025 100644 --- a/qtfred/src/mission/dialogs/SceneBrowserModel.h +++ b/qtfred/src/mission/dialogs/SceneBrowserModel.h @@ -50,6 +50,8 @@ class SceneBrowserModel : public AbstractDialogModel { QVector getLayerNames() const; void toggleLayerVisibility(const QString& layerName); + bool renameLayer(const QString& oldName, const QString& newName, SCP_string* error); + static bool isDefaultLayer(const QString& name); void moveObjectToLayer(int objNum, const QString& layerName); void moveWingToLayer(int wingIndex, const QString& layerName); void moveWaypointPathToLayer(int waypointListIndex, const QString& layerName); diff --git a/qtfred/src/ui/FredView.cpp b/qtfred/src/ui/FredView.cpp index 3080f68a8a7..375c6688ae6 100644 --- a/qtfred/src/ui/FredView.cpp +++ b/qtfred/src/ui/FredView.cpp @@ -2117,8 +2117,11 @@ void FredView::populateMoveToLayerMenu(int targetObject, QMenu* targetMenu) { _viewport->getLayerVisibility(layerName, &visible); auto* layerAction = new QAction(QString::fromStdString(layerName), dest); - layerAction->setCheckable(true); - layerAction->setChecked(_viewport->getObjectLayerName(targetObject) == layerName); + if (_viewport->getObjectLayerName(targetObject) == layerName) { + auto font = layerAction->font(); + font.setBold(true); + layerAction->setFont(font); + } layerAction->setEnabled(visible); connect(layerAction, &QAction::triggered, this, [this, layerName, targetObject]() { diff --git a/qtfred/src/ui/dialogs/LayerManagerDialog.cpp b/qtfred/src/ui/dialogs/LayerManagerDialog.cpp index 8d1a9ca3138..570ce5e259a 100644 --- a/qtfred/src/ui/dialogs/LayerManagerDialog.cpp +++ b/qtfred/src/ui/dialogs/LayerManagerDialog.cpp @@ -77,8 +77,10 @@ void LayerManagerDialog::updateUi() { _refreshing = false; - // Update delete button based on selection - ui->deleteLayerButton->setEnabled(ui->layerList->currentRow() > 0); + // Update rename/delete buttons based on selection (row 0 is the default layer) + const bool nonDefaultSelected = ui->layerList->currentRow() > 0; + ui->renameLayerButton->setEnabled(nonDefaultSelected); + ui->deleteLayerButton->setEnabled(nonDefaultSelected); } void LayerManagerDialog::on_addLayerButton_clicked() { @@ -106,6 +108,42 @@ void LayerManagerDialog::on_addLayerButton_clicked() { } } +void LayerManagerDialog::on_renameLayerButton_clicked() { + auto* item = ui->layerList->currentItem(); + if (item == nullptr) { + return; + } + + const QString oldName = item->text(); + if (_model->isDefaultLayer(oldName.toUtf8().constData())) { + QMessageBox::warning(this, tr("Layer Error"), tr("The default layer cannot be renamed.")); + return; + } + + bool ok = false; + auto newName = QInputDialog::getText(this, tr("Rename Layer"), tr("Layer name:"), QLineEdit::Normal, oldName, &ok).trimmed(); + if (!ok || newName == oldName) { + if (ok && newName.isEmpty()) { + QMessageBox::warning(this, tr("Layer Error"), tr("Layer name cannot be empty.")); + } + return; + } + + SCP_string error; + if (!_model->renameLayer(oldName.toUtf8().constData(), newName.toUtf8().constData(), &error)) { + QMessageBox::warning(this, tr("Layer Error"), QString::fromStdString(error)); + return; + } + + // Keep the renamed layer selected + for (int i = 0; i < ui->layerList->count(); ++i) { + if (ui->layerList->item(i)->text() == newName) { + ui->layerList->setCurrentRow(i); + break; + } + } +} + void LayerManagerDialog::on_deleteLayerButton_clicked() { auto* item = ui->layerList->currentItem(); if (item == nullptr) { @@ -125,6 +163,7 @@ void LayerManagerDialog::on_deleteLayerButton_clicked() { } void LayerManagerDialog::on_layerList_currentRowChanged(int row) { + ui->renameLayerButton->setEnabled(row > 0); ui->deleteLayerButton->setEnabled(row > 0); } diff --git a/qtfred/src/ui/dialogs/LayerManagerDialog.h b/qtfred/src/ui/dialogs/LayerManagerDialog.h index a65ae96d897..6a39cf375f3 100644 --- a/qtfred/src/ui/dialogs/LayerManagerDialog.h +++ b/qtfred/src/ui/dialogs/LayerManagerDialog.h @@ -23,6 +23,7 @@ class LayerManagerDialog final : public QDialog { private slots: void on_addLayerButton_clicked(); + void on_renameLayerButton_clicked(); void on_deleteLayerButton_clicked(); void on_layerList_currentRowChanged(int row); void on_layerList_itemChanged(QListWidgetItem* item); diff --git a/qtfred/src/ui/panels/SceneBrowserPanel.cpp b/qtfred/src/ui/panels/SceneBrowserPanel.cpp index bd736ac110d..7f18210bd65 100644 --- a/qtfred/src/ui/panels/SceneBrowserPanel.cpp +++ b/qtfred/src/ui/panels/SceneBrowserPanel.cpp @@ -6,8 +6,10 @@ #include #include +#include #include #include +#include namespace fso::fred { @@ -410,12 +412,24 @@ void SceneBrowserPanel::onCustomContextMenuRequested(const QPoint& pos) auto* item = _tree->itemAt(pos); if (!item) return; - // Don't show a context menu for layer header items or category items + const auto globalPos = _tree->viewport()->mapToGlobal(pos); + + // Layer header item: offer a small menu to rename the layer auto varLayer = item->data(0, IsLayerItemRole); - if (!varLayer.isNull()) return; - if (item->flags() == Qt::ItemIsEnabled) return; // category item + if (!varLayer.isNull()) { + const auto layerName = item->data(0, LayerNameRole).toString(); + + QMenu menu; + auto* renameAction = menu.addAction(tr("Rename Layer")); + renameAction->setEnabled(!dialogs::SceneBrowserModel::isDefaultLayer(layerName)); + if (menu.exec(globalPos) == renameAction) { + renameLayer(layerName); + } + return; + } - const auto globalPos = _tree->viewport()->mapToGlobal(pos); + // Category items are not actionable + if (item->flags() == Qt::ItemIsEnabled) return; // category item auto varObjNum = item->data(0, ObjNumRole); auto varWing = item->data(0, WingIndexRole); @@ -431,6 +445,24 @@ void SceneBrowserPanel::onCustomContextMenuRequested(const QPoint& pos) } } +void SceneBrowserPanel::renameLayer(const QString& layerName) +{ + bool ok = false; + const auto newName = QInputDialog::getText(this, tr("Rename Layer"), tr("Layer name:"), + QLineEdit::Normal, layerName, &ok).trimmed(); + if (!ok || newName == layerName) { + if (ok && newName.isEmpty()) { + QMessageBox::warning(this, tr("Layer Error"), tr("Layer name cannot be empty.")); + } + return; + } + + SCP_string error; + if (!_model->renameLayer(layerName, newName, &error)) { + QMessageBox::warning(this, tr("Layer Error"), QString::fromStdString(error)); + } +} + void SceneBrowserPanel::onSearchTextChanged(const QString& text) { _model->setNameFilter(text); diff --git a/qtfred/src/ui/panels/SceneBrowserPanel.h b/qtfred/src/ui/panels/SceneBrowserPanel.h index f19ae50cdb0..de65b1571b1 100644 --- a/qtfred/src/ui/panels/SceneBrowserPanel.h +++ b/qtfred/src/ui/panels/SceneBrowserPanel.h @@ -49,6 +49,7 @@ private slots: private: // NOLINT(readability-redundant-access-specifiers) void rebuildTree(); + void renameLayer(const QString& layerName); void updateFloatingMargins(bool floating); void rememberExpansionState(); bool expandedStateOrDefault(const QString& key, bool defaultExpanded) const; diff --git a/qtfred/ui/LayerManagerDialog.ui b/qtfred/ui/LayerManagerDialog.ui index e79c8f9c39a..1eabf4698d9 100644 --- a/qtfred/ui/LayerManagerDialog.ui +++ b/qtfred/ui/LayerManagerDialog.ui @@ -36,6 +36,13 @@ + + + + Rename Layer + + +