File indexing completed on 2024-12-22 05:05:20
0001 // SPDX-FileCopyrightText: 2016 Sandro Knauß <knauss@kolabsys.com> 0002 // SPDX-License-Identifier: LGPL-2.0-or-later 0003 0004 #include "partmodel.h" 0005 0006 #include "htmlutils.h" 0007 #include "objecttreeparser.h" 0008 #include "utils.h" 0009 0010 #include <KLocalizedString> 0011 0012 #include <QDebug> 0013 #include <QGpgME/Protocol> 0014 #include <QRegularExpression> 0015 #include <QStringLiteral> 0016 #include <QTextDocument> 0017 0018 // We return a pair containing the trimmed string, as well as a boolean indicating whether the string was trimmed or not 0019 std::pair<QString, bool> PartModel::trim(const QString &text) 0020 { 0021 // The delimiters have <p>.? prefixed including the .? because sometimes we get a byte order mark <feff> (seen with user-agent: 0022 // Microsoft-MacOutlook/10.1d.0.190908) We match both regulard withspace with \s and non-breaking spaces with \u00A0 0023 const QList<QRegularExpression> delimiters{ 0024 // English 0025 QRegularExpression{QStringLiteral("<p>.?-+Original(\\s|\u00A0)Message-+"), QRegularExpression::CaseInsensitiveOption}, 0026 // The remainder is not quoted 0027 QRegularExpression{QStringLiteral("<p>.?On.*wrote:"), QRegularExpression::CaseInsensitiveOption}, 0028 // The remainder is quoted 0029 QRegularExpression{QStringLiteral("> On.*wrote:"), QRegularExpression::CaseInsensitiveOption}, 0030 0031 // German 0032 // Forwarded 0033 QRegularExpression{QStringLiteral("<p>.?Von:.*</p>"), QRegularExpression::CaseInsensitiveOption}, 0034 // Reply 0035 QRegularExpression{QStringLiteral("<p>.?Am.*schrieb.*:</p>"), QRegularExpression::CaseInsensitiveOption}, 0036 // Signature 0037 QRegularExpression{QStringLiteral("<p>.?--(\\s|\u00A0)<br>"), QRegularExpression::CaseInsensitiveOption}, 0038 }; 0039 0040 for (const auto &expression : delimiters) { 0041 auto i = expression.globalMatch(text); 0042 while (i.hasNext()) { 0043 const auto match = i.next(); 0044 const int startOffset = match.capturedStart(0); 0045 // This is a very simplistic detection for an inline reply where we would have the patterns before the actual message content. 0046 // We simply ignore anything we find within the first few lines. 0047 if (startOffset >= 5) { 0048 return {text.mid(0, startOffset), true}; 0049 } else { 0050 // Search for the next delimiter 0051 continue; 0052 } 0053 } 0054 } 0055 0056 return {text, false}; 0057 } 0058 0059 static QString addCss(const QString &s) 0060 { 0061 // Get the default font from QApplication 0062 static const QString fontFamily = QFont{}.family(); 0063 // overflow:hidden ensures no scrollbars are ever shown. 0064 static const QString css = QStringLiteral("<style>\n") 0065 + QStringLiteral( 0066 "body {\n" 0067 " overflow:hidden;\n" 0068 " font-family: \"%1\" ! important;\n" 0069 " color: #31363b ! important;\n" 0070 " background-color: #fcfcfc ! important\n" 0071 "}\n") 0072 .arg(fontFamily) 0073 + QStringLiteral("blockquote { \n" 0074 " border-left: 2px solid #bdc3c7 ! important;\n" 0075 "}\n") 0076 + QStringLiteral("</style>"); 0077 0078 const QString header = QLatin1StringView( 0079 "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">\n" 0080 "<html><head><title></title>") 0081 + css + QLatin1StringView("</head>\n<body>\n"); 0082 return header + s + QStringLiteral("</body></html>"); 0083 } 0084 0085 class PartModelPrivate 0086 { 0087 public: 0088 PartModelPrivate(PartModel *q_ptr, const std::shared_ptr<MimeTreeParser::ObjectTreeParser> &parser) 0089 : q(q_ptr) 0090 , mParser(parser) 0091 { 0092 collectContents(); 0093 } 0094 0095 ~PartModelPrivate() = default; 0096 0097 void checkPart(const MimeTreeParser::MessagePart::Ptr part) 0098 { 0099 mMimeTypeCache[part.data()] = part->mimeType(); 0100 // Extract the content of the part and 0101 mContents.insert(part.data(), extractContent(part.data())); 0102 } 0103 0104 // Recursively find encapsulated messages 0105 void findEncapsulated(const MimeTreeParser::EncapsulatedRfc822MessagePart::Ptr &e) 0106 { 0107 mEncapsulatedParts[e.data()] = mParser->collectContentParts(e); 0108 for (const auto &subPart : std::as_const(mEncapsulatedParts[e.data()])) { 0109 checkPart(subPart); 0110 mParents[subPart.data()] = e.data(); 0111 if (auto encapsulatedSub = subPart.dynamicCast<MimeTreeParser::EncapsulatedRfc822MessagePart>()) { 0112 findEncapsulated(encapsulatedSub); 0113 } 0114 } 0115 } 0116 0117 QVariant extractContent(MimeTreeParser::MessagePart *messagePart) 0118 { 0119 if (auto alternativePart = dynamic_cast<MimeTreeParser::AlternativeMessagePart *>(messagePart)) { 0120 if (alternativePart->availableModes().contains(MimeTreeParser::AlternativeMessagePart::MultipartIcal)) { 0121 return alternativePart->icalContent(); 0122 } 0123 } 0124 0125 auto preprocessPlaintext = [&](const QString &text) { 0126 // Reduce consecutive new lines to never exceed 2 0127 auto cleaned = text; 0128 cleaned.replace(QRegularExpression(QStringLiteral("[\n\r]{2,}")), QStringLiteral("\n\n")); 0129 0130 // We always do rich text (so we get highlighted links and stuff). 0131 const auto html = Qt::convertFromPlainText(cleaned, Qt::WhiteSpaceNormal); 0132 if (trimMail) { 0133 const auto result = PartModel::trim(html); 0134 isTrimmed = result.second; 0135 Q_EMIT q->trimMailChanged(); 0136 return MimeTreeParser::linkify(result.first); 0137 } 0138 return MimeTreeParser::linkify(html); 0139 }; 0140 0141 if (messagePart->isHtml()) { 0142 if (dynamic_cast<MimeTreeParser::AlternativeMessagePart *>(messagePart)) { 0143 containsHtmlAndPlain = true; 0144 Q_EMIT q->containsHtmlChanged(); 0145 if (!showHtml) { 0146 return preprocessPlaintext(messagePart->plaintextContent()); 0147 } 0148 } 0149 return addCss(mParser->resolveCidLinks(messagePart->htmlContent())); 0150 } 0151 0152 if (auto attachmentPart = dynamic_cast<MimeTreeParser::AttachmentMessagePart *>(messagePart)) { 0153 auto node = attachmentPart->node(); 0154 if (node && mMimeTypeCache[attachmentPart] == "text/calendar") { 0155 return messagePart->text(); 0156 } 0157 } 0158 0159 return preprocessPlaintext(messagePart->text()); 0160 } 0161 0162 QVariant contentForPart(MimeTreeParser::MessagePart *messagePart) const 0163 { 0164 return mContents.value(messagePart); 0165 } 0166 0167 void collectContents() 0168 { 0169 mEncapsulatedParts.clear(); 0170 mParents.clear(); 0171 mContents.clear(); 0172 containsHtmlAndPlain = false; 0173 isTrimmed = false; 0174 0175 const auto parts = mParser->collectContentParts(); 0176 MimeTreeParser::MessagePart::List filteredParts; 0177 0178 for (const auto &part : parts) { 0179 if (part->node()) { 0180 const auto contentType = part->node()->contentType(); 0181 if (contentType && contentType->hasParameter(QStringLiteral("protected-headers"))) { 0182 const auto contentDisposition = part->node()->contentDisposition(); 0183 if (contentDisposition && contentDisposition->disposition() == KMime::Headers::CDinline) { 0184 continue; 0185 } 0186 } 0187 } 0188 0189 filteredParts << part; 0190 } 0191 0192 for (const auto &part : std::as_const(filteredParts)) { 0193 checkPart(part); 0194 if (auto encapsulatedPart = part.dynamicCast<MimeTreeParser::EncapsulatedRfc822MessagePart>()) { 0195 findEncapsulated(encapsulatedPart); 0196 } 0197 } 0198 0199 for (const auto &part : std::as_const(filteredParts)) { 0200 if (mMimeTypeCache[part.data()] == "text/calendar") { 0201 mParts.prepend(part); 0202 } else { 0203 mParts.append(part); 0204 } 0205 } 0206 } 0207 0208 PartModel *q; 0209 MimeTreeParser::MessagePart::List mParts; 0210 QHash<MimeTreeParser::MessagePart *, QByteArray> mMimeTypeCache; 0211 QHash<MimeTreeParser::MessagePart *, MimeTreeParser::MessagePart::List> mEncapsulatedParts; 0212 QHash<MimeTreeParser::MessagePart *, MimeTreeParser::MessagePart *> mParents; 0213 QMap<MimeTreeParser::MessagePart *, QVariant> mContents; 0214 std::shared_ptr<MimeTreeParser::ObjectTreeParser> mParser; 0215 bool showHtml{false}; 0216 bool containsHtmlAndPlain{false}; 0217 bool trimMail{false}; 0218 bool isTrimmed{false}; 0219 }; 0220 0221 PartModel::PartModel(std::shared_ptr<MimeTreeParser::ObjectTreeParser> parser) 0222 : d(std::unique_ptr<PartModelPrivate>(new PartModelPrivate(this, parser))) 0223 { 0224 } 0225 0226 PartModel::~PartModel() 0227 { 0228 } 0229 0230 void PartModel::setShowHtml(bool html) 0231 { 0232 if (d->showHtml != html) { 0233 beginResetModel(); 0234 d->showHtml = html; 0235 d->collectContents(); 0236 endResetModel(); 0237 Q_EMIT showHtmlChanged(); 0238 } 0239 } 0240 0241 bool PartModel::showHtml() const 0242 { 0243 return d->showHtml; 0244 } 0245 0246 void PartModel::setTrimMail(bool trim) 0247 { 0248 if (d->trimMail != trim) { 0249 beginResetModel(); 0250 d->trimMail = trim; 0251 d->collectContents(); 0252 endResetModel(); 0253 Q_EMIT trimMailChanged(); 0254 } 0255 } 0256 0257 bool PartModel::trimMail() const 0258 { 0259 return d->trimMail; 0260 } 0261 0262 bool PartModel::isTrimmed() const 0263 { 0264 return d->isTrimmed; 0265 } 0266 0267 bool PartModel::containsHtml() const 0268 { 0269 return d->containsHtmlAndPlain; 0270 } 0271 0272 QHash<int, QByteArray> PartModel::roleNames() const 0273 { 0274 QHash<int, QByteArray> roles; 0275 roles[TypeRole] = "type"; 0276 roles[ContentRole] = "content"; 0277 roles[IsEmbeddedRole] = "embedded"; 0278 roles[IsEncryptedRole] = "encrypted"; 0279 roles[IsSignedRole] = "signed"; 0280 roles[SecurityLevelRole] = "securityLevel"; 0281 roles[EncryptionSecurityLevelRole] = "encryptionSecurityLevel"; 0282 roles[SignatureSecurityLevelRole] = "signatureSecurityLevel"; 0283 roles[ErrorType] = "errorType"; 0284 roles[ErrorString] = "errorString"; 0285 roles[IsErrorRole] = "error"; 0286 roles[SenderRole] = "sender"; 0287 roles[SignatureDetails] = "signatureDetails"; 0288 roles[EncryptionDetails] = "encryptionDetails"; 0289 roles[DateRole] = "date"; 0290 return roles; 0291 } 0292 0293 QModelIndex PartModel::index(int row, int column, const QModelIndex &parent) const 0294 { 0295 if (row < 0 || column != 0) { 0296 return QModelIndex(); 0297 } 0298 if (parent.isValid()) { 0299 const auto part = static_cast<MimeTreeParser::MessagePart *>(parent.internalPointer()); 0300 auto encapsulatedPart = dynamic_cast<MimeTreeParser::EncapsulatedRfc822MessagePart *>(part); 0301 0302 if (encapsulatedPart) { 0303 const auto parts = d->mEncapsulatedParts[encapsulatedPart]; 0304 if (row < parts.size()) { 0305 return createIndex(row, column, parts.at(row).data()); 0306 } 0307 } 0308 return QModelIndex(); 0309 } 0310 if (row < d->mParts.size()) { 0311 return createIndex(row, column, d->mParts.at(row).data()); 0312 } 0313 return QModelIndex(); 0314 } 0315 0316 SignatureInfo encryptionInfo(MimeTreeParser::MessagePart *messagePart) 0317 { 0318 SignatureInfo signatureInfo; 0319 const auto encryptions = messagePart->encryptions(); 0320 if (encryptions.size() > 1) { 0321 qWarning() << "Can't deal with more than one encryption"; 0322 } 0323 for (const auto &encryptionPart : encryptions) { 0324 signatureInfo.keyId = encryptionPart->partMetaData()->keyId; 0325 signatureInfo.cryptoProto = encryptionPart->cryptoProto(); 0326 signatureInfo.decryptRecipients = encryptionPart->decryptRecipients(); 0327 } 0328 return signatureInfo; 0329 }; 0330 0331 SignatureInfo signatureInfo(MimeTreeParser::MessagePart *messagePart) 0332 { 0333 SignatureInfo signatureInfo; 0334 const auto signatures = messagePart->signatures(); 0335 if (signatures.size() > 1) { 0336 qWarning() << "Can't deal with more than one signature"; 0337 } 0338 for (const auto &signaturePart : signatures) { 0339 signatureInfo.keyId = signaturePart->partMetaData()->keyId; 0340 signatureInfo.cryptoProto = signaturePart->cryptoProto(); 0341 signatureInfo.keyMissing = signaturePart->partMetaData()->sigSummary & GpgME::Signature::KeyMissing; 0342 signatureInfo.keyExpired = signaturePart->partMetaData()->sigSummary & GpgME::Signature::KeyExpired; 0343 signatureInfo.keyRevoked = signaturePart->partMetaData()->sigSummary & GpgME::Signature::KeyRevoked; 0344 signatureInfo.sigExpired = signaturePart->partMetaData()->sigSummary & GpgME::Signature::SigExpired; 0345 signatureInfo.crlMissing = signaturePart->partMetaData()->sigSummary & GpgME::Signature::CrlMissing; 0346 signatureInfo.crlTooOld = signaturePart->partMetaData()->sigSummary & GpgME::Signature::CrlTooOld; 0347 signatureInfo.signer = signaturePart->partMetaData()->signer; 0348 signatureInfo.isCompliant = signaturePart->partMetaData()->isCompliant; 0349 signatureInfo.signerMailAddresses = signaturePart->partMetaData()->signerMailAddresses; 0350 signatureInfo.signatureIsGood = signaturePart->partMetaData()->isGoodSignature; 0351 signatureInfo.keyTrust = signaturePart->partMetaData()->keyTrust; 0352 } 0353 return signatureInfo; 0354 } 0355 0356 QVariant PartModel::data(const QModelIndex &index, int role) const 0357 { 0358 if (!index.isValid()) { 0359 return QVariant(); 0360 } 0361 0362 if (index.internalPointer()) { 0363 const auto messagePart = static_cast<MimeTreeParser::MessagePart *>(index.internalPointer()); 0364 // qWarning() << "Found message part " << messagePart->metaObject()->className() << messagePart->partMetaData()->status << messagePart->error(); 0365 Q_ASSERT(messagePart); 0366 switch (role) { 0367 case Qt::DisplayRole: 0368 return QStringLiteral("Content%1"); 0369 case SenderRole: { 0370 if (auto e = dynamic_cast<MimeTreeParser::EncapsulatedRfc822MessagePart *>(messagePart)) { 0371 return e->from(); 0372 } 0373 return {}; 0374 } 0375 case DateRole: { 0376 if (auto e = dynamic_cast<MimeTreeParser::EncapsulatedRfc822MessagePart *>(messagePart)) { 0377 return e->date(); 0378 } 0379 return {}; 0380 } 0381 case TypeRole: { 0382 if (messagePart->error()) { 0383 return QVariant::fromValue(Types::Error); 0384 } 0385 if (dynamic_cast<MimeTreeParser::EncapsulatedRfc822MessagePart *>(messagePart)) { 0386 return QVariant::fromValue(Types::Encapsulated); 0387 } 0388 if (auto alternativePart = dynamic_cast<MimeTreeParser::AlternativeMessagePart *>(messagePart)) { 0389 if (alternativePart->availableModes().contains(MimeTreeParser::AlternativeMessagePart::MultipartIcal)) { 0390 return QVariant::fromValue(Types::Ical); 0391 } 0392 } 0393 if (auto attachmentPart = dynamic_cast<MimeTreeParser::AttachmentMessagePart *>(messagePart)) { 0394 auto node = attachmentPart->node(); 0395 if (!node) { 0396 qWarning() << "no content for attachment"; 0397 return {}; 0398 } 0399 if (d->mMimeTypeCache[attachmentPart] == "text/calendar") { 0400 return QVariant::fromValue(Types::Ical); 0401 } 0402 } 0403 if (!d->showHtml && d->containsHtmlAndPlain) { 0404 return QVariant::fromValue(Types::Plain); 0405 } 0406 // For simple html we don't need a browser 0407 auto complexHtml = [&] { 0408 if (messagePart->isHtml()) { 0409 const auto text = messagePart->htmlContent(); 0410 if (text.contains(QStringLiteral("<!DOCTYPE html PUBLIC"))) { 0411 // We can probably deal with this if it adheres to the strict dtd 0412 //(that's what our composer produces as well) 0413 if (!text.contains(QStringLiteral("http://www.w3.org/TR/REC-html40/strict.dtd"))) { 0414 return true; 0415 } 0416 } 0417 // Blockquotes don't support any styling which would be necessary so they become readable. 0418 if (text.contains(QStringLiteral("blockquote"))) { 0419 return true; 0420 } 0421 // Media queries are too advanced 0422 if (text.contains(QStringLiteral("@media"))) { 0423 return true; 0424 } 0425 // auto css properties are not supported e.g margin-left: auto; 0426 if (text.contains(QStringLiteral(": auto;"))) { 0427 return true; 0428 } 0429 return false; 0430 } else { 0431 return false; 0432 } 0433 }(); 0434 if (complexHtml) { 0435 return QVariant::fromValue(Types::Html); 0436 } 0437 return QVariant::fromValue(Types::Plain); 0438 } 0439 case IsEmbeddedRole: 0440 return false; 0441 case IsErrorRole: 0442 return messagePart->error(); 0443 case ContentRole: 0444 return d->contentForPart(messagePart); 0445 case IsEncryptedRole: 0446 return messagePart->encryptionState() != MimeTreeParser::KMMsgNotEncrypted; 0447 case IsSignedRole: 0448 return messagePart->signatureState() != MimeTreeParser::KMMsgNotSigned; 0449 case SecurityLevelRole: { 0450 auto signature = messagePart->signatureState(); 0451 auto encryption = messagePart->encryptionState(); 0452 bool messageIsSigned = signature == MimeTreeParser::KMMsgPartiallySigned || signature == MimeTreeParser::KMMsgFullySigned; 0453 bool messageIsEncrypted = encryption == MimeTreeParser::KMMsgPartiallyEncrypted || encryption == MimeTreeParser::KMMsgFullyEncrypted; 0454 0455 if (messageIsSigned) { 0456 const auto sigInfo = signatureInfo(messagePart); 0457 if (!sigInfo.signatureIsGood) { 0458 if (sigInfo.keyMissing || sigInfo.keyExpired) { 0459 return SecurityLevel::NotSoGood; 0460 } 0461 return SecurityLevel::Bad; 0462 } 0463 } 0464 // All good 0465 if ((messageIsSigned || messageIsEncrypted) && !messagePart->error()) { 0466 return SecurityLevel::Good; 0467 } 0468 // No info 0469 return SecurityLevel::Unknow; 0470 } 0471 case EncryptionSecurityLevelRole: { 0472 auto encryption = messagePart->encryptionState(); 0473 bool messageIsEncrypted = encryption == MimeTreeParser::KMMsgPartiallyEncrypted || encryption == MimeTreeParser::KMMsgFullyEncrypted; 0474 if (messagePart->error()) { 0475 return SecurityLevel::Bad; 0476 } 0477 // All good 0478 if (messageIsEncrypted) { 0479 return SecurityLevel::Good; 0480 } 0481 // No info 0482 return SecurityLevel::Unknow; 0483 } 0484 case SignatureSecurityLevelRole: { 0485 auto signature = messagePart->signatureState(); 0486 bool messageIsSigned = signature == MimeTreeParser::KMMsgPartiallySigned || signature == MimeTreeParser::KMMsgFullySigned; 0487 if (messageIsSigned) { 0488 const auto sigInfo = signatureInfo(messagePart); 0489 if (!sigInfo.signatureIsGood) { 0490 if (sigInfo.keyMissing || sigInfo.keyExpired) { 0491 return SecurityLevel::NotSoGood; 0492 } 0493 return SecurityLevel::Bad; 0494 } 0495 return SecurityLevel::Good; 0496 } 0497 // No info 0498 return SecurityLevel::Unknow; 0499 } 0500 case SignatureDetails: 0501 return QVariant::fromValue(signatureInfo(messagePart)); 0502 case EncryptionDetails: 0503 return QVariant::fromValue(encryptionInfo(messagePart)); 0504 case ErrorType: 0505 return messagePart->error(); 0506 case ErrorString: { 0507 switch (messagePart->error()) { 0508 case MimeTreeParser::MessagePart::NoKeyError: { 0509 if (auto encryptedMessagePart = dynamic_cast<MimeTreeParser::EncryptedMessagePart *>(messagePart)) { 0510 if (encryptedMessagePart->isNoSecKey()) { 0511 QString errorMessage; 0512 if (encryptedMessagePart->cryptoProto() == QGpgME::smime()) { 0513 errorMessage += 0514 i18ndc("mimetreeparser", "@info:status", "This message cannot be decrypted with any S/MIME certificate in your keyring."); 0515 } else { 0516 errorMessage += 0517 i18ndc("mimetreeparser", "@info:status", "This message cannot be decrypted with any OpenPGP certificate in your keyring."); 0518 } 0519 if (!encryptedMessagePart->decryptRecipients().empty()) { 0520 errorMessage += QLatin1Char(' ') 0521 + i18ndcp("mimetreeparser", 0522 "@info:status", 0523 "The message is encrypted for the following certificate:", 0524 "The message is encrypted for the following certificates:", 0525 encryptedMessagePart->decryptRecipients().size()); 0526 errorMessage += 0527 MimeTreeParser::decryptRecipientsToHtml(encryptedMessagePart->decryptRecipients(), encryptedMessagePart->cryptoProto()); 0528 } 0529 return errorMessage; 0530 } 0531 } 0532 } 0533 0534 return messagePart->errorString(); 0535 0536 case MimeTreeParser::MessagePart::PassphraseError: 0537 return i18ndc("mimetreeparser", "@info:status", "Wrong passphrase."); 0538 case MimeTreeParser::MessagePart::UnknownError: 0539 break; 0540 default: 0541 break; 0542 } 0543 return messagePart->errorString(); 0544 } 0545 } 0546 } 0547 return QVariant(); 0548 } 0549 0550 QModelIndex PartModel::parent(const QModelIndex &index) const 0551 { 0552 if (index.isValid()) { 0553 if (auto indexPart = static_cast<MimeTreeParser::MessagePart *>(index.internalPointer())) { 0554 for (const auto &part : std::as_const(d->mParts)) { 0555 if (part.data() == indexPart) { 0556 return QModelIndex(); 0557 } 0558 } 0559 const auto parentPart = d->mParents[indexPart]; 0560 Q_ASSERT(parentPart); 0561 int row = 0; 0562 const auto parts = d->mEncapsulatedParts[parentPart]; 0563 for (const auto &part : parts) { 0564 if (part.data() == indexPart) { 0565 break; 0566 } 0567 row++; 0568 } 0569 return createIndex(row, 0, parentPart); 0570 } 0571 return {}; 0572 } 0573 return {}; 0574 } 0575 0576 int PartModel::rowCount(const QModelIndex &parent) const 0577 { 0578 if (parent.isValid()) { 0579 const auto part = static_cast<MimeTreeParser::MessagePart *>(parent.internalPointer()); 0580 auto encapsulatedPart = dynamic_cast<MimeTreeParser::EncapsulatedRfc822MessagePart *>(part); 0581 0582 if (encapsulatedPart) { 0583 const auto parts = d->mEncapsulatedParts[encapsulatedPart]; 0584 return parts.size(); 0585 } 0586 return 0; 0587 } 0588 return d->mParts.count(); 0589 } 0590 0591 int PartModel::columnCount(const QModelIndex &) const 0592 { 0593 return 1; 0594 } 0595 0596 #include "moc_partmodel.cpp"