Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
messagemodel.cpp
Go to the documentation of this file.
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
6#include "globals.h"
7#include "statistics.h"
8
9#include <QtCore/QCoreApplication>
10#include <QtCore/QDebug>
11
12#include <QtWidgets/QMessageBox>
13#include <QtGui/QPainter>
14#include <QtGui/QPixmap>
15#include <QtGui/QTextDocument>
16
17#include <private/qtranslator_p.h>
18
19#include <limits.h>
20
21using namespace Qt::Literals::StringLiterals;
22
23static QString resolveNcr(QStringView str)
24{
25 constexpr QStringView notation = u"&#";
26 constexpr QChar cx = u'x';
27 constexpr QChar ce = u';';
28
29 QString result;
30 result.reserve(str.size());
31 qsizetype offset = str.indexOf(notation);
32 while (offset >= 0) {
33
34 qsizetype metaLen = 2;
35 if (str.size() <= offset + metaLen)
36 break;
37
38 int base = 10;
39 if (const QChar ch = str[offset + metaLen]; ch == cx) {
40 metaLen++;
41 base = 16;
42 }
43 offset += metaLen;
44
45 const qsizetype end = str.sliced(offset).indexOf(ce);
46 if (end > 0) {
47 bool valid;
48 if (const uint c = str.sliced(offset, end).toUInt(&valid, base);
49 valid && c <= QChar::LastValidCodePoint) {
50 if (QChar::requiresSurrogates(c))
51 result += str.sliced(0, offset - metaLen) + QChar(QChar::highSurrogate(c))
52 + QChar(QChar::lowSurrogate(c));
53 else
54 result += str.sliced(0, offset - metaLen) + QChar(c);
55 str.slice(offset + end + 1);
56 offset = str.indexOf(notation);
57 continue;
58 }
59 }
60 result += str.sliced(0, offset);
61 str.slice(offset);
62 offset = str.indexOf(notation);
63 }
64 result += str;
65 return result;
66}
67
68static QString showNcr(const QString &str)
69{
70 QString result;
71 result.reserve(str.size());
72 for (const QChar ch : str) {
73 if (uint c = ch.unicode(); Q_UNLIKELY(!ch.isPrint() && c > 0x20))
74 result += QString("&#x%1;"_L1).arg(c, 0, 16);
75 else
76 result += ch;
77 }
78 return result;
79}
80
81static QString adjustNcrVisibility(const QString &str, bool ncrMode)
82{
83 return ncrMode ? showNcr(str) : resolveNcr(str);
84}
85
86QT_BEGIN_NAMESPACE
87
88/******************************************************************************
89 *
90 * MessageItem
91 *
92 *****************************************************************************/
93
94MessageItem::MessageItem(const TranslatorMessage &message)
95 : m_message(message), m_danger(false), m_ncrMode(false)
96{
97 if (m_message.translation().isEmpty())
98 m_message.setTranslation(QString());
99}
100
101
102bool MessageItem::compare(const QString &findText, bool matchSubstring,
103 Qt::CaseSensitivity cs) const
104{
105 return matchSubstring
106 ? text().indexOf(findText, 0, cs) >= 0
107 : text().compare(findText, cs) == 0;
108}
109
110void MessageItem::setTranslation(const QString &translation)
111{
112 m_message.setTranslation(resolveNcr(translation));
113}
114
116{
117 return adjustNcrVisibility(m_message.sourceText(), m_ncrMode);
118}
119
121{
122 return adjustNcrVisibility(m_message.extra("po-msgid_plural"_L1), m_ncrMode);
123}
124
126{
127 return adjustNcrVisibility(m_message.translation(), m_ncrMode);
128}
129
131{
132 QStringList translations;
133 translations.reserve(m_message.translations().size());
134 for (QString &trans : m_message.translations())
135 translations.append(adjustNcrVisibility(trans, m_ncrMode));
136 return translations;
137}
138
139void MessageItem::setTranslations(const QStringList &translations)
140{
141 QStringList trans;
142 trans.reserve(translations.size());
143 for (const QString &t : translations)
144 trans.append(resolveNcr(t));
145
146 m_message.setTranslations(trans);
147}
148
149/******************************************************************************
150 *
151 * GroupItem
152 *
153 *****************************************************************************/
154
155void GroupItem::appendToComment(const QString &str)
156{
157 if (!m_comment.isEmpty())
158 m_comment += "\n\n"_L1;
159 m_comment += str;
160}
161
163{
164 if (i >= 0 && i < msgItemList.size())
165 return const_cast<MessageItem *>(&msgItemList[i]);
166 Q_ASSERT(i >= 0 && i < msgItemList.size());
167 return 0;
168}
169
170MessageItem *GroupItem::findMessage(const QString &sourcetext, const QString &comment) const
171{
172 for (int i = 0; i < messageCount(); ++i) {
174 if (mi->text() == sourcetext && mi->comment() == comment)
175 return mi;
176 }
177 return 0;
178}
179
180MessageItem *GroupItem::findMessageById(const QString &msgid) const
181{
182 for (int i = 0, cnt = messageCount(); i < cnt; ++i) {
184 if (m->id() == msgid)
185 return m;
186 }
187 return 0;
188}
189
190/******************************************************************************
191 *
192 * DataModel
193 *
194 *****************************************************************************/
195
196DataModel::DataModel(QObject *parent)
197 : QObject(parent),
198 m_modified(false),
199 m_numMessages(0),
200 m_srcWords(0),
201 m_srcChars(0),
202 m_srcCharsSpc(0),
203 m_language(QLocale::Language(-1)),
204 m_sourceLanguage(QLocale::Language(-1)),
205 m_territory(QLocale::Territory(-1)),
206 m_sourceTerritory(QLocale::Territory(-1))
207{}
208
210{
211 QStringList translations =
212 Translator::normalizedTranslations(m.message(), m_numerusForms.size());
213 QStringList ncrTranslations;
214 ncrTranslations.reserve(translations.size());
215 for (const QString &translate : std::as_const(translations))
216 ncrTranslations.append(adjustNcrVisibility(translate, m.ncrMode()));
217 return ncrTranslations;
218}
219
221{
222 const auto &list = type == IDBASED ? m_labelList : m_contextList;
223 if (groupId >= 0 && groupId < list.size())
224 return const_cast<GroupItem *>(&list[groupId]);
225 return 0;
226}
227
229{
231}
232
234{
235 if (GroupItem *g = groupItem(index))
236 return g->messageItem(index.message());
237 return 0;
238}
239
240GroupItem *DataModel::findGroup(const QString &groupName, TranslationType type) const
241{
242 const auto &list = type == IDBASED ? m_labelList : m_contextList;
243 for (int g = 0; g < list.size(); ++g) {
244 GroupItem *gi = groupItem(g, type);
245 if (gi->group() == groupName)
246 return gi;
247 }
248 return 0;
249}
250
251MessageItem *DataModel::findMessage(const QString &context, const QString &label,
252 const QString &sourcetext, const QString &comment) const
253{
254 if (context.isEmpty()) {
255 if (GroupItem *gi = findGroup(label, IDBASED))
256 return gi->findMessage(sourcetext, comment);
257 } else {
258 if (GroupItem *gi = findGroup(context, TEXTBASED))
259 return gi->findMessage(sourcetext, comment);
260 }
261 return 0;
262}
263
264static int calcMergeScore(const DataModel *one, const DataModel *two)
265{
266 int inBoth = 0;
267
268 auto countSameMessages = [two, one, &inBoth](int count, TranslationType type) {
269 for (int i = 0; i < count; ++i) {
270 GroupItem *gi = two->groupItem(i, type);
271 if (GroupItem *g = one->findGroup(gi->group(), type)) {
272 for (int j = 0; j < gi->messageCount(); ++j) {
274 if (g->findMessage(m->text(), m->comment()))
275 ++inBoth;
276 }
277 }
278 }
279 };
280
281 countSameMessages(two->contextCount(), TEXTBASED);
282 countSameMessages(two->labelCount(), IDBASED);
283
284 return inBoth * 100 / two->messageCount();
285}
286
287bool DataModel::isWellMergeable(const DataModel *other) const
288{
289 if (!other->messageCount() || !messageCount())
290 return true;
291
292 return calcMergeScore(this, other) + calcMergeScore(other, this) > 90;
293}
294
295bool DataModel::load(const QString &fileName, bool *langGuessed, QWidget *parent)
296{
297 Translator tor;
299 bool ok = tor.load(fileName, cd, "auto"_L1);
300 if (!ok) {
301 QMessageBox::warning(parent, QObject::tr("Qt Linguist"), cd.error());
302 return false;
303 }
304
305 if (!tor.messageCount()) {
306 QMessageBox::warning(parent, QObject::tr("Qt Linguist"),
307 tr("The translation file '%1' will not be loaded because it is empty.")
308 .arg(fileName.toHtmlEscaped()));
309 return false;
310 }
311
313 if (!dupes.byId.isEmpty() || !dupes.byContents.isEmpty()) {
314 QString err = tr("<qt>Duplicate messages found in '%1':").arg(fileName.toHtmlEscaped());
315 int numdups = 0;
316 for (auto it = dupes.byId.begin(); it != dupes.byId.end(); ++it) {
317 if (++numdups >= 5) {
318 err += tr("<p>[more duplicates omitted]");
319 goto doWarn;
320 }
321 err += tr("<p>* ID: %1").arg(tor.message(it.key()).id().toHtmlEscaped());
322 }
323 for (auto it = dupes.byContents.begin(); it != dupes.byContents.end(); ++it) {
324 const TranslatorMessage &msg = tor.message(it.key());
325 if (++numdups >= 5) {
326 err += tr("<p>[more duplicates omitted]");
327 break;
328 }
329 err += tr("<p>* Context: %1<br>* Source: %2")
330 .arg(msg.context().toHtmlEscaped(), msg.sourceText().toHtmlEscaped());
331 if (!msg.comment().isEmpty())
332 err += tr("<br>* Comment: %3").arg(msg.comment().toHtmlEscaped());
333 }
334 doWarn:
335 QMessageBox::warning(parent, QObject::tr("Qt Linguist"), err);
336 }
337
338 m_srcFileName = fileName;
339 m_relativeLocations = (tor.locationsType() == Translator::RelativeLocations);
340 m_extra = tor.extras();
341 m_contextList.clear();
342 m_labelList.clear();
343 m_numMessages = 0;
344
345 m_srcWords = 0;
346 m_srcChars = 0;
347 m_srcCharsSpc = 0;
348
349 auto loadMessage = [this](const TranslatorMessage &msg, const QString &group,
350 QList<GroupItem> &list, QHash<QString, int> &groups,
351 TranslationType type) {
352 if (!groups.contains(group)) {
353 groups.insert(group, list.size());
354 list.append(GroupItem(type, group));
355 }
356 GroupItem *gi = groupItem(groups.value(group), type);
357 if (msg.sourceText() == QLatin1String(ContextComment)) {
358 gi->appendToComment(msg.comment());
359 } else {
360 MessageItem tmp(msg);
362 gi->incrementFinishedCount();
365 doCharCounting(tmp.text(), m_srcWords, m_srcChars, m_srcCharsSpc);
366 doCharCounting(tmp.pluralText(), m_srcWords, m_srcChars, m_srcCharsSpc);
367 gi->incrementNonobsoleteCount();
368 }
369 gi->appendMessage(tmp);
370 ++m_numMessages;
371 }
372 };
373
374 QHash<QString, int> labels;
375 QHash<QString, int> contexts;
376 for (const TranslatorMessage &msg : tor.messages()) {
377 if (const QString ctx = msg.context(); !ctx.isEmpty())
378 loadMessage(msg, ctx, m_contextList, contexts, TEXTBASED);
379 else
380 loadMessage(msg, msg.label(), m_labelList, labels, IDBASED);
381 }
382
383 // Try to detect the correct language in the following order
384 // 1. Look for the language attribute in the ts
385 // if that fails
386 // 2. Guestimate the language from the filename
387 // (expecting the qt_{en,de}.ts convention)
388 // if that fails
389 // 3. Retrieve the locale from the system.
390 *langGuessed = false;
391 QString lang = tor.languageCode();
392 if (lang.isEmpty()) {
393 lang = QFileInfo(fileName).baseName();
394 int pos = lang.indexOf(u'_');
395 if (pos != -1)
396 lang.remove(0, pos + 1);
397 else
398 lang.clear();
399 *langGuessed = true;
400 }
401 QLocale::Language l;
402 QLocale::Territory c;
403 Translator::languageAndTerritory(lang, &l, &c);
404 if (l == QLocale::C) {
405 QLocale sys;
406 l = sys.language();
407 c = sys.territory();
408 *langGuessed = true;
409 }
410 if (!setLanguageAndTerritory(l, c))
411 QMessageBox::warning(parent, QObject::tr("Qt Linguist"),
412 tr("Linguist does not know the plural rules for '%1'.\n"
413 "Will assume a single universal form.")
414 .arg(m_localizedLanguage));
415 // Try to detect the correct source language in the following order
416 // 1. Look for the language attribute in the ts
417 // if that fails
418 // 2. Assume English
419 lang = tor.sourceLanguageCode();
420 if (lang.isEmpty()) {
421 l = QLocale::C;
422 c = QLocale::AnyTerritory;
423 } else {
424 Translator::languageAndTerritory(lang, &l, &c);
425 }
426 setSourceLanguageAndTerritory(l, c);
427
428 setModified(false);
429
430 return true;
431}
432
433bool DataModel::save(const QString &fileName, QWidget *parent)
434{
435 Translator tor;
436 for (DataModelIterator it(IDBASED, this); it.isValid(); ++it)
440
441 tor.setLanguageCode(Translator::makeLanguageCode(m_language, m_territory));
442 tor.setSourceLanguageCode(Translator::makeLanguageCode(m_sourceLanguage, m_sourceTerritory));
445 tor.setExtras(m_extra);
448 bool ok = tor.save(fileName, cd, "auto"_L1);
449 if (ok)
450 setModified(false);
451 if (!cd.error().isEmpty())
452 QMessageBox::warning(parent, QObject::tr("Qt Linguist"), cd.error());
453 return ok;
454}
455
456bool DataModel::saveAs(const QString &newFileName, QWidget *parent)
457{
458 if (!save(newFileName, parent))
459 return false;
460 m_srcFileName = newFileName;
461 return true;
462}
463
464bool DataModel::release(const QString &fileName, bool verbose, bool ignoreUnfinished,
465 TranslatorSaveMode mode, QWidget *parent)
466{
467 QFile file(fileName);
468 if (!file.open(QIODevice::WriteOnly)) {
469 QMessageBox::warning(parent, QObject::tr("Qt Linguist"),
470 tr("Cannot create '%2': %1").arg(file.errorString()).arg(fileName));
471 return false;
472 }
473 Translator tor;
474 QLocale locale(m_language, m_territory);
475 tor.setLanguageCode(locale.name());
476 for (DataModelIterator it(IDBASED, this); it.isValid(); ++it)
481 cd.m_verbose = verbose;
482 cd.m_ignoreUnfinished = ignoreUnfinished;
483 cd.m_saveMode = mode;
484 bool ok = saveQM(tor, file, cd);
485 if (!ok)
486 QMessageBox::warning(parent, QObject::tr("Qt Linguist"), cd.error());
487 return ok;
488}
489
490void DataModel::doCharCounting(const QString &text, int &trW, int &trC, int &trCS)
491{
492 trCS += text.size();
493 bool inWord = false;
494 for (int i = 0; i < text.size(); ++i) {
495 if (text[i].isLetterOrNumber() || text[i] == u'_') {
496 if (!inWord) {
497 ++trW;
498 inWord = true;
499 }
500 } else {
501 inWord = false;
502 }
503 if (!text[i].isSpace())
504 trC++;
505 }
506}
507
508bool DataModel::setLanguageAndTerritory(QLocale::Language lang, QLocale::Territory territory)
509{
510 if (m_language == lang && m_territory == territory)
511 return true;
512 m_language = lang;
513 m_territory = territory;
514
515 if (lang == QLocale::C || uint(lang) > uint(QLocale::LastLanguage)) // XXX does this make any sense?
516 lang = QLocale::English;
517 bool ok = getCountNeed(lang, territory, m_countRefNeeds, &m_numerusForms);
518 QLocale loc(lang, territory);
519 // Add territory name if we couldn't match the (lang, territory) combination,
520 // or if the language is used in more than one territory.
521 const bool mentionTerritory = (loc.territory() != territory) || [lang, territory]() {
522 const auto locales = QLocale::matchingLocales(lang, QLocale::AnyScript,
523 QLocale::AnyTerritory);
524 return std::any_of(locales.cbegin(), locales.cend(), [territory](const QLocale &locale) {
525 return locale.territory() != territory;
526 });
527 }();
528 m_localizedLanguage = mentionTerritory
529 //: <language> (<territory>)
530 ? tr("%1 (%2)").arg(loc.nativeLanguageName(), loc.nativeTerritoryName())
531 : loc.nativeLanguageName();
532 if (!ok) {
533 m_numerusForms.clear();
534 m_numerusForms << tr("Universal Form");
535 }
536 emit languageChanged();
537 setModified(true);
538 return ok;
539}
540
541void DataModel::setSourceLanguageAndTerritory(QLocale::Language lang, QLocale::Territory territory)
542{
543 if (m_sourceLanguage == lang && m_sourceTerritory == territory)
544 return;
545 m_sourceLanguage = lang;
546 m_sourceTerritory = territory;
547 setModified(true);
548}
549
551{
552
553 StatisticalData stats {};
554
555 auto updateMessageStatistics = [&stats, this](const MessageItem *mi) {
556 if (mi->isObsolete()) {
557 stats.obsoleteMsg++;
558 } else if (mi->isFinished()) {
559 bool hasDanger = false;
560 for (const QString &trnsl : mi->translations()) {
561 doCharCounting(trnsl, stats.wordsFinished, stats.charsFinished, stats.charsSpacesFinished);
562 hasDanger |= mi->danger();
563 }
564 if (hasDanger)
566 else
568 } else if (mi->isUnfinished()) {
569 bool hasDanger = false;
570 for (const QString &trnsl : mi->translations()) {
571 doCharCounting(trnsl, stats.wordsUnfinished, stats.charsUnfinished, stats.charsSpacesUnfinished);
572 hasDanger |= mi->danger();
573 }
574 if (hasDanger)
576 else
578 }
579 };
580
581 for (DataModelIterator it(IDBASED, this); it.isValid(); ++it)
582 updateMessageStatistics(it.current());
584 updateMessageStatistics(it.current());
585
586 stats.wordsSource = m_srcWords;
587 stats.charsSource = m_srcChars;
588 stats.charsSpacesSource = m_srcCharsSpc;
589 emit statsChanged(stats);
590}
591
592void DataModel::setModified(bool isModified)
593{
594 if (m_modified == isModified)
595 return;
596 m_modified = isModified;
597 emit modifiedChanged();
598}
599
600QString DataModel::prettifyPlainFileName(const QString &fn)
601{
602 static QString workdir = QDir::currentPath() + u'/';
603
604 return QDir::toNativeSeparators(fn.startsWith(workdir) ? fn.mid(workdir.size()) : fn);
605}
606
607QString DataModel::prettifyFileName(const QString &fn)
608{
609 if (fn.startsWith(u'='))
610 return u'=' + prettifyPlainFileName(fn.mid(1));
611 else
612 return prettifyPlainFileName(fn);
613}
614
615/******************************************************************************
616 *
617 * DataModelIterator
618 *
619 *****************************************************************************/
620
622 int message)
623 : DataIndex(type, group, message), m_model(model)
624{
625}
626
628{
629 const qsizetype size =
630 isIdBased() ? m_model->m_labelList.size() : m_model->m_contextList.size();
631 return m_group < size;
632}
633
635{
636 ++m_message;
637 const qsizetype size = isIdBased() ? m_model->m_labelList.at(m_group).messageCount()
638 : m_model->m_contextList.at(m_group).messageCount();
639 if (m_message >= size) {
640 ++m_group;
641 m_message = 0;
642 }
643}
644
646{
647 return m_model->messageItem(*this);
648}
649
650/******************************************************************************
651 *
652 * MultiGroupItem
653 *
654 *****************************************************************************/
655
656MultiGroupItem::MultiGroupItem(int oldCount, GroupItem *groupItem, bool writable)
657 : m_group(groupItem->group()),
659 m_translationType(groupItem->translationType())
660{
661 QList<MessageItem *> mList;
662 QList<MessageItem *> eList;
663 for (int j = 0; j < groupItem->messageCount(); ++j) {
664 MessageItem *m = groupItem->messageItem(j);
665 mList.append(m);
666 eList.append(0);
667 m_multiMessageList.append(MultiMessageItem(m));
668 }
669 for (int i = 0; i < oldCount; ++i) {
670 m_messageLists.append(eList);
671 m_writableMessageLists.append(0);
672 m_groupList.append(0);
673 }
674 m_messageLists.append(mList);
675 m_writableMessageLists.append(writable ? &m_messageLists.last() : 0);
676 m_groupList.append(groupItem);
677}
678
679void MultiGroupItem::appendEmptyModel()
680{
681 QList<MessageItem *> eList;
682 for (int j = 0; j < messageCount(); ++j)
683 eList.append(0);
684 m_messageLists.append(eList);
685 m_writableMessageLists.append(0);
686 m_groupList.append(0);
687}
688
689void MultiGroupItem::assignLastModel(GroupItem *g, bool writable)
690{
691 if (writable)
692 m_writableMessageLists.last() = &m_messageLists.last();
693 m_groupList.last() = g;
694}
695
696// XXX this is not needed, yet
697void MultiGroupItem::moveModel(int oldPos, int newPos)
698{
699 m_groupList.insert(newPos, m_groupList[oldPos]);
700 m_messageLists.insert(newPos, m_messageLists[oldPos]);
701 m_writableMessageLists.insert(newPos, m_writableMessageLists[oldPos]);
702 removeModel(oldPos < newPos ? oldPos : oldPos + 1);
703}
704
705void MultiGroupItem::removeModel(int pos)
706{
707 m_groupList.removeAt(pos);
708 m_messageLists.removeAt(pos);
709 m_writableMessageLists.removeAt(pos);
710}
711
712void MultiGroupItem::putMessageItem(int pos, MessageItem *m)
713{
714 m_messageLists.last()[pos] = m;
715}
716
717void MultiGroupItem::appendMessageItems(const QList<MessageItem *> &m)
718{
719 QList<MessageItem *> nullItems = m; // Basically, just a reservation
720 for (int i = 0; i < nullItems.size(); ++i)
721 nullItems[i] = 0;
722 for (int i = 0; i < m_messageLists.size() - 1; ++i)
723 m_messageLists[i] += nullItems;
724 m_messageLists.last() += m;
725 for (MessageItem *mi : m)
726 m_multiMessageList.append(MultiMessageItem(mi));
727}
728
729void MultiGroupItem::removeMultiMessageItem(int pos)
730{
731 for (int i = 0; i < m_messageLists.size(); ++i)
732 m_messageLists[i].removeAt(pos);
733 m_multiMessageList.removeAt(pos);
734}
735
737{
738 for (int i = 0; i < m_messageLists.size(); ++i)
739 if (m_messageLists[i][msgIdx] && !m_messageLists[i][msgIdx]->isObsolete())
740 return i;
741 return -1;
742}
743
744int MultiGroupItem::findMessage(const QString &sourcetext, const QString &comment) const
745{
746 for (int i = 0, cnt = messageCount(); i < cnt; ++i) {
748 if (m->text() == sourcetext && m->comment() == comment)
749 return i;
750 }
751 return -1;
752}
753
754int MultiGroupItem::findMessageById(const QString &id) const
755{
756 for (int i = 0, cnt = messageCount(); i < cnt; ++i) {
758 if (m->id() == id)
759 return i;
760 }
761 return -1;
762}
763
764/******************************************************************************
765 *
766 * MultiDataModel
767 *
768 *****************************************************************************/
769
771 QColor(210, 235, 250), // blue
772 QColor(210, 250, 220), // green
773 QColor(250, 240, 210), // yellow
774 QColor(210, 250, 250), // cyan
775 QColor(250, 230, 200), // orange
776 QColor(250, 210, 210), // red
777 QColor(235, 210, 250), // purple
778};
779
780static const QColor darkPaletteColors[7] = {
781 QColor(60, 80, 100), // blue
782 QColor(50, 90, 70), // green
783 QColor(100, 90, 50), // yellow
784 QColor(50, 90, 90), // cyan
785 QColor(100, 70, 50), // orange
786 QColor(90, 50, 50), // red
787 QColor(70, 50, 90), // purple
788};
789
790MultiDataModel::MultiDataModel(QObject *parent) :
791 QObject(parent),
792 m_numFinished(0),
793 m_numEditable(0),
794 m_numMessages(0),
795 m_modified(false)
796{
798
799 m_bitmap = QBitmap(8, 8);
800 m_bitmap.clear();
801 QPainter p(&m_bitmap);
802 for (int j = 0; j < 8; ++j)
803 for (int k = 0; k < 8; ++k)
804 if ((j + k) & 4)
805 p.drawPoint(j, k);
806}
807
809{
810 qDeleteAll(m_dataModels);
811}
812
814{
815 QBrush brush(m_colors[model % 7]);
816 if (!isModelWritable(model))
817 brush.setTexture(m_bitmap);
818 return brush;
819}
820
822{
823 m_colors = isDarkMode() ? darkPaletteColors : lightPaletteColors;
824}
825
827{
829 return true;
830
831 int inBothNew = 0;
832
833 auto countInBothNew = [dm, &inBothNew, this](int count, TranslationType type) {
834 for (int i = 0; i < count; ++i) {
835 GroupItem *g = dm->groupItem(i, type);
836 if (MultiGroupItem *mgi = findGroup(g->group(), type)) {
837 for (int j = 0; j < g->messageCount(); ++j) {
839 // During merging, when calculating the well-mergeability ratio,
840 // we reward ID-based messages with the same IDs for having the same label.
841 // This is not a strict requirement, since linguists can still represent
842 // merging of the messages with the same ID and different labels reasonably.
843 // However, if too many messages share the same ID but have different labels,
844 // merging them provides little value and may reduce user convenience.
845 if ((type == TEXTBASED && mgi->findMessage(m->text(), m->comment()) >= 0)
846 || (type == IDBASED && mgi->findMessageById(m->id()) >= 0))
847 ++inBothNew;
848 }
849 }
850 }
851 };
852
853 countInBothNew(dm->contextCount(), TEXTBASED);
854 countInBothNew(dm->labelCount(), IDBASED);
855
856 int newRatio = inBothNew * 100 / dm->messageCount();
857 int inBothOld = 0;
858
859 auto countInBothOld = [this, dm, &inBothOld](int count, TranslationType type) {
860 for (int k = 0; k < count; ++k) {
862 if (GroupItem *g = dm->findGroup(mgi->group(), type)) {
863 for (int j = 0; j < mgi->messageCount(); ++j) {
865 if ((type == TEXTBASED && g->findMessage(m->text(), m->comment()))
866 || (type == IDBASED && g->findMessageById(m->id())))
867 ++inBothOld;
868 }
869 }
870 }
871 };
872
873 countInBothOld(contextCount(), TEXTBASED);
874 countInBothOld(labelCount(), IDBASED);
875
876 int oldRatio = inBothOld * 100 / messageCount();
877
878 return newRatio + oldRatio > 90;
879}
880
881void MultiDataModel::append(DataModel *dm, bool readWrite)
882{
883 int insCol = modelCount() + 1;
884 m_dataModels.append(dm);
885
886 auto appendGroups = [this, dm, readWrite, insCol](TranslationType type, MessageModel *msgModel,
887 QList<MultiGroupItem> &multiGroupList) {
888 qsizetype count = type == IDBASED ? labelCount() : contextCount();
889 msgModel->beginInsertColumns(QModelIndex(), insCol, insCol);
890 for (int j = 0; j < count; ++j) {
891 msgModel->beginInsertColumns(msgModel->createIndex(j, 0), insCol, insCol);
892 multiGroupList[j].appendEmptyModel();
893 msgModel->endInsertColumns();
894 }
895 msgModel->endInsertColumns();
896 count = type == IDBASED ? dm->labelCount() : dm->contextCount();
897 int appendedGroups = 0;
898 for (int i = 0; i < count; ++i) {
899 GroupItem *g = dm->groupItem(i, type);
900 int gidx = findGroupIndex(g->group(), type);
901 if (gidx >= 0) {
902 MultiGroupItem *mgi = multiGroupItem(gidx, type);
903 mgi->assignLastModel(g, readWrite);
904 QList<MessageItem *> appendItems;
905 for (int j = 0; j < g->messageCount(); ++j) {
907
908 int msgIdx = type == IDBASED ? mgi->findMessageById(m->id())
909 : mgi->findMessage(m->text(), m->comment());
910
911 if (msgIdx >= 0)
912 mgi->putMessageItem(msgIdx, m);
913 else
914 appendItems << m;
915 }
916 if (!appendItems.isEmpty()) {
917 int msgCnt = mgi->messageCount();
918 msgModel->beginInsertRows(msgModel->createIndex(gidx, 0), msgCnt,
919 msgCnt + appendItems.size() - 1);
920 mgi->appendMessageItems(appendItems);
921 msgModel->endInsertRows();
922 m_numMessages += appendItems.size();
923 }
924 } else {
925 multiGroupList << MultiGroupItem(modelCount() - 1, g, readWrite);
926 m_numMessages += g->messageCount();
927 ++appendedGroups;
928 }
929 }
930 if (appendedGroups) {
931 // Do that en block to avoid itemview inefficiency. It doesn't hurt that we
932 // announce the availability of the data "long" after it was actually added.
933 const qsizetype groupCount = type == IDBASED ? labelCount() : contextCount();
934 msgModel->beginInsertRows(QModelIndex(), groupCount - appendedGroups, groupCount - 1);
935 msgModel->endInsertRows();
936 }
937 };
938
939 appendGroups(TEXTBASED, m_textBasedMsgModel, m_multiContextList);
940 appendGroups(IDBASED, m_idBasedMsgModel, m_multiLabelList);
941
942 dm->setWritable(readWrite);
943 updateCountsOnAdd(modelCount() - 1, readWrite);
944 connect(dm, &DataModel::modifiedChanged,
945 this, &MultiDataModel::onModifiedChanged);
946 connect(dm, &DataModel::languageChanged,
947 this, &MultiDataModel::onLanguageChanged);
948 connect(dm, &DataModel::statsChanged,
949 this, &MultiDataModel::statsChanged);
950 emit modelAppended();
951}
952
953void MultiDataModel::close(int model)
954{
955 if (m_dataModels.size() == 1) {
957 } else {
958 int delCol = model + 1;
959 auto removeModel = [delCol, model](auto *msgModel, auto &list) {
960 msgModel->beginRemoveColumns(QModelIndex(), delCol, delCol);
961 for (int i = list.size(); --i >= 0;) {
962 msgModel->beginRemoveColumns(msgModel->createIndex(i, 0), delCol, delCol);
963 list[i].removeModel(model);
964 msgModel->endRemoveColumns();
965 }
966 msgModel->endRemoveColumns();
967 };
968
969 updateCountsOnRemove(model, isModelWritable(model));
970 removeModel(m_idBasedMsgModel, m_multiLabelList);
971 removeModel(m_textBasedMsgModel, m_multiContextList);
972 delete m_dataModels.takeAt(model);
973 emit modelDeleted(model);
974
975 auto removeMessages = [this](auto *msgModel, auto &list) {
976 for (int i = list.size(); --i >= 0;) {
977 auto &mi = list[i];
978 QModelIndex idx = msgModel->createIndex(i, 0);
979 for (int j = mi.messageCount(); --j >= 0;)
980 if (mi.multiMessageItem(j)->isEmpty()) {
981 msgModel->beginRemoveRows(idx, j, j);
982 mi.removeMultiMessageItem(j);
983 msgModel->endRemoveRows();
984 --m_numMessages;
985 }
986 if (!mi.messageCount()) {
987 msgModel->beginRemoveRows(QModelIndex(), i, i);
988 list.removeAt(i);
989 msgModel->endRemoveRows();
990 }
991 }
992 };
993
994 removeMessages(m_idBasedMsgModel, m_multiLabelList);
995 removeMessages(m_textBasedMsgModel, m_multiContextList);
996 onModifiedChanged();
997 }
998}
999
1001{
1002 m_idBasedMsgModel->beginResetModel();
1003 m_textBasedMsgModel->beginResetModel();
1004 m_numFinished = 0;
1005 m_numEditable = 0;
1006 m_numMessages = 0;
1007 qDeleteAll(m_dataModels);
1008 m_dataModels.clear();
1009 m_multiContextList.clear();
1010 m_multiLabelList.clear();
1011 m_textBasedMsgModel->endResetModel();
1012 m_idBasedMsgModel->endResetModel();
1013 emit allModelsDeleted();
1014 onModifiedChanged();
1015}
1016
1017// XXX this is not needed, yet
1018void MultiDataModel::moveModel(int oldPos, int newPos)
1019{
1020 int delPos = oldPos < newPos ? oldPos : oldPos + 1;
1021 m_dataModels.insert(newPos, m_dataModels[oldPos]);
1022 m_dataModels.removeAt(delPos);
1023 for (int i = 0; i < m_multiContextList.size(); ++i)
1024 m_multiContextList[i].moveModel(oldPos, newPos);
1025 for (int i = 0; i < m_multiLabelList.size(); ++i)
1026 m_multiLabelList[i].moveModel(oldPos, newPos);
1027}
1028
1029QStringList MultiDataModel::prettifyFileNames(const QStringList &names)
1030{
1031 QStringList out;
1032
1033 for (const QString &name : names)
1034 out << DataModel::prettifyFileName(name);
1035 return out;
1036}
1037
1038QString MultiDataModel::condenseFileNames(const QStringList &names)
1039{
1040 if (names.isEmpty())
1041 return QString();
1042
1043 if (names.size() < 2)
1044 return names.first();
1045
1046 QString prefix = names.first();
1047 if (prefix.startsWith(u'='))
1048 prefix.remove(0, 1);
1049 QString suffix = prefix;
1050 for (int i = 1; i < names.size(); ++i) {
1051 QString fn = names[i];
1052 if (fn.startsWith(u'='))
1053 fn.remove(0, 1);
1054 for (int j = 0; j < prefix.size(); ++j)
1055 if (fn[j] != prefix[j]) {
1056 if (j < prefix.size()) {
1057 while (j > 0 && prefix[j - 1].isLetterOrNumber())
1058 --j;
1059 prefix.truncate(j);
1060 }
1061 break;
1062 }
1063 int fnl = fn.size() - 1;
1064 int sxl = suffix.size() - 1;
1065 for (int k = 0; k <= sxl; ++k)
1066 if (fn[fnl - k] != suffix[sxl - k]) {
1067 if (k < sxl) {
1068 while (k > 0 && suffix[sxl - k + 1].isLetterOrNumber())
1069 --k;
1070 if (prefix.size() + k > fnl)
1071 --k;
1072 suffix.remove(0, sxl - k + 1);
1073 }
1074 break;
1075 }
1076 }
1077 QString ret = prefix + u'{';
1078 int pxl = prefix.size();
1079 int sxl = suffix.size();
1080 for (int j = 0; j < names.size(); ++j) {
1081 if (j)
1082 ret += u',';
1083 int off = pxl;
1084 QString fn = names[j];
1085 if (fn.startsWith(u'=')) {
1086 ret += u'=';
1087 ++off;
1088 }
1089 ret += fn.mid(off, fn.size() - sxl - off);
1090 }
1091 ret += u'}' + suffix;
1092 return ret;
1093}
1094
1096{
1097 QStringList names;
1098 for (DataModel *dm : m_dataModels)
1099 names << (dm->isWritable() ? QString() : QString::fromLatin1("=")) + dm->srcFileName(pretty);
1100 return names;
1101}
1102
1104{
1105 return condenseFileNames(srcFileNames(pretty));
1106}
1107
1112
1114{
1115 for (const DataModel *mdl : m_dataModels)
1116 if (mdl->isModified())
1117 return true;
1118 return false;
1119}
1120
1121void MultiDataModel::onModifiedChanged()
1122{
1123 bool modified = isModified();
1124 if (modified != m_modified) {
1125 emit modifiedChanged(modified);
1126 m_modified = modified;
1127 }
1128}
1129
1130void MultiDataModel::onLanguageChanged()
1131{
1132 int i = 0;
1133 while (sender() != m_dataModels[i])
1134 ++i;
1135 emit languageChanged(i);
1136}
1137
1138GroupItem *MultiDataModel::groupItem(const MultiDataIndex &index) const
1139{
1141}
1142
1143int MultiDataModel::isFileLoaded(const QString &name) const
1144{
1145 for (int i = 0; i < m_dataModels.size(); ++i)
1146 if (m_dataModels[i]->srcFileName() == name)
1147 return i;
1148 return -1;
1149}
1150
1151int MultiDataModel::findGroupIndex(const QString &group, TranslationType type) const
1152{
1153 const auto &list = type == IDBASED ? m_multiLabelList : m_multiContextList;
1154 for (int i = 0; i < list.size(); ++i) {
1155 const MultiGroupItem &mg = list[i];
1156 if (mg.group() == group)
1157 return i;
1158 }
1159 return -1;
1160}
1161
1162MultiGroupItem *MultiDataModel::findGroup(const QString &group, TranslationType type) const
1163{
1164 const auto &list = type == IDBASED ? m_multiLabelList : m_multiContextList;
1165 for (int i = 0; i < list.size(); ++i) {
1166 const MultiGroupItem &mgi = list[i];
1167 if (mgi.group() == group)
1168 return const_cast<MultiGroupItem *>(&mgi);
1169 }
1170 return 0;
1171}
1172
1174{
1175 const auto &list = index.isIdBased() ? m_multiLabelList : m_multiContextList;
1176 return const_cast<MultiGroupItem *>(&list[index.group()]);
1177}
1178
1180{
1181 qsizetype groupCount = index.isIdBased() ? labelCount() : contextCount();
1182 if (index.group() < groupCount && index.group() >= 0 && model >= 0 && model < modelCount()) {
1183 MultiGroupItem *mgi = multiGroupItem(index);
1184 if (index.message() < mgi->messageCount())
1185 return mgi->messageItem(model, index.message());
1186 }
1187 Q_ASSERT(model >= 0 && model < modelCount());
1188 Q_ASSERT(index.group() < groupCount);
1189 return 0;
1190}
1191
1192void MultiDataModel::setTranslation(const MultiDataIndex &index, const QString &translation)
1193{
1194 MessageItem *m = messageItem(index);
1195 if (translation == m->translation())
1196 return;
1197 m->setTranslation(translation);
1198 setModified(index.model(), true);
1199 emit translationChanged(index);
1200}
1201
1202void MultiDataModel::setFinished(const MultiDataIndex &index, bool finished)
1203{
1204 MultiGroupItem *mgi = multiGroupItem(index);
1206 GroupItem *gi = groupItem(index);
1207 MessageItem *m = messageItem(index);
1209 if (type == TranslatorMessage::Unfinished && finished) {
1211 mm->decrementUnfinishedCount();
1212 if (!mm->countUnfinished()) {
1213 incrementFinishedCount();
1214 mgi->incrementFinishedCount();
1215 emit multiGroupDataChanged(index);
1216 }
1217 gi->incrementFinishedCount();
1218 if (m->danger()) {
1219 gi->incrementFinishedDangerCount();
1220 gi->decrementUnfinishedDangerCount();
1222 emit groupDataChanged(index);
1223 } else if (gi->finishedCount() == gi->nonobsoleteCount()) {
1224 emit groupDataChanged(index);
1225 }
1226 emit messageDataChanged(index);
1227 setModified(index.model(), true);
1228 } else if (type == TranslatorMessage::Finished && !finished) {
1230 mm->incrementUnfinishedCount();
1231 if (mm->countUnfinished() == 1) {
1232 decrementFinishedCount();
1233 mgi->decrementFinishedCount();
1234 emit multiGroupDataChanged(index);
1235 }
1236 gi->decrementFinishedCount();
1237 if (m->danger()) {
1238 gi->decrementFinishedDangerCount();
1239 gi->incrementUnfinishedDangerCount();
1240 if (gi->unfinishedDangerCount() == 1
1242 emit groupDataChanged(index);
1243 } else if (gi->finishedCount() + 1 == gi->nonobsoleteCount()) {
1244 emit groupDataChanged(index);
1245 }
1246 emit messageDataChanged(index);
1247 setModified(index.model(), true);
1248 }
1249}
1250
1251void MultiDataModel::setDanger(const MultiDataIndex &index, bool danger)
1252{
1253 GroupItem *gi = groupItem(index);
1254 MessageItem *m = messageItem(index);
1255 if (!m->danger() && danger) {
1256 if (m->isFinished()) {
1257 gi->incrementFinishedDangerCount();
1258 if (gi->finishedDangerCount() == 1)
1259 emit groupDataChanged(index);
1260 } else {
1261 gi->incrementUnfinishedDangerCount();
1262 if (gi->unfinishedDangerCount() == 1)
1263 emit groupDataChanged(index);
1264 }
1265 emit messageDataChanged(index);
1266 m->setDanger(danger);
1267 } else if (m->danger() && !danger) {
1268 if (m->isFinished()) {
1269 gi->decrementFinishedDangerCount();
1271 emit groupDataChanged(index);
1272 } else {
1273 gi->decrementUnfinishedDangerCount();
1275 emit groupDataChanged(index);
1276 }
1277 emit messageDataChanged(index);
1278 m->setDanger(danger);
1279 }
1280}
1281
1282void MultiDataModel::updateCountsOnAdd(int model, bool writable)
1283{
1284 auto updateCount = [model, writable, this](auto &mg) {
1285 for (int j = 0; j < mg.messageCount(); ++j)
1286 if (MessageItem *m = mg.messageItem(model, j)) {
1287 MultiMessageItem *mm = mg.multiMessageItem(j);
1288 mm->incrementNonnullCount();
1289 if (!m->isObsolete()) {
1290 if (writable) {
1291 if (!mm->countEditable()) {
1292 mg.incrementEditableCount();
1293 incrementEditableCount();
1294 if (m->isFinished()) {
1295 mg.incrementFinishedCount();
1296 incrementFinishedCount();
1297 } else {
1298 mm->incrementUnfinishedCount();
1299 }
1300 } else if (!m->isFinished()) {
1301 if (!mm->isUnfinished()) {
1302 mg.decrementFinishedCount();
1303 decrementFinishedCount();
1304 }
1305 mm->incrementUnfinishedCount();
1306 }
1307 mm->incrementEditableCount();
1308 }
1309 mg.incrementNonobsoleteCount();
1310 mm->incrementNonobsoleteCount();
1311 }
1312 }
1313 };
1314 for (auto &mg : m_multiContextList)
1315 updateCount(mg);
1316 for (auto &mg : m_multiLabelList)
1317 updateCount(mg);
1318}
1319
1320void MultiDataModel::updateCountsOnRemove(int model, bool writable)
1321{
1322 auto updateCount = [model, writable, this](auto &mg) {
1323 for (int j = 0; j < mg.messageCount(); ++j)
1324 if (MessageItem *m = mg.messageItem(model, j)) {
1325 MultiMessageItem *mm = mg.multiMessageItem(j);
1326 mm->decrementNonnullCount();
1327 if (!m->isObsolete()) {
1328 mm->decrementNonobsoleteCount();
1329 mg.decrementNonobsoleteCount();
1330 if (writable) {
1331 mm->decrementEditableCount();
1332 if (!mm->countEditable()) {
1333 mg.decrementEditableCount();
1334 decrementEditableCount();
1335 if (m->isFinished()) {
1336 mg.decrementFinishedCount();
1337 decrementFinishedCount();
1338 } else {
1339 mm->decrementUnfinishedCount();
1340 }
1341 } else if (!m->isFinished()) {
1342 mm->decrementUnfinishedCount();
1343 if (!mm->isUnfinished()) {
1344 mg.incrementFinishedCount();
1345 incrementFinishedCount();
1346 }
1347 }
1348 }
1349 }
1350 }
1351 };
1352 for (auto &mg : m_multiContextList)
1353 updateCount(mg);
1354 for (auto &mg : m_multiLabelList)
1355 updateCount(mg);
1356}
1357
1358/******************************************************************************
1359 *
1360 * MultiDataModelIterator
1361 *
1362 *****************************************************************************/
1363
1365 int model, int group, int message)
1366 : MultiDataIndex(type, model, group, message), m_dataModel(dataModel)
1367{
1368}
1369
1371{
1372 Q_ASSERT(isValid());
1373 ++m_message;
1374 const qsizetype count = isIdBased()
1375 ? m_dataModel->m_multiLabelList.at(m_group).messageCount()
1376 : m_dataModel->m_multiContextList.at(m_group).messageCount();
1377 if (m_message >= count) {
1378 ++m_group;
1379 m_message = 0;
1380 }
1381}
1382
1384{
1385 const qsizetype size = isIdBased() ? m_dataModel->m_multiLabelList.size()
1386 : m_dataModel->m_multiContextList.size();
1387 return m_group < size;
1388}
1389
1391{
1392 return m_dataModel->messageItem(*this);
1393}
1394
1395/******************************************************************************
1396 *
1397 * MessageModel
1398 *
1399 *****************************************************************************/
1400
1401MessageModel::MessageModel(TranslationType translationType, QObject *parent, MultiDataModel *data)
1402 : QAbstractItemModel(parent), m_data(data), m_translationType(translationType)
1403{
1404 if (translationType == IDBASED)
1405 data->m_idBasedMsgModel = this;
1406 else
1407 data->m_textBasedMsgModel = this;
1408 connect(m_data, &MultiDataModel::multiGroupDataChanged, this,
1409 &MessageModel::multiGroupItemChanged);
1410 connect(m_data, &MultiDataModel::groupDataChanged, this, &MessageModel::groupItemChanged);
1411 connect(m_data, &MultiDataModel::messageDataChanged, this, &MessageModel::messageItemChanged);
1412}
1413
1414QModelIndex MessageModel::index(int row, int column, const QModelIndex &parent) const
1415{
1416 if (!parent.isValid())
1417 return createIndex(row, column);
1418 if (!parent.internalId())
1419 return createIndex(row, column, parent.row() + 1);
1420 return QModelIndex();
1421}
1422
1423QModelIndex MessageModel::parent(const QModelIndex& index) const
1424{
1425 if (index.internalId())
1426 return createIndex(index.internalId() - 1, 0);
1427 return QModelIndex();
1428}
1429
1430void MessageModel::multiGroupItemChanged(const MultiDataIndex &index)
1431{
1432 if (index.translationType() != m_translationType)
1433 return;
1434 QModelIndex idx = createIndex(index.group(), m_data->modelCount() + 2);
1435 emit dataChanged(idx, idx);
1436}
1437
1438void MessageModel::groupItemChanged(const MultiDataIndex &index)
1439{
1440 if (index.translationType() != m_translationType)
1441 return;
1442 QModelIndex idx = createIndex(index.group(), index.model() + 1);
1443 emit dataChanged(idx, idx);
1444}
1445
1446void MessageModel::messageItemChanged(const MultiDataIndex &index)
1447{
1448 if (index.translationType() != m_translationType)
1449 return;
1450 QModelIndex idx = createIndex(index.message(), index.model() + 1, index.group() + 1);
1451 emit dataChanged(idx, idx);
1452}
1453
1455{
1456 if (index.message() < 0) // Should be unused case
1457 return createIndex(index.group(), index.model() + 1);
1458 return createIndex(index.message(), index.model() + 1, index.group() + 1);
1459}
1460
1461int MessageModel::rowCount(const QModelIndex &parent) const
1462{
1463 if (!parent.isValid())
1464 return m_translationType == IDBASED ? m_data->labelCount()
1465 : m_data->contextCount(); // contexts
1466 if (!parent.internalId()) // messages
1467 return m_data->multiGroupItem(parent.row(), m_translationType)->messageCount();
1468 return 0;
1469}
1470
1471int MessageModel::columnCount(const QModelIndex &) const
1472{
1473 return m_data->modelCount() + 3;
1474}
1475
1476QVariant MessageModel::data(const QModelIndex &index, int role) const
1477{
1478 static QVariant pxOn;
1479 static QVariant pxOff;
1480 static QVariant pxObsolete;
1481 static QVariant pxDanger;
1482 static QVariant pxWarning;
1483 static QVariant pxEmpty;
1484
1485 static Qt::ColorScheme mode = Qt::ColorScheme::Unknown; // to prevent creating new QPixmaps
1486 // every time the method is called
1487
1488 if (bool dark = isDarkMode();
1489 (dark && mode != Qt::ColorScheme::Dark) || (!dark && mode != Qt::ColorScheme::Light)) {
1490 pxOn = createMarkIcon(TranslationMarks::OnMark, dark);
1491 pxOff = createMarkIcon(TranslationMarks::OffMark, dark);
1492 pxObsolete = createMarkIcon(TranslationMarks::ObsoleteMark, dark);
1493 pxDanger = createMarkIcon(TranslationMarks::DangerMark, dark);
1494 pxWarning = createMarkIcon(TranslationMarks::WarningMark, dark);
1495 pxEmpty = createMarkIcon(TranslationMarks::EmptyMark, dark);
1496 mode = dark ? Qt::ColorScheme::Dark : Qt::ColorScheme::Light;
1497 }
1498
1499 int row = index.row();
1500 int column = index.column() - 1;
1501
1502 if (column < 0)
1503 return QVariant();
1504
1505 int numLangs = m_data->modelCount();
1506
1507 if (role == Qt::ToolTipRole && column < numLangs) {
1508 return tr("Completion status for %1").arg(m_data->model(column)->localizedLanguage());
1509 } else if (index.internalId()) {
1510 // this is a message
1511 int crow = index.internalId() - 1;
1512 MultiGroupItem *mgi = m_data->multiGroupItem(crow, m_translationType);
1513 if (row >= mgi->messageCount() || !index.isValid())
1514 return QVariant();
1515
1516 if (role == Qt::DisplayRole || (role == Qt::ToolTipRole && column == numLangs)) {
1517 switch (column - numLangs) {
1518 case 0: // Source text
1519 {
1520 if (m_translationType == IDBASED)
1521 return mgi->multiMessageItem(row)->id();
1522 else if (const QString text = mgi->multiMessageItem(row)->text(); !text.isEmpty())
1523 return text.simplified();
1524 else
1525 return tr("<context comment>");
1526 }
1527 case 1:
1528 if (m_translationType == IDBASED)
1529 return mgi->multiMessageItem(row)->text();
1530 Q_FALLTHROUGH();
1531 default: // Status or dummy column => no text
1532 return QVariant();
1533 }
1534 }
1535 else if (role == Qt::DecorationRole && column < numLangs) {
1536 if (MessageItem *msgItem = mgi->messageItem(column, row)) {
1537 switch (msgItem->message().type()) {
1539 if (msgItem->translation().isEmpty())
1540 return pxEmpty;
1541 if (msgItem->danger())
1542 return pxDanger;
1543 return pxOff;
1545 if (msgItem->danger())
1546 return pxWarning;
1547 return pxOn;
1548 default:
1549 return pxObsolete;
1550 }
1551 }
1552 return QVariant();
1553 }
1554 else if (role == SortRole) {
1555 switch (column - numLangs) {
1556 case 0: // Source text
1557 return mgi->multiMessageItem(row)->text().simplified().remove(u'&');
1558 case 1: // Dummy column
1559 return QVariant();
1560 default:
1561 if (MessageItem *msgItem = mgi->messageItem(column, row)) {
1562 int rslt = !msgItem->translation().isEmpty();
1563 if (!msgItem->danger())
1564 rslt |= 2;
1565 if (msgItem->isObsolete())
1566 rslt |= 8;
1567 else if (msgItem->isFinished())
1568 rslt |= 4;
1569 return rslt;
1570 }
1571 return INT_MAX;
1572 }
1573 } else if (role == Qt::ForegroundRole && column > 0
1574 && mgi->multiMessageItem(row)->isObsolete()) {
1575 return QBrush(Qt::darkGray);
1576 } else if (role == Qt::ForegroundRole && column == numLangs
1577 && mgi->multiMessageItem(row)->text().isEmpty()) {
1578 return QBrush(QColor(0, 0xa0, 0xa0));
1579 } else if (role == Qt::BackgroundRole) {
1580 if (column < numLangs && numLangs != 1)
1581 return m_data->brushForModel(column);
1582 }
1583 } else {
1584 // this is a context or a label
1585 const qsizetype groupCount =
1586 m_translationType == IDBASED ? m_data->labelCount() : m_data->contextCount();
1587 if (row >= groupCount || !index.isValid())
1588 return QVariant();
1589
1590 MultiGroupItem *mgi = m_data->multiGroupItem(row, m_translationType);
1591 if (role == Qt::DisplayRole || role == Qt::ToolTipRole) {
1592 switch (column - numLangs) {
1593 case 0: // Context/Label
1594 {
1595 const QString groupName = mgi->group().simplified();
1596 if (m_translationType == IDBASED and groupName.isEmpty()) {
1597 return tr("<unnamed label>");
1598 }
1599 return groupName;
1600 }
1601 case 1: {
1602 if (role == Qt::ToolTipRole) {
1603 return tr("%n unfinished message(s) left.", 0,
1605 }
1606 return QString::asprintf("%d/%d", mgi->getNumFinished(), mgi->getNumEditable());
1607 }
1608 default:
1609 return QVariant(); // Status => no text
1610 }
1611 }
1612 else if (role == Qt::FontRole && column == m_data->modelCount()) {
1613 QFont boldFont;
1614 boldFont.setBold(true);
1615 return boldFont;
1616 }
1617 else if (role == Qt::DecorationRole && column < numLangs) {
1618 if (GroupItem *groupItem = mgi->groupItem(column)) {
1619 if (groupItem->isObsolete())
1620 return pxObsolete;
1621 if (groupItem->isFinished())
1622 return groupItem->finishedDangerCount() > 0 ? pxWarning : pxOn;
1623 return groupItem->unfinishedDangerCount() > 0 ? pxDanger : pxOff;
1624 }
1625 return QVariant();
1626 }
1627 else if (role == SortRole) {
1628 switch (column - numLangs) {
1629 case 0: // Context/Label (same as display role)
1630 return mgi->group().simplified();
1631 case 1: // Items
1632 return mgi->getNumEditable();
1633 default: // Percent
1634 if (GroupItem *groupItem = mgi->groupItem(column)) {
1635 int totalItems = groupItem->nonobsoleteCount();
1636 int percent =
1637 totalItems ? (100 * groupItem->finishedCount()) / totalItems : 100;
1638 int rslt = percent * (((1 << 28) - 1) / 100) + totalItems;
1639 if (groupItem->isObsolete()) {
1640 rslt |= (1 << 30);
1641 } else if (groupItem->isFinished()) {
1642 rslt |= (1 << 29);
1643 if (!groupItem->finishedDangerCount())
1644 rslt |= (1 << 28);
1645 } else {
1646 if (!groupItem->unfinishedDangerCount())
1647 rslt |= (1 << 28);
1648 }
1649 return rslt;
1650 }
1651 return INT_MAX;
1652 }
1653 } else if (role == Qt::ForegroundRole && column >= numLangs && mgi->isObsolete()) {
1654 return QBrush(Qt::darkGray);
1655 } else if (role == Qt::ForegroundRole && column == numLangs
1656 && m_translationType == IDBASED) {
1657 return QBrush(QColor(0, 0xa0, 0xa0));
1658 } else if (role == Qt::BackgroundRole) {
1659 if (column < numLangs && numLangs != 1) {
1660 QBrush brush = m_data->brushForModel(column);
1661 if (row & 1) {
1662 brush.setColor(brush.color().darker(108));
1663 }
1664 return brush;
1665 }
1666 }
1667 }
1668 return QVariant();
1669}
1670
1671MultiDataIndex MessageModel::dataIndex(const QModelIndex &index, int model) const
1672{
1673 Q_ASSERT(index.isValid());
1674 Q_ASSERT(index.internalId());
1675 return MultiDataIndex(m_translationType, model, index.internalId() - 1, index.row());
1676}
1677
1678QT_END_NAMESPACE
TranslatorSaveMode m_saveMode
Definition translator.h:62
bool m_ignoreUnfinished
Definition translator.h:58
DataIndex(TranslationType type, int group=-1, int message=-1)
int message() const
TranslationType translationType() const
bool isIdBased() const
int group() const
bool isValid() const
DataModelIterator(TranslationType type, const DataModel *model=0, int groupNo=0, int messageNo=0)
MessageItem * current() const
void languageChanged()
bool load(const QString &fileName, bool *langGuessed, QWidget *parent)
GroupItem * findGroup(const QString &group, TranslationType type) const
void doCharCounting(const QString &text, int &trW, int &trC, int &trCS)
bool saveAs(const QString &newFileName, QWidget *parent)
MessageItem * messageItem(const DataIndex &index) const
bool isWellMergeable(const DataModel *other) const
void setModified(bool dirty)
MessageItem * findMessage(const QString &context, const QString &label, const QString &sourcetext, const QString &comment) const
void updateStatistics()
bool release(const QString &fileName, bool verbose, bool ignoreUnfinished, TranslatorSaveMode mode, QWidget *parent)
GroupItem * groupItem(int index, TranslationType type) const
int messageCount() const
void setWritable(bool writable)
int labelCount() const
GroupItem * groupItem(DataIndex) const
QStringList normalizedTranslations(const MessageItem &m) const
int contextCount() const
bool isFinished() const
MessageItem * findMessage(const QString &sourcetext, const QString &comment) const
int unfinishedDangerCount() const
int nonobsoleteCount() const
MessageItem * findMessageById(const QString &msgid) const
bool isObsolete() const
TranslationType translationType() const
MessageItem * messageItem(int i) const
int messageCount() const
int finishedDangerCount() const
int finishedCount() const
void setTranslation(const QString &translation)
bool danger() const
MessageItem(const TranslatorMessage &message)
bool compare(const QString &findText, bool matchSubstring, Qt::CaseSensitivity cs) const
bool isFinished() const
QString text() const
const TranslatorMessage & message() const
void setDanger(bool danger)
QString pluralText() const
void setTranslations(const QStringList &translations)
QString translation() const
QStringList translations() const
bool isUnfinished() const
TranslatorMessage::Type type() const
void setType(TranslatorMessage::Type type)
bool isObsolete() const
QModelIndex modelIndex(const MultiDataIndex &index)
QModelIndex parent(const QModelIndex &index) const override
QModelIndex index(int row, int column, const QModelIndex &parent=QModelIndex()) const override
Returns the index of the item in the model specified by the given row, column and parent index.
QVariant data(const QModelIndex &index, int role=Qt::DisplayRole) const override
Returns the data stored under the given role for the item referred to by the index.
MessageModel(TranslationType translationType, QObject *parent, MultiDataModel *data)
MultiDataIndex dataIndex(const QModelIndex &index, int model) const
int rowCount(const QModelIndex &parent=QModelIndex()) const override
Returns the number of rows under the given parent.
int columnCount(const QModelIndex &parent=QModelIndex()) const override
Returns the number of columns for the children of the given parent.
int model() const
MultiDataIndex(TranslationType type=TEXTBASED, int model=-1, int group=-1, int message=-1)
MultiDataModelIterator(TranslationType type, MultiDataModel *model=0, int modelNo=-1, int groupNo=0, int messageNo=0)
MessageItem * current() const
int findGroupIndex(const QString &group, TranslationType type) const
QStringList srcFileNames(bool pretty=false) const
int labelCount() const
void append(DataModel *dm, bool readWrite)
MessageItem * messageItem(const MultiDataIndex &index, int model) const
void setModified(int model, bool dirty)
MultiGroupItem * multiGroupItem(const MultiDataIndex &index) const
DataModel * model(int i)
bool isModified() const
MultiGroupItem * multiGroupItem(int idx, TranslationType type) const
MultiGroupItem * findGroup(const QString &group, TranslationType type) const
QString condensedSrcFileNames(bool pretty=false) const
QBrush brushForModel(int model) const
int isFileLoaded(const QString &name) const
void setTranslation(const MultiDataIndex &index, const QString &translation)
int messageCount() const
void setDanger(const MultiDataIndex &index, bool danger)
int modelCount() const
bool isModelWritable(int model) const
MultiMessageItem * multiMessageItem(const MultiDataIndex &index) const
void groupDataChanged(const MultiDataIndex &index)
int contextCount() const
MessageItem * messageItem(const MultiDataIndex &index) const
void setFinished(const MultiDataIndex &index, bool finished)
void messageDataChanged(const MultiDataIndex &index)
bool isWellMergeable(const DataModel *dm) const
void moveModel(int oldPos, int newPos)
void close(int model)
LocationsType locationsType() const
Definition translator.h:126
void setExtras(const ExtraData &extras)
Definition translator.h:158
void append(const TranslatorMessage &msg)
void setLocationsType(LocationsType lt)
Definition translator.h:125
@ RelativeLocations
Definition translator.h:124
@ AbsoluteLocations
Definition translator.h:124
int messageCount() const
Definition translator.h:139
Duplicates resolveDuplicates()
void normalizeTranslations(ConversionData &cd)
const ExtraData & extras() const
Definition translator.h:157
bool isDarkMode()
Definition globals.cpp:38
static const QColor lightPaletteColors[7]
static QString resolveNcr(QStringView str)
static int calcMergeScore(const DataModel *one, const DataModel *two)
static QString showNcr(const QString &str)
static const QColor darkPaletteColors[7]
static QString adjustNcrVisibility(const QString &str, bool ncrMode)
TranslationType
@ IDBASED
@ TEXTBASED
MultiGroupItem(int oldCount, GroupItem *groupItem, bool writable)
int messageCount() const
MultiMessageItem * multiMessageItem(int msgIdx) const
GroupItem * groupItem(int model) const
int findMessage(const QString &sourcetext, const QString &comment) const
int getNumEditable() const
int findMessageById(const QString &id) const
int getNumFinished() const
MessageItem * messageItem(int model, int msgIdx) const
int firstNonobsoleteMessageIndex(int msgIdx) const
int countUnfinished() const
int countEditable() const
bool isUnfinished() const
int unfinishedMsgNoDanger
Definition statistics.h:26
int translatedMsgNoDanger
Definition statistics.h:23
int unfinishedMsgDanger
Definition statistics.h:27
int translatedMsgDanger
Definition statistics.h:24
#define ContextComment
Definition translator.h:226
TranslatorSaveMode