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
+
+
+
-