From 3207962c0d306223a6618ca7e8d9a12edb0af348 Mon Sep 17 00:00:00 2001
From: Mike Nelson
Date: Sat, 2 May 2026 14:23:47 -0500
Subject: [PATCH 1/3] make waypoint multi select and direct edit
---
.../dialogs/WaypointEditorDialogModel.cpp | 365 ++++++++++--------
.../dialogs/WaypointEditorDialogModel.h | 32 +-
qtfred/src/ui/FredView.cpp | 2 +-
.../src/ui/dialogs/WaypointEditorDialog.cpp | 68 ++--
qtfred/src/ui/dialogs/WaypointEditorDialog.h | 5 +-
qtfred/ui/WaypointEditorDialog.ui | 70 ++--
6 files changed, 274 insertions(+), 268 deletions(-)
diff --git a/qtfred/src/mission/dialogs/WaypointEditorDialogModel.cpp b/qtfred/src/mission/dialogs/WaypointEditorDialogModel.cpp
index 48f4b95c30d..4971409b70e 100644
--- a/qtfred/src/mission/dialogs/WaypointEditorDialogModel.cpp
+++ b/qtfred/src/mission/dialogs/WaypointEditorDialogModel.cpp
@@ -15,151 +15,114 @@ WaypointEditorDialogModel::WaypointEditorDialogModel(QObject* parent, EditorView
initializeData();
}
-bool WaypointEditorDialogModel::apply()
-{
- if (!validateData()) {
- return false;
- }
-
- // apply name
- char old_name[NAME_LENGTH];
- strcpy_s(old_name, _editor->cur_waypoint_list->get_name());
- _editor->cur_waypoint_list->set_name(_currentName.c_str());
- auto str = _editor->cur_waypoint_list->get_name();
- if (strcmp(old_name, str) != 0) {
- _editor->missionChanged();
- update_sexp_references(old_name, str);
- _editor->ai_update_goal_references(sexp_ref_type::WAYPOINT_PATH, old_name, str);
-
- for (auto &wpt : _editor->cur_waypoint_list->get_waypoints()) {
- char old_buf[NAME_LENGTH];
- char new_buf[NAME_LENGTH];
- waypoint_stuff_name(old_buf, old_name, wpt.get_index() + 1);
- waypoint_stuff_name(new_buf, str, wpt.get_index() + 1);
- update_sexp_references(old_buf, new_buf);
- _editor->ai_update_goal_references(sexp_ref_type::WAYPOINT, old_buf, new_buf);
- }
- }
-
- // apply display properties
- _editor->cur_waypoint_list->set_no_draw_lines(_noDrawLines);
- if (_hasCustomColor)
- _editor->cur_waypoint_list->set_color((ubyte)_colorR, (ubyte)_colorG, (ubyte)_colorB);
- else
- _editor->cur_waypoint_list->clear_color();
-
- _editor->missionChanged();
-
+bool WaypointEditorDialogModel::apply() {
return true;
}
-void WaypointEditorDialogModel::reject()
-{
- // do nothing
-}
+void WaypointEditorDialogModel::reject() {}
-void WaypointEditorDialogModel::initializeData()
-{
- _enabled = true;
+void WaypointEditorDialogModel::initializeData() {
+ _selectedWaypointPaths.clear();
- if (query_valid_object(_editor->currentObject) && Objects[_editor->currentObject].type == OBJ_WAYPOINT) {
- Assertion(_editor->cur_waypoint_list == find_waypoint_list_with_instance(Objects[_editor->currentObject].instance), "Waypoint no longer exists in the mission!");
+ // Collect unique waypoint list indices from all marked OBJ_WAYPOINT objects
+ SCP_vector seen(Waypoint_lists.size(), false);
+ for (auto* ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) {
+ if (ptr->type == OBJ_WAYPOINT && ptr->flags[Object::Object_Flags::Marked]) {
+ int listIdx = calc_waypoint_list_index(ptr->instance);
+ if (listIdx >= 0 && listIdx < static_cast(Waypoint_lists.size()) && !seen[listIdx]) {
+ seen[listIdx] = true;
+ _selectedWaypointPaths.push_back(listIdx);
+ }
+ }
}
- updateWaypointPathList();
+ // Fall back to cur_waypoint_list if nothing marked
+ if (_selectedWaypointPaths.empty() && _editor->cur_waypoint_list != nullptr) {
+ int idx = find_index_of_waypoint_list(_editor->cur_waypoint_list);
+ if (idx >= 0) {
+ _selectedWaypointPaths.push_back(idx);
+ }
+ }
- if (_editor->cur_waypoint_list != nullptr) {
- _currentName = _editor->cur_waypoint_list->get_name();
- _noDrawLines = _editor->cur_waypoint_list->get_no_draw_lines();
- _hasCustomColor = _editor->cur_waypoint_list->get_has_custom_color();
- _colorR = _editor->cur_waypoint_list->get_color_r();
- _colorG = _editor->cur_waypoint_list->get_color_g();
- _colorB = _editor->cur_waypoint_list->get_color_b();
+ if (!_selectedWaypointPaths.empty()) {
+ const auto& path = Waypoint_lists[_selectedWaypointPaths.front()];
+ _currentName = path.get_name();
+ _noDrawLines = path.get_no_draw_lines();
+ _hasCustomColor = path.get_has_custom_color();
+ _colorR = path.get_color_r();
+ _colorG = path.get_color_g();
+ _colorB = path.get_color_b();
} else {
- _currentName = "";
+ _currentName.clear();
_noDrawLines = false;
_hasCustomColor = false;
_colorR = _colorG = _colorB = 255;
- _enabled = false;
}
Q_EMIT waypointPathMarkingChanged();
_modified = false;
}
-void WaypointEditorDialogModel::updateWaypointPathList()
-{
-
- _waypointPathList.clear();
- _currentWaypointPathSelected = -1;
-
- for (size_t i = 0; i < Waypoint_lists.size(); ++i) {
- _waypointPathList.emplace_back(Waypoint_lists[i].get_name(), static_cast(i));
- }
+bool WaypointEditorDialogModel::hasValidSelection() const {
+ return !_selectedWaypointPaths.empty();
+}
- if (_editor->cur_waypoint_list != nullptr) {
- int index = find_index_of_waypoint_list(_editor->cur_waypoint_list);
- Assertion(index >= 0, "Could not find waypoint path in waypoint path list!");
- _currentWaypointPathSelected = index;
- }
+bool WaypointEditorDialogModel::hasMultipleSelection() const {
+ return _selectedWaypointPaths.size() > 1;
}
-bool WaypointEditorDialogModel::validateData()
-{
- // Reset flag before applying
- _bypass_errors = false;
+bool WaypointEditorDialogModel::hasAnyPathsInMission() const {
+ return !Waypoint_lists.empty();
+}
- if (query_valid_object(_editor->currentObject) && Objects[_editor->currentObject].type == OBJ_WAYPOINT) {
- Assertion(_editor->cur_waypoint_list == find_waypoint_list_with_instance(Objects[_editor->currentObject].instance), "Waypoint no longer exists in the mission!");
- }
+int WaypointEditorDialogModel::getSelectionCount() const {
+ return static_cast(_selectedWaypointPaths.size());
+}
+bool WaypointEditorDialogModel::validateName(const SCP_string& name) {
// wing name collision
for (auto& wing : Wings) {
- if (!stricmp(wing.name, _currentName.c_str())) {
+ if (!stricmp(wing.name, name.c_str())) {
showErrorDialogNoCancel("This waypoint path name is already being used by a wing");
return false;
}
}
// ship name collision
- object* ptr = GET_FIRST(&obj_used_list);
- while (ptr != END_OF_LIST(&obj_used_list)) {
+ for (auto* ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) {
if ((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) {
- if (!stricmp(_currentName.c_str(), Ships[ptr->instance].ship_name)) {
+ if (!stricmp(name.c_str(), Ships[ptr->instance].ship_name)) {
showErrorDialogNoCancel("This waypoint path name is already being used by a ship");
return false;
}
}
-
- ptr = GET_NEXT(ptr);
}
- // We don't need to check teams. "Unknown" is a valid name and also an IFF.
-
// target priority group name collision
for (auto& ai : Ai_tp_list) {
- if (!stricmp(_currentName.c_str(), ai.name)) {
+ if (!stricmp(name.c_str(), ai.name)) {
showErrorDialogNoCancel("This waypoint path name is already being used by a target priority group");
return false;
}
}
// waypoint path name collision
+ const waypoint_list* current_path = _selectedWaypointPaths.empty() ? nullptr : &Waypoint_lists[_selectedWaypointPaths.front()];
for (const auto& ii : Waypoint_lists) {
- if (!stricmp(ii.get_name(), _currentName.c_str()) && (&ii != _editor->cur_waypoint_list)) {
+ if (!stricmp(ii.get_name(), name.c_str()) && (&ii != current_path)) {
showErrorDialogNoCancel("This waypoint path name is already being used by another waypoint path");
return false;
}
}
// jump node name collision
- if (jumpnode_get_by_name(_currentName.c_str()) != nullptr) {
+ if (jumpnode_get_by_name(name.c_str()) != nullptr) {
showErrorDialogNoCancel("This waypoint path name is already being used by a jump node");
return false;
}
// formatting
- if (!_currentName.empty() && _currentName[0] == '<') {
+ if (!name.empty() && name[0] == '<') {
showErrorDialogNoCancel("Waypoint names not allowed to begin with '<'");
return false;
}
@@ -167,132 +130,196 @@ bool WaypointEditorDialogModel::validateData()
return true;
}
-void WaypointEditorDialogModel::showErrorDialogNoCancel(const SCP_string& message)
-{
+void WaypointEditorDialogModel::showErrorDialogNoCancel(const SCP_string& message) {
if (_bypass_errors) {
return;
}
-
_bypass_errors = true;
_viewport->dialogProvider->showButtonDialog(DialogType::Error, "Error", message, {DialogButton::Ok});
}
-void WaypointEditorDialogModel::onSelectedObjectChanged(int) {
- initializeData();
-}
-
-void WaypointEditorDialogModel::onSelectedObjectMarkingChanged(int, bool) {
- initializeData();
-}
-
-void WaypointEditorDialogModel::onMissionChanged()
-{
- // When the mission is changed we also need to update our data in case one of our elements changed
- initializeData();
-}
-
const SCP_string& WaypointEditorDialogModel::getCurrentName() const {
return _currentName;
}
-void WaypointEditorDialogModel::setCurrentName(const SCP_string& name)
-{
- modify(_currentName, name);
-}
+bool WaypointEditorDialogModel::setCurrentName(const SCP_string& name) {
+ if (hasMultipleSelection() || _selectedWaypointPaths.empty()) {
+ return true;
+ }
-int WaypointEditorDialogModel::getCurrentlySelectedPath() const {
- return _currentWaypointPathSelected;
-}
+ _bypass_errors = false;
-void WaypointEditorDialogModel::setCurrentlySelectedPath(int id)
-{
- if (_currentWaypointPathSelected == id) {
- // Nothing to do here
- return;
- }
+ SCP_string trimmed = name;
+ SCP_trim(trimmed);
- if (id < 0 || id >= static_cast(Waypoint_lists.size())) {
- return; // out of range; ignore
+ if (!validateName(trimmed)) {
+ return false;
}
- // Only apply if there is actually a current path to save changes to.
- bool canProceed = (_editor->cur_waypoint_list == nullptr) || apply();
+ auto& path = Waypoint_lists[_selectedWaypointPaths.front()];
- if (canProceed) {
- _editor->unmark_all();
+ char old_name[NAME_LENGTH];
+ strcpy_s(old_name, path.get_name());
+ path.set_name(trimmed.c_str());
+ const char* new_name = path.get_name();
- // mark all waypoints belonging to the selected list
- int listIndex = id;
- for (auto* ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) {
- if (ptr->type == OBJ_WAYPOINT) {
- if (calc_waypoint_list_index(ptr->instance) == listIndex) {
- _editor->markObject(OBJ_INDEX(ptr));
- }
- }
+ if (strcmp(old_name, new_name) != 0) {
+ update_sexp_references(old_name, new_name);
+ _editor->ai_update_goal_references(sexp_ref_type::WAYPOINT_PATH, old_name, new_name);
+
+ for (auto& wpt : path.get_waypoints()) {
+ char old_buf[NAME_LENGTH];
+ char new_buf[NAME_LENGTH];
+ waypoint_stuff_name(old_buf, old_name, wpt.get_index() + 1);
+ waypoint_stuff_name(new_buf, new_name, wpt.get_index() + 1);
+ update_sexp_references(old_buf, new_buf);
+ _editor->ai_update_goal_references(sexp_ref_type::WAYPOINT, old_buf, new_buf);
}
+ }
+
+ _currentName = new_name;
+ set_modified();
+ _editor->missionChanged();
+ return true;
+}
- _currentWaypointPathSelected = id;
+bool WaypointEditorDialogModel::getNoDrawLines() const { return _noDrawLines; }
+
+void WaypointEditorDialogModel::setNoDrawLines(bool val) {
+ _noDrawLines = val;
+ for (auto idx : _selectedWaypointPaths) {
+ Waypoint_lists[idx].set_no_draw_lines(val);
}
+ set_modified();
+ _editor->missionChanged();
}
-bool WaypointEditorDialogModel::isEnabled() const {
- return _enabled;
+bool WaypointEditorDialogModel::getHasCustomColor() const { return _hasCustomColor; }
+
+void WaypointEditorDialogModel::setHasCustomColor(bool val) {
+ _hasCustomColor = val;
+ for (auto idx : _selectedWaypointPaths) {
+ if (val) {
+ Waypoint_lists[idx].set_color((ubyte)_colorR, (ubyte)_colorG, (ubyte)_colorB);
+ } else {
+ Waypoint_lists[idx].clear_color();
+ }
+ }
+ set_modified();
+ _editor->missionChanged();
}
-const SCP_vector>& WaypointEditorDialogModel::getWaypointPathList() const
-{
- return _waypointPathList;
+int WaypointEditorDialogModel::getColorR() const { return _colorR; }
+
+void WaypointEditorDialogModel::setColorR(int r) {
+ CLAMP(r, 0, 255);
+ _colorR = r;
+ if (_hasCustomColor) {
+ for (auto idx : _selectedWaypointPaths) {
+ Waypoint_lists[idx].set_color((ubyte)_colorR, (ubyte)_colorG, (ubyte)_colorB);
+ }
+ }
+ set_modified();
+ _editor->missionChanged();
+}
+
+int WaypointEditorDialogModel::getColorG() const { return _colorG; }
+
+void WaypointEditorDialogModel::setColorG(int g) {
+ CLAMP(g, 0, 255);
+ _colorG = g;
+ if (_hasCustomColor) {
+ for (auto idx : _selectedWaypointPaths) {
+ Waypoint_lists[idx].set_color((ubyte)_colorR, (ubyte)_colorG, (ubyte)_colorB);
+ }
+ }
+ set_modified();
+ _editor->missionChanged();
}
-SCP_string WaypointEditorDialogModel::getLayer() const
-{
- if (_editor->cur_waypoint_list == nullptr)
- return "";
+int WaypointEditorDialogModel::getColorB() const { return _colorB; }
+
+void WaypointEditorDialogModel::setColorB(int b) {
+ CLAMP(b, 0, 255);
+ _colorB = b;
+ if (_hasCustomColor) {
+ for (auto idx : _selectedWaypointPaths) {
+ Waypoint_lists[idx].set_color((ubyte)_colorR, (ubyte)_colorG, (ubyte)_colorB);
+ }
+ }
+ set_modified();
+ _editor->missionChanged();
+}
- int listIndex = find_index_of_waypoint_list(_editor->cur_waypoint_list);
+SCP_string WaypointEditorDialogModel::getLayer() const {
SCP_string result;
bool first = true;
-
- for (auto* ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) {
- if (ptr->type == OBJ_WAYPOINT && calc_waypoint_list_index(ptr->instance) == listIndex) {
- SCP_string layer = _viewport->getObjectLayerName(OBJ_INDEX(ptr));
- if (first) {
- result = layer;
- first = false;
- } else if (result != layer) {
- return "";
+ for (auto pathIdx : _selectedWaypointPaths) {
+ for (auto* ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) {
+ if (ptr->type == OBJ_WAYPOINT && calc_waypoint_list_index(ptr->instance) == pathIdx) {
+ SCP_string layer = _viewport->getObjectLayerName(OBJ_INDEX(ptr));
+ if (first) {
+ result = layer;
+ first = false;
+ } else if (result != layer) {
+ return "";
+ }
}
}
}
return result;
}
-void WaypointEditorDialogModel::setLayer(const SCP_string& layer)
-{
- if (_editor->cur_waypoint_list == nullptr)
+void WaypointEditorDialogModel::setLayer(const SCP_string& layer) {
+ for (auto pathIdx : _selectedWaypointPaths) {
+ for (auto* ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) {
+ if (ptr->type == OBJ_WAYPOINT && calc_waypoint_list_index(ptr->instance) == pathIdx) {
+ _viewport->moveObjectToLayer(OBJ_INDEX(ptr), layer);
+ }
+ }
+ }
+ set_modified();
+ _editor->missionChanged();
+}
+
+void WaypointEditorDialogModel::selectWaypointPathByIndex(int idx) {
+ if (idx < 0 || idx >= static_cast(Waypoint_lists.size()))
return;
- int listIndex = find_index_of_waypoint_list(_editor->cur_waypoint_list);
+ _editor->unmark_all();
for (auto* ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) {
- if (ptr->type == OBJ_WAYPOINT && calc_waypoint_list_index(ptr->instance) == listIndex) {
- _viewport->moveObjectToLayer(OBJ_INDEX(ptr), layer);
+ if (ptr->type == OBJ_WAYPOINT && calc_waypoint_list_index(ptr->instance) == idx) {
+ _editor->markObject(OBJ_INDEX(ptr));
}
}
- set_modified();
- _editor->missionChanged();
}
-bool WaypointEditorDialogModel::getNoDrawLines() const { return _noDrawLines; }
-void WaypointEditorDialogModel::setNoDrawLines(bool val) { modify(_noDrawLines, val); }
+void WaypointEditorDialogModel::selectNextPath() {
+ if (Waypoint_lists.empty())
+ return;
+ int current = _selectedWaypointPaths.empty() ? -1 : _selectedWaypointPaths.front();
+ int next = (current + 1) % static_cast(Waypoint_lists.size());
+ selectWaypointPathByIndex(next);
+}
-bool WaypointEditorDialogModel::getHasCustomColor() const { return _hasCustomColor; }
-void WaypointEditorDialogModel::setHasCustomColor(bool val) { modify(_hasCustomColor, val); }
+void WaypointEditorDialogModel::selectPreviousPath() {
+ if (Waypoint_lists.empty())
+ return;
+ int current = _selectedWaypointPaths.empty() ? 0 : _selectedWaypointPaths.front();
+ int prev = (current - 1 + static_cast(Waypoint_lists.size())) % static_cast(Waypoint_lists.size());
+ selectWaypointPathByIndex(prev);
+}
-int WaypointEditorDialogModel::getColorR() const { return _colorR; }
-int WaypointEditorDialogModel::getColorG() const { return _colorG; }
-int WaypointEditorDialogModel::getColorB() const { return _colorB; }
-void WaypointEditorDialogModel::setColorR(int r) { modify(_colorR, r); }
-void WaypointEditorDialogModel::setColorG(int g) { modify(_colorG, g); }
-void WaypointEditorDialogModel::setColorB(int b) { modify(_colorB, b); }
+void WaypointEditorDialogModel::onSelectedObjectChanged(int) {
+ initializeData();
+}
+
+void WaypointEditorDialogModel::onSelectedObjectMarkingChanged(int, bool) {
+ initializeData();
+}
+
+void WaypointEditorDialogModel::onMissionChanged() {
+ initializeData();
+}
} // namespace fso::fred::dialogs
diff --git a/qtfred/src/mission/dialogs/WaypointEditorDialogModel.h b/qtfred/src/mission/dialogs/WaypointEditorDialogModel.h
index 2d633b167f7..e90db022508 100644
--- a/qtfred/src/mission/dialogs/WaypointEditorDialogModel.h
+++ b/qtfred/src/mission/dialogs/WaypointEditorDialogModel.h
@@ -3,19 +3,22 @@
namespace fso::fred::dialogs {
-class WaypointEditorDialogModel: public AbstractDialogModel {
- Q_OBJECT
+class WaypointEditorDialogModel : public AbstractDialogModel {
+ Q_OBJECT
- public:
+public:
WaypointEditorDialogModel(QObject* parent, EditorViewport* viewport);
bool apply() override;
void reject() override;
+ bool hasValidSelection() const;
+ bool hasMultipleSelection() const;
+ bool hasAnyPathsInMission() const;
+ int getSelectionCount() const;
+
const SCP_string& getCurrentName() const;
- void setCurrentName(const SCP_string& name);
- int getCurrentlySelectedPath() const;
- void setCurrentlySelectedPath(int elementId);
+ bool setCurrentName(const SCP_string& name);
bool getNoDrawLines() const;
void setNoDrawLines(bool val);
@@ -28,31 +31,28 @@ class WaypointEditorDialogModel: public AbstractDialogModel {
void setColorG(int g);
void setColorB(int b);
- bool isEnabled() const;
- const SCP_vector>& getWaypointPathList() const;
-
SCP_string getLayer() const;
void setLayer(const SCP_string& layer);
+ void selectNextPath();
+ void selectPreviousPath();
+
signals:
void waypointPathMarkingChanged();
-
private slots:
void onSelectedObjectChanged(int);
void onSelectedObjectMarkingChanged(int, bool);
void onMissionChanged();
- private: // NOLINT(readability-redundant-access-specifiers)
+private:
void initializeData();
- void updateWaypointPathList();
- bool validateData();
void showErrorDialogNoCancel(const SCP_string& message);
+ bool validateName(const SCP_string& name);
+ void selectWaypointPathByIndex(int idx);
+ SCP_vector _selectedWaypointPaths; // indices into Waypoint_lists
SCP_string _currentName;
- int _currentWaypointPathSelected = -1;
- bool _enabled = false;
- SCP_vector> _waypointPathList;
bool _bypass_errors = false;
bool _noDrawLines = false;
bool _hasCustomColor = false;
diff --git a/qtfred/src/ui/FredView.cpp b/qtfred/src/ui/FredView.cpp
index c186c79737d..00446cda0fe 100644
--- a/qtfred/src/ui/FredView.cpp
+++ b/qtfred/src/ui/FredView.cpp
@@ -959,7 +959,7 @@ void FredView::onUpdateContextToolbar() {
});
_contextToolBar->addAction(selWingAct);
}
- } else if (effectiveType == OBJ_WAYPOINT && (numMarked <= 1 || multiSharedWaypointList != nullptr)) {
+ } else if (effectiveType == OBJ_WAYPOINT) {
addBtn(tr("Edit Waypoint Path"), &FredView::on_actionWaypoint_Paths_triggered);
} else if (numMarked <= 1 && effectiveType == OBJ_JUMP_NODE) {
addBtn(tr("Edit Jump Node"), &FredView::on_actionJump_Nodes_triggered);
diff --git a/qtfred/src/ui/dialogs/WaypointEditorDialog.cpp b/qtfred/src/ui/dialogs/WaypointEditorDialog.cpp
index 3eaa0aa06f5..b783a40a6d4 100644
--- a/qtfred/src/ui/dialogs/WaypointEditorDialog.cpp
+++ b/qtfred/src/ui/dialogs/WaypointEditorDialog.cpp
@@ -38,36 +38,33 @@ void WaypointEditorDialog::initializeUi()
{
util::SignalBlockers blockers(this);
- updateWaypointListComboBox();
-
ui->layerCombo->clear();
for (const auto& name : _viewport->getLayerNames()) {
ui->layerCombo->addItem(QString::fromStdString(name), QString::fromStdString(name));
}
- bool enabled = _model->isEnabled();
- ui->nameEdit->setEnabled(enabled);
+ const bool enabled = _model->hasValidSelection();
+ const bool hasAny = _model->hasAnyPathsInMission();
+ const bool multiSelect = _model->hasMultipleSelection();
+
+ ui->nameEdit->setEnabled(enabled && !multiSelect);
ui->noDrawLinesCheck->setEnabled(enabled);
ui->customColorCheck->setEnabled(enabled);
ui->layerCombo->setEnabled(enabled);
-}
-
-void WaypointEditorDialog::updateWaypointListComboBox()
-{
- ui->pathSelection->clear();
+ ui->prevPathButton->setEnabled(hasAny);
+ ui->nextPathButton->setEnabled(hasAny);
- for (auto& wp : _model->getWaypointPathList()) {
- ui->pathSelection->addItem(QString::fromStdString(wp.first), wp.second);
+ if (multiSelect) {
+ setWindowTitle(QString("Edit %1 Waypoint Paths").arg(_model->getSelectionCount()));
+ } else {
+ setWindowTitle("Waypoint Path Editor");
}
-
- ui->pathSelection->setEnabled(!_model->getWaypointPathList().empty());
}
void WaypointEditorDialog::updateUi()
{
util::SignalBlockers blockers(this);
ui->nameEdit->setText(QString::fromStdString(_model->getCurrentName()));
- ui->pathSelection->setCurrentIndex(ui->pathSelection->findData(_model->getCurrentlySelectedPath()));
ui->layerCombo->setCurrentIndex(ui->layerCombo->findData(QString::fromStdString(_model->getLayer())));
ui->noDrawLinesCheck->setChecked(_model->getNoDrawLines());
@@ -76,7 +73,7 @@ void WaypointEditorDialog::updateUi()
ui->colorGSpinBox->setValue(_model->getColorG());
ui->colorBSpinBox->setValue(_model->getColorB());
- bool colorEnabled = _model->isEnabled() && _model->getHasCustomColor();
+ const bool colorEnabled = _model->hasValidSelection() && _model->getHasCustomColor();
ui->colorRSpinBox->setEnabled(colorEnabled);
ui->colorGSpinBox->setEnabled(colorEnabled);
ui->colorBSpinBox->setEnabled(colorEnabled);
@@ -93,70 +90,55 @@ void WaypointEditorDialog::updateColorSwatch()
.arg(_model->getColorB()));
}
-void WaypointEditorDialog::on_pathSelection_currentIndexChanged(int index)
+void WaypointEditorDialog::on_prevPathButton_clicked()
{
- auto itemId = ui->pathSelection->itemData(index).value();
- _model->setCurrentlySelectedPath(itemId);
+ _model->selectPreviousPath();
}
-// This will run any time an edit is finished which includes the entire window closing, losing focus,
-// the user clicking elsewhere in the dialog, or pressing Enter in the edit box.
-// This is ok here because this is literally the only field that can be edited but if this dialog
-// ever expands then it would be wise to change the whole thing to an ok/cancel type dialog.
-void WaypointEditorDialog::on_nameEdit_editingFinished()
+void WaypointEditorDialog::on_nextPathButton_clicked()
{
- // Waypoint editor applies immediately when the name is changed
- // so save the current, try to apply, if fails, restore the current
- // and update the text in the edit box
-
- SCP_string current = _model->getCurrentName();
-
- SCP_string newText = ui->nameEdit->text().toUtf8().constData();
- _model->setCurrentName(newText);
+ _model->selectNextPath();
+}
- if (!_model->apply()) {
+void WaypointEditorDialog::on_nameEdit_editingFinished()
+{
+ if (!_model->setCurrentName(ui->nameEdit->text().toUtf8().constData())) {
util::SignalBlockers blockers(this);
- // If apply failed, restore the old name
- ui->nameEdit->setText(QString::fromStdString(current));
- _model->setCurrentName(current); // Restore the model's current name
+ ui->nameEdit->setText(QString::fromStdString(_model->getCurrentName()));
}
}
void WaypointEditorDialog::on_noDrawLinesCheck_toggled(bool checked)
{
_model->setNoDrawLines(checked);
- _model->apply();
}
void WaypointEditorDialog::on_customColorCheck_toggled(bool checked)
{
_model->setHasCustomColor(checked);
- ui->colorRSpinBox->setEnabled(checked);
- ui->colorGSpinBox->setEnabled(checked);
- ui->colorBSpinBox->setEnabled(checked);
+ const bool colorEnabled = checked;
+ ui->colorRSpinBox->setEnabled(colorEnabled);
+ ui->colorGSpinBox->setEnabled(colorEnabled);
+ ui->colorBSpinBox->setEnabled(colorEnabled);
updateColorSwatch();
- _model->apply();
}
void WaypointEditorDialog::on_colorRSpinBox_valueChanged(int value)
{
_model->setColorR(value);
updateColorSwatch();
- _model->apply();
}
void WaypointEditorDialog::on_colorGSpinBox_valueChanged(int value)
{
_model->setColorG(value);
updateColorSwatch();
- _model->apply();
}
void WaypointEditorDialog::on_colorBSpinBox_valueChanged(int value)
{
_model->setColorB(value);
updateColorSwatch();
- _model->apply();
}
void WaypointEditorDialog::on_layerCombo_currentIndexChanged(int index)
diff --git a/qtfred/src/ui/dialogs/WaypointEditorDialog.h b/qtfred/src/ui/dialogs/WaypointEditorDialog.h
index d7d9f9b49f9..f0221ac16ea 100644
--- a/qtfred/src/ui/dialogs/WaypointEditorDialog.h
+++ b/qtfred/src/ui/dialogs/WaypointEditorDialog.h
@@ -16,7 +16,8 @@ class WaypointEditorDialog : public QDialog {
~WaypointEditorDialog() override;
private slots:
- void on_pathSelection_currentIndexChanged(int index);
+ void on_prevPathButton_clicked();
+ void on_nextPathButton_clicked();
void on_nameEdit_editingFinished();
void on_noDrawLinesCheck_toggled(bool checked);
void on_customColorCheck_toggled(bool checked);
@@ -31,10 +32,8 @@ private slots:
std::unique_ptr _model;
void initializeUi();
- void updateWaypointListComboBox();
void updateUi();
void updateColorSwatch();
};
} // namespace fso::fred::dialogs
-
diff --git a/qtfred/ui/WaypointEditorDialog.ui b/qtfred/ui/WaypointEditorDialog.ui
index 703e8663d04..6476e65adae 100644
--- a/qtfred/ui/WaypointEditorDialog.ui
+++ b/qtfred/ui/WaypointEditorDialog.ui
@@ -24,25 +24,23 @@
Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
- -
-
-
- Wa&ypoint Path
-
-
- pathSelection
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
+
-
+
+
-
+
+
+ &Prev
+
+
+
+ -
+
+
+ &Next
+
+
+
+
-
@@ -60,6 +58,23 @@
-
+ -
+
+
+ Layer
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
-
@@ -136,23 +151,6 @@
- -
-
-
- Layer
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
-
From 1b5dd36cb1e668433804b48acedbfe6185117a9d Mon Sep 17 00:00:00 2001
From: Mike Nelson
Date: Sat, 2 May 2026 14:55:35 -0500
Subject: [PATCH 2/3] clang
---
qtfred/src/mission/dialogs/WaypointEditorDialogModel.cpp | 2 +-
qtfred/src/mission/dialogs/WaypointEditorDialogModel.h | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/qtfred/src/mission/dialogs/WaypointEditorDialogModel.cpp b/qtfred/src/mission/dialogs/WaypointEditorDialogModel.cpp
index 4971409b70e..d613aa7ece3 100644
--- a/qtfred/src/mission/dialogs/WaypointEditorDialogModel.cpp
+++ b/qtfred/src/mission/dialogs/WaypointEditorDialogModel.cpp
@@ -71,7 +71,7 @@ bool WaypointEditorDialogModel::hasMultipleSelection() const {
return _selectedWaypointPaths.size() > 1;
}
-bool WaypointEditorDialogModel::hasAnyPathsInMission() const {
+bool WaypointEditorDialogModel::hasAnyPathsInMission() {
return !Waypoint_lists.empty();
}
diff --git a/qtfred/src/mission/dialogs/WaypointEditorDialogModel.h b/qtfred/src/mission/dialogs/WaypointEditorDialogModel.h
index e90db022508..70b11535e31 100644
--- a/qtfred/src/mission/dialogs/WaypointEditorDialogModel.h
+++ b/qtfred/src/mission/dialogs/WaypointEditorDialogModel.h
@@ -14,7 +14,7 @@ class WaypointEditorDialogModel : public AbstractDialogModel {
bool hasValidSelection() const;
bool hasMultipleSelection() const;
- bool hasAnyPathsInMission() const;
+ static bool hasAnyPathsInMission();
int getSelectionCount() const;
const SCP_string& getCurrentName() const;
@@ -45,7 +45,7 @@ private slots:
void onSelectedObjectMarkingChanged(int, bool);
void onMissionChanged();
-private:
+private: // NOLINT(readability-redundant-access-specifiers)
void initializeData();
void showErrorDialogNoCancel(const SCP_string& message);
bool validateName(const SCP_string& name);
From 3bded23f860e666a89cfbf38e33755c7cd9c75ba Mon Sep 17 00:00:00 2001
From: Mike Nelson
Date: Mon, 4 May 2026 09:08:33 -0500
Subject: [PATCH 3/3] update qtfred help docs
---
.../doc/dialogs/WaypointEditorDialog.html | 24 ++++++++++++++-----
1 file changed, 18 insertions(+), 6 deletions(-)
diff --git a/qtfred/help-src/doc/dialogs/WaypointEditorDialog.html b/qtfred/help-src/doc/dialogs/WaypointEditorDialog.html
index 32ec880f2e5..d34410632b0 100644
--- a/qtfred/help-src/doc/dialogs/WaypointEditorDialog.html
+++ b/qtfred/help-src/doc/dialogs/WaypointEditorDialog.html
@@ -14,20 +14,32 @@ Waypoint Editor
Waypoint paths are simply ordered sequences of arbitrary positions in space and can
be used for any purpose a mission designer can express through SEXPs and AI goals.
+The dialog tracks the viewport selection. Select waypoint objects in the viewport
+to edit their path. When multiple paths are selected, display property changes
+(No Draw Lines, Custom Color, Layer) apply to all selected paths at once.
+
+Navigation
+
+ | Button | Description |
+ | Prev / Next | Cycles the viewport selection to the previous or next
+ waypoint path in the mission, allowing sequential editing without switching
+ back to the viewport. |
+
+
Key fields
| Field | Description |
- | Waypoint path | Selects which path to edit. New paths are created by
- placing waypoint objects in the viewport. |
| Name | Identifies the path in SEXPs and AI goal assignments. Must be
- unique. |
- | Layer | The layer the waypoint path is assigned to. |
+ unique. When multiple paths are selected, shows the name of the first selected
+ path and renames only that path.
+ | Layer | The layer the waypoint path is assigned to. Applied to all
+ selected paths. |
| No Draw Lines | When checked, the connecting lines between waypoints
are hidden in the viewport. The waypoint points themselves are still
- visible. |
+ visible. Applied to all selected paths.
| Custom Color | When checked, enables the RGB fields below to set a
custom color for rendering this path's points and lines in the viewport.
- Does not affect in-game appearance. |
+ Does not affect in-game appearance. Applied to all selected paths.
Individual waypoint positions are moved by selecting the waypoint