From e690f31abfa775ffffd628251ad9a39270692c93 Mon Sep 17 00:00:00 2001 From: The Force <2040992+TheForce172@users.noreply.github.com> Date: Sun, 15 Feb 2026 18:16:56 +0000 Subject: [PATCH 01/17] Release Build Safety --- .../ShipEditor/ShipEditorDialogModel.cpp | 1 - .../ui/dialogs/ShipEditor/ShipEditorDialog.cpp | 17 +++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.cpp b/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.cpp index 9d75963081b..87742a8cdba 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.cpp @@ -559,7 +559,6 @@ SCP_vector> ShipEditorDialogModel::getArrivalPaths() } else { allowed = (m_path_mask & (1 << i)) != 0; } - m_path_list.emplace_back(name, allowed); } return m_path_list; } diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.cpp b/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.cpp index 15beeed4f40..daa2cc8050f 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.cpp +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.cpp @@ -302,9 +302,11 @@ void ShipEditorDialog::updateArrival(bool overwrite) // determine if this ship has a docking bay pm = model_get(Ship_info[Ships[objp->instance].ship_info_index].model_num); Assert(pm); - if (pm->ship_bay && (pm->ship_bay->num_paths > 0)) { - auto ship = get_ship_from_obj(objp); - ui->arrivalTargetCombo->addItem(Ships[ship].ship_name, QVariant(ship)); + if (pm) { + if (pm->ship_bay && (pm->ship_bay->num_paths > 0)) { + auto ship = get_ship_from_obj(objp); + ui->arrivalTargetCombo->addItem(Ships[ship].ship_name, QVariant(ship)); + } } } } @@ -363,10 +365,13 @@ void ShipEditorDialog::updateDeparture(bool overwrite) // determine if this ship has a docking bay pm = model_get(Ship_info[Ships[objp->instance].ship_info_index].model_num); Assert(pm); - if (pm->ship_bay && (pm->ship_bay->num_paths > 0)) { - auto ship = get_ship_from_obj(objp); - ui->departureTargetCombo->addItem(Ships[ship].ship_name, QVariant(ship)); + if (pm != nullptr) { + if (pm->ship_bay && (pm->ship_bay->num_paths > 0)) { + auto ship = get_ship_from_obj(objp); + ui->departureTargetCombo->addItem(Ships[ship].ship_name, QVariant(ship)); + } } + } } ui->departureTargetCombo->setCurrentIndex(ui->departureTargetCombo->findData(_model->getDepartureTarget())); From de0d62ee76517bdec4d3009f169828d7fa045f9e Mon Sep 17 00:00:00 2001 From: The Force <2040992+TheForce172@users.noreply.github.com> Date: Mon, 30 Mar 2026 17:50:05 +0100 Subject: [PATCH 02/17] Move to using standard models. --- .../ShipEditor/ShipWeaponsDialogModel.cpp | 22 +- .../ShipEditor/ShipWeaponsDialogModel.h | 176 ++-- .../src/ui/dialogs/ShipEditor/BankModel.cpp | 809 +++++++++--------- qtfred/src/ui/dialogs/ShipEditor/BankModel.h | 104 +-- .../dialogs/ShipEditor/ShipWeaponsDialog.cpp | 55 +- .../ui/dialogs/ShipEditor/ShipWeaponsDialog.h | 141 +-- qtfred/src/ui/widgets/bankTree.cpp | 73 +- 7 files changed, 666 insertions(+), 714 deletions(-) diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp index cfaff25571b..ea3c75a9c18 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp @@ -1,10 +1,14 @@ #include "ShipWeaponsDialogModel.h" namespace fso::fred { -Banks::Banks(SCP_string _name, int aiIndex, int _ship, int multiedit, ship_subsys* _subsys) - : m_isMultiEdit(multiedit), name(std::move(_name)), subsys(_subsys), initalAI(aiIndex), ship(_ship) +Banks::Banks(SCP_string _name, int aiIndex, int _ship, int multiedit, int _id, ship_subsys* _subsys) + : m_isMultiEdit(multiedit), name(std::move(_name)), subsys(_subsys), initalAI(aiIndex), ship(_ship), id(_id) { aiClass = aiIndex; } +int Banks::getId() const +{ + return id; +} void Banks::add(Bank* bank) { banks.push_back(bank); @@ -159,7 +163,9 @@ void ShipWeaponsDialogModel::initializeData(bool isMultiEdit) void ShipWeaponsDialogModel::initPrimary(int inst, bool first) { - auto pilotBank = new Banks("Pilot", Ships[inst].weapons.ai_class, inst, m_isMultiEdit); + int id = 0; + auto pilotBank = new Banks("Pilot", Ships[inst].weapons.ai_class, inst, m_isMultiEdit, id); + id++; if (first) { auto pilot = Ships[inst].weapons; for (int i = 0; i < MAX_SHIP_PRIMARY_BANKS; i++) { @@ -176,7 +182,7 @@ void ShipWeaponsDialogModel::initPrimary(int inst, bool first) for (pss = GET_FIRST(ssl); pss != END_OF_LIST(ssl); pss = GET_NEXT(pss)) { model_subsystem* psub = pss->system_info; if (psub->type == SUBSYSTEM_TURRET) { - auto turretBank = new Banks(psub->subobj_name, pss->weapons.ai_class, inst, m_isMultiEdit, pss); + auto turretBank = new Banks(psub->subobj_name, pss->weapons.ai_class, inst, m_isMultiEdit,id, pss); for (int i = 0; i < MAX_SHIP_PRIMARY_BANKS; i++) { if (pss->weapons.primary_bank_weapons[i] >= 0) { const int maxAmmo = get_max_ammo_count_for_primary_turret_bank(&pss->weapons, @@ -188,6 +194,7 @@ void ShipWeaponsDialogModel::initPrimary(int inst, bool first) } if (!turretBank->empty()) { PrimaryBanks.push_back(turretBank); + id++; } else { delete turretBank; } @@ -226,7 +233,9 @@ void ShipWeaponsDialogModel::initPrimary(int inst, bool first) void ShipWeaponsDialogModel::initSecondary(int inst, bool first) { - auto pilotBank = new Banks("Pilot", Ships[inst].weapons.ai_class, inst, m_isMultiEdit); + int id = 0; + auto pilotBank = new Banks("Pilot", Ships[inst].weapons.ai_class, inst, m_isMultiEdit, id); + id++; if (first) { auto pilot = Ships[inst].weapons; for (int i = 0; i < MAX_SHIP_SECONDARY_BANKS; i++) { @@ -243,7 +252,7 @@ void ShipWeaponsDialogModel::initSecondary(int inst, bool first) for (pss = GET_FIRST(ssl); pss != END_OF_LIST(ssl); pss = GET_NEXT(pss)) { model_subsystem* psub = pss->system_info; if (psub->type == SUBSYSTEM_TURRET) { - auto turretBank = new Banks(psub->subobj_name, pss->weapons.ai_class, inst, m_isMultiEdit, pss); + auto turretBank = new Banks(psub->subobj_name, pss->weapons.ai_class, inst, m_isMultiEdit,id, pss); for (int i = 0; i < MAX_SHIP_SECONDARY_BANKS; i++) { if (pss->weapons.secondary_bank_weapons[i] >= 0) { const int maxAmmo = get_max_ammo_count_for_turret_bank(&pss->weapons, @@ -255,6 +264,7 @@ void ShipWeaponsDialogModel::initSecondary(int inst, bool first) } if (!turretBank->empty()) { SecondaryBanks.push_back(turretBank); + id++; } else { delete turretBank; } diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h index d63908e1c6b..e5b0b483ac9 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h @@ -1,88 +1,90 @@ -#pragma once - -#include "../AbstractDialogModel.h" - -#include - -namespace fso::fred { -struct Bank; -struct Banks { - Banks(SCP_string name, int aiIndex, int ship, int multiedit, ship_subsys* subsys = nullptr); - - public: - void add(Bank*); - Bank* getByBankId(const int id); - SCP_string getName() const; - int getShip() const; - ship_subsys* getSubsys() const; - bool empty() const; - SCP_vector getBanks() const; - int getAiClass() const; - void setAiClass(int); - bool m_isMultiEdit; - int getInitalAI() const; - - private: - SCP_string name; - ship_subsys* subsys; - int aiClass; - int initalAI; - SCP_vector banks; - int ship; -}; -struct Bank { - public: - Bank(const int weaponId, const int bankId, const int ammoMax, const int ammo, Banks* parent); - - int getWeaponId() const; - int getAmmo() const; - int getBankId() const; - int getMaxAmmo() const; - - void setWeapon(const int id); - void setAmmo(const int ammo); - - private: - int weaponId; - int bankId; - int ammo; - int ammoMax; - Banks* parent; -}; -namespace dialogs { -/** - * @brief QTFred's Weapons Editor Model - */ -class ShipWeaponsDialogModel : public AbstractDialogModel { - public: - /** - * @brief QTFred's Weapons Editor Model Constructer. - * @param [in/out] parent The dialogs parent. - * @param [in/out] viewport Editor viewport. - * @param [in] multi If editing multiple ships. - */ - ShipWeaponsDialogModel(QObject* parent, EditorViewport* viewport, bool multi); - - // void initTertiary(int inst, bool first); - - bool apply() override; - void reject() override; - SCP_vector getPrimaryBanks() const; - SCP_vector getSecondaryBanks() const; - // SCP_vector getTertiaryBanks() const; - - private: - void saveShip(int inst); - void initPrimary(const int inst, bool first); - - void initSecondary(int inst, bool first); - void initializeData(bool multi); - bool m_isMultiEdit; - int m_ship; - bool big = true; - SCP_vector PrimaryBanks; - SCP_vector SecondaryBanks; - // SCP_vector TertiaryBanks; -}; -} // namespace dialogs +#pragma once + +#include "../AbstractDialogModel.h" + +#include + +namespace fso::fred { +struct Bank; +struct Banks { + Banks(SCP_string name, int aiIndex, int ship, int multiedit, int _id, ship_subsys* subsys = nullptr); + + public: + int getId() const; + void add(Bank*); + Bank* getByBankId(const int id); + SCP_string getName() const; + int getShip() const; + ship_subsys* getSubsys() const; + bool empty() const; + SCP_vector getBanks() const; + int getAiClass() const; + void setAiClass(int); + bool m_isMultiEdit; + int getInitalAI() const; + + private: + SCP_string name; + ship_subsys* subsys; + int aiClass; + int initalAI; + SCP_vector banks; + int ship; + int id; +}; +struct Bank { + public: + Bank(const int weaponId, const int bankId, const int ammoMax, const int ammo, Banks* parent); + + int getWeaponId() const; + int getAmmo() const; + int getBankId() const; + int getMaxAmmo() const; + + void setWeapon(const int id); + void setAmmo(const int ammo); + + private: + int weaponId; + int bankId; + int ammo; + int ammoMax; + Banks* parent; +}; +namespace dialogs { +/** + * @brief QTFred's Weapons Editor Model + */ +class ShipWeaponsDialogModel : public AbstractDialogModel { + public: + /** + * @brief QTFred's Weapons Editor Model Constructer. + * @param [in/out] parent The dialogs parent. + * @param [in/out] viewport Editor viewport. + * @param [in] multi If editing multiple ships. + */ + ShipWeaponsDialogModel(QObject* parent, EditorViewport* viewport, bool multi); + + // void initTertiary(int inst, bool first); + + bool apply() override; + void reject() override; + SCP_vector getPrimaryBanks() const; + SCP_vector getSecondaryBanks() const; + // SCP_vector getTertiaryBanks() const; + + private: + void saveShip(int inst); + void initPrimary(const int inst, bool first); + + void initSecondary(int inst, bool first); + void initializeData(bool multi); + bool m_isMultiEdit; + int m_ship; + bool big = true; + SCP_vector PrimaryBanks; + SCP_vector SecondaryBanks; + // SCP_vector TertiaryBanks; +}; +} // namespace dialogs } // namespace fso::fred \ No newline at end of file diff --git a/qtfred/src/ui/dialogs/ShipEditor/BankModel.cpp b/qtfred/src/ui/dialogs/ShipEditor/BankModel.cpp index e5d6f148004..694061d1891 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/BankModel.cpp +++ b/qtfred/src/ui/dialogs/ShipEditor/BankModel.cpp @@ -1,404 +1,407 @@ -#include "ShipWeaponsDialog.h" - -#include -#include -namespace fso::fred { -BankTreeItem::BankTreeItem(BankTreeItem* parentItem, QString inName) : name(std::move(inName)), m_parentItem(parentItem) {} -BankTreeItem::~BankTreeItem() -{ - qDeleteAll(m_childItems); -} -void BankTreeItem::appendChild(BankTreeItem* item) -{ - m_childItems.append(item); -} -BankTreeItem* BankTreeItem::child(int row) const -{ - if (row < 0 || row >= m_childItems.size()) - return nullptr; - return m_childItems.at(row); -} -int BankTreeItem::childCount() const -{ - return m_childItems.count(); -} -int BankTreeItem::childNumber() const -{ - if (m_parentItem) - return m_parentItem->m_childItems.indexOf(const_cast(this)); - return 0; -} -BankTreeItem* BankTreeItem::parentItem() -{ - return m_parentItem; -} - -bool BankTreeItem::insertLabel(int position, const QString& newName, Banks* newBanks) -{ - if (position < 0 || position > m_childItems.size()) - return false; - - auto* item = new BankTreeLabel(newName, newBanks, this); - m_childItems.insert(position, item); - - return true; -} - -bool BankTreeItem::insertBank(int position, Bank* newBank) -{ - if (position < 0 || position > m_childItems.size()) - return false; - - auto* item = new BankTreeBank(newBank, this); - m_childItems.insert(position, item); - - return true; -} - -QString BankTreeItem::getName() const -{ - return name; -} - -int BankTreeBank::getId() const -{ - return bank->getWeaponId(); -} - -BankTreeBank::BankTreeBank(Bank* inBank, BankTreeItem* parentItem) : BankTreeItem(parentItem), bank(inBank) -{ - switch (bank->getWeaponId()) { - case -2: - this->name = "CONFLICT"; - break; - case -1: - this->name = "None"; - break; - default: - this->name = Weapon_info[bank->getWeaponId()].name; - } -} - -QVariant BankTreeBank::data(int column) const -{ - switch (column) { - case 0: - return name; - break; - case 1: - return bank->getAmmo(); - break; - default: - return {}; - } -} - -Qt::ItemFlags BankTreeBank::getFlags(int column) const -{ - switch (column) { - case 0: - return Qt::ItemIsDropEnabled | Qt::ItemIsSelectable; - break; - case 1: - return Qt::ItemIsEditable; - break; - default: - return {}; - } -} - -void BankTreeBank::setWeapon(int id) -{ - bank->setWeapon(id); - if (id == -1) { - name = "None"; - } else { - name = Weapon_info[id].name; - } -} - -void BankTreeBank::setAmmo(int value) -{ - Assert(bank != nullptr); - bank->setAmmo(value); -} - -BankTreeLabel::BankTreeLabel(const QString& inName, Banks* inBanks, BankTreeItem* parentItem) - : BankTreeItem(parentItem, inName), banks(inBanks) -{ -} - -QVariant BankTreeLabel::data(int column) const -{ - switch (column) { - case 0: - return name + " (" + Ai_class_names[banks->getAiClass()] + ")"; - break; - default: - return {}; - } -} - -Qt::ItemFlags BankTreeLabel::getFlags(int column) const -{ - Q_UNUSED(column); - return Qt::ItemIsSelectable; -} - -void BankTreeLabel::setAIClass(int value) -{ - Assert(banks != nullptr); - banks->setAiClass(value); -} - -bool BankTreeLabel::setData(int column, const QVariant& value) -{ - Q_UNUSED(column); - setAIClass(value.toInt()); - return true; -} - -bool BankTreeBank::setData(int column, const QVariant& value) -{ - switch (column) { - case 1: - setAmmo(value.toInt()); - return true; - break; - default: - return false; - } -} -BankTreeModel::BankTreeModel(const SCP_vector& data, QObject* parent) : QAbstractItemModel(parent) -{ - rootItem = new BankTreeRoot(); - - setupModelData(data, rootItem); -} - -void BankTreeModel::setupModelData(const SCP_vector& data, BankTreeItem* parent) -{ - for (auto banks : data) { - parent->insertLabel(parent->childCount(), banks->getName().c_str(), banks); - BankTreeItem* currentParent = parent->child(parent->childCount() - 1); - for (auto bank : banks->getBanks()) { - currentParent->insertBank(currentParent->childCount(), bank); - } - } -} - -QVariant BankTreeModel::headerData(int section, Qt::Orientation orientation, int role) const -{ - Q_UNUSED(orientation); - if (role == Qt::DisplayRole) { - switch (section) { - case 0: - return tr("Bank Name/Weapon"); - case 1: - return tr("Ammo"); - default: - return QString(""); - } - } - return {}; -} - -BankTreeModel::~BankTreeModel() -{ - delete rootItem; -} - -int BankTreeModel::columnCount(const QModelIndex& parent) const -{ - Q_UNUSED(parent); - return 2; -} - -QVariant BankTreeModel::data(const QModelIndex& index, int role) const -{ - if (!index.isValid()) { - return {}; - } - - if (role != Qt::DisplayRole && role != Qt::EditRole) - return {}; - - BankTreeItem* item = getItem(index); - - return item->data(index.column()); -} - -BankTreeItem* BankTreeModel::getItem(const QModelIndex index) const -{ - if (index.isValid()) { - auto* item = static_cast(index.internalPointer()); - if (item) - return item; - } - return rootItem; -} - -bool BankTreeModel::setData(const QModelIndex& index, const QVariant& value, int role) -{ - if (role != Qt::EditRole) - return false; - - BankTreeItem* item = getItem(index); // getItem(index); - if (!item) { - return false; - } - bool result = item->setData(index.column(), value); - QVector roles; - roles.append(role); - QAbstractItemModel::dataChanged(index, index, roles); - return result; -} - -int BankTreeModel::rowCount(const QModelIndex& parent) const -{ - if (parent.isValid() && parent.column() > 0) - return 0; - - const BankTreeItem* parentItem = getItem(parent); - - return parentItem ? parentItem->childCount() : 0; -} - -Qt::ItemFlags BankTreeModel::flags(const QModelIndex& index) const -{ - Qt::ItemFlags defaultFlags = QAbstractItemModel::flags(index); - defaultFlags.setFlag(Qt::ItemIsSelectable, false); - - if (index.isValid()) { - auto* item = static_cast(index.internalPointer()); - return item->getFlags(index.column()) | defaultFlags; - } else { - return Qt::NoItemFlags; - } -} - -QModelIndex BankTreeModel::index(int row, int column, const QModelIndex& parent) const -{ - if (parent.isValid() && parent.column() != 0) - return {}; - - BankTreeItem* parentItem = getItem(parent); - if (!parentItem) - return {}; - - BankTreeItem* childItem = parentItem->child(row); - if (childItem) - return createIndex(row, column, childItem); - return {}; -} -QModelIndex BankTreeModel::parent(const QModelIndex& index) const -{ - if (!index.isValid()) - return {}; - BankTreeItem* childItem = getItem(index); - BankTreeItem* parentItem = childItem ? childItem->parentItem() : nullptr; - - if (parentItem == rootItem || !parentItem) - return {}; - return createIndex(parentItem->childNumber(), 0, parentItem); -} -QStringList BankTreeModel::mimeTypes() const -{ - QStringList types; - types << "application/weaponid"; - return types; -} - -bool BankTreeModel::canDropMimeData(const QMimeData* data, - Qt::DropAction action, - int row, - int column, - const QModelIndex& parent) const -{ - Q_UNUSED(action); - Q_UNUSED(row); - Q_UNUSED(parent); - - if (!data->hasFormat("application/weaponid")) - return false; - BankTreeItem* item = this->getItem(parent); - Qt::ItemFlags flags = item->getFlags(column); - return flags.testFlag(Qt::ItemIsDropEnabled); -} -bool BankTreeModel::dropMimeData(const QMimeData* data, - Qt::DropAction action, - int row, - int column, - const QModelIndex& parent) -{ - if (!canDropMimeData(data, action, row, column, parent)) - return false; - - if (action == Qt::IgnoreAction) - return true; - - if (row == -1 && !parent.isValid()) - return false; - - QByteArray encodedData = data->data("application/weaponid"); - QDataStream stream(&encodedData, QIODevice::ReadOnly); - while (!stream.atEnd()) { - int id = 0; - stream >> id; - setWeapon(parent, id); - } - return true; -} - -void BankTreeModel::setWeapon(const QModelIndex& index, int data) -{ - auto item = dynamic_cast(this->getItem(index)); - Assert(item != nullptr); - if (item != nullptr) { - item->setWeapon(data); - QVector roles; - QAbstractItemModel::dataChanged(index, index, roles); - } -} - -bool BankTreeRoot::setData(int column, const QVariant& value) -{ - Q_UNUSED(column); - Q_UNUSED(value); - return false; -} -QVariant BankTreeRoot::data(int column) const -{ - switch (column) { - case 0: - return "Name/Weapon"; - break; - case 1: - return "Ammo"; - break; - default: - return {}; - } -} -Qt::ItemFlags BankTreeRoot::getFlags(int column) const -{ - Q_UNUSED(column); - return {}; -} - -int BankTreeModel::checktype(const QModelIndex index) const -{ - int type; - BankTreeItem* item = getItem(index); - auto bankTest = dynamic_cast(item); - auto labelTest = dynamic_cast(item); - if (bankTest) { - type = 0; - } else if (labelTest) { - type = 1; - } else { - type = -1; - } - return type; -} +#include "ShipWeaponsDialog.h" + +#include +#include +namespace fso::fred { +//BankTreeItem::BankTreeItem(BankTreeItem* parentItem, QString inName) : name(std::move(inName)), m_parentItem(parentItem) {} +/* BankTreeItem::~BankTreeItem() +{ + qDeleteAll(m_childItems); +} +void BankTreeItem::appendChild(BankTreeItem* item) +{ + m_childItems.append(item); +} +BankTreeItem* BankTreeItem::child(int row) const +{ + if (row < 0 || row >= m_childItems.size()) + return nullptr; + return m_childItems.at(row); +} +int BankTreeItem::childCount() const +{ + return m_childItems.count(); +} +int BankTreeItem::childNumber() const +{ + if (m_parentItem) + return m_parentItem->m_childItems.indexOf(const_cast(this)); + return 0; +} +BankTreeItem* BankTreeItem::parentItem() +{ + return m_parentItem; +} + +bool BankTreeItem::insertLabel(int position, const QString& newName, Banks* newBanks) +{ + if (position < 0 || position > m_childItems.size()) + return false; + + auto* item = new BankTreeLabel(newName, newBanks, this); + m_childItems.insert(position, item); + + return true; +} + +bool BankTreeItem::insertBank(int position, Bank* newBank) +{ + if (position < 0 || position > m_childItems.size()) + return false; + + auto* item = new BankTreeBank(newBank, this); + m_childItems.insert(position, item); + + return true; +} + +QString BankTreeItem::getName() const +{ + return name; +} + +int BankTreeBank::getId() const +{ + return bank->getWeaponId(); +} + +BankTreeBank::BankTreeBank(Bank* inBank, BankTreeItem* parentItem) : BankTreeItem(parentItem), bank(inBank) +{ + switch (bank->getWeaponId()) { + case -2: + this->name = "CONFLICT"; + break; + case -1: + this->name = "None"; + break; + default: + this->name = Weapon_info[bank->getWeaponId()].name; + } +} + +QVariant BankTreeBank::data(int column) const +{ + switch (column) { + case 0: + return name; + break; + case 1: + return bank->getAmmo(); + break; + default: + return {}; + } +} + +Qt::ItemFlags BankTreeBank::getFlags(int column) const +{ + switch (column) { + case 0: + return Qt::ItemIsDropEnabled | Qt::ItemIsSelectable; + break; + case 1: + return Qt::ItemIsEditable; + break; + default: + return {}; + } +} + +void BankTreeBank::setWeapon(int id) +{ + bank->setWeapon(id); + if (id == -1) { + name = "None"; + } else { + name = Weapon_info[id].name; + } +} + +void BankTreeBank::setAmmo(int value) +{ + Assert(bank != nullptr); + bank->setAmmo(value); +} + +BankTreeLabel::BankTreeLabel(const QString& inName, Banks* inBanks, BankTreeItem* parentItem) + : BankTreeItem(parentItem, inName), banks(inBanks) +{ +} + +QVariant BankTreeLabel::data(int column) const +{ + switch (column) { + case 0: + return name + " (" + Ai_class_names[banks->getAiClass()] + ")"; + break; + default: + return {}; + } +} + +Qt::ItemFlags BankTreeLabel::getFlags(int column) const +{ + Q_UNUSED(column); + return Qt::ItemIsSelectable; +} + +void BankTreeLabel::setAIClass(int value) +{ + Assert(banks != nullptr); + banks->setAiClass(value); +} + +bool BankTreeLabel::setData(int column, const QVariant& value) +{ + Q_UNUSED(column); + setAIClass(value.toInt()); + return true; +} + +bool BankTreeBank::setData(int column, const QVariant& value) +{ + switch (column) { + case 1: + setAmmo(value.toInt()); + return true; + break; + default: + return false; + } +} +BankTreeModel::BankTreeModel(const SCP_vector& data, QObject* parent) : QAbstractItemModel(parent) +{ + rootItem = new BankTreeRoot(); + + setupModelData(data, rootItem); +} + +void BankTreeModel::setupModelData(const SCP_vector& data, BankTreeItem* parent) +{ + for (auto banks : data) { + parent->insertLabel(parent->childCount(), banks->getName().c_str(), banks); + BankTreeItem* currentParent = parent->child(parent->childCount() - 1); + for (auto bank : banks->getBanks()) { + currentParent->insertBank(currentParent->childCount(), bank); + } + } +} + +QVariant BankTreeModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + Q_UNUSED(orientation); + if (role == Qt::DisplayRole) { + switch (section) { + case 0: + return tr("Bank Name/Weapon"); + case 1: + return tr("Ammo"); + default: + return QString(""); + } + } + return {}; +} + +BankTreeModel::~BankTreeModel() +{ + delete rootItem; +} + +int BankTreeModel::columnCount(const QModelIndex& parent) const +{ + Q_UNUSED(parent); + return 2; +} + +QVariant BankTreeModel::data(const QModelIndex& index, int role) const +{ + if (!index.isValid()) { + return {}; + } + + if (role != Qt::DisplayRole && role != Qt::EditRole) + return {}; + + BankTreeItem* item = getItem(index); + + return item->data(index.column()); +} + +BankTreeItem* BankTreeModel::getItem(const QModelIndex index) const +{ + if (index.isValid()) { + auto* item = static_cast(index.internalPointer()); + if (item) + return item; + } + return rootItem; +} + +bool BankTreeModel::setData(const QModelIndex& index, const QVariant& value, int role) +{ + if (role != Qt::EditRole) + return false; + + BankTreeItem* item = getItem(index); // getItem(index); + if (!item) { + return false; + } + bool result = item->setData(index.column(), value); + QVector roles; + roles.append(role); + QAbstractItemModel::dataChanged(index, index, roles); + return result; +} + +int BankTreeModel::rowCount(const QModelIndex& parent) const +{ + if (parent.isValid() && parent.column() > 0) + return 0; + + const BankTreeItem* parentItem = getItem(parent); + + return parentItem ? parentItem->childCount() : 0; +} + +Qt::ItemFlags BankTreeModel::flags(const QModelIndex& index) const +{ + Qt::ItemFlags defaultFlags = QAbstractItemModel::flags(index); + defaultFlags.setFlag(Qt::ItemIsSelectable, false); + + if (index.isValid()) { + auto* item = static_cast(index.internalPointer()); + return item->getFlags(index.column()) | defaultFlags; + } else { + return Qt::NoItemFlags; + } +} + +QModelIndex BankTreeModel::index(int row, int column, const QModelIndex& parent) const +{ + if (parent.isValid() && parent.column() != 0) + return {}; + + BankTreeItem* parentItem = getItem(parent); + if (!parentItem) + return {}; + + BankTreeItem* childItem = parentItem->child(row); + if (childItem) + return createIndex(row, column, childItem); + return {}; +} +QModelIndex BankTreeModel::parent(const QModelIndex& index) const +{ + if (!index.isValid()) + return {}; + BankTreeItem* childItem = getItem(index); + BankTreeItem* parentItem = childItem ? childItem->parentItem() : nullptr; + + if (parentItem == rootItem || !parentItem) + return {}; + return createIndex(parentItem->childNumber(), 0, parentItem); +} +QStringList BankTreeModel::mimeTypes() const +{ + QStringList types; + types << "application/weaponid"; + return types; +} + +bool BankTreeModel::canDropMimeData(const QMimeData* data, + Qt::DropAction action, + int row, + int column, + const QModelIndex& parent) const +{ + Q_UNUSED(action); + Q_UNUSED(row); + Q_UNUSED(parent); + + if (!data->hasFormat("application/weaponid")) + return false; + BankTreeItem* item = this->getItem(parent); + Qt::ItemFlags flags = item->getFlags(column); + return flags.testFlag(Qt::ItemIsDropEnabled); +} +bool BankTreeModel::dropMimeData(const QMimeData* data, + Qt::DropAction action, + int row, + int column, + const QModelIndex& parent) +{ + if (!canDropMimeData(data, action, row, column, parent)) + return false; + + if (action == Qt::IgnoreAction) + return true; + + if (row == -1 && !parent.isValid()) + return false; + + QByteArray encodedData = data->data("application/weaponid"); + QDataStream stream(&encodedData, QIODevice::ReadOnly); + while (!stream.atEnd()) { + int id = 0; + stream >> id; + setWeapon(parent, id); + } + return true; +} + +void BankTreeModel::setWeapon(const QModelIndex& index, int data) +{ + auto item = dynamic_cast(this->getItem(index)); + Assert(item != nullptr); + if (item != nullptr) { + item->setWeapon(data); + QVector roles; + QAbstractItemModel::dataChanged(index, index, roles); + } +} + +bool BankTreeRoot::setData(int column, const QVariant& value) +{ + Q_UNUSED(column); + Q_UNUSED(value); + return false; +} +QVariant BankTreeRoot::data(int column) const +{ + switch (column) { + case 0: + return "Name/Weapon"; + break; + case 1: + return "Ammo"; + break; + default: + return {}; + } +} +Qt::ItemFlags BankTreeRoot::getFlags(int column) const +{ + Q_UNUSED(column); + return {}; +} + +int BankTreeModel::checktype(const QModelIndex index) const +{ + int type; + BankTreeItem* item = getItem(index); + auto bankTest = dynamic_cast(item); + auto labelTest = dynamic_cast(item); + if (bankTest) { + type = 0; + } else if (labelTest) { + type = 1; + } else { + type = -1; + } + return type; +} +*/ +//BankTreeModel::BankTreeModel(QObject* parent) : QStandardItemModel(parent) {} +//BankTreeModel::~BankTreeModel() = default; } // namespace fso::fred \ No newline at end of file diff --git a/qtfred/src/ui/dialogs/ShipEditor/BankModel.h b/qtfred/src/ui/dialogs/ShipEditor/BankModel.h index a0b42d475e4..184a31a3239 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/BankModel.h +++ b/qtfred/src/ui/dialogs/ShipEditor/BankModel.h @@ -1,94 +1,12 @@ -#pragma once -#include "mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h" - -#include -namespace fso::fred { -class BankTreeItem { - public: - explicit BankTreeItem(BankTreeItem* parentItem = nullptr, QString inName = ""); - virtual ~BankTreeItem(); - virtual QVariant data(int column) const = 0; - void appendChild(BankTreeItem* child); - BankTreeItem* child(int row) const; - int childCount() const; - int childNumber() const; - BankTreeItem* parentItem(); - bool insertLabel(int position, const QString& name, Banks* banks); - bool insertBank(int position, Bank* banks); - - QString getName() const; - virtual bool setData(int column, const QVariant& value) = 0; - virtual Qt::ItemFlags getFlags(int column) const = 0; - QList m_childItems; - - protected: - QString name; - - private: - BankTreeItem* m_parentItem; -}; -class BankTreeRoot : public BankTreeItem { - bool setData(int column, const QVariant& value) override; - QVariant data(int column) const override; - Qt::ItemFlags getFlags(int column) const override; -}; -class BankTreeBank : public BankTreeItem { - public: - explicit BankTreeBank(Bank* inBank, BankTreeItem* parentItem = nullptr); - void setWeapon(int id); - void setAmmo(int value); - int getId() const; - bool setData(int column, const QVariant& value) override; - QVariant data(int column) const override; - Qt::ItemFlags getFlags(int column) const override; - - private: - Bank* bank; -}; -class BankTreeLabel : public BankTreeItem { - public: - explicit BankTreeLabel(const QString& name, Banks* banks, BankTreeItem* parentItem = nullptr); - void setAIClass(int value); - bool setData(int column, const QVariant& value) override; - QVariant data(int column) const override; - Qt::ItemFlags getFlags(int column) const override; - - private: - Banks* banks; -}; - -class BankTreeModel : public QAbstractItemModel { - Q_OBJECT - public: - BankTreeModel(const SCP_vector& data, QObject* parent = nullptr); - ~BankTreeModel() override; - int columnCount(const QModelIndex& parent) const override; - QVariant data(const QModelIndex& index, int role) const override; - - QModelIndex index(int row, int column, const QModelIndex& parent = QModelIndex()) const override; - QModelIndex parent(const QModelIndex& index) const override; - - int rowCount(const QModelIndex& parent = QModelIndex()) const override; - - Qt::ItemFlags flags(const QModelIndex& index) const override; - - QStringList mimeTypes() const override; - bool canDropMimeData(const QMimeData* data, - Qt::DropAction action, - int row, - int column, - const QModelIndex& parent) const override; - bool - dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) override; - void setWeapon(const QModelIndex& index, int data); - QVariant headerData(int section, Qt::Orientation orientation, int role) const override; - - bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override; - int checktype(const QModelIndex index) const; - - private: - BankTreeItem* rootItem; - BankTreeItem* getItem(const QModelIndex index) const; - static void setupModelData(const SCP_vector& data, BankTreeItem* parent); -}; +#pragma once +#include "mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h" + +#include +namespace fso::fred { +//class BankTreeModel : public QStandardItemModel { + //Q_OBJECT + //public: + //BankTreeModel(QObject* parent = nullptr); + //~BankTreeModel() override; +//}; } // namespace fso::fred \ No newline at end of file diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp index 025fe86c9bb..a213591411a 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp @@ -21,13 +21,15 @@ ShipWeaponsDialog::ShipWeaponsDialog(QDialog* parent, EditorViewport* viewport, // Build the model of ship weapons and set inital mode. if (!_model->getPrimaryBanks().empty()) { const util::SignalBlockers blockers(this); - bankModel = new BankTreeModel(_model->getPrimaryBanks(), this); + loadBankModel(_model->getPrimaryBanks()); + //bankModel = new BankTreeModel(_model->getPrimaryBanks(), this); ui->radioPrimary->setChecked(true); dialogMode = 0; weapons = new WeaponModel(0); } else if (!_model->getSecondaryBanks().empty()) { const util::SignalBlockers blockers(this); - bankModel = new BankTreeModel(_model->getSecondaryBanks(), this); + loadBankModel(_model->getSecondaryBanks()); + //bankModel = new BankTreeModel(_model->getSecondaryBanks(), this); ui->radioSecondary->setChecked(true); dialogMode = 1; weapons = new WeaponModel(1); @@ -97,7 +99,8 @@ void ShipWeaponsDialog::closeEvent(QCloseEvent* event) void ShipWeaponsDialog::on_setAllButton_clicked() { for (auto& index : ui->treeBanks->selectionModel()->selectedIndexes()) { - bankModel->setWeapon(index, ui->listWeapons->currentIndex().data(Qt::UserRole).toInt()); + bankModel->setData(index, ui->listWeapons->currentIndex().data(Qt::UserRole).toInt(), Qt::UserRole); + //bankModel->setWeapon(index, ui->listWeapons->currentIndex().data(Qt::UserRole).toInt()); } } void ShipWeaponsDialog::on_tblButton_clicked() @@ -129,13 +132,15 @@ void ShipWeaponsDialog::modeChanged(const bool enabled, const int mode) { if (enabled) { if (mode == 0) { - bankModel = new BankTreeModel(_model->getPrimaryBanks(), this); + loadBankModel(_model->getPrimaryBanks()); + //bankModel = new BankTreeModel(_model->getPrimaryBanks(), this); dialogMode = 0; delete weapons; weapons = new WeaponModel(0); ui->listWeapons->setModel(weapons); } else if (mode == 1) { - bankModel = new BankTreeModel(_model->getSecondaryBanks(), this); + loadBankModel(_model->getSecondaryBanks()); + //bankModel = new BankTreeModel(_model->getSecondaryBanks(), this); dialogMode = 1; delete weapons; weapons = new WeaponModel(1); @@ -149,7 +154,8 @@ void ShipWeaponsDialog::modeChanged(const bool enabled, const int mode) "Somehow an Illegal mode has been set. Get a coder.\n Illegal mode is " + std::to_string(mode), {DialogButton::Ok}); ui->radioPrimary->toggled(true); - bankModel = new BankTreeModel(_model->getPrimaryBanks(), this); + loadBankModel(_model->getPrimaryBanks()); + //bankModel = new BankTreeModel(_model->getPrimaryBanks(), this); dialogMode = 0; } // Reconnect beacuse the model has changed @@ -210,6 +216,43 @@ void ShipWeaponsDialog::aiClassChanged(const int index) m_currentAI = ui->aiCombo->itemData(index).toInt(); } +void ShipWeaponsDialog::loadBankModel(SCP_vector modelBanks) { + const util::SignalBlockers blockers(this); + bankModel = new QStandardItemModel(this); + bankModel->removeRows(0, bankModel->rowCount()); + for (auto banks : modelBanks) { + auto item = new QStandardItem(); + item->setData(banks->getName().c_str(), Qt::DisplayRole); + item->setData(true, Qt::UserRole + 2); + item->setData(banks->getId(), Qt::UserRole + 3); + bankModel->appendRow(item); + //parent->insertLabel(parent->childCount(), banks->getName().c_str(), banks); + //auto currentParent = parent->child(parent->childCount() - 1); + for (auto bank : banks->getBanks()) { + auto subitem = new QStandardItem(); + QString string; + switch (bank->getWeaponId()) { + case -2: + string = "CONFLICT"; + break; + case -1: + string = "None"; + break; + default: + string = Weapon_info[bank->getWeaponId()].name; + } + subitem->setData(string, Qt::DisplayRole); + subitem->setData(bank->getWeaponId(), Qt::UserRole); + subitem->setData(false, Qt::UserRole + 2); + subitem->setData(bank->getBankId(), Qt::UserRole + 3); + subitem->setData(bank->getAmmo(), Qt::UserRole + 4); + subitem->setData(bank->getMaxAmmo(), Qt::UserRole + 5); + subitem->appendRow(subitem); + //currentParent->insertBank(currentParent->childCount(), bank); + } + } +} + void ShipWeaponsDialog::on_aiButton_clicked() { for (auto& index : ui->treeBanks->selectionModel()->selectedIndexes()) { diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h index 7b5efb83578..ad9128bc1f8 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h @@ -1,68 +1,75 @@ -#ifndef SHIPWEAPONSDIALOG_H -#define SHIPWEAPONSDIALOG_H - -#include "ui/dialogs/ShipEditor/BankModel.h" -#include "ui/widgets/weaponList.h" - -#include -#include - -#include -#include - -namespace fso::fred::dialogs { - -namespace Ui { -class ShipWeaponsDialog; -} -/** - * @brief QTFred's Weapons Editor - */ -class ShipWeaponsDialog : public QDialog { - Q_OBJECT - - public: - /** - * @brief QTFred's Weapons Editor Constructer. - * @param [in/out] parent The dialogs parent. - * @param [in/out] viewport Editor viewport. - * @param [in] isMultiEdit If editing multiple ships. - */ - explicit ShipWeaponsDialog(QDialog* parent, EditorViewport* viewport, bool isMultiEdit); - ~ShipWeaponsDialog() override; - - void accept() override; - void reject() override; - - protected: - void closeEvent(QCloseEvent*) override; - - private slots: - void on_buttonClose_clicked(); - void on_aiButton_clicked(); - void on_setAllButton_clicked(); - void on_tblButton_clicked(); - void on_radioPrimary_toggled(bool checked); - void on_radioSecondary_toggled(bool checked); - void on_radioTertiary_toggled(bool checked); - void on_aiCombo_currentIndexChanged(int index); - - private: // NOLINT(readability-redundant-access-specifiers) - std::unique_ptr ui; - std::unique_ptr _model; - /** - * @brief Changes current weapon type. - * @param [in] enabled Always True - * @param [in] mode The mode to change to. 0 = Primary, 1 = Secondary - */ - void modeChanged(const bool enabled, const int mode); - EditorViewport* _viewport; - void updateUI(); - BankTreeModel* bankModel; - int dialogMode; - WeaponModel* weapons; - int m_currentAI = 0; - void aiClassChanged(const int index); -}; -} // namespace fso::fred::dialogs +#ifndef SHIPWEAPONSDIALOG_H +#define SHIPWEAPONSDIALOG_H + +#include "ui/dialogs/ShipEditor/BankModel.h" +#include "ui/widgets/weaponList.h" + +#include + +#include +#include + +namespace fso::fred::dialogs { +//Weapon ID = UserRole +//Wapon name = DisplayRole +//Bank or subsys = UserRole + 2 (true = subsys) false = bank +// id = UserRole + 3 +// ammo = UserRole + 4 +// maxammo = UserRole + 5 +namespace Ui { +class ShipWeaponsDialog; +} +/** + * @brief QTFred's Weapons Editor + */ +class ShipWeaponsDialog : public QDialog { + Q_OBJECT + + public: + /** + * @brief QTFred's Weapons Editor Constructer. + * @param [in/out] parent The dialogs parent. + * @param [in/out] viewport Editor viewport. + * @param [in] isMultiEdit If editing multiple ships. + */ + explicit ShipWeaponsDialog(QDialog* parent, EditorViewport* viewport, bool isMultiEdit); + ~ShipWeaponsDialog() override; + + void accept() override; + void reject() override; + + protected: + void closeEvent(QCloseEvent*) override; + + private slots: + void on_buttonClose_clicked(); + void on_aiButton_clicked(); + void on_setAllButton_clicked(); + void on_tblButton_clicked(); + void on_radioPrimary_toggled(bool checked); + void on_radioSecondary_toggled(bool checked); + void on_radioTertiary_toggled(bool checked); + void on_aiCombo_currentIndexChanged(int index); + + private: // NOLINT(readability-redundant-access-specifiers) + std::unique_ptr ui; + std::unique_ptr _model; + /** + * @brief Changes current weapon type. + * @param [in] enabled Always True + * @param [in] mode The mode to change to. 0 = Primary, 1 = Secondary + */ + void modeChanged(const bool enabled, const int mode); + EditorViewport* _viewport; + void updateUI(); + QStandardItemModel* bankModel; + int dialogMode; + WeaponModel* weapons; + int m_currentAI = 0; + void aiClassChanged(const int index); + + void loadBankModel(SCP_vector); + +}; +} // namespace fso::fred::dialogs #endif \ No newline at end of file diff --git a/qtfred/src/ui/widgets/bankTree.cpp b/qtfred/src/ui/widgets/bankTree.cpp index f2d28dd22ee..996465cff1b 100644 --- a/qtfred/src/ui/widgets/bankTree.cpp +++ b/qtfred/src/ui/widgets/bankTree.cpp @@ -28,7 +28,7 @@ void bankTree::dragMoveEvent(QDragMoveEvent* event) if (!index.isValid()) { return; } - if (dynamic_cast(model())->checktype(index) == 0) { + if (model()->data(index, Qt::UserRole + 2) == false) { event->accept(); } else { event->ignore(); @@ -36,65 +36,34 @@ void bankTree::dragMoveEvent(QDragMoveEvent* event) } void bankTree::selectionChanged(const QItemSelection& selected, const QItemSelection& deselected) { - QItemSelection newlySelected; - QItemSelection select; - QItemSelection deselect(deselected); - if (selected.empty()) { - QTreeView::selectionChanged(selected, deselected); - if (selectionModel()->selectedIndexes().empty()) { - typeSelected = -1; - } - return; - } - for (auto& sidx : selected.indexes()) { - bool match = false; - for (auto& didx : deselected.indexes()) { - if (sidx == didx) { - match = true; - break; - } - } - if (!match) { - QItemSelectionRange selection(sidx); - newlySelected.append(selection); - } - } - if (!newlySelected.empty()) { - if (typeSelected == -1) { - typeSelected = dynamic_cast(model())->checktype(newlySelected.indexes().first()); - for (auto& sidx : newlySelected.indexes()) { - if (dynamic_cast(model())->checktype(sidx) == typeSelected) { - QItemSelectionRange selection(sidx); - select.append(selection); - } - } - } else { - int type = dynamic_cast(model())->checktype(newlySelected.indexes().first()); - if (type != typeSelected) { - typeSelected = type; - for (auto& sidx : selected.indexes()) { - QItemSelectionRange selection(sidx); - deselect.append(selection); - } - for (auto& sidx : newlySelected.indexes()) { - if (dynamic_cast(model())->checktype(sidx) == typeSelected) { - QItemSelectionRange selection(sidx); - select.append(selection); + auto indexes = selected.indexes(); + if (!indexes.isEmpty()) { + auto& first = indexes.first(); + if (first.isValid()) { + if (model()->data(first, Qt::UserRole + 2) == true) { + for (auto& index : + model()->match(model()->index(0, 0), Qt::UserRole + 2, false, -1, Qt::MatchRecursive)) { + if (index.isValid()) { + auto item = dynamic_cast(model())->itemFromIndex(index); + Qt::ItemFlags flags = item->flags(); + flags &= ~Qt::ItemIsSelectable; + item->setFlags(flags); } } - selectionModel()->clear(); - typeSelected = -1; } else { - for (auto& sidx : newlySelected.indexes()) { - if (dynamic_cast(model())->checktype(sidx) == typeSelected) { - QItemSelectionRange selection(sidx); - select.append(selection); + for (auto& index : + model()->match(model()->index(0, 0), Qt::UserRole + 2, true, -1, Qt::MatchRecursive)) { + if (index.isValid()) { + auto item = dynamic_cast(model())->itemFromIndex(index); + Qt::ItemFlags flags = item->flags(); + flags &= ~Qt::ItemIsSelectable; + item->setFlags(flags); } } } } } - QTreeView::selectionChanged(select, deselect); + QTreeView::selectionChanged(selected, deselected); } int bankTree::getTypeSelected() const { From 36bd9f0f7ac0563337923e08d7947d8b47b585a2 Mon Sep 17 00:00:00 2001 From: The Force <2040992+TheForce172@users.noreply.github.com> Date: Fri, 24 Apr 2026 21:32:59 +0100 Subject: [PATCH 03/17] Avoid using .indexes() to fix heap coruption --- qtfred/src/ui/widgets/bankTree.cpp | 39 ++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/qtfred/src/ui/widgets/bankTree.cpp b/qtfred/src/ui/widgets/bankTree.cpp index 996465cff1b..56737a0b30a 100644 --- a/qtfred/src/ui/widgets/bankTree.cpp +++ b/qtfred/src/ui/widgets/bankTree.cpp @@ -34,7 +34,7 @@ void bankTree::dragMoveEvent(QDragMoveEvent* event) event->ignore(); } } -void bankTree::selectionChanged(const QItemSelection& selected, const QItemSelection& deselected) +/* void bankTree::selectionChanged(const QItemSelection& selected, const QItemSelection& deselected) { auto indexes = selected.indexes(); if (!indexes.isEmpty()) { @@ -63,9 +63,44 @@ void bankTree::selectionChanged(const QItemSelection& selected, const QItemSelec } } } + QTreeView::selectionChanged(selected, deselected); +}*/ + +void bankTree::selectionChanged(const QItemSelection& selected, const QItemSelection& deselected) { + // Iterate QItemSelection directly as QList via const ref + // avoids calling .indexes() which returns a new QList by value + bool hasSelection = false; + bool isBank = false; + for (const QItemSelectionRange& range : selected) { + QModelIndex first = range.topLeft(); + if (first.isValid()) { + hasSelection = true; + isBank = (model()->data(first, Qt::UserRole + 2) == true); + break; + } + } + + if (hasSelection) { + // Traverse model manually to avoid model()->match() returning QList by value + bool disableValue = !isBank; + std::function traverse = [&](const QModelIndex& parent) { + for (int row = 0; row < model()->rowCount(parent); ++row) { + QModelIndex idx = model()->index(row, 0, parent); + if (model()->data(idx, Qt::UserRole + 2) == disableValue) { + auto* item = dynamic_cast(model())->itemFromIndex(idx); + item->setFlags(item->flags() & ~Qt::ItemIsSelectable); + } + if (model()->hasChildren(idx)) { + traverse(idx); + } + } + }; + traverse(QModelIndex()); + } + QTreeView::selectionChanged(selected, deselected); } -int bankTree::getTypeSelected() const + int bankTree::getTypeSelected() const { return typeSelected; } From 695b869a14610ec7426ef478b3cc6122cc901f4a Mon Sep 17 00:00:00 2001 From: The Force <2040992+TheForce172@users.noreply.github.com> Date: Tue, 12 May 2026 15:42:59 +0100 Subject: [PATCH 04/17] More work on selection function --- .../dialogs/ShipEditor/ShipWeaponsDialog.cpp | 18 +++-- .../ui/dialogs/ShipEditor/ShipWeaponsDialog.h | 1 + qtfred/src/ui/widgets/bankTree.cpp | 72 ++++++++++++------- qtfred/src/ui/widgets/bankTree.h | 49 ++++++------- 4 files changed, 84 insertions(+), 56 deletions(-) diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp index a213591411a..c45fab140fe 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp @@ -186,13 +186,13 @@ void ShipWeaponsDialog::updateUI() ui->treeBanks->expandAll(); // Setall button - if (ui->treeBanks->getTypeSelected() == 0) { + if (ui->treeBanks->getTypeSelected() == false) { ui->setAllButton->setEnabled(true); } else { ui->setAllButton->setEnabled(false); } // Change AI Button - if (ui->treeBanks->getTypeSelected() == 1) { + if (ui->treeBanks->getTypeSelected() == true) { ui->aiButton->setEnabled(true); } else { ui->aiButton->setEnabled(false); @@ -222,9 +222,12 @@ void ShipWeaponsDialog::loadBankModel(SCP_vector modelBanks) { bankModel->removeRows(0, bankModel->rowCount()); for (auto banks : modelBanks) { auto item = new QStandardItem(); - item->setData(banks->getName().c_str(), Qt::DisplayRole); + const SCP_string name = banks->getName() + " ( " + Ai_class_names[banks->getAiClass()] + " ) "; + item->setData(name.c_str(), Qt::DisplayRole); item->setData(true, Qt::UserRole + 2); item->setData(banks->getId(), Qt::UserRole + 3); + item->setData(banks->getAiClass(), Qt::UserRole + 6); + item->setFlags(item->flags() & ~Qt::ItemIsEditable); bankModel->appendRow(item); //parent->insertLabel(parent->childCount(), banks->getName().c_str(), banks); //auto currentParent = parent->child(parent->childCount() - 1); @@ -247,7 +250,8 @@ void ShipWeaponsDialog::loadBankModel(SCP_vector modelBanks) { subitem->setData(bank->getBankId(), Qt::UserRole + 3); subitem->setData(bank->getAmmo(), Qt::UserRole + 4); subitem->setData(bank->getMaxAmmo(), Qt::UserRole + 5); - subitem->appendRow(subitem); + subitem->setFlags(item->flags() & ~Qt::ItemIsEditable); + item->appendRow(subitem); //currentParent->insertBank(currentParent->childCount(), bank); } } @@ -256,7 +260,11 @@ void ShipWeaponsDialog::loadBankModel(SCP_vector modelBanks) { void ShipWeaponsDialog::on_aiButton_clicked() { for (auto& index : ui->treeBanks->selectionModel()->selectedIndexes()) { - bankModel->setData(index, m_currentAI); + bankModel->setData(index, m_currentAI, Qt::UserRole + 6); + //bankModel->setData(index, m_currentAI, Qt::DisplayRole); + const SCP_string oldName = bankModel->data(index, Qt::DisplayRole).toString().toStdString(); + const size_t end = oldName.find(' '); + const SCP_string justname = oldName.substr(0, end); } } diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h index ad9128bc1f8..f12a382dbaf 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h @@ -16,6 +16,7 @@ namespace fso::fred::dialogs { // id = UserRole + 3 // ammo = UserRole + 4 // maxammo = UserRole + 5 + // aiclass = Qt::UserRole + 6 namespace Ui { class ShipWeaponsDialog; } diff --git a/qtfred/src/ui/widgets/bankTree.cpp b/qtfred/src/ui/widgets/bankTree.cpp index 56737a0b30a..fd198610899 100644 --- a/qtfred/src/ui/widgets/bankTree.cpp +++ b/qtfred/src/ui/widgets/bankTree.cpp @@ -66,42 +66,60 @@ void bankTree::dragMoveEvent(QDragMoveEvent* event) QTreeView::selectionChanged(selected, deselected); }*/ -void bankTree::selectionChanged(const QItemSelection& selected, const QItemSelection& deselected) { - // Iterate QItemSelection directly as QList via const ref - // avoids calling .indexes() which returns a new QList by value - bool hasSelection = false; - bool isBank = false; - for (const QItemSelectionRange& range : selected) { - QModelIndex first = range.topLeft(); - if (first.isValid()) { - hasSelection = true; - isBank = (model()->data(first, Qt::UserRole + 2) == true); - break; - } - } +void bankTree::selectionChanged(const QItemSelection& selected, const QItemSelection& deselected) +{ + // 1. Update the internal selection state first + QTreeView::selectionChanged(selected, deselected); - if (hasSelection) { - // Traverse model manually to avoid model()->match() returning QList by value - bool disableValue = !isBank; + QItemSelectionModel* sm = selectionModel(); + QStandardItemModel* m = qobject_cast(model()); + if (!m) + return; + + // Helper lambda to update the selectable flag across the whole tree + auto updateTreeFlags = [&](bool isFiltering, bool filterForBank) { std::function traverse = [&](const QModelIndex& parent) { - for (int row = 0; row < model()->rowCount(parent); ++row) { - QModelIndex idx = model()->index(row, 0, parent); - if (model()->data(idx, Qt::UserRole + 2) == disableValue) { - auto* item = dynamic_cast(model())->itemFromIndex(idx); - item->setFlags(item->flags() & ~Qt::ItemIsSelectable); + for (int r = 0; r < m->rowCount(parent); ++r) { + QModelIndex idx = m->index(r, 0, parent); + QStandardItem* item = m->itemFromIndex(idx); + if (!item) + continue; + + if (!isFiltering) { + // Reset mode: Everything becomes selectable + item->setFlags(item->flags() | Qt::ItemIsSelectable); + } else { + // Filter mode: Only items matching the 'bank' status stay selectable + bool itemIsBank = m->data(idx, Qt::UserRole + 2).toBool(); + if (itemIsBank == filterForBank) { + item->setFlags(item->flags() | Qt::ItemIsSelectable); + } else { + item->setFlags(item->flags() & ~Qt::ItemIsSelectable); + } } - if (model()->hasChildren(idx)) { + + if (m->hasChildren(idx)) traverse(idx); - } } }; traverse(QModelIndex()); - } + }; - QTreeView::selectionChanged(selected, deselected); + // 2. Handle the "Last Item Unselected" case (prevents the crash) + updateTreeFlags(false, false); // Disable filtering, reset all to selectable + + // 3. We have a selection, so determine the current "Master Type" + // safe because we checked hasSelection() + if (!sm->selectedIndexes().empty()) { + QModelIndex first = sm->selectedIndexes().first(); + if (first.isValid()) { + currentSelectionIsNotBank = m->data(first, Qt::UserRole + 2).toBool(); + updateTreeFlags(true, currentSelectionIsNotBank); + } + } } - int bankTree::getTypeSelected() const +bool bankTree::getTypeSelected() const { - return typeSelected; + return currentSelectionIsNotBank; } } // namespace fso::fred \ No newline at end of file diff --git a/qtfred/src/ui/widgets/bankTree.h b/qtfred/src/ui/widgets/bankTree.h index 81268d173cf..f6a30210f94 100644 --- a/qtfred/src/ui/widgets/bankTree.h +++ b/qtfred/src/ui/widgets/bankTree.h @@ -1,25 +1,26 @@ -#pragma once -#include "ui/dialogs/ShipEditor/BankModel.h" - -#include - -#include -#include -#include -#include -#include -namespace fso::fred { -class bankTree : public QTreeView { - Q_OBJECT - public: - bankTree(QWidget*); - void selectionChanged(const QItemSelection& selected, const QItemSelection& deselected) override; - int getTypeSelected() const; - - protected: - void dragEnterEvent(QDragEnterEvent*) override; - void dropEvent(QDropEvent* event) override; - void dragMoveEvent(QDragMoveEvent*) override; - int typeSelected = -1; -}; +#pragma once +#include "ui/dialogs/ShipEditor/BankModel.h" + +#include + +#include +#include +#include +#include +#include +namespace fso::fred { +class bankTree : public QTreeView { + Q_OBJECT + public: + bankTree(QWidget*); + void selectionChanged(const QItemSelection& selected, const QItemSelection& deselected) override; + bool getTypeSelected() const; + + protected: + void dragEnterEvent(QDragEnterEvent*) override; + void dropEvent(QDropEvent* event) override; + void dragMoveEvent(QDragMoveEvent*) override; + int typeSelected = -1; + bool currentSelectionIsNotBank; +}; } // namespace fso::fred \ No newline at end of file From 6ae4dacbb0875bc9e391abb19a60c434dfa51966 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Tue, 12 May 2026 12:10:31 -0500 Subject: [PATCH 05/17] major cleanup and refactor --- qtfred/source_groups.cmake | 2 - .../ShipEditor/ShipWeaponsDialogModel.cpp | 124 +++- .../ShipEditor/ShipWeaponsDialogModel.h | 27 +- .../dialogs/ShipEditor/ShipWeaponsDialog.cpp | 566 ++++++++++++------ .../ui/dialogs/ShipEditor/ShipWeaponsDialog.h | 158 ++--- qtfred/src/ui/widgets/bankTree.cpp | 120 ++-- qtfred/src/ui/widgets/bankTree.h | 55 +- qtfred/src/ui/widgets/weaponList.cpp | 102 ---- qtfred/src/ui/widgets/weaponList.h | 38 -- qtfred/ui/ShipWeaponsDialog.ui | 436 +++++++++----- 10 files changed, 947 insertions(+), 681 deletions(-) delete mode 100644 qtfred/src/ui/widgets/weaponList.cpp delete mode 100644 qtfred/src/ui/widgets/weaponList.h diff --git a/qtfred/source_groups.cmake b/qtfred/source_groups.cmake index 6b3704bfa26..189b5f4fd40 100644 --- a/qtfred/source_groups.cmake +++ b/qtfred/source_groups.cmake @@ -330,8 +330,6 @@ add_file_folder("Source/UI/Widgets" src/ui/widgets/ShipFlagCheckbox.cpp src/ui/widgets/SimpleListSelectDialog.cpp src/ui/widgets/SimpleListSelectDialog.h - src/ui/widgets/weaponList.cpp - src/ui/widgets/weaponList.h src/ui/widgets/MusicComboWidget.cpp src/ui/widgets/MusicComboWidget.h ) diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp index ea3c75a9c18..f43f1efed93 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp @@ -1,5 +1,103 @@ #include "ShipWeaponsDialogModel.h" + +#include + +#include + namespace fso::fred { +WeaponItem::WeaponItem(int inID, QString inName, bool inAllowed) + : name(std::move(inName)), id(inID), allowed(inAllowed) +{ +} + +WeaponModel::WeaponModel(int type, int shipClass, bool bigShip) +{ + weapons.push_back(new WeaponItem(-1, "None", true)); + + const bool haveShipInfo = shipClass >= 0 && shipClass < ship_info_size(); + // allowed_weapons is a player-loadout concept and only meaningful for fighters/bombers. + // On other ship classes (capships, support, etc.) every weapon is rendered as normal. + const bool applyAllowedTint = haveShipInfo && Ship_info[shipClass].is_fighter_bomber(); + + const int wantedSubtype = (type == 0) ? WP_LASER : WP_MISSILE; + const bool acceptBeams = (type == 0); + + for (int i = 0; i < static_cast(Weapon_info.size()); i++) { + const auto& w = Weapon_info[i]; + if (w.wi_flags[Weapon::Info_Flags::No_fred]) { + continue; + } + if (w.wi_flags[Weapon::Info_Flags::Child]) { + continue; + } + const bool subtypeMatches = (w.subtype == wantedSubtype) || (acceptBeams && w.subtype == WP_BEAM); + if (!subtypeMatches) { + continue; + } + if (!bigShip && w.wi_flags[Weapon::Info_Flags::Big_only]) { + continue; + } + const bool allowed = !applyAllowedTint || Ship_info[shipClass].allowed_weapons[i] != 0; + weapons.push_back(new WeaponItem(i, w.name, allowed)); + } +} +WeaponModel::~WeaponModel() +{ + for (auto pointer : weapons) { + delete pointer; + } +} +int WeaponModel::rowCount(const QModelIndex& parent) const +{ + Q_UNUSED(parent); + return static_cast(weapons.size()); +} +QVariant WeaponModel::data(const QModelIndex& index, int role) const +{ + if (!index.isValid() || index.row() < 0 || index.row() >= weapons.size()) { + return {}; + } + const auto* item = weapons[index.row()]; + switch (role) { + case Qt::DisplayRole: + return item->name; + case Qt::UserRole: + return item->id; + case Qt::ForegroundRole: + return item->allowed ? QVariant() : QVariant(QBrush(Qt::gray)); + case Qt::ToolTipRole: + return item->allowed ? QVariant() : QVariant(QStringLiteral("Not in this ship class's allowed weapons list.")); + default: + return {}; + } +} +Qt::ItemFlags WeaponModel::flags(const QModelIndex& index) const +{ + auto base = QAbstractListModel::flags(index); + if (index.isValid()) { + base |= Qt::ItemIsDragEnabled; + } + return base; +} +QStringList WeaponModel::mimeTypes() const +{ + return {QStringLiteral("application/weaponid")}; +} +QMimeData* WeaponModel::mimeData(const QModelIndexList& indexes) const +{ + auto mimeData = new QMimeData(); + QByteArray encodedData; + QDataStream stream(&encodedData, QIODevice::WriteOnly); + for (auto& index : indexes) { + if (index.isValid()) { + int id = data(index, Qt::UserRole).toInt(); + stream << id; + } + } + mimeData->setData("application/weaponid", encodedData); + return mimeData; +} + Banks::Banks(SCP_string _name, int aiIndex, int _ship, int multiedit, int _id, ship_subsys* _subsys) : m_isMultiEdit(multiedit), name(std::move(_name)), subsys(_subsys), initalAI(aiIndex), ship(_ship), id(_id) { @@ -13,10 +111,10 @@ void Banks::add(Bank* bank) { banks.push_back(bank); } -Bank* Banks::getByBankId(const int id) +Bank* Banks::getByBankId(const int bankId) { for (auto bank : banks) { - if (id == bank->getWeaponId()) + if (bankId == bank->getWeaponId()) return bank; } return nullptr; @@ -103,6 +201,12 @@ int Bank::getMaxAmmo() const void Bank::setWeapon(const int id) { weaponId = id; + if (id < 0) { + // "None" or CONFLICT placeholder... no weapon assigned, no ammo capacity. + ammoMax = 0; + ammo = 0; + return; + } if (Weapon_info[id].subtype == WP_LASER || Weapon_info[id].subtype == WP_BEAM) { if (parent->getName() == "Pilot") { ammoMax = get_max_ammo_count_for_primary_bank(parent->getShip(), bankId, id); @@ -116,6 +220,9 @@ void Bank::setWeapon(const int id) ammoMax = get_max_ammo_count_for_turret_bank(&parent->getSubsys()->weapons, bankId, id); } } + if (ammo > ammoMax) { + ammo = ammoMax; + } } void Bank::setAmmo(const int newAmmo) { @@ -377,6 +484,19 @@ SCP_vector ShipWeaponsDialogModel::getSecondaryBanks() const { return SecondaryBanks; } +int ShipWeaponsDialogModel::getShipClass() const +{ + return Ships[m_ship].ship_info_index; +} +bool ShipWeaponsDialogModel::isBigShip() const +{ + return big; +} +void ShipWeaponsDialogModel::notifyChanged() +{ + set_modified(); + modelChanged(); +} /* void ShipWeaponsDialogModel::initTertiary(int inst, bool first) { } diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h index e5b0b483ac9..d96c926d6d3 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h @@ -4,7 +4,29 @@ #include +#include +#include + namespace fso::fred { +struct WeaponItem { + WeaponItem(int id, QString name, bool allowed); + const QString name; + const int id; + const bool allowed; +}; +class WeaponModel : public QAbstractListModel { + Q_OBJECT + public: + WeaponModel(int type, int shipClass, bool bigShip); + ~WeaponModel() override; + int rowCount(const QModelIndex& parent = QModelIndex()) const override; + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + Qt::ItemFlags flags(const QModelIndex& index) const override; + QStringList mimeTypes() const override; + QMimeData* mimeData(const QModelIndexList& indexes) const override; + QVector weapons; +}; + struct Bank; struct Banks { Banks(SCP_string name, int aiIndex, int ship, int multiedit, int _id, ship_subsys* subsys = nullptr); @@ -12,7 +34,7 @@ struct Banks { public: int getId() const; void add(Bank*); - Bank* getByBankId(const int id); + Bank* getByBankId(const int bankId); SCP_string getName() const; int getShip() const; ship_subsys* getSubsys() const; @@ -71,6 +93,9 @@ class ShipWeaponsDialogModel : public AbstractDialogModel { void reject() override; SCP_vector getPrimaryBanks() const; SCP_vector getSecondaryBanks() const; + int getShipClass() const; + bool isBigShip() const; + void notifyChanged(); // SCP_vector getTertiaryBanks() const; private: diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp index c45fab140fe..5993b5a621c 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp @@ -1,94 +1,188 @@ #include "ShipWeaponsDialog.h" -#include #include "ui_ShipWeaponsDialog.h" #include +#include #include #include #include -#include +#include + namespace fso::fred::dialogs { + ShipWeaponsDialog::ShipWeaponsDialog(QDialog* parent, EditorViewport* viewport, bool isMultiEdit) - : QDialog(parent), ui(new Ui::ShipWeaponsDialog()), _model(new ShipWeaponsDialogModel(this, viewport, isMultiEdit)), - _viewport(viewport) + : QDialog(parent), ui(new Ui::ShipWeaponsDialog()), + _model(new ShipWeaponsDialogModel(this, viewport, isMultiEdit)), _viewport(viewport) { ui->setupUi(this); - // connect(this, &QDialog::accepted, _model.get(), &ShipWeaponsDialogModel::apply); - - // Build the model of ship weapons and set inital mode. - if (!_model->getPrimaryBanks().empty()) { - const util::SignalBlockers blockers(this); - loadBankModel(_model->getPrimaryBanks()); - //bankModel = new BankTreeModel(_model->getPrimaryBanks(), this); - ui->radioPrimary->setChecked(true); - dialogMode = 0; - weapons = new WeaponModel(0); - } else if (!_model->getSecondaryBanks().empty()) { - const util::SignalBlockers blockers(this); - loadBankModel(_model->getSecondaryBanks()); - //bankModel = new BankTreeModel(_model->getSecondaryBanks(), this); - ui->radioSecondary->setChecked(true); - dialogMode = 1; - weapons = new WeaponModel(1); - } else { + if (_model->getPrimaryBanks().empty() && _model->getSecondaryBanks().empty()) { Error("No Valid Weapon banks on ship"); } - ui->treeBanks->setModel(bankModel); - ui->listWeapons->setModel(weapons); - - connect(ui->treeBanks->selectionModel()->model(), - &QAbstractItemModel::dataChanged, - this, - &ShipWeaponsDialog::updateUI); - // Update the UI whenever selections change - connect(ui->treeBanks->selectionModel(), - &QItemSelectionModel::selectionChanged, - this, - &ShipWeaponsDialog::updateUI); - connect(ui->listWeapons->selectionModel(), - &QItemSelectionModel::selectionChanged, - this, - &ShipWeaponsDialog::updateUI); - - // Setup ai combo box - // connect(ui->AICombo, - // static_cast(&QComboBox::currentIndexChanged), - // this, - //&ShipWeaponsDialog::aiClassChanged); - - // Resize Bank view - ui->treeBanks->expandAll(); - ui->treeBanks->header()->setSectionResizeMode(QHeaderView::ResizeToContents); + + // Tertiary banks are not implemented in the game engine so hide the placeholder tab until they are. + ui->tabWidget->removeTab(2); + + initTab(_primary, Primary); + initTab(_secondary, Secondary); + + // Default to the first tab that has banks. + if (_model->getPrimaryBanks().empty() && !_model->getSecondaryBanks().empty()) { + ui->tabWidget->setCurrentIndex(1); + } + updateUI(); } ShipWeaponsDialog::~ShipWeaponsDialog() { - delete bankModel; - delete weapons; + delete _primary.bankModel; + delete _primary.weapons; + delete _secondary.bankModel; + delete _secondary.weapons; +} + +void ShipWeaponsDialog::initTab(TabState& tab, Mode mode) +{ + tab.mode = mode; + + if (mode == Primary) { + tab.tree = ui->primaryTreeBanks; + tab.list = ui->primaryListWeapons; + tab.setAllButton = ui->primarySetAllButton; + tab.tblButton = ui->primaryTblButton; + tab.aiButton = ui->primaryAiButton; + tab.aiCombo = ui->primaryAiCombo; + tab.aiGroup = ui->primaryAiGroup; + } else { + tab.tree = ui->secondaryTreeBanks; + tab.list = ui->secondaryListWeapons; + tab.setAllButton = ui->secondarySetAllButton; + tab.tblButton = ui->secondaryTblButton; + tab.aiButton = ui->secondaryAiButton; + tab.aiCombo = ui->secondaryAiCombo; + tab.aiGroup = ui->secondaryAiGroup; + } + + const util::SignalBlockers blockers(this); + + tab.bankModel = new QStandardItemModel(this); + tab.weapons = new WeaponModel(static_cast(mode), _model->getShipClass(), _model->isBigShip()); + loadBankModel(tab); + tab.tree->setModel(tab.bankModel); + tab.list->setModel(tab.weapons); + tab.tree->expandAll(); + tab.tree->setHeaderHidden(true); + tab.tree->header()->setSectionResizeMode(QHeaderView::ResizeToContents); + + tab.aiCombo->clear(); + for (int i = 0; i < Num_ai_classes; i++) { + tab.aiCombo->addItem(Ai_class_names[i], QVariant(i)); + } + + connect(tab.tree->selectionModel(), &QItemSelectionModel::selectionChanged, this, + [this, &tab](const QItemSelection&, const QItemSelection&) { updateTabUI(tab); }); + connect(tab.list->selectionModel(), &QItemSelectionModel::selectionChanged, this, + [this, &tab](const QItemSelection&, const QItemSelection&) { updateTabUI(tab); }); + connect(tab.setAllButton, &QPushButton::clicked, this, [this, &tab]() { onSetAllClicked(tab); }); + connect(tab.aiButton, &QPushButton::clicked, this, [this, &tab]() { onAiButtonClicked(tab); }); + connect(tab.tblButton, &QPushButton::clicked, this, [this, &tab]() { onTblButtonClicked(tab); }); + connect(tab.aiCombo, QOverload::of(&QComboBox::currentIndexChanged), this, + [this, &tab](int idx) { onAiComboChanged(tab, idx); }); + connect(tab.bankModel, &QStandardItemModel::itemChanged, this, + [this, &tab](QStandardItem* item) { onBankItemChanged(tab, item); }); + + const auto banks = banksForMode(mode); + const int tabIndex = (mode == Primary) ? 0 : 1; + ui->tabWidget->setTabEnabled(tabIndex, !banks.empty()); +} + +SCP_vector ShipWeaponsDialog::banksForMode(Mode mode) const +{ + switch (mode) { + case Primary: + return _model->getPrimaryBanks(); + case Secondary: + return _model->getSecondaryBanks(); + default: + return {}; + } +} + +SCP_string ShipWeaponsDialog::banksLabel(const Banks* banks) const +{ + if (banks->getName() == "Pilot") { + return banks->getName(); + } + return banks->getName() + " ( " + Ai_class_names[banks->getAiClass()] + " ) "; +} + +void ShipWeaponsDialog::loadBankModel(TabState& tab) +{ + const util::SignalBlockers blockers(this); + tab.internalUpdate = true; + tab.bankModel->removeRows(0, tab.bankModel->rowCount()); + tab.bankModel->setColumnCount(2); + for (auto banks : banksForMode(tab.mode)) { + auto nameItem = new QStandardItem(); + const SCP_string name = banksLabel(banks); + nameItem->setData(name.c_str(), Qt::DisplayRole); + nameItem->setData(true, Qt::UserRole + 2); + nameItem->setData(banks->getId(), Qt::UserRole + 3); + nameItem->setData(banks->getAiClass(), Qt::UserRole + 6); + nameItem->setFlags(nameItem->flags() & ~Qt::ItemIsEditable); + auto labelAmmoItem = new QStandardItem(); + labelAmmoItem->setFlags(Qt::NoItemFlags); + tab.bankModel->appendRow({nameItem, labelAmmoItem}); + for (auto bank : banks->getBanks()) { + auto weaponItem = new QStandardItem(); + QString weaponName; + switch (bank->getWeaponId()) { + case -2: + weaponName = "CONFLICT"; + break; + case -1: + weaponName = "None"; + break; + default: + weaponName = Weapon_info[bank->getWeaponId()].name; + } + weaponItem->setData(weaponName, Qt::DisplayRole); + weaponItem->setData(bank->getWeaponId(), Qt::UserRole); + weaponItem->setData(false, Qt::UserRole + 2); + weaponItem->setData(bank->getBankId(), Qt::UserRole + 3); + weaponItem->setData(bank->getAmmo(), Qt::UserRole + 4); + weaponItem->setData(bank->getMaxAmmo(), Qt::UserRole + 5); + weaponItem->setFlags(weaponItem->flags() & ~Qt::ItemIsEditable); + + auto ammoItem = new QStandardItem(); + if (bank->getMaxAmmo() > 0) { + ammoItem->setData(bank->getAmmo(), Qt::DisplayRole); + ammoItem->setData(bank->getAmmo(), Qt::EditRole); + ammoItem->setFlags(ammoItem->flags() | Qt::ItemIsEditable); + } else { + ammoItem->setFlags(Qt::NoItemFlags); + } + nameItem->appendRow({weaponItem, ammoItem}); + } + } + tab.internalUpdate = false; } void ShipWeaponsDialog::accept() { - // If apply() returns true, close the dialog if (_model->apply()) { QDialog::accept(); } - // else: validation failed, don't close } void ShipWeaponsDialog::reject() { - // Asks the user if they want to save changes, if any - // If they do, it runs _model->apply() and returns the success value - // If they don't, it runs _model->reject() and returns true if (rejectOrCloseHandler(this, _model.get(), _viewport)) { - QDialog::reject(); // actually close + QDialog::reject(); } - // else: do nothing, don't close } void ShipWeaponsDialog::closeEvent(QCloseEvent* event) @@ -96,181 +190,255 @@ void ShipWeaponsDialog::closeEvent(QCloseEvent* event) reject(); event->ignore(); } -void ShipWeaponsDialog::on_setAllButton_clicked() + +void ShipWeaponsDialog::on_okAndCancelButtons_accepted() +{ + accept(); +} + +void ShipWeaponsDialog::on_okAndCancelButtons_rejected() { - for (auto& index : ui->treeBanks->selectionModel()->selectedIndexes()) { - bankModel->setData(index, ui->listWeapons->currentIndex().data(Qt::UserRole).toInt(), Qt::UserRole); - //bankModel->setWeapon(index, ui->listWeapons->currentIndex().data(Qt::UserRole).toInt()); + reject(); +} + +void ShipWeaponsDialog::onSetAllClicked(TabState& tab) +{ + const QModelIndex weaponIdx = tab.list->currentIndex(); + if (!weaponIdx.isValid()) { + return; + } + const int weaponId = weaponIdx.data(Qt::UserRole).toInt(); + bool anyChanged = false; + for (const QModelIndex& index : tab.tree->selectionModel()->selectedIndexes()) { + Bank* bank = bankForIndex(tab, index); + if (bank == nullptr) { + continue; + } + if (bank->getWeaponId() == weaponId) { + continue; + } + bank->setWeapon(weaponId); + refreshBankItem(tab, index); + anyChanged = true; + } + if (anyChanged) { + _model->notifyChanged(); } } -void ShipWeaponsDialog::on_tblButton_clicked() + +void ShipWeaponsDialog::onAiButtonClicked(TabState& tab) { - int wc = ui->listWeapons->currentIndex().data(Qt::UserRole).toInt(); - if (wc >= 0) { - auto dialog = new TableViewerDialog(this, _viewport, "Weapon TBL Data", - "weapons.tbl", "*-wep.tbm", Weapon_info[wc].name); - dialog->show(); + bool anyChanged = false; + for (const QModelIndex& index : tab.tree->selectionModel()->selectedIndexes()) { + Banks* banks = banksForIndex(tab, index); + if (banks == nullptr) { + continue; + } + if (banks->getAiClass() == tab.currentAI) { + continue; + } + banks->setAiClass(tab.currentAI); + refreshBankItem(tab, index); + anyChanged = true; + } + if (anyChanged) { + _model->notifyChanged(); } } -void ShipWeaponsDialog::on_radioPrimary_toggled(bool checked) + +void ShipWeaponsDialog::onTblButtonClicked(TabState& tab) { - modeChanged(checked, 0); + const int wc = tab.list->currentIndex().data(Qt::UserRole).toInt(); + if (wc >= 0) { + auto dialog = new TableViewerDialog(this, _viewport, "Weapon TBL Data", "weapons.tbl", "*-wep.tbm", + Weapon_info[wc].name); + dialog->show(); + } } -void ShipWeaponsDialog::on_radioSecondary_toggled(bool checked) + +void ShipWeaponsDialog::onAiComboChanged(TabState& tab, int index) { - modeChanged(checked, 1); + tab.currentAI = tab.aiCombo->itemData(index).toInt(); } -void ShipWeaponsDialog::on_radioTertiary_toggled(bool checked) + +void ShipWeaponsDialog::updateUI() { - modeChanged(checked, 2); + updateTabUI(_primary); + updateTabUI(_secondary); } -void ShipWeaponsDialog::on_aiCombo_currentIndexChanged(int index) + +void ShipWeaponsDialog::updateTabUI(TabState& tab) { - aiClassChanged(index); + const util::SignalBlockers blockers(this); + + tab.tree->expandAll(); + + const auto selType = tab.tree->getSelectionType(); + tab.setAllButton->setEnabled(selType == bankTree::SelectionType::Weapon); + + // Pilot AI maps to Ships[].weapons.ai_class, which the Ship Editor also owns. Keep that single + // point of truth. A slight regression from old FRED but a more clear separation of responsibilities. + bool aiEditable = (selType == bankTree::SelectionType::Bank); + if (selType == bankTree::SelectionType::Bank) { + Banks* firstBanks = nullptr; + for (const QModelIndex& idx : tab.tree->selectionModel()->selectedIndexes()) { + if (idx.column() != 0) { + continue; + } + Banks* banks = banksForIndex(tab, idx); + if (banks == nullptr) { + continue; + } + if (firstBanks == nullptr) { + firstBanks = banks; + } + if (banks->getName() == "Pilot") { + aiEditable = false; + } + } + if (firstBanks != nullptr) { + tab.currentAI = firstBanks->getAiClass(); + } + } + tab.aiGroup->setEnabled(aiEditable); + tab.aiCombo->setCurrentIndex(tab.aiCombo->findData(tab.currentAI)); + + const bool hasWeaponSelection = tab.list->selectionModel()->hasSelection() && + tab.list->currentIndex().data(Qt::UserRole).toInt() != -1; + tab.tblButton->setEnabled(hasWeaponSelection); } -void ShipWeaponsDialog::modeChanged(const bool enabled, const int mode) + +Banks* ShipWeaponsDialog::banksForIndex(const TabState& tab, const QModelIndex& idx) const { - if (enabled) { - if (mode == 0) { - loadBankModel(_model->getPrimaryBanks()); - //bankModel = new BankTreeModel(_model->getPrimaryBanks(), this); - dialogMode = 0; - delete weapons; - weapons = new WeaponModel(0); - ui->listWeapons->setModel(weapons); - } else if (mode == 1) { - loadBankModel(_model->getSecondaryBanks()); - //bankModel = new BankTreeModel(_model->getSecondaryBanks(), this); - dialogMode = 1; - delete weapons; - weapons = new WeaponModel(1); - ui->listWeapons->setModel(weapons); - } else if (mode == 2) { - // bankModel = new BankTreeModel(_model->getTertiaryBanks(), this); - dialogMode = 2; - } else { - _viewport->dialogProvider->showButtonDialog(DialogType::Error, - "Illegal Mode", - "Somehow an Illegal mode has been set. Get a coder.\n Illegal mode is " + std::to_string(mode), - {DialogButton::Ok}); - ui->radioPrimary->toggled(true); - loadBankModel(_model->getPrimaryBanks()); - //bankModel = new BankTreeModel(_model->getPrimaryBanks(), this); - dialogMode = 0; + if (!idx.isValid()) { + return nullptr; + } + if (!idx.data(Qt::UserRole + 2).toBool()) { + return nullptr; + } + const int banksId = idx.data(Qt::UserRole + 3).toInt(); + for (Banks* banks : banksForMode(tab.mode)) { + if (banks->getId() == banksId) { + return banks; } - // Reconnect beacuse the model has changed - connect(ui->treeBanks->selectionModel()->model(), - &QAbstractItemModel::dataChanged, - this, - &ShipWeaponsDialog::updateUI); - connect(ui->treeBanks->selectionModel(), - &QItemSelectionModel::selectionChanged, - this, - &ShipWeaponsDialog::updateUI); - connect(ui->listWeapons->selectionModel(), - &QItemSelectionModel::selectionChanged, - this, - &ShipWeaponsDialog::updateUI); - ui->treeBanks->setModel(bankModel); - ui->treeBanks->expandAll(); } - updateUI(); + return nullptr; } -void ShipWeaponsDialog::updateUI() + +Bank* ShipWeaponsDialog::bankForIndex(const TabState& tab, const QModelIndex& idx) const { - const util::SignalBlockers blockers(this); - // Radio Buttons - ui->radioPrimary->setEnabled(!_model->getPrimaryBanks().empty()); - ui->radioSecondary->setEnabled(!_model->getSecondaryBanks().empty()); - ui->radioTertiary->setEnabled(false); - - ui->treeBanks->expandAll(); - // Setall button - if (ui->treeBanks->getTypeSelected() == false) { - ui->setAllButton->setEnabled(true); - } else { - ui->setAllButton->setEnabled(false); + if (!idx.isValid()) { + return nullptr; } - // Change AI Button - if (ui->treeBanks->getTypeSelected() == true) { - ui->aiButton->setEnabled(true); - } else { - ui->aiButton->setEnabled(false); + if (idx.data(Qt::UserRole + 2).toBool()) { + return nullptr; } - // AI Combo Box - ui->aiCombo->clear(); - for (int i = 0; i < Num_ai_classes; i++) { - ui->aiCombo->addItem(Ai_class_names[i], QVariant(i)); + const QModelIndex parent = idx.parent(); + if (!parent.isValid()) { + return nullptr; } - ui->aiCombo->setCurrentIndex(ui->aiCombo->findData(m_currentAI)); - if (ui->listWeapons->selectionModel()->hasSelection() && - ui->listWeapons->currentIndex().data(Qt::UserRole).toInt() != -1) { - ui->tblButton->setEnabled(true); - } else { - ui->tblButton->setEnabled(false); + Banks* parentBanks = banksForIndex(tab, parent); + if (parentBanks == nullptr) { + return nullptr; + } + const int bankId = idx.data(Qt::UserRole + 3).toInt(); + for (Bank* bank : parentBanks->getBanks()) { + if (bank->getBankId() == bankId) { + return bank; + } } + return nullptr; } -void ShipWeaponsDialog::aiClassChanged(const int index) +void ShipWeaponsDialog::refreshBankItem(TabState& tab, const QModelIndex& idx) { - m_currentAI = ui->aiCombo->itemData(index).toInt(); -} + if (!idx.isValid() || tab.bankModel == nullptr) { + return; + } + const QModelIndex col0 = idx.sibling(idx.row(), 0); + tab.internalUpdate = true; + if (col0.data(Qt::UserRole + 2).toBool()) { + Banks* banks = banksForIndex(tab, col0); + if (banks != nullptr) { + const SCP_string name = banksLabel(banks); + tab.bankModel->setData(col0, name.c_str(), Qt::DisplayRole); + tab.bankModel->setData(col0, banks->getAiClass(), Qt::UserRole + 6); + } + tab.internalUpdate = false; + return; + } + Bank* bank = bankForIndex(tab, col0); + if (bank == nullptr) { + tab.internalUpdate = false; + return; + } + QString name; + switch (bank->getWeaponId()) { + case -2: + name = "CONFLICT"; + break; + case -1: + name = "None"; + break; + default: + name = Weapon_info[bank->getWeaponId()].name; + break; + } + tab.bankModel->setData(col0, name, Qt::DisplayRole); + tab.bankModel->setData(col0, bank->getWeaponId(), Qt::UserRole); + tab.bankModel->setData(col0, bank->getAmmo(), Qt::UserRole + 4); + tab.bankModel->setData(col0, bank->getMaxAmmo(), Qt::UserRole + 5); -void ShipWeaponsDialog::loadBankModel(SCP_vector modelBanks) { - const util::SignalBlockers blockers(this); - bankModel = new QStandardItemModel(this); - bankModel->removeRows(0, bankModel->rowCount()); - for (auto banks : modelBanks) { - auto item = new QStandardItem(); - const SCP_string name = banks->getName() + " ( " + Ai_class_names[banks->getAiClass()] + " ) "; - item->setData(name.c_str(), Qt::DisplayRole); - item->setData(true, Qt::UserRole + 2); - item->setData(banks->getId(), Qt::UserRole + 3); - item->setData(banks->getAiClass(), Qt::UserRole + 6); - item->setFlags(item->flags() & ~Qt::ItemIsEditable); - bankModel->appendRow(item); - //parent->insertLabel(parent->childCount(), banks->getName().c_str(), banks); - //auto currentParent = parent->child(parent->childCount() - 1); - for (auto bank : banks->getBanks()) { - auto subitem = new QStandardItem(); - QString string; - switch (bank->getWeaponId()) { - case -2: - string = "CONFLICT"; - break; - case -1: - string = "None"; - break; - default: - string = Weapon_info[bank->getWeaponId()].name; + const QModelIndex col1 = idx.sibling(idx.row(), 1); + if (col1.isValid()) { + QStandardItem* ammoItem = tab.bankModel->itemFromIndex(col1); + if (ammoItem != nullptr) { + if (bank->getMaxAmmo() > 0) { + ammoItem->setData(bank->getAmmo(), Qt::DisplayRole); + ammoItem->setData(bank->getAmmo(), Qt::EditRole); + ammoItem->setFlags(ammoItem->flags() | Qt::ItemIsEditable | Qt::ItemIsSelectable | Qt::ItemIsEnabled); + } else { + ammoItem->setData(QVariant(), Qt::DisplayRole); + ammoItem->setData(QVariant(), Qt::EditRole); + ammoItem->setFlags(Qt::NoItemFlags); } - subitem->setData(string, Qt::DisplayRole); - subitem->setData(bank->getWeaponId(), Qt::UserRole); - subitem->setData(false, Qt::UserRole + 2); - subitem->setData(bank->getBankId(), Qt::UserRole + 3); - subitem->setData(bank->getAmmo(), Qt::UserRole + 4); - subitem->setData(bank->getMaxAmmo(), Qt::UserRole + 5); - subitem->setFlags(item->flags() & ~Qt::ItemIsEditable); - item->appendRow(subitem); - //currentParent->insertBank(currentParent->childCount(), bank); } } + tab.internalUpdate = false; } -void ShipWeaponsDialog::on_aiButton_clicked() +void ShipWeaponsDialog::onBankItemChanged(TabState& tab, QStandardItem* item) { - for (auto& index : ui->treeBanks->selectionModel()->selectedIndexes()) { - bankModel->setData(index, m_currentAI, Qt::UserRole + 6); - //bankModel->setData(index, m_currentAI, Qt::DisplayRole); - const SCP_string oldName = bankModel->data(index, Qt::DisplayRole).toString().toStdString(); - const size_t end = oldName.find(' '); - const SCP_string justname = oldName.substr(0, end); + if (item == nullptr || tab.internalUpdate) { + return; } + if (item->column() != 1) { + return; + } + const QModelIndex col0 = item->index().sibling(item->row(), 0); + if (!col0.isValid() || col0.data(Qt::UserRole + 2).toBool()) { + return; + } + Bank* bank = bankForIndex(tab, col0); + if (bank == nullptr) { + return; + } + bool ok = false; + int requested = item->data(Qt::EditRole).toInt(&ok); + if (!ok) { + requested = bank->getAmmo(); + } + const int clamped = std::max(0, std::min(requested, bank->getMaxAmmo())); + if (clamped != bank->getAmmo()) { + bank->setAmmo(clamped); + _model->notifyChanged(); + } + // Always write back the canonical value. This covers the case where the user typed an out-of-range + // number and our clamp differs from what they entered. + tab.internalUpdate = true; + item->setData(clamped, Qt::DisplayRole); + item->setData(clamped, Qt::EditRole); + tab.internalUpdate = false; } -void ShipWeaponsDialog::on_buttonClose_clicked() -{ - accept(); -} - -} // namespace fso::fred::dialogs \ No newline at end of file +} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h index f12a382dbaf..cb67a8ceeac 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h @@ -1,76 +1,82 @@ -#ifndef SHIPWEAPONSDIALOG_H -#define SHIPWEAPONSDIALOG_H - -#include "ui/dialogs/ShipEditor/BankModel.h" -#include "ui/widgets/weaponList.h" - -#include - -#include -#include - -namespace fso::fred::dialogs { -//Weapon ID = UserRole -//Wapon name = DisplayRole -//Bank or subsys = UserRole + 2 (true = subsys) false = bank -// id = UserRole + 3 -// ammo = UserRole + 4 -// maxammo = UserRole + 5 - // aiclass = Qt::UserRole + 6 -namespace Ui { -class ShipWeaponsDialog; -} -/** - * @brief QTFred's Weapons Editor - */ -class ShipWeaponsDialog : public QDialog { - Q_OBJECT - - public: - /** - * @brief QTFred's Weapons Editor Constructer. - * @param [in/out] parent The dialogs parent. - * @param [in/out] viewport Editor viewport. - * @param [in] isMultiEdit If editing multiple ships. - */ - explicit ShipWeaponsDialog(QDialog* parent, EditorViewport* viewport, bool isMultiEdit); - ~ShipWeaponsDialog() override; - - void accept() override; - void reject() override; - - protected: - void closeEvent(QCloseEvent*) override; - - private slots: - void on_buttonClose_clicked(); - void on_aiButton_clicked(); - void on_setAllButton_clicked(); - void on_tblButton_clicked(); - void on_radioPrimary_toggled(bool checked); - void on_radioSecondary_toggled(bool checked); - void on_radioTertiary_toggled(bool checked); - void on_aiCombo_currentIndexChanged(int index); - - private: // NOLINT(readability-redundant-access-specifiers) - std::unique_ptr ui; - std::unique_ptr _model; - /** - * @brief Changes current weapon type. - * @param [in] enabled Always True - * @param [in] mode The mode to change to. 0 = Primary, 1 = Secondary - */ - void modeChanged(const bool enabled, const int mode); - EditorViewport* _viewport; - void updateUI(); - QStandardItemModel* bankModel; - int dialogMode; - WeaponModel* weapons; - int m_currentAI = 0; - void aiClassChanged(const int index); - - void loadBankModel(SCP_vector); - -}; -} // namespace fso::fred::dialogs -#endif \ No newline at end of file +#ifndef SHIPWEAPONSDIALOG_H +#define SHIPWEAPONSDIALOG_H + +#include "ui/dialogs/ShipEditor/BankModel.h" +#include "ui/widgets/bankTree.h" + +#include + +#include +#include +#include +#include +#include + +namespace fso::fred::dialogs { + +namespace Ui { +class ShipWeaponsDialog; +} + +class ShipWeaponsDialog : public QDialog { + Q_OBJECT + + public: + explicit ShipWeaponsDialog(QDialog* parent, EditorViewport* viewport, bool isMultiEdit); + ~ShipWeaponsDialog() override; + + void accept() override; + void reject() override; + + protected: + void closeEvent(QCloseEvent*) override; + + private slots: + void on_okAndCancelButtons_accepted(); + void on_okAndCancelButtons_rejected(); + + private: + enum Mode { Primary = 0, Secondary = 1, Tertiary = 2 }; + + struct TabState { + Mode mode = Primary; + bankTree* tree = nullptr; + QListView* list = nullptr; + QPushButton* setAllButton = nullptr; + QPushButton* tblButton = nullptr; + QPushButton* aiButton = nullptr; + QComboBox* aiCombo = nullptr; + QWidget* aiGroup = nullptr; + QStandardItemModel* bankModel = nullptr; + WeaponModel* weapons = nullptr; + int currentAI = 0; + // Set while the dialog itself is writing into bankModel, so itemChanged handlers can ignore the resulting signals. + bool internalUpdate = false; + }; + + void initTab(TabState& tab, Mode mode); + void loadBankModel(TabState& tab); + void updateTabUI(TabState& tab); + void updateUI(); + + void onSetAllClicked(TabState& tab); + void onAiButtonClicked(TabState& tab); + void onTblButtonClicked(TabState& tab); + void onAiComboChanged(TabState& tab, int index); + void onBankItemChanged(TabState& tab, QStandardItem* item); + + Bank* bankForIndex(const TabState& tab, const QModelIndex& idx) const; + Banks* banksForIndex(const TabState& tab, const QModelIndex& idx) const; + void refreshBankItem(TabState& tab, const QModelIndex& idx); + SCP_vector banksForMode(Mode mode) const; + SCP_string banksLabel(const Banks* banks) const; + + std::unique_ptr ui; + std::unique_ptr _model; + EditorViewport* _viewport; + + TabState _primary; + TabState _secondary; +}; +} // namespace fso::fred::dialogs +#endif diff --git a/qtfred/src/ui/widgets/bankTree.cpp b/qtfred/src/ui/widgets/bankTree.cpp index fd198610899..93c562c67d8 100644 --- a/qtfred/src/ui/widgets/bankTree.cpp +++ b/qtfred/src/ui/widgets/bankTree.cpp @@ -34,92 +34,62 @@ void bankTree::dragMoveEvent(QDragMoveEvent* event) event->ignore(); } } -/* void bankTree::selectionChanged(const QItemSelection& selected, const QItemSelection& deselected) -{ - auto indexes = selected.indexes(); - if (!indexes.isEmpty()) { - auto& first = indexes.first(); - if (first.isValid()) { - if (model()->data(first, Qt::UserRole + 2) == true) { - for (auto& index : - model()->match(model()->index(0, 0), Qt::UserRole + 2, false, -1, Qt::MatchRecursive)) { - if (index.isValid()) { - auto item = dynamic_cast(model())->itemFromIndex(index); - Qt::ItemFlags flags = item->flags(); - flags &= ~Qt::ItemIsSelectable; - item->setFlags(flags); - } - } - } else { - for (auto& index : - model()->match(model()->index(0, 0), Qt::UserRole + 2, true, -1, Qt::MatchRecursive)) { - if (index.isValid()) { - auto item = dynamic_cast(model())->itemFromIndex(index); - Qt::ItemFlags flags = item->flags(); - flags &= ~Qt::ItemIsSelectable; - item->setFlags(flags); - } - } - } - } - } - QTreeView::selectionChanged(selected, deselected); -}*/ void bankTree::selectionChanged(const QItemSelection& selected, const QItemSelection& deselected) { - // 1. Update the internal selection state first QTreeView::selectionChanged(selected, deselected); - QItemSelectionModel* sm = selectionModel(); - QStandardItemModel* m = qobject_cast(model()); - if (!m) + if (m_autoFiltering) { return; + } - // Helper lambda to update the selectable flag across the whole tree - auto updateTreeFlags = [&](bool isFiltering, bool filterForBank) { - std::function traverse = [&](const QModelIndex& parent) { - for (int r = 0; r < m->rowCount(parent); ++r) { - QModelIndex idx = m->index(r, 0, parent); - QStandardItem* item = m->itemFromIndex(idx); - if (!item) - continue; - - if (!isFiltering) { - // Reset mode: Everything becomes selectable - item->setFlags(item->flags() | Qt::ItemIsSelectable); - } else { - // Filter mode: Only items matching the 'bank' status stay selectable - bool itemIsBank = m->data(idx, Qt::UserRole + 2).toBool(); - if (itemIsBank == filterForBank) { - item->setFlags(item->flags() | Qt::ItemIsSelectable); - } else { - item->setFlags(item->flags() & ~Qt::ItemIsSelectable); - } - } - - if (m->hasChildren(idx)) - traverse(idx); - } - }; - traverse(QModelIndex()); - }; + const auto newlySelected = selected.indexes(); + if (newlySelected.isEmpty()) { + return; + } - // 2. Handle the "Last Item Unselected" case (prevents the crash) - updateTreeFlags(false, false); // Disable filtering, reset all to selectable + QModelIndex pivot; + for (const QModelIndex& idx : newlySelected) { + if (idx.column() == 0 && idx.isValid()) { + pivot = idx; + break; + } + } + if (!pivot.isValid()) { + return; + } + const bool pivotIsBank = pivot.data(Qt::UserRole + 2).toBool(); - // 3. We have a selection, so determine the current "Master Type" - // safe because we checked hasSelection() - if (!sm->selectedIndexes().empty()) { - QModelIndex first = sm->selectedIndexes().first(); - if (first.isValid()) { - currentSelectionIsNotBank = m->data(first, Qt::UserRole + 2).toBool(); - updateTreeFlags(true, currentSelectionIsNotBank); + QItemSelectionModel* sm = selectionModel(); + QItemSelection toDeselect; + for (const QModelIndex& idx : sm->selectedIndexes()) { + if (idx.column() != 0) { + continue; + } + if (idx.data(Qt::UserRole + 2).toBool() != pivotIsBank) { + toDeselect.select(idx, idx); } } + + if (!toDeselect.isEmpty()) { + m_autoFiltering = true; + sm->select(toDeselect, QItemSelectionModel::Deselect | QItemSelectionModel::Rows); + m_autoFiltering = false; + } } -bool bankTree::getTypeSelected() const + +bankTree::SelectionType bankTree::getSelectionType() const { - return currentSelectionIsNotBank; + const auto* sm = selectionModel(); + if (sm == nullptr) { + return SelectionType::None; + } + for (const QModelIndex& idx : sm->selectedIndexes()) { + if (idx.column() != 0 || !idx.isValid()) { + continue; + } + return idx.data(Qt::UserRole + 2).toBool() ? SelectionType::Bank : SelectionType::Weapon; + } + return SelectionType::None; } -} // namespace fso::fred \ No newline at end of file +} // namespace fso::fred diff --git a/qtfred/src/ui/widgets/bankTree.h b/qtfred/src/ui/widgets/bankTree.h index f6a30210f94..75032adfb43 100644 --- a/qtfred/src/ui/widgets/bankTree.h +++ b/qtfred/src/ui/widgets/bankTree.h @@ -1,26 +1,29 @@ -#pragma once -#include "ui/dialogs/ShipEditor/BankModel.h" - -#include - -#include -#include -#include -#include -#include -namespace fso::fred { -class bankTree : public QTreeView { - Q_OBJECT - public: - bankTree(QWidget*); - void selectionChanged(const QItemSelection& selected, const QItemSelection& deselected) override; - bool getTypeSelected() const; - - protected: - void dragEnterEvent(QDragEnterEvent*) override; - void dropEvent(QDropEvent* event) override; - void dragMoveEvent(QDragMoveEvent*) override; - int typeSelected = -1; - bool currentSelectionIsNotBank; -}; -} // namespace fso::fred \ No newline at end of file +#pragma once +#include "ui/dialogs/ShipEditor/BankModel.h" + +#include + +#include +#include +#include +#include +#include +namespace fso::fred { +class bankTree : public QTreeView { + Q_OBJECT + public: + enum class SelectionType { None, Bank, Weapon }; + + bankTree(QWidget*); + void selectionChanged(const QItemSelection& selected, const QItemSelection& deselected) override; + SelectionType getSelectionType() const; + + protected: + void dragEnterEvent(QDragEnterEvent*) override; + void dropEvent(QDropEvent* event) override; + void dragMoveEvent(QDragMoveEvent*) override; + + private: + bool m_autoFiltering = false; +}; +} // namespace fso::fred diff --git a/qtfred/src/ui/widgets/weaponList.cpp b/qtfred/src/ui/widgets/weaponList.cpp deleted file mode 100644 index 78f40afd018..00000000000 --- a/qtfred/src/ui/widgets/weaponList.cpp +++ /dev/null @@ -1,102 +0,0 @@ -#include "weaponList.h" - -namespace fso::fred { -weaponList::weaponList(QWidget* parent) : QListView(parent) {} - -void weaponList::mousePressEvent(QMouseEvent* event) -{ - if (event->button() == Qt::LeftButton) { - dragStartPosition = event->pos(); - } - QListView::mousePressEvent(event); -} -void weaponList::mouseMoveEvent(QMouseEvent* event) -{ - if (!(event->buttons() & Qt::LeftButton)) - return; - if ((event->pos() - dragStartPosition).manhattanLength() < QApplication::startDragDistance()) - return; - QModelIndex idx = currentIndex(); - if (!idx.isValid()) { - return; - } - auto drag = new QDrag(this); - QModelIndexList idxs; - idxs.append(idx); - QMimeData* mimeData = model()->mimeData(idxs); - auto iconPixmap = new QPixmap(); - QPainter painter(iconPixmap); - painter.setFont(QFont("Arial")); - painter.drawText(QPoint(100, 100), model()->data(idx, Qt::DisplayRole).toString()); - drag->setPixmap(*iconPixmap); - drag->setMimeData(mimeData); - drag->exec(); -} - -WeaponModel::WeaponModel(int type) -{ - auto noWeapon = new WeaponItem(-1, "None"); - weapons.push_back(noWeapon); - if (type == 0) { - for (int i = 0; i < static_cast(Weapon_info.size()); i++) { - const auto& w = Weapon_info[i]; - if (w.subtype == WP_LASER || w.subtype == WP_BEAM) { - if (!w.wi_flags[Weapon::Info_Flags::No_fred]) { - auto newWeapon = new WeaponItem(i, w.name); - weapons.push_back(newWeapon); - } - } - } - } else if (type == 1) { - for (int i = 0; i < static_cast(Weapon_info.size()); i++) { - const auto& w = Weapon_info[i]; - if (w.subtype == WP_MISSILE) { - if (!w.wi_flags[Weapon::Info_Flags::No_fred]) { - auto newWeapon = new WeaponItem(i, w.name); - weapons.push_back(newWeapon); - } - } - } - } -} -WeaponModel::~WeaponModel() -{ - for (auto pointer : weapons) { - delete pointer; - } -} -int WeaponModel::rowCount(const QModelIndex& parent) const -{ - Q_UNUSED(parent); - return static_cast(weapons.size()); -} -QVariant WeaponModel::data(const QModelIndex& index, int role) const -{ - if (role == Qt::DisplayRole) { - const QString out = weapons[index.row()]->name; - return out; - } - if (role == Qt::UserRole) { - const int id = weapons[index.row()]->id; - return id; - } - return {}; -} -QMimeData* WeaponModel::mimeData(const QModelIndexList& indexes) const -{ - auto mimeData = new QMimeData(); - QByteArray encodedData; - QDataStream stream(&encodedData, QIODevice::WriteOnly); - for (auto& index : indexes) { - if (index.isValid()) { - int id = data(index, Qt::UserRole).toInt(); - stream << id; - } - } - - mimeData->setData("application/weaponid", encodedData); - - return mimeData; -} -WeaponItem::WeaponItem(const int inID, QString inName) : name(std::move(inName)), id(inID) {} -} // namespace fso::fred \ No newline at end of file diff --git a/qtfred/src/ui/widgets/weaponList.h b/qtfred/src/ui/widgets/weaponList.h deleted file mode 100644 index ed15b120798..00000000000 --- a/qtfred/src/ui/widgets/weaponList.h +++ /dev/null @@ -1,38 +0,0 @@ -#pragma once -#include - -#include -#include -#include -#include -#include -#include -namespace fso::fred { -struct WeaponItem { - WeaponItem(const int id, QString name); - const QString name; - const int id; -}; -class WeaponModel : public QAbstractListModel { - Q_OBJECT - public: - WeaponModel(int type); - ~WeaponModel() override; - int rowCount(const QModelIndex& parent = QModelIndex()) const override; - QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; - QMimeData* mimeData(const QModelIndexList& indexes) const override; - QVector weapons; -}; -class weaponList : public QListView { - Q_OBJECT - public: - weaponList(QWidget* parent); - - protected: - void mousePressEvent(QMouseEvent* event) override; - void mouseMoveEvent(QMouseEvent* event) override; - QPoint dragStartPosition; - - private: -}; -} // namespace fso::fred \ No newline at end of file diff --git a/qtfred/ui/ShipWeaponsDialog.ui b/qtfred/ui/ShipWeaponsDialog.ui index 0d6329025fa..fddefe3159c 100644 --- a/qtfred/ui/ShipWeaponsDialog.ui +++ b/qtfred/ui/ShipWeaponsDialog.ui @@ -18,181 +18,297 @@ - - - Mode + + + 0 - - - - - Primary - - - - - - - Secondary - - - - - - - Tertiary - - - - - - - - - - - - Weapons - - - - - - QAbstractScrollArea::AdjustIgnored + + + Primary + + + + + + + + Weapons + + + + + + QAbstractScrollArea::AdjustIgnored + + + QAbstractItemView::NoEditTriggers + + + true + + + QAbstractItemView::DragOnly + + + + + + + View Table + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Set Selected + + + false + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Banks + + + + + + QAbstractScrollArea::AdjustToContentsOnFirstShow + + + QAbstractItemView::DropOnly + + + QAbstractItemView::ExtendedSelection + + + QAbstractItemView::SelectItems + + + false + + + + + + + + + + + + + 0 - - QAbstractItemView::NoEditTriggers + + 0 - - true + + 0 - - QAbstractItemView::DragOnly + + 0 - - - - - - View Table + + + + + + + Change AI + + + + + + + + + + + Secondary + + + + + + + + Weapons + + + + + + QAbstractScrollArea::AdjustIgnored + + + QAbstractItemView::NoEditTriggers + + + true + + + QAbstractItemView::DragOnly + + + + + + + View Table + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Set Selected + + + false + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Banks + + + + + + QAbstractScrollArea::AdjustToContentsOnFirstShow + + + QAbstractItemView::DropOnly + + + QAbstractItemView::ExtendedSelection + + + QAbstractItemView::SelectItems + + + false + + + + + + + + + + + + + 0 - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Set Selected - - - false - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Banks - - - - - - QAbstractScrollArea::AdjustToContentsOnFirstShow + + 0 - - QAbstractItemView::DropOnly + + 0 - - QAbstractItemView::MultiSelection + + 0 - - QAbstractItemView::SelectItems - - - false - - - - - - - - - - - - - - - - - Change AI - - - - + + + + + + + Change AI + + + + + + + + + + + Tertiary + + + - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Close - - - - + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + - - fso::fred::weaponList - QListView -
ui/widgets/weaponList.h
-
fso::fred::bankTree QTreeView From 7e2812220e4a2baf6de59e16844993303ffe255a Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Tue, 12 May 2026 12:56:36 -0500 Subject: [PATCH 06/17] enable drag and drop --- .../ShipEditor/ShipWeaponsDialogModel.cpp | 85 +++++---- .../dialogs/ShipEditor/ShipWeaponsDialog.cpp | 173 ++++++++++++++++-- .../ui/dialogs/ShipEditor/ShipWeaponsDialog.h | 5 + qtfred/src/ui/widgets/bankTree.cpp | 98 ++++++++-- qtfred/src/ui/widgets/bankTree.h | 7 +- qtfred/ui/ShipWeaponsDialog.ui | 4 +- 6 files changed, 308 insertions(+), 64 deletions(-) diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp index f43f1efed93..06e815c023f 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp @@ -207,22 +207,21 @@ void Bank::setWeapon(const int id) ammo = 0; return; } + const int shipClass = Ships[parent->getShip()].ship_info_index; if (Weapon_info[id].subtype == WP_LASER || Weapon_info[id].subtype == WP_BEAM) { if (parent->getName() == "Pilot") { - ammoMax = get_max_ammo_count_for_primary_bank(parent->getShip(), bankId, id); + ammoMax = get_max_ammo_count_for_primary_bank(shipClass, bankId, id); } else { ammoMax = get_max_ammo_count_for_primary_turret_bank(&parent->getSubsys()->weapons, bankId, id); } } else { if (parent->getName() == "Pilot") { - ammoMax = get_max_ammo_count_for_bank(parent->getShip(), bankId, id); + ammoMax = get_max_ammo_count_for_bank(shipClass, bankId, id); } else { ammoMax = get_max_ammo_count_for_turret_bank(&parent->getSubsys()->weapons, bankId, id); } } - if (ammo > ammoMax) { - ammo = ammoMax; - } + ammo = ammoMax; } void Bank::setAmmo(const int newAmmo) { @@ -275,29 +274,39 @@ void ShipWeaponsDialogModel::initPrimary(int inst, bool first) id++; if (first) { auto pilot = Ships[inst].weapons; - for (int i = 0; i < MAX_SHIP_PRIMARY_BANKS; i++) { - if (pilot.primary_bank_weapons[i] >= 0) { - const int maxAmmo = - get_max_ammo_count_for_primary_bank(Ships[inst].ship_info_index, i, pilot.primary_bank_weapons[i]); - const int ammo = fl2ir(pilot.primary_bank_ammo[i] * maxAmmo / 100.0f); - pilotBank->add(new Bank(pilot.primary_bank_weapons[i], i, maxAmmo, ammo, pilotBank)); + const int shipClass = Ships[inst].ship_info_index; + const int numPilotBanks = Ship_info[shipClass].num_primary_banks; + for (int i = 0; i < numPilotBanks; i++) { + const int weaponId = pilot.primary_bank_weapons[i]; + int maxAmmo = 0; + int ammo = 0; + if (weaponId >= 0) { + maxAmmo = get_max_ammo_count_for_primary_bank(shipClass, i, weaponId); + ammo = fl2ir(pilot.primary_bank_ammo[i] * maxAmmo / 100.0f); } + pilotBank->add(new Bank(weaponId, i, maxAmmo, ammo, pilotBank)); + } + if (!pilotBank->empty()) { + PrimaryBanks.push_back(pilotBank); + } else { + delete pilotBank; } - PrimaryBanks.push_back(pilotBank); ship_subsys* ssl = &Ships[inst].subsys_list; ship_subsys* pss; for (pss = GET_FIRST(ssl); pss != END_OF_LIST(ssl); pss = GET_NEXT(pss)) { model_subsystem* psub = pss->system_info; if (psub->type == SUBSYSTEM_TURRET) { auto turretBank = new Banks(psub->subobj_name, pss->weapons.ai_class, inst, m_isMultiEdit,id, pss); - for (int i = 0; i < MAX_SHIP_PRIMARY_BANKS; i++) { - if (pss->weapons.primary_bank_weapons[i] >= 0) { - const int maxAmmo = get_max_ammo_count_for_primary_turret_bank(&pss->weapons, - i, - pss->weapons.primary_bank_weapons[i]); - const int ammo = fl2ir(pss->weapons.primary_bank_ammo[i] * maxAmmo / 100.0f); - turretBank->add(new Bank(pss->weapons.primary_bank_weapons[i], i, maxAmmo, ammo, turretBank)); + const int numTurretBanks = pss->weapons.num_primary_banks; + for (int i = 0; i < numTurretBanks; i++) { + const int weaponId = pss->weapons.primary_bank_weapons[i]; + int maxAmmo = 0; + int ammo = 0; + if (weaponId >= 0) { + maxAmmo = get_max_ammo_count_for_primary_turret_bank(&pss->weapons, i, weaponId); + ammo = fl2ir(pss->weapons.primary_bank_ammo[i] * maxAmmo / 100.0f); } + turretBank->add(new Bank(weaponId, i, maxAmmo, ammo, turretBank)); } if (!turretBank->empty()) { PrimaryBanks.push_back(turretBank); @@ -345,29 +354,39 @@ void ShipWeaponsDialogModel::initSecondary(int inst, bool first) id++; if (first) { auto pilot = Ships[inst].weapons; - for (int i = 0; i < MAX_SHIP_SECONDARY_BANKS; i++) { - if (pilot.secondary_bank_weapons[i] >= 0) { - const int maxAmmo = - get_max_ammo_count_for_bank(Ships[inst].ship_info_index, i, pilot.secondary_bank_weapons[i]); - const int ammo = fl2ir(pilot.secondary_bank_ammo[i] * maxAmmo / 100.0f); - pilotBank->add(new Bank(pilot.secondary_bank_weapons[i], i, maxAmmo, ammo, pilotBank)); + const int shipClass = Ships[inst].ship_info_index; + const int numPilotBanks = Ship_info[shipClass].num_secondary_banks; + for (int i = 0; i < numPilotBanks; i++) { + const int weaponId = pilot.secondary_bank_weapons[i]; + int maxAmmo = 0; + int ammo = 0; + if (weaponId >= 0) { + maxAmmo = get_max_ammo_count_for_bank(shipClass, i, weaponId); + ammo = fl2ir(pilot.secondary_bank_ammo[i] * maxAmmo / 100.0f); } + pilotBank->add(new Bank(weaponId, i, maxAmmo, ammo, pilotBank)); + } + if (!pilotBank->empty()) { + SecondaryBanks.push_back(pilotBank); + } else { + delete pilotBank; } - SecondaryBanks.push_back(pilotBank); ship_subsys* ssl = &Ships[inst].subsys_list; ship_subsys* pss; for (pss = GET_FIRST(ssl); pss != END_OF_LIST(ssl); pss = GET_NEXT(pss)) { model_subsystem* psub = pss->system_info; if (psub->type == SUBSYSTEM_TURRET) { auto turretBank = new Banks(psub->subobj_name, pss->weapons.ai_class, inst, m_isMultiEdit,id, pss); - for (int i = 0; i < MAX_SHIP_SECONDARY_BANKS; i++) { - if (pss->weapons.secondary_bank_weapons[i] >= 0) { - const int maxAmmo = get_max_ammo_count_for_turret_bank(&pss->weapons, - i, - pss->weapons.secondary_bank_weapons[i]); - const int ammo = fl2ir(pss->weapons.secondary_bank_ammo[i] * maxAmmo / 100.0f); - turretBank->add(new Bank(pss->weapons.secondary_bank_weapons[i], i, maxAmmo, ammo, turretBank)); + const int numTurretBanks = pss->weapons.num_secondary_banks; + for (int i = 0; i < numTurretBanks; i++) { + const int weaponId = pss->weapons.secondary_bank_weapons[i]; + int maxAmmo = 0; + int ammo = 0; + if (weaponId >= 0) { + maxAmmo = get_max_ammo_count_for_turret_bank(&pss->weapons, i, weaponId); + ammo = fl2ir(pss->weapons.secondary_bank_ammo[i] * maxAmmo / 100.0f); } + turretBank->add(new Bank(weaponId, i, maxAmmo, ammo, turretBank)); } if (!turretBank->empty()) { SecondaryBanks.push_back(turretBank); diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp index 5993b5a621c..1e08e8f3e75 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp @@ -9,9 +9,51 @@ #include #include +#include +#include namespace fso::fred::dialogs { +namespace { +// QStandardItem collapses Qt::EditRole into Qt::DisplayRole, so we can't have a formatted string +// in DisplayRole alongside an int in EditRole. Use a custom role for the spinbox's value. +constexpr int AmmoValueRole = Qt::UserRole + 7; + +QString formatAmmoDisplay(int current, int max) +{ + return QString::number(current) + "/" + QString::number(max); +} + +class AmmoSpinBoxDelegate : public QStyledItemDelegate { + public: + using QStyledItemDelegate::QStyledItemDelegate; + + QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& /*option*/, + const QModelIndex& index) const override + { + auto* editor = new QSpinBox(parent); + editor->setMinimum(0); + const QModelIndex col0 = index.sibling(index.row(), 0); + editor->setMaximum(col0.data(Qt::UserRole + 5).toInt()); + editor->setFrame(false); + editor->setAutoFillBackground(true); + return editor; + } + + void setEditorData(QWidget* editor, const QModelIndex& index) const override + { + static_cast(editor)->setValue(index.data(AmmoValueRole).toInt()); + } + + void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const override + { + auto* spin = static_cast(editor); + spin->interpretText(); + model->setData(index, spin->value(), AmmoValueRole); + } +}; +} // namespace + ShipWeaponsDialog::ShipWeaponsDialog(QDialog* parent, EditorViewport* viewport, bool isMultiEdit) : QDialog(parent), ui(new Ui::ShipWeaponsDialog()), _model(new ShipWeaponsDialogModel(this, viewport, isMultiEdit)), _viewport(viewport) @@ -76,6 +118,7 @@ void ShipWeaponsDialog::initTab(TabState& tab, Mode mode) tab.tree->expandAll(); tab.tree->setHeaderHidden(true); tab.tree->header()->setSectionResizeMode(QHeaderView::ResizeToContents); + tab.tree->setItemDelegateForColumn(1, new AmmoSpinBoxDelegate(this)); tab.aiCombo->clear(); for (int i = 0; i < Num_ai_classes; i++) { @@ -93,6 +136,14 @@ void ShipWeaponsDialog::initTab(TabState& tab, Mode mode) [this, &tab](int idx) { onAiComboChanged(tab, idx); }); connect(tab.bankModel, &QStandardItemModel::itemChanged, this, [this, &tab](QStandardItem* item) { onBankItemChanged(tab, item); }); + connect(tab.tree, &bankTree::weaponDroppedFromList, this, + [this, &tab](const QModelIndex& target, int weaponId) { + onWeaponDroppedFromList(tab, target, weaponId); + }); + connect(tab.tree, &bankTree::weaponMoved, this, + [this, &tab](const QModelIndex& target, int weaponId, int sourceBanksId, int sourceBankId, bool isCopy) { + onWeaponMoved(tab, target, weaponId, sourceBanksId, sourceBankId, isCopy); + }); const auto banks = banksForMode(mode); const int tabIndex = (mode == Primary) ? 0 : 1; @@ -155,16 +206,26 @@ void ShipWeaponsDialog::loadBankModel(TabState& tab) weaponItem->setData(bank->getBankId(), Qt::UserRole + 3); weaponItem->setData(bank->getAmmo(), Qt::UserRole + 4); weaponItem->setData(bank->getMaxAmmo(), Qt::UserRole + 5); - weaponItem->setFlags(weaponItem->flags() & ~Qt::ItemIsEditable); + Qt::ItemFlags weaponFlags = weaponItem->flags() & ~Qt::ItemIsEditable; + // Only slots that have a real weapon can be dragged out. + if (bank->getWeaponId() >= 0) { + weaponFlags |= Qt::ItemIsDragEnabled; + } else { + weaponFlags &= ~Qt::ItemIsDragEnabled; + } + weaponItem->setFlags(weaponFlags); auto ammoItem = new QStandardItem(); + Qt::ItemFlags ammoFlags = Qt::ItemIsSelectable | Qt::ItemIsEnabled; if (bank->getMaxAmmo() > 0) { - ammoItem->setData(bank->getAmmo(), Qt::DisplayRole); - ammoItem->setData(bank->getAmmo(), Qt::EditRole); - ammoItem->setFlags(ammoItem->flags() | Qt::ItemIsEditable); + ammoItem->setData(formatAmmoDisplay(bank->getAmmo(), bank->getMaxAmmo()), Qt::DisplayRole); + ammoItem->setData(bank->getAmmo(), AmmoValueRole); + ammoFlags |= Qt::ItemIsEditable; } else { - ammoItem->setFlags(Qt::NoItemFlags); + ammoItem->setData(QString(), Qt::DisplayRole); + ammoItem->setData(QVariant(), AmmoValueRole); } + ammoItem->setFlags(ammoFlags); nameItem->appendRow({weaponItem, ammoItem}); } } @@ -388,20 +449,30 @@ void ShipWeaponsDialog::refreshBankItem(TabState& tab, const QModelIndex& idx) tab.bankModel->setData(col0, bank->getWeaponId(), Qt::UserRole); tab.bankModel->setData(col0, bank->getAmmo(), Qt::UserRole + 4); tab.bankModel->setData(col0, bank->getMaxAmmo(), Qt::UserRole + 5); + if (QStandardItem* weaponItem = tab.bankModel->itemFromIndex(col0)) { + Qt::ItemFlags f = weaponItem->flags(); + if (bank->getWeaponId() >= 0) { + f |= Qt::ItemIsDragEnabled; + } else { + f &= ~Qt::ItemIsDragEnabled; + } + weaponItem->setFlags(f); + } const QModelIndex col1 = idx.sibling(idx.row(), 1); if (col1.isValid()) { QStandardItem* ammoItem = tab.bankModel->itemFromIndex(col1); if (ammoItem != nullptr) { + Qt::ItemFlags ammoFlags = Qt::ItemIsSelectable | Qt::ItemIsEnabled; if (bank->getMaxAmmo() > 0) { - ammoItem->setData(bank->getAmmo(), Qt::DisplayRole); - ammoItem->setData(bank->getAmmo(), Qt::EditRole); - ammoItem->setFlags(ammoItem->flags() | Qt::ItemIsEditable | Qt::ItemIsSelectable | Qt::ItemIsEnabled); + ammoItem->setData(formatAmmoDisplay(bank->getAmmo(), bank->getMaxAmmo()), Qt::DisplayRole); + ammoItem->setData(bank->getAmmo(), AmmoValueRole); + ammoFlags |= Qt::ItemIsEditable; } else { - ammoItem->setData(QVariant(), Qt::DisplayRole); - ammoItem->setData(QVariant(), Qt::EditRole); - ammoItem->setFlags(Qt::NoItemFlags); + ammoItem->setData(QString(), Qt::DisplayRole); + ammoItem->setData(QVariant(), AmmoValueRole); } + ammoItem->setFlags(ammoFlags); } } tab.internalUpdate = false; @@ -424,7 +495,7 @@ void ShipWeaponsDialog::onBankItemChanged(TabState& tab, QStandardItem* item) return; } bool ok = false; - int requested = item->data(Qt::EditRole).toInt(&ok); + int requested = item->data(AmmoValueRole).toInt(&ok); if (!ok) { requested = bank->getAmmo(); } @@ -436,9 +507,83 @@ void ShipWeaponsDialog::onBankItemChanged(TabState& tab, QStandardItem* item) // Always write back the canonical value. This covers the case where the user typed an out-of-range // number and our clamp differs from what they entered. tab.internalUpdate = true; - item->setData(clamped, Qt::DisplayRole); - item->setData(clamped, Qt::EditRole); + item->setData(formatAmmoDisplay(clamped, bank->getMaxAmmo()), Qt::DisplayRole); + item->setData(clamped, AmmoValueRole); tab.internalUpdate = false; } +void ShipWeaponsDialog::onWeaponDroppedFromList(TabState& tab, const QModelIndex& target, int weaponId) +{ + Bank* bank = bankForIndex(tab, target); + if (bank == nullptr) { + return; + } + if (bank->getWeaponId() == weaponId) { + return; + } + bank->setWeapon(weaponId); + refreshBankItem(tab, target); + _model->notifyChanged(); +} + +void ShipWeaponsDialog::onWeaponMoved(TabState& tab, const QModelIndex& target, int weaponId, + int sourceBanksId, int sourceBankId, bool isCopy) +{ + Bank* targetBank = bankForIndex(tab, target); + if (targetBank == nullptr) { + return; + } + + Bank* sourceBank = nullptr; + for (Banks* banks : banksForMode(tab.mode)) { + if (banks->getId() != sourceBanksId) { + continue; + } + for (Bank* b : banks->getBanks()) { + if (b->getBankId() == sourceBankId) { + sourceBank = b; + break; + } + } + break; + } + if (sourceBank == nullptr || sourceBank == targetBank) { + return; + } + + targetBank->setWeapon(weaponId); + if (!isCopy) { + sourceBank->setWeapon(-1); + } + + refreshBankItem(tab, target); + const QModelIndex sourceIdx = indexForBank(tab, sourceBanksId, sourceBankId); + if (sourceIdx.isValid()) { + refreshBankItem(tab, sourceIdx); + } + _model->notifyChanged(); +} + +QModelIndex ShipWeaponsDialog::indexForBank(const TabState& tab, int banksId, int bankId) const +{ + if (tab.bankModel == nullptr) { + return {}; + } + const int topRows = tab.bankModel->rowCount(); + for (int i = 0; i < topRows; i++) { + const QModelIndex parent = tab.bankModel->index(i, 0); + if (parent.data(Qt::UserRole + 3).toInt() != banksId) { + continue; + } + const int childRows = tab.bankModel->rowCount(parent); + for (int j = 0; j < childRows; j++) { + const QModelIndex child = tab.bankModel->index(j, 0, parent); + if (child.data(Qt::UserRole + 3).toInt() == bankId) { + return child; + } + } + } + return {}; +} + } // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h index cb67a8ceeac..cc1fc9d56ef 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h @@ -64,6 +64,11 @@ class ShipWeaponsDialog : public QDialog { void onTblButtonClicked(TabState& tab); void onAiComboChanged(TabState& tab, int index); void onBankItemChanged(TabState& tab, QStandardItem* item); + void onWeaponDroppedFromList(TabState& tab, const QModelIndex& target, int weaponId); + void onWeaponMoved(TabState& tab, const QModelIndex& target, int weaponId, int sourceBanksId, + int sourceBankId, bool isCopy); + + QModelIndex indexForBank(const TabState& tab, int banksId, int bankId) const; Bank* bankForIndex(const TabState& tab, const QModelIndex& idx) const; Banks* banksForIndex(const TabState& tab, const QModelIndex& idx) const; diff --git a/qtfred/src/ui/widgets/bankTree.cpp b/qtfred/src/ui/widgets/bankTree.cpp index 93c562c67d8..ca72eb047c1 100644 --- a/qtfred/src/ui/widgets/bankTree.cpp +++ b/qtfred/src/ui/widgets/bankTree.cpp @@ -1,38 +1,110 @@ #include "bankTree.h" + +#include + namespace fso::fred { + +namespace { +constexpr const char* MIME_WEAPON_ID = "application/weaponid"; +constexpr const char* MIME_BANK_TREE_WEAPON = "application/banktreeweapon"; +} // namespace + bankTree::bankTree(QWidget* parent) : QTreeView(parent) { setAcceptDrops(true); } + void bankTree::dragEnterEvent(QDragEnterEvent* event) { - if (event->mimeData()->hasFormat("application/weaponid")) { + if (event->mimeData()->hasFormat(MIME_WEAPON_ID) || event->mimeData()->hasFormat(MIME_BANK_TREE_WEAPON)) { event->acceptProposedAction(); + } else { + event->ignore(); } } + +void bankTree::dragMoveEvent(QDragMoveEvent* event) +{ + const QModelIndex index = indexAt(event->pos()); + if (!index.isValid() || index.data(Qt::UserRole + 2).toBool()) { + event->ignore(); + return; + } + event->acceptProposedAction(); +} + void bankTree::dropEvent(QDropEvent* event) { - auto item = indexAt(event->pos()); - if (!item.isValid()) { + QModelIndex target = indexAt(event->pos()); + if (!target.isValid() || target.data(Qt::UserRole + 2).toBool()) { + event->ignore(); return; } - bool accepted = model()->dropMimeData(event->mimeData(), Qt::CopyAction, -1, 0, item); - if (accepted) { + if (target.column() != 0) { + target = target.sibling(target.row(), 0); + } + + const QMimeData* mime = event->mimeData(); + if (mime->hasFormat(MIME_BANK_TREE_WEAPON)) { + QByteArray bytes = mime->data(MIME_BANK_TREE_WEAPON); + QDataStream stream(&bytes, QIODevice::ReadOnly); + int weaponId = 0; + int sourceBanksId = 0; + int sourceBankId = 0; + stream >> weaponId >> sourceBanksId >> sourceBankId; + const bool isCopy = (event->keyboardModifiers() & Qt::ControlModifier) != 0; + weaponMoved(target, weaponId, sourceBanksId, sourceBankId, isCopy); + event->acceptProposedAction(); + } else if (mime->hasFormat(MIME_WEAPON_ID)) { + QByteArray bytes = mime->data(MIME_WEAPON_ID); + QDataStream stream(&bytes, QIODevice::ReadOnly); + int weaponId = 0; + stream >> weaponId; + weaponDroppedFromList(target, weaponId); event->acceptProposedAction(); + } else { + event->ignore(); } } -void bankTree::dragMoveEvent(QDragMoveEvent* event) + +void bankTree::startDrag(Qt::DropActions /*supportedActions*/) { - auto pos = QCursor::pos(); - auto index = indexAt(pos); - if (!index.isValid()) { + QModelIndex source; + for (const QModelIndex& idx : selectionModel()->selectedIndexes()) { + if (idx.column() != 0 || !idx.isValid()) { + continue; + } + if (idx.data(Qt::UserRole + 2).toBool()) { + continue; + } + source = idx; + break; + } + if (!source.isValid()) { return; } - if (model()->data(index, Qt::UserRole + 2) == false) { - event->accept(); - } else { - event->ignore(); + const int weaponId = source.data(Qt::UserRole).toInt(); + // "None" (-1) and "CONFLICT" (-2) slots have no weapon to drag. + if (weaponId < 0) { + return; } + + const QModelIndex parent = source.parent(); + if (!parent.isValid()) { + return; + } + const int sourceBanksId = parent.data(Qt::UserRole + 3).toInt(); + const int sourceBankId = source.data(Qt::UserRole + 3).toInt(); + + auto* mime = new QMimeData(); + QByteArray bytes; + QDataStream stream(&bytes, QIODevice::WriteOnly); + stream << weaponId << sourceBanksId << sourceBankId; + mime->setData(MIME_BANK_TREE_WEAPON, bytes); + + auto* drag = new QDrag(this); + drag->setMimeData(mime); + drag->exec(Qt::MoveAction | Qt::CopyAction, Qt::MoveAction); } void bankTree::selectionChanged(const QItemSelection& selected, const QItemSelection& deselected) diff --git a/qtfred/src/ui/widgets/bankTree.h b/qtfred/src/ui/widgets/bankTree.h index 75032adfb43..56eb11ba433 100644 --- a/qtfred/src/ui/widgets/bankTree.h +++ b/qtfred/src/ui/widgets/bankTree.h @@ -1,8 +1,6 @@ #pragma once #include "ui/dialogs/ShipEditor/BankModel.h" -#include - #include #include #include @@ -18,10 +16,15 @@ class bankTree : public QTreeView { void selectionChanged(const QItemSelection& selected, const QItemSelection& deselected) override; SelectionType getSelectionType() const; + signals: + void weaponDroppedFromList(const QModelIndex& target, int weaponId); + void weaponMoved(const QModelIndex& target, int weaponId, int sourceBanksId, int sourceBankId, bool isCopy); + protected: void dragEnterEvent(QDragEnterEvent*) override; void dropEvent(QDropEvent* event) override; void dragMoveEvent(QDragMoveEvent*) override; + void startDrag(Qt::DropActions supportedActions) override; private: bool m_autoFiltering = false; diff --git a/qtfred/ui/ShipWeaponsDialog.ui b/qtfred/ui/ShipWeaponsDialog.ui index fddefe3159c..dc72c7e1cc0 100644 --- a/qtfred/ui/ShipWeaponsDialog.ui +++ b/qtfred/ui/ShipWeaponsDialog.ui @@ -109,7 +109,7 @@ QAbstractScrollArea::AdjustToContentsOnFirstShow - QAbstractItemView::DropOnly + QAbstractItemView::DragDrop QAbstractItemView::ExtendedSelection @@ -244,7 +244,7 @@ QAbstractScrollArea::AdjustToContentsOnFirstShow - QAbstractItemView::DropOnly + QAbstractItemView::DragDrop QAbstractItemView::ExtendedSelection From aee735e1efa8134eca9784d0c21b834a1b58eedd Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Tue, 12 May 2026 13:32:55 -0500 Subject: [PATCH 07/17] multi edit support --- .../ShipEditor/ShipWeaponsDialogModel.cpp | 293 +++++++++++------- .../ShipEditor/ShipWeaponsDialogModel.h | 7 +- .../dialogs/ShipEditor/ShipWeaponsDialog.cpp | 79 ++++- 3 files changed, 248 insertions(+), 131 deletions(-) diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp index 06e815c023f..69eced6c424 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp @@ -5,6 +5,67 @@ #include namespace fso::fred { +namespace { +// Compare one slot of the "tracking" Bank (from the first ship's view) against the corresponding +// slot of a subsequent ship. Mark the bank as CONFLICT (-2) where they disagree, but never +// clobber an already-CONFLICT marker. +void reconcileSlot(Bank* bank, int otherWeaponId, int otherAmmoPct) +{ + if (bank->getWeaponId() == -2) { + return; + } + if (bank->getWeaponId() != otherWeaponId) { + bank->setWeapon(-2); + return; + } + if (bank->getAmmo() == -2 || bank->getMaxAmmo() <= 0) { + return; + } + const int otherAmmo = fl2ir(otherAmmoPct * bank->getMaxAmmo() / 100.0f); + if (bank->getAmmo() != otherAmmo) { + bank->setAmmo(-2); + } +} + +Banks* findBanksByName(const SCP_vector& banks, const SCP_string& name) +{ + for (Banks* b : banks) { + if (b->getName() == name) { + return b; + } + } + return nullptr; +} + +ship_subsys* findTurretByName(int inst, const SCP_string& name) +{ + ship_subsys* ssl = &Ships[inst].subsys_list; + for (ship_subsys* pss = GET_FIRST(ssl); pss != END_OF_LIST(ssl); pss = GET_NEXT(pss)) { + if (pss->system_info->type == SUBSYSTEM_TURRET && + SCP_string(pss->system_info->subobj_name) == name) { + return pss; + } + } + return nullptr; +} + +void saveSlotsTo(ship_weapon& target, const SCP_vector& bankList, bool isPrimary) +{ + int* weapons = isPrimary ? target.primary_bank_weapons : target.secondary_bank_weapons; + int* ammo = isPrimary ? target.primary_bank_ammo : target.secondary_bank_ammo; + for (Bank* bank : bankList) { + if (bank->getWeaponId() == -2) { + continue; // weapon CONFLICT — preserve per-ship weapon + } + weapons[bank->getBankId()] = bank->getWeaponId(); + if (bank->getAmmo() != -2) { + ammo[bank->getBankId()] = bank->getMaxAmmo() ? fl2ir(bank->getAmmo() * 100.0f / bank->getMaxAmmo()) : 0; + } + // else: ammo CONFLICT — preserve per-ship ammo + } +} +} // namespace + WeaponItem::WeaponItem(int inID, QString inName, bool inAllowed) : name(std::move(inName)), id(inID), allowed(inAllowed) { @@ -111,14 +172,6 @@ void Banks::add(Bank* bank) { banks.push_back(bank); } -Bank* Banks::getByBankId(const int bankId) -{ - for (auto bank : banks) { - if (bankId == bank->getWeaponId()) - return bank; - } - return nullptr; -} SCP_string Banks::getName() const { return name; @@ -141,35 +194,62 @@ SCP_vector Banks::getBanks() const } int Banks::getAiClass() const { - if (name == "Pilot") { - return Ships[ship].weapons.ai_class; - } else { - return subsys->weapons.ai_class; + auto readShip = [&](int inst) -> int { + if (name == "Pilot") { + return Ships[inst].weapons.ai_class; + } + ship_subsys* pss = findTurretByName(inst, name); + return pss ? pss->weapons.ai_class : 0; + }; + + if (!m_isMultiEdit) { + return readShip(ship); } + + int common = -1; + bool initialized = false; + for (object* 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) || !ptr->flags[Object::Object_Flags::Marked]) { + continue; + } + const int ai = readShip(ptr->instance); + if (!initialized) { + common = ai; + initialized = true; + } else if (ai != common) { + return -1; + } + } + return common; } void Banks::setAiClass(int newClass) { + aiClass = newClass; + aiClassDirty = true; + + auto applyToShip = [&](int inst) { + if (name == "Pilot") { + Ships[inst].weapons.ai_class = newClass; + } else if (ship_subsys* pss = findTurretByName(inst, name)) { + pss->weapons.ai_class = newClass; + } + }; + if (m_isMultiEdit) { - object* ptr = GET_FIRST(&obj_used_list); - while (ptr != END_OF_LIST(&obj_used_list)) { - if (((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) && (ptr->flags[Object::Object_Flags::Marked])) { - int inst = ptr->instance; - if (name == "Pilot") { - Ships[inst].ai_index = newClass; - } else { - subsys->weapons.ai_class = newClass; - } + for (object* 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) || !ptr->flags[Object::Object_Flags::Marked]) { + continue; } - ptr = GET_NEXT(ptr); + applyToShip(ptr->instance); } } else { - if (name == "Pilot") { - Ships[ship].weapons.ai_class = newClass; - } else { - subsys->weapons.ai_class = newClass; - } + applyToShip(ship); } } +bool Banks::isAiClassDirty() const +{ + return aiClassDirty; +} int Banks::getInitalAI() const { return initalAI; @@ -233,6 +313,22 @@ ShipWeaponsDialogModel::ShipWeaponsDialogModel(QObject* parent, EditorViewport* { initializeData(isMultiEdit); } +bool ShipWeaponsDialogModel::selectedShipsShareClass() +{ + int sharedClass = -1; + for (object* 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) || !ptr->flags[Object::Object_Flags::Marked]) { + continue; + } + const int cls = Ships[ptr->instance].ship_info_index; + if (sharedClass < 0) { + sharedClass = cls; + } else if (cls != sharedClass) { + return false; + } + } + return true; +} void ShipWeaponsDialogModel::initializeData(bool isMultiEdit) { m_isMultiEdit = isMultiEdit; @@ -317,31 +413,28 @@ void ShipWeaponsDialogModel::initPrimary(int inst, bool first) } } } else { - for (int i = 0; i < static_cast(PrimaryBanks[0]->getBanks().size()); i++) { - if (PrimaryBanks[0]->getBanks()[i]->getWeaponId() != Ships[inst].weapons.primary_bank_weapons[i]) { - PrimaryBanks[0]->getBanks()[i]->setWeapon(-2); - } - if (PrimaryBanks[0]->getBanks()[i]->getAmmo() != Ships[inst].weapons.primary_bank_ammo[i]) { - PrimaryBanks[0]->getBanks()[i]->setAmmo(-2); + // Subsequent ship: reconcile each slot against the tracking Banks built from the first ship. + delete pilotBank; // unused; we already have one from the first ship + if (Banks* tracking = findBanksByName(PrimaryBanks, "Pilot")) { + const auto bankList = tracking->getBanks(); + for (size_t i = 0; i < bankList.size(); i++) { + reconcileSlot(bankList[i], Ships[inst].weapons.primary_bank_weapons[i], + Ships[inst].weapons.primary_bank_ammo[i]); } } ship_subsys* ssl = &Ships[inst].subsys_list; - ship_subsys* pss; - for (pss = GET_FIRST(ssl); pss != END_OF_LIST(ssl); pss = GET_NEXT(pss)) { - model_subsystem* psub = pss->system_info; - if (psub->type == SUBSYSTEM_TURRET) { - for (auto banks : PrimaryBanks) { - if (banks->getSubsys() == pss) { - for (int i = 0; i < static_cast(banks->getBanks().size()); i++) { - if (banks->getBanks()[i]->getWeaponId() != pss->weapons.primary_bank_weapons[i]) { - banks->getBanks()[i]->setWeapon(-2); - } - if (banks->getBanks()[i]->getAmmo() != pss->weapons.primary_bank_ammo[i]) { - banks->getBanks()[i]->setAmmo(-2); - } - } - } - } + for (ship_subsys* pss = GET_FIRST(ssl); pss != END_OF_LIST(ssl); pss = GET_NEXT(pss)) { + if (pss->system_info->type != SUBSYSTEM_TURRET) { + continue; + } + Banks* tracking = findBanksByName(PrimaryBanks, pss->system_info->subobj_name); + if (tracking == nullptr) { + continue; + } + const auto bankList = tracking->getBanks(); + for (size_t i = 0; i < bankList.size(); i++) { + reconcileSlot(bankList[i], pss->weapons.primary_bank_weapons[i], + pss->weapons.primary_bank_ammo[i]); } } } @@ -397,76 +490,45 @@ void ShipWeaponsDialogModel::initSecondary(int inst, bool first) } } } else { - for (int i = 0; i < static_cast(SecondaryBanks[0]->getBanks().size()); i++) { - if (SecondaryBanks[0]->getBanks()[i]->getWeaponId() != Ships[inst].weapons.secondary_bank_weapons[i]) { - SecondaryBanks[0]->getBanks()[i]->setWeapon(-2); - } - if (SecondaryBanks[0]->getBanks()[i]->getAmmo() != Ships[inst].weapons.secondary_bank_ammo[i]) { - SecondaryBanks[0]->getBanks()[i]->setAmmo(-2); + delete pilotBank; + if (Banks* tracking = findBanksByName(SecondaryBanks, "Pilot")) { + const auto bankList = tracking->getBanks(); + for (size_t i = 0; i < bankList.size(); i++) { + reconcileSlot(bankList[i], Ships[inst].weapons.secondary_bank_weapons[i], + Ships[inst].weapons.secondary_bank_ammo[i]); } } ship_subsys* ssl = &Ships[inst].subsys_list; - ship_subsys* pss; - for (pss = GET_FIRST(ssl); pss != END_OF_LIST(ssl); pss = GET_NEXT(pss)) { - model_subsystem* psub = pss->system_info; - if (psub->type == SUBSYSTEM_TURRET) { - for (auto banks : SecondaryBanks) { - if (banks->getSubsys() == pss) { - for (int i = 0; i < static_cast(banks->getBanks().size()); i++) { - if (banks->getByBankId(i)->getWeaponId() != pss->weapons.secondary_bank_weapons[i]) { - banks->getByBankId(i)->setWeapon(-2); - } - if (banks->getByBankId(i)->getAmmo() != pss->weapons.secondary_bank_ammo[i]) { - banks->getByBankId(i)->setAmmo(-2); - } - } - } - } + for (ship_subsys* pss = GET_FIRST(ssl); pss != END_OF_LIST(ssl); pss = GET_NEXT(pss)) { + if (pss->system_info->type != SUBSYSTEM_TURRET) { + continue; + } + Banks* tracking = findBanksByName(SecondaryBanks, pss->system_info->subobj_name); + if (tracking == nullptr) { + continue; + } + const auto bankList = tracking->getBanks(); + for (size_t i = 0; i < bankList.size(); i++) { + reconcileSlot(bankList[i], pss->weapons.secondary_bank_weapons[i], + pss->weapons.secondary_bank_ammo[i]); } } } } void ShipWeaponsDialogModel::saveShip(int inst) { - for (auto Turret : PrimaryBanks) { - if (Turret->getName() == "Pilot") { - for (auto bank : Turret->getBanks()) { - if (bank->getWeaponId() != -2) { - Ships[inst].weapons.primary_bank_weapons[bank->getBankId()] = bank->getWeaponId(); - Ships[inst].weapons.primary_bank_ammo[bank->getBankId()] = - bank->getMaxAmmo() ? fl2ir(bank->getAmmo() * 100.0f / bank->getMaxAmmo()) : 0; - } - } - } else { - ship_subsys* pss = Turret->getSubsys(); - for (auto bank : Turret->getBanks()) { - if (bank->getWeaponId() != -2) { - pss->weapons.primary_bank_weapons[bank->getBankId()] = bank->getWeaponId(); - pss->weapons.primary_bank_ammo[bank->getBankId()] = - bank->getMaxAmmo() ? fl2ir(bank->getAmmo() * 100.0f / bank->getMaxAmmo()) : 0; - } - } + auto saveBank = [&](Banks* turret, bool isPrimary) { + if (turret->getName() == "Pilot") { + saveSlotsTo(Ships[inst].weapons, turret->getBanks(), isPrimary); + } else if (ship_subsys* pss = findTurretByName(inst, turret->getName())) { + saveSlotsTo(pss->weapons, turret->getBanks(), isPrimary); } + }; + for (Banks* turret : PrimaryBanks) { + saveBank(turret, true); } - for (auto Turret : SecondaryBanks) { - if (Turret->getName() == "Pilot") { - for (auto bank : Turret->getBanks()) { - if (bank->getWeaponId() != -2) { - Ships[inst].weapons.secondary_bank_weapons[bank->getBankId()] = bank->getWeaponId(); - Ships[inst].weapons.secondary_bank_ammo[bank->getBankId()] = - bank->getMaxAmmo() ? fl2ir(bank->getAmmo() * 100.0f / bank->getMaxAmmo()) : 0; - } - } - } else { - ship_subsys* pss = Turret->getSubsys(); - for (auto bank : Turret->getBanks()) { - if (bank->getWeaponId() != -2) { - pss->weapons.secondary_bank_weapons[bank->getBankId()] = bank->getWeaponId(); - pss->weapons.secondary_bank_ammo[bank->getBankId()] = - bank->getMaxAmmo() ? fl2ir(bank->getAmmo() * 100.0f / bank->getMaxAmmo()) : 0; - } - } - } + for (Banks* turret : SecondaryBanks) { + saveBank(turret, false); } } bool ShipWeaponsDialogModel::apply() @@ -488,11 +550,18 @@ bool ShipWeaponsDialogModel::apply() } void ShipWeaponsDialogModel::reject() { - for (auto Turret : PrimaryBanks) { - Turret->setAiClass(Turret->getInitalAI()); + // Only restore AI on banks the user explicitly changed. In multi-edit this can only + // roll back to the first ship's initial AI for all ships which is acceptable since we only + // reach here when the user took the explicit Change AI action. + for (Banks* banks : PrimaryBanks) { + if (banks->isAiClassDirty()) { + banks->setAiClass(banks->getInitalAI()); + } } - for (auto Turret : SecondaryBanks) { - Turret->setAiClass(Turret->getInitalAI()); + for (Banks* banks : SecondaryBanks) { + if (banks->isAiClassDirty()) { + banks->setAiClass(banks->getInitalAI()); + } } } SCP_vector ShipWeaponsDialogModel::getPrimaryBanks() const diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h index d96c926d6d3..5152f1e349f 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h @@ -34,14 +34,15 @@ struct Banks { public: int getId() const; void add(Bank*); - Bank* getByBankId(const int bankId); SCP_string getName() const; int getShip() const; ship_subsys* getSubsys() const; bool empty() const; SCP_vector getBanks() const; + // Returns the single consistent AI class for this bank-set; -1 if multi-edit and ships disagree. int getAiClass() const; void setAiClass(int); + bool isAiClassDirty() const; bool m_isMultiEdit; int getInitalAI() const; @@ -50,6 +51,7 @@ struct Banks { ship_subsys* subsys; int aiClass; int initalAI; + bool aiClassDirty = false; SCP_vector banks; int ship; int id; @@ -87,7 +89,8 @@ class ShipWeaponsDialogModel : public AbstractDialogModel { */ ShipWeaponsDialogModel(QObject* parent, EditorViewport* viewport, bool multi); - // void initTertiary(int inst, bool first); + // True iff all currently-marked ships share the same ship_info_index. Used to gate multi-edit. + static bool selectedShipsShareClass(); bool apply() override; void reject() override; diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp index 1e08e8f3e75..cc46ceff012 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp @@ -24,6 +24,11 @@ QString formatAmmoDisplay(int current, int max) return QString::number(current) + "/" + QString::number(max); } +QString formatAmmoConflict(int max) +{ + return QStringLiteral("--/") + QString::number(max); +} + class AmmoSpinBoxDelegate : public QStyledItemDelegate { public: using QStyledItemDelegate::QStyledItemDelegate; @@ -167,7 +172,11 @@ SCP_string ShipWeaponsDialog::banksLabel(const Banks* banks) const if (banks->getName() == "Pilot") { return banks->getName(); } - return banks->getName() + " ( " + Ai_class_names[banks->getAiClass()] + " ) "; + const int ai = banks->getAiClass(); + if (ai < 0) { + return banks->getName() + " (Mixed AI)"; + } + return banks->getName() + " ( " + Ai_class_names[ai] + " ) "; } void ShipWeaponsDialog::loadBankModel(TabState& tab) @@ -218,8 +227,14 @@ void ShipWeaponsDialog::loadBankModel(TabState& tab) auto ammoItem = new QStandardItem(); Qt::ItemFlags ammoFlags = Qt::ItemIsSelectable | Qt::ItemIsEnabled; if (bank->getMaxAmmo() > 0) { - ammoItem->setData(formatAmmoDisplay(bank->getAmmo(), bank->getMaxAmmo()), Qt::DisplayRole); - ammoItem->setData(bank->getAmmo(), AmmoValueRole); + if (bank->getAmmo() == -2) { + // Multi-edit ammo CONFLICT: show --/max, still editable so user can resolve. + ammoItem->setData(formatAmmoConflict(bank->getMaxAmmo()), Qt::DisplayRole); + ammoItem->setData(0, AmmoValueRole); + } else { + ammoItem->setData(formatAmmoDisplay(bank->getAmmo(), bank->getMaxAmmo()), Qt::DisplayRole); + ammoItem->setData(bank->getAmmo(), AmmoValueRole); + } ammoFlags |= Qt::ItemIsEditable; } else { ammoItem->setData(QString(), Qt::DisplayRole); @@ -289,16 +304,24 @@ void ShipWeaponsDialog::onSetAllClicked(TabState& tab) void ShipWeaponsDialog::onAiButtonClicked(TabState& tab) { + const int comboIdx = tab.aiCombo->currentIndex(); + if (comboIdx < 0) { + return; // No AI picked in the combo (still blank from a mixed selection). + } + const int newAi = tab.aiCombo->itemData(comboIdx).toInt(); bool anyChanged = false; for (const QModelIndex& index : tab.tree->selectionModel()->selectedIndexes()) { + if (index.column() != 0) { + continue; + } Banks* banks = banksForIndex(tab, index); - if (banks == nullptr) { + if (banks == nullptr || banks->getName() == "Pilot") { continue; } - if (banks->getAiClass() == tab.currentAI) { + if (banks->getAiClass() == newAi) { continue; } - banks->setAiClass(tab.currentAI); + banks->setAiClass(newAi); refreshBankItem(tab, index); anyChanged = true; } @@ -319,6 +342,9 @@ void ShipWeaponsDialog::onTblButtonClicked(TabState& tab) void ShipWeaponsDialog::onAiComboChanged(TabState& tab, int index) { + if (index < 0) { + return; + } tab.currentAI = tab.aiCombo->itemData(index).toInt(); } @@ -338,10 +364,14 @@ void ShipWeaponsDialog::updateTabUI(TabState& tab) tab.setAllButton->setEnabled(selType == bankTree::SelectionType::Weapon); // Pilot AI maps to Ships[].weapons.ai_class, which the Ship Editor also owns. Keep that single - // point of truth. A slight regression from old FRED but a more clear separation of responsibilities. + // point of truth. Other selected banks (turrets) collectively determine the combo state: + // - all same AI -> show that value + // - any mixed across ships, or differing AIs across selected turrets -> combo blank, user picks bool aiEditable = (selType == bankTree::SelectionType::Bank); + int displayedAi = -1; + bool combinedInitialized = false; + bool combinedMixed = false; if (selType == bankTree::SelectionType::Bank) { - Banks* firstBanks = nullptr; for (const QModelIndex& idx : tab.tree->selectionModel()->selectedIndexes()) { if (idx.column() != 0) { continue; @@ -350,19 +380,29 @@ void ShipWeaponsDialog::updateTabUI(TabState& tab) if (banks == nullptr) { continue; } - if (firstBanks == nullptr) { - firstBanks = banks; - } if (banks->getName() == "Pilot") { aiEditable = false; + continue; + } + const int ai = banks->getAiClass(); + if (ai < 0) { + combinedMixed = true; + } else if (!combinedInitialized) { + displayedAi = ai; + combinedInitialized = true; + } else if (displayedAi != ai) { + combinedMixed = true; } - } - if (firstBanks != nullptr) { - tab.currentAI = firstBanks->getAiClass(); } } tab.aiGroup->setEnabled(aiEditable); - tab.aiCombo->setCurrentIndex(tab.aiCombo->findData(tab.currentAI)); + if (aiEditable && combinedInitialized && !combinedMixed) { + tab.currentAI = displayedAi; + tab.aiCombo->setCurrentIndex(tab.aiCombo->findData(displayedAi)); + } else { + // Mixed across selection, or only Pilot selected (combo greyed): clear the combo. + tab.aiCombo->setCurrentIndex(-1); + } const bool hasWeaponSelection = tab.list->selectionModel()->hasSelection() && tab.list->currentIndex().data(Qt::UserRole).toInt() != -1; @@ -465,8 +505,13 @@ void ShipWeaponsDialog::refreshBankItem(TabState& tab, const QModelIndex& idx) if (ammoItem != nullptr) { Qt::ItemFlags ammoFlags = Qt::ItemIsSelectable | Qt::ItemIsEnabled; if (bank->getMaxAmmo() > 0) { - ammoItem->setData(formatAmmoDisplay(bank->getAmmo(), bank->getMaxAmmo()), Qt::DisplayRole); - ammoItem->setData(bank->getAmmo(), AmmoValueRole); + if (bank->getAmmo() == -2) { + ammoItem->setData(QString(), Qt::DisplayRole); + ammoItem->setData(0, AmmoValueRole); + } else { + ammoItem->setData(formatAmmoDisplay(bank->getAmmo(), bank->getMaxAmmo()), Qt::DisplayRole); + ammoItem->setData(bank->getAmmo(), AmmoValueRole); + } ammoFlags |= Qt::ItemIsEditable; } else { ammoItem->setData(QString(), Qt::DisplayRole); From 0138cb19f93320cc229db9ae02afd11c80ac9a0f Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Tue, 12 May 2026 13:44:15 -0500 Subject: [PATCH 08/17] cleanup --- qtfred/source_groups.cmake | 2 - .../ShipEditor/ShipWeaponsDialogModel.cpp | 15 +- .../ShipEditor/ShipWeaponsDialogModel.h | 15 +- .../src/ui/dialogs/ShipEditor/BankModel.cpp | 407 ------------------ qtfred/src/ui/dialogs/ShipEditor/BankModel.h | 12 - .../dialogs/ShipEditor/ShipWeaponsDialog.cpp | 45 +- .../ui/dialogs/ShipEditor/ShipWeaponsDialog.h | 4 +- qtfred/src/ui/widgets/bankTree.cpp | 16 +- qtfred/src/ui/widgets/bankTree.h | 7 +- 9 files changed, 37 insertions(+), 486 deletions(-) delete mode 100644 qtfred/src/ui/dialogs/ShipEditor/BankModel.cpp delete mode 100644 qtfred/src/ui/dialogs/ShipEditor/BankModel.h diff --git a/qtfred/source_groups.cmake b/qtfred/source_groups.cmake index 189b5f4fd40..95331e243a3 100644 --- a/qtfred/source_groups.cmake +++ b/qtfred/source_groups.cmake @@ -261,8 +261,6 @@ add_file_folder("Source/UI/Dialogs/ShipEditor" src/ui/dialogs/ShipEditor/ShipTextureReplacementDialog.cpp src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h - src/ui/dialogs/ShipEditor/BankModel.cpp - src/ui/dialogs/ShipEditor/BankModel.h src/ui/dialogs/ShipEditor/ShipCustomWarpDialog.h src/ui/dialogs/ShipEditor/ShipCustomWarpDialog.cpp src/ui/dialogs/ShipEditor/ShipAltShipClass.h diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp index 69eced6c424..68f8d5998ab 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp @@ -160,7 +160,7 @@ QMimeData* WeaponModel::mimeData(const QModelIndexList& indexes) const } Banks::Banks(SCP_string _name, int aiIndex, int _ship, int multiedit, int _id, ship_subsys* _subsys) - : m_isMultiEdit(multiedit), name(std::move(_name)), subsys(_subsys), initalAI(aiIndex), ship(_ship), id(_id) + : m_isMultiEdit(multiedit), name(std::move(_name)), subsys(_subsys), initialAI(aiIndex), ship(_ship), id(_id) { aiClass = aiIndex; } @@ -250,9 +250,9 @@ bool Banks::isAiClassDirty() const { return aiClassDirty; } -int Banks::getInitalAI() const +int Banks::getInitialAI() const { - return initalAI; + return initialAI; } Bank::Bank(const int _weaponId, const int _bankId, const int _ammoMax, const int _ammo, Banks* _parent) { @@ -349,7 +349,6 @@ void ShipWeaponsDialogModel::initializeData(bool isMultiEdit) big = false; initPrimary(inst, first); initSecondary(inst, first); - // initTertiary(inst, first); first = false; } ptr = GET_NEXT(ptr); @@ -555,12 +554,12 @@ void ShipWeaponsDialogModel::reject() // reach here when the user took the explicit Change AI action. for (Banks* banks : PrimaryBanks) { if (banks->isAiClassDirty()) { - banks->setAiClass(banks->getInitalAI()); + banks->setAiClass(banks->getInitialAI()); } } for (Banks* banks : SecondaryBanks) { if (banks->isAiClassDirty()) { - banks->setAiClass(banks->getInitalAI()); + banks->setAiClass(banks->getInitialAI()); } } } @@ -585,9 +584,5 @@ void ShipWeaponsDialogModel::notifyChanged() set_modified(); modelChanged(); } -/* void ShipWeaponsDialogModel::initTertiary(int inst, bool first) { - -} -*/ } // namespace dialogs } // namespace fso::fred \ No newline at end of file diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h index 5152f1e349f..81ed9b799ce 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h @@ -44,13 +44,13 @@ struct Banks { void setAiClass(int); bool isAiClassDirty() const; bool m_isMultiEdit; - int getInitalAI() const; + int getInitialAI() const; private: SCP_string name; ship_subsys* subsys; int aiClass; - int initalAI; + int initialAI; bool aiClassDirty = false; SCP_vector banks; int ship; @@ -76,17 +76,8 @@ struct Bank { Banks* parent; }; namespace dialogs { -/** - * @brief QTFred's Weapons Editor Model - */ class ShipWeaponsDialogModel : public AbstractDialogModel { public: - /** - * @brief QTFred's Weapons Editor Model Constructer. - * @param [in/out] parent The dialogs parent. - * @param [in/out] viewport Editor viewport. - * @param [in] multi If editing multiple ships. - */ ShipWeaponsDialogModel(QObject* parent, EditorViewport* viewport, bool multi); // True iff all currently-marked ships share the same ship_info_index. Used to gate multi-edit. @@ -99,7 +90,6 @@ class ShipWeaponsDialogModel : public AbstractDialogModel { int getShipClass() const; bool isBigShip() const; void notifyChanged(); - // SCP_vector getTertiaryBanks() const; private: void saveShip(int inst); @@ -112,7 +102,6 @@ class ShipWeaponsDialogModel : public AbstractDialogModel { bool big = true; SCP_vector PrimaryBanks; SCP_vector SecondaryBanks; - // SCP_vector TertiaryBanks; }; } // namespace dialogs } // namespace fso::fred \ No newline at end of file diff --git a/qtfred/src/ui/dialogs/ShipEditor/BankModel.cpp b/qtfred/src/ui/dialogs/ShipEditor/BankModel.cpp deleted file mode 100644 index 694061d1891..00000000000 --- a/qtfred/src/ui/dialogs/ShipEditor/BankModel.cpp +++ /dev/null @@ -1,407 +0,0 @@ -#include "ShipWeaponsDialog.h" - -#include -#include -namespace fso::fred { -//BankTreeItem::BankTreeItem(BankTreeItem* parentItem, QString inName) : name(std::move(inName)), m_parentItem(parentItem) {} -/* BankTreeItem::~BankTreeItem() -{ - qDeleteAll(m_childItems); -} -void BankTreeItem::appendChild(BankTreeItem* item) -{ - m_childItems.append(item); -} -BankTreeItem* BankTreeItem::child(int row) const -{ - if (row < 0 || row >= m_childItems.size()) - return nullptr; - return m_childItems.at(row); -} -int BankTreeItem::childCount() const -{ - return m_childItems.count(); -} -int BankTreeItem::childNumber() const -{ - if (m_parentItem) - return m_parentItem->m_childItems.indexOf(const_cast(this)); - return 0; -} -BankTreeItem* BankTreeItem::parentItem() -{ - return m_parentItem; -} - -bool BankTreeItem::insertLabel(int position, const QString& newName, Banks* newBanks) -{ - if (position < 0 || position > m_childItems.size()) - return false; - - auto* item = new BankTreeLabel(newName, newBanks, this); - m_childItems.insert(position, item); - - return true; -} - -bool BankTreeItem::insertBank(int position, Bank* newBank) -{ - if (position < 0 || position > m_childItems.size()) - return false; - - auto* item = new BankTreeBank(newBank, this); - m_childItems.insert(position, item); - - return true; -} - -QString BankTreeItem::getName() const -{ - return name; -} - -int BankTreeBank::getId() const -{ - return bank->getWeaponId(); -} - -BankTreeBank::BankTreeBank(Bank* inBank, BankTreeItem* parentItem) : BankTreeItem(parentItem), bank(inBank) -{ - switch (bank->getWeaponId()) { - case -2: - this->name = "CONFLICT"; - break; - case -1: - this->name = "None"; - break; - default: - this->name = Weapon_info[bank->getWeaponId()].name; - } -} - -QVariant BankTreeBank::data(int column) const -{ - switch (column) { - case 0: - return name; - break; - case 1: - return bank->getAmmo(); - break; - default: - return {}; - } -} - -Qt::ItemFlags BankTreeBank::getFlags(int column) const -{ - switch (column) { - case 0: - return Qt::ItemIsDropEnabled | Qt::ItemIsSelectable; - break; - case 1: - return Qt::ItemIsEditable; - break; - default: - return {}; - } -} - -void BankTreeBank::setWeapon(int id) -{ - bank->setWeapon(id); - if (id == -1) { - name = "None"; - } else { - name = Weapon_info[id].name; - } -} - -void BankTreeBank::setAmmo(int value) -{ - Assert(bank != nullptr); - bank->setAmmo(value); -} - -BankTreeLabel::BankTreeLabel(const QString& inName, Banks* inBanks, BankTreeItem* parentItem) - : BankTreeItem(parentItem, inName), banks(inBanks) -{ -} - -QVariant BankTreeLabel::data(int column) const -{ - switch (column) { - case 0: - return name + " (" + Ai_class_names[banks->getAiClass()] + ")"; - break; - default: - return {}; - } -} - -Qt::ItemFlags BankTreeLabel::getFlags(int column) const -{ - Q_UNUSED(column); - return Qt::ItemIsSelectable; -} - -void BankTreeLabel::setAIClass(int value) -{ - Assert(banks != nullptr); - banks->setAiClass(value); -} - -bool BankTreeLabel::setData(int column, const QVariant& value) -{ - Q_UNUSED(column); - setAIClass(value.toInt()); - return true; -} - -bool BankTreeBank::setData(int column, const QVariant& value) -{ - switch (column) { - case 1: - setAmmo(value.toInt()); - return true; - break; - default: - return false; - } -} -BankTreeModel::BankTreeModel(const SCP_vector& data, QObject* parent) : QAbstractItemModel(parent) -{ - rootItem = new BankTreeRoot(); - - setupModelData(data, rootItem); -} - -void BankTreeModel::setupModelData(const SCP_vector& data, BankTreeItem* parent) -{ - for (auto banks : data) { - parent->insertLabel(parent->childCount(), banks->getName().c_str(), banks); - BankTreeItem* currentParent = parent->child(parent->childCount() - 1); - for (auto bank : banks->getBanks()) { - currentParent->insertBank(currentParent->childCount(), bank); - } - } -} - -QVariant BankTreeModel::headerData(int section, Qt::Orientation orientation, int role) const -{ - Q_UNUSED(orientation); - if (role == Qt::DisplayRole) { - switch (section) { - case 0: - return tr("Bank Name/Weapon"); - case 1: - return tr("Ammo"); - default: - return QString(""); - } - } - return {}; -} - -BankTreeModel::~BankTreeModel() -{ - delete rootItem; -} - -int BankTreeModel::columnCount(const QModelIndex& parent) const -{ - Q_UNUSED(parent); - return 2; -} - -QVariant BankTreeModel::data(const QModelIndex& index, int role) const -{ - if (!index.isValid()) { - return {}; - } - - if (role != Qt::DisplayRole && role != Qt::EditRole) - return {}; - - BankTreeItem* item = getItem(index); - - return item->data(index.column()); -} - -BankTreeItem* BankTreeModel::getItem(const QModelIndex index) const -{ - if (index.isValid()) { - auto* item = static_cast(index.internalPointer()); - if (item) - return item; - } - return rootItem; -} - -bool BankTreeModel::setData(const QModelIndex& index, const QVariant& value, int role) -{ - if (role != Qt::EditRole) - return false; - - BankTreeItem* item = getItem(index); // getItem(index); - if (!item) { - return false; - } - bool result = item->setData(index.column(), value); - QVector roles; - roles.append(role); - QAbstractItemModel::dataChanged(index, index, roles); - return result; -} - -int BankTreeModel::rowCount(const QModelIndex& parent) const -{ - if (parent.isValid() && parent.column() > 0) - return 0; - - const BankTreeItem* parentItem = getItem(parent); - - return parentItem ? parentItem->childCount() : 0; -} - -Qt::ItemFlags BankTreeModel::flags(const QModelIndex& index) const -{ - Qt::ItemFlags defaultFlags = QAbstractItemModel::flags(index); - defaultFlags.setFlag(Qt::ItemIsSelectable, false); - - if (index.isValid()) { - auto* item = static_cast(index.internalPointer()); - return item->getFlags(index.column()) | defaultFlags; - } else { - return Qt::NoItemFlags; - } -} - -QModelIndex BankTreeModel::index(int row, int column, const QModelIndex& parent) const -{ - if (parent.isValid() && parent.column() != 0) - return {}; - - BankTreeItem* parentItem = getItem(parent); - if (!parentItem) - return {}; - - BankTreeItem* childItem = parentItem->child(row); - if (childItem) - return createIndex(row, column, childItem); - return {}; -} -QModelIndex BankTreeModel::parent(const QModelIndex& index) const -{ - if (!index.isValid()) - return {}; - BankTreeItem* childItem = getItem(index); - BankTreeItem* parentItem = childItem ? childItem->parentItem() : nullptr; - - if (parentItem == rootItem || !parentItem) - return {}; - return createIndex(parentItem->childNumber(), 0, parentItem); -} -QStringList BankTreeModel::mimeTypes() const -{ - QStringList types; - types << "application/weaponid"; - return types; -} - -bool BankTreeModel::canDropMimeData(const QMimeData* data, - Qt::DropAction action, - int row, - int column, - const QModelIndex& parent) const -{ - Q_UNUSED(action); - Q_UNUSED(row); - Q_UNUSED(parent); - - if (!data->hasFormat("application/weaponid")) - return false; - BankTreeItem* item = this->getItem(parent); - Qt::ItemFlags flags = item->getFlags(column); - return flags.testFlag(Qt::ItemIsDropEnabled); -} -bool BankTreeModel::dropMimeData(const QMimeData* data, - Qt::DropAction action, - int row, - int column, - const QModelIndex& parent) -{ - if (!canDropMimeData(data, action, row, column, parent)) - return false; - - if (action == Qt::IgnoreAction) - return true; - - if (row == -1 && !parent.isValid()) - return false; - - QByteArray encodedData = data->data("application/weaponid"); - QDataStream stream(&encodedData, QIODevice::ReadOnly); - while (!stream.atEnd()) { - int id = 0; - stream >> id; - setWeapon(parent, id); - } - return true; -} - -void BankTreeModel::setWeapon(const QModelIndex& index, int data) -{ - auto item = dynamic_cast(this->getItem(index)); - Assert(item != nullptr); - if (item != nullptr) { - item->setWeapon(data); - QVector roles; - QAbstractItemModel::dataChanged(index, index, roles); - } -} - -bool BankTreeRoot::setData(int column, const QVariant& value) -{ - Q_UNUSED(column); - Q_UNUSED(value); - return false; -} -QVariant BankTreeRoot::data(int column) const -{ - switch (column) { - case 0: - return "Name/Weapon"; - break; - case 1: - return "Ammo"; - break; - default: - return {}; - } -} -Qt::ItemFlags BankTreeRoot::getFlags(int column) const -{ - Q_UNUSED(column); - return {}; -} - -int BankTreeModel::checktype(const QModelIndex index) const -{ - int type; - BankTreeItem* item = getItem(index); - auto bankTest = dynamic_cast(item); - auto labelTest = dynamic_cast(item); - if (bankTest) { - type = 0; - } else if (labelTest) { - type = 1; - } else { - type = -1; - } - return type; -} -*/ -//BankTreeModel::BankTreeModel(QObject* parent) : QStandardItemModel(parent) {} -//BankTreeModel::~BankTreeModel() = default; -} // namespace fso::fred \ No newline at end of file diff --git a/qtfred/src/ui/dialogs/ShipEditor/BankModel.h b/qtfred/src/ui/dialogs/ShipEditor/BankModel.h deleted file mode 100644 index 184a31a3239..00000000000 --- a/qtfred/src/ui/dialogs/ShipEditor/BankModel.h +++ /dev/null @@ -1,12 +0,0 @@ -#pragma once -#include "mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h" - -#include -namespace fso::fred { -//class BankTreeModel : public QStandardItemModel { - //Q_OBJECT - //public: - //BankTreeModel(QObject* parent = nullptr); - //~BankTreeModel() override; -//}; -} // namespace fso::fred \ No newline at end of file diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp index cc46ceff012..24538cd4e6c 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp @@ -39,7 +39,7 @@ class AmmoSpinBoxDelegate : public QStyledItemDelegate { auto* editor = new QSpinBox(parent); editor->setMinimum(0); const QModelIndex col0 = index.sibling(index.row(), 0); - editor->setMaximum(col0.data(Qt::UserRole + 5).toInt()); + editor->setMaximum(col0.data(BankItemMaxAmmoRole).toInt()); editor->setFrame(false); editor->setAutoFillBackground(true); return editor; @@ -137,8 +137,6 @@ void ShipWeaponsDialog::initTab(TabState& tab, Mode mode) connect(tab.setAllButton, &QPushButton::clicked, this, [this, &tab]() { onSetAllClicked(tab); }); connect(tab.aiButton, &QPushButton::clicked, this, [this, &tab]() { onAiButtonClicked(tab); }); connect(tab.tblButton, &QPushButton::clicked, this, [this, &tab]() { onTblButtonClicked(tab); }); - connect(tab.aiCombo, QOverload::of(&QComboBox::currentIndexChanged), this, - [this, &tab](int idx) { onAiComboChanged(tab, idx); }); connect(tab.bankModel, &QStandardItemModel::itemChanged, this, [this, &tab](QStandardItem* item) { onBankItemChanged(tab, item); }); connect(tab.tree, &bankTree::weaponDroppedFromList, this, @@ -189,9 +187,8 @@ void ShipWeaponsDialog::loadBankModel(TabState& tab) auto nameItem = new QStandardItem(); const SCP_string name = banksLabel(banks); nameItem->setData(name.c_str(), Qt::DisplayRole); - nameItem->setData(true, Qt::UserRole + 2); - nameItem->setData(banks->getId(), Qt::UserRole + 3); - nameItem->setData(banks->getAiClass(), Qt::UserRole + 6); + nameItem->setData(true, BankItemIsLabelRole); + nameItem->setData(banks->getId(), BankItemIdRole); nameItem->setFlags(nameItem->flags() & ~Qt::ItemIsEditable); auto labelAmmoItem = new QStandardItem(); labelAmmoItem->setFlags(Qt::NoItemFlags); @@ -211,10 +208,9 @@ void ShipWeaponsDialog::loadBankModel(TabState& tab) } weaponItem->setData(weaponName, Qt::DisplayRole); weaponItem->setData(bank->getWeaponId(), Qt::UserRole); - weaponItem->setData(false, Qt::UserRole + 2); - weaponItem->setData(bank->getBankId(), Qt::UserRole + 3); - weaponItem->setData(bank->getAmmo(), Qt::UserRole + 4); - weaponItem->setData(bank->getMaxAmmo(), Qt::UserRole + 5); + weaponItem->setData(false, BankItemIsLabelRole); + weaponItem->setData(bank->getBankId(), BankItemIdRole); + weaponItem->setData(bank->getMaxAmmo(), BankItemMaxAmmoRole); Qt::ItemFlags weaponFlags = weaponItem->flags() & ~Qt::ItemIsEditable; // Only slots that have a real weapon can be dragged out. if (bank->getWeaponId() >= 0) { @@ -340,14 +336,6 @@ void ShipWeaponsDialog::onTblButtonClicked(TabState& tab) } } -void ShipWeaponsDialog::onAiComboChanged(TabState& tab, int index) -{ - if (index < 0) { - return; - } - tab.currentAI = tab.aiCombo->itemData(index).toInt(); -} - void ShipWeaponsDialog::updateUI() { updateTabUI(_primary); @@ -397,7 +385,6 @@ void ShipWeaponsDialog::updateTabUI(TabState& tab) } tab.aiGroup->setEnabled(aiEditable); if (aiEditable && combinedInitialized && !combinedMixed) { - tab.currentAI = displayedAi; tab.aiCombo->setCurrentIndex(tab.aiCombo->findData(displayedAi)); } else { // Mixed across selection, or only Pilot selected (combo greyed): clear the combo. @@ -414,10 +401,10 @@ Banks* ShipWeaponsDialog::banksForIndex(const TabState& tab, const QModelIndex& if (!idx.isValid()) { return nullptr; } - if (!idx.data(Qt::UserRole + 2).toBool()) { + if (!idx.data(BankItemIsLabelRole).toBool()) { return nullptr; } - const int banksId = idx.data(Qt::UserRole + 3).toInt(); + const int banksId = idx.data(BankItemIdRole).toInt(); for (Banks* banks : banksForMode(tab.mode)) { if (banks->getId() == banksId) { return banks; @@ -431,7 +418,7 @@ Bank* ShipWeaponsDialog::bankForIndex(const TabState& tab, const QModelIndex& id if (!idx.isValid()) { return nullptr; } - if (idx.data(Qt::UserRole + 2).toBool()) { + if (idx.data(BankItemIsLabelRole).toBool()) { return nullptr; } const QModelIndex parent = idx.parent(); @@ -442,7 +429,7 @@ Bank* ShipWeaponsDialog::bankForIndex(const TabState& tab, const QModelIndex& id if (parentBanks == nullptr) { return nullptr; } - const int bankId = idx.data(Qt::UserRole + 3).toInt(); + const int bankId = idx.data(BankItemIdRole).toInt(); for (Bank* bank : parentBanks->getBanks()) { if (bank->getBankId() == bankId) { return bank; @@ -458,12 +445,11 @@ void ShipWeaponsDialog::refreshBankItem(TabState& tab, const QModelIndex& idx) } const QModelIndex col0 = idx.sibling(idx.row(), 0); tab.internalUpdate = true; - if (col0.data(Qt::UserRole + 2).toBool()) { + if (col0.data(BankItemIsLabelRole).toBool()) { Banks* banks = banksForIndex(tab, col0); if (banks != nullptr) { const SCP_string name = banksLabel(banks); tab.bankModel->setData(col0, name.c_str(), Qt::DisplayRole); - tab.bankModel->setData(col0, banks->getAiClass(), Qt::UserRole + 6); } tab.internalUpdate = false; return; @@ -487,8 +473,7 @@ void ShipWeaponsDialog::refreshBankItem(TabState& tab, const QModelIndex& idx) } tab.bankModel->setData(col0, name, Qt::DisplayRole); tab.bankModel->setData(col0, bank->getWeaponId(), Qt::UserRole); - tab.bankModel->setData(col0, bank->getAmmo(), Qt::UserRole + 4); - tab.bankModel->setData(col0, bank->getMaxAmmo(), Qt::UserRole + 5); + tab.bankModel->setData(col0, bank->getMaxAmmo(), BankItemMaxAmmoRole); if (QStandardItem* weaponItem = tab.bankModel->itemFromIndex(col0)) { Qt::ItemFlags f = weaponItem->flags(); if (bank->getWeaponId() >= 0) { @@ -532,7 +517,7 @@ void ShipWeaponsDialog::onBankItemChanged(TabState& tab, QStandardItem* item) return; } const QModelIndex col0 = item->index().sibling(item->row(), 0); - if (!col0.isValid() || col0.data(Qt::UserRole + 2).toBool()) { + if (!col0.isValid() || col0.data(BankItemIsLabelRole).toBool()) { return; } Bank* bank = bankForIndex(tab, col0); @@ -617,13 +602,13 @@ QModelIndex ShipWeaponsDialog::indexForBank(const TabState& tab, int banksId, in const int topRows = tab.bankModel->rowCount(); for (int i = 0; i < topRows; i++) { const QModelIndex parent = tab.bankModel->index(i, 0); - if (parent.data(Qt::UserRole + 3).toInt() != banksId) { + if (parent.data(BankItemIdRole).toInt() != banksId) { continue; } const int childRows = tab.bankModel->rowCount(parent); for (int j = 0; j < childRows; j++) { const QModelIndex child = tab.bankModel->index(j, 0, parent); - if (child.data(Qt::UserRole + 3).toInt() == bankId) { + if (child.data(BankItemIdRole).toInt() == bankId) { return child; } } diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h index cc1fc9d56ef..838eb1c206a 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h @@ -1,10 +1,10 @@ #ifndef SHIPWEAPONSDIALOG_H #define SHIPWEAPONSDIALOG_H -#include "ui/dialogs/ShipEditor/BankModel.h" #include "ui/widgets/bankTree.h" #include +#include #include #include @@ -49,7 +49,6 @@ class ShipWeaponsDialog : public QDialog { QWidget* aiGroup = nullptr; QStandardItemModel* bankModel = nullptr; WeaponModel* weapons = nullptr; - int currentAI = 0; // Set while the dialog itself is writing into bankModel, so itemChanged handlers can ignore the resulting signals. bool internalUpdate = false; }; @@ -62,7 +61,6 @@ class ShipWeaponsDialog : public QDialog { void onSetAllClicked(TabState& tab); void onAiButtonClicked(TabState& tab); void onTblButtonClicked(TabState& tab); - void onAiComboChanged(TabState& tab, int index); void onBankItemChanged(TabState& tab, QStandardItem* item); void onWeaponDroppedFromList(TabState& tab, const QModelIndex& target, int weaponId); void onWeaponMoved(TabState& tab, const QModelIndex& target, int weaponId, int sourceBanksId, diff --git a/qtfred/src/ui/widgets/bankTree.cpp b/qtfred/src/ui/widgets/bankTree.cpp index ca72eb047c1..3043fc181ca 100644 --- a/qtfred/src/ui/widgets/bankTree.cpp +++ b/qtfred/src/ui/widgets/bankTree.cpp @@ -26,7 +26,7 @@ void bankTree::dragEnterEvent(QDragEnterEvent* event) void bankTree::dragMoveEvent(QDragMoveEvent* event) { const QModelIndex index = indexAt(event->pos()); - if (!index.isValid() || index.data(Qt::UserRole + 2).toBool()) { + if (!index.isValid() || index.data(BankItemIsLabelRole).toBool()) { event->ignore(); return; } @@ -36,7 +36,7 @@ void bankTree::dragMoveEvent(QDragMoveEvent* event) void bankTree::dropEvent(QDropEvent* event) { QModelIndex target = indexAt(event->pos()); - if (!target.isValid() || target.data(Qt::UserRole + 2).toBool()) { + if (!target.isValid() || target.data(BankItemIsLabelRole).toBool()) { event->ignore(); return; } @@ -74,7 +74,7 @@ void bankTree::startDrag(Qt::DropActions /*supportedActions*/) if (idx.column() != 0 || !idx.isValid()) { continue; } - if (idx.data(Qt::UserRole + 2).toBool()) { + if (idx.data(BankItemIsLabelRole).toBool()) { continue; } source = idx; @@ -93,8 +93,8 @@ void bankTree::startDrag(Qt::DropActions /*supportedActions*/) if (!parent.isValid()) { return; } - const int sourceBanksId = parent.data(Qt::UserRole + 3).toInt(); - const int sourceBankId = source.data(Qt::UserRole + 3).toInt(); + const int sourceBanksId = parent.data(BankItemIdRole).toInt(); + const int sourceBankId = source.data(BankItemIdRole).toInt(); auto* mime = new QMimeData(); QByteArray bytes; @@ -130,7 +130,7 @@ void bankTree::selectionChanged(const QItemSelection& selected, const QItemSelec if (!pivot.isValid()) { return; } - const bool pivotIsBank = pivot.data(Qt::UserRole + 2).toBool(); + const bool pivotIsBank = pivot.data(BankItemIsLabelRole).toBool(); QItemSelectionModel* sm = selectionModel(); QItemSelection toDeselect; @@ -138,7 +138,7 @@ void bankTree::selectionChanged(const QItemSelection& selected, const QItemSelec if (idx.column() != 0) { continue; } - if (idx.data(Qt::UserRole + 2).toBool() != pivotIsBank) { + if (idx.data(BankItemIsLabelRole).toBool() != pivotIsBank) { toDeselect.select(idx, idx); } } @@ -160,7 +160,7 @@ bankTree::SelectionType bankTree::getSelectionType() const if (idx.column() != 0 || !idx.isValid()) { continue; } - return idx.data(Qt::UserRole + 2).toBool() ? SelectionType::Bank : SelectionType::Weapon; + return idx.data(BankItemIsLabelRole).toBool() ? SelectionType::Bank : SelectionType::Weapon; } return SelectionType::None; } diff --git a/qtfred/src/ui/widgets/bankTree.h b/qtfred/src/ui/widgets/bankTree.h index 56eb11ba433..f9fb1374b5d 100644 --- a/qtfred/src/ui/widgets/bankTree.h +++ b/qtfred/src/ui/widgets/bankTree.h @@ -1,5 +1,4 @@ #pragma once -#include "ui/dialogs/ShipEditor/BankModel.h" #include #include @@ -7,6 +6,12 @@ #include #include namespace fso::fred { +// Custom data roles stored on items of the bank tree's QStandardItemModel. +// (Qt::UserRole itself is used for the weapon-id on weapon-slot rows.) +constexpr int BankItemIsLabelRole = Qt::UserRole + 2; // bool: true on bank-label rows, false on weapon-slot rows +constexpr int BankItemIdRole = Qt::UserRole + 3; // Banks::getId() on labels, Bank::getBankId() on slots +constexpr int BankItemMaxAmmoRole = Qt::UserRole + 5; // weapon's max ammo on the bank, 0 if not applicable + class bankTree : public QTreeView { Q_OBJECT public: From 2553695a797318237c0bb2d1df5559e71943eacc Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Tue, 12 May 2026 14:09:54 -0500 Subject: [PATCH 09/17] fix build issue on linux --- qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.cpp b/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.cpp index 87742a8cdba..9d75963081b 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.cpp @@ -559,6 +559,7 @@ SCP_vector> ShipEditorDialogModel::getArrivalPaths() } else { allowed = (m_path_mask & (1 << i)) != 0; } + m_path_list.emplace_back(name, allowed); } return m_path_list; } From 7b181c43f46850402bebedb681ebbe44019bc0ae Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Tue, 12 May 2026 14:24:03 -0500 Subject: [PATCH 10/17] revert non-weapons dialog changes --- .../ui/dialogs/ShipEditor/ShipEditorDialog.cpp | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.cpp b/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.cpp index daa2cc8050f..15beeed4f40 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.cpp +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.cpp @@ -302,11 +302,9 @@ void ShipEditorDialog::updateArrival(bool overwrite) // determine if this ship has a docking bay pm = model_get(Ship_info[Ships[objp->instance].ship_info_index].model_num); Assert(pm); - if (pm) { - if (pm->ship_bay && (pm->ship_bay->num_paths > 0)) { - auto ship = get_ship_from_obj(objp); - ui->arrivalTargetCombo->addItem(Ships[ship].ship_name, QVariant(ship)); - } + if (pm->ship_bay && (pm->ship_bay->num_paths > 0)) { + auto ship = get_ship_from_obj(objp); + ui->arrivalTargetCombo->addItem(Ships[ship].ship_name, QVariant(ship)); } } } @@ -365,13 +363,10 @@ void ShipEditorDialog::updateDeparture(bool overwrite) // determine if this ship has a docking bay pm = model_get(Ship_info[Ships[objp->instance].ship_info_index].model_num); Assert(pm); - if (pm != nullptr) { - if (pm->ship_bay && (pm->ship_bay->num_paths > 0)) { - auto ship = get_ship_from_obj(objp); - ui->departureTargetCombo->addItem(Ships[ship].ship_name, QVariant(ship)); - } + if (pm->ship_bay && (pm->ship_bay->num_paths > 0)) { + auto ship = get_ship_from_obj(objp); + ui->departureTargetCombo->addItem(Ships[ship].ship_name, QVariant(ship)); } - } } ui->departureTargetCombo->setCurrentIndex(ui->departureTargetCombo->findData(_model->getDepartureTarget())); From 5206fcd16c62a7b89eaacab3043e65071aa94a05 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Tue, 12 May 2026 15:11:32 -0500 Subject: [PATCH 11/17] clang --- .../ShipEditor/ShipWeaponsDialogModel.cpp | 121 ++++++++---------- .../ShipEditor/ShipWeaponsDialogModel.h | 13 +- .../dialogs/ShipEditor/ShipWeaponsDialog.cpp | 22 ++-- .../ui/dialogs/ShipEditor/ShipWeaponsDialog.h | 6 +- 4 files changed, 73 insertions(+), 89 deletions(-) diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp index 68f8d5998ab..9bca99e5359 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp @@ -71,7 +71,8 @@ WeaponItem::WeaponItem(int inID, QString inName, bool inAllowed) { } -WeaponModel::WeaponModel(int type, int shipClass, bool bigShip) +WeaponModel::WeaponModel(int type, int shipClass, bool bigShip, QObject* parent) + : QAbstractListModel(parent) { weapons.push_back(new WeaponItem(-1, "None", true)); @@ -160,9 +161,14 @@ QMimeData* WeaponModel::mimeData(const QModelIndexList& indexes) const } Banks::Banks(SCP_string _name, int aiIndex, int _ship, int multiedit, int _id, ship_subsys* _subsys) - : m_isMultiEdit(multiedit), name(std::move(_name)), subsys(_subsys), initialAI(aiIndex), ship(_ship), id(_id) + : m_isMultiEdit(multiedit), name(std::move(_name)), subsys(_subsys), currentAi(aiIndex), ship(_ship), id(_id) { - aiClass = aiIndex; +} +Banks::~Banks() +{ + for (Bank* b : banks) { + delete b; + } } int Banks::getId() const { @@ -194,66 +200,26 @@ SCP_vector Banks::getBanks() const } int Banks::getAiClass() const { - auto readShip = [&](int inst) -> int { - if (name == "Pilot") { - return Ships[inst].weapons.ai_class; - } - ship_subsys* pss = findTurretByName(inst, name); - return pss ? pss->weapons.ai_class : 0; - }; - - if (!m_isMultiEdit) { - return readShip(ship); - } - - int common = -1; - bool initialized = false; - for (object* 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) || !ptr->flags[Object::Object_Flags::Marked]) { - continue; - } - const int ai = readShip(ptr->instance); - if (!initialized) { - common = ai; - initialized = true; - } else if (ai != common) { - return -1; - } - } - return common; + return currentAi; } void Banks::setAiClass(int newClass) { - aiClass = newClass; + currentAi = newClass; aiClassDirty = true; - - auto applyToShip = [&](int inst) { - if (name == "Pilot") { - Ships[inst].weapons.ai_class = newClass; - } else if (ship_subsys* pss = findTurretByName(inst, name)) { - pss->weapons.ai_class = newClass; - } - }; - - if (m_isMultiEdit) { - for (object* 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) || !ptr->flags[Object::Object_Flags::Marked]) { - continue; - } - applyToShip(ptr->instance); - } - } else { - applyToShip(ship); +} +void Banks::reconcileAiClass(int otherAi) +{ + if (currentAi == -1) { + return; + } + if (currentAi != otherAi) { + currentAi = -1; } } bool Banks::isAiClassDirty() const { return aiClassDirty; } -int Banks::getInitialAI() const -{ - return initialAI; -} Bank::Bank(const int _weaponId, const int _bankId, const int _ammoMax, const int _ammo, Banks* _parent) { this->weaponId = _weaponId; @@ -313,6 +279,15 @@ ShipWeaponsDialogModel::ShipWeaponsDialogModel(QObject* parent, EditorViewport* { initializeData(isMultiEdit); } +ShipWeaponsDialogModel::~ShipWeaponsDialogModel() +{ + for (Banks* b : PrimaryBanks) { + delete b; + } + for (Banks* b : SecondaryBanks) { + delete b; + } +} bool ShipWeaponsDialogModel::selectedShipsShareClass() { int sharedClass = -1; @@ -336,8 +311,14 @@ void ShipWeaponsDialogModel::initializeData(bool isMultiEdit) SecondaryBanks.clear(); m_ship = _editor->cur_ship; - if (m_ship == -1) + if (m_ship == -1) { + Assertion(_editor->currentObject >= 0 && _editor->currentObject < MAX_OBJECTS, + "ShipWeaponsDialog opened with no valid current ship and an out-of-range currentObject (%d)", + _editor->currentObject); m_ship = Objects[_editor->currentObject].instance; + } + Assertion(m_ship >= 0 && m_ship < MAX_SHIPS, + "ShipWeaponsDialog resolved to invalid ship index %d", m_ship); if (m_isMultiEdit) { object* ptr = GET_FIRST(&obj_used_list); @@ -415,6 +396,7 @@ void ShipWeaponsDialogModel::initPrimary(int inst, bool first) // Subsequent ship: reconcile each slot against the tracking Banks built from the first ship. delete pilotBank; // unused; we already have one from the first ship if (Banks* tracking = findBanksByName(PrimaryBanks, "Pilot")) { + tracking->reconcileAiClass(Ships[inst].weapons.ai_class); const auto bankList = tracking->getBanks(); for (size_t i = 0; i < bankList.size(); i++) { reconcileSlot(bankList[i], Ships[inst].weapons.primary_bank_weapons[i], @@ -430,6 +412,7 @@ void ShipWeaponsDialogModel::initPrimary(int inst, bool first) if (tracking == nullptr) { continue; } + tracking->reconcileAiClass(pss->weapons.ai_class); const auto bankList = tracking->getBanks(); for (size_t i = 0; i < bankList.size(); i++) { reconcileSlot(bankList[i], pss->weapons.primary_bank_weapons[i], @@ -491,6 +474,7 @@ void ShipWeaponsDialogModel::initSecondary(int inst, bool first) } else { delete pilotBank; if (Banks* tracking = findBanksByName(SecondaryBanks, "Pilot")) { + tracking->reconcileAiClass(Ships[inst].weapons.ai_class); const auto bankList = tracking->getBanks(); for (size_t i = 0; i < bankList.size(); i++) { reconcileSlot(bankList[i], Ships[inst].weapons.secondary_bank_weapons[i], @@ -506,6 +490,7 @@ void ShipWeaponsDialogModel::initSecondary(int inst, bool first) if (tracking == nullptr) { continue; } + tracking->reconcileAiClass(pss->weapons.ai_class); const auto bankList = tracking->getBanks(); for (size_t i = 0; i < bankList.size(); i++) { reconcileSlot(bankList[i], pss->weapons.secondary_bank_weapons[i], @@ -517,10 +502,18 @@ void ShipWeaponsDialogModel::initSecondary(int inst, bool first) void ShipWeaponsDialogModel::saveShip(int inst) { auto saveBank = [&](Banks* turret, bool isPrimary) { + ship_weapon* target = nullptr; if (turret->getName() == "Pilot") { - saveSlotsTo(Ships[inst].weapons, turret->getBanks(), isPrimary); + target = &Ships[inst].weapons; } else if (ship_subsys* pss = findTurretByName(inst, turret->getName())) { - saveSlotsTo(pss->weapons, turret->getBanks(), isPrimary); + target = &pss->weapons; + } + if (target == nullptr) { + return; + } + saveSlotsTo(*target, turret->getBanks(), isPrimary); + if (turret->isAiClassDirty()) { + target->ai_class = turret->getAiClass(); } }; for (Banks* turret : PrimaryBanks) { @@ -549,19 +542,9 @@ bool ShipWeaponsDialogModel::apply() } void ShipWeaponsDialogModel::reject() { - // Only restore AI on banks the user explicitly changed. In multi-edit this can only - // roll back to the first ship's initial AI for all ships which is acceptable since we only - // reach here when the user took the explicit Change AI action. - for (Banks* banks : PrimaryBanks) { - if (banks->isAiClassDirty()) { - banks->setAiClass(banks->getInitialAI()); - } - } - for (Banks* banks : SecondaryBanks) { - if (banks->isAiClassDirty()) { - banks->setAiClass(banks->getInitialAI()); - } - } + // Weapons, ammo, and AI class are all buffered in the Banks/Bank model and only written + // to Ships[] in apply(). Cancel/close is therefore a true no-op as far as mission data + // is concerned: the next dialog open re-reads ship state from scratch. } SCP_vector ShipWeaponsDialogModel::getPrimaryBanks() const { diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h index 81ed9b799ce..9e8bab5ce1c 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h @@ -17,7 +17,7 @@ struct WeaponItem { class WeaponModel : public QAbstractListModel { Q_OBJECT public: - WeaponModel(int type, int shipClass, bool bigShip); + WeaponModel(int type, int shipClass, bool bigShip, QObject* parent = nullptr); ~WeaponModel() override; int rowCount(const QModelIndex& parent = QModelIndex()) const override; QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; @@ -30,6 +30,7 @@ class WeaponModel : public QAbstractListModel { struct Bank; struct Banks { Banks(SCP_string name, int aiIndex, int ship, int multiedit, int _id, ship_subsys* subsys = nullptr); + ~Banks(); public: int getId() const; @@ -39,18 +40,19 @@ struct Banks { ship_subsys* getSubsys() const; bool empty() const; SCP_vector getBanks() const; - // Returns the single consistent AI class for this bank-set; -1 if multi-edit and ships disagree. + // Returns the cached AI class for this bank-set; -1 if multi-edit and ships disagree. int getAiClass() const; void setAiClass(int); + // Called per-additional-ship during multi-edit init: marks currentAi mixed (-1) if otherAi + // differs from the cached value. Does not mark the bank dirty. + void reconcileAiClass(int otherAi); bool isAiClassDirty() const; bool m_isMultiEdit; - int getInitialAI() const; private: SCP_string name; ship_subsys* subsys; - int aiClass; - int initialAI; + int currentAi; bool aiClassDirty = false; SCP_vector banks; int ship; @@ -79,6 +81,7 @@ namespace dialogs { class ShipWeaponsDialogModel : public AbstractDialogModel { public: ShipWeaponsDialogModel(QObject* parent, EditorViewport* viewport, bool multi); + ~ShipWeaponsDialogModel() override; // True iff all currently-marked ships share the same ship_info_index. Used to gate multi-edit. static bool selectedShipsShareClass(); diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp index 24538cd4e6c..ac1e2273af7 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp @@ -69,7 +69,10 @@ ShipWeaponsDialog::ShipWeaponsDialog(QDialog* parent, EditorViewport* viewport, Error("No Valid Weapon banks on ship"); } - // Tertiary banks are not implemented in the game engine so hide the placeholder tab until they are. + // Tertiary banks are not implemented in the game engine yet, but the UI scaffolding (the + // tertiaryPage in the .ui file, the Mode::Tertiary enum value, the default branch in + // banksForMode) is intentionally preserved so that wiring the engine side later is a small + // follow-up rather than a re-build. ui->tabWidget->removeTab(2); initTab(_primary, Primary); @@ -83,13 +86,7 @@ ShipWeaponsDialog::ShipWeaponsDialog(QDialog* parent, EditorViewport* viewport, updateUI(); } -ShipWeaponsDialog::~ShipWeaponsDialog() -{ - delete _primary.bankModel; - delete _primary.weapons; - delete _secondary.bankModel; - delete _secondary.weapons; -} +ShipWeaponsDialog::~ShipWeaponsDialog() = default; void ShipWeaponsDialog::initTab(TabState& tab, Mode mode) { @@ -116,7 +113,7 @@ void ShipWeaponsDialog::initTab(TabState& tab, Mode mode) const util::SignalBlockers blockers(this); tab.bankModel = new QStandardItemModel(this); - tab.weapons = new WeaponModel(static_cast(mode), _model->getShipClass(), _model->isBigShip()); + tab.weapons = new WeaponModel(static_cast(mode), _model->getShipClass(), _model->isBigShip(), this); loadBankModel(tab); tab.tree->setModel(tab.bankModel); tab.list->setModel(tab.weapons); @@ -161,11 +158,12 @@ SCP_vector ShipWeaponsDialog::banksForMode(Mode mode) const case Secondary: return _model->getSecondaryBanks(); default: + // Tertiary banks are unsupported by the engine; placeholder mode returns no banks. return {}; } } -SCP_string ShipWeaponsDialog::banksLabel(const Banks* banks) const +SCP_string ShipWeaponsDialog::banksLabel(const Banks* banks) { if (banks->getName() == "Pilot") { return banks->getName(); @@ -491,7 +489,7 @@ void ShipWeaponsDialog::refreshBankItem(TabState& tab, const QModelIndex& idx) Qt::ItemFlags ammoFlags = Qt::ItemIsSelectable | Qt::ItemIsEnabled; if (bank->getMaxAmmo() > 0) { if (bank->getAmmo() == -2) { - ammoItem->setData(QString(), Qt::DisplayRole); + ammoItem->setData(formatAmmoConflict(bank->getMaxAmmo()), Qt::DisplayRole); ammoItem->setData(0, AmmoValueRole); } else { ammoItem->setData(formatAmmoDisplay(bank->getAmmo(), bank->getMaxAmmo()), Qt::DisplayRole); @@ -594,7 +592,7 @@ void ShipWeaponsDialog::onWeaponMoved(TabState& tab, const QModelIndex& target, _model->notifyChanged(); } -QModelIndex ShipWeaponsDialog::indexForBank(const TabState& tab, int banksId, int bankId) const +QModelIndex ShipWeaponsDialog::indexForBank(const TabState& tab, int banksId, int bankId) { if (tab.bankModel == nullptr) { return {}; diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h index 838eb1c206a..c233b4c542e 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h @@ -35,7 +35,7 @@ class ShipWeaponsDialog : public QDialog { void on_okAndCancelButtons_accepted(); void on_okAndCancelButtons_rejected(); - private: + private: // NOLINT(readability-redundant-access-specifiers) enum Mode { Primary = 0, Secondary = 1, Tertiary = 2 }; struct TabState { @@ -66,13 +66,13 @@ class ShipWeaponsDialog : public QDialog { void onWeaponMoved(TabState& tab, const QModelIndex& target, int weaponId, int sourceBanksId, int sourceBankId, bool isCopy); - QModelIndex indexForBank(const TabState& tab, int banksId, int bankId) const; + static QModelIndex indexForBank(const TabState& tab, int banksId, int bankId); Bank* bankForIndex(const TabState& tab, const QModelIndex& idx) const; Banks* banksForIndex(const TabState& tab, const QModelIndex& idx) const; void refreshBankItem(TabState& tab, const QModelIndex& idx); SCP_vector banksForMode(Mode mode) const; - SCP_string banksLabel(const Banks* banks) const; + static SCP_string banksLabel(const Banks* banks); std::unique_ptr ui; std::unique_ptr _model; From 2e676daeb3cca33a3874f5103ad617e3a5f28c0d Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Tue, 12 May 2026 16:37:29 -0500 Subject: [PATCH 12/17] clang --- .../src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp index 9bca99e5359..c54bea9ce14 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp @@ -312,12 +312,12 @@ void ShipWeaponsDialogModel::initializeData(bool isMultiEdit) m_ship = _editor->cur_ship; if (m_ship == -1) { - Assertion(_editor->currentObject >= 0 && _editor->currentObject < MAX_OBJECTS, + Assertion(_editor->currentObject >= 0 && _editor->currentObject < MAX_OBJECTS, // NOLINT(readability-simplify-boolean-expr) "ShipWeaponsDialog opened with no valid current ship and an out-of-range currentObject (%d)", _editor->currentObject); m_ship = Objects[_editor->currentObject].instance; } - Assertion(m_ship >= 0 && m_ship < MAX_SHIPS, + Assertion(m_ship >= 0 && m_ship < MAX_SHIPS, // NOLINT(readability-simplify-boolean-expr) "ShipWeaponsDialog resolved to invalid ship index %d", m_ship); if (m_isMultiEdit) { From a5d671861087c26258d065771a611b4f5b7a7bcb Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Tue, 12 May 2026 16:39:47 -0500 Subject: [PATCH 13/17] update help doc --- .../doc/dialogs/ShipWeaponsDialog.html | 111 +++++++++++++++--- 1 file changed, 97 insertions(+), 14 deletions(-) diff --git a/qtfred/help-src/doc/dialogs/ShipWeaponsDialog.html b/qtfred/help-src/doc/dialogs/ShipWeaponsDialog.html index 32182e89bf1..9ba65772726 100644 --- a/qtfred/help-src/doc/dialogs/ShipWeaponsDialog.html +++ b/qtfred/help-src/doc/dialogs/ShipWeaponsDialog.html @@ -9,23 +9,106 @@

Ship Weapons

Accessed via the Weapons button in the Ship Editor.

-

Assigns weapons to the ship's banks and turrets for this mission instance, -overriding the ship class defaults.

+

Assigns weapons, starting ammo, and per-turret AI class to the ship's banks and +turrets for this mission instance, overriding the ship class defaults.

+

All changes are buffered. Nothing is written to the mission until you click +OK; Cancel and closing the dialog discard +everything.

-

Mode

-

Select Primary, Secondary, or -Turret to switch which set of banks is shown. The -Tertiary option is not currently implemented.

+

Layout

+

Weapons are split across two tabs:

+
    +
  • Primary - primary weapon banks for the pilot and any + turret with a primary slot.
  • +
  • Secondary - secondary banks for the pilot and any + turret with a secondary slot.
  • +
+

The dialog opens on whichever tab has banks first; if a ship has only secondaries, +the Secondary tab is shown.

+

Each tab contains:

+
    +
  • A Weapons list on the left showing weapons valid for this + bank type and ship size. Weapons that are not in the ship class's allowed + loadout are shown in gray with a tooltip explaining why; you can still assign + them in the editor.
  • +
  • A Banks tree on the right showing Pilot and each + turret as collapsible groups, with their bank slots and current ammo + underneath.
  • +
  • An AI selector beneath the tree for changing a turret's AI + class.
  • +

Assigning weapons

-

The right panel lists the banks or turrets for the selected mode. Select the bank -or turret you want to change. Then select the desired weapon from the left panel and -click Set Selected to assign it.

-

Where a weapon uses ammo, the ammo count appears as a second column next to the -weapon name. Click the value to edit the starting ammo for that bank directly.

+

Several ways:

+
    +
  • Drag from the weapons list onto a bank slot. The slot is set + to the dropped weapon, and ammo capacity is recalculated.
  • +
  • Drag from one bank slot to another. By default the weapon is + moved (the source slot becomes empty). Hold Ctrl while dropping to + copy instead.
  • +
  • Select one or more bank slots, pick a weapon from the list, and + click Set Selected. The chosen weapon is assigned to every selected + slot at once. Set Selected is enabled only when slot rows + (not turret-group rows) are selected.
  • +
+

Selection within the Banks tree is restricted to one row type at a time: clicking +a turret-group row clears any selected slot rows, and vice versa. This keeps the +AI controls and Set Selected controls clearly applicable to whatever is currently +highlighted.

-

Turret AI class

-

When in Turret mode, a dropdown and button at the bottom allow the AI class of the -selected turret to be changed independently of the ship's overall AI class.

+

Editing ammo

+

Where a weapon uses ammo, the slot's row has a second column showing +current/max. Click the value to edit; a spinbox appears, clamped to the +maximum capacity for that weapon and bank. The display refreshes as soon as the +edit is committed.

+ +

View Table

+

With a weapon highlighted in the list, the View Table button +opens a viewer for that weapon's weapons.tbl entry (including any +modular -wep.tbm overrides). Useful for quickly checking damage, +ammo capacity, or flags without leaving the dialog.

+ +

AI class

+

Each turret has its own AI class, independent of the ship's overall AI class. +Select one or more turret-group rows (the parent rows, not the individual +slots), pick an AI class from the combo, and click Change AI. The +new value is applied to every selected turret on every marked ship.

+

The combo reflects the selection:

+
    +
  • All selected turrets share the same AI - the combo shows that value.
  • +
  • Selected turrets disagree (within one ship, or across ships in multi-edit) + - the combo is blank; pick a value and click Change AI to set them + all to that value.
  • +
+

The Pilot row is shown for context but its AI is owned by the +Ship Editor and cannot be changed here. When +only the Pilot row is selected the AI controls are disabled.

+ +

Editing multiple ships at once

+

If multiple ships of the same class are marked when the dialog is opened, +edits apply to all of them. The dialog reconciles per-slot state from the marked +ships at open time:

+
    +
  • Slots where every marked ship has the same weapon and ammo are shown + normally.
  • +
  • Slots where ships disagree on the weapon show + CONFLICT. Leaving the slot as CONFLICT preserves each + ship's existing weapon on OK; assigning a weapon overwrites all of them.
  • +
  • Slots where ships agree on the weapon but disagree on ammo show + --/max. Leaving it preserves each ship's existing ammo; entering + a value applies that value to all marked ships.
  • +
  • Turret AI is reconciled similarly: matching AIs show in the combo, mixed + AIs show as (Mixed AI) in the turret label and leave the combo + blank.
  • +
+

Multi-edit is only available when every marked ship belongs to the same ship +class. The Ship Editor's Weapons button is disabled +otherwise.

+ +

OK and Cancel

+

OK commits every change (weapons, ammo, and AI) to every +relevant ship and closes the dialog. Cancel, closing the dialog +window, or pressing Esc discards every change; no mission data +is touched. If unsaved changes exist when closing, you are prompted to keep them.

From 46088a7ee89b99125c2aee4ce17d361b09abc5f7 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Wed, 13 May 2026 18:50:13 -0500 Subject: [PATCH 14/17] better model/dialog code split --- .../ShipEditor/ShipWeaponsDialogModel.cpp | 131 +++++------------- .../ShipEditor/ShipWeaponsDialogModel.h | 25 +--- .../dialogs/ShipEditor/ShipWeaponsDialog.cpp | 49 ++++++- .../ui/dialogs/ShipEditor/ShipWeaponsDialog.h | 3 +- qtfred/src/ui/widgets/bankTree.cpp | 1 - qtfred/src/ui/widgets/bankTree.h | 4 + 6 files changed, 95 insertions(+), 118 deletions(-) diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp index c54bea9ce14..cb8b0418ca5 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp @@ -2,8 +2,6 @@ #include -#include - namespace fso::fred { namespace { // Compare one slot of the "tracking" Bank (from the first ship's view) against the corresponding @@ -66,100 +64,6 @@ void saveSlotsTo(ship_weapon& target, const SCP_vector& bankList, bool is } } // namespace -WeaponItem::WeaponItem(int inID, QString inName, bool inAllowed) - : name(std::move(inName)), id(inID), allowed(inAllowed) -{ -} - -WeaponModel::WeaponModel(int type, int shipClass, bool bigShip, QObject* parent) - : QAbstractListModel(parent) -{ - weapons.push_back(new WeaponItem(-1, "None", true)); - - const bool haveShipInfo = shipClass >= 0 && shipClass < ship_info_size(); - // allowed_weapons is a player-loadout concept and only meaningful for fighters/bombers. - // On other ship classes (capships, support, etc.) every weapon is rendered as normal. - const bool applyAllowedTint = haveShipInfo && Ship_info[shipClass].is_fighter_bomber(); - - const int wantedSubtype = (type == 0) ? WP_LASER : WP_MISSILE; - const bool acceptBeams = (type == 0); - - for (int i = 0; i < static_cast(Weapon_info.size()); i++) { - const auto& w = Weapon_info[i]; - if (w.wi_flags[Weapon::Info_Flags::No_fred]) { - continue; - } - if (w.wi_flags[Weapon::Info_Flags::Child]) { - continue; - } - const bool subtypeMatches = (w.subtype == wantedSubtype) || (acceptBeams && w.subtype == WP_BEAM); - if (!subtypeMatches) { - continue; - } - if (!bigShip && w.wi_flags[Weapon::Info_Flags::Big_only]) { - continue; - } - const bool allowed = !applyAllowedTint || Ship_info[shipClass].allowed_weapons[i] != 0; - weapons.push_back(new WeaponItem(i, w.name, allowed)); - } -} -WeaponModel::~WeaponModel() -{ - for (auto pointer : weapons) { - delete pointer; - } -} -int WeaponModel::rowCount(const QModelIndex& parent) const -{ - Q_UNUSED(parent); - return static_cast(weapons.size()); -} -QVariant WeaponModel::data(const QModelIndex& index, int role) const -{ - if (!index.isValid() || index.row() < 0 || index.row() >= weapons.size()) { - return {}; - } - const auto* item = weapons[index.row()]; - switch (role) { - case Qt::DisplayRole: - return item->name; - case Qt::UserRole: - return item->id; - case Qt::ForegroundRole: - return item->allowed ? QVariant() : QVariant(QBrush(Qt::gray)); - case Qt::ToolTipRole: - return item->allowed ? QVariant() : QVariant(QStringLiteral("Not in this ship class's allowed weapons list.")); - default: - return {}; - } -} -Qt::ItemFlags WeaponModel::flags(const QModelIndex& index) const -{ - auto base = QAbstractListModel::flags(index); - if (index.isValid()) { - base |= Qt::ItemIsDragEnabled; - } - return base; -} -QStringList WeaponModel::mimeTypes() const -{ - return {QStringLiteral("application/weaponid")}; -} -QMimeData* WeaponModel::mimeData(const QModelIndexList& indexes) const -{ - auto mimeData = new QMimeData(); - QByteArray encodedData; - QDataStream stream(&encodedData, QIODevice::WriteOnly); - for (auto& index : indexes) { - if (index.isValid()) { - int id = data(index, Qt::UserRole).toInt(); - stream << id; - } - } - mimeData->setData("application/weaponid", encodedData); - return mimeData; -} - Banks::Banks(SCP_string _name, int aiIndex, int _ship, int multiedit, int _id, ship_subsys* _subsys) : m_isMultiEdit(multiedit), name(std::move(_name)), subsys(_subsys), currentAi(aiIndex), ship(_ship), id(_id) { @@ -554,6 +458,41 @@ SCP_vector ShipWeaponsDialogModel::getSecondaryBanks() const { return SecondaryBanks; } +SCP_vector ShipWeaponsDialogModel::getAvailableWeapons(WeaponListType type) const +{ + SCP_vector result; + result.push_back(WeaponItem{-1, "None", true}); + + const int shipClass = getShipClass(); + const bool haveShipInfo = shipClass >= 0 && shipClass < ship_info_size(); + // allowed_weapons is a player-loadout concept and only meaningful for fighters/bombers. + // On other ship classes (capships, support, etc.) every weapon is rendered as normal. + const bool applyAllowedTint = haveShipInfo && Ship_info[shipClass].is_fighter_bomber(); + + const bool isPrimary = (type == WeaponListType::Primary); + const int wantedSubtype = isPrimary ? WP_LASER : WP_MISSILE; + const bool acceptBeams = isPrimary; + + for (int i = 0; i < static_cast(Weapon_info.size()); i++) { + const auto& w = Weapon_info[i]; + if (w.wi_flags[Weapon::Info_Flags::No_fred]) { + continue; + } + if (w.wi_flags[Weapon::Info_Flags::Child]) { + continue; + } + const bool subtypeMatches = (w.subtype == wantedSubtype) || (acceptBeams && w.subtype == WP_BEAM); + if (!subtypeMatches) { + continue; + } + if (!big && w.wi_flags[Weapon::Info_Flags::Big_only]) { + continue; + } + const bool allowed = !applyAllowedTint || Ship_info[shipClass].allowed_weapons[i] != 0; + result.push_back(WeaponItem{i, w.name, allowed}); + } + return result; +} int ShipWeaponsDialogModel::getShipClass() const { return Ships[m_ship].ship_info_index; diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h index 9e8bab5ce1c..97af7e38222 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h @@ -4,29 +4,15 @@ #include -#include -#include - namespace fso::fred { struct WeaponItem { - WeaponItem(int id, QString name, bool allowed); - const QString name; - const int id; - const bool allowed; -}; -class WeaponModel : public QAbstractListModel { - Q_OBJECT - public: - WeaponModel(int type, int shipClass, bool bigShip, QObject* parent = nullptr); - ~WeaponModel() override; - int rowCount(const QModelIndex& parent = QModelIndex()) const override; - QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; - Qt::ItemFlags flags(const QModelIndex& index) const override; - QStringList mimeTypes() const override; - QMimeData* mimeData(const QModelIndexList& indexes) const override; - QVector weapons; + int id; + SCP_string name; + bool allowed; }; +enum class WeaponListType { Primary, Secondary /*, Tertiary — engine support pending */ }; + struct Bank; struct Banks { Banks(SCP_string name, int aiIndex, int ship, int multiedit, int _id, ship_subsys* subsys = nullptr); @@ -90,6 +76,7 @@ class ShipWeaponsDialogModel : public AbstractDialogModel { void reject() override; SCP_vector getPrimaryBanks() const; SCP_vector getSecondaryBanks() const; + SCP_vector getAvailableWeapons(WeaponListType type) const; int getShipClass() const; bool isBigShip() const; void notifyChanged(); diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp index ac1e2273af7..89e1cbb24f8 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp @@ -7,8 +7,10 @@ #include #include +#include #include #include +#include #include #include @@ -29,6 +31,33 @@ QString formatAmmoConflict(int max) return QStringLiteral("--/") + QString::number(max); } +// bankTree's drop handler expects the "application/weaponid" MIME type with a single int payload. +// QStandardItemModel's built-in mimeData uses application/x-qabstractitemmodeldatalist instead, so +// we override here to keep the existing contract. +class WeaponListModel : public QStandardItemModel { + public: + using QStandardItemModel::QStandardItemModel; + + QStringList mimeTypes() const override + { + return {QLatin1String(MIME_WEAPON_ID)}; + } + + QMimeData* mimeData(const QModelIndexList& indexes) const override + { + auto* mime = new QMimeData(); + QByteArray bytes; + QDataStream stream(&bytes, QIODevice::WriteOnly); + for (const QModelIndex& index : indexes) { + if (index.isValid()) { + stream << index.data(Qt::UserRole).toInt(); + } + } + mime->setData(QLatin1String(MIME_WEAPON_ID), bytes); + return mime; + } +}; + class AmmoSpinBoxDelegate : public QStyledItemDelegate { public: using QStyledItemDelegate::QStyledItemDelegate; @@ -113,8 +142,9 @@ void ShipWeaponsDialog::initTab(TabState& tab, Mode mode) const util::SignalBlockers blockers(this); tab.bankModel = new QStandardItemModel(this); - tab.weapons = new WeaponModel(static_cast(mode), _model->getShipClass(), _model->isBigShip(), this); + tab.weapons = new WeaponListModel(this); loadBankModel(tab); + loadWeaponList(tab); tab.tree->setModel(tab.bankModel); tab.list->setModel(tab.weapons); tab.tree->expandAll(); @@ -334,6 +364,23 @@ void ShipWeaponsDialog::onTblButtonClicked(TabState& tab) } } +void ShipWeaponsDialog::loadWeaponList(TabState& tab) +{ + tab.weapons->clear(); + const auto listType = (tab.mode == Primary) ? WeaponListType::Primary : WeaponListType::Secondary; + for (const WeaponItem& item : _model->getAvailableWeapons(listType)) { + auto* row = new QStandardItem(); + row->setData(QString::fromUtf8(item.name.c_str()), Qt::DisplayRole); + row->setData(item.id, Qt::UserRole); + if (!item.allowed) { + row->setData(QBrush(Qt::gray), Qt::ForegroundRole); + row->setData(QStringLiteral("Not in this ship class's allowed weapons list."), Qt::ToolTipRole); + } + row->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled); + tab.weapons->appendRow(row); + } +} + void ShipWeaponsDialog::updateUI() { updateTabUI(_primary); diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h index c233b4c542e..45bd3234271 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h @@ -48,13 +48,14 @@ class ShipWeaponsDialog : public QDialog { QComboBox* aiCombo = nullptr; QWidget* aiGroup = nullptr; QStandardItemModel* bankModel = nullptr; - WeaponModel* weapons = nullptr; + QStandardItemModel* weapons = nullptr; // Set while the dialog itself is writing into bankModel, so itemChanged handlers can ignore the resulting signals. bool internalUpdate = false; }; void initTab(TabState& tab, Mode mode); void loadBankModel(TabState& tab); + void loadWeaponList(TabState& tab); void updateTabUI(TabState& tab); void updateUI(); diff --git a/qtfred/src/ui/widgets/bankTree.cpp b/qtfred/src/ui/widgets/bankTree.cpp index 3043fc181ca..43b02f14fa9 100644 --- a/qtfred/src/ui/widgets/bankTree.cpp +++ b/qtfred/src/ui/widgets/bankTree.cpp @@ -5,7 +5,6 @@ namespace fso::fred { namespace { -constexpr const char* MIME_WEAPON_ID = "application/weaponid"; constexpr const char* MIME_BANK_TREE_WEAPON = "application/banktreeweapon"; } // namespace diff --git a/qtfred/src/ui/widgets/bankTree.h b/qtfred/src/ui/widgets/bankTree.h index f9fb1374b5d..73963fccbfa 100644 --- a/qtfred/src/ui/widgets/bankTree.h +++ b/qtfred/src/ui/widgets/bankTree.h @@ -6,6 +6,10 @@ #include #include namespace fso::fred { +// MIME type for drags originating from the weapons-list view into the bank tree. +// Payload: a single int (weapon id) written via QDataStream. +constexpr const char* MIME_WEAPON_ID = "application/weaponid"; + // Custom data roles stored on items of the bank tree's QStandardItemModel. // (Qt::UserRole itself is used for the weapon-id on weapon-slot rows.) constexpr int BankItemIsLabelRole = Qt::UserRole + 2; // bool: true on bank-label rows, false on weapon-slot rows From 9fb58a9b14b512caeb0904862c6beb7d54e6942a Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Wed, 13 May 2026 19:16:25 -0500 Subject: [PATCH 15/17] further cleanup --- .../ShipEditor/ShipWeaponsDialogModel.cpp | 136 ++++++++++-------- .../ShipEditor/ShipWeaponsDialogModel.h | 28 ++-- .../dialogs/ShipEditor/ShipWeaponsDialog.cpp | 42 ++---- .../ui/dialogs/ShipEditor/ShipWeaponsDialog.h | 6 +- 4 files changed, 106 insertions(+), 106 deletions(-) diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp index cb8b0418ca5..a88ee5057e1 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp @@ -25,11 +25,11 @@ void reconcileSlot(Bank* bank, int otherWeaponId, int otherAmmoPct) } } -Banks* findBanksByName(const SCP_vector& banks, const SCP_string& name) +Banks* findBanksByName(const SCP_vector>& banks, const SCP_string& name) { - for (Banks* b : banks) { + for (const auto& b : banks) { if (b->getName() == name) { - return b; + return b.get(); } } return nullptr; @@ -64,23 +64,18 @@ void saveSlotsTo(ship_weapon& target, const SCP_vector& bankList, bool is } } // namespace -Banks::Banks(SCP_string _name, int aiIndex, int _ship, int multiedit, int _id, ship_subsys* _subsys) - : m_isMultiEdit(multiedit), name(std::move(_name)), subsys(_subsys), currentAi(aiIndex), ship(_ship), id(_id) +Banks::Banks(SCP_string _name, int aiIndex, int _ship, int _id, ship_subsys* _subsys) + : name(std::move(_name)), subsys(_subsys), currentAi(aiIndex), ship(_ship), id(_id) { } -Banks::~Banks() -{ - for (Bank* b : banks) { - delete b; - } -} +Banks::~Banks() = default; int Banks::getId() const { return id; } -void Banks::add(Bank* bank) +void Banks::add(std::unique_ptr bank) { - banks.push_back(bank); + banks.push_back(std::move(bank)); } SCP_string Banks::getName() const { @@ -100,7 +95,12 @@ bool Banks::empty() const } SCP_vector Banks::getBanks() const { - return banks; + SCP_vector result; + result.reserve(banks.size()); + for (const auto& b : banks) { + result.push_back(b.get()); + } + return result; } int Banks::getAiClass() const { @@ -183,15 +183,7 @@ ShipWeaponsDialogModel::ShipWeaponsDialogModel(QObject* parent, EditorViewport* { initializeData(isMultiEdit); } -ShipWeaponsDialogModel::~ShipWeaponsDialogModel() -{ - for (Banks* b : PrimaryBanks) { - delete b; - } - for (Banks* b : SecondaryBanks) { - delete b; - } -} +ShipWeaponsDialogModel::~ShipWeaponsDialogModel() = default; bool ShipWeaponsDialogModel::selectedShipsShareClass() { int sharedClass = -1; @@ -249,10 +241,10 @@ void ShipWeaponsDialogModel::initializeData(bool isMultiEdit) void ShipWeaponsDialogModel::initPrimary(int inst, bool first) { - int id = 0; - auto pilotBank = new Banks("Pilot", Ships[inst].weapons.ai_class, inst, m_isMultiEdit, id); - id++; if (first) { + int id = 0; + auto pilotBank = std::make_unique("Pilot", Ships[inst].weapons.ai_class, inst, id); + id++; auto pilot = Ships[inst].weapons; const int shipClass = Ships[inst].ship_info_index; const int numPilotBanks = Ship_info[shipClass].num_primary_banks; @@ -264,19 +256,16 @@ void ShipWeaponsDialogModel::initPrimary(int inst, bool first) maxAmmo = get_max_ammo_count_for_primary_bank(shipClass, i, weaponId); ammo = fl2ir(pilot.primary_bank_ammo[i] * maxAmmo / 100.0f); } - pilotBank->add(new Bank(weaponId, i, maxAmmo, ammo, pilotBank)); + pilotBank->add(std::make_unique(weaponId, i, maxAmmo, ammo, pilotBank.get())); } if (!pilotBank->empty()) { - PrimaryBanks.push_back(pilotBank); - } else { - delete pilotBank; + PrimaryBanks.push_back(std::move(pilotBank)); } ship_subsys* ssl = &Ships[inst].subsys_list; - ship_subsys* pss; - for (pss = GET_FIRST(ssl); pss != END_OF_LIST(ssl); pss = GET_NEXT(pss)) { + for (ship_subsys* pss = GET_FIRST(ssl); pss != END_OF_LIST(ssl); pss = GET_NEXT(pss)) { model_subsystem* psub = pss->system_info; if (psub->type == SUBSYSTEM_TURRET) { - auto turretBank = new Banks(psub->subobj_name, pss->weapons.ai_class, inst, m_isMultiEdit,id, pss); + auto turretBank = std::make_unique(psub->subobj_name, pss->weapons.ai_class, inst, id, pss); const int numTurretBanks = pss->weapons.num_primary_banks; for (int i = 0; i < numTurretBanks; i++) { const int weaponId = pss->weapons.primary_bank_weapons[i]; @@ -286,19 +275,16 @@ void ShipWeaponsDialogModel::initPrimary(int inst, bool first) maxAmmo = get_max_ammo_count_for_primary_turret_bank(&pss->weapons, i, weaponId); ammo = fl2ir(pss->weapons.primary_bank_ammo[i] * maxAmmo / 100.0f); } - turretBank->add(new Bank(weaponId, i, maxAmmo, ammo, turretBank)); + turretBank->add(std::make_unique(weaponId, i, maxAmmo, ammo, turretBank.get())); } if (!turretBank->empty()) { - PrimaryBanks.push_back(turretBank); + PrimaryBanks.push_back(std::move(turretBank)); id++; - } else { - delete turretBank; } } } } else { // Subsequent ship: reconcile each slot against the tracking Banks built from the first ship. - delete pilotBank; // unused; we already have one from the first ship if (Banks* tracking = findBanksByName(PrimaryBanks, "Pilot")) { tracking->reconcileAiClass(Ships[inst].weapons.ai_class); const auto bankList = tracking->getBanks(); @@ -328,10 +314,10 @@ void ShipWeaponsDialogModel::initPrimary(int inst, bool first) void ShipWeaponsDialogModel::initSecondary(int inst, bool first) { - int id = 0; - auto pilotBank = new Banks("Pilot", Ships[inst].weapons.ai_class, inst, m_isMultiEdit, id); - id++; if (first) { + int id = 0; + auto pilotBank = std::make_unique("Pilot", Ships[inst].weapons.ai_class, inst, id); + id++; auto pilot = Ships[inst].weapons; const int shipClass = Ships[inst].ship_info_index; const int numPilotBanks = Ship_info[shipClass].num_secondary_banks; @@ -343,19 +329,16 @@ void ShipWeaponsDialogModel::initSecondary(int inst, bool first) maxAmmo = get_max_ammo_count_for_bank(shipClass, i, weaponId); ammo = fl2ir(pilot.secondary_bank_ammo[i] * maxAmmo / 100.0f); } - pilotBank->add(new Bank(weaponId, i, maxAmmo, ammo, pilotBank)); + pilotBank->add(std::make_unique(weaponId, i, maxAmmo, ammo, pilotBank.get())); } if (!pilotBank->empty()) { - SecondaryBanks.push_back(pilotBank); - } else { - delete pilotBank; + SecondaryBanks.push_back(std::move(pilotBank)); } ship_subsys* ssl = &Ships[inst].subsys_list; - ship_subsys* pss; - for (pss = GET_FIRST(ssl); pss != END_OF_LIST(ssl); pss = GET_NEXT(pss)) { + for (ship_subsys* pss = GET_FIRST(ssl); pss != END_OF_LIST(ssl); pss = GET_NEXT(pss)) { model_subsystem* psub = pss->system_info; if (psub->type == SUBSYSTEM_TURRET) { - auto turretBank = new Banks(psub->subobj_name, pss->weapons.ai_class, inst, m_isMultiEdit,id, pss); + auto turretBank = std::make_unique(psub->subobj_name, pss->weapons.ai_class, inst, id, pss); const int numTurretBanks = pss->weapons.num_secondary_banks; for (int i = 0; i < numTurretBanks; i++) { const int weaponId = pss->weapons.secondary_bank_weapons[i]; @@ -365,18 +348,15 @@ void ShipWeaponsDialogModel::initSecondary(int inst, bool first) maxAmmo = get_max_ammo_count_for_turret_bank(&pss->weapons, i, weaponId); ammo = fl2ir(pss->weapons.secondary_bank_ammo[i] * maxAmmo / 100.0f); } - turretBank->add(new Bank(weaponId, i, maxAmmo, ammo, turretBank)); + turretBank->add(std::make_unique(weaponId, i, maxAmmo, ammo, turretBank.get())); } if (!turretBank->empty()) { - SecondaryBanks.push_back(turretBank); + SecondaryBanks.push_back(std::move(turretBank)); id++; - } else { - delete turretBank; } } } } else { - delete pilotBank; if (Banks* tracking = findBanksByName(SecondaryBanks, "Pilot")) { tracking->reconcileAiClass(Ships[inst].weapons.ai_class); const auto bankList = tracking->getBanks(); @@ -420,11 +400,11 @@ void ShipWeaponsDialogModel::saveShip(int inst) target->ai_class = turret->getAiClass(); } }; - for (Banks* turret : PrimaryBanks) { - saveBank(turret, true); + for (const auto& turret : PrimaryBanks) { + saveBank(turret.get(), true); } - for (Banks* turret : SecondaryBanks) { - saveBank(turret, false); + for (const auto& turret : SecondaryBanks) { + saveBank(turret.get(), false); } } bool ShipWeaponsDialogModel::apply() @@ -452,11 +432,21 @@ void ShipWeaponsDialogModel::reject() } SCP_vector ShipWeaponsDialogModel::getPrimaryBanks() const { - return PrimaryBanks; + SCP_vector result; + result.reserve(PrimaryBanks.size()); + for (const auto& b : PrimaryBanks) { + result.push_back(b.get()); + } + return result; } SCP_vector ShipWeaponsDialogModel::getSecondaryBanks() const { - return SecondaryBanks; + SCP_vector result; + result.reserve(SecondaryBanks.size()); + for (const auto& b : SecondaryBanks) { + result.push_back(b.get()); + } + return result; } SCP_vector ShipWeaponsDialogModel::getAvailableWeapons(WeaponListType type) const { @@ -493,6 +483,32 @@ SCP_vector ShipWeaponsDialogModel::getAvailableWeapons(WeaponListTyp } return result; } +SCP_string ShipWeaponsDialogModel::getWeaponName(int weaponId) const +{ + if (weaponId == -2) { + return "CONFLICT"; + } + if (weaponId < 0 || weaponId >= static_cast(Weapon_info.size())) { + return "None"; + } + return Weapon_info[weaponId].name; +} +SCP_vector ShipWeaponsDialogModel::getAiClassNames() const +{ + SCP_vector result; + result.reserve(Num_ai_classes); + for (int i = 0; i < Num_ai_classes; i++) { + result.emplace_back(Ai_class_names[i]); + } + return result; +} +SCP_string ShipWeaponsDialogModel::getAiClassName(int aiClass) const +{ + if (aiClass < 0 || aiClass >= Num_ai_classes) { + return ""; + } + return Ai_class_names[aiClass]; +} int ShipWeaponsDialogModel::getShipClass() const { return Ships[m_ship].ship_info_index; @@ -507,4 +523,4 @@ void ShipWeaponsDialogModel::notifyChanged() modelChanged(); } } // namespace dialogs -} // namespace fso::fred \ No newline at end of file +} // namespace fso::fred diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h index 97af7e38222..cec07704bb7 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h @@ -11,16 +11,20 @@ struct WeaponItem { bool allowed; }; -enum class WeaponListType { Primary, Secondary /*, Tertiary — engine support pending */ }; +enum class WeaponListType { Primary, Secondary /*, Tertiary */ }; +// Bank/Banks are buffered representations of a ship's weapon banks. Mutations through +// their setters (Bank::setWeapon, Bank::setAmmo, Banks::setAiClass) update only this +// in-memory state. They do not mark the model dirty or emit modelChanged(). Callers +// must call ShipWeaponsDialogModel::notifyChanged() after a batch of edits. struct Bank; struct Banks { - Banks(SCP_string name, int aiIndex, int ship, int multiedit, int _id, ship_subsys* subsys = nullptr); + Banks(SCP_string name, int aiIndex, int ship, int _id, ship_subsys* subsys = nullptr); ~Banks(); public: int getId() const; - void add(Bank*); + void add(std::unique_ptr); SCP_string getName() const; int getShip() const; ship_subsys* getSubsys() const; @@ -33,14 +37,13 @@ struct Banks { // differs from the cached value. Does not mark the bank dirty. void reconcileAiClass(int otherAi); bool isAiClassDirty() const; - bool m_isMultiEdit; private: SCP_string name; ship_subsys* subsys; int currentAi; bool aiClassDirty = false; - SCP_vector banks; + SCP_vector> banks; int ship; int id; }; @@ -77,21 +80,24 @@ class ShipWeaponsDialogModel : public AbstractDialogModel { SCP_vector getPrimaryBanks() const; SCP_vector getSecondaryBanks() const; SCP_vector getAvailableWeapons(WeaponListType type) const; + // "None" for -1, "CONFLICT" for -2, otherwise the weapon table name. + SCP_string getWeaponName(int weaponId) const; + SCP_vector getAiClassNames() const; + SCP_string getAiClassName(int aiClass) const; int getShipClass() const; bool isBigShip() const; void notifyChanged(); private: void saveShip(int inst); - void initPrimary(const int inst, bool first); - + void initPrimary(int inst, bool first); void initSecondary(int inst, bool first); - void initializeData(bool multi); + void initializeData(bool isMultiEdit); bool m_isMultiEdit; int m_ship; bool big = true; - SCP_vector PrimaryBanks; - SCP_vector SecondaryBanks; + SCP_vector> PrimaryBanks; + SCP_vector> SecondaryBanks; }; } // namespace dialogs -} // namespace fso::fred \ No newline at end of file +} // namespace fso::fred diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp index 89e1cbb24f8..9b5f2aff666 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp @@ -153,8 +153,9 @@ void ShipWeaponsDialog::initTab(TabState& tab, Mode mode) tab.tree->setItemDelegateForColumn(1, new AmmoSpinBoxDelegate(this)); tab.aiCombo->clear(); - for (int i = 0; i < Num_ai_classes; i++) { - tab.aiCombo->addItem(Ai_class_names[i], QVariant(i)); + const auto aiNames = _model->getAiClassNames(); + for (int i = 0; i < static_cast(aiNames.size()); i++) { + tab.aiCombo->addItem(QString::fromUtf8(aiNames[i].c_str()), QVariant(i)); } connect(tab.tree->selectionModel(), &QItemSelectionModel::selectionChanged, this, @@ -193,7 +194,7 @@ SCP_vector ShipWeaponsDialog::banksForMode(Mode mode) const } } -SCP_string ShipWeaponsDialog::banksLabel(const Banks* banks) +SCP_string ShipWeaponsDialog::banksLabel(const Banks* banks) const { if (banks->getName() == "Pilot") { return banks->getName(); @@ -202,7 +203,7 @@ SCP_string ShipWeaponsDialog::banksLabel(const Banks* banks) if (ai < 0) { return banks->getName() + " (Mixed AI)"; } - return banks->getName() + " ( " + Ai_class_names[ai] + " ) "; + return banks->getName() + " ( " + _model->getAiClassName(ai) + " ) "; } void ShipWeaponsDialog::loadBankModel(TabState& tab) @@ -223,18 +224,8 @@ void ShipWeaponsDialog::loadBankModel(TabState& tab) tab.bankModel->appendRow({nameItem, labelAmmoItem}); for (auto bank : banks->getBanks()) { auto weaponItem = new QStandardItem(); - QString weaponName; - switch (bank->getWeaponId()) { - case -2: - weaponName = "CONFLICT"; - break; - case -1: - weaponName = "None"; - break; - default: - weaponName = Weapon_info[bank->getWeaponId()].name; - } - weaponItem->setData(weaponName, Qt::DisplayRole); + const SCP_string weaponName = _model->getWeaponName(bank->getWeaponId()); + weaponItem->setData(QString::fromUtf8(weaponName.c_str()), Qt::DisplayRole); weaponItem->setData(bank->getWeaponId(), Qt::UserRole); weaponItem->setData(false, BankItemIsLabelRole); weaponItem->setData(bank->getBankId(), BankItemIdRole); @@ -358,8 +349,8 @@ void ShipWeaponsDialog::onTblButtonClicked(TabState& tab) { const int wc = tab.list->currentIndex().data(Qt::UserRole).toInt(); if (wc >= 0) { - auto dialog = new TableViewerDialog(this, _viewport, "Weapon TBL Data", "weapons.tbl", "*-wep.tbm", - Weapon_info[wc].name); + const SCP_string name = _model->getWeaponName(wc); + auto dialog = new TableViewerDialog(this, _viewport, "Weapon TBL Data", "weapons.tbl", "*-wep.tbm", name.c_str()); dialog->show(); } } @@ -504,19 +495,8 @@ void ShipWeaponsDialog::refreshBankItem(TabState& tab, const QModelIndex& idx) tab.internalUpdate = false; return; } - QString name; - switch (bank->getWeaponId()) { - case -2: - name = "CONFLICT"; - break; - case -1: - name = "None"; - break; - default: - name = Weapon_info[bank->getWeaponId()].name; - break; - } - tab.bankModel->setData(col0, name, Qt::DisplayRole); + const SCP_string name = _model->getWeaponName(bank->getWeaponId()); + tab.bankModel->setData(col0, QString::fromUtf8(name.c_str()), Qt::DisplayRole); tab.bankModel->setData(col0, bank->getWeaponId(), Qt::UserRole); tab.bankModel->setData(col0, bank->getMaxAmmo(), BankItemMaxAmmoRole); if (QStandardItem* weaponItem = tab.bankModel->itemFromIndex(col0)) { diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h index 45bd3234271..b8df2089015 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h @@ -1,5 +1,4 @@ -#ifndef SHIPWEAPONSDIALOG_H -#define SHIPWEAPONSDIALOG_H +#pragma once #include "ui/widgets/bankTree.h" @@ -73,7 +72,7 @@ class ShipWeaponsDialog : public QDialog { Banks* banksForIndex(const TabState& tab, const QModelIndex& idx) const; void refreshBankItem(TabState& tab, const QModelIndex& idx); SCP_vector banksForMode(Mode mode) const; - static SCP_string banksLabel(const Banks* banks); + SCP_string banksLabel(const Banks* banks) const; std::unique_ptr ui; std::unique_ptr _model; @@ -83,4 +82,3 @@ class ShipWeaponsDialog : public QDialog { TabState _secondary; }; } // namespace fso::fred::dialogs -#endif From 352bfe8d46705d2c1c87cc1ddf0bff3f90634dec Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Wed, 13 May 2026 19:50:18 -0500 Subject: [PATCH 16/17] static --- .../mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp | 6 +++--- .../src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp index a88ee5057e1..43dfc52cec3 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp @@ -483,7 +483,7 @@ SCP_vector ShipWeaponsDialogModel::getAvailableWeapons(WeaponListTyp } return result; } -SCP_string ShipWeaponsDialogModel::getWeaponName(int weaponId) const +SCP_string ShipWeaponsDialogModel::getWeaponName(int weaponId) { if (weaponId == -2) { return "CONFLICT"; @@ -493,7 +493,7 @@ SCP_string ShipWeaponsDialogModel::getWeaponName(int weaponId) const } return Weapon_info[weaponId].name; } -SCP_vector ShipWeaponsDialogModel::getAiClassNames() const +SCP_vector ShipWeaponsDialogModel::getAiClassNames() { SCP_vector result; result.reserve(Num_ai_classes); @@ -502,7 +502,7 @@ SCP_vector ShipWeaponsDialogModel::getAiClassNames() const } return result; } -SCP_string ShipWeaponsDialogModel::getAiClassName(int aiClass) const +SCP_string ShipWeaponsDialogModel::getAiClassName(int aiClass) { if (aiClass < 0 || aiClass >= Num_ai_classes) { return ""; diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h index cec07704bb7..0add1b6f608 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h @@ -81,9 +81,9 @@ class ShipWeaponsDialogModel : public AbstractDialogModel { SCP_vector getSecondaryBanks() const; SCP_vector getAvailableWeapons(WeaponListType type) const; // "None" for -1, "CONFLICT" for -2, otherwise the weapon table name. - SCP_string getWeaponName(int weaponId) const; - SCP_vector getAiClassNames() const; - SCP_string getAiClassName(int aiClass) const; + static SCP_string getWeaponName(int weaponId); + static SCP_vector getAiClassNames(); + static SCP_string getAiClassName(int aiClass); int getShipClass() const; bool isBigShip() const; void notifyChanged(); From 529b20cf35f54d94991dd33722b295c40b4e1e1d Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Fri, 15 May 2026 10:09:43 -0500 Subject: [PATCH 17/17] keep primary and secondary banks aligned --- .../dialogs/ShipEditor/ShipWeaponsDialog.cpp | 33 +++++++++++++++++++ .../ui/dialogs/ShipEditor/ShipWeaponsDialog.h | 1 + 2 files changed, 34 insertions(+) diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp index 9b5f2aff666..cb05100761c 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp @@ -324,6 +324,7 @@ void ShipWeaponsDialog::onAiButtonClicked(TabState& tab) return; // No AI picked in the combo (still blank from a mixed selection). } const int newAi = tab.aiCombo->itemData(comboIdx).toInt(); + TabState& otherTab = (&tab == &_primary) ? _secondary : _primary; bool anyChanged = false; for (const QModelIndex& index : tab.tree->selectionModel()->selectedIndexes()) { if (index.column() != 0) { @@ -338,9 +339,26 @@ void ShipWeaponsDialog::onAiButtonClicked(TabState& tab) } banks->setAiClass(newAi); refreshBankItem(tab, index); + // A turret with both primary and secondary banks has a Banks in each tab pointing at the + // same ship_subsys::weapons.ai_class. Keep them in lockstep so saveShip() can't have one + // tab silently overwrite the other, and so the other tab's label stays accurate. + for (Banks* sibling : banksForMode(otherTab.mode)) { + if (sibling == nullptr || sibling->getName() != banks->getName()) { + continue; + } + if (sibling->getAiClass() != newAi) { + sibling->setAiClass(newAi); + } + const QModelIndex siblingIdx = indexForBanks(otherTab, sibling->getId()); + if (siblingIdx.isValid()) { + refreshBankItem(otherTab, siblingIdx); + } + break; + } anyChanged = true; } if (anyChanged) { + updateTabUI(otherTab); _model->notifyChanged(); } } @@ -641,4 +659,19 @@ QModelIndex ShipWeaponsDialog::indexForBank(const TabState& tab, int banksId, in return {}; } +QModelIndex ShipWeaponsDialog::indexForBanks(const TabState& tab, int banksId) +{ + if (tab.bankModel == nullptr) { + return {}; + } + const int topRows = tab.bankModel->rowCount(); + for (int i = 0; i < topRows; i++) { + const QModelIndex parent = tab.bankModel->index(i, 0); + if (parent.data(BankItemIdRole).toInt() == banksId) { + return parent; + } + } + return {}; +} + } // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h index b8df2089015..75b4e221ac6 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h @@ -67,6 +67,7 @@ class ShipWeaponsDialog : public QDialog { int sourceBankId, bool isCopy); static QModelIndex indexForBank(const TabState& tab, int banksId, int bankId); + static QModelIndex indexForBanks(const TabState& tab, int banksId); Bank* bankForIndex(const TabState& tab, const QModelIndex& idx) const; Banks* banksForIndex(const TabState& tab, const QModelIndex& idx) const;