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
6 changes: 4 additions & 2 deletions qtfred/help-src/doc/general/LayerManagerDialog.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ <h2>Layers tab</h2>
<p>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.</p>
<p>Use <strong>Add</strong> to create a new layer. Use <strong>Delete</strong> to remove
the selected layer. The <em>Default</em> layer cannot be deleted.</p>
<p>Use <strong>Add Layer</strong> to create a new layer. Use <strong>Rename Layer</strong>
to rename the selected layer; every object on it is updated to the new name. Use
<strong>Delete Layer</strong> to remove the selected layer, reassigning its objects to the
Default layer. The <em>Default</em> layer cannot be renamed or deleted.</p>

<h2>Filters tab</h2>
<p>Works in conjunction with layers to provide additional visibility control. Allows
Expand Down
3 changes: 3 additions & 0 deletions qtfred/help-src/doc/general/SceneBrowserDialog.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ <h2>Layer visibility</h2>
<p>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 <a href="LayerManagerDialog.html">Layer Manager</a>.</p>
<p><strong>Right-click</strong> a layer row for a menu with <strong>Rename Layer</strong>,
which renames the layer just as the Layer Manager does. The <em>Default</em> layer cannot be
renamed.</p>

<div class="note">Hiding objects via the layer checkbox does not delete or disable them.
Hidden objects remain in the mission file and function normally in-game.</div>
Expand Down
50 changes: 50 additions & 0 deletions qtfred/src/mission/EditorViewport.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<size_t>(-1)) {
Comment thread
JohnAFernandez marked this conversation as resolved.
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<size_t>(-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<int> 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<size_t>(-1)) {
Expand Down
1 change: 1 addition & 0 deletions qtfred/src/mission/EditorViewport.h
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ class EditorViewport {
SCP_vector<SCP_string> 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();
Expand Down
8 changes: 8 additions & 0 deletions qtfred/src/mission/dialogs/LayerManagerDialogModel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
1 change: 1 addition & 0 deletions qtfred/src/mission/dialogs/LayerManagerDialogModel.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions qtfred/src/mission/dialogs/SceneBrowserModel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions qtfred/src/mission/dialogs/SceneBrowserModel.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ class SceneBrowserModel : public AbstractDialogModel {
QVector<QString> 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);
Expand Down
7 changes: 5 additions & 2 deletions qtfred/src/ui/FredView.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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]() {
Expand Down
43 changes: 41 additions & 2 deletions qtfred/src/ui/dialogs/LayerManagerDialog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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) {
Expand All @@ -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);
}

Expand Down
1 change: 1 addition & 0 deletions qtfred/src/ui/dialogs/LayerManagerDialog.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
40 changes: 36 additions & 4 deletions qtfred/src/ui/panels/SceneBrowserPanel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
#include <mission/dialogs/SceneBrowserModel.h>
#include <ui/FredView.h>

#include <QInputDialog>
#include <QLayout>
#include <QMenu>
#include <QMessageBox>

namespace fso::fred {

Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down
1 change: 1 addition & 0 deletions qtfred/src/ui/panels/SceneBrowserPanel.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
7 changes: 7 additions & 0 deletions qtfred/ui/LayerManagerDialog.ui
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="renameLayerButton">
<property name="text">
<string>Rename Layer</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="deleteLayerButton">
<property name="text">
Expand Down
Loading