/** * @RDE_HEADER@ * HMI Plugin for ParticipantManagement - Participant Edit Model * $Id$ * $HeadURL$ * $Author$ * Created by Patric Karwat, SLSE, 2016-10-17 */ #include "participant_edit_model.h" #include #include #include #include #include #include "participant_firing_unit_validator.h" #include #include #include #include #include namespace dds { namespace core { static inline uint qHash(const ParticipantManagement::TypeClass &tc) { return ::qHash(tc.underlying()); } } } typedef QVector > QModelIndexPairList; namespace ParticipantManagement { static QVariant participantToQVariant(const ParticipantData &pd) { QByteArray ba; QDataStream ds(&ba, QIODevice::WriteOnly); ds << pd.id() << pd.partyId() << pd.hierarchyNumber() << (pd.equipmentNumber().is_set() ? pd.equipmentNumber().get() : -1) << pd.typeId() << pd.externalId() << QString::fromStdString(pd.symbol()) << QString::fromStdString(pd.name()) << QString::fromStdString(pd.identification()) << QString::fromStdString(pd.designator()) << QString::fromStdString(pd.tacticalIdentifier()) << QString::fromStdString(pd.info()) << pd.assignState().underlying() << pd.isSimulated() << pd.isDeleted() << pd.modId() << pd.superiorModId(); return ba; } static ParticipantData qVariantToParticipant(const QVariant &data) { ParticipantData pd; const QByteArray ba = data.toByteArray(); QDataStream ds(ba); int id, partyId, hierarchyNumber, equipmentNumber, typeId, assignState, modId, superiorModId; QString symbol, name, identification, designator, tacticalIdentifier, info; bool isSimulated, isDeleted; qint64 externalId; ds >> id >> partyId >> hierarchyNumber >> equipmentNumber >> typeId >> externalId >> symbol >> name >> identification >> designator >> tacticalIdentifier >> info >> assignState >> isSimulated >> isDeleted >> modId >> superiorModId; pd.id(id); pd.partyId(partyId); pd.hierarchyNumber(hierarchyNumber); if (equipmentNumber >= 0) pd.equipmentNumber(equipmentNumber); pd.typeId(typeId); pd.externalId(externalId); pd.symbol(symbol.toStdString()); pd.name(name.toStdString()); pd.identification(identification.toStdString()); pd.designator(designator.toStdString()); pd.tacticalIdentifier(tacticalIdentifier.toStdString()); pd.info(info.toStdString()); pd.assignState(static_cast(assignState)); pd.isSimulated(isSimulated); pd.isDeleted(isDeleted); pd.modId(modId); pd.superiorModId(superiorModId); return pd; } static bool isChecked(bool bIsInSet, Qt::CheckState cs) { if (cs == Qt::Checked) return bIsInSet; // Qt::Unchecked return !bIsInSet; } bool compareStrings(const std::string &s1, const std::string &s2, const ParticipantEditModel::CheckedTreeItem *p1, const ParticipantEditModel::CheckedTreeItem *p2) { const int iRet = strcmp(s1.c_str(), s2.c_str()); if (iRet == 0) return p1->participant.hierarchyNumber() < p2->participant.hierarchyNumber(); return iRet < 0; } //============================================================================================================================= // PARTICIPANTEDITMODEL //============================================================================================================================= ParticipantEditModel::ParticipantEditModel(QObject *parent, const std::shared_ptr &ppmDtc, const std::shared_ptr &asbIfc) : ParticipantIconModelBase( ppmDtc, asbIfc, { tr("Hierarchy/Designator"), tr("Tact. Identifier"), tr("Class"), tr("Type"), tr("Equip. No."), tr("Participant Id"), tr("Info"), tr("Sign") }, new CheckedTreeItem(QVariant()), parent, getLogger("ParticipantEditModel") ) , m_partyId(-1) , m_bRowsAreCheckable(false) , m_bUnitsSelectable(true) , m_missingSuperiorIdTimer(new QTimer(this)) , m_firingUnitValidator(new FiringUnitValidator(ppmDtc, this)) { connect(this, &ParticipantEditModel::signalDataDropped, this, &ParticipantEditModel::internalDataChanged); connect(m_dtc.get(), &ParticipantManagement::Interface::Dtc::participantChanged, this, &ParticipantEditModel::onParticipantChanged); setMimeType(QStringLiteral("ParticipantEditModel_%1").arg(qlonglong(this))); m_missingSuperiorIdTimer->setInterval(500); m_missingSuperiorIdTimer->setSingleShot(true); connect(m_missingSuperiorIdTimer, &QTimer::timeout, this, &ParticipantEditModel::processMissingSuperiorId); } //----------------------------------------------------------------------------------------------------------------------------- // PUBLIC MEMBER FUNCTIONS //----------------------------------------------------------------------------------------------------------------------------- Qt::ItemFlags ParticipantEditModel::flags(const QModelIndex &index) const { auto ret = ParticipantIconModelBase::flags(index); if (m_bRowsAreCheckable && index.column() == 0) ret |= Qt::ItemIsUserCheckable; // are units not selectable? if (!m_bUnitsSelectable && isUnitType(privateData(index)->participant.typeId())) ret &= ~Qt::ItemIsSelectable; // can not drag the whole tree, also we allow only dragging the first column if (index.column() != 0 || !index.parent().isValid()) ret &= ~Qt::ItemIsDragEnabled; // drop also only on first column if (index.column() != 0) ret &= ~Qt::ItemIsDropEnabled; return ret; } QVariant ParticipantEditModel::headerData(int section, Qt::Orientation orientation, int role) const { if (role == LtsWidgets::LtsColumnFilterProxyModel::ComboboxFilterRole && orientation == Qt::Horizontal && (section == ColTypeClass || section == ColType)) { LtsWidgets::ComboboxFilterContent cfc; cfc.data.reserve(m_usedTypeIds.size()); LtsWidgets::ComboboxFilterData cfd; if (section == ColType) { for (const int typeId : m_usedTypeIds) { cfd.data = typeId; cfd.text = ParticipantManagement::trTypeName(m_types.value(typeId).name()); cfc.data.push_back(cfd); } } else if (section == ColTypeClass) { for (const auto &typeClassId : m_usedTypeClassIds) { cfd.data = typeClassId.underlying(); cfd.text = ParticipantManagement::toString(typeClassId); cfc.data.push_back(cfd); } } cfc.sortItems = true; return cfc.toVariant(); } return ParticipantIconModelBase::headerData(section, orientation, role); } QVariant ParticipantEditModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) return ParticipantIconModelBase::data(index, role); if (role != Qt::DisplayRole && role != Qt::CheckStateRole && role != Qt::ToolTipRole && role != LtsWidgets::LtsColumnFilterProxyModel::ComboboxFilterRole) return ParticipantIconModelBase::data(index, role); const CheckedTreeItem *item = privateData(index); const ParticipantManagement::ParticipantData &participantData = item->participant; if (role == Qt::ToolTipRole) // special treatment of tooltip to avoid unnecessary memory allocation { if (index.column() != ColDesignator) return QVariant(); QString toolTip = QStringLiteral(""); QModelIndex idx = ParticipantEditModel::index(index.row(), 0, index.parent()); QIcon icon = privateData(idx)->icon(); if (!icon.isNull()) { const QPixmap &img = icon.pixmap(100, 100); QByteArray data; QBuffer buffer(&data); img.save(&buffer, "PNG", 100); toolTip += QStringLiteral("
").arg(QString::fromLatin1(data.toBase64())); } // if for (int i = 0; i < columnCount(); ++i) toolTip.append(QStringLiteral("
%1: %2").arg( headerData(i, Qt::Horizontal, Qt::DisplayRole).toString(), data(ParticipantEditModel::index(index.row(), i, index.parent()), Qt::DisplayRole).toString()) ); return toolTip; } // if switch (role) { case Qt::DisplayRole: switch (index.column()) { case ColDesignator: return QString::fromStdString(participantData.designator()); case ColIdentifier: return QString::fromStdString(participantData.tacticalIdentifier()); case ColTypeClass: return typeClassStringFromTypeId(participantData.typeId()); case ColType: return ParticipantManagement::trTypeName(m_types.value(participantData.typeId()).name()); case ColEquipmentNumber: if (!participantData.equipmentNumber().is_set()) { if (participantData.assignState() == ParticipantAssignState::HandoutExcon) return 0; return QVariant(); } if (participantData.assignState() == ParticipantAssignState::ExconWmsOverruled) return tr("%1 (+)").arg(participantData.equipmentNumber().get()); if (participantData.externalId() > 0) return tr("%1 (external)").arg(participantData.equipmentNumber().get()); return participantData.equipmentNumber().get(); case ColParticipantId: return participantData.id(); case ColInfo: return QString::fromStdString(participantData.info()); case ColSign: return QString::fromStdString(participantData.identification()); default: Q_UNREACHABLE(); return QVariant(); } // switch case Qt::CheckStateRole: if (!m_bRowsAreCheckable || index.column() != 0) return ParticipantIconModelBase::data(index, role); return item->bIsChecked ? Qt::Checked : Qt::Unchecked; case LtsWidgets::LtsColumnFilterProxyModel::ComboboxFilterRole: if (index.column() == ColTypeClass) return typeClassIdFromTypeId(participantData.typeId()).underlying(); if (index.column() == ColType) return participantData.typeId(); return QVariant(); default: Q_UNREACHABLE(); return QVariant(); } // switch } bool ParticipantEditModel::setData(const QModelIndex &index, const QVariant &value, int role) { if (role != Qt::CheckStateRole || index.column() != 0 || !index.isValid()) return ParticipantIconModelBase::setData(index, value, role); if (!value.canConvert()) return false; CheckedTreeItem *item = privateData(index); if (item->bIsChecked != value.toBool()) { item->bIsChecked = value.toBool(); emit dataChanged(index, index, {role}); } return true; } bool ParticipantEditModel::canDropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) const { if (!ParticipantIconModelBase::canDropMimeData(data, action, row, column, parent)) return false; // only drop on first column allowed if (column > 0) return false; // we do not allow creating a new root knot if (!parent.isValid() && m_rootItem->childCount() > 0) return false; const CheckedTreeItem *parentItem = privateData(parent); if (!isUnitType(parentItem->participant.typeId())) return false; DEBUG_LOG(m_logger) << "can drop on " << row << ", " << column << ", parent: " << parent.row() << "," << parent.column() << ", " << parent.isValid() << ", action: " << action; // make sure to not drop a parent on a child of this parent // QAbstractItemModel takes care for this within internal drops, // but can not do this for external drops (e.g. tactical organisation) const QVector tiSources = getMimeDataItems(data, m_mimeType); if (tiSources.isEmpty()) return false; // copy due to qDeleteAll! const ParticipantData pdSource = static_cast(tiSources.first())->participant; qDeleteAll(tiSources); TypeClass dataTypeClass = typeClassIdFromTypeId(pdSource.typeId()); bool isDataTypeClassSupport = dataTypeClass == TypeClass::inner_enum::SupportEntity || dataTypeClass == TypeClass::inner_enum::SupportUnit; bool isOwnTypeClassSupport = m_partyId == 1; if (isOwnTypeClassSupport != isDataTypeClassSupport) return false; // can only happen when same party if (pdSource.partyId() == parentItem->participant.partyId()) { const int idSource = pdSource.id(); // 0 -> new item, no dnd between different models - all is fine if (idSource != 0 && isParentOf(parentItem, idSource)) { DEBUG_LOG(m_logger) << "isParentOf() for idSource: " << idSource << " is true - no drop"; return false; } } if (m_firingUnitValidator) { FiringUnitValidator::FiringUnitRestrictions restrictions = m_firingUnitValidator->getRestrictions(this, parent); // Verbund (non unit) participants must not be directly moved if (m_dtc->isAdlerVerbund(pdSource.typeId())) { const Type &type = m_dtc->type(pdSource.typeId()); if (type.typeClass() != TypeClass::Unit) return false; } if (restrictions != FiringUnitValidator::NO_RESTRICTION) { if (restrictions.testFlag(FiringUnitValidator::IS_AT_MAX_CAPACITY)) return false; if (restrictions.testFlag(FiringUnitValidator::ONLY_SAME_GEAR_CLASS_ARTILLERY_ALLOWED)) { if (!m_firingUnitValidator->isSameGearClassArtillery(parentItem->participant.typeId(), pdSource.typeId())) return false; if (m_firingUnitValidator->isArtilleryUnit(parentItem->participant.typeId()) && m_firingUnitValidator->isArtilleryUnit(pdSource.typeId())) return false; } if (restrictions.testFlag(FiringUnitValidator::NO_ARTILLERY_PARTICIPANTS_ALLOWED)) { if (m_firingUnitValidator->isArtilleryWeaponSystem(pdSource.typeId())) return false; } if (restrictions.testFlag(FiringUnitValidator::NO_TYPE_CHANGE_ALLOWED)) { const Type allowedType = m_firingUnitValidator->getAllowedType(this, parent); if (pdSource.typeId() != allowedType.id()) return false; if (m_dtc->isAdlerVerbund(pdSource.typeId())) return false; } } } // Verbund participants must not be moved to other parties if (m_dtc->isAdlerVerbund(pdSource.typeId()) && parentItem->participant.partyId() != pdSource.partyId()) return false; return true; } bool ParticipantEditModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) { if (!ParticipantIconModelBase::dropMimeData(data, action, row, column, parent)) return false; // update hierarchy numbers const CheckedTreeItem *cti = privateData(parent); if (!cti) return false; const auto children = cti->children(); for (int r = 0; r < children.size(); ++r) { CheckedTreeItem *child = static_cast(cti->child(r)); // hopefully qsfpm will sort *after* dropMimeData has finished... child->participant.hierarchyNumber(r + 1); } return true; } bool ParticipantEditModel::onReinitModel(int partyId, const QSet &checkedParticipants, Qt::CheckState cs) { m_symbolResultMapping.clear(); m_missingSuperiorId.clear(); m_usedTypeIds.clear(); m_usedTypeClassIds.clear(); m_partyId = partyId; const ParticipantManagement::Party &party = m_dtc->party(m_partyId); m_partyRole = AdvancedSymbology::roleToRole<>(party.role()); m_partyColor = QColor::fromRgb(QRgb(party.color())); ParticipantIconModelBase::resetModel(); QHash tmap; QHash> parentId2id; gatherParticipants(tmap, parentId2id); // insert recursivly insertChildrenOf(0, m_rootItem, QModelIndex(), parentId2id, tmap, checkedParticipants, cs); // unset modifed flag resetModifedFlag(m_rootItem); return true; } bool ParticipantEditModel::insertRow(int row, const QModelIndex &parent, NGLTreeItem *item) { return ParticipantIconModelBase::insertRow(row, parent, item); } bool ParticipantEditModel::removeRow(int row, const QModelIndex &parent) { return ParticipantIconModelBase::removeRow(row, parent); } bool ParticipantEditModel::isRowRemovable(const CheckedTreeItem *parentItem) const { if (!parentItem) return false; const ParticipantManagement::ParticipantData &participantData = parentItem->participant; if (participantData.equipmentNumber().is_set() && participantData.equipmentNumber().get() > 0) return false; for (int i = 0; i < parentItem->childCount(); i++) { if (!isRowRemovable(static_cast(parentItem->child(i)))) return false; } return true; } void ParticipantEditModel::dataCommitted() { m_dataToDelete.clear(); QHash tmap; QHash> parentId2id; gatherParticipants(tmap, parentId2id); // update recursivly, reupdate later if a new participant was not yet found if (!updateChildrenOf(0, m_rootItem, parentId2id, tmap)) QTimer::singleShot(500, this, &ParticipantEditModel::dataCommitted); resetModifedFlag(m_rootItem); } void ParticipantEditModel::processMissingSuperiorId() { if (m_missingSuperiorId.isEmpty()) return; // Consider if a complex subtree is move from one party to another... // Edit Participant dialog should be updated accurately: // build tree QHash tmap; QHash> parentId2id; gatherParticipants(tmap, parentId2id); // traverse every subtree: for (int id : qAsConst(m_missingSuperiorId)) { if (!tmap.contains(id)) continue; const ParticipantManagement::Participant &p = m_dtc->participant(id); if (p.id() != id) continue; onParticipantChanged(p, DataModification::eModified); if (!parentId2id.contains(id)) continue; const QSet &childIds = parentId2id.value(id); for (int childId : qAsConst(childIds)) { const ParticipantManagement::Participant &p = m_dtc->participant(childId); if (p.id() != childId) continue; onParticipantChanged(p, DataModification::eModified); } } // finish -> clear m_missingSuperiorId.clear(); } void ParticipantEditModel::selectItemByParticipantId(int participantId, const std::function &selectItem, const QModelIndex &parent) { for (int i = 0; i < rowCount(parent); ++i) { const QModelIndex idx = index(i, 0, parent); const ParticipantManagement::ParticipantData &data = privateData(idx)->participant; if (hasChildren(idx)) selectItemByParticipantId(participantId, selectItem, idx); if (data.id() == participantId) return selectItem(idx); } return; } QModelIndexList ParticipantEditModel::getIndexesByParticipantIds(const std::vector &participantIds, const QModelIndex &parent) const { QModelIndexList indexes; // clazy:exclude=reserve-candidate for (int row = 0; row < rowCount(parent); ++row) { const QModelIndex idx = index(row, 0, parent); const ParticipantManagement::ParticipantData &data = privateData(idx)->participant; if (hasChildren(idx)) indexes += getIndexesByParticipantIds(participantIds, idx); auto it = std::find(participantIds.begin(), participantIds.end(), data.id()); if (it != participantIds.end()) indexes.append(idx); } // for return indexes; } void ParticipantEditModel::setRowsCheckable(bool bCheckable) { if (m_bRowsAreCheckable == bCheckable) return; m_bRowsAreCheckable = bCheckable; onReinitModel(m_partyId); // TODO: not really needed, only invalidate first column } bool ParticipantEditModel::setCheckStateForParticipants(const QSet &checkedParticipants, Qt::CheckState cs) { if (cs != Qt::Checked && cs != Qt::Unchecked) return false; if (m_rootItem->childCount() < 1) QVector(); CheckedTreeItem *item = static_cast(m_rootItem); setCheckState(item, checkedParticipants, cs); return true; } ParticipantManagement::ParticipantData ParticipantEditModel::getParticipantData(const QModelIndex &idx) const { return privateData(idx)->participant; } std::vector ParticipantEditModel::participantData(bool bOnlyModified) const { std::vector ret; if (m_rootItem->childCount() < 1) return ret; int modId = 1000000000; for (int i = 0; i < m_rootItem->childCount(); ++i) { const auto c = getChildrenOf(m_rootItem->child(i), 0, 1, bOnlyModified, modId); ret.insert(ret.end(), c.begin(), c.end()); } ret.insert(ret.end(), m_dataToDelete.begin(), m_dataToDelete.end()); return ret; } QSet ParticipantEditModel::participantIds() const { return participantIds(m_rootItem); } QSet ParticipantEditModel::checkStateForParticipants(Qt::CheckState cs) const { if (m_rootItem->childCount() < 1) QSet(); CheckedTreeItem *item = static_cast(m_rootItem); return checkState(item, cs); } QVector ParticipantEditModel::usedEquipmentNumbers() const { return usedEquipmentNumbers(m_rootItem); } ParticipantEditModel::CheckedTreeItem *ParticipantEditModel::privateData(const QModelIndex &index) { return static_cast(ParticipantIconModelBase::privateData(index)); } const ParticipantEditModel::CheckedTreeItem *ParticipantEditModel::privateData(const QModelIndex &index) const { return static_cast(ParticipantIconModelBase::privateData(index)); } QModelIndex ParticipantEditModel::indexFromParticipantId(int participantId) const { return indexFromParticipantId(participantId, static_cast(m_rootItem)); } QString ParticipantEditModel::getHierarchyFromIndex(const QModelIndex &index) const { if (!index.isValid()) return QString(); const ParticipantManagement::ParticipantData &participantData = privateData(index)->participant; return m_dtc->hierarchyFromParticipant(participantData.id()); } //----------------------------------------------------------------------------------------------------------------------------- // PROTECTED MEMBER FUNCTIONS //----------------------------------------------------------------------------------------------------------------------------- void ParticipantEditModel::gatherParticipants(QHash &tmap, QHash> &parentId2id) const { auto cb = [this, &tmap, &parentId2id](const Participant & part) { if (part.isDeleted()) return; if (part.partyId() != m_partyId) return; // parentId -> id for lookup parentId2id[part.superiorId()] += part.id(); tmap.insert(part.id(), m_dtc->convertParticipantToParticipantData(part)); }; m_dtc->participants(cb); } std::vector ParticipantEditModel::getChildrenOf(NGLTreeItem *item, int parentId, int hierarchyNumber, bool bOnlyModifed, int &highestModId) const { if (!item) return {}; std::vector ret; const CheckedTreeItem *cti = static_cast(item); ParticipantManagement::ParticipantData participantData = cti->participant; participantData.superiorModId(parentId); participantData.hierarchyNumber(hierarchyNumber); participantData.partyId(m_partyId); // when moved from one party to another (e.g. tactical organisation) if (participantData.modId() == 0) participantData.modId(++highestModId); // check for children of this item for (int i = 0; i < item->childCount(); i++) { const auto c = getChildrenOf(item->child(i), participantData.modId(), i + 1, bOnlyModifed, highestModId); ret.insert(ret.end(), c.begin(), c.end()); } if (!bOnlyModifed || item->modified() || !ret.empty()) ret.push_back(std::move(participantData)); return ret; } bool ParticipantEditModel::updateChildrenOf(int parentId, NGLTreeItem *parent, const QHash> &parentId2id, const QHash &tmap) { bool bRet = true; const int childCount = parent->childCount(); for (int i = 0; i < childCount; ++i) { ParticipantEditModel::CheckedTreeItem *childItem = static_cast(parent->child(i)); ParticipantData &pdItem = childItem->participant; m_usedTypeIds += pdItem.typeId(); m_usedTypeClassIds += typeClassIdFromTypeId(pdItem.typeId()); if (pdItem.id() == 0) { const QSet &childIds = parentId2id.value(parentId); for (int childId : childIds) { const auto &childData = tmap.value(childId); if (childData.hierarchyNumber() - 1 == i) { pdItem.id(childData.id()); childItem->setModified(false); break; } } } if (!pdItem.id()) { bRet = false; continue; } if (!updateChildrenOf(pdItem.id(), childItem, parentId2id, tmap)) bRet = false; } return bRet; } void ParticipantEditModel::insertChildrenOf(int parentId, NGLTreeItem *parent, const QModelIndex &parentIdx, const QHash> &parentId2id, const QHash &tmap, const QSet &checkedParticipants, Qt::CheckState cs) { const QSet childIds = parentId2id.value(parentId); QMap currentHierachyNumbers; // no QSet, must be sorted! for (int childId : childIds) { auto it = tmap.constFind(childId); if (it == tmap.constEnd()) { WARNING_LOG(m_logger) << "Can not find participant with id " << childId << " - ignoring"; continue; } const ParticipantData &data = it.value(); m_usedTypeIds += data.typeId(); m_usedTypeClassIds += typeClassIdFromTypeId(data.typeId()); int insertPos = 0; if (data.hierarchyNumber() > 0) { auto hierIt = currentHierachyNumbers.insert(data.hierarchyNumber(), data.hierarchyNumber()); insertPos = std::distance(currentHierachyNumbers.begin(), hierIt); } else { // append if no valid hierarchy number is set const NGLTreeItem *treeItemParent = privateData(parentIdx); if (treeItemParent) insertPos = treeItemParent->childCount(); } CheckedTreeItem *childItem = static_cast(createItem(QVariant(), QIcon())); childItem->participant = data; childItem->bIsChecked = isChecked(checkedParticipants.contains(data.id()), cs); DEBUG_LOG(m_logger) << "insert Child " << childItem << "(" << data.id() << ", " << data.hierarchyNumber() << ") to parent " << parent << " at pos " << insertPos; if ( data.id() == 0 && data.hierarchyNumber() == 0) { WARNING_LOG(m_logger) << "avoid endless loop in recursion"; return; } insertRow(insertPos, parentIdx, childItem); tryRenderSymbol(QString::fromStdString(data.symbol()), childItem); // ToDO add role here const QModelIndex childIdx = index(insertPos, 0, parentIdx); insertChildrenOf(data.id(), childItem, childIdx, parentId2id, tmap, checkedParticipants, cs); } } bool ParticipantEditModel::isParentOf(const NGLTreeItem *item, int participantId) const { if (item == m_rootItem) return false; const CheckedTreeItem *cti = static_cast(item); if (cti->participant.id() == participantId) return true; return isParentOf(item->parent(), participantId); } QSet ParticipantEditModel::participantIds(NGLTreeItem *parentItem) const { QSet ret; ret.reserve(parentItem->childCount() + 1); ret += participantIdFromItem(parentItem); for (int i = 0; i < parentItem->childCount(); ++i) ret += participantIds(parentItem->child(i)); return ret; } QSet ParticipantEditModel::checkState(CheckedTreeItem *parentItem, Qt::CheckState cs) const { QSet ret; if (parentItem != m_rootItem) { if ((parentItem->bIsChecked && cs == Qt::Checked) || (!parentItem->bIsChecked && cs == Qt::Unchecked)) ret += participantIdFromItem(parentItem); } for (int i = 0; i < parentItem->childCount(); ++i) ret += checkState(static_cast(parentItem->child(i)), cs); return ret; } QVector ParticipantEditModel::usedEquipmentNumbers(NGLTreeItem *parentItem) const { QVector equipmentNumbers; equipmentNumbers.reserve(parentItem->childCount() + 1); int equipmentNumber = equipmentNumberFromItem(parentItem); if (equipmentNumber > 0) equipmentNumbers.append(equipmentNumber); for (int i = 0; i < parentItem->childCount(); ++i) equipmentNumbers.append(usedEquipmentNumbers(parentItem->child(i))); return equipmentNumbers; } void ParticipantEditModel::setCheckState(CheckedTreeItem *parentItem, const QSet &checkedParticipants, Qt::CheckState cs) { if (parentItem != m_rootItem) { int participantId = participantIdFromItem(parentItem); const auto newState = isChecked(checkedParticipants.contains(participantId), cs); if (newState != parentItem->bIsChecked) { parentItem->bIsChecked = newState; const QModelIndex idx = indexFromItem(parentItem); emit dataChanged(idx, idx, {Qt::CheckStateRole}); } } for (int i = 0; i < parentItem->childCount(); ++i) setCheckState(static_cast(parentItem->child(i)), checkedParticipants, cs); } QModelIndex ParticipantEditModel::indexFromParticipantId(int participantId, const CheckedTreeItem *cti) const { if (cti->participant.id() == participantId) return indexFromItem(cti); for (int row = 0; row < cti->childCount(); ++row) { const QModelIndex idx = indexFromParticipantId(participantId, static_cast(cti->child(row))); if (idx.isValid()) return idx; } return {}; } int ParticipantEditModel::participantIdFromItem(NGLTreeItem *item) const { const CheckedTreeItem *cti = static_cast(item); return cti->participant.id(); } int ParticipantEditModel::equipmentNumberFromItem(NGLTreeItem *item) const { const CheckedTreeItem *cti = static_cast(item); return cti->participant.equipmentNumber().is_set() ? cti->participant.equipmentNumber().get() : 0; } ParticipantEditModel::CheckedTreeItem *ParticipantEditModel::searchParticipant(const NGLTreeItem *parentItem, int participantId) const { const int rowCount = parentItem->childCount(); for (int r = 0; r < rowCount; ++r) { CheckedTreeItem *child = static_cast(parentItem->child(r)); if (child->participant.id() == participantId) return child; child = searchParticipant(child, participantId); if (child) return child; } return nullptr; } NGLTreeItem *ParticipantEditModel::createItem(const QVariant &data, const QIcon &icon) const { CheckedTreeItem *cti = new CheckedTreeItem(data, icon); cti->participant = qVariantToParticipant(data); if (cti->participant.symbol().size() != 0) { auto *that = const_cast(this); that->tryRenderSymbol(QString::fromStdString(cti->participant.symbol()), cti); } return cti; } QVariant ParticipantEditModel::getItemData(const NGLTreeItem *item) const { const CheckedTreeItem *cti = static_cast(item); return participantToQVariant(cti->participant); } //----------------------------------------------------------------------------------------------------------------------------- // SLOTS //----------------------------------------------------------------------------------------------------------------------------- int ParticipantEditModel::insertParticipant(const QModelIndex &parent, ParticipantManagement::ParticipantData participantData, bool bNewParticipant) { const QModelIndex parentCol0 = parent.column() == 0 ? parent : indexFromItem(privateData(parent)); const int row = rowCount(parentCol0); if (bNewParticipant) { participantData.id(0); // need new participantId participantData.modId(0); // need new modId } participantData.partyId(m_partyId); participantData.hierarchyNumber(row + 1); CheckedTreeItem *item = static_cast(createItem(participantToQVariant(participantData), QIcon())); item->setModified(true); if (!insertRow(row, parentCol0, item)) return -1; tryRenderSymbol(QString::fromStdString(participantData.symbol()), item); emit internalDataChanged(); return row; } int ParticipantEditModel::onInsertRow(const QModelIndex &parent, const ParticipantData &participantData) { return insertParticipant(parent, participantData, true); } bool ParticipantEditModel::onUpdateRow(const QModelIndex &index, const ParticipantManagement::ParticipantData &participantData) { if (!index.isValid()) return false; CheckedTreeItem *item = privateData(index); const ParticipantManagement::ParticipantData &oldData = item->participant; if (participantData == oldData) return true; if (oldData.symbol() != participantData.symbol()) tryRenderSymbol(QString::fromStdString(participantData.symbol()), item); emit internalDataChanged(); item->participant = participantData; return setItemData(index, QVariant(), item->icon()); } bool ParticipantEditModel::onDeleteRow(const QModelIndex &idx, bool isFirstItem) { while (rowCount(idx) > 0) onDeleteRow(ParticipantIconModelBase::index(0, 0, idx), false); if (isFirstItem) //edit mode { CheckedTreeItem *item = privateData(idx); ParticipantManagement::ParticipantData data = item->participant; data.isDeleted(true); data.assignState() = ParticipantAssignState::NotAssigned; data.equipmentNumber().reset(); if (data.modId() != 0) //only append old items to remove list m_dataToDelete.push_back(std::move(data)); } removeRow(idx.row(), idx.parent()); emit internalDataChanged(); return true; } void ParticipantEditModel::onParticipantChanged(const ParticipantManagement::Participant &data, DataModification modType) { if (data.partyId() != m_partyId) return; Q_UNUSED(modType); CheckedTreeItem *cti = searchParticipant(m_rootItem, data.id()); if (cti) { cti->participant = m_dtc->convertParticipantToParticipantData(data); emit dataChanged(indexFromItem(cti, 0), indexFromItem(cti, ColSign)); return; } CheckedTreeItem *ctiParent = searchParticipant(m_rootItem, data.superiorId()); if (ctiParent) { int insertPos = 0; if (data.hierarchyNumber() > 0) { for (int i = 0; i < ctiParent->childCount(); ++i) { CheckedTreeItem *child = static_cast(ctiParent->child(i)); if (child->participant.hierarchyNumber() > data.hierarchyNumber()) { insertPos = i; break; } } } else { // append if no valid hierarchy number is set insertPos = ctiParent->childCount(); } CheckedTreeItem *childItem = static_cast(createItem(QVariant(), QIcon())); childItem->participant = m_dtc->convertParticipantToParticipantData(data); DEBUG_LOG(m_logger) << "insert Child " << childItem << "(" << data.id() << ", " << data.hierarchyNumber() << ") to parent " << ctiParent << " at pos " << insertPos; insertRow(insertPos, indexFromItem(ctiParent), childItem); tryRenderSymbol(QString::fromStdString(data.symbol()), childItem); // ToDO add role here return; } else { // superiorId() was not found, most likely due to order of incomming updates: // Like dialog 'Edit Participant' m_missingSuperiorId.insert(data.superiorId()); m_missingSuperiorIdTimer->start(); } // onReinitModel(m_partyId); } //============================================================================================================================= // PARTICIPANTTREEFILTERPROXYMODEL //============================================================================================================================= //----------------------------------------------------------------------------------------------------------------------------- // PUBLIC MEMBER FUNCTIONS //----------------------------------------------------------------------------------------------------------------------------- void ParticipantTreeFilterProxyModel::setAllowedTypes(const QSet &allowedTypes) { if (m_allowedTypes == allowedTypes) return; m_allowedTypes = allowedTypes; invalidateFilter(); } void ParticipantTreeFilterProxyModel::setSourceModel(QAbstractItemModel *sourceModel) { m_sourceModel = qobject_cast(sourceModel); setRecursiveFilteringEnabled(true); LtsWidgets::LtsColumnFilterProxyModel::setSourceModel(sourceModel); } //----------------------------------------------------------------------------------------------------------------------------- // PROTECTED MEMBER FUNCTIONS //----------------------------------------------------------------------------------------------------------------------------- bool ParticipantTreeFilterProxyModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const { if ((!m_sourceModel) || (source_left.column() != source_right.column())) return LtsWidgets::LtsColumnFilterProxyModel::lessThan(source_left, source_right); const ParticipantEditModel::CheckedTreeItem *left = m_sourceModel->privateData(source_left); const ParticipantEditModel::CheckedTreeItem *right = m_sourceModel->privateData(source_right); switch (source_left.column()) { case ParticipantEditModel::ColDesignator: return left->participant.hierarchyNumber() < right->participant.hierarchyNumber(); case ParticipantEditModel::ColIdentifier: return compareStrings(left->participant.tacticalIdentifier(), right->participant.tacticalIdentifier(), left, right); case ParticipantEditModel::ColTypeClass: case ParticipantEditModel::ColType: break; // string compare case ParticipantEditModel::ColEquipmentNumber: if (!left->participant.equipmentNumber().is_set() && !right->participant.equipmentNumber().is_set()) return left->participant.hierarchyNumber() < right->participant.hierarchyNumber(); if (!left->participant.equipmentNumber().is_set()) return true; if (!right->participant.equipmentNumber().is_set()) return false; return left->participant.equipmentNumber().get() < right->participant.equipmentNumber().get(); case ParticipantEditModel::ColParticipantId: return left->participant.id() < right->participant.id(); case ParticipantEditModel::ColInfo: return compareStrings(left->participant.info(), right->participant.info(), left, right); case ParticipantEditModel::ColSign: return compareStrings(left->participant.identification(), right->participant.identification(), left, right); } return LtsWidgets::LtsColumnFilterProxyModel::lessThan(source_left, source_right); } bool ParticipantTreeFilterProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const { if ((!m_sourceModel) || (m_allowedTypes.isEmpty())) return LtsWidgets::LtsColumnFilterProxyModel::filterAcceptsRow(source_row, source_parent); const ParticipantEditModel::CheckedTreeItem *parentItem = m_sourceModel->privateData(source_parent); if (!parentItem) return false; const ParticipantEditModel::CheckedTreeItem *childItem = static_cast(parentItem->child(source_row)); if (!childItem) return false; #if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) if (!m_allowedTypes.contains(childItem->participant.typeId())) return false; #else // we have to do recursive filtering by ourselves (until we only support Qt >= 5.10) if ((recursiveFilteringEnabled()) && (!filterTypeRecursive(childItem))) return false; #endif return LtsWidgets::LtsColumnFilterProxyModel::filterAcceptsRow(source_row, source_parent); } #if QT_VERSION < QT_VERSION_CHECK(5, 10, 0) bool ParticipantTreeFilterProxyModel::filterTypeRecursive(const ParticipantEditModel::CheckedTreeItem *item) const { if (m_allowedTypes.contains(item->participant.typeId())) return true; for (int i = 0; i < item->childCount(); ++i) { const ParticipantEditModel::CheckedTreeItem *child = static_cast(item->child(i)); if (m_allowedTypes.contains(child->participant.typeId())) return true; if (filterTypeRecursive(child)) return true; } return false; } #endif //============================================================================================================================= // PARTICIPANTTABLEFILTERPROXYMODEL //============================================================================================================================= ParticipantTableFilterProxyModel::ParticipantTableFilterProxyModel(QObject *parent) : LtsWidgets::LtsColumnFilterProxyModel(parent) , m_logger(ParticipantManagement::getLogger(QStringLiteral("ParticipantTableFilterProxyModel"))) { setRecursiveFilteringEnabled(false); } //----------------------------------------------------------------------------------------------------------------------------- // PUBLIC MEMBER FUNCTIONS //----------------------------------------------------------------------------------------------------------------------------- void ParticipantTableFilterProxyModel::init() { if ((!m_mapFromSource.isEmpty()) || (!m_mapToSource.isEmpty())) resetMapping(); mapTreeToTable(); // m_mapFromSource and m_mapToSource are ready m_mapFromSourceInternal = m_mapFromSource; m_mapToSourceInternal = m_mapToSource; m_mapFromSourceFiltered = m_mapFromSource; m_mapToSourceFiltered = m_mapToSource; onFilter(); if (m_sortData.isActive) onSort(m_sortData.column, m_sortData.order); } void ParticipantTableFilterProxyModel::resetMapping() { m_mapFromSourceInternal.clear(); m_mapToSourceInternal.clear(); m_mapFromSource.clear(); m_mapToSource.clear(); m_mapFromSourceFiltered.clear(); m_mapToSourceFiltered.clear(); m_sortMap.clear(); } void ParticipantTableFilterProxyModel::setSourceModel(QAbstractItemModel *sourceModel) { m_sourceModel = qobject_cast(sourceModel); LtsWidgets::LtsColumnFilterProxyModel::setSourceModel(sourceModel); } int ParticipantTableFilterProxyModel::rowCount(const QModelIndex &parent) const { return parent.isValid() ? 0 : m_mapFromSourceInternal.size(); } int ParticipantTableFilterProxyModel::columnCount(const QModelIndex &parent) const { return m_sourceModel->columnCount(mapToSource(parent)); } QVariant ParticipantTableFilterProxyModel::data(const QModelIndex &index, int role) const { if ((index.column() == ParticipantEditModel::Columns::ColDesignator) && (role == Qt::DisplayRole)) return m_sourceModel->getHierarchyFromIndex(mapToSource(index)); return LtsColumnFilterProxyModel::data(index, role); } QVariant ParticipantTableFilterProxyModel::headerData(int section, Qt::Orientation orientation, int role) const { return m_sourceModel->headerData(section, orientation, role); } QModelIndex ParticipantTableFilterProxyModel::mapFromSource(const QModelIndex &sourceIndex) const { if (!m_mapFromSourceInternal.contains(sourceIndex)) return {}; return createIndex(m_mapFromSourceInternal.value(sourceIndex), sourceIndex.column()); } QModelIndex ParticipantTableFilterProxyModel::mapToSource(const QModelIndex &proxyIndex) const { if (!m_mapToSourceInternal.contains(proxyIndex.row())) // this includes !proxyIndex.isValid() return {}; QModelIndex idx = m_mapToSourceInternal.value(proxyIndex.row()); return m_sourceModel->index(idx.row(), proxyIndex.column(), idx.parent()); } QModelIndex ParticipantTableFilterProxyModel::index(int row, int column, const QModelIndex &parent) const { return parent.isValid() ? QModelIndex() : createIndex(row, column); } QModelIndex ParticipantTableFilterProxyModel::parent(const QModelIndex &child) const { Q_UNUSED(child); return {}; } QModelIndex ParticipantTableFilterProxyModel::sibling(int row, int column, const QModelIndex &idx) const { return idx.row() == row ? createIndex(row, column) : QModelIndex(); } bool ParticipantTableFilterProxyModel::hasChildren(const QModelIndex &parent) const { return !parent.isValid(); } //----------------------------------------------------------------------------------------------------------------------------- // PROTECTED MEMBER FUNCTIONS //----------------------------------------------------------------------------------------------------------------------------- bool ParticipantTableFilterProxyModel::lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight) const { if (sourceLeft.column() != sourceRight.column()) return false; const ParticipantEditModel::CheckedTreeItem *left = m_sourceModel->privateData(sourceLeft); const ParticipantEditModel::CheckedTreeItem *right = m_sourceModel->privateData(sourceRight); switch (sourceLeft.column()) { case ParticipantEditModel::ColDesignator: return left->participant.hierarchyNumber() < right->participant.hierarchyNumber(); case ParticipantEditModel::ColIdentifier: return compareStrings(left->participant.tacticalIdentifier(), right->participant.tacticalIdentifier(), left, right); case ParticipantEditModel::ColTypeClass: case ParticipantEditModel::ColType: return LtsWidgets::LtsColumnFilterProxyModel::lessThan(sourceLeft, sourceRight); case ParticipantEditModel::ColEquipmentNumber: if (!left->participant.equipmentNumber().is_set() && !right->participant.equipmentNumber().is_set()) return left->participant.hierarchyNumber() < right->participant.hierarchyNumber(); if (!left->participant.equipmentNumber().is_set()) return true; if (!right->participant.equipmentNumber().is_set()) return false; return left->participant.equipmentNumber().get() < right->participant.equipmentNumber().get(); case ParticipantEditModel::ColParticipantId: return left->participant.id() < right->participant.id(); case ParticipantEditModel::ColInfo: return compareStrings(left->participant.info(), right->participant.info(), left, right); case ParticipantEditModel::ColSign: return compareStrings(left->participant.identification(), right->participant.identification(), left, right); default: return false; } // switch } bool ParticipantTableFilterProxyModel::filterAcceptsColumn(int source_column, const QModelIndex &source_parent) const { Q_UNUSED(source_column); Q_UNUSED(source_parent); return true; } bool ParticipantTableFilterProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const { Q_UNUSED(source_row); Q_UNUSED(source_parent); return true; } bool ParticipantTableFilterProxyModel::filterAcceptsRowInternal(int sourceRow, const QModelIndex &sourceParent) const { const QModelIndex index = m_sourceModel->index(sourceRow, 0, sourceParent); if (m_sourceModel->hasChildren(index)) { const ParticipantEditModel::CheckedTreeItem *parent = static_cast(index.internalPointer()); for (int i = 0; i < parent->childCount(); ++i) if (filterAcceptsRowInternal(i, index)) return true; } // if // column data filters const QHash cdf = columnDataFilters(); for (auto it = cdf.begin(); it != cdf.end(); ++it) { const QModelIndex idx = m_sourceModel->index(sourceRow, it.key(), sourceParent); const QVariant var = m_sourceModel->data(idx, LtsWidgets::LtsColumnFilterProxyModel::ComboboxFilterRole).toString(); if (var != it.value()) return false; } // for // column string filters const QHash csf = columnStringFilters(); for (auto it = csf.begin(); it != csf.end(); ++it) { const QRegularExpression ®Expr = it.value(); if (!regExpr.isValid()) continue; const QModelIndex idx = m_sourceModel->index(sourceRow, it.key(), sourceParent); const QString str = m_sourceModel->data(idx, Qt::DisplayRole).toString(); if (!str.contains(regExpr)) return false; } // for return true; } //----------------------------------------------------------------------------------------------------------------------------- // PRIVATE MEMBER FUNCTIONS //----------------------------------------------------------------------------------------------------------------------------- int ParticipantTableFilterProxyModel::mapTreeToTable(const QModelIndex &parent, int row) { const ParticipantEditModel::CheckedTreeItem *item = m_sourceModel->privateData(parent); // static_cast(parent.internalPointer()) for (int i = 0; i < item->childCount(); ++i) { QModelIndex child = m_sourceModel->index(i, 0, parent); m_mapFromSource.insert(child, row); m_mapToSource.insert(row, child); ++row; if (m_sourceModel->hasChildren(child)) row = mapTreeToTable(child, row); } // for return row; } //----------------------------------------------------------------------------------------------------------------------------- // SLOTS //----------------------------------------------------------------------------------------------------------------------------- void ParticipantTableFilterProxyModel::onFilter() { const QHash &colDataFilters = columnDataFilters(); const QHash &colStringFilters = columnStringFilters(); m_isFilterActive = !colDataFilters.isEmpty() || !colStringFilters.isEmpty(); if (m_isFilterActive) { m_mapToSourceFiltered.clear(); m_mapFromSourceFiltered.clear(); for (auto it = m_mapToSource.begin(); it != m_mapToSource.end(); ++it) // filter all source model indexes { int row = it.key(); QModelIndex index = it.value(); if (filterAcceptsRowInternal(index.row(), index.parent())) { m_mapToSourceFiltered.insert(row, index); m_mapFromSourceFiltered.insert(index, row); } // if } // for if (m_mapToSourceFiltered.size() != m_mapToSource.size()) { QList rows = m_mapToSourceFiltered.keys(); std::sort(rows.begin(), rows.end()); int count = 0; for (const auto &row : qAsConst(rows)) { if (row == count) { ++count; continue; } // if QModelIndex index = m_mapToSourceFiltered.value(row); m_mapToSourceFiltered.remove(row); m_mapFromSourceFiltered.remove(index); m_mapToSourceFiltered.insert(count, index); m_mapFromSourceFiltered.insert(index, count); ++count; } // for } // if m_mapToSourceInternal = m_mapToSourceFiltered; m_mapFromSourceInternal = m_mapFromSourceFiltered; } // if else { m_mapToSourceInternal = m_mapToSource; m_mapFromSourceInternal = m_mapFromSource; m_mapToSourceFiltered = m_mapToSource; m_mapFromSourceFiltered = m_mapFromSource; } // else if ((m_sortData.isActive) && !m_mapFromSourceInternal.isEmpty()) onSort(m_sortData.column, m_sortData.order); emit layoutChanged(); } void ParticipantTableFilterProxyModel::onSort(int column, Qt::SortOrder order) { if ((column < 0) || (column >= m_sourceModel->columnCount())) return; m_sortData.isActive = true; m_sortData.column = column; m_sortData.order = order; m_sortMap.clear(); m_sortMap.reserve(m_mapToSourceFiltered.size()); // m_sortMap is a QVector // undo previous sorting m_mapFromSourceInternal = m_mapFromSourceFiltered; m_mapToSourceInternal = m_mapToSourceFiltered; // insertion sort for (int oldRow = 0; oldRow < rowCount(); ++oldRow) { QModelIndex currentIndex = mapToSource(index(oldRow, column)); int newRow = 0; for (; newRow < oldRow; ++newRow) { QModelIndex compareIndex = mapToSource(index(m_sortMap.value(newRow), column)); if (lessThan(currentIndex, compareIndex)) break; } // for m_sortMap.insert(newRow, oldRow); } // for if (m_sortData.descending()) std::reverse(m_sortMap.begin(), m_sortMap.end()); m_mapToSourceInternal.clear(); m_mapFromSourceInternal.clear(); for (int newRow = 0; newRow < m_sortMap.size(); ++newRow) { QModelIndex index = m_mapToSourceFiltered.value(m_sortMap[newRow]); m_mapFromSourceInternal.insert(index, newRow); m_mapToSourceInternal.insert(newRow, index); } // for emit layoutChanged(); } } //namespace ParticipantManagement