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:

+ +

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

+

Each tab contains:

+

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:

+ +

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:

+ +

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:

+ +

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.

diff --git a/qtfred/source_groups.cmake b/qtfred/source_groups.cmake index 6b3704bfa26..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 @@ -330,8 +328,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 cfaff25571b..43dfc52cec3 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp @@ -1,22 +1,82 @@ #include "ShipWeaponsDialogModel.h" + +#include + 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) +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) { - aiClass = aiIndex; + 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); + } } -void Banks::add(Bank* bank) + +Banks* findBanksByName(const SCP_vector>& banks, const SCP_string& name) { - banks.push_back(bank); + for (const auto& b : banks) { + if (b->getName() == name) { + return b.get(); + } + } + return nullptr; } -Bank* Banks::getByBankId(const int id) + +ship_subsys* findTurretByName(int inst, const SCP_string& name) { - for (auto bank : banks) { - if (id == bank->getWeaponId()) - return bank; + 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 + +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() = default; +int Banks::getId() const +{ + return id; +} +void Banks::add(std::unique_ptr bank) +{ + banks.push_back(std::move(bank)); +} SCP_string Banks::getName() const { return name; @@ -35,42 +95,34 @@ 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 { - if (name == "Pilot") { - return Ships[ship].weapons.ai_class; - } else { - return subsys->weapons.ai_class; - } + return currentAi; } void Banks::setAiClass(int 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; - } - } - ptr = GET_NEXT(ptr); - } - } else { - if (name == "Pilot") { - Ships[ship].weapons.ai_class = newClass; - } else { - subsys->weapons.ai_class = newClass; - } + currentAi = newClass; + aiClassDirty = true; +} +void Banks::reconcileAiClass(int otherAi) +{ + if (currentAi == -1) { + return; + } + if (currentAi != otherAi) { + currentAi = -1; } } -int Banks::getInitalAI() const +bool Banks::isAiClassDirty() const { - return initalAI; + return aiClassDirty; } Bank::Bank(const int _weaponId, const int _bankId, const int _ammoMax, const int _ammo, Banks* _parent) { @@ -99,19 +151,27 @@ 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; + } + 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); } } + ammo = ammoMax; } void Bank::setAmmo(const int newAmmo) { @@ -123,6 +183,23 @@ ShipWeaponsDialogModel::ShipWeaponsDialogModel(QObject* parent, EditorViewport* { initializeData(isMultiEdit); } +ShipWeaponsDialogModel::~ShipWeaponsDialogModel() = default; +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; @@ -130,8 +207,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, // 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, // NOLINT(readability-simplify-boolean-expr) + "ShipWeaponsDialog resolved to invalid ship index %d", m_ship); if (m_isMultiEdit) { object* ptr = GET_FIRST(&obj_used_list); @@ -143,7 +226,6 @@ void ShipWeaponsDialogModel::initializeData(bool isMultiEdit) big = false; initPrimary(inst, first); initSecondary(inst, first); - // initTertiary(inst, first); first = false; } ptr = GET_NEXT(ptr); @@ -159,66 +241,72 @@ 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); if (first) { + int id = 0; + auto pilotBank = std::make_unique("Pilot", Ships[inst].weapons.ai_class, inst, id); + id++; 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(std::make_unique(weaponId, i, maxAmmo, ammo, pilotBank.get())); + } + if (!pilotBank->empty()) { + PrimaryBanks.push_back(std::move(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)) { + 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, 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)); + 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]; + 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(std::make_unique(weaponId, i, maxAmmo, ammo, turretBank.get())); } if (!turretBank->empty()) { - PrimaryBanks.push_back(turretBank); - } else { - delete turretBank; + PrimaryBanks.push_back(std::move(turretBank)); + id++; } } } } 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. + 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], + 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; + } + 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], + pss->weapons.primary_bank_ammo[i]); } } } @@ -226,111 +314,97 @@ 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); if (first) { + int id = 0; + auto pilotBank = std::make_unique("Pilot", Ships[inst].weapons.ai_class, inst, id); + id++; 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(std::make_unique(weaponId, i, maxAmmo, ammo, pilotBank.get())); + } + if (!pilotBank->empty()) { + SecondaryBanks.push_back(std::move(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)) { + 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, 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)); + 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]; + 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(std::make_unique(weaponId, i, maxAmmo, ammo, turretBank.get())); } if (!turretBank->empty()) { - SecondaryBanks.push_back(turretBank); - } else { - delete turretBank; + SecondaryBanks.push_back(std::move(turretBank)); + id++; } } } } 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); + 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], + 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; + } + 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], + 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) { + ship_weapon* target = nullptr; + if (turret->getName() == "Pilot") { + target = &Ships[inst].weapons; + } else if (ship_subsys* pss = findTurretByName(inst, turret->getName())) { + target = &pss->weapons; } - } - 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; - } - } + if (target == nullptr) { + return; } + saveSlotsTo(*target, turret->getBanks(), isPrimary); + if (turret->isAiClassDirty()) { + target->ai_class = turret->getAiClass(); + } + }; + for (const auto& turret : PrimaryBanks) { + saveBank(turret.get(), true); + } + for (const auto& turret : SecondaryBanks) { + saveBank(turret.get(), false); } } bool ShipWeaponsDialogModel::apply() @@ -352,24 +426,101 @@ bool ShipWeaponsDialogModel::apply() } void ShipWeaponsDialogModel::reject() { - for (auto Turret : PrimaryBanks) { - Turret->setAiClass(Turret->getInitalAI()); - } - for (auto Turret : SecondaryBanks) { - Turret->setAiClass(Turret->getInitalAI()); - } + // 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 { - 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; } -/* void ShipWeaponsDialogModel::initTertiary(int inst, bool first) { +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; +} +SCP_string ShipWeaponsDialogModel::getWeaponName(int weaponId) +{ + 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() +{ + 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) +{ + if (aiClass < 0 || aiClass >= Num_ai_classes) { + return ""; + } + return Ai_class_names[aiClass]; +} +int ShipWeaponsDialogModel::getShipClass() const +{ + return Ships[m_ship].ship_info_index; +} +bool ShipWeaponsDialogModel::isBigShip() const +{ + return big; +} +void ShipWeaponsDialogModel::notifyChanged() +{ + set_modified(); + 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 d63908e1c6b..0add1b6f608 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h @@ -1,88 +1,103 @@ -#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 -} // namespace fso::fred \ No newline at end of file +#pragma once + +#include "../AbstractDialogModel.h" + +#include + +namespace fso::fred { +struct WeaponItem { + int id; + SCP_string name; + bool allowed; +}; + +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 _id, ship_subsys* subsys = nullptr); + ~Banks(); + + public: + int getId() const; + void add(std::unique_ptr); + SCP_string getName() const; + int getShip() const; + ship_subsys* getSubsys() const; + bool empty() const; + SCP_vector getBanks() const; + // 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; + + private: + SCP_string name; + ship_subsys* subsys; + int currentAi; + bool aiClassDirty = false; + 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 { +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(); + + bool apply() override; + void reject() override; + 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. + static SCP_string getWeaponName(int weaponId); + static SCP_vector getAiClassNames(); + static SCP_string getAiClassName(int aiClass); + int getShipClass() const; + bool isBigShip() const; + void notifyChanged(); + + private: + void saveShip(int inst); + void initPrimary(int inst, bool first); + void initSecondary(int inst, bool first); + void initializeData(bool isMultiEdit); + bool m_isMultiEdit; + int m_ship; + bool big = true; + SCP_vector> PrimaryBanks; + SCP_vector> SecondaryBanks; +}; +} // namespace dialogs +} // namespace fso::fred diff --git a/qtfred/src/ui/dialogs/ShipEditor/BankModel.cpp b/qtfred/src/ui/dialogs/ShipEditor/BankModel.cpp deleted file mode 100644 index e5d6f148004..00000000000 --- a/qtfred/src/ui/dialogs/ShipEditor/BankModel.cpp +++ /dev/null @@ -1,404 +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; -} -} // 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 a0b42d475e4..00000000000 --- a/qtfred/src/ui/dialogs/ShipEditor/BankModel.h +++ /dev/null @@ -1,94 +0,0 @@ -#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); -}; -} // 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..cb05100761c 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp @@ -1,92 +1,279 @@ #include "ShipWeaponsDialog.h" -#include #include "ui_ShipWeaponsDialog.h" #include +#include #include #include +#include #include -#include +#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); +} + +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; + + 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(BankItemMaxAmmoRole).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) + : 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); - 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); - 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 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); + 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() +ShipWeaponsDialog::~ShipWeaponsDialog() = default; + +void ShipWeaponsDialog::initTab(TabState& tab, Mode mode) { - delete bankModel; - delete weapons; + 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 WeaponListModel(this); + loadBankModel(tab); + loadWeaponList(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.tree->setItemDelegateForColumn(1, new AmmoSpinBoxDelegate(this)); + + tab.aiCombo->clear(); + 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, + [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.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; + 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: + // Tertiary banks are unsupported by the engine; placeholder mode returns no banks. + return {}; + } +} + +SCP_string ShipWeaponsDialog::banksLabel(const Banks* banks) const +{ + if (banks->getName() == "Pilot") { + return banks->getName(); + } + const int ai = banks->getAiClass(); + if (ai < 0) { + return banks->getName() + " (Mixed AI)"; + } + return banks->getName() + " ( " + _model->getAiClassName(ai) + " ) "; +} + +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, BankItemIsLabelRole); + nameItem->setData(banks->getId(), BankItemIdRole); + 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(); + 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); + 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) { + 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) { + 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); + ammoItem->setData(QVariant(), AmmoValueRole); + } + ammoItem->setFlags(ammoFlags); + 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) @@ -94,132 +281,397 @@ void ShipWeaponsDialog::closeEvent(QCloseEvent* event) reject(); event->ignore(); } -void ShipWeaponsDialog::on_setAllButton_clicked() + +void ShipWeaponsDialog::on_okAndCancelButtons_accepted() +{ + accept(); +} + +void ShipWeaponsDialog::on_okAndCancelButtons_rejected() +{ + reject(); +} + +void ShipWeaponsDialog::onSetAllClicked(TabState& tab) { - for (auto& index : ui->treeBanks->selectionModel()->selectedIndexes()) { - bankModel->setWeapon(index, ui->listWeapons->currentIndex().data(Qt::UserRole).toInt()); + 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::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(); + TabState& otherTab = (&tab == &_primary) ? _secondary : _primary; + bool anyChanged = false; + for (const QModelIndex& index : tab.tree->selectionModel()->selectedIndexes()) { + if (index.column() != 0) { + continue; + } + Banks* banks = banksForIndex(tab, index); + if (banks == nullptr || banks->getName() == "Pilot") { + continue; + } + if (banks->getAiClass() == newAi) { + continue; + } + 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(); } } -void ShipWeaponsDialog::on_tblButton_clicked() + +void ShipWeaponsDialog::onTblButtonClicked(TabState& tab) { - int wc = ui->listWeapons->currentIndex().data(Qt::UserRole).toInt(); + 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(); } } -void ShipWeaponsDialog::on_radioPrimary_toggled(bool checked) + +void ShipWeaponsDialog::loadWeaponList(TabState& tab) { - modeChanged(checked, 0); + 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::on_radioSecondary_toggled(bool checked) + +void ShipWeaponsDialog::updateUI() { - modeChanged(checked, 1); + updateTabUI(_primary); + updateTabUI(_secondary); +} + +void ShipWeaponsDialog::updateTabUI(TabState& tab) +{ + 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. 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) { + for (const QModelIndex& idx : tab.tree->selectionModel()->selectedIndexes()) { + if (idx.column() != 0) { + continue; + } + Banks* banks = banksForIndex(tab, idx); + if (banks == nullptr) { + continue; + } + 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; + } + } + } + tab.aiGroup->setEnabled(aiEditable); + if (aiEditable && combinedInitialized && !combinedMixed) { + 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; + tab.tblButton->setEnabled(hasWeaponSelection); } -void ShipWeaponsDialog::on_radioTertiary_toggled(bool checked) + +Banks* ShipWeaponsDialog::banksForIndex(const TabState& tab, const QModelIndex& idx) const { - modeChanged(checked, 2); + if (!idx.isValid()) { + return nullptr; + } + if (!idx.data(BankItemIsLabelRole).toBool()) { + return nullptr; + } + const int banksId = idx.data(BankItemIdRole).toInt(); + for (Banks* banks : banksForMode(tab.mode)) { + if (banks->getId() == banksId) { + return banks; + } + } + return nullptr; } -void ShipWeaponsDialog::on_aiCombo_currentIndexChanged(int index) + +Bank* ShipWeaponsDialog::bankForIndex(const TabState& tab, const QModelIndex& idx) const { - aiClassChanged(index); + if (!idx.isValid()) { + return nullptr; + } + if (idx.data(BankItemIsLabelRole).toBool()) { + return nullptr; + } + const QModelIndex parent = idx.parent(); + if (!parent.isValid()) { + return nullptr; + } + Banks* parentBanks = banksForIndex(tab, parent); + if (parentBanks == nullptr) { + return nullptr; + } + const int bankId = idx.data(BankItemIdRole).toInt(); + for (Bank* bank : parentBanks->getBanks()) { + if (bank->getBankId() == bankId) { + return bank; + } + } + return nullptr; } -void ShipWeaponsDialog::modeChanged(const bool enabled, const int mode) + +void ShipWeaponsDialog::refreshBankItem(TabState& tab, const QModelIndex& idx) { - if (enabled) { - if (mode == 0) { - 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); - dialogMode = 1; - delete weapons; - weapons = new WeaponModel(1); - ui->listWeapons->setModel(weapons); - } else if (mode == 2) { - // bankModel = new BankTreeModel(_model->getTertiaryBanks(), this); - dialogMode = 2; + if (!idx.isValid() || tab.bankModel == nullptr) { + return; + } + const QModelIndex col0 = idx.sibling(idx.row(), 0); + tab.internalUpdate = true; + 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.internalUpdate = false; + return; + } + Bank* bank = bankForIndex(tab, col0); + if (bank == nullptr) { + tab.internalUpdate = false; + return; + } + 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)) { + Qt::ItemFlags f = weaponItem->flags(); + if (bank->getWeaponId() >= 0) { + f |= Qt::ItemIsDragEnabled; } 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); - bankModel = new BankTreeModel(_model->getPrimaryBanks(), this); - dialogMode = 0; - } - // 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(); + f &= ~Qt::ItemIsDragEnabled; + } + weaponItem->setFlags(f); } - updateUI(); + + 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) { + if (bank->getAmmo() == -2) { + 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); + ammoItem->setData(QVariant(), AmmoValueRole); + } + ammoItem->setFlags(ammoFlags); + } + } + tab.internalUpdate = false; } -void ShipWeaponsDialog::updateUI() + +void ShipWeaponsDialog::onBankItemChanged(TabState& tab, QStandardItem* item) { - 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() == 0) { - ui->setAllButton->setEnabled(true); - } else { - ui->setAllButton->setEnabled(false); + if (item == nullptr || tab.internalUpdate) { + return; } - // Change AI Button - if (ui->treeBanks->getTypeSelected() == 1) { - ui->aiButton->setEnabled(true); - } else { - ui->aiButton->setEnabled(false); + if (item->column() != 1) { + return; } - // 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 col0 = item->index().sibling(item->row(), 0); + if (!col0.isValid() || col0.data(BankItemIsLabelRole).toBool()) { + return; } - 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); + Bank* bank = bankForIndex(tab, col0); + if (bank == nullptr) { + return; + } + bool ok = false; + int requested = item->data(AmmoValueRole).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(formatAmmoDisplay(clamped, bank->getMaxAmmo()), Qt::DisplayRole); + item->setData(clamped, AmmoValueRole); + tab.internalUpdate = false; } -void ShipWeaponsDialog::aiClassChanged(const int index) +void ShipWeaponsDialog::onWeaponDroppedFromList(TabState& tab, const QModelIndex& target, int weaponId) { - m_currentAI = ui->aiCombo->itemData(index).toInt(); + Bank* bank = bankForIndex(tab, target); + if (bank == nullptr) { + return; + } + if (bank->getWeaponId() == weaponId) { + return; + } + bank->setWeapon(weaponId); + refreshBankItem(tab, target); + _model->notifyChanged(); } -void ShipWeaponsDialog::on_aiButton_clicked() +void ShipWeaponsDialog::onWeaponMoved(TabState& tab, const QModelIndex& target, int weaponId, + int sourceBanksId, int sourceBankId, bool isCopy) { - for (auto& index : ui->treeBanks->selectionModel()->selectedIndexes()) { - bankModel->setData(index, m_currentAI); + 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(); } -void ShipWeaponsDialog::on_buttonClose_clicked() +QModelIndex ShipWeaponsDialog::indexForBank(const TabState& tab, int banksId, int bankId) { - accept(); + 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) { + 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(BankItemIdRole).toInt() == bankId) { + return child; + } + } + } + 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 \ 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 7b5efb83578..75b4e221ac6 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h @@ -1,33 +1,26 @@ -#ifndef SHIPWEAPONSDIALOG_H -#define SHIPWEAPONSDIALOG_H +#pragma once -#include "ui/dialogs/ShipEditor/BankModel.h" -#include "ui/widgets/weaponList.h" +#include "ui/widgets/bankTree.h" #include -#include +#include #include +#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; @@ -38,31 +31,55 @@ class ShipWeaponsDialog : public QDialog { 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); + void on_okAndCancelButtons_accepted(); + void on_okAndCancelButtons_rejected(); private: // NOLINT(readability-redundant-access-specifiers) + 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; + 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(); + + void onSetAllClicked(TabState& tab); + void onAiButtonClicked(TabState& tab); + void onTblButtonClicked(TabState& tab); + 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); + + 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; + 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; - /** - * @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); + + TabState _primary; + TabState _secondary; }; } // 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..43b02f14fa9 100644 --- a/qtfred/src/ui/widgets/bankTree.cpp +++ b/qtfred/src/ui/widgets/bankTree.cpp @@ -1,103 +1,166 @@ #include "bankTree.h" + +#include + namespace fso::fred { + +namespace { +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(BankItemIsLabelRole).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(BankItemIsLabelRole).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(BankItemIsLabelRole).toBool()) { + continue; + } + source = idx; + break; + } + if (!source.isValid()) { return; } - if (dynamic_cast(model())->checktype(index) == 0) { - 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(BankItemIdRole).toInt(); + const int sourceBankId = source.data(BankItemIdRole).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) { - QItemSelection newlySelected; - QItemSelection select; - QItemSelection deselect(deselected); - if (selected.empty()) { - QTreeView::selectionChanged(selected, deselected); - if (selectionModel()->selectedIndexes().empty()) { - typeSelected = -1; + QTreeView::selectionChanged(selected, deselected); + + if (m_autoFiltering) { + return; + } + + const auto newlySelected = selected.indexes(); + if (newlySelected.isEmpty()) { + return; + } + + QModelIndex pivot; + for (const QModelIndex& idx : newlySelected) { + if (idx.column() == 0 && idx.isValid()) { + pivot = idx; + break; } + } + if (!pivot.isValid()) { return; } - for (auto& sidx : selected.indexes()) { - bool match = false; - for (auto& didx : deselected.indexes()) { - if (sidx == didx) { - match = true; - break; - } + const bool pivotIsBank = pivot.data(BankItemIsLabelRole).toBool(); + + QItemSelectionModel* sm = selectionModel(); + QItemSelection toDeselect; + for (const QModelIndex& idx : sm->selectedIndexes()) { + if (idx.column() != 0) { + continue; } - if (!match) { - QItemSelectionRange selection(sidx); - newlySelected.append(selection); + if (idx.data(BankItemIsLabelRole).toBool() != pivotIsBank) { + toDeselect.select(idx, idx); } } - 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); - } - } - selectionModel()->clear(); - typeSelected = -1; - } else { - for (auto& sidx : newlySelected.indexes()) { - if (dynamic_cast(model())->checktype(sidx) == typeSelected) { - QItemSelectionRange selection(sidx); - select.append(selection); - } - } - } - } + + if (!toDeselect.isEmpty()) { + m_autoFiltering = true; + sm->select(toDeselect, QItemSelectionModel::Deselect | QItemSelectionModel::Rows); + m_autoFiltering = false; } - QTreeView::selectionChanged(select, deselect); } -int bankTree::getTypeSelected() const + +bankTree::SelectionType bankTree::getSelectionType() const { - return typeSelected; + 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(BankItemIsLabelRole).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 81268d173cf..73963fccbfa 100644 --- a/qtfred/src/ui/widgets/bankTree.h +++ b/qtfred/src/ui/widgets/bankTree.h @@ -1,7 +1,4 @@ #pragma once -#include "ui/dialogs/ShipEditor/BankModel.h" - -#include #include #include @@ -9,17 +6,36 @@ #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 +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: + enum class SelectionType { None, Bank, Weapon }; + bankTree(QWidget*); void selectionChanged(const QItemSelection& selected, const QItemSelection& deselected) override; - int getTypeSelected() const; + 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; - int typeSelected = -1; + void startDrag(Qt::DropActions supportedActions) override; + + private: + bool m_autoFiltering = false; }; -} // namespace fso::fred \ No newline at end of file +} // 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..dc72c7e1cc0 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::DragDrop + + + 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::DragDrop + + + 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