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
+
+ | Button | Description |
+ | Prev / Next | Cycles the viewport selection to the previous or next
+ jump node in the mission, allowing sequential editing without switching back
+ to the viewport. |
+
Key fields
| Field | Description |
- | Jump node | Selects which placed node to edit. |
| Name | Internal identifier used in SEXPs and mission scripting.
- Must be unique. |
- | Layer | The layer the jump node is assigned to. |
+ Must be unique. When multiple nodes are selected, shows the name of the first
+ selected node and renames only that node.
+ | Layer | The layer the jump node is assigned to. Applied to all
+ selected nodes. |
| Display name | Name shown to the player on the HUD and in targeting.
- If left blank, the internal name is used instead. |
+ If left blank, the internal name is used instead. When multiple nodes are
+ selected, applies only to the first selected node.
| Model file | POF model rendered at the node's position. Uses the
- default jump node model if left blank. |
+ default jump node model if left blank. When multiple nodes are selected,
+ applies only to the first selected node.
| Node color (RGB) | Color used to render the node model in the
- mission. |
- | Hidden by default | When checked, the node starts the mission hidden
- |
+ mission. The color swatch next to the fields shows a live preview of the
+ current color. Applied to all selected nodes.
+ | Hidden by default | When checked, the node starts the mission hidden.
+ Applied to all selected nodes. |