1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
3
4#include "messagemodel.h"
5#include "statistics.h"
6
7#include <QtCore/QCoreApplication>
8#include <QtCore/QDebug>
9
10#include <QtWidgets/QMessageBox>
11#include <QtGui/QPainter>
12#include <QtGui/QPixmap>
13#include <QtGui/QTextDocument>
14
15#include <private/qtranslator_p.h>
16
17#include <limits.h>
18
19QT_BEGIN_NAMESPACE
20
21/******************************************************************************
22 *
23 * MessageItem
24 *
25 *****************************************************************************/
26
27MessageItem::MessageItem(const TranslatorMessage &message)
28 : m_message(message),
29 m_danger(false)
30{
31 if (m_message.translation().isEmpty())
32 m_message.setTranslation(QString());
33}
34
35
36bool MessageItem::compare(const QString &findText, bool matchSubstring,
37 Qt::CaseSensitivity cs) const
38{
39 return matchSubstring
40 ? text().indexOf(s: findText, from: 0, cs) >= 0
41 : text().compare(s: findText, cs) == 0;
42}
43
44/******************************************************************************
45 *
46 * ContextItem
47 *
48 *****************************************************************************/
49
50ContextItem::ContextItem(const QString &context)
51 : m_context(context),
52 m_finishedCount(0),
53 m_finishedDangerCount(0),
54 m_unfinishedDangerCount(0),
55 m_nonobsoleteCount(0)
56{}
57
58void ContextItem::appendToComment(const QString &str)
59{
60 if (!m_comment.isEmpty())
61 m_comment += QLatin1String("\n\n");
62 m_comment += str;
63}
64
65MessageItem *ContextItem::messageItem(int i) const
66{
67 if (i >= 0 && i < msgItemList.size())
68 return const_cast<MessageItem *>(&msgItemList[i]);
69 Q_ASSERT(i >= 0 && i < msgItemList.size());
70 return 0;
71}
72
73MessageItem *ContextItem::findMessage(const QString &sourcetext, const QString &comment) const
74{
75 for (int i = 0; i < messageCount(); ++i) {
76 MessageItem *mi = messageItem(i);
77 if (mi->text() == sourcetext && mi->comment() == comment)
78 return mi;
79 }
80 return 0;
81}
82
83/******************************************************************************
84 *
85 * DataModel
86 *
87 *****************************************************************************/
88
89DataModel::DataModel(QObject *parent)
90 : QObject(parent),
91 m_modified(false),
92 m_numMessages(0),
93 m_srcWords(0),
94 m_srcChars(0),
95 m_srcCharsSpc(0),
96 m_language(QLocale::Language(-1)),
97 m_sourceLanguage(QLocale::Language(-1)),
98 m_territory(QLocale::Territory(-1)),
99 m_sourceTerritory(QLocale::Territory(-1))
100{}
101
102QStringList DataModel::normalizedTranslations(const MessageItem &m) const
103{
104 return Translator::normalizedTranslations(m: m.message(), numPlurals: m_numerusForms.size());
105}
106
107ContextItem *DataModel::contextItem(int context) const
108{
109 if (context >= 0 && context < m_contextList.size())
110 return const_cast<ContextItem *>(&m_contextList[context]);
111 Q_ASSERT(context >= 0 && context < m_contextList.size());
112 return 0;
113}
114
115MessageItem *DataModel::messageItem(const DataIndex &index) const
116{
117 if (ContextItem *c = contextItem(context: index.context()))
118 return c->messageItem(i: index.message());
119 return 0;
120}
121
122ContextItem *DataModel::findContext(const QString &context) const
123{
124 for (int c = 0; c < m_contextList.size(); ++c) {
125 ContextItem *ctx = contextItem(context: c);
126 if (ctx->context() == context)
127 return ctx;
128 }
129 return 0;
130}
131
132MessageItem *DataModel::findMessage(const QString &context,
133 const QString &sourcetext, const QString &comment) const
134{
135 if (ContextItem *ctx = findContext(context))
136 return ctx->findMessage(sourcetext, comment);
137 return 0;
138}
139
140static int calcMergeScore(const DataModel *one, const DataModel *two)
141{
142 int inBoth = 0;
143 for (int i = 0; i < two->contextCount(); ++i) {
144 ContextItem *oc = two->contextItem(context: i);
145 if (ContextItem *c = one->findContext(context: oc->context())) {
146 for (int j = 0; j < oc->messageCount(); ++j) {
147 MessageItem *m = oc->messageItem(i: j);
148 if (c->findMessage(sourcetext: m->text(), comment: m->comment()))
149 ++inBoth;
150 }
151 }
152 }
153 return inBoth * 100 / two->messageCount();
154}
155
156bool DataModel::isWellMergeable(const DataModel *other) const
157{
158 if (!other->messageCount() || !messageCount())
159 return true;
160
161 return calcMergeScore(one: this, two: other) + calcMergeScore(one: other, two: this) > 90;
162}
163
164bool DataModel::load(const QString &fileName, bool *langGuessed, QWidget *parent)
165{
166 Translator tor;
167 ConversionData cd;
168 bool ok = tor.load(filename: fileName, err&: cd, format: QLatin1String("auto"));
169 if (!ok) {
170 QMessageBox::warning(parent, title: QObject::tr(s: "Qt Linguist"), text: cd.error());
171 return false;
172 }
173
174 if (!tor.messageCount()) {
175 QMessageBox::warning(parent, title: QObject::tr(s: "Qt Linguist"),
176 text: tr(s: "The translation file '%1' will not be loaded because it is empty.")
177 .arg(a: fileName.toHtmlEscaped()));
178 return false;
179 }
180
181 const Translator::Duplicates dupes = tor.resolveDuplicates();
182 if (!dupes.byId.isEmpty() || !dupes.byContents.isEmpty()) {
183 QString err = tr(s: "<qt>Duplicate messages found in '%1':").arg(a: fileName.toHtmlEscaped());
184 int numdups = 0;
185 for (auto it = dupes.byId.begin(); it != dupes.byId.end(); ++it) {
186 if (++numdups >= 5) {
187 err += tr(s: "<p>[more duplicates omitted]");
188 goto doWarn;
189 }
190 err += tr(s: "<p>* ID: %1").arg(a: tor.message(i: it.key()).id().toHtmlEscaped());
191 }
192 for (auto it = dupes.byContents.begin(); it != dupes.byContents.end(); ++it) {
193 const TranslatorMessage &msg = tor.message(i: it.key());
194 if (++numdups >= 5) {
195 err += tr(s: "<p>[more duplicates omitted]");
196 break;
197 }
198 err += tr(s: "<p>* Context: %1<br>* Source: %2")
199 .arg(args: msg.context().toHtmlEscaped(), args: msg.sourceText().toHtmlEscaped());
200 if (!msg.comment().isEmpty())
201 err += tr(s: "<br>* Comment: %3").arg(a: msg.comment().toHtmlEscaped());
202 }
203 doWarn:
204 QMessageBox::warning(parent, title: QObject::tr(s: "Qt Linguist"), text: err);
205 }
206
207 m_srcFileName = fileName;
208 m_relativeLocations = (tor.locationsType() == Translator::RelativeLocations);
209 m_extra = tor.extras();
210 m_contextList.clear();
211 m_numMessages = 0;
212
213 QHash<QString, int> contexts;
214
215 m_srcWords = 0;
216 m_srcChars = 0;
217 m_srcCharsSpc = 0;
218
219 for (const TranslatorMessage &msg : tor.messages()) {
220 if (!contexts.contains(key: msg.context())) {
221 contexts.insert(key: msg.context(), value: m_contextList.size());
222 m_contextList.append(t: ContextItem(msg.context()));
223 }
224
225 ContextItem *c = contextItem(context: contexts.value(key: msg.context()));
226 if (msg.sourceText() == QLatin1String(ContextComment)) {
227 c->appendToComment(str: msg.comment());
228 } else {
229 MessageItem tmp(msg);
230 if (msg.type() == TranslatorMessage::Finished)
231 c->incrementFinishedCount();
232 if (msg.type() == TranslatorMessage::Finished || msg.type() == TranslatorMessage::Unfinished) {
233 doCharCounting(text: tmp.text(), trW&: m_srcWords, trC&: m_srcChars, trCS&: m_srcCharsSpc);
234 doCharCounting(text: tmp.pluralText(), trW&: m_srcWords, trC&: m_srcChars, trCS&: m_srcCharsSpc);
235 c->incrementNonobsoleteCount();
236 }
237 c->appendMessage(msg: tmp);
238 ++m_numMessages;
239 }
240 }
241
242 // Try to detect the correct language in the following order
243 // 1. Look for the language attribute in the ts
244 // if that fails
245 // 2. Guestimate the language from the filename
246 // (expecting the qt_{en,de}.ts convention)
247 // if that fails
248 // 3. Retrieve the locale from the system.
249 *langGuessed = false;
250 QString lang = tor.languageCode();
251 if (lang.isEmpty()) {
252 lang = QFileInfo(fileName).baseName();
253 int pos = lang.indexOf(c: QLatin1Char('_'));
254 if (pos != -1)
255 lang.remove(i: 0, len: pos + 1);
256 else
257 lang.clear();
258 *langGuessed = true;
259 }
260 QLocale::Language l;
261 QLocale::Territory c;
262 Translator::languageAndTerritory(languageCode: lang, langPtr: &l, territoryPtr: &c);
263 if (l == QLocale::C) {
264 QLocale sys;
265 l = sys.language();
266 c = sys.territory();
267 *langGuessed = true;
268 }
269 if (!setLanguageAndTerritory(lang: l, territory: c))
270 QMessageBox::warning(parent, title: QObject::tr(s: "Qt Linguist"),
271 text: tr(s: "Linguist does not know the plural rules for '%1'.\n"
272 "Will assume a single universal form.")
273 .arg(a: m_localizedLanguage));
274 // Try to detect the correct source language in the following order
275 // 1. Look for the language attribute in the ts
276 // if that fails
277 // 2. Assume English
278 lang = tor.sourceLanguageCode();
279 if (lang.isEmpty()) {
280 l = QLocale::C;
281 c = QLocale::AnyTerritory;
282 } else {
283 Translator::languageAndTerritory(languageCode: lang, langPtr: &l, territoryPtr: &c);
284 }
285 setSourceLanguageAndTerritory(lang: l, territory: c);
286
287 setModified(false);
288
289 return true;
290}
291
292bool DataModel::save(const QString &fileName, QWidget *parent)
293{
294 Translator tor;
295 for (DataModelIterator it(this); it.isValid(); ++it)
296 tor.append(msg: it.current()->message());
297
298 tor.setLanguageCode(Translator::makeLanguageCode(language: m_language, territory: m_territory));
299 tor.setSourceLanguageCode(Translator::makeLanguageCode(language: m_sourceLanguage, territory: m_sourceTerritory));
300 tor.setLocationsType(m_relativeLocations ? Translator::RelativeLocations
301 : Translator::AbsoluteLocations);
302 tor.setExtras(m_extra);
303 ConversionData cd;
304 tor.normalizeTranslations(cd);
305 bool ok = tor.save(filename: fileName, err&: cd, format: QLatin1String("auto"));
306 if (ok)
307 setModified(false);
308 if (!cd.error().isEmpty())
309 QMessageBox::warning(parent, title: QObject::tr(s: "Qt Linguist"), text: cd.error());
310 return ok;
311}
312
313bool DataModel::saveAs(const QString &newFileName, QWidget *parent)
314{
315 if (!save(fileName: newFileName, parent))
316 return false;
317 m_srcFileName = newFileName;
318 return true;
319}
320
321bool DataModel::release(const QString &fileName, bool verbose, bool ignoreUnfinished,
322 TranslatorSaveMode mode, QWidget *parent)
323{
324 QFile file(fileName);
325 if (!file.open(flags: QIODevice::WriteOnly)) {
326 QMessageBox::warning(parent, title: QObject::tr(s: "Qt Linguist"),
327 text: tr(s: "Cannot create '%2': %1").arg(a: file.errorString()).arg(a: fileName));
328 return false;
329 }
330 Translator tor;
331 QLocale locale(m_language, m_territory);
332 tor.setLanguageCode(locale.name());
333 for (DataModelIterator it(this); it.isValid(); ++it)
334 tor.append(msg: it.current()->message());
335 ConversionData cd;
336 cd.m_verbose = verbose;
337 cd.m_ignoreUnfinished = ignoreUnfinished;
338 cd.m_saveMode = mode;
339 cd.m_idBased =
340 std::all_of(first: tor.messages().begin(), last: tor.messages().end(),
341 pred: [](const TranslatorMessage &message) { return !message.id().isEmpty(); });
342 bool ok = saveQM(translator: tor, dev&: file, cd);
343 if (!ok)
344 QMessageBox::warning(parent, title: QObject::tr(s: "Qt Linguist"), text: cd.error());
345 return ok;
346}
347
348void DataModel::doCharCounting(const QString &text, int &trW, int &trC, int &trCS)
349{
350 trCS += text.size();
351 bool inWord = false;
352 for (int i = 0; i < text.size(); ++i) {
353 if (text[i].isLetterOrNumber() || text[i] == QLatin1Char('_')) {
354 if (!inWord) {
355 ++trW;
356 inWord = true;
357 }
358 } else {
359 inWord = false;
360 }
361 if (!text[i].isSpace())
362 trC++;
363 }
364}
365
366bool DataModel::setLanguageAndTerritory(QLocale::Language lang, QLocale::Territory territory)
367{
368 if (m_language == lang && m_territory == territory)
369 return true;
370 m_language = lang;
371 m_territory = territory;
372
373 if (lang == QLocale::C || uint(lang) > uint(QLocale::LastLanguage)) // XXX does this make any sense?
374 lang = QLocale::English;
375 QByteArray rules;
376 bool ok = getNumerusInfo(language: lang, territory, rules: &rules, forms: &m_numerusForms, gettextRules: 0);
377 QLocale loc(lang, territory);
378 // Add territory name if we couldn't match the (lang, territory) combination,
379 // or if the language is used in more than one territory.
380 const bool mentionTerritory = (loc.territory() != territory) || [lang, territory]() {
381 const auto locales = QLocale::matchingLocales(language: lang, script: QLocale::AnyScript,
382 territory: QLocale::AnyTerritory);
383 return std::any_of(first: locales.cbegin(), last: locales.cend(), pred: [territory](const QLocale &locale) {
384 return locale.territory() != territory;
385 });
386 }();
387 m_localizedLanguage = mentionTerritory
388 //: <language> (<territory>)
389 ? tr(s: "%1 (%2)").arg(args: loc.nativeLanguageName(), args: loc.nativeTerritoryName())
390 : loc.nativeLanguageName();
391 m_countRefNeeds.clear();
392 for (int i = 0; i < rules.size(); ++i) {
393 m_countRefNeeds.append(t: !(rules.at(i) == Q_EQ && (i == (rules.size() - 2) || rules.at(i: i + 2) == (char)Q_NEWRULE)));
394 while (++i < rules.size() && rules.at(i) != (char)Q_NEWRULE) {}
395 }
396 m_countRefNeeds.append(t: true);
397 if (!ok) {
398 m_numerusForms.clear();
399 m_numerusForms << tr(s: "Universal Form");
400 }
401 emit languageChanged();
402 setModified(true);
403 return ok;
404}
405
406void DataModel::setSourceLanguageAndTerritory(QLocale::Language lang, QLocale::Territory territory)
407{
408 if (m_sourceLanguage == lang && m_sourceTerritory == territory)
409 return;
410 m_sourceLanguage = lang;
411 m_sourceTerritory = territory;
412 setModified(true);
413}
414
415void DataModel::updateStatistics()
416{
417 StatisticalData stats {};
418 for (DataModelIterator it(this); it.isValid(); ++it) {
419 const MessageItem *mi = it.current();
420 if (mi->isObsolete()) {
421 stats.obsoleteMsg++;
422 } else if (mi->isFinished()) {
423 bool hasDanger = false;
424 for (const QString &trnsl : mi->translations()) {
425 doCharCounting(text: trnsl, trW&: stats.wordsFinished, trC&: stats.charsFinished, trCS&: stats.charsSpacesFinished);
426 hasDanger |= mi->danger();
427 }
428 if (hasDanger)
429 stats.translatedMsgDanger++;
430 else
431 stats.translatedMsgNoDanger++;
432 } else if (mi->isUnfinished()) {
433 bool hasDanger = false;
434 for (const QString &trnsl : mi->translations()) {
435 doCharCounting(text: trnsl, trW&: stats.wordsUnfinished, trC&: stats.charsUnfinished, trCS&: stats.charsSpacesUnfinished);
436 hasDanger |= mi->danger();
437 }
438 if (hasDanger)
439 stats.unfinishedMsgDanger++;
440 else
441 stats.unfinishedMsgNoDanger++;
442 }
443 }
444 stats.wordsSource = m_srcWords;
445 stats.charsSource = m_srcChars;
446 stats.charsSpacesSource = m_srcCharsSpc;
447 emit statsChanged(newStats: stats);
448}
449
450void DataModel::setModified(bool isModified)
451{
452 if (m_modified == isModified)
453 return;
454 m_modified = isModified;
455 emit modifiedChanged();
456}
457
458QString DataModel::prettifyPlainFileName(const QString &fn)
459{
460 static QString workdir = QDir::currentPath() + QLatin1Char('/');
461
462 return QDir::toNativeSeparators(pathName: fn.startsWith(s: workdir) ? fn.mid(position: workdir.size()) : fn);
463}
464
465QString DataModel::prettifyFileName(const QString &fn)
466{
467 if (fn.startsWith(c: QLatin1Char('=')))
468 return QLatin1Char('=') + prettifyPlainFileName(fn: fn.mid(position: 1));
469 else
470 return prettifyPlainFileName(fn);
471}
472
473/******************************************************************************
474 *
475 * DataModelIterator
476 *
477 *****************************************************************************/
478
479DataModelIterator::DataModelIterator(DataModel *model, int context, int message)
480 : DataIndex(context, message), m_model(model)
481{
482}
483
484bool DataModelIterator::isValid() const
485{
486 return m_context < m_model->m_contextList.size();
487}
488
489void DataModelIterator::operator++()
490{
491 ++m_message;
492 if (m_message >= m_model->m_contextList.at(i: m_context).messageCount()) {
493 ++m_context;
494 m_message = 0;
495 }
496}
497
498MessageItem *DataModelIterator::current() const
499{
500 return m_model->messageItem(index: *this);
501}
502
503
504/******************************************************************************
505 *
506 * MultiMessageItem
507 *
508 *****************************************************************************/
509
510MultiMessageItem::MultiMessageItem(const MessageItem *m)
511 : m_id(m->id()),
512 m_text(m->text()),
513 m_pluralText(m->pluralText()),
514 m_comment(m->comment()),
515 m_nonnullCount(0),
516 m_nonobsoleteCount(0),
517 m_editableCount(0),
518 m_unfinishedCount(0)
519{
520}
521
522/******************************************************************************
523 *
524 * MultiContextItem
525 *
526 *****************************************************************************/
527
528MultiContextItem::MultiContextItem(int oldCount, ContextItem *ctx, bool writable)
529 : m_context(ctx->context()),
530 m_comment(ctx->comment()),
531 m_finishedCount(0),
532 m_editableCount(0),
533 m_nonobsoleteCount(0)
534{
535 QList<MessageItem *> mList;
536 QList<MessageItem *> eList;
537 for (int j = 0; j < ctx->messageCount(); ++j) {
538 MessageItem *m = ctx->messageItem(i: j);
539 mList.append(t: m);
540 eList.append(t: 0);
541 m_multiMessageList.append(t: MultiMessageItem(m));
542 }
543 for (int i = 0; i < oldCount; ++i) {
544 m_messageLists.append(t: eList);
545 m_writableMessageLists.append(t: 0);
546 m_contextList.append(t: 0);
547 }
548 m_messageLists.append(t: mList);
549 m_writableMessageLists.append(t: writable ? &m_messageLists.last() : 0);
550 m_contextList.append(t: ctx);
551}
552
553void MultiContextItem::appendEmptyModel()
554{
555 QList<MessageItem *> eList;
556 for (int j = 0; j < messageCount(); ++j)
557 eList.append(t: 0);
558 m_messageLists.append(t: eList);
559 m_writableMessageLists.append(t: 0);
560 m_contextList.append(t: 0);
561}
562
563void MultiContextItem::assignLastModel(ContextItem *ctx, bool writable)
564{
565 if (writable)
566 m_writableMessageLists.last() = &m_messageLists.last();
567 m_contextList.last() = ctx;
568}
569
570// XXX this is not needed, yet
571void MultiContextItem::moveModel(int oldPos, int newPos)
572{
573 m_contextList.insert(i: newPos, t: m_contextList[oldPos]);
574 m_messageLists.insert(i: newPos, t: m_messageLists[oldPos]);
575 m_writableMessageLists.insert(i: newPos, t: m_writableMessageLists[oldPos]);
576 removeModel(pos: oldPos < newPos ? oldPos : oldPos + 1);
577}
578
579void MultiContextItem::removeModel(int pos)
580{
581 m_contextList.removeAt(i: pos);
582 m_messageLists.removeAt(i: pos);
583 m_writableMessageLists.removeAt(i: pos);
584}
585
586void MultiContextItem::putMessageItem(int pos, MessageItem *m)
587{
588 m_messageLists.last()[pos] = m;
589}
590
591void MultiContextItem::appendMessageItems(const QList<MessageItem *> &m)
592{
593 QList<MessageItem *> nullItems = m; // Basically, just a reservation
594 for (int i = 0; i < nullItems.size(); ++i)
595 nullItems[i] = 0;
596 for (int i = 0; i < m_messageLists.size() - 1; ++i)
597 m_messageLists[i] += nullItems;
598 m_messageLists.last() += m;
599 for (MessageItem *mi : m)
600 m_multiMessageList.append(t: MultiMessageItem(mi));
601}
602
603void MultiContextItem::removeMultiMessageItem(int pos)
604{
605 for (int i = 0; i < m_messageLists.size(); ++i)
606 m_messageLists[i].removeAt(i: pos);
607 m_multiMessageList.removeAt(i: pos);
608}
609
610int MultiContextItem::firstNonobsoleteMessageIndex(int msgIdx) const
611{
612 for (int i = 0; i < m_messageLists.size(); ++i)
613 if (m_messageLists[i][msgIdx] && !m_messageLists[i][msgIdx]->isObsolete())
614 return i;
615 return -1;
616}
617
618int MultiContextItem::findMessage(const QString &sourcetext, const QString &comment) const
619{
620 for (int i = 0, cnt = messageCount(); i < cnt; ++i) {
621 MultiMessageItem *m = multiMessageItem(msgIdx: i);
622 if (m->text() == sourcetext && m->comment() == comment)
623 return i;
624 }
625 return -1;
626}
627
628int MultiContextItem::findMessageById(const QString &id) const
629{
630 for (int i = 0, cnt = messageCount(); i < cnt; ++i) {
631 MultiMessageItem *m = multiMessageItem(msgIdx: i);
632 if (m->id() == id)
633 return i;
634 }
635 return -1;
636}
637
638/******************************************************************************
639 *
640 * MultiDataModel
641 *
642 *****************************************************************************/
643
644static const uchar paletteRGBs[7][3] = {
645 { 236, 244, 255 }, // blue
646 { 236, 255, 255 }, // cyan
647 { 236, 255, 232 }, // green
648 { 255, 255, 230 }, // yellow
649 { 255, 242, 222 }, // orange
650 { 255, 236, 236 }, // red
651 { 252, 236, 255 } // purple
652};
653
654MultiDataModel::MultiDataModel(QObject *parent) :
655 QObject(parent),
656 m_numFinished(0),
657 m_numEditable(0),
658 m_numMessages(0),
659 m_modified(false)
660{
661 for (int i = 0; i < 7; ++i)
662 m_colors[i] = QColor(paletteRGBs[i][0], paletteRGBs[i][1], paletteRGBs[i][2]);
663
664 m_bitmap = QBitmap(8, 8);
665 m_bitmap.clear();
666 QPainter p(&m_bitmap);
667 for (int j = 0; j < 8; ++j)
668 for (int k = 0; k < 8; ++k)
669 if ((j + k) & 4)
670 p.drawPoint(x: j, y: k);
671}
672
673MultiDataModel::~MultiDataModel()
674{
675 qDeleteAll(c: m_dataModels);
676}
677
678QBrush MultiDataModel::brushForModel(int model) const
679{
680 QBrush brush(m_colors[model % 7]);
681 if (!isModelWritable(model))
682 brush.setTexture(m_bitmap);
683 return brush;
684}
685
686bool MultiDataModel::isWellMergeable(const DataModel *dm) const
687{
688 if (!dm->messageCount() || !messageCount())
689 return true;
690
691 int inBothNew = 0;
692 for (int i = 0; i < dm->contextCount(); ++i) {
693 ContextItem *c = dm->contextItem(context: i);
694 if (MultiContextItem *mc = findContext(context: c->context())) {
695 for (int j = 0; j < c->messageCount(); ++j) {
696 MessageItem *m = c->messageItem(i: j);
697 if (mc->findMessage(sourcetext: m->text(), comment: m->comment()) >= 0)
698 ++inBothNew;
699 }
700 }
701 }
702 int newRatio = inBothNew * 100 / dm->messageCount();
703
704 int inBothOld = 0;
705 for (int k = 0; k < contextCount(); ++k) {
706 MultiContextItem *mc = multiContextItem(ctxIdx: k);
707 if (ContextItem *c = dm->findContext(context: mc->context())) {
708 for (int j = 0; j < mc->messageCount(); ++j) {
709 MultiMessageItem *m = mc->multiMessageItem(msgIdx: j);
710 if (c->findMessage(sourcetext: m->text(), comment: m->comment()))
711 ++inBothOld;
712 }
713 }
714 }
715 int oldRatio = inBothOld * 100 / messageCount();
716
717 return newRatio + oldRatio > 90;
718}
719
720void MultiDataModel::append(DataModel *dm, bool readWrite)
721{
722 int insCol = modelCount() + 1;
723 m_msgModel->beginInsertColumns(parent: QModelIndex(), first: insCol, last: insCol);
724 m_dataModels.append(t: dm);
725 for (int j = 0; j < contextCount(); ++j) {
726 m_msgModel->beginInsertColumns(parent: m_msgModel->createIndex(arow: j, acolumn: 0), first: insCol, last: insCol);
727 m_multiContextList[j].appendEmptyModel();
728 m_msgModel->endInsertColumns();
729 }
730 m_msgModel->endInsertColumns();
731 int appendedContexts = 0;
732 for (int i = 0; i < dm->contextCount(); ++i) {
733 ContextItem *c = dm->contextItem(context: i);
734 int mcx = findContextIndex(context: c->context());
735 if (mcx >= 0) {
736 MultiContextItem *mc = multiContextItem(ctxIdx: mcx);
737 mc->assignLastModel(ctx: c, writable: readWrite);
738 QList<MessageItem *> appendItems;
739 for (int j = 0; j < c->messageCount(); ++j) {
740 MessageItem *m = c->messageItem(i: j);
741
742 int msgIdx = -1;
743 if (!m->id().isEmpty()) // id based translation
744 msgIdx = mc->findMessageById(id: m->id());
745
746 if (msgIdx == -1)
747 msgIdx = mc->findMessage(sourcetext: m->text(), comment: m->comment());
748
749 if (msgIdx >= 0)
750 mc->putMessageItem(pos: msgIdx, m);
751 else
752 appendItems << m;
753 }
754 if (!appendItems.isEmpty()) {
755 int msgCnt = mc->messageCount();
756 m_msgModel->beginInsertRows(parent: m_msgModel->createIndex(arow: mcx, acolumn: 0),
757 first: msgCnt, last: msgCnt + appendItems.size() - 1);
758 mc->appendMessageItems(m: appendItems);
759 m_msgModel->endInsertRows();
760 m_numMessages += appendItems.size();
761 }
762 } else {
763 m_multiContextList << MultiContextItem(modelCount() - 1, c, readWrite);
764 m_numMessages += c->messageCount();
765 ++appendedContexts;
766 }
767 }
768 if (appendedContexts) {
769 // Do that en block to avoid itemview inefficiency. It doesn't hurt that we
770 // announce the availability of the data "long" after it was actually added.
771 m_msgModel->beginInsertRows(parent: QModelIndex(),
772 first: contextCount() - appendedContexts, last: contextCount() - 1);
773 m_msgModel->endInsertRows();
774 }
775 dm->setWritable(readWrite);
776 updateCountsOnAdd(model: modelCount() - 1, writable: readWrite);
777 connect(sender: dm, signal: &DataModel::modifiedChanged,
778 context: this, slot: &MultiDataModel::onModifiedChanged);
779 connect(sender: dm, signal: &DataModel::languageChanged,
780 context: this, slot: &MultiDataModel::onLanguageChanged);
781 connect(sender: dm, signal: &DataModel::statsChanged,
782 context: this, slot: &MultiDataModel::statsChanged);
783 emit modelAppended();
784}
785
786void MultiDataModel::close(int model)
787{
788 if (m_dataModels.size() == 1) {
789 closeAll();
790 } else {
791 updateCountsOnRemove(model, writable: isModelWritable(model));
792 int delCol = model + 1;
793 m_msgModel->beginRemoveColumns(parent: QModelIndex(), first: delCol, last: delCol);
794 for (int i = m_multiContextList.size(); --i >= 0;) {
795 m_msgModel->beginRemoveColumns(parent: m_msgModel->createIndex(arow: i, acolumn: 0), first: delCol, last: delCol);
796 m_multiContextList[i].removeModel(pos: model);
797 m_msgModel->endRemoveColumns();
798 }
799 delete m_dataModels.takeAt(i: model);
800 m_msgModel->endRemoveColumns();
801 emit modelDeleted(model);
802 for (int i = m_multiContextList.size(); --i >= 0;) {
803 MultiContextItem &mc = m_multiContextList[i];
804 QModelIndex contextIdx = m_msgModel->createIndex(arow: i, acolumn: 0);
805 for (int j = mc.messageCount(); --j >= 0;)
806 if (mc.multiMessageItem(msgIdx: j)->isEmpty()) {
807 m_msgModel->beginRemoveRows(parent: contextIdx, first: j, last: j);
808 mc.removeMultiMessageItem(pos: j);
809 m_msgModel->endRemoveRows();
810 --m_numMessages;
811 }
812 if (!mc.messageCount()) {
813 m_msgModel->beginRemoveRows(parent: QModelIndex(), first: i, last: i);
814 m_multiContextList.removeAt(i);
815 m_msgModel->endRemoveRows();
816 }
817 }
818 onModifiedChanged();
819 }
820}
821
822void MultiDataModel::closeAll()
823{
824 m_msgModel->beginResetModel();
825 m_numFinished = 0;
826 m_numEditable = 0;
827 m_numMessages = 0;
828 qDeleteAll(c: m_dataModels);
829 m_dataModels.clear();
830 m_multiContextList.clear();
831 m_msgModel->endResetModel();
832 emit allModelsDeleted();
833 onModifiedChanged();
834}
835
836// XXX this is not needed, yet
837void MultiDataModel::moveModel(int oldPos, int newPos)
838{
839 int delPos = oldPos < newPos ? oldPos : oldPos + 1;
840 m_dataModels.insert(i: newPos, t: m_dataModels[oldPos]);
841 m_dataModels.removeAt(i: delPos);
842 for (int i = 0; i < m_multiContextList.size(); ++i)
843 m_multiContextList[i].moveModel(oldPos, newPos);
844}
845
846QStringList MultiDataModel::prettifyFileNames(const QStringList &names)
847{
848 QStringList out;
849
850 for (const QString &name : names)
851 out << DataModel::prettifyFileName(fn: name);
852 return out;
853}
854
855QString MultiDataModel::condenseFileNames(const QStringList &names)
856{
857 if (names.isEmpty())
858 return QString();
859
860 if (names.size() < 2)
861 return names.first();
862
863 QString prefix = names.first();
864 if (prefix.startsWith(c: QLatin1Char('=')))
865 prefix.remove(i: 0, len: 1);
866 QString suffix = prefix;
867 for (int i = 1; i < names.size(); ++i) {
868 QString fn = names[i];
869 if (fn.startsWith(c: QLatin1Char('=')))
870 fn.remove(i: 0, len: 1);
871 for (int j = 0; j < prefix.size(); ++j)
872 if (fn[j] != prefix[j]) {
873 if (j < prefix.size()) {
874 while (j > 0 && prefix[j - 1].isLetterOrNumber())
875 --j;
876 prefix.truncate(pos: j);
877 }
878 break;
879 }
880 int fnl = fn.size() - 1;
881 int sxl = suffix.size() - 1;
882 for (int k = 0; k <= sxl; ++k)
883 if (fn[fnl - k] != suffix[sxl - k]) {
884 if (k < sxl) {
885 while (k > 0 && suffix[sxl - k + 1].isLetterOrNumber())
886 --k;
887 if (prefix.size() + k > fnl)
888 --k;
889 suffix.remove(i: 0, len: sxl - k + 1);
890 }
891 break;
892 }
893 }
894 QString ret = prefix + QLatin1Char('{');
895 int pxl = prefix.size();
896 int sxl = suffix.size();
897 for (int j = 0; j < names.size(); ++j) {
898 if (j)
899 ret += QLatin1Char(',');
900 int off = pxl;
901 QString fn = names[j];
902 if (fn.startsWith(c: QLatin1Char('='))) {
903 ret += QLatin1Char('=');
904 ++off;
905 }
906 ret += fn.mid(position: off, n: fn.size() - sxl - off);
907 }
908 ret += QLatin1Char('}') + suffix;
909 return ret;
910}
911
912QStringList MultiDataModel::srcFileNames(bool pretty) const
913{
914 QStringList names;
915 for (DataModel *dm : m_dataModels)
916 names << (dm->isWritable() ? QString() : QString::fromLatin1(ba: "=")) + dm->srcFileName(pretty);
917 return names;
918}
919
920QString MultiDataModel::condensedSrcFileNames(bool pretty) const
921{
922 return condenseFileNames(names: srcFileNames(pretty));
923}
924
925bool MultiDataModel::isModified() const
926{
927 for (const DataModel *mdl : m_dataModels)
928 if (mdl->isModified())
929 return true;
930 return false;
931}
932
933void MultiDataModel::onModifiedChanged()
934{
935 bool modified = isModified();
936 if (modified != m_modified) {
937 emit modifiedChanged(modified);
938 m_modified = modified;
939 }
940}
941
942void MultiDataModel::onLanguageChanged()
943{
944 int i = 0;
945 while (sender() != m_dataModels[i])
946 ++i;
947 emit languageChanged(model: i);
948}
949
950int MultiDataModel::isFileLoaded(const QString &name) const
951{
952 for (int i = 0; i < m_dataModels.size(); ++i)
953 if (m_dataModels[i]->srcFileName() == name)
954 return i;
955 return -1;
956}
957
958int MultiDataModel::findContextIndex(const QString &context) const
959{
960 for (int i = 0; i < m_multiContextList.size(); ++i) {
961 const MultiContextItem &mc = m_multiContextList[i];
962 if (mc.context() == context)
963 return i;
964 }
965 return -1;
966}
967
968MultiContextItem *MultiDataModel::findContext(const QString &context) const
969{
970 for (int i = 0; i < m_multiContextList.size(); ++i) {
971 const MultiContextItem &mc = m_multiContextList[i];
972 if (mc.context() == context)
973 return const_cast<MultiContextItem *>(&mc);
974 }
975 return 0;
976}
977
978MessageItem *MultiDataModel::messageItem(const MultiDataIndex &index, int model) const
979{
980 if (index.context() < contextCount() && model >= 0 && model < modelCount()) {
981 MultiContextItem *mc = multiContextItem(ctxIdx: index.context());
982 if (index.message() < mc->messageCount())
983 return mc->messageItem(model, msgIdx: index.message());
984 }
985 Q_ASSERT(model >= 0 && model < modelCount());
986 Q_ASSERT(index.context() < contextCount());
987 return 0;
988}
989
990void MultiDataModel::setTranslation(const MultiDataIndex &index, const QString &translation)
991{
992 MessageItem *m = messageItem(index);
993 if (translation == m->translation())
994 return;
995 m->setTranslation(translation);
996 setModified(model: index.model(), dirty: true);
997 emit translationChanged(index);
998}
999
1000void MultiDataModel::setFinished(const MultiDataIndex &index, bool finished)
1001{
1002 MultiContextItem *mc = multiContextItem(ctxIdx: index.context());
1003 MultiMessageItem *mm = mc->multiMessageItem(msgIdx: index.message());
1004 ContextItem *c = contextItem(index);
1005 MessageItem *m = messageItem(index);
1006 TranslatorMessage::Type type = m->type();
1007 if (type == TranslatorMessage::Unfinished && finished) {
1008 m->setType(TranslatorMessage::Finished);
1009 mm->decrementUnfinishedCount();
1010 if (!mm->countUnfinished()) {
1011 incrementFinishedCount();
1012 mc->incrementFinishedCount();
1013 emit multiContextDataChanged(index);
1014 }
1015 c->incrementFinishedCount();
1016 if (m->danger()) {
1017 c->incrementFinishedDangerCount();
1018 c->decrementUnfinishedDangerCount();
1019 if (!c->unfinishedDangerCount()
1020 || c->finishedCount() == c->nonobsoleteCount())
1021 emit contextDataChanged(index);
1022 } else if (c->finishedCount() == c->nonobsoleteCount()) {
1023 emit contextDataChanged(index);
1024 }
1025 emit messageDataChanged(index);
1026 setModified(model: index.model(), dirty: true);
1027 } else if (type == TranslatorMessage::Finished && !finished) {
1028 m->setType(TranslatorMessage::Unfinished);
1029 mm->incrementUnfinishedCount();
1030 if (mm->countUnfinished() == 1) {
1031 decrementFinishedCount();
1032 mc->decrementFinishedCount();
1033 emit multiContextDataChanged(index);
1034 }
1035 c->decrementFinishedCount();
1036 if (m->danger()) {
1037 c->decrementFinishedDangerCount();
1038 c->incrementUnfinishedDangerCount();
1039 if (c->unfinishedDangerCount() == 1
1040 || c->finishedCount() + 1 == c->nonobsoleteCount())
1041 emit contextDataChanged(index);
1042 } else if (c->finishedCount() + 1 == c->nonobsoleteCount()) {
1043 emit contextDataChanged(index);
1044 }
1045 emit messageDataChanged(index);
1046 setModified(model: index.model(), dirty: true);
1047 }
1048}
1049
1050void MultiDataModel::setDanger(const MultiDataIndex &index, bool danger)
1051{
1052 ContextItem *c = contextItem(index);
1053 MessageItem *m = messageItem(index);
1054 if (!m->danger() && danger) {
1055 if (m->isFinished()) {
1056 c->incrementFinishedDangerCount();
1057 if (c->finishedDangerCount() == 1)
1058 emit contextDataChanged(index);
1059 } else {
1060 c->incrementUnfinishedDangerCount();
1061 if (c->unfinishedDangerCount() == 1)
1062 emit contextDataChanged(index);
1063 }
1064 emit messageDataChanged(index);
1065 m->setDanger(danger);
1066 } else if (m->danger() && !danger) {
1067 if (m->isFinished()) {
1068 c->decrementFinishedDangerCount();
1069 if (!c->finishedDangerCount())
1070 emit contextDataChanged(index);
1071 } else {
1072 c->decrementUnfinishedDangerCount();
1073 if (!c->unfinishedDangerCount())
1074 emit contextDataChanged(index);
1075 }
1076 emit messageDataChanged(index);
1077 m->setDanger(danger);
1078 }
1079}
1080
1081void MultiDataModel::updateCountsOnAdd(int model, bool writable)
1082{
1083 for (int i = 0; i < m_multiContextList.size(); ++i) {
1084 MultiContextItem &mc = m_multiContextList[i];
1085 for (int j = 0; j < mc.messageCount(); ++j)
1086 if (MessageItem *m = mc.messageItem(model, msgIdx: j)) {
1087 MultiMessageItem *mm = mc.multiMessageItem(msgIdx: j);
1088 mm->incrementNonnullCount();
1089 if (!m->isObsolete()) {
1090 if (writable) {
1091 if (!mm->countEditable()) {
1092 mc.incrementEditableCount();
1093 incrementEditableCount();
1094 if (m->isFinished()) {
1095 mc.incrementFinishedCount();
1096 incrementFinishedCount();
1097 } else {
1098 mm->incrementUnfinishedCount();
1099 }
1100 } else if (!m->isFinished()) {
1101 if (!mm->isUnfinished()) {
1102 mc.decrementFinishedCount();
1103 decrementFinishedCount();
1104 }
1105 mm->incrementUnfinishedCount();
1106 }
1107 mm->incrementEditableCount();
1108 }
1109 mc.incrementNonobsoleteCount();
1110 mm->incrementNonobsoleteCount();
1111 }
1112 }
1113 }
1114}
1115
1116void MultiDataModel::updateCountsOnRemove(int model, bool writable)
1117{
1118 for (int i = 0; i < m_multiContextList.size(); ++i) {
1119 MultiContextItem &mc = m_multiContextList[i];
1120 for (int j = 0; j < mc.messageCount(); ++j)
1121 if (MessageItem *m = mc.messageItem(model, msgIdx: j)) {
1122 MultiMessageItem *mm = mc.multiMessageItem(msgIdx: j);
1123 mm->decrementNonnullCount();
1124 if (!m->isObsolete()) {
1125 mm->decrementNonobsoleteCount();
1126 mc.decrementNonobsoleteCount();
1127 if (writable) {
1128 mm->decrementEditableCount();
1129 if (!mm->countEditable()) {
1130 mc.decrementEditableCount();
1131 decrementEditableCount();
1132 if (m->isFinished()) {
1133 mc.decrementFinishedCount();
1134 decrementFinishedCount();
1135 } else {
1136 mm->decrementUnfinishedCount();
1137 }
1138 } else if (!m->isFinished()) {
1139 mm->decrementUnfinishedCount();
1140 if (!mm->isUnfinished()) {
1141 mc.incrementFinishedCount();
1142 incrementFinishedCount();
1143 }
1144 }
1145 }
1146 }
1147 }
1148 }
1149}
1150
1151/******************************************************************************
1152 *
1153 * MultiDataModelIterator
1154 *
1155 *****************************************************************************/
1156
1157MultiDataModelIterator::MultiDataModelIterator(MultiDataModel *dataModel, int model, int context, int message)
1158 : MultiDataIndex(model, context, message), m_dataModel(dataModel)
1159{
1160}
1161
1162void MultiDataModelIterator::operator++()
1163{
1164 Q_ASSERT(isValid());
1165 ++m_message;
1166 if (m_message >= m_dataModel->m_multiContextList.at(i: m_context).messageCount()) {
1167 ++m_context;
1168 m_message = 0;
1169 }
1170}
1171
1172bool MultiDataModelIterator::isValid() const
1173{
1174 return m_context < m_dataModel->m_multiContextList.size();
1175}
1176
1177MessageItem *MultiDataModelIterator::current() const
1178{
1179 return m_dataModel->messageItem(index: *this);
1180}
1181
1182
1183/******************************************************************************
1184 *
1185 * MessageModel
1186 *
1187 *****************************************************************************/
1188
1189MessageModel::MessageModel(QObject *parent, MultiDataModel *data)
1190 : QAbstractItemModel(parent), m_data(data)
1191{
1192 data->m_msgModel = this;
1193 connect(sender: m_data, signal: &MultiDataModel::multiContextDataChanged,
1194 context: this, slot: &MessageModel::multiContextItemChanged);
1195 connect(sender: m_data, signal: &MultiDataModel::contextDataChanged,
1196 context: this, slot: &MessageModel::contextItemChanged);
1197 connect(sender: m_data, signal: &MultiDataModel::messageDataChanged,
1198 context: this, slot: &MessageModel::messageItemChanged);
1199}
1200
1201QModelIndex MessageModel::index(int row, int column, const QModelIndex &parent) const
1202{
1203 if (!parent.isValid())
1204 return createIndex(arow: row, acolumn: column);
1205 if (!parent.internalId())
1206 return createIndex(arow: row, acolumn: column, aid: parent.row() + 1);
1207 return QModelIndex();
1208}
1209
1210QModelIndex MessageModel::parent(const QModelIndex& index) const
1211{
1212 if (index.internalId())
1213 return createIndex(arow: index.internalId() - 1, acolumn: 0);
1214 return QModelIndex();
1215}
1216
1217void MessageModel::multiContextItemChanged(const MultiDataIndex &index)
1218{
1219 QModelIndex idx = createIndex(arow: index.context(), acolumn: m_data->modelCount() + 2);
1220 emit dataChanged(topLeft: idx, bottomRight: idx);
1221}
1222
1223void MessageModel::contextItemChanged(const MultiDataIndex &index)
1224{
1225 QModelIndex idx = createIndex(arow: index.context(), acolumn: index.model() + 1);
1226 emit dataChanged(topLeft: idx, bottomRight: idx);
1227}
1228
1229void MessageModel::messageItemChanged(const MultiDataIndex &index)
1230{
1231 QModelIndex idx = createIndex(arow: index.message(), acolumn: index.model() + 1, aid: index.context() + 1);
1232 emit dataChanged(topLeft: idx, bottomRight: idx);
1233}
1234
1235QModelIndex MessageModel::modelIndex(const MultiDataIndex &index)
1236{
1237 if (index.message() < 0) // Should be unused case
1238 return createIndex(arow: index.context(), acolumn: index.model() + 1);
1239 return createIndex(arow: index.message(), acolumn: index.model() + 1, aid: index.context() + 1);
1240}
1241
1242int MessageModel::rowCount(const QModelIndex &parent) const
1243{
1244 if (!parent.isValid())
1245 return m_data->contextCount(); // contexts
1246 if (!parent.internalId()) // messages
1247 return m_data->multiContextItem(ctxIdx: parent.row())->messageCount();
1248 return 0;
1249}
1250
1251int MessageModel::columnCount(const QModelIndex &parent) const
1252{
1253 if (!parent.isValid())
1254 return m_data->modelCount() + 3;
1255 return m_data->modelCount() + 2;
1256}
1257
1258QVariant MessageModel::data(const QModelIndex &index, int role) const
1259{
1260 static QVariant pxOn =
1261 QVariant::fromValue(value: QPixmap(QLatin1String(":/images/s_check_on.png")));
1262 static QVariant pxOff =
1263 QVariant::fromValue(value: QPixmap(QLatin1String(":/images/s_check_off.png")));
1264 static QVariant pxObsolete =
1265 QVariant::fromValue(value: QPixmap(QLatin1String(":/images/s_check_obsolete.png")));
1266 static QVariant pxDanger =
1267 QVariant::fromValue(value: QPixmap(QLatin1String(":/images/s_check_danger.png")));
1268 static QVariant pxWarning =
1269 QVariant::fromValue(value: QPixmap(QLatin1String(":/images/s_check_warning.png")));
1270 static QVariant pxEmpty =
1271 QVariant::fromValue(value: QPixmap(QLatin1String(":/images/s_check_empty.png")));
1272
1273 int row = index.row();
1274 int column = index.column() - 1;
1275 if (column < 0)
1276 return QVariant();
1277
1278 int numLangs = m_data->modelCount();
1279
1280 if (role == Qt::ToolTipRole && column < numLangs) {
1281 return tr(s: "Completion status for %1").arg(a: m_data->model(i: column)->localizedLanguage());
1282 } else if (index.internalId()) {
1283 // this is a message
1284 int crow = index.internalId() - 1;
1285 MultiContextItem *mci = m_data->multiContextItem(ctxIdx: crow);
1286 if (row >= mci->messageCount() || !index.isValid())
1287 return QVariant();
1288
1289 if (role == Qt::DisplayRole || (role == Qt::ToolTipRole && column == numLangs)) {
1290 switch (column - numLangs) {
1291 case 0: // Source text
1292 {
1293 MultiMessageItem *msgItem = mci->multiMessageItem(msgIdx: row);
1294 if (msgItem->text().isEmpty()) {
1295 if (mci->context().isEmpty())
1296 return tr(s: "<file header>");
1297 else
1298 return tr(s: "<context comment>");
1299 }
1300 return msgItem->text().simplified();
1301 }
1302 default: // Status or dummy column => no text
1303 return QVariant();
1304 }
1305 }
1306 else if (role == Qt::DecorationRole && column < numLangs) {
1307 if (MessageItem *msgItem = mci->messageItem(model: column, msgIdx: row)) {
1308 switch (msgItem->message().type()) {
1309 case TranslatorMessage::Unfinished:
1310 if (msgItem->translation().isEmpty())
1311 return pxEmpty;
1312 if (msgItem->danger())
1313 return pxDanger;
1314 return pxOff;
1315 case TranslatorMessage::Finished:
1316 if (msgItem->danger())
1317 return pxWarning;
1318 return pxOn;
1319 default:
1320 return pxObsolete;
1321 }
1322 }
1323 return QVariant();
1324 }
1325 else if (role == SortRole) {
1326 switch (column - numLangs) {
1327 case 0: // Source text
1328 return mci->multiMessageItem(msgIdx: row)->text().simplified().remove(c: QLatin1Char('&'));
1329 case 1: // Dummy column
1330 return QVariant();
1331 default:
1332 if (MessageItem *msgItem = mci->messageItem(model: column, msgIdx: row)) {
1333 int rslt = !msgItem->translation().isEmpty();
1334 if (!msgItem->danger())
1335 rslt |= 2;
1336 if (msgItem->isObsolete())
1337 rslt |= 8;
1338 else if (msgItem->isFinished())
1339 rslt |= 4;
1340 return rslt;
1341 }
1342 return INT_MAX;
1343 }
1344 }
1345 else if (role == Qt::ForegroundRole && column > 0
1346 && mci->multiMessageItem(msgIdx: row)->isObsolete()) {
1347 return QBrush(Qt::darkGray);
1348 }
1349 else if (role == Qt::ForegroundRole && column == numLangs
1350 && mci->multiMessageItem(msgIdx: row)->text().isEmpty()) {
1351 return QBrush(QColor(0, 0xa0, 0xa0));
1352 }
1353 else if (role == Qt::BackgroundRole) {
1354 if (column < numLangs && numLangs != 1)
1355 return m_data->brushForModel(model: column);
1356 }
1357 } else {
1358 // this is a context
1359 if (row >= m_data->contextCount() || !index.isValid())
1360 return QVariant();
1361
1362 MultiContextItem *mci = m_data->multiContextItem(ctxIdx: row);
1363
1364 if (role == Qt::DisplayRole || role == Qt::ToolTipRole) {
1365 switch (column - numLangs) {
1366 case 0: // Context
1367 {
1368 if (mci->context().isEmpty())
1369 return tr(s: "<unnamed context>");
1370 return mci->context().simplified();
1371 }
1372 case 1:
1373 {
1374 if (role == Qt::ToolTipRole) {
1375 return tr(s: "%n unfinished message(s) left.", c: 0,
1376 n: mci->getNumEditable() - mci->getNumFinished());
1377 }
1378 return QString::asprintf(format: "%d/%d", mci->getNumFinished(), mci->getNumEditable());
1379 }
1380 default:
1381 return QVariant(); // Status => no text
1382 }
1383 }
1384 else if (role == Qt::DecorationRole && column < numLangs) {
1385 if (ContextItem *contextItem = mci->contextItem(model: column)) {
1386 if (contextItem->isObsolete())
1387 return pxObsolete;
1388 if (contextItem->isFinished())
1389 return contextItem->finishedDangerCount() > 0 ? pxWarning : pxOn;
1390 return contextItem->unfinishedDangerCount() > 0 ? pxDanger : pxOff;
1391 }
1392 return QVariant();
1393 }
1394 else if (role == SortRole) {
1395 switch (column - numLangs) {
1396 case 0: // Context (same as display role)
1397 return mci->context().simplified();
1398 case 1: // Items
1399 return mci->getNumEditable();
1400 default: // Percent
1401 if (ContextItem *contextItem = mci->contextItem(model: column)) {
1402 int totalItems = contextItem->nonobsoleteCount();
1403 int percent = totalItems ? (100 * contextItem->finishedCount()) / totalItems : 100;
1404 int rslt = percent * (((1 << 28) - 1) / 100) + totalItems;
1405 if (contextItem->isObsolete()) {
1406 rslt |= (1 << 30);
1407 } else if (contextItem->isFinished()) {
1408 rslt |= (1 << 29);
1409 if (!contextItem->finishedDangerCount())
1410 rslt |= (1 << 28);
1411 } else {
1412 if (!contextItem->unfinishedDangerCount())
1413 rslt |= (1 << 28);
1414 }
1415 return rslt;
1416 }
1417 return INT_MAX;
1418 }
1419 }
1420 else if (role == Qt::ForegroundRole && column >= numLangs
1421 && m_data->multiContextItem(ctxIdx: row)->isObsolete()) {
1422 return QBrush(Qt::darkGray);
1423 }
1424 else if (role == Qt::ForegroundRole && column == numLangs
1425 && m_data->multiContextItem(ctxIdx: row)->context().isEmpty()) {
1426 return QBrush(QColor(0, 0xa0, 0xa0));
1427 }
1428 else if (role == Qt::BackgroundRole) {
1429 if (column < numLangs && numLangs != 1) {
1430 QBrush brush = m_data->brushForModel(model: column);
1431 if (row & 1) {
1432 brush.setColor(brush.color().darker(f: 108));
1433 }
1434 return brush;
1435 }
1436 }
1437 }
1438 return QVariant();
1439}
1440
1441MultiDataIndex MessageModel::dataIndex(const QModelIndex &index, int model) const
1442{
1443 Q_ASSERT(index.isValid());
1444 Q_ASSERT(index.internalId());
1445 return MultiDataIndex(model, index.internalId() - 1, index.row());
1446}
1447
1448QT_END_NAMESPACE
1449

source code of qttools/src/linguist/linguist/messagemodel.cpp