diff --git a/qtfred/source_groups.cmake b/qtfred/source_groups.cmake index 4c8caee9e79..861d7556220 100644 --- a/qtfred/source_groups.cmake +++ b/qtfred/source_groups.cmake @@ -72,6 +72,8 @@ add_file_folder("Source/Mission/Dialogs" src/mission/dialogs/MusicTBLViewerModel.h src/mission/dialogs/ObjectOrientEditorDialogModel.cpp src/mission/dialogs/ObjectOrientEditorDialogModel.h + src/mission/dialogs/PropEditorDialogModel.cpp + src/mission/dialogs/PropEditorDialogModel.h src/mission/dialogs/ReinforcementsEditorDialogModel.cpp src/mission/dialogs/ReinforcementsEditorDialogModel.h src/mission/dialogs/RelativeCoordinatesDialogModel.cpp @@ -172,6 +174,8 @@ add_file_folder("Source/UI/Dialogs" src/ui/dialogs/MusicTBLViewer.h src/ui/dialogs/ObjectOrientEditorDialog.cpp src/ui/dialogs/ObjectOrientEditorDialog.h + src/ui/dialogs/PropEditorDialog.cpp + src/ui/dialogs/PropEditorDialog.h src/ui/dialogs/ReinforcementsEditorDialog.cpp src/ui/dialogs/ReinforcementsEditorDialog.h src/ui/dialogs/RelativeCoordinatesDialog.cpp @@ -250,6 +254,8 @@ add_file_folder("Source/UI/Widgets" src/ui/widgets/CampaignMissionGraph.h src/ui/widgets/ColorComboBox.cpp src/ui/widgets/ColorComboBox.h + src/ui/widgets/PropComboBox.cpp + src/ui/widgets/PropComboBox.h src/ui/widgets/LineEditDelegate.cpp src/ui/widgets/LineEditDelegate.h src/ui/widgets/FlagList.cpp @@ -298,6 +304,7 @@ add_file_folder("UI" ui/MissionSpecDialog.ui ui/MusicPlayerDialog.ui ui/ObjectOrientationDialog.ui + ui/PropEditorDialog.ui ui/ReinforcementsDialog.ui ui/RelativeCoordinatesDialog.ui ui/SelectionDialog.ui diff --git a/qtfred/src/mission/EditorViewport.cpp b/qtfred/src/mission/EditorViewport.cpp index 3fa52cd7098..79ae5bac0bc 100644 --- a/qtfred/src/mission/EditorViewport.cpp +++ b/qtfred/src/mission/EditorViewport.cpp @@ -13,6 +13,7 @@ #include "EditorViewport.h" #include #include +#include #include namespace { @@ -880,6 +881,10 @@ void EditorViewport::drag_rotate_save_backup() { } int EditorViewport::create_object_on_grid(int x, int y, int waypoint_instance) { + return create_object_on_grid(x, y, waypoint_instance, false); +} + +int EditorViewport::create_object_on_grid(int x, int y, int waypoint_instance, bool create_prop) { int obj = -1; float rval; vec3d dir, pos; @@ -890,7 +895,7 @@ int EditorViewport::create_object_on_grid(int x, int y, int waypoint_instance) { if (rval >= 0.0f) { editor->unmark_all(); - obj = create_object(&pos, waypoint_instance); + obj = create_object(&pos, waypoint_instance, create_prop); if (obj >= 0) { editor->markObject(obj); @@ -904,27 +909,38 @@ int EditorViewport::create_object_on_grid(int x, int y, int waypoint_instance) { return obj; } -int EditorViewport::create_object(vec3d* pos, int waypoint_instance) { +int EditorViewport::create_object(vec3d* pos, int waypoint_instance, bool create_prop) { int obj, n; + if (create_prop) { + if (cur_prop_index < 0 || cur_prop_index >= prop_info_size()) { + return -1; + } - if (cur_model_index == editor->Id_select_type_waypoint) { - obj = editor->create_waypoint(pos, waypoint_instance); - } else if (cur_model_index == editor->Id_select_type_jump_node) { - CJumpNode jnp(pos); - obj = jnp.GetSCPObjectNumber(); - Jump_nodes.push_back(std::move(jnp)); - } else if(Ship_info[cur_model_index].flags[Ship::Info_Flags::No_fred]){ - obj = -1; - } else { // creating a ship - obj = editor->create_ship(NULL, pos, cur_model_index); - if (obj == -1) + obj = prop_create(nullptr, pos, cur_prop_index); + if (obj == -1) { return -1; + } + } else { - n = Objects[obj].instance; - Ships[n].arrival_cue = alloc_sexp("true", SEXP_ATOM, SEXP_ATOM_OPERATOR, -1, -1); - Ships[n].departure_cue = alloc_sexp("false", SEXP_ATOM, SEXP_ATOM_OPERATOR, -1, -1); - Ships[n].cargo1 = 0; + if (cur_model_index == editor->Id_select_type_waypoint) { + obj = editor->create_waypoint(pos, waypoint_instance); + } else if (cur_model_index == editor->Id_select_type_jump_node) { + CJumpNode jnp(pos); + obj = jnp.GetSCPObjectNumber(); + Jump_nodes.push_back(std::move(jnp)); + } else if(Ship_info[cur_model_index].flags[Ship::Info_Flags::No_fred]){ + obj = -1; + } else { // creating a ship + obj = editor->create_ship(nullptr, pos, cur_model_index); + if (obj == -1) + return -1; + + n = Objects[obj].instance; + Ships[n].arrival_cue = alloc_sexp("true", SEXP_ATOM, SEXP_ATOM_OPERATOR, -1, -1); + Ships[n].departure_cue = alloc_sexp("false", SEXP_ATOM, SEXP_ATOM_OPERATOR, -1, -1); + Ships[n].cargo1 = 0; + } } if (obj < 0) @@ -937,6 +953,12 @@ int EditorViewport::create_object(vec3d* pos, int waypoint_instance) { } void EditorViewport::initialSetup() { cur_model_index = get_default_player_ship_index(); + for (int i = 0; i < prop_info_size(); ++i) { + if (!Prop_info[i].flags[Prop::Info_Flags::No_fred]) { + cur_prop_index = i; + break; + } + } } int EditorViewport::duplicate_marked_objects() diff --git a/qtfred/src/mission/EditorViewport.h b/qtfred/src/mission/EditorViewport.h index cc02875e68f..90a6ef2f1d1 100644 --- a/qtfred/src/mission/EditorViewport.h +++ b/qtfred/src/mission/EditorViewport.h @@ -97,8 +97,9 @@ class EditorViewport { void drag_rotate_save_backup(); int create_object_on_grid(int x, int y, int waypoint_instance); + int create_object_on_grid(int x, int y, int waypoint_instance, bool create_prop); - int create_object(vec3d *pos, int waypoint_instance = -1); + int create_object(vec3d *pos, int waypoint_instance = -1, bool create_prop = false); int duplicate_marked_objects(); int drag_objects(int x, int y); @@ -146,6 +147,7 @@ class EditorViewport { int Dup_drag = 0; int cur_model_index = 0; + int cur_prop_index = -1; bool Bg_bitmap_dialog = false; @@ -176,4 +178,3 @@ class EditorViewport { } - diff --git a/qtfred/src/mission/FredRenderer.cpp b/qtfred/src/mission/FredRenderer.cpp index b92ccc10371..6902899d803 100644 --- a/qtfred/src/mission/FredRenderer.cpp +++ b/qtfred/src/mission/FredRenderer.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -508,6 +509,11 @@ void FredRenderer::display_ship_info(int cur_object_index) { } else if (objp->type == OBJ_JUMP_NODE) { CJumpNode* jnp = jumpnode_get_by_objnum(OBJ_INDEX(objp)); sprintf(buf, "%s\n%s", jnp->GetName(), jnp->GetDisplayName()); + } else if (objp->type == OBJ_PROP) { + auto propp = prop_id_lookup(objp->instance); + if (propp != nullptr) { + sprintf(buf, "%s\n", propp->prop_name); + } } else Assert(0); } @@ -839,7 +845,42 @@ void FredRenderer::render_one_model_htl(object* objp, } // build flags - if ((view().Show_ship_models || view().Show_outlines) && ((objp->type == OBJ_SHIP) || (objp->type == OBJ_START))) { + if ((objp->type == OBJ_PROP) && (view().Show_ship_models || view().Show_outlines)) { + uint64_t flags = MR_NORMAL; + + if (!view().Lighting_on) { + flags |= MR_NO_LIGHTING; + } + + if (view().FullDetail) { + flags |= MR_FULL_DETAIL; + } + + auto propp = prop_id_lookup(objp->instance); + if (propp == nullptr || !SCP_vector_inbounds(Prop_info, propp->prop_info_index)) { + return; + } + + model_render_params render_info; + render_info.set_debug_flags(0); + + if (Fred_outline) { + render_info.set_color(Fred_outline >> 16, (Fred_outline >> 8) & 0xff, Fred_outline & 0xff); + render_info.set_flags(flags | MR_SHOW_OUTLINE_HTL | MR_NO_LIGHTING | MR_NO_POLYS | MR_NO_TEXTURING); + model_render_immediate(&render_info, + Prop_info[propp->prop_info_index].model_num, + propp->model_instance_num, + &objp->orient, + &objp->pos); + } + + render_info.set_flags(flags); + model_render_immediate(&render_info, + Prop_info[propp->prop_info_index].model_num, + propp->model_instance_num, + &objp->orient, + &objp->pos); + } else if ((view().Show_ship_models || view().Show_outlines) && ((objp->type == OBJ_SHIP) || (objp->type == OBJ_START))) { uint64_t flags = 0; g3_start_instance_matrix(&Eye_position, &Eye_matrix, 0); @@ -933,6 +974,10 @@ void FredRenderer::render_one_model_htl(object* objp, r = 196; g = 32; b = 196; + } else if (objp->type == OBJ_PROP) { + r = 255; + g = 255; + b = 255; } else Assert(0); diff --git a/qtfred/src/mission/dialogs/PropEditorDialogModel.cpp b/qtfred/src/mission/dialogs/PropEditorDialogModel.cpp new file mode 100644 index 00000000000..06678b28318 --- /dev/null +++ b/qtfred/src/mission/dialogs/PropEditorDialogModel.cpp @@ -0,0 +1,327 @@ +#include "mission/dialogs/PropEditorDialogModel.h" + +#include +#include +#include + +#include + +namespace fso::fred::dialogs { + +PropEditorDialogModel::PropEditorDialogModel(QObject* parent, EditorViewport* viewport) : AbstractDialogModel(parent, viewport) { + connect(viewport->editor, &Editor::currentObjectChanged, this, &PropEditorDialogModel::onSelectedObjectChanged); + connect(viewport->editor, &Editor::objectMarkingChanged, this, &PropEditorDialogModel::onSelectedObjectMarkingChanged); + connect(viewport->editor, &Editor::missionChanged, this, &PropEditorDialogModel::onMissionChanged); + + initializeData(); +} + +bool PropEditorDialogModel::apply() { + if (!hasValidSelection()) { + return true; + } + + if (!validateData()) { + return false; + } + + for (auto obj_idx : _selectedPropObjects) { + if (!query_valid_object(obj_idx) || Objects[obj_idx].type != OBJ_PROP) { + continue; + } + + auto instance = Objects[obj_idx].instance; + auto prp = prop_id_lookup(instance); + if (prp == nullptr) { + continue; + } + + if (!hasMultipleSelection()) { + strcpy_s(prp->prop_name, _propName.c_str()); + } + + for (size_t i = 0; i < _flagLabels.size(); ++i) { + auto state = _flagState[i]; + if (state == Qt::PartiallyChecked) { + continue; + } + + auto flag_index = _flagLabels[i].second; + if (flag_index >= Num_parse_prop_flags) { + continue; + } + + auto& def = Parse_prop_flags[flag_index]; + if (!stricmp(def.name, "no_collide")) { + Objects[obj_idx].flags.set(Object::Object_Flags::Collides, state != Qt::Checked); + } + } + } + + _editor->missionChanged(); + return true; +} + +void PropEditorDialogModel::reject() { + // no-op +} + +void PropEditorDialogModel::initializeData() { + _flagLabels.clear(); + _flagState.clear(); + _selectedPropObjects = getSelectedPropObjects(); + + for (size_t i = 0; i < Num_parse_prop_flags; ++i) { + auto& def = Parse_prop_flags[i]; + auto& desc = Parse_prop_flag_descriptions[i]; + SCP_string label = def.name; + label += " ("; + label += desc.flag_desc; + label += ")"; + _flagLabels.emplace_back(label, i); + _flagState.push_back(Qt::Unchecked); + } + + if (hasValidSelection()) { + if (!hasMultipleSelection()) { + auto prp = prop_id_lookup(Objects[_selectedPropObjects.front()].instance); + Assertion(prp != nullptr, "Selected prop could not be found."); + _propName = prp->prop_name; + } else { + _propName.clear(); + } + + for (size_t i = 0; i < _flagLabels.size(); ++i) { + bool first = true; + for (auto obj_idx : _selectedPropObjects) { + if (!query_valid_object(obj_idx) || Objects[obj_idx].type != OBJ_PROP) { + continue; + } + + auto value = getFlagValueForObject(Objects[obj_idx], _flagLabels[i].second); + if (first) { + _flagState[i] = value ? Qt::Checked : Qt::Unchecked; + first = false; + } else { + _flagState[i] = tristate_set(value, _flagState[i]); + } + } + } + } else { + _propName.clear(); + } + + Q_EMIT modelDataChanged(); +} + +bool PropEditorDialogModel::validateData() { + _bypass_errors = false; + + if (hasMultipleSelection()) { + // Name is not editable for multi-select and only flags are applied. + return true; + } + + SCP_trim(_propName); + if (_propName.empty()) { + showErrorDialogNoCancel("A prop name cannot be empty."); + return false; + } + + std::unordered_set selected_instances; + for (auto obj_idx : _selectedPropObjects) { + if (query_valid_object(obj_idx) && Objects[obj_idx].type == OBJ_PROP) { + selected_instances.insert(Objects[obj_idx].instance); + } + } + + for (size_t i = 0; i < Props.size(); ++i) { + if (selected_instances.find(static_cast(i)) != selected_instances.end() || !Props[i].has_value()) { + continue; + } + + if (!stricmp(_propName.c_str(), Props[i].value().prop_name)) { + showErrorDialogNoCancel("This prop name is already being used by another prop."); + return false; + } + } + + return true; +} + +void PropEditorDialogModel::showErrorDialogNoCancel(const SCP_string& message) { + if (_bypass_errors) { + return; + } + + _bypass_errors = true; + _viewport->dialogProvider->showButtonDialog(DialogType::Error, "Error", message, {DialogButton::Ok}); +} + +void PropEditorDialogModel::selectPropFromObjectList(object* start, bool forward) { + auto ptr = start; + + while (ptr != END_OF_LIST(&obj_used_list)) { + if (ptr->type == OBJ_PROP) { + _editor->unmark_all(); + _editor->markObject(OBJ_INDEX(ptr)); + return; + } + ptr = forward ? GET_NEXT(ptr) : GET_PREV(ptr); + } + + ptr = forward ? GET_FIRST(&obj_used_list) : GET_LAST(&obj_used_list); + while (ptr != END_OF_LIST(&obj_used_list)) { + if (ptr->type == OBJ_PROP) { + _editor->unmark_all(); + _editor->markObject(OBJ_INDEX(ptr)); + return; + } + ptr = forward ? GET_NEXT(ptr) : GET_PREV(ptr); + } +} + +void PropEditorDialogModel::selectFirstPropInMission() { + for (auto* ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) { + if (ptr->type == OBJ_PROP) { + _editor->unmark_all(); + _editor->markObject(OBJ_INDEX(ptr)); + return; + } + } +} + +SCP_vector PropEditorDialogModel::getSelectedPropObjects() const { + SCP_vector selected; + for (auto* ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) { + if (ptr->type == OBJ_PROP && ptr->flags[Object::Object_Flags::Marked]) { + selected.push_back(OBJ_INDEX(ptr)); + } + } + + if (selected.empty() && query_valid_object(_editor->currentObject) && Objects[_editor->currentObject].type == OBJ_PROP) { + selected.push_back(_editor->currentObject); + } + + return selected; +} + +bool PropEditorDialogModel::getFlagValueForObject(const object& obj, size_t flag_index) { + if (flag_index >= Num_parse_prop_flags) { + return false; + } + + auto& def = Parse_prop_flags[flag_index]; + if (!stricmp(def.name, "no_collide")) { + return !obj.flags[Object::Object_Flags::Collides]; + } + + return false; +} + +int PropEditorDialogModel::tristate_set(bool value, int current_state) { + if (value) { + if (current_state == Qt::Unchecked) { + return Qt::PartiallyChecked; + } + } else { + if (current_state == Qt::Checked) { + return Qt::PartiallyChecked; + } + } + + if (current_state == Qt::PartiallyChecked) { + return Qt::PartiallyChecked; + } + + return value ? Qt::Checked : Qt::Unchecked; +} + +bool PropEditorDialogModel::hasValidSelection() const { + return !_selectedPropObjects.empty(); +} + +bool PropEditorDialogModel::hasMultipleSelection() const { + return _selectedPropObjects.size() > 1; +} + +bool PropEditorDialogModel::hasAnyPropsInMission() { + for (auto* ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) { + if (ptr->type == OBJ_PROP) { + return true; + } + } + + return false; +} + +const SCP_string& PropEditorDialogModel::getPropName() const { + return _propName; +} + +void PropEditorDialogModel::setPropName(const SCP_string& name) { + if (hasMultipleSelection()) { + return; + } + modify(_propName, name); +} + +const SCP_vector>& PropEditorDialogModel::getFlagLabels() const { + return _flagLabels; +} + +const SCP_vector& PropEditorDialogModel::getFlagState() const { + return _flagState; +} + +void PropEditorDialogModel::setFlagState(size_t index, int state) { + if (!SCP_vector_inbounds(_flagState, index)) { + return; + } + + if (_flagState[index] != state) { + _flagState[index] = state; + set_modified(); + Q_EMIT modelChanged(); + } +} + +void PropEditorDialogModel::selectNextProp() { + if (!hasValidSelection()) { + if (hasAnyPropsInMission()) { + selectFirstPropInMission(); + } + return; + } + + if (apply()) { + selectPropFromObjectList(GET_NEXT(&Objects[_selectedPropObjects.front()]), true); + } +} + +void PropEditorDialogModel::selectPreviousProp() { + if (!hasValidSelection()) { + if (hasAnyPropsInMission()) { + selectFirstPropInMission(); + } + return; + } + + if (apply()) { + selectPropFromObjectList(GET_PREV(&Objects[_selectedPropObjects.front()]), false); + } +} + +void PropEditorDialogModel::onSelectedObjectChanged(int) { + initializeData(); +} + +void PropEditorDialogModel::onSelectedObjectMarkingChanged(int, bool) { + initializeData(); +} + +void PropEditorDialogModel::onMissionChanged() { + initializeData(); +} + +} diff --git a/qtfred/src/mission/dialogs/PropEditorDialogModel.h b/qtfred/src/mission/dialogs/PropEditorDialogModel.h new file mode 100644 index 00000000000..0773617c684 --- /dev/null +++ b/qtfred/src/mission/dialogs/PropEditorDialogModel.h @@ -0,0 +1,55 @@ +#pragma once + +#include "mission/dialogs/AbstractDialogModel.h" +#include "mission/missionparse.h" + +namespace fso::fred::dialogs { + +class PropEditorDialogModel : public AbstractDialogModel { + Q_OBJECT + + public: + PropEditorDialogModel(QObject* parent, EditorViewport* viewport); + + bool apply() override; + void reject() override; + + bool hasValidSelection() const; + bool hasMultipleSelection() const; + static bool hasAnyPropsInMission(); + const SCP_string& getPropName() const; + void setPropName(const SCP_string& name); + + const SCP_vector>& getFlagLabels() const; + const SCP_vector& getFlagState() const; + void setFlagState(size_t index, int state); + + void selectNextProp(); + void selectPreviousProp(); + + signals: + void modelDataChanged(); + + private slots: + void onSelectedObjectChanged(int); + void onSelectedObjectMarkingChanged(int, bool); + void onMissionChanged(); + + private: // NOLINT(readability-redundant-access-specifiers) + void initializeData(); + bool validateData(); + void showErrorDialogNoCancel(const SCP_string& message); + void selectPropFromObjectList(object* start, bool forward); + void selectFirstPropInMission(); + SCP_vector getSelectedPropObjects() const; + static bool getFlagValueForObject(const object& obj, size_t flag_index); + static int tristate_set(bool value, int current_state); + + SCP_string _propName; + SCP_vector> _flagLabels; + SCP_vector _flagState; + SCP_vector _selectedPropObjects; + bool _bypass_errors = false; +}; + +} diff --git a/qtfred/src/ui/FredView.cpp b/qtfred/src/ui/FredView.cpp index c511a0392bb..8a7243a6308 100644 --- a/qtfred/src/ui/FredView.cpp +++ b/qtfred/src/ui/FredView.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -94,6 +95,10 @@ FredView::FredView(QWidget* parent) : QMainWindow(parent), ui(new Ui::FredView() initializePopupMenus(); initializeGroupActions(); + + auto propsAction = new QAction(tr("Props"), this); + connect(propsAction, &QAction::triggered, this, &FredView::on_actionProps_triggered); + ui->menuObjects->insertAction(ui->actionWaypoint_Paths, propsAction); } FredView::~FredView() { @@ -117,6 +122,12 @@ void FredView::setEditor(Editor* editor, EditorViewport* viewport) { ui->toolBar->addWidget(_shipClassBox.get()); connect(_shipClassBox.get(), &ColorComboBox::shipClassSelected, this, &FredView::onShipClassSelected); + auto propLabel = new QLabel(tr("Props"), ui->toolBar); + ui->toolBar->addWidget(propLabel); + _propClassBox.reset(new PropComboBox(nullptr)); + ui->toolBar->addWidget(_propClassBox.get()); + connect(_propClassBox.get(), &PropComboBox::propClassSelected, this, &FredView::onPropClassSelected); + connect(fred, &Editor::missionLoaded, this, &FredView::on_mission_loaded); // Sets the initial window title @@ -130,6 +141,7 @@ void FredView::setEditor(Editor* editor, EditorViewport* viewport) { connect(this, &FredView::viewIdle, this, &FredView::onUpdateCameraControlActions); connect(this, &FredView::viewIdle, this, &FredView::onUpdateSelectionLock); connect(this, &FredView::viewIdle, this, &FredView::onUpdateShipClassBox); + connect(this, &FredView::viewIdle, this, &FredView::onUpdatePropClassBox); connect(this, &FredView::viewIdle, this, &FredView::onUpdateEditorActions); connect(this, &FredView::viewIdle, this, &FredView::onUpdateWingActionStatus); connect(this, @@ -761,9 +773,20 @@ void FredView::onUpdateSelectionLock() { void FredView::onUpdateShipClassBox() { _shipClassBox->selectShipClass(_viewport->cur_model_index); } +void FredView::onUpdatePropClassBox() { + if (_propClassBox) { + if (_viewport->cur_prop_index < 0 && _propClassBox->count() > 0) { + onPropClassSelected(_propClassBox->itemData(0).value()); + } + _propClassBox->selectPropClass(_viewport->cur_prop_index); + } +} void FredView::onShipClassSelected(int ship_class) { _viewport->cur_model_index = ship_class; } +void FredView::onPropClassSelected(int prop_class) { + _viewport->cur_prop_index = prop_class; +} void FredView::on_actionAsteroid_Field_triggered(bool) { auto asteroidFieldEditor = new dialogs::AsteroidEditorDialog(this, _viewport); asteroidFieldEditor->setAttribute(Qt::WA_DeleteOnClose); @@ -825,6 +848,18 @@ void FredView::on_actionWings_triggered(bool) _wingEditorDialog->activateWindow(); } } +void FredView::on_actionProps_triggered(bool) +{ + if (!_propEditorDialog) { + _propEditorDialog = new dialogs::PropEditorDialog(this, _viewport); + _propEditorDialog->setAttribute(Qt::WA_DeleteOnClose); + connect(_propEditorDialog, &QObject::destroyed, this, [this]() { _propEditorDialog = nullptr; }); + _propEditorDialog->show(); + } else { + _propEditorDialog->raise(); + _propEditorDialog->activateWindow(); + } +} void FredView::on_actionCampaign_triggered(bool) { //TODO: Save if Changes auto editorCampaign = new dialogs::CampaignEditorDialog(this, _viewport); @@ -931,9 +966,15 @@ void FredView::handleObjectEditor(int objNum) { } else { Assertion(objNum >= 0, "Popup object is not valid when editObjectTriggered was called!"); - if ((Objects[objNum].type == OBJ_START) || (Objects[objNum].type == OBJ_SHIP)) { - on_actionShips_triggered(false); - } else if (Objects[objNum].type == OBJ_JUMP_NODE || Objects[objNum].type == OBJ_WAYPOINT) { + if ((Objects[objNum].type == OBJ_START) || (Objects[objNum].type == OBJ_SHIP)) { + on_actionShips_triggered(false); + } else if (Objects[objNum].type == OBJ_PROP) { + + // Select the object before displaying the dialog + fred->selectObject(objNum); + + on_actionProps_triggered(false); + } else if (Objects[objNum].type == OBJ_JUMP_NODE || Objects[objNum].type == OBJ_WAYPOINT) { // Select the object before displaying the dialog fred->selectObject(objNum); diff --git a/qtfred/src/ui/FredView.h b/qtfred/src/ui/FredView.h index 870df71eb42..83275106cfb 100644 --- a/qtfred/src/ui/FredView.h +++ b/qtfred/src/ui/FredView.h @@ -12,6 +12,7 @@ #include #include +#include namespace fso { namespace fred { @@ -22,6 +23,7 @@ class RenderWidget; namespace dialogs { class ShipEditorDialog; class WingEditorDialog; +class PropEditorDialog; } namespace Ui { @@ -102,6 +104,7 @@ class FredView: public QMainWindow, public IDialogProvider { void on_actionObjects_triggered(bool); void on_actionShips_triggered(bool); void on_actionWings_triggered(bool); + void on_actionProps_triggered(bool); void on_actionCampaign_triggered(bool); void on_actionCommand_Briefing_triggered(bool); void on_actionDebriefing_triggered(bool); @@ -218,12 +221,14 @@ class FredView: public QMainWindow, public IDialogProvider { std::unique_ptr ui; std::unique_ptr _shipClassBox; + std::unique_ptr _propClassBox; Editor* fred = nullptr; EditorViewport* _viewport = nullptr; fso::fred::dialogs::ShipEditorDialog* _shipEditorDialog = nullptr; fso::fred::dialogs::WingEditorDialog* _wingEditorDialog = nullptr; + fso::fred::dialogs::PropEditorDialog* _propEditorDialog = nullptr; bool _inKeyPressHandler = false; bool _inKeyReleaseHandler = false; @@ -234,10 +239,12 @@ class FredView: public QMainWindow, public IDialogProvider { void onUpdateCameraControlActions(); void onUpdateSelectionLock(); void onUpdateShipClassBox(); + void onUpdatePropClassBox(); void onUpdateEditorActions(); void onUpdateWingActionStatus(); void onShipClassSelected(int ship_class); + void onPropClassSelected(int prop_class); void windowActivated(); void windowDeactivated(); diff --git a/qtfred/src/ui/dialogs/PropEditorDialog.cpp b/qtfred/src/ui/dialogs/PropEditorDialog.cpp new file mode 100644 index 00000000000..91a2065182b --- /dev/null +++ b/qtfred/src/ui/dialogs/PropEditorDialog.cpp @@ -0,0 +1,87 @@ +#include "ui/dialogs/PropEditorDialog.h" + +#include "ui_PropEditorDialog.h" + +#include +#include + +#include + +namespace fso::fred::dialogs { + +PropEditorDialog::PropEditorDialog(FredView* parent, EditorViewport* viewport) + : QDialog(parent), ui(new ::Ui::PropEditorDialog()), _model(new PropEditorDialogModel(this, viewport)) { + ui->setupUi(this); + + initializeUi(); + updateUi(); + + connect(_model.get(), &PropEditorDialogModel::modelDataChanged, this, [this]() { + initializeUi(); + updateUi(); + }); + + connect(ui->propFlagsListWidget, &fso::fred::FlagListWidget::flagToggled, this, [this](const QString& name, int checked) { + const auto& labels = _model->getFlagLabels(); + for (size_t i = 0; i < labels.size(); ++i) { + if (name == QString::fromStdString(labels[i].first)) { + _model->setFlagState(i, checked); + break; + } + } + + // Applying immediately can re-enter FlagListWidget while it is still processing + // itemChanged, which may invalidate the underlying item/model pointers. + // Queue the apply until the current signal stack unwinds. + QMetaObject::invokeMethod(this, [this]() { _model->apply(); }, Qt::QueuedConnection); + }); + + resize(QDialog::sizeHint()); +} + +PropEditorDialog::~PropEditorDialog() = default; + +void PropEditorDialog::initializeUi() { + util::SignalBlockers blockers(this); + + const auto& labels = _model->getFlagLabels(); + QVector> toWidget; + toWidget.reserve(static_cast(labels.size())); + + for (size_t i = 0; i < labels.size(); ++i) { + toWidget.append({QString::fromStdString(labels[i].first), _model->getFlagState()[i]}); + } + ui->propFlagsListWidget->setFlags(toWidget); + ui->propFlagsListWidget->setFilterVisible(true); + ui->propFlagsListWidget->setToolbarVisible(true); + + const auto enable = _model->hasValidSelection(); + const auto has_props = _model->hasAnyPropsInMission(); + ui->propNameLineEdit->setEnabled(enable && !_model->hasMultipleSelection()); + ui->propFlagsListWidget->setEnabled(enable); + ui->nextButton->setEnabled(has_props); + ui->prevButton->setEnabled(has_props); +} + +void PropEditorDialog::updateUi() { + util::SignalBlockers blockers(this); + + ui->propNameLineEdit->setText(QString::fromStdString(_model->getPropName())); +} + +void PropEditorDialog::on_propNameLineEdit_editingFinished() { + _model->setPropName(ui->propNameLineEdit->text().toUtf8().constData()); + if (!_model->apply()) { + updateUi(); + } +} + +void PropEditorDialog::on_nextButton_clicked() { + _model->selectNextProp(); +} + +void PropEditorDialog::on_prevButton_clicked() { + _model->selectPreviousProp(); +} + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/PropEditorDialog.h b/qtfred/src/ui/dialogs/PropEditorDialog.h new file mode 100644 index 00000000000..7ba98be1141 --- /dev/null +++ b/qtfred/src/ui/dialogs/PropEditorDialog.h @@ -0,0 +1,34 @@ +#pragma once + +#include + +#include +#include + +namespace Ui { +class PropEditorDialog; +} + +namespace fso::fred::dialogs { + +class PropEditorDialog : public QDialog { + Q_OBJECT + + public: + PropEditorDialog(FredView* parent, EditorViewport* viewport); + ~PropEditorDialog() override; + + private slots: + void on_propNameLineEdit_editingFinished(); + void on_nextButton_clicked(); + void on_prevButton_clicked(); + + private: // NOLINT(readability-redundant-access-specifiers) + std::unique_ptr<::Ui::PropEditorDialog> ui; + std::unique_ptr _model; + + void initializeUi(); + void updateUi(); +}; + +} diff --git a/qtfred/src/ui/widgets/PropComboBox.cpp b/qtfred/src/ui/widgets/PropComboBox.cpp new file mode 100644 index 00000000000..7f9f0b19dd3 --- /dev/null +++ b/qtfred/src/ui/widgets/PropComboBox.cpp @@ -0,0 +1,59 @@ +#if defined(_MSC_VER) && _MSC_VER <= 1920 + #define QT_NO_FLOAT16_OPERATORS +#endif + +#include "PropComboBox.h" + +#include +#include + +#include +#include + +namespace fso::fred { + +PropComboBox::PropComboBox(QWidget* parent) : QComboBox(parent) { + fredApp->runAfterInit([this]() { + initialize(); + }); + + connect(this, + static_cast(&QComboBox::currentIndexChanged), + this, + &PropComboBox::indexChanged); +} + +void PropComboBox::initialize() { + clear(); + + for (int i = 0; i < prop_info_size(); ++i) { + if (Prop_info[i].flags[Prop::Info_Flags::No_fred]) { + continue; + } + + addItem(QString::fromStdString(Prop_info[i].name), i); + + auto category = prop_get_category(Prop_info[i].category_index); + if (category != nullptr) { + setItemData(count() - 1, + QBrush(QColor(category->list_color.red, category->list_color.green, category->list_color.blue)), + Qt::ForegroundRole); + } + } +} + +void PropComboBox::selectPropClass(int prop_class) { + auto index = findData(prop_class); + setCurrentIndex(index); +} + +void PropComboBox::indexChanged(int index) { + if (index < 0) { + return; + } + + auto propClass = itemData(index).value(); + Q_EMIT propClassSelected(propClass); +} + +} diff --git a/qtfred/src/ui/widgets/PropComboBox.h b/qtfred/src/ui/widgets/PropComboBox.h new file mode 100644 index 00000000000..703567ad9f2 --- /dev/null +++ b/qtfred/src/ui/widgets/PropComboBox.h @@ -0,0 +1,23 @@ +#pragma once + +#include + +namespace fso::fred { + +class PropComboBox : public QComboBox { + Q_OBJECT + + public: + explicit PropComboBox(QWidget* parent = nullptr); + + void selectPropClass(int prop_class); + void initialize(); + + signals: + void propClassSelected(int prop_class); + + private: + void indexChanged(int index); +}; + +} diff --git a/qtfred/src/ui/widgets/renderwidget.cpp b/qtfred/src/ui/widgets/renderwidget.cpp index 4c6d9295c6e..d1d1ba40b70 100644 --- a/qtfred/src/ui/widgets/renderwidget.cpp +++ b/qtfred/src/ui/widgets/renderwidget.cpp @@ -235,7 +235,8 @@ void RenderWidget::mousePressEvent(QMouseEvent* event) { if (!_viewport->Bg_bitmap_dialog) { if (_viewport->on_object == -1) { _viewport->Selection_lock = 0; // force off selection lock - _viewport->on_object = _viewport->create_object_on_grid(event->x(), event->y(), waypoint_instance); + auto spawn_prop = event->modifiers().testFlag(Qt::ShiftModifier); + _viewport->on_object = _viewport->create_object_on_grid(event->x(), event->y(), waypoint_instance, spawn_prop); } else { _viewport->Dup_drag = 1; diff --git a/qtfred/ui/PropEditorDialog.ui b/qtfred/ui/PropEditorDialog.ui new file mode 100644 index 00000000000..0548bebf09f --- /dev/null +++ b/qtfred/ui/PropEditorDialog.ui @@ -0,0 +1,84 @@ + + + PropEditorDialog + + + + 0 + 0 + 420 + 320 + + + + Prop Editor + + + + + + + + Name: + + + + + + + + + + + + true + + + true + + + + + + + + + Previous + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Next + + + + + + + + + + fso::fred::FlagListWidget + QWidget +
ui/widgets/FlagList.h
+ 1 +
+
+ + +