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

+ + + +
ButtonDescription
Prev / NextCycles the viewport selection to the previous or next + waypoint path in the mission, allowing sequential editing without switching + back to the viewport.
+

Key fields

- - + unique. When multiple paths are selected, shows the name of the first selected + path and renames only that path. + + visible. Applied to all selected paths. + Does not affect in-game appearance. Applied to all selected paths.
FieldDescription
Waypoint pathSelects which path to edit. New paths are created by - placing waypoint objects in the viewport.
NameIdentifies the path in SEXPs and AI goal assignments. Must be - unique.
LayerThe layer the waypoint path is assigned to.
LayerThe layer the waypoint path is assigned to. Applied to all + selected paths.
No Draw LinesWhen checked, the connecting lines between waypoints are hidden in the viewport. The waypoint points themselves are still - visible.
Custom ColorWhen 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.
Individual waypoint positions are moved by selecting the waypoint