From 20cb40daaa8cd21ded4cd2617a77f272bca3a98a Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Sat, 2 May 2026 14:22:40 -0500 Subject: [PATCH 1/3] make jump node multi select and direct edit --- .../dialogs/JumpNodeEditorDialogModel.cpp | 490 +++++++++--------- .../dialogs/JumpNodeEditorDialogModel.h | 33 +- qtfred/src/ui/FredView.cpp | 2 +- .../src/ui/dialogs/JumpNodeEditorDialog.cpp | 93 ++-- qtfred/src/ui/dialogs/JumpNodeEditorDialog.h | 12 +- qtfred/ui/JumpNodeEditorDialog.ui | 39 +- 6 files changed, 362 insertions(+), 307 deletions(-) diff --git a/qtfred/src/mission/dialogs/JumpNodeEditorDialogModel.cpp b/qtfred/src/mission/dialogs/JumpNodeEditorDialogModel.cpp index 926c2f4b74c..2bc8014e33b 100644 --- a/qtfred/src/mission/dialogs/JumpNodeEditorDialogModel.cpp +++ b/qtfred/src/mission/dialogs/JumpNodeEditorDialogModel.cpp @@ -16,70 +16,36 @@ JumpNodeEditorDialogModel::JumpNodeEditorDialogModel(QObject* parent, EditorView connect(viewport->editor, &Editor::currentObjectChanged, this, &JumpNodeEditorDialogModel::onSelectedObjectChanged); connect(viewport->editor, &Editor::objectMarkingChanged, this, &JumpNodeEditorDialogModel::onSelectedObjectMarkingChanged); connect(viewport->editor, &Editor::missionChanged, this, &JumpNodeEditorDialogModel::onMissionChanged); - + initializeData(); } -bool JumpNodeEditorDialogModel::apply() -{ - if (_currentlySelectedNodeIndex < 0) { - // Nothing to apply - return true; - } - - // Validate - if (!validateData()) { - return false; - } - - // Commit - auto* jnp = jumpnode_get_by_objnum(getSelectedJumpNodeObjnum(_currentlySelectedNodeIndex)); - Assertion(jnp != nullptr, "Jump node not found during apply!"); - - char old_name_buf[NAME_LENGTH]; - std::strncpy(old_name_buf, jnp->GetName(), NAME_LENGTH - 1); - old_name_buf[NAME_LENGTH - 1] = '\0'; +bool JumpNodeEditorDialogModel::apply() { + return true; +} - lcl_fred_replace_stuff(_display); +void JumpNodeEditorDialogModel::reject() {} - jnp->SetName(_name.c_str()); - jnp->SetDisplayName(lcase_equal(_display, "") ? _name.c_str() : _display.c_str()); +void JumpNodeEditorDialogModel::initializeData() +{ + _selectedJumpNodes.clear(); - // Only set a non default model - if (!lcase_equal(_modelFilename, JN_DEFAULT_MODEL)) { - jnp->SetModel(_modelFilename.c_str()); + // Collect all marked OBJ_JUMP_NODE objects + for (auto* ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) { + if (ptr->type == OBJ_JUMP_NODE && ptr->flags[Object::Object_Flags::Marked]) { + _selectedJumpNodes.push_back(OBJ_INDEX(ptr)); + } } - jnp->SetAlphaColor(_red, _green, _blue, _alpha); - jnp->SetVisibility(!_hidden); - - // Update sexp references when name changes - if (strcmp(old_name_buf, _name.c_str()) != 0) { - update_sexp_references(old_name_buf, _name.c_str()); + // Fall back to currentObject if nothing is marked + if (_selectedJumpNodes.empty() && query_valid_object(_editor->currentObject) && + Objects[_editor->currentObject].type == OBJ_JUMP_NODE) { + _selectedJumpNodes.push_back(_editor->currentObject); } - _editor->missionChanged(); - return true; -} - -void JumpNodeEditorDialogModel::reject() -{ - // do nothing -} - -void JumpNodeEditorDialogModel::initializeData() -{ - buildNodeList(); - - // Find the currently selected object if it's a jump node - int objnum = -1; - if (query_valid_object(_editor->currentObject) && Objects[_editor->currentObject].type == OBJ_JUMP_NODE) { - objnum = _editor->currentObject; - } - - if (objnum >= 0) { - auto* jnp = jumpnode_get_by_objnum(objnum); - Assertion(jnp != nullptr, "Jump node not found for current object!"); + if (!_selectedJumpNodes.empty()) { + auto* jnp = jumpnode_get_by_objnum(_selectedJumpNodes.front()); + Assertion(jnp != nullptr, "Jump node not found for selected object!"); _name = jnp->GetName(); _display = jnp->HasDisplayName() ? jnp->GetDisplayName() : ""; @@ -88,7 +54,7 @@ void JumpNodeEditorDialogModel::initializeData() if (auto* pm = model_get(model_num)) { _modelFilename = pm->filename; } else { - _modelFilename.clear(); + _modelFilename = JN_DEFAULT_MODEL; } const auto& c = jnp->GetColor(); @@ -99,12 +65,27 @@ void JumpNodeEditorDialogModel::initializeData() _hidden = jnp->IsHidden(); - // Find the index of the jump node in the local list - for (const auto& node : _nodes) { - if (!stricmp(node.first.c_str(), _name.c_str())) { - _currentlySelectedNodeIndex = node.second; - break; + if (hasMultipleSelection()) { + bool displayConsistent = true; + bool modelConsistent = true; + for (size_t i = 1; i < _selectedJumpNodes.size(); ++i) { + auto* other = jumpnode_get_by_objnum(_selectedJumpNodes[i]); + if (!other) continue; + + SCP_string otherDisplay = other->HasDisplayName() ? other->GetDisplayName() : ""; + if (_display != otherDisplay) + displayConsistent = false; + + SCP_string otherModel; + if (auto* pm = model_get(other->GetModelNumber())) + otherModel = pm->filename; + else + otherModel = JN_DEFAULT_MODEL; + if (_modelFilename != otherModel) + modelConsistent = false; } + if (!displayConsistent) _display.clear(); + if (!modelConsistent) _modelFilename.clear(); } } else { _name.clear(); @@ -112,42 +93,47 @@ void JumpNodeEditorDialogModel::initializeData() _modelFilename.clear(); _red = _green = _blue = _alpha = 0; _hidden = false; - - _currentlySelectedNodeIndex = -1; } Q_EMIT jumpNodeMarkingChanged(); _modified = false; } -void JumpNodeEditorDialogModel::buildNodeList() -{ - _nodes.clear(); - int idx = 0; - for (auto& node : Jump_nodes) { - _nodes.emplace_back(node.GetName(), idx++); - } +bool JumpNodeEditorDialogModel::hasValidSelection() const { + return !_selectedJumpNodes.empty(); } -bool JumpNodeEditorDialogModel::validateData() -{ - _bypass_errors = false; +bool JumpNodeEditorDialogModel::hasMultipleSelection() const { + return _selectedJumpNodes.size() > 1; +} + +bool JumpNodeEditorDialogModel::hasAnyNodesInMission() const { + return !Jump_nodes.empty(); +} - SCP_trim(_name); +int JumpNodeEditorDialogModel::getSelectionCount() const { + return static_cast(_selectedJumpNodes.size()); +} - const SCP_string name = _name; +void JumpNodeEditorDialogModel::showErrorDialogNoCancel(const SCP_string& message) { + if (_bypass_errors) { + return; + } + _bypass_errors = true; + _viewport->dialogProvider->showButtonDialog(DialogType::Error, "Error", message, {DialogButton::Ok}); +} + +bool JumpNodeEditorDialogModel::validateName(const SCP_string& name) { if (name.empty()) { showErrorDialogNoCancel("A jump node name cannot be empty."); return false; } - // Disallow leading '<' - if (!name.empty() && name[0] == '<') { + if (name[0] == '<') { showErrorDialogNoCancel("Jump node names are not allowed to begin with '<'."); return false; } - // Wing name collision for (auto& wing : Wings) { if (!stricmp(wing.name, name.c_str())) { showErrorDialogNoCancel("This jump node name is already being used by a wing."); @@ -155,7 +141,6 @@ bool JumpNodeEditorDialogModel::validateData() } } - // Ship/start name collision 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(name.c_str(), Ships[ptr->instance].ship_name)) { @@ -165,7 +150,6 @@ bool JumpNodeEditorDialogModel::validateData() } } - // AI target priority group collision for (auto& ai : Ai_tp_list) { if (!stricmp(name.c_str(), ai.name)) { showErrorDialogNoCancel("This jump node name is already being used by a target priority group."); @@ -173,230 +157,268 @@ bool JumpNodeEditorDialogModel::validateData() } } - // Waypoint path collision if (find_matching_waypoint_list(name.c_str()) != nullptr) { showErrorDialogNoCancel("This jump node name is already being used by a waypoint path."); return false; } - // Another jump node with the same name (but not this one) - auto* jnp = jumpnode_get_by_objnum(getSelectedJumpNodeObjnum(_currentlySelectedNodeIndex)); + auto* current_jnp = _selectedJumpNodes.empty() ? nullptr : jumpnode_get_by_objnum(_selectedJumpNodes.front()); auto* found = jumpnode_get_by_name(name.c_str()); - if (found != nullptr && found != jnp) { + if (found != nullptr && found != current_jnp) { showErrorDialogNoCancel("This jump node name is already being used by another jump node."); return false; } - if (!cf_exists_full(_modelFilename.c_str(), CF_TYPE_MODELS)) { - showErrorDialogNoCancel("This jump node model file does not exist."); - return false; - } - return true; } -void JumpNodeEditorDialogModel::showErrorDialogNoCancel(const SCP_string& message) -{ - if (_bypass_errors) { - return; +bool JumpNodeEditorDialogModel::setName(const SCP_string& v) { + if (hasMultipleSelection() || _selectedJumpNodes.empty()) { + return true; } - _bypass_errors = true; - _viewport->dialogProvider->showButtonDialog(DialogType::Error, "Error", message, {DialogButton::Ok}); -} + _bypass_errors = false; -int JumpNodeEditorDialogModel::getSelectedJumpNodeObjnum(int idx) const -{ - // Find the jump node and then mark it - for (const auto& node : Jump_nodes) { - if (!stricmp(node.GetName(), _nodes[idx].first.c_str())) { - return node.GetSCPObjectNumber(); - } + SCP_string trimmed = v; + SCP_trim(trimmed); + + if (!validateName(trimmed)) { + return false; } - return -1; -} + auto* jnp = jumpnode_get_by_objnum(_selectedJumpNodes.front()); + if (!jnp) { + return false; + } -void JumpNodeEditorDialogModel::onSelectedObjectChanged(int) -{ - initializeData(); -} + char old_name[NAME_LENGTH]; + std::strncpy(old_name, jnp->GetName(), NAME_LENGTH - 1); + old_name[NAME_LENGTH - 1] = '\0'; -void JumpNodeEditorDialogModel::onSelectedObjectMarkingChanged(int, bool) -{ - initializeData(); -} + jnp->SetName(trimmed.c_str()); -void JumpNodeEditorDialogModel::onMissionChanged() -{ - initializeData(); -} + if (strcmp(old_name, trimmed.c_str()) != 0) { + update_sexp_references(old_name, trimmed.c_str()); + } -const SCP_vector>& JumpNodeEditorDialogModel::getJumpNodeList() const -{ - return _nodes; + _name = trimmed; + set_modified(); + _editor->missionChanged(); + return true; } -void JumpNodeEditorDialogModel::selectJumpNodeByListIndex(int idx) -{ - if (_currentlySelectedNodeIndex == idx) { - // No change - return; +const SCP_string& JumpNodeEditorDialogModel::getName() const { return _name; } + +bool JumpNodeEditorDialogModel::setDisplayName(const SCP_string& v) { + if (_selectedJumpNodes.empty() || v.empty()) { + return true; } - if (!SCP_vector_inbounds(_nodes, idx)) - return; + SCP_string display = v; + lcl_fred_replace_stuff(display); + const bool useNodeName = lcase_equal(display, ""); - if (apply()) { - _editor->unmark_all(); + for (auto objnum : _selectedJumpNodes) { + auto* jnp = jumpnode_get_by_objnum(objnum); + if (!jnp) continue; + jnp->SetDisplayName(useNodeName ? jnp->GetName() : display.c_str()); + } - int objnum = getSelectedJumpNodeObjnum(idx); + _display = useNodeName ? "" : display; + set_modified(); + _editor->missionChanged(); + return true; +} - if (objnum < 0) { - _currentlySelectedNodeIndex = -1; - return; - } +const SCP_string& JumpNodeEditorDialogModel::getDisplayName() const { return _display; } - _editor->markObject(objnum); - _currentlySelectedNodeIndex = idx; +bool JumpNodeEditorDialogModel::setModelFilename(const SCP_string& v) { + if (_selectedJumpNodes.empty() || v.empty()) { + return true; } -} -int JumpNodeEditorDialogModel::getCurrentJumpNodeIndex() const -{ - return _currentlySelectedNodeIndex; -} -bool JumpNodeEditorDialogModel::hasValidSelection() const -{ - return _currentlySelectedNodeIndex >= 0; -} + if (!lcase_equal(v, JN_DEFAULT_MODEL) && !cf_exists_full(v.c_str(), CF_TYPE_MODELS)) { + showErrorDialogNoCancel("This jump node model file does not exist."); + return false; + } -void JumpNodeEditorDialogModel::setName(const SCP_string& v) -{ - SCP_trim(_name); - - SCP_string current = _name; + _modelFilename = v; + const bool useDefault = lcase_equal(_modelFilename, JN_DEFAULT_MODEL); - _name = v; - if (apply()) { - set_modified(); - } else { - _name = current; // restore the old name + for (auto objnum : _selectedJumpNodes) { + auto* jnp = jumpnode_get_by_objnum(objnum); + if (!jnp) continue; + if (!useDefault) { + jnp->SetModel(_modelFilename.c_str()); + } } -} -const SCP_string& JumpNodeEditorDialogModel::getName() const -{ - return _name; + set_modified(); + _editor->missionChanged(); + return true; } -void JumpNodeEditorDialogModel::setDisplayName(const SCP_string& v) -{ - modify(_display, v); - apply(); // Apply changes immediately to update the display name -} +const SCP_string& JumpNodeEditorDialogModel::getModelFilename() const { return _modelFilename; } -const SCP_string& JumpNodeEditorDialogModel::getDisplayName() const -{ - return _display; +void JumpNodeEditorDialogModel::setColorR(int v) { + CLAMP(v, 0, 255); + _red = v; + for (auto objnum : _selectedJumpNodes) { + auto* jnp = jumpnode_get_by_objnum(objnum); + if (jnp) { + jnp->SetAlphaColor(_red, _green, _blue, _alpha); + } + } + set_modified(); + _editor->missionChanged(); } -void JumpNodeEditorDialogModel::setModelFilename(const SCP_string& v) -{ - SCP_string current = _modelFilename; +int JumpNodeEditorDialogModel::getColorR() const { return _red; } - _modelFilename = v; - if (apply()) { - set_modified(); - } else { - _modelFilename = current; // restore the old name +void JumpNodeEditorDialogModel::setColorG(int v) { + CLAMP(v, 0, 255); + _green = v; + for (auto objnum : _selectedJumpNodes) { + auto* jnp = jumpnode_get_by_objnum(objnum); + if (jnp) { + jnp->SetAlphaColor(_red, _green, _blue, _alpha); + } } + set_modified(); + _editor->missionChanged(); } -const SCP_string& JumpNodeEditorDialogModel::getModelFilename() const -{ - return _modelFilename; -} +int JumpNodeEditorDialogModel::getColorG() const { return _green; } -void JumpNodeEditorDialogModel::setColorR(int v) -{ +void JumpNodeEditorDialogModel::setColorB(int v) { CLAMP(v, 0, 255); - modify(_red, v); - apply(); // Apply changes immediately to update the model color + _blue = v; + for (auto objnum : _selectedJumpNodes) { + auto* jnp = jumpnode_get_by_objnum(objnum); + if (jnp) { + jnp->SetAlphaColor(_red, _green, _blue, _alpha); + } + } + set_modified(); + _editor->missionChanged(); } -int JumpNodeEditorDialogModel::getColorR() const -{ - return _red; -} +int JumpNodeEditorDialogModel::getColorB() const { return _blue; } -void JumpNodeEditorDialogModel::setColorG(int v) -{ +void JumpNodeEditorDialogModel::setColorA(int v) { CLAMP(v, 0, 255); - modify(_green, v); - apply(); // Apply changes immediately to update the model color + _alpha = v; + for (auto objnum : _selectedJumpNodes) { + auto* jnp = jumpnode_get_by_objnum(objnum); + if (jnp) { + jnp->SetAlphaColor(_red, _green, _blue, _alpha); + } + } + set_modified(); + _editor->missionChanged(); } -int JumpNodeEditorDialogModel::getColorG() const -{ - return _green; +int JumpNodeEditorDialogModel::getColorA() const { return _alpha; } + +void JumpNodeEditorDialogModel::setHidden(bool v) { + _hidden = v; + for (auto objnum : _selectedJumpNodes) { + auto* jnp = jumpnode_get_by_objnum(objnum); + if (jnp) { + jnp->SetVisibility(!v); + } + } + set_modified(); + _editor->missionChanged(); } -void JumpNodeEditorDialogModel::setColorB(int v) -{ - CLAMP(v, 0, 255); - modify(_blue, v); - apply(); // Apply changes immediately to update the model color +bool JumpNodeEditorDialogModel::getHidden() const { return _hidden; } + +SCP_string JumpNodeEditorDialogModel::getLayer() const { + SCP_string result; + bool first = true; + for (auto objnum : _selectedJumpNodes) { + SCP_string layer = _viewport->getObjectLayerName(objnum); + if (first) { + result = layer; + first = false; + } else if (result != layer) { + return ""; + } + } + return result; } -int JumpNodeEditorDialogModel::getColorB() const -{ - return _blue; +void JumpNodeEditorDialogModel::setLayer(const SCP_string& v) { + for (auto objnum : _selectedJumpNodes) { + _viewport->moveObjectToLayer(objnum, v); + } + set_modified(); + _editor->missionChanged(); } -void JumpNodeEditorDialogModel::setColorA(int v) -{ - CLAMP(v, 0, 255); - modify(_alpha, v); - apply(); // Apply changes immediately to update the model color +void JumpNodeEditorDialogModel::selectNodeFromObjectList(object* start, bool forward) { + auto* ptr = start; + while (ptr != END_OF_LIST(&obj_used_list)) { + if (ptr->type == OBJ_JUMP_NODE) { + _editor->unmark_all(); + _editor->markObject(OBJ_INDEX(ptr)); + return; + } + ptr = forward ? GET_NEXT(ptr) : GET_PREV(ptr); + } + // Wrap around + ptr = forward ? GET_FIRST(&obj_used_list) : GET_LAST(&obj_used_list); + while (ptr != END_OF_LIST(&obj_used_list)) { + if (ptr->type == OBJ_JUMP_NODE) { + _editor->unmark_all(); + _editor->markObject(OBJ_INDEX(ptr)); + return; + } + ptr = forward ? GET_NEXT(ptr) : GET_PREV(ptr); + } } -int JumpNodeEditorDialogModel::getColorA() const -{ - return _alpha; +void JumpNodeEditorDialogModel::selectFirstNodeInMission() { + for (auto* ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) { + if (ptr->type == OBJ_JUMP_NODE) { + _editor->unmark_all(); + _editor->markObject(OBJ_INDEX(ptr)); + return; + } + } } -void JumpNodeEditorDialogModel::setHidden(bool v) -{ - modify(_hidden, v); - apply(); // Apply changes immediately to update the visibility +void JumpNodeEditorDialogModel::selectNextNode() { + if (!hasValidSelection()) { + if (hasAnyNodesInMission()) { + selectFirstNodeInMission(); + } + return; + } + selectNodeFromObjectList(GET_NEXT(&Objects[_selectedJumpNodes.front()]), true); } -bool JumpNodeEditorDialogModel::getHidden() const -{ - return _hidden; +void JumpNodeEditorDialogModel::selectPreviousNode() { + if (!hasValidSelection()) { + if (hasAnyNodesInMission()) { + selectFirstNodeInMission(); + } + return; + } + selectNodeFromObjectList(GET_PREV(&Objects[_selectedJumpNodes.front()]), false); } -SCP_string JumpNodeEditorDialogModel::getLayer() const -{ - if (_currentlySelectedNodeIndex < 0) - return ""; - int objnum = getSelectedJumpNodeObjnum(_currentlySelectedNodeIndex); - if (objnum < 0) - return ""; - return _viewport->getObjectLayerName(objnum); +void JumpNodeEditorDialogModel::onSelectedObjectChanged(int) { + initializeData(); } -void JumpNodeEditorDialogModel::setLayer(const SCP_string& v) -{ - if (_currentlySelectedNodeIndex < 0) - return; - int objnum = getSelectedJumpNodeObjnum(_currentlySelectedNodeIndex); - if (objnum < 0) - return; - _viewport->moveObjectToLayer(objnum, v); - set_modified(); - _editor->missionChanged(); +void JumpNodeEditorDialogModel::onSelectedObjectMarkingChanged(int, bool) { + initializeData(); +} + +void JumpNodeEditorDialogModel::onMissionChanged() { + initializeData(); } } // namespace fso::fred::dialogs diff --git a/qtfred/src/mission/dialogs/JumpNodeEditorDialogModel.h b/qtfred/src/mission/dialogs/JumpNodeEditorDialogModel.h index bfb171886a2..2fbe0ae0987 100644 --- a/qtfred/src/mission/dialogs/JumpNodeEditorDialogModel.h +++ b/qtfred/src/mission/dialogs/JumpNodeEditorDialogModel.h @@ -5,22 +5,22 @@ namespace fso::fred::dialogs { class JumpNodeEditorDialogModel : public AbstractDialogModel { Q_OBJECT - public: +public: explicit JumpNodeEditorDialogModel(QObject* parent, EditorViewport* viewport); bool apply() override; void reject() override; - const SCP_vector>& getJumpNodeList() const; - void selectJumpNodeByListIndex(int idx); - int getCurrentJumpNodeIndex() const; bool hasValidSelection() const; + bool hasMultipleSelection() const; + bool hasAnyNodesInMission() const; + int getSelectionCount() const; - void setName(const SCP_string& v); + bool setName(const SCP_string& v); const SCP_string& getName() const; - void setDisplayName(const SCP_string& v); // "" means use Name + bool setDisplayName(const SCP_string& v); // "" means use Name const SCP_string& getDisplayName() const; - void setModelFilename(const SCP_string& v); + bool setModelFilename(const SCP_string& v); const SCP_string& getModelFilename() const; void setColorR(int v); @@ -38,22 +38,25 @@ class JumpNodeEditorDialogModel : public AbstractDialogModel { SCP_string getLayer() const; void setLayer(const SCP_string& v); - signals: + void selectNextNode(); + void selectPreviousNode(); + +signals: void jumpNodeMarkingChanged(); - private slots: +private slots: void onSelectedObjectChanged(int); void onSelectedObjectMarkingChanged(int, bool); void onMissionChanged(); - private: // NOLINT(readability-redundant-access-specifiers) +private: void initializeData(); - void buildNodeList(); - bool validateData(); void showErrorDialogNoCancel(const SCP_string& message); - int getSelectedJumpNodeObjnum(int idx) const; + bool validateName(const SCP_string& name); + void selectNodeFromObjectList(object* start, bool forward); + void selectFirstNodeInMission(); - int _currentlySelectedNodeIndex = -1; + SCP_vector _selectedJumpNodes; // objnums of selected jump nodes SCP_string _name; SCP_string _display; @@ -61,8 +64,6 @@ class JumpNodeEditorDialogModel : public AbstractDialogModel { int _red = 0, _green = 0, _blue = 0, _alpha = 0; bool _hidden = false; - SCP_vector> _nodes; - bool _bypass_errors = false; }; diff --git a/qtfred/src/ui/FredView.cpp b/qtfred/src/ui/FredView.cpp index c186c79737d..0a9a666255d 100644 --- a/qtfred/src/ui/FredView.cpp +++ b/qtfred/src/ui/FredView.cpp @@ -961,7 +961,7 @@ void FredView::onUpdateContextToolbar() { } } else if (effectiveType == OBJ_WAYPOINT && (numMarked <= 1 || multiSharedWaypointList != nullptr)) { addBtn(tr("Edit Waypoint Path"), &FredView::on_actionWaypoint_Paths_triggered); - } else if (numMarked <= 1 && effectiveType == OBJ_JUMP_NODE) { + } else if (effectiveType == OBJ_JUMP_NODE) { addBtn(tr("Edit Jump Node"), &FredView::on_actionJump_Nodes_triggered); } else if (effectiveType == OBJ_PROP) { addBtn(tr("Edit Prop"), &FredView::on_actionProps_triggered); diff --git a/qtfred/src/ui/dialogs/JumpNodeEditorDialog.cpp b/qtfred/src/ui/dialogs/JumpNodeEditorDialog.cpp index c32ad94967f..f2492479ac5 100644 --- a/qtfred/src/ui/dialogs/JumpNodeEditorDialog.cpp +++ b/qtfred/src/ui/dialogs/JumpNodeEditorDialog.cpp @@ -36,21 +36,32 @@ void JumpNodeEditorDialog::initializeUi() { util::SignalBlockers blockers(this); - updateJumpNodeListComboBox(); - enableOrDisableControls(); -} - -void JumpNodeEditorDialog::updateJumpNodeListComboBox() -{ - ui->selectJumpNodeComboBox->clear(); - - for (auto& wp : _model->getJumpNodeList()) { - ui->selectJumpNodeComboBox->addItem(QString::fromStdString(wp.first), wp.second); + ui->layerCombo->clear(); + for (const auto& name : _viewport->getLayerNames()) { + ui->layerCombo->addItem(QString::fromStdString(name), QString::fromStdString(name)); } - ui->selectJumpNodeComboBox->setEnabled(!_model->getJumpNodeList().empty()); - - ui->selectJumpNodeComboBox->setCurrentIndex(_model->getCurrentJumpNodeIndex()); + const bool enabled = _model->hasValidSelection(); + const bool hasAny = _model->hasAnyNodesInMission(); + const bool multiSelect = _model->hasMultipleSelection(); + + ui->nameLineEdit->setEnabled(enabled && !multiSelect); + ui->displayNameLineEdit->setEnabled(enabled); + ui->modelFileLineEdit->setEnabled(enabled); + ui->redSpinBox->setEnabled(enabled); + ui->greenSpinBox->setEnabled(enabled); + ui->blueSpinBox->setEnabled(enabled); + ui->alphaSpinBox->setEnabled(enabled); + ui->hiddenByDefaultCheckBox->setEnabled(enabled); + ui->layerCombo->setEnabled(enabled); + ui->prevNodeButton->setEnabled(hasAny); + ui->nextNodeButton->setEnabled(hasAny); + + if (multiSelect) { + setWindowTitle(QString("Edit %1 Jump Nodes").arg(_model->getSelectionCount())); + } else { + setWindowTitle("Jump Node Editor"); + } } void JumpNodeEditorDialog::updateUi() @@ -68,69 +79,77 @@ void JumpNodeEditorDialog::updateUi() ui->hiddenByDefaultCheckBox->setChecked(_model->getHidden()); - ui->layerCombo->clear(); - for (const auto& name : _viewport->getLayerNames()) { - ui->layerCombo->addItem(QString::fromStdString(name), QString::fromStdString(name)); - } ui->layerCombo->setCurrentIndex(ui->layerCombo->findData(QString::fromStdString(_model->getLayer()))); + + updateColorSwatch(); +} + +void JumpNodeEditorDialog::updateColorSwatch() +{ + ui->colorSwatch->setStyleSheet(QString("background: rgba(%1,%2,%3,%4);" + "border: 1px solid #444; border-radius: 3px;") + .arg(_model->getColorR()) + .arg(_model->getColorG()) + .arg(_model->getColorB()) + .arg(_model->getColorA())); } -void JumpNodeEditorDialog::enableOrDisableControls() +void JumpNodeEditorDialog::on_prevNodeButton_clicked() { - const bool enable = _model->hasValidSelection(); - - ui->nameLineEdit->setEnabled(enable); - ui->displayNameLineEdit->setEnabled(enable); - ui->modelFileLineEdit->setEnabled(enable); - ui->redSpinBox->setEnabled(enable); - ui->greenSpinBox->setEnabled(enable); - ui->blueSpinBox->setEnabled(enable); - ui->alphaSpinBox->setEnabled(enable); - ui->hiddenByDefaultCheckBox->setEnabled(enable); - ui->layerCombo->setEnabled(enable); + _model->selectPreviousNode(); } -void JumpNodeEditorDialog::on_selectJumpNodeComboBox_currentIndexChanged(int index) +void JumpNodeEditorDialog::on_nextNodeButton_clicked() { - auto itemId = ui->selectJumpNodeComboBox->itemData(index).value(); - _model->selectJumpNodeByListIndex(itemId); + _model->selectNextNode(); } void JumpNodeEditorDialog::on_nameLineEdit_editingFinished() { - _model->setName(ui->nameLineEdit->text().toUtf8().constData()); - updateUi(); // Update immediately in case the name change is rejected + if (!_model->setName(ui->nameLineEdit->text().toUtf8().constData())) { + util::SignalBlockers blockers(this); + ui->nameLineEdit->setText(QString::fromStdString(_model->getName())); + } } void JumpNodeEditorDialog::on_displayNameLineEdit_editingFinished() { - _model->setDisplayName(ui->displayNameLineEdit->text().toUtf8().constData()); + if (!_model->setDisplayName(ui->displayNameLineEdit->text().toUtf8().constData())) { + util::SignalBlockers blockers(this); + ui->displayNameLineEdit->setText(QString::fromStdString(_model->getDisplayName())); + } } void JumpNodeEditorDialog::on_modelFileLineEdit_editingFinished() { - _model->setModelFilename(ui->modelFileLineEdit->text().toUtf8().constData()); - updateUi(); // Update immediately in case the name change is rejected + if (!_model->setModelFilename(ui->modelFileLineEdit->text().toUtf8().constData())) { + util::SignalBlockers blockers(this); + ui->modelFileLineEdit->setText(QString::fromStdString(_model->getModelFilename())); + } } void JumpNodeEditorDialog::on_redSpinBox_valueChanged(int value) { _model->setColorR(value); + updateColorSwatch(); } void JumpNodeEditorDialog::on_greenSpinBox_valueChanged(int value) { _model->setColorG(value); + updateColorSwatch(); } void JumpNodeEditorDialog::on_blueSpinBox_valueChanged(int value) { _model->setColorB(value); + updateColorSwatch(); } void JumpNodeEditorDialog::on_alphaSpinBox_valueChanged(int value) { _model->setColorA(value); + updateColorSwatch(); } void JumpNodeEditorDialog::on_hiddenByDefaultCheckBox_toggled(bool checked) diff --git a/qtfred/src/ui/dialogs/JumpNodeEditorDialog.h b/qtfred/src/ui/dialogs/JumpNodeEditorDialog.h index df9d316654e..557794255b4 100644 --- a/qtfred/src/ui/dialogs/JumpNodeEditorDialog.h +++ b/qtfred/src/ui/dialogs/JumpNodeEditorDialog.h @@ -12,12 +12,13 @@ class JumpNodeEditorDialog; class JumpNodeEditorDialog : public QDialog { Q_OBJECT - public: +public: JumpNodeEditorDialog(FredView* parent, EditorViewport* viewport); ~JumpNodeEditorDialog() override; - private slots: - void on_selectJumpNodeComboBox_currentIndexChanged(int index); +private slots: + void on_prevNodeButton_clicked(); + void on_nextNodeButton_clicked(); void on_nameLineEdit_editingFinished(); void on_displayNameLineEdit_editingFinished(); void on_modelFileLineEdit_editingFinished(); @@ -28,15 +29,14 @@ class JumpNodeEditorDialog : public QDialog { void on_hiddenByDefaultCheckBox_toggled(bool checked); void on_layerCombo_currentIndexChanged(int index); - private: // NOLINT(readability-redundant-access-specifiers) +private: // NOLINT(readability-redundant-access-specifiers) EditorViewport* _viewport; std::unique_ptr ui; std::unique_ptr _model; void initializeUi(); - void updateJumpNodeListComboBox(); void updateUi(); - void enableOrDisableControls(); + void updateColorSwatch(); }; } // namespace fso::fred::dialogs diff --git a/qtfred/ui/JumpNodeEditorDialog.ui b/qtfred/ui/JumpNodeEditorDialog.ui index 396c8947711..abd1a9672e6 100644 --- a/qtfred/ui/JumpNodeEditorDialog.ui +++ b/qtfred/ui/JumpNodeEditorDialog.ui @@ -26,26 +26,23 @@ - - - + + + - Select Jump Node + &Prev - - + + + + &Next + + - - - - Qt::Horizontal - - - @@ -191,6 +188,22 @@ + + + + + 28 + 28 + + + + QFrame::Box + + + + + + From e9f2751861d58bdf718b54e639815faa24512872 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Sat, 2 May 2026 14:56:39 -0500 Subject: [PATCH 2/3] clang --- qtfred/src/mission/dialogs/JumpNodeEditorDialogModel.cpp | 2 +- qtfred/src/mission/dialogs/JumpNodeEditorDialogModel.h | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/qtfred/src/mission/dialogs/JumpNodeEditorDialogModel.cpp b/qtfred/src/mission/dialogs/JumpNodeEditorDialogModel.cpp index 2bc8014e33b..6ccadeb80d6 100644 --- a/qtfred/src/mission/dialogs/JumpNodeEditorDialogModel.cpp +++ b/qtfred/src/mission/dialogs/JumpNodeEditorDialogModel.cpp @@ -107,7 +107,7 @@ bool JumpNodeEditorDialogModel::hasMultipleSelection() const { return _selectedJumpNodes.size() > 1; } -bool JumpNodeEditorDialogModel::hasAnyNodesInMission() const { +bool JumpNodeEditorDialogModel::hasAnyNodesInMission() { return !Jump_nodes.empty(); } diff --git a/qtfred/src/mission/dialogs/JumpNodeEditorDialogModel.h b/qtfred/src/mission/dialogs/JumpNodeEditorDialogModel.h index 2fbe0ae0987..8e5fec641f3 100644 --- a/qtfred/src/mission/dialogs/JumpNodeEditorDialogModel.h +++ b/qtfred/src/mission/dialogs/JumpNodeEditorDialogModel.h @@ -13,7 +13,7 @@ class JumpNodeEditorDialogModel : public AbstractDialogModel { bool hasValidSelection() const; bool hasMultipleSelection() const; - bool hasAnyNodesInMission() const; + static bool hasAnyNodesInMission(); int getSelectionCount() const; bool setName(const SCP_string& v); @@ -49,7 +49,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 620856de18ffddf17488cf588df38465eab70493 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Mon, 4 May 2026 09:10:16 -0500 Subject: [PATCH 3/3] update qtfred help docs --- .../doc/dialogs/JumpNodeEditorDialog.html | 36 +++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/qtfred/help-src/doc/dialogs/JumpNodeEditorDialog.html b/qtfred/help-src/doc/dialogs/JumpNodeEditorDialog.html index 6440d073845..4d6d9cd2f8f 100644 --- a/qtfred/help-src/doc/dialogs/JumpNodeEditorDialog.html +++ b/qtfred/help-src/doc/dialogs/JumpNodeEditorDialog.html @@ -10,24 +10,40 @@

Jump Node Editor

Opens via Editors › Jump Node Editor.

Creates and configures subspace jump nodes. Jump nodes are fixed points in space that -ships can use to enter or exit subspace. They appear on the HUD radar and can be targeted. -Select a node from the drop-down to edit its properties.

+ships can use to enter or exit subspace. They appear on the HUD radar and can be +targeted.

+ +

The dialog tracks the viewport selection. Select jump node objects in the viewport +to edit their properties. When multiple nodes are selected, color, layer, and hidden +state changes apply to all selected nodes at once.

+ +

Navigation

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

Key fields

- - + Must be unique. When multiple nodes are selected, shows the name of the first + selected node and renames only that node. + + If left blank, the internal name is used instead. When multiple nodes are + selected, applies only to the first selected node. + default jump node model if left blank. When multiple nodes are selected, + applies only to the first selected node. - + mission. The color swatch next to the fields shows a live preview of the + current color. Applied to all selected nodes. +
FieldDescription
Jump nodeSelects which placed node to edit.
NameInternal identifier used in SEXPs and mission scripting. - Must be unique.
LayerThe layer the jump node is assigned to.
LayerThe layer the jump node is assigned to. Applied to all + selected nodes.
Display nameName shown to the player on the HUD and in targeting. - If left blank, the internal name is used instead.
Model filePOF model rendered at the node's position. Uses the - default jump node model if left blank.
Node color (RGB)Color used to render the node model in the - mission.
Hidden by defaultWhen checked, the node starts the mission hidden -
Hidden by defaultWhen checked, the node starts the mission hidden. + Applied to all selected nodes.