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

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

Key fields

- - + unique. When multiple paths are selected the Name field is read-only. Rename + single paths individually. + + visible. Applied to all selected paths. + Does not affect in-game appearance. Applied to all selected paths.
FieldDescription
Waypoint pathSelects which path to edit. New paths are created by - placing waypoint objects in the viewport.
NameIdentifies the path in SEXPs and AI goal assignments. Must be - unique.
LayerThe layer the waypoint path is assigned to.
LayerThe layer the waypoint path is assigned to. Applied to all + selected paths.
No Draw LinesWhen checked, the connecting lines between waypoints are hidden in the viewport. The waypoint points themselves are still - visible.
Custom ColorWhen checked, enables the RGB fields below to set a custom color for rendering this path's points and lines in the viewport. - Does not affect in-game appearance.
Individual waypoint positions are moved by selecting the waypoint 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 - - - -