diff --git a/qtfred/help-src/doc/dialogs/WaypointEditorDialog.html b/qtfred/help-src/doc/dialogs/WaypointEditorDialog.html
index 32ec880f2e5..5ba892c31b4 100644
--- a/qtfred/help-src/doc/dialogs/WaypointEditorDialog.html
+++ b/qtfred/help-src/doc/dialogs/WaypointEditorDialog.html
@@ -14,20 +14,32 @@
Waypoint Editor
Waypoint paths are simply ordered sequences of arbitrary positions in space and can
be used for any purpose a mission designer can express through SEXPs and AI goals.
+The dialog tracks the viewport selection. Select waypoint objects in the viewport
+to edit their path. When multiple paths are selected, display property changes
+(No Draw Lines, Custom Color, Layer) apply to all selected paths at once.
+
+Navigation
+
+ | Button | Description |
+ | Prev / Next | Cycles the viewport selection to the previous or next
+ waypoint path in the mission, allowing sequential editing without switching
+ back to the viewport. |
+
+
Key fields
| Field | Description |
- | Waypoint path | Selects which path to edit. New paths are created by
- placing waypoint objects in the viewport. |
| Name | Identifies the path in SEXPs and AI goal assignments. Must be
- unique. |
- | Layer | The layer the waypoint path is assigned to. |
+ unique. When multiple paths are selected the Name field is read-only. Rename
+ single paths individually.
+ | Layer | The layer the waypoint path is assigned to. Applied to all
+ selected paths. |
| No Draw Lines | When checked, the connecting lines between waypoints
are hidden in the viewport. The waypoint points themselves are still
- visible. |
+ visible. Applied to all selected paths.
| Custom Color | When checked, enables the RGB fields below to set a
custom color for rendering this path's points and lines in the viewport.
- Does not affect in-game appearance. |
+ Does not affect in-game appearance. Applied to all selected paths.
Individual waypoint positions are moved by selecting the waypoint
diff --git a/qtfred/src/mission/dialogs/WaypointEditorDialogModel.cpp b/qtfred/src/mission/dialogs/WaypointEditorDialogModel.cpp
index 48f4b95c30d..ec053d77c4b 100644
--- a/qtfred/src/mission/dialogs/WaypointEditorDialogModel.cpp
+++ b/qtfred/src/mission/dialogs/WaypointEditorDialogModel.cpp
@@ -2,10 +2,34 @@
#include
#include
#include
+#include
#include "mission/dialogs/WaypointEditorDialogModel.h"
namespace fso::fred::dialogs {
+namespace {
+
+// Writes one or more color channels to every selected path that already uses a custom color.
+// Channels still flagged mixed retain their per-path value; non-mixed channels take the model value.
+// Skips paths without custom color so a channel edit never silently turns custom-color on.
+void applyChannelsToAllPaths(const SCP_vector& selected,
+ int r, int g, int b,
+ bool rMixed, bool gMixed, bool bMixed)
+{
+ for (auto idx : selected) {
+ auto& path = Waypoint_lists[idx];
+ if (!path.get_has_custom_color()) {
+ continue;
+ }
+ int outR = rMixed ? path.get_color_r() : r;
+ int outG = gMixed ? path.get_color_g() : g;
+ int outB = bMixed ? path.get_color_b() : b;
+ path.set_color(outR, outG, outB);
+ }
+}
+
+} // namespace
+
WaypointEditorDialogModel::WaypointEditorDialogModel(QObject* parent, EditorViewport* viewport) :
AbstractDialogModel(parent, viewport) {
connect(viewport->editor, &Editor::currentObjectChanged, this, &WaypointEditorDialogModel::onSelectedObjectChanged);
@@ -15,151 +39,138 @@ 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();
+ _noDrawLinesMixed = false;
+ _hasCustomColorMixed = false;
+ _redMixed = _greenMixed = _blueMixed = false;
- 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();
+
+ if (hasMultipleSelection()) {
+ for (size_t i = 1; i < _selectedWaypointPaths.size(); ++i) {
+ const auto& other = Waypoint_lists[_selectedWaypointPaths[i]];
+ if (other.get_no_draw_lines() != _noDrawLines)
+ _noDrawLinesMixed = true;
+ if (other.get_has_custom_color() != _hasCustomColor)
+ _hasCustomColorMixed = true;
+ if (other.get_color_r() != _colorR)
+ _redMixed = true;
+ if (other.get_color_g() != _colorG)
+ _greenMixed = true;
+ if (other.get_color_b() != _colorB)
+ _blueMixed = true;
+ }
+ }
} 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;
+bool WaypointEditorDialogModel::hasValidSelection() const {
+ return !_selectedWaypointPaths.empty();
+}
- for (size_t i = 0; i < Waypoint_lists.size(); ++i) {
- _waypointPathList.emplace_back(Waypoint_lists[i].get_name(), static_cast(i));
- }
+bool WaypointEditorDialogModel::hasMultipleSelection() const {
+ return _selectedWaypointPaths.size() > 1;
+}
- 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::hasAnyPathsInMission() {
+ return !Waypoint_lists.empty();
}
-bool WaypointEditorDialogModel::validateData()
-{
- // Reset flag before applying
- _bypass_errors = false;
+int WaypointEditorDialogModel::getSelectionCount() const {
+ return static_cast(_selectedWaypointPaths.size());
+}
- 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!");
+bool WaypointEditorDialogModel::validateName(const SCP_string& name) {
+ if (name.empty()) {
+ showErrorDialogNoCancel("Waypoint path name cannot be empty.");
+ return false;
}
// 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 = &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[0] == '<') {
showErrorDialogNoCancel("Waypoint names not allowed to begin with '<'");
return false;
}
@@ -167,132 +178,262 @@ 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();
+const SCP_string& WaypointEditorDialogModel::getCurrentName() const {
+ return _currentName;
}
-void WaypointEditorDialogModel::onSelectedObjectMarkingChanged(int, bool) {
- initializeData();
-}
+bool WaypointEditorDialogModel::setCurrentName(const SCP_string& name) {
+ if (hasMultipleSelection() || _selectedWaypointPaths.empty()) {
+ // Name field should be disabled in these cases; signal failure so the dialog
+ // restores the displayed text from getCurrentName().
+ return false;
+ }
-void WaypointEditorDialogModel::onMissionChanged()
-{
- // When the mission is changed we also need to update our data in case one of our elements changed
- initializeData();
-}
+ _bypass_errors = false;
-const SCP_string& WaypointEditorDialogModel::getCurrentName() const {
- return _currentName;
-}
+ SCP_string trimmed = name;
+ SCP_trim(trimmed);
-void WaypointEditorDialogModel::setCurrentName(const SCP_string& name)
-{
- modify(_currentName, name);
-}
+ if (!validateName(trimmed)) {
+ return false;
+ }
-int WaypointEditorDialogModel::getCurrentlySelectedPath() const {
- return _currentWaypointPathSelected;
-}
+ auto& path = Waypoint_lists[_selectedWaypointPaths.front()];
-void WaypointEditorDialogModel::setCurrentlySelectedPath(int id)
-{
- if (_currentWaypointPathSelected == id) {
- // Nothing to do here
- return;
+ 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();
+
+ 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);
+ }
}
- if (id < 0 || id >= static_cast(Waypoint_lists.size())) {
- return; // out of range; ignore
+ _currentName = new_name;
+ _suppressRefresh = true;
+ set_modified();
+ _editor->missionChanged();
+ _suppressRefresh = false;
+ return true;
+}
+
+bool WaypointEditorDialogModel::getNoDrawLines() const { return _noDrawLines; }
+
+int WaypointEditorDialogModel::getNoDrawLinesState() const {
+ if (_noDrawLinesMixed) return Qt::PartiallyChecked;
+ return _noDrawLines ? Qt::Checked : Qt::Unchecked;
+}
+
+void WaypointEditorDialogModel::setNoDrawLines(bool val) {
+ _noDrawLines = val;
+ _noDrawLinesMixed = false;
+ for (auto idx : _selectedWaypointPaths) {
+ Waypoint_lists[idx].set_no_draw_lines(val);
}
+ _suppressRefresh = true;
+ set_modified();
+ _editor->missionChanged();
+ _suppressRefresh = false;
+}
- // Only apply if there is actually a current path to save changes to.
- bool canProceed = (_editor->cur_waypoint_list == nullptr) || apply();
+bool WaypointEditorDialogModel::getHasCustomColor() const { return _hasCustomColor; }
- if (canProceed) {
- _editor->unmark_all();
+int WaypointEditorDialogModel::getHasCustomColorState() const {
+ if (_hasCustomColorMixed) return Qt::PartiallyChecked;
+ return _hasCustomColor ? Qt::Checked : Qt::Unchecked;
+}
- // 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));
- }
- }
+void WaypointEditorDialogModel::setHasCustomColor(bool val) {
+ _hasCustomColor = val;
+ _hasCustomColorMixed = false;
+ for (auto idx : _selectedWaypointPaths) {
+ auto& path = Waypoint_lists[idx];
+ if (val) {
+ // Preserve per-path channel values for channels still flagged mixed; apply
+ // the model's resolved channel values otherwise.
+ int outR = _redMixed ? path.get_color_r() : _colorR;
+ int outG = _greenMixed ? path.get_color_g() : _colorG;
+ int outB = _blueMixed ? path.get_color_b() : _colorB;
+ path.set_color(outR, outG, outB);
+ } else {
+ path.clear_color();
}
+ }
+ _suppressRefresh = true;
+ set_modified();
+ _editor->missionChanged();
+ _suppressRefresh = false;
+}
+
+int WaypointEditorDialogModel::getColorR() const { return _colorR; }
- _currentWaypointPathSelected = id;
+void WaypointEditorDialogModel::setColorR(int r) {
+ // The spinbox uses -1 as its "mixed" sentinel (minimum=-1, specialValueText=" ").
+ // A read-back of that sentinel must leave _redMixed untouched... don't "simplify"
+ // this guard by letting the value fall through to a clear.
+ if (r < 0) return;
+ CLAMP(r, 0, 255);
+ _colorR = r;
+ _redMixed = false;
+ if (_hasCustomColor && !_hasCustomColorMixed) {
+ applyChannelsToAllPaths(_selectedWaypointPaths, _colorR, _colorG, _colorB,
+ _redMixed, _greenMixed, _blueMixed);
+ _suppressRefresh = true;
+ set_modified();
+ _editor->missionChanged();
+ _suppressRefresh = false;
}
}
-bool WaypointEditorDialogModel::isEnabled() const {
- return _enabled;
+int WaypointEditorDialogModel::getColorG() const { return _colorG; }
+
+void WaypointEditorDialogModel::setColorG(int g) {
+ // -1 = spinbox "mixed" sentinel; leave _greenMixed untouched. See setColorR.
+ if (g < 0) return;
+ CLAMP(g, 0, 255);
+ _colorG = g;
+ _greenMixed = false;
+ if (_hasCustomColor && !_hasCustomColorMixed) {
+ applyChannelsToAllPaths(_selectedWaypointPaths, _colorR, _colorG, _colorB,
+ _redMixed, _greenMixed, _blueMixed);
+ _suppressRefresh = true;
+ set_modified();
+ _editor->missionChanged();
+ _suppressRefresh = false;
+ }
}
-const SCP_vector>& WaypointEditorDialogModel::getWaypointPathList() const
-{
- return _waypointPathList;
+int WaypointEditorDialogModel::getColorB() const { return _colorB; }
+
+void WaypointEditorDialogModel::setColorB(int b) {
+ // -1 = spinbox "mixed" sentinel; leave _blueMixed untouched. See setColorR.
+ if (b < 0) return;
+ CLAMP(b, 0, 255);
+ _colorB = b;
+ _blueMixed = false;
+ if (_hasCustomColor && !_hasCustomColorMixed) {
+ applyChannelsToAllPaths(_selectedWaypointPaths, _colorR, _colorG, _colorB,
+ _redMixed, _greenMixed, _blueMixed);
+ _suppressRefresh = true;
+ set_modified();
+ _editor->missionChanged();
+ _suppressRefresh = false;
+ }
}
-SCP_string WaypointEditorDialogModel::getLayer() const
-{
- if (_editor->cur_waypoint_list == nullptr)
- return "";
+bool WaypointEditorDialogModel::isColorRMixed() const { return _redMixed; }
+bool WaypointEditorDialogModel::isColorGMixed() const { return _greenMixed; }
+bool WaypointEditorDialogModel::isColorBMixed() const { return _blueMixed; }
+bool WaypointEditorDialogModel::hasAnyColorMixed() const {
+ return _redMixed || _greenMixed || _blueMixed;
+}
- int listIndex = find_index_of_waypoint_list(_editor->cur_waypoint_list);
+SCP_string WaypointEditorDialogModel::getLayer() const {
+ std::unordered_set selected(_selectedWaypointPaths.begin(), _selectedWaypointPaths.end());
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 "";
- }
+ if (ptr->type != OBJ_WAYPOINT) continue;
+ if (selected.find(calc_waypoint_list_index(ptr->instance)) == selected.end()) continue;
+ 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) {
+ // moveObjectToLayer may unmark objects (hidden target layer) and fires
+ // notifyLayerStructureChanged. Both reach back into our refresh slots and would
+ // otherwise rebuild the dialog once per affected waypoint object.
+ _suppressRefresh = true;
+ std::unordered_set selected(_selectedWaypointPaths.begin(), _selectedWaypointPaths.end());
+ for (auto* ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) {
+ if (ptr->type != OBJ_WAYPOINT) continue;
+ if (selected.find(calc_waypoint_list_index(ptr->instance)) == selected.end()) continue;
+ _viewport->moveObjectToLayer(OBJ_INDEX(ptr), layer);
+ }
+ set_modified();
+ _editor->missionChanged();
+ _suppressRefresh = false;
+ // One refresh now that the dust has settled.
+ initializeData();
+}
+
+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);
+ // Prevent rebuilding the dialog for each waypoint as we mark them
+ _suppressRefresh = true;
+ _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();
+ _suppressRefresh = false;
+ initializeData();
}
-bool WaypointEditorDialogModel::getNoDrawLines() const { return _noDrawLines; }
-void WaypointEditorDialogModel::setNoDrawLines(bool val) { modify(_noDrawLines, val); }
+void WaypointEditorDialogModel::selectNextPath() {
+ if (Waypoint_lists.empty())
+ return;
+ if (_selectedWaypointPaths.empty()) {
+ selectWaypointPathByIndex(0);
+ return;
+ }
+ int next = (_selectedWaypointPaths.front() + 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;
+ if (_selectedWaypointPaths.empty()) {
+ selectWaypointPathByIndex(static_cast(Waypoint_lists.size()) - 1);
+ return;
+ }
+ int prev = (_selectedWaypointPaths.front() - 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) {
+ if (_suppressRefresh) return;
+ initializeData();
+}
+
+void WaypointEditorDialogModel::onSelectedObjectMarkingChanged(int, bool) {
+ if (_suppressRefresh) return;
+ initializeData();
+}
+
+void WaypointEditorDialogModel::onMissionChanged() {
+ if (_suppressRefresh) return;
+ initializeData();
+}
} // namespace fso::fred::dialogs
diff --git a/qtfred/src/mission/dialogs/WaypointEditorDialogModel.h b/qtfred/src/mission/dialogs/WaypointEditorDialogModel.h
index 2d633b167f7..5bdb49ab1d7 100644
--- a/qtfred/src/mission/dialogs/WaypointEditorDialogModel.h
+++ b/qtfred/src/mission/dialogs/WaypointEditorDialogModel.h
@@ -3,24 +3,31 @@
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;
+ static bool hasAnyPathsInMission();
+ 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;
+ int getNoDrawLinesState() const; // Qt::CheckState as int
void setNoDrawLines(bool val);
+
bool getHasCustomColor() const;
+ int getHasCustomColorState() const; // Qt::CheckState as int
void setHasCustomColor(bool val);
+
int getColorR() const;
int getColorG() const;
int getColorB() const;
@@ -28,35 +35,45 @@ class WaypointEditorDialogModel: public AbstractDialogModel {
void setColorG(int g);
void setColorB(int b);
- bool isEnabled() const;
- const SCP_vector>& getWaypointPathList() const;
+ bool isColorRMixed() const;
+ bool isColorGMixed() const;
+ bool isColorBMixed() const;
+ bool hasAnyColorMixed() 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: // NOLINT(readability-redundant-access-specifiers)
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;
int _colorR = 255, _colorG = 255, _colorB = 255;
+
+ bool _noDrawLinesMixed = false;
+ bool _hasCustomColorMixed = false;
+ bool _redMixed = false, _greenMixed = false, _blueMixed = false;
+
+ // Guards against re-entry into initializeData() from selection/marking/mission signals
+ // while we're already mutating mission state (e.g., setLayer fans out unmarks).
+ bool _suppressRefresh = false;
};
} // namespace fso::fred::dialogs
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..441496dc8f4 100644
--- a/qtfred/src/ui/dialogs/WaypointEditorDialog.cpp
+++ b/qtfred/src/ui/dialogs/WaypointEditorDialog.cpp
@@ -20,6 +20,12 @@ WaypointEditorDialog::WaypointEditorDialog(FredView* parent, EditorViewport* vie
ui->nameEdit->setMaxLength(NAME_LENGTH - 1);
+ // -1 is the "mixed selection" sentinel; shown as blank via specialValueText.
+ for (auto* sb : {ui->colorRSpinBox, ui->colorGSpinBox, ui->colorBSpinBox}) {
+ sb->setMinimum(-1);
+ sb->setSpecialValueText(" ");
+ }
+
initializeUi();
updateUi();
@@ -38,45 +44,49 @@ 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);
-}
+ ui->prevPathButton->setEnabled(hasAny);
+ ui->nextPathButton->setEnabled(hasAny);
-void WaypointEditorDialog::updateWaypointListComboBox()
-{
- ui->pathSelection->clear();
-
- 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());
- ui->customColorCheck->setChecked(_model->getHasCustomColor());
- ui->colorRSpinBox->setValue(_model->getColorR());
- ui->colorGSpinBox->setValue(_model->getColorG());
- ui->colorBSpinBox->setValue(_model->getColorB());
+ const int noDrawState = _model->getNoDrawLinesState();
+ ui->noDrawLinesCheck->setTristate(noDrawState == Qt::PartiallyChecked);
+ ui->noDrawLinesCheck->setCheckState(static_cast(noDrawState));
+
+ const int customColorState = _model->getHasCustomColorState();
+ ui->customColorCheck->setTristate(customColorState == Qt::PartiallyChecked);
+ ui->customColorCheck->setCheckState(static_cast(customColorState));
- bool colorEnabled = _model->isEnabled() && _model->getHasCustomColor();
+ ui->colorRSpinBox->setValue(_model->isColorRMixed() ? -1 : _model->getColorR());
+ ui->colorGSpinBox->setValue(_model->isColorGMixed() ? -1 : _model->getColorG());
+ ui->colorBSpinBox->setValue(_model->isColorBMixed() ? -1 : _model->getColorB());
+
+ const bool customResolved = customColorState == Qt::Checked;
+ const bool colorEnabled = _model->hasValidSelection() && customResolved;
ui->colorRSpinBox->setEnabled(colorEnabled);
ui->colorGSpinBox->setEnabled(colorEnabled);
ui->colorBSpinBox->setEnabled(colorEnabled);
@@ -86,6 +96,14 @@ void WaypointEditorDialog::updateUi()
void WaypointEditorDialog::updateColorSwatch()
{
+ if (_model->hasAnyColorMixed()) {
+ ui->colorSwatch->setText("?");
+ ui->colorSwatch->setAlignment(Qt::AlignCenter);
+ ui->colorSwatch->setStyleSheet("background: #888; color: white;"
+ "border: 1px solid #444; border-radius: 3px;");
+ return;
+ }
+ ui->colorSwatch->setText("");
ui->colorSwatch->setStyleSheet(QString("background: rgb(%1,%2,%3);"
"border: 1px solid #444; border-radius: 3px;")
.arg(_model->getColorR())
@@ -93,70 +111,54 @@ 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)
+void WaypointEditorDialog::on_noDrawLinesCheck_clicked()
{
- _model->setNoDrawLines(checked);
- _model->apply();
+ // clicked() (not toggled()) so a click on a tri-state PartiallyChecked box still routes here.
+ _model->setNoDrawLines(ui->noDrawLinesCheck->isChecked());
+ // User has resolved any partial state; refresh so tristate(true) is cleared.
+ updateUi();
}
-void WaypointEditorDialog::on_customColorCheck_toggled(bool checked)
+void WaypointEditorDialog::on_customColorCheck_clicked()
{
- _model->setHasCustomColor(checked);
- ui->colorRSpinBox->setEnabled(checked);
- ui->colorGSpinBox->setEnabled(checked);
- ui->colorBSpinBox->setEnabled(checked);
- updateColorSwatch();
- _model->apply();
+ _model->setHasCustomColor(ui->customColorCheck->isChecked());
+ updateUi();
}
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..d5241fdb282 100644
--- a/qtfred/src/ui/dialogs/WaypointEditorDialog.h
+++ b/qtfred/src/ui/dialogs/WaypointEditorDialog.h
@@ -16,10 +16,11 @@ 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);
+ void on_noDrawLinesCheck_clicked();
+ void on_customColorCheck_clicked();
void on_colorRSpinBox_valueChanged(int value);
void on_colorGSpinBox_valueChanged(int value);
void on_colorBSpinBox_valueChanged(int value);
@@ -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
-
-
-
-