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(), 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 const auto &translations = mi->translations();
561 for (const QString &trnsl : translations) {
562 doCharCounting(trnsl, stats.wordsFinished, stats.charsFinished, stats.charsSpacesFinished);
563 hasDanger |= mi->danger();
564 }
565 if (hasDanger)
567 else
569 } else if (mi->isUnfinished()) {
570 bool hasDanger = false;
571 const auto &translations = mi->translations();
572 for (const QString &trnsl : translations) {
573 doCharCounting(trnsl, stats.wordsUnfinished, stats.charsUnfinished, stats.charsSpacesUnfinished);
574 hasDanger |= mi->danger();
575 }
576 if (hasDanger)
578 else
580 }
581 };
582
583 for (DataModelIterator it(IDBASED, this); it.isValid(); ++it)
584 updateMessageStatistics(it.current());
586 updateMessageStatistics(it.current());
587
588 stats.wordsSource = m_srcWords;
589 stats.charsSource = m_srcChars;
590 stats.charsSpacesSource = m_srcCharsSpc;
591 emit statsChanged(stats);
592}
593
594void DataModel::setModified(bool isModified)
595{
596 if (m_modified == isModified)
597 return;
598 m_modified = isModified;
599 emit modifiedChanged();
600}
601
602QString DataModel::prettifyPlainFileName(const QString &fn)
603{
604 static QString workdir = QDir::currentPath() + u'/';
605
606 return QDir::toNativeSeparators(fn.startsWith(workdir) ? fn.mid(workdir.size()) : fn);
607}
608
609QString DataModel::prettifyFileName(const QString &fn)
610{
611 if (fn.startsWith(u'='))
612 return u'=' + prettifyPlainFileName(fn.mid(1));
613 else
614 return prettifyPlainFileName(fn);
615}
616
617/******************************************************************************
618 *
619 * DataModelIterator
620 *
621 *****************************************************************************/
622
624 int message)
625 : DataIndex(type, group, message), m_model(model)
626{
627}
628
630{
631 const qsizetype size =
632 isIdBased() ? m_model->m_labelList.size() : m_model->m_contextList.size();
633 return m_group < size;
634}
635
637{
638 ++m_message;
639 const qsizetype size = isIdBased() ? m_model->m_labelList.at(m_group).messageCount()
640 : m_model->m_contextList.at(m_group).messageCount();
641 if (m_message >= size) {
642 ++m_group;
643 m_message = 0;
644 }
645}
646
648{
649 return m_model->messageItem(*this);
650}
651
652/******************************************************************************
653 *
654 * MultiGroupItem
655 *
656 *****************************************************************************/
657
658MultiGroupItem::MultiGroupItem(int oldCount, GroupItem *groupItem, bool writable)
659 : m_group(groupItem->group()),
661 m_translationType(groupItem->translationType())
662{
663 QList<MessageItem *> mList;
664 QList<MessageItem *> eList;
665 for (int j = 0; j < groupItem->messageCount(); ++j) {
666 MessageItem *m = groupItem->messageItem(j);
667 mList.append(m);
668 eList.append(0);
669 m_multiMessageList.append(MultiMessageItem(m));
670 }
671 for (int i = 0; i < oldCount; ++i) {
672 m_messageLists.append(eList);
673 m_writableMessageLists.append(0);
674 m_groupList.append(0);
675 }
676 m_messageLists.append(mList);
677 m_writableMessageLists.append(writable ? &m_messageLists.last() : 0);
678 m_groupList.append(groupItem);
679}
680
681void MultiGroupItem::appendEmptyModel()
682{
683 QList<MessageItem *> eList;
684 for (int j = 0; j < messageCount(); ++j)
685 eList.append(0);
686 m_messageLists.append(eList);
687 m_writableMessageLists.append(0);
688 m_groupList.append(0);
689}
690
691void MultiGroupItem::assignLastModel(GroupItem *g, bool writable)
692{
693 if (writable)
694 m_writableMessageLists.last() = &m_messageLists.last();
695 m_groupList.last() = g;
696}
697
698// XXX this is not needed, yet
699void MultiGroupItem::moveModel(int oldPos, int newPos)
700{
701 m_groupList.insert(newPos, m_groupList[oldPos]);
702 m_messageLists.insert(newPos, m_messageLists[oldPos]);
703 m_writableMessageLists.insert(newPos, m_writableMessageLists[oldPos]);
704 removeModel(oldPos < newPos ? oldPos : oldPos + 1);
705}
706
707void MultiGroupItem::removeModel(int pos)
708{
709 m_groupList.removeAt(pos);
710 m_messageLists.removeAt(pos);
711 m_writableMessageLists.removeAt(pos);
712}
713
714void MultiGroupItem::putMessageItem(int pos, MessageItem *m)
715{
716 m_messageLists.last()[pos] = m;
717}
718
719void MultiGroupItem::appendMessageItems(const QList<MessageItem *> &m)
720{
721 QList<MessageItem *> nullItems = m; // Basically, just a reservation
722 for (int i = 0; i < nullItems.size(); ++i)
723 nullItems[i] = 0;
724 for (int i = 0; i < m_messageLists.size() - 1; ++i)
725 m_messageLists[i] += nullItems;
726 m_messageLists.last() += m;
727 for (MessageItem *mi : m)
728 m_multiMessageList.append(MultiMessageItem(mi));
729}
730
731void MultiGroupItem::removeMultiMessageItem(int pos)
732{
733 for (int i = 0; i < m_messageLists.size(); ++i)
734 m_messageLists[i].removeAt(pos);
735 m_multiMessageList.removeAt(pos);
736}
737
739{
740 for (int i = 0; i < m_messageLists.size(); ++i)
741 if (m_messageLists[i][msgIdx] && !m_messageLists[i][msgIdx]->isObsolete())
742 return i;
743 return -1;
744}
745
746int MultiGroupItem::findMessage(const QString &sourcetext, const QString &comment) const
747{
748 for (int i = 0, cnt = messageCount(); i < cnt; ++i) {
750 if (m->text() == sourcetext && m->comment() == comment)
751 return i;
752 }
753 return -1;
754}
755
756int MultiGroupItem::findMessageById(const QString &id) const
757{
758 for (int i = 0, cnt = messageCount(); i < cnt; ++i) {
760 if (m->id() == id)
761 return i;
762 }
763 return -1;
764}
765
766/******************************************************************************
767 *
768 * MultiDataModel
769 *
770 *****************************************************************************/
771
773 QColor(210, 235, 250), // blue
774 QColor(210, 250, 220), // green
775 QColor(250, 240, 210), // yellow
776 QColor(210, 250, 250), // cyan
777 QColor(250, 230, 200), // orange
778 QColor(250, 210, 210), // red
779 QColor(235, 210, 250), // purple
780};
781
782static const QColor darkPaletteColors[7] = {
783 QColor(60, 80, 100), // blue
784 QColor(50, 90, 70), // green
785 QColor(100, 90, 50), // yellow
786 QColor(50, 90, 90), // cyan
787 QColor(100, 70, 50), // orange
788 QColor(90, 50, 50), // red
789 QColor(70, 50, 90), // purple
790};
791
792MultiDataModel::MultiDataModel(QObject *parent) :
793 QObject(parent),
794 m_numFinished(0),
795 m_numEditable(0),
796 m_numMessages(0),
797 m_modified(false)
798{
800
801 m_bitmap = QBitmap(8, 8);
802 m_bitmap.clear();
803 QPainter p(&m_bitmap);
804 for (int j = 0; j < 8; ++j)
805 for (int k = 0; k < 8; ++k)
806 if ((j + k) & 4)
807 p.drawPoint(j, k);
808}
809
811{
812 qDeleteAll(m_dataModels);
813}
814
816{
817 QBrush brush(m_colors[model % 7]);
818 if (!isModelWritable(model))
819 brush.setTexture(m_bitmap);
820 return brush;
821}
822
824{
825 m_colors = isDarkMode() ? darkPaletteColors : lightPaletteColors;
826}
827
829{
831 return true;
832
833 int inBothNew = 0;
834
835 auto countInBothNew = [dm, &inBothNew, this](int count, TranslationType type) {
836 for (int i = 0; i < count; ++i) {
837 GroupItem *g = dm->groupItem(i, type);
838 if (MultiGroupItem *mgi = findGroup(g->group(), type)) {
839 for (int j = 0; j < g->messageCount(); ++j) {
841 // During merging, when calculating the well-mergeability ratio,
842 // we reward ID-based messages with the same IDs for having the same label.
843 // This is not a strict requirement, since linguists can still represent
844 // merging of the messages with the same ID and different labels reasonably.
845 // However, if too many messages share the same ID but have different labels,
846 // merging them provides little value and may reduce user convenience.
847 if ((type == TEXTBASED && mgi->findMessage(m->text(), m->comment()) >= 0)
848 || (type == IDBASED && mgi->findMessageById(m->id()) >= 0))
849 ++inBothNew;
850 }
851 }
852 }
853 };
854
855 countInBothNew(dm->contextCount(), TEXTBASED);
856 countInBothNew(dm->labelCount(), IDBASED);
857
858 int newRatio = inBothNew * 100 / dm->messageCount();
859 int inBothOld = 0;
860
861 auto countInBothOld = [this, dm, &inBothOld](int count, TranslationType type) {
862 for (int k = 0; k < count; ++k) {
864 if (GroupItem *g = dm->findGroup(mgi->group(), type)) {
865 for (int j = 0; j < mgi->messageCount(); ++j) {
867 if ((type == TEXTBASED && g->findMessage(m->text(), m->comment()))
868 || (type == IDBASED && g->findMessageById(m->id())))
869 ++inBothOld;
870 }
871 }
872 }
873 };
874
875 countInBothOld(contextCount(), TEXTBASED);
876 countInBothOld(labelCount(), IDBASED);
877
878 int oldRatio = inBothOld * 100 / messageCount();
879
880 return newRatio + oldRatio > 90;
881}
882
883void MultiDataModel::append(DataModel *dm, bool readWrite)
884{
885 int insCol = modelCount() + 1;
886 m_dataModels.append(dm);
887
888 auto appendGroups = [this, dm, readWrite, insCol](TranslationType type, MessageModel *msgModel,
889 QList<MultiGroupItem> &multiGroupList) {
890 qsizetype count = type == IDBASED ? labelCount() : contextCount();
891 msgModel->beginInsertColumns(QModelIndex(), insCol, insCol);
892 for (int j = 0; j < count; ++j) {
893 msgModel->beginInsertColumns(msgModel->createIndex(j, 0), insCol, insCol);
894 multiGroupList[j].appendEmptyModel();
895 msgModel->endInsertColumns();
896 }
897 msgModel->endInsertColumns();
898 count = type == IDBASED ? dm->labelCount() : dm->contextCount();
899 int appendedGroups = 0;
900 for (int i = 0; i < count; ++i) {
901 GroupItem *g = dm->groupItem(i, type);
902 int gidx = findGroupIndex(g->group(), type);
903 if (gidx >= 0) {
904 MultiGroupItem *mgi = multiGroupItem(gidx, type);
905 mgi->assignLastModel(g, readWrite);
906 QList<MessageItem *> appendItems;
907 for (int j = 0; j < g->messageCount(); ++j) {
909
910 int msgIdx = type == IDBASED ? mgi->findMessageById(m->id())
911 : mgi->findMessage(m->text(), m->comment());
912
913 if (msgIdx >= 0)
914 mgi->putMessageItem(msgIdx, m);
915 else
916 appendItems << m;
917 }
918 if (!appendItems.isEmpty()) {
919 int msgCnt = mgi->messageCount();
920 msgModel->beginInsertRows(msgModel->createIndex(gidx, 0), msgCnt,
921 msgCnt + appendItems.size() - 1);
922 mgi->appendMessageItems(appendItems);
923 msgModel->endInsertRows();
924 m_numMessages += appendItems.size();
925 }
926 } else {
927 multiGroupList << MultiGroupItem(modelCount() - 1, g, readWrite);
928 m_numMessages += g->messageCount();
929 ++appendedGroups;
930 }
931 }
932 if (appendedGroups) {
933 // Do that en block to avoid itemview inefficiency. It doesn't hurt that we
934 // announce the availability of the data "long" after it was actually added.
935 const qsizetype groupCount = type == IDBASED ? labelCount() : contextCount();
936 msgModel->beginInsertRows(QModelIndex(), groupCount - appendedGroups, groupCount - 1);
937 msgModel->endInsertRows();
938 }
939 };
940
941 appendGroups(TEXTBASED, m_textBasedMsgModel, m_multiContextList);
942 appendGroups(IDBASED, m_idBasedMsgModel, m_multiLabelList);
943
944 dm->setWritable(readWrite);
945 updateCountsOnAdd(modelCount() - 1, readWrite);
946 connect(dm, &DataModel::modifiedChanged,
947 this, &MultiDataModel::onModifiedChanged);
948 connect(dm, &DataModel::languageChanged,
949 this, &MultiDataModel::onLanguageChanged);
950 connect(dm, &DataModel::statsChanged,
952 emit modelAppended();
953}
954
955void MultiDataModel::close(int model)
956{
957 if (m_dataModels.size() == 1) {
959 } else {
960 int delCol = model + 1;
961 auto removeModel = [delCol, model](auto *msgModel, auto &list) {
962 msgModel->beginRemoveColumns(QModelIndex(), delCol, delCol);
963 for (int i = list.size(); --i >= 0;) {
964 msgModel->beginRemoveColumns(msgModel->createIndex(i, 0), delCol, delCol);
965 list[i].removeModel(model);
966 msgModel->endRemoveColumns();
967 }
968 msgModel->endRemoveColumns();
969 };
970
971 updateCountsOnRemove(model, isModelWritable(model));
972 removeModel(m_idBasedMsgModel, m_multiLabelList);
973 removeModel(m_textBasedMsgModel, m_multiContextList);
974 delete m_dataModels.takeAt(model);
975 emit modelDeleted(model);
976
977 auto removeMessages = [this](auto *msgModel, auto &list) {
978 for (int i = list.size(); --i >= 0;) {
979 auto &mi = list[i];
980 QModelIndex idx = msgModel->createIndex(i, 0);
981 for (int j = mi.messageCount(); --j >= 0;)
982 if (mi.multiMessageItem(j)->isEmpty()) {
983 msgModel->beginRemoveRows(idx, j, j);
984 mi.removeMultiMessageItem(j);
985 msgModel->endRemoveRows();
986 --m_numMessages;
987 }
988 if (!mi.messageCount()) {
989 msgModel->beginRemoveRows(QModelIndex(), i, i);
990 list.removeAt(i);
991 msgModel->endRemoveRows();
992 }
993 }
994 };
995
996 removeMessages(m_idBasedMsgModel, m_multiLabelList);
997 removeMessages(m_textBasedMsgModel, m_multiContextList);
998 onModifiedChanged();
999 }
1000}
1001
1003{
1004 m_idBasedMsgModel->beginResetModel();
1005 m_textBasedMsgModel->beginResetModel();
1006 m_numFinished = 0;
1007 m_numEditable = 0;
1008 m_numMessages = 0;
1009 qDeleteAll(m_dataModels);
1010 m_dataModels.clear();
1011 m_multiContextList.clear();
1012 m_multiLabelList.clear();
1013 m_textBasedMsgModel->endResetModel();
1014 m_idBasedMsgModel->endResetModel();
1015 emit allModelsDeleted();
1016 onModifiedChanged();
1017}
1018
1019// XXX this is not needed, yet
1020void MultiDataModel::moveModel(int oldPos, int newPos)
1021{
1022 int delPos = oldPos < newPos ? oldPos : oldPos + 1;
1023 m_dataModels.insert(newPos, m_dataModels[oldPos]);
1024 m_dataModels.removeAt(delPos);
1025 for (int i = 0; i < m_multiContextList.size(); ++i)
1026 m_multiContextList[i].moveModel(oldPos, newPos);
1027 for (int i = 0; i < m_multiLabelList.size(); ++i)
1028 m_multiLabelList[i].moveModel(oldPos, newPos);
1029}
1030
1031QStringList MultiDataModel::prettifyFileNames(const QStringList &names)
1032{
1033 QStringList out;
1034
1035 for (const QString &name : names)
1036 out << DataModel::prettifyFileName(name);
1037 return out;
1038}
1039
1040QString MultiDataModel::condenseFileNames(const QStringList &names)
1041{
1042 if (names.isEmpty())
1043 return QString();
1044
1045 if (names.size() < 2)
1046 return names.first();
1047
1048 QString prefix = names.first();
1049 if (prefix.startsWith(u'='))
1050 prefix.remove(0, 1);
1051 QString suffix = prefix;
1052 for (int i = 1; i < names.size(); ++i) {
1053 QString fn = names[i];
1054 if (fn.startsWith(u'='))
1055 fn.remove(0, 1);
1056 for (int j = 0; j < prefix.size(); ++j)
1057 if (fn[j] != prefix[j]) {
1058 if (j < prefix.size()) {
1059 while (j > 0 && prefix[j - 1].isLetterOrNumber())
1060 --j;
1061 prefix.truncate(j);
1062 }
1063 break;
1064 }
1065 int fnl = fn.size() - 1;
1066 int sxl = suffix.size() - 1;
1067 for (int k = 0; k <= sxl; ++k)
1068 if (fn[fnl - k] != suffix[sxl - k]) {
1069 if (k < sxl) {
1070 while (k > 0 && suffix[sxl - k + 1].isLetterOrNumber())
1071 --k;
1072 if (prefix.size() + k > fnl)
1073 --k;
1074 suffix.remove(0, sxl - k + 1);
1075 }
1076 break;
1077 }
1078 }
1079 QString ret = prefix + u'{';
1080 int pxl = prefix.size();
1081 int sxl = suffix.size();
1082 for (int j = 0; j < names.size(); ++j) {
1083 if (j)
1084 ret += u',';
1085 int off = pxl;
1086 QString fn = names[j];
1087 if (fn.startsWith(u'=')) {
1088 ret += u'=';
1089 ++off;
1090 }
1091 ret += fn.mid(off, fn.size() - sxl - off);
1092 }
1093 ret += u'}' + suffix;
1094 return ret;
1095}
1096
1098{
1099 QStringList names;
1100 for (DataModel *dm : m_dataModels)
1101 names << (dm->isWritable() ? QString() : QString::fromLatin1("=")) + dm->srcFileName(pretty);
1102 return names;
1103}
1104
1106{
1107 return condenseFileNames(srcFileNames(pretty));
1108}
1109
1114
1116{
1117 for (const DataModel *mdl : m_dataModels)
1118 if (mdl->isModified())
1119 return true;
1120 return false;
1121}
1122
1123void MultiDataModel::onModifiedChanged()
1124{
1125 bool modified = isModified();
1126 if (modified != m_modified) {
1127 emit modifiedChanged(modified);
1128 m_modified = modified;
1129 }
1130}
1131
1132void MultiDataModel::onLanguageChanged()
1133{
1134 int i = 0;
1135 while (sender() != m_dataModels[i])
1136 ++i;
1137 emit languageChanged(i);
1138}
1139
1140GroupItem *MultiDataModel::groupItem(const MultiDataIndex &index) const
1141{
1143}
1144
1145int MultiDataModel::isFileLoaded(const QString &name) const
1146{
1147 for (int i = 0; i < m_dataModels.size(); ++i)
1148 if (m_dataModels[i]->srcFileName() == name)
1149 return i;
1150 return -1;
1151}
1152
1153int MultiDataModel::findGroupIndex(const QString &group, TranslationType type) const
1154{
1155 const auto &list = type == IDBASED ? m_multiLabelList : m_multiContextList;
1156 for (int i = 0; i < list.size(); ++i) {
1157 const MultiGroupItem &mg = list[i];
1158 if (mg.group() == group)
1159 return i;
1160 }
1161 return -1;
1162}
1163
1164MultiGroupItem *MultiDataModel::findGroup(const QString &group, TranslationType type) const
1165{
1166 const auto &list = type == IDBASED ? m_multiLabelList : m_multiContextList;
1167 for (int i = 0; i < list.size(); ++i) {
1168 const MultiGroupItem &mgi = list[i];
1169 if (mgi.group() == group)
1170 return const_cast<MultiGroupItem *>(&mgi);
1171 }
1172 return 0;
1173}
1174
1176{
1177 const auto &list = index.isIdBased() ? m_multiLabelList : m_multiContextList;
1178 return const_cast<MultiGroupItem *>(&list[index.group()]);
1179}
1180
1182{
1183 qsizetype groupCount = index.isIdBased() ? labelCount() : contextCount();
1184 if (index.group() < groupCount && index.group() >= 0 && model >= 0 && model < modelCount()) {
1185 MultiGroupItem *mgi = multiGroupItem(index);
1186 if (index.message() < mgi->messageCount())
1187 return mgi->messageItem(model, index.message());
1188 }
1189 Q_ASSERT(model >= 0 && model < modelCount());
1190 Q_ASSERT(index.group() < groupCount);
1191 return 0;
1192}
1193
1194void MultiDataModel::setTranslation(const MultiDataIndex &index, const QString &translation)
1195{
1196 MessageItem *m = messageItem(index);
1197 if (translation == m->translation())
1198 return;
1199 m->setTranslation(translation);
1200 setModified(index.model(), true);
1201 emit translationChanged(index);
1202}
1203
1204void MultiDataModel::setTranslations(const MultiDataIndex &index, const QStringList &translations)
1205{
1206 MessageItem *m = messageItem(index);
1207 if (translations == m->translations())
1208 return;
1209 m->setTranslations(translations);
1210 setModified(index.model(), true);
1211 emit translationChanged(index);
1212}
1213
1214void MultiDataModel::setFinished(const MultiDataIndex &index, bool finished)
1215{
1216 MultiGroupItem *mgi = multiGroupItem(index);
1218 GroupItem *gi = groupItem(index);
1219 MessageItem *m = messageItem(index);
1221 if (type == TranslatorMessage::Unfinished && finished) {
1223 mm->decrementUnfinishedCount();
1224 if (!mm->countUnfinished()) {
1225 incrementFinishedCount();
1226 mgi->incrementFinishedCount();
1227 emit multiGroupDataChanged(index);
1228 }
1229 gi->incrementFinishedCount();
1230 if (m->danger()) {
1231 gi->incrementFinishedDangerCount();
1232 gi->decrementUnfinishedDangerCount();
1234 emit groupDataChanged(index);
1235 } else if (gi->finishedCount() == gi->nonobsoleteCount()) {
1236 emit groupDataChanged(index);
1237 }
1238 emit messageDataChanged(index);
1239 setModified(index.model(), true);
1240 } else if (type == TranslatorMessage::Finished && !finished) {
1242 mm->incrementUnfinishedCount();
1243 if (mm->countUnfinished() == 1) {
1244 decrementFinishedCount();
1245 mgi->decrementFinishedCount();
1246 emit multiGroupDataChanged(index);
1247 }
1248 gi->decrementFinishedCount();
1249 if (m->danger()) {
1250 gi->decrementFinishedDangerCount();
1251 gi->incrementUnfinishedDangerCount();
1252 if (gi->unfinishedDangerCount() == 1
1254 emit groupDataChanged(index);
1255 } else if (gi->finishedCount() + 1 == gi->nonobsoleteCount()) {
1256 emit groupDataChanged(index);
1257 }
1258 emit messageDataChanged(index);
1259 setModified(index.model(), true);
1260 }
1261}
1262
1263void MultiDataModel::setDanger(const MultiDataIndex &index, bool danger)
1264{
1265 GroupItem *gi = groupItem(index);
1266 MessageItem *m = messageItem(index);
1267 if (!m->danger() && danger) {
1268 if (m->isFinished()) {
1269 gi->incrementFinishedDangerCount();
1270 if (gi->finishedDangerCount() == 1)
1271 emit groupDataChanged(index);
1272 } else {
1273 gi->incrementUnfinishedDangerCount();
1274 if (gi->unfinishedDangerCount() == 1)
1275 emit groupDataChanged(index);
1276 }
1277 emit messageDataChanged(index);
1278 m->setDanger(danger);
1279 } else if (m->danger() && !danger) {
1280 if (m->isFinished()) {
1281 gi->decrementFinishedDangerCount();
1283 emit groupDataChanged(index);
1284 } else {
1285 gi->decrementUnfinishedDangerCount();
1287 emit groupDataChanged(index);
1288 }
1289 emit messageDataChanged(index);
1290 m->setDanger(danger);
1291 }
1292}
1293
1294void MultiDataModel::updateCountsOnAdd(int model, bool writable)
1295{
1296 auto updateCount = [model, writable, this](auto &mg) {
1297 for (int j = 0; j < mg.messageCount(); ++j)
1298 if (MessageItem *m = mg.messageItem(model, j)) {
1299 MultiMessageItem *mm = mg.multiMessageItem(j);
1300 mm->incrementNonnullCount();
1301 if (!m->isObsolete()) {
1302 if (writable) {
1303 if (!mm->countEditable()) {
1304 mg.incrementEditableCount();
1305 incrementEditableCount();
1306 if (m->isFinished()) {
1307 mg.incrementFinishedCount();
1308 incrementFinishedCount();
1309 } else {
1310 mm->incrementUnfinishedCount();
1311 }
1312 } else if (!m->isFinished()) {
1313 if (!mm->isUnfinished()) {
1314 mg.decrementFinishedCount();
1315 decrementFinishedCount();
1316 }
1317 mm->incrementUnfinishedCount();
1318 }
1319 mm->incrementEditableCount();
1320 }
1321 mg.incrementNonobsoleteCount();
1322 mm->incrementNonobsoleteCount();
1323 }
1324 }
1325 };
1326 for (auto &mg : m_multiContextList)
1327 updateCount(mg);
1328 for (auto &mg : m_multiLabelList)
1329 updateCount(mg);
1330}
1331
1332void MultiDataModel::updateCountsOnRemove(int model, bool writable)
1333{
1334 auto updateCount = [model, writable, this](auto &mg) {
1335 for (int j = 0; j < mg.messageCount(); ++j)
1336 if (MessageItem *m = mg.messageItem(model, j)) {
1337 MultiMessageItem *mm = mg.multiMessageItem(j);
1338 mm->decrementNonnullCount();
1339 if (!m->isObsolete()) {
1340 mm->decrementNonobsoleteCount();
1341 mg.decrementNonobsoleteCount();
1342 if (writable) {
1343 mm->decrementEditableCount();
1344 if (!mm->countEditable()) {
1345 mg.decrementEditableCount();
1346 decrementEditableCount();
1347 if (m->isFinished()) {
1348 mg.decrementFinishedCount();
1349 decrementFinishedCount();
1350 } else {
1351 mm->decrementUnfinishedCount();
1352 }
1353 } else if (!m->isFinished()) {
1354 mm->decrementUnfinishedCount();
1355 if (!mm->isUnfinished()) {
1356 mg.incrementFinishedCount();
1357 incrementFinishedCount();
1358 }
1359 }
1360 }
1361 }
1362 }
1363 };
1364 for (auto &mg : m_multiContextList)
1365 updateCount(mg);
1366 for (auto &mg : m_multiLabelList)
1367 updateCount(mg);
1368}
1369
1370/******************************************************************************
1371 *
1372 * MultiDataModelIterator
1373 *
1374 *****************************************************************************/
1375
1377 int model, int group, int message)
1378 : MultiDataIndex(type, model, group, message), m_dataModel(dataModel)
1379{
1380}
1381
1383{
1384 Q_ASSERT(isValid());
1385 ++m_message;
1386 const qsizetype count = isIdBased()
1387 ? m_dataModel->m_multiLabelList.at(m_group).messageCount()
1388 : m_dataModel->m_multiContextList.at(m_group).messageCount();
1389 if (m_message >= count) {
1390 ++m_group;
1391 m_message = 0;
1392 }
1393}
1394
1396{
1397 const qsizetype size = isIdBased() ? m_dataModel->m_multiLabelList.size()
1398 : m_dataModel->m_multiContextList.size();
1399 return m_group < size;
1400}
1401
1403{
1404 return m_dataModel->messageItem(*this);
1405}
1406
1407/******************************************************************************
1408 *
1409 * MessageModel
1410 *
1411 *****************************************************************************/
1412
1413MessageModel::MessageModel(TranslationType translationType, QObject *parent, MultiDataModel *data)
1414 : QAbstractItemModel(parent), m_data(data), m_translationType(translationType)
1415{
1416 if (translationType == IDBASED)
1417 data->m_idBasedMsgModel = this;
1418 else
1419 data->m_textBasedMsgModel = this;
1420 connect(m_data, &MultiDataModel::multiGroupDataChanged, this,
1421 &MessageModel::multiGroupItemChanged);
1422 connect(m_data, &MultiDataModel::groupDataChanged, this, &MessageModel::groupItemChanged);
1423 connect(m_data, &MultiDataModel::messageDataChanged, this, &MessageModel::messageItemChanged);
1424}
1425
1426QModelIndex MessageModel::index(int row, int column, const QModelIndex &parent) const
1427{
1428 if (!parent.isValid())
1429 return createIndex(row, column);
1430 if (!parent.internalId())
1431 return createIndex(row, column, parent.row() + 1);
1432 return QModelIndex();
1433}
1434
1435QModelIndex MessageModel::parent(const QModelIndex& index) const
1436{
1437 if (index.internalId())
1438 return createIndex(index.internalId() - 1, 0);
1439 return QModelIndex();
1440}
1441
1442void MessageModel::multiGroupItemChanged(const MultiDataIndex &index)
1443{
1444 if (index.translationType() != m_translationType)
1445 return;
1446 QModelIndex idx = createIndex(index.group(), m_data->modelCount() + 2);
1447 emit dataChanged(idx, idx);
1448}
1449
1450void MessageModel::groupItemChanged(const MultiDataIndex &index)
1451{
1452 if (index.translationType() != m_translationType)
1453 return;
1454 QModelIndex idx = createIndex(index.group(), index.model() + 1);
1455 emit dataChanged(idx, idx);
1456}
1457
1458void MessageModel::messageItemChanged(const MultiDataIndex &index)
1459{
1460 if (index.translationType() != m_translationType)
1461 return;
1462 QModelIndex idx = createIndex(index.message(), index.model() + 1, index.group() + 1);
1463 emit dataChanged(idx, idx);
1464}
1465
1467{
1468 if (index.message() < 0) // Should be unused case
1469 return createIndex(index.group(), index.model() + 1);
1470 return createIndex(index.message(), index.model() + 1, index.group() + 1);
1471}
1472
1473int MessageModel::rowCount(const QModelIndex &parent) const
1474{
1475 if (!parent.isValid())
1476 return m_translationType == IDBASED ? m_data->labelCount()
1477 : m_data->contextCount(); // contexts
1478 if (!parent.internalId()) // messages
1479 return m_data->multiGroupItem(parent.row(), m_translationType)->messageCount();
1480 return 0;
1481}
1482
1483int MessageModel::columnCount(const QModelIndex &) const
1484{
1485 return m_data->modelCount() + 3;
1486}
1487
1488QVariant MessageModel::data(const QModelIndex &index, int role) const
1489{
1490 static QVariant pxOn;
1491 static QVariant pxOff;
1492 static QVariant pxObsolete;
1493 static QVariant pxDanger;
1494 static QVariant pxWarning;
1495 static QVariant pxEmpty;
1496
1497 static Qt::ColorScheme mode = Qt::ColorScheme::Unknown; // to prevent creating new QPixmaps
1498 // every time the method is called
1499
1500 if (bool dark = isDarkMode();
1501 (dark && mode != Qt::ColorScheme::Dark) || (!dark && mode != Qt::ColorScheme::Light)) {
1502 pxOn = createMarkIcon(TranslationMarks::OnMark, dark);
1503 pxOff = createMarkIcon(TranslationMarks::OffMark, dark);
1504 pxObsolete = createMarkIcon(TranslationMarks::ObsoleteMark, dark);
1505 pxDanger = createMarkIcon(TranslationMarks::DangerMark, dark);
1506 pxWarning = createMarkIcon(TranslationMarks::WarningMark, dark);
1507 pxEmpty = createMarkIcon(TranslationMarks::EmptyMark, dark);
1508 mode = dark ? Qt::ColorScheme::Dark : Qt::ColorScheme::Light;
1509 }
1510
1511 int row = index.row();
1512 int column = index.column() - 1;
1513
1514 if (column < 0)
1515 return QVariant();
1516
1517 int numLangs = m_data->modelCount();
1518
1519 if (role == Qt::ToolTipRole && column < numLangs) {
1520 return tr("Completion status for %1").arg(m_data->model(column)->localizedLanguage());
1521 } else if (index.internalId()) {
1522 // this is a message
1523 int crow = index.internalId() - 1;
1524 MultiGroupItem *mgi = m_data->multiGroupItem(crow, m_translationType);
1525 if (row >= mgi->messageCount() || !index.isValid())
1526 return QVariant();
1527
1528 if (role == Qt::DisplayRole || (role == Qt::ToolTipRole && column == numLangs)) {
1529 switch (column - numLangs) {
1530 case 0: // Source text
1531 {
1532 if (m_translationType == IDBASED)
1533 return mgi->multiMessageItem(row)->id();
1534 else if (const QString text = mgi->multiMessageItem(row)->text(); !text.isEmpty())
1535 return text.simplified();
1536 else
1537 return tr("<context comment>");
1538 }
1539 case 1:
1540 if (m_translationType == IDBASED)
1541 return mgi->multiMessageItem(row)->text();
1542 Q_FALLTHROUGH();
1543 default: // Status or dummy column => no text
1544 return QVariant();
1545 }
1546 }
1547 else if (role == Qt::DecorationRole && column < numLangs) {
1548 if (MessageItem *msgItem = mgi->messageItem(column, row)) {
1549 switch (msgItem->message().type()) {
1551 if (msgItem->translation().isEmpty())
1552 return pxEmpty;
1553 if (msgItem->danger())
1554 return pxDanger;
1555 return pxOff;
1557 if (msgItem->danger())
1558 return pxWarning;
1559 return pxOn;
1560 default:
1561 return pxObsolete;
1562 }
1563 }
1564 return QVariant();
1565 }
1566 else if (role == SortRole) {
1567 switch (column - numLangs) {
1568 case 0: // Source text
1569 return mgi->multiMessageItem(row)->text().simplified().remove(u'&');
1570 case 1: // Dummy column
1571 return QVariant();
1572 default:
1573 if (MessageItem *msgItem = mgi->messageItem(column, row)) {
1574 int rslt = !msgItem->translation().isEmpty();
1575 if (!msgItem->danger())
1576 rslt |= 2;
1577 if (msgItem->isObsolete())
1578 rslt |= 8;
1579 else if (msgItem->isFinished())
1580 rslt |= 4;
1581 return rslt;
1582 }
1583 return INT_MAX;
1584 }
1585 } else if (role == Qt::ForegroundRole && column > 0
1586 && mgi->multiMessageItem(row)->isObsolete()) {
1587 return QBrush(Qt::darkGray);
1588 } else if (role == Qt::ForegroundRole && column == numLangs
1589 && mgi->multiMessageItem(row)->text().isEmpty()) {
1590 return QBrush(QColor(0, 0xa0, 0xa0));
1591 } else if (role == Qt::BackgroundRole) {
1592 if (column < numLangs && numLangs != 1)
1593 return m_data->brushForModel(column);
1594 }
1595 } else {
1596 // this is a context or a label
1597 const qsizetype groupCount =
1598 m_translationType == IDBASED ? m_data->labelCount() : m_data->contextCount();
1599 if (row >= groupCount || !index.isValid())
1600 return QVariant();
1601
1602 MultiGroupItem *mgi = m_data->multiGroupItem(row, m_translationType);
1603 if (role == Qt::DisplayRole || role == Qt::ToolTipRole) {
1604 switch (column - numLangs) {
1605 case 0: // Context/Label
1606 {
1607 const QString groupName = mgi->group().simplified();
1608 if (m_translationType == IDBASED and groupName.isEmpty()) {
1609 return tr("<unnamed label>");
1610 }
1611 return groupName;
1612 }
1613 case 1: {
1614 if (role == Qt::ToolTipRole) {
1615 return tr("%n unfinished message(s) left.", 0,
1617 }
1618 return QString::asprintf("%d/%d", mgi->getNumFinished(), mgi->getNumEditable());
1619 }
1620 default:
1621 return QVariant(); // Status => no text
1622 }
1623 }
1624 else if (role == Qt::FontRole && column == m_data->modelCount()) {
1625 QFont boldFont;
1626 boldFont.setBold(true);
1627 return boldFont;
1628 }
1629 else if (role == Qt::DecorationRole && column < numLangs) {
1630 if (GroupItem *groupItem = mgi->groupItem(column)) {
1631 if (groupItem->isObsolete())
1632 return pxObsolete;
1633 if (groupItem->isFinished())
1634 return groupItem->finishedDangerCount() > 0 ? pxWarning : pxOn;
1635 return groupItem->unfinishedDangerCount() > 0 ? pxDanger : pxOff;
1636 }
1637 return QVariant();
1638 }
1639 else if (role == SortRole) {
1640 switch (column - numLangs) {
1641 case 0: // Context/Label (same as display role)
1642 return mgi->group().simplified();
1643 case 1: // Items
1644 return mgi->getNumEditable();
1645 default: // Percent
1646 if (GroupItem *groupItem = mgi->groupItem(column)) {
1647 int totalItems = groupItem->nonobsoleteCount();
1648 int percent =
1649 totalItems ? (100 * groupItem->finishedCount()) / totalItems : 100;
1650 int rslt = percent * (((1 << 28) - 1) / 100) + totalItems;
1651 if (groupItem->isObsolete()) {
1652 rslt |= (1 << 30);
1653 } else if (groupItem->isFinished()) {
1654 rslt |= (1 << 29);
1655 if (!groupItem->finishedDangerCount())
1656 rslt |= (1 << 28);
1657 } else {
1658 if (!groupItem->unfinishedDangerCount())
1659 rslt |= (1 << 28);
1660 }
1661 return rslt;
1662 }
1663 return INT_MAX;
1664 }
1665 } else if (role == Qt::ForegroundRole && column >= numLangs && mgi->isObsolete()) {
1666 return QBrush(Qt::darkGray);
1667 } else if (role == Qt::ForegroundRole && column == numLangs
1668 && m_translationType == IDBASED) {
1669 return QBrush(QColor(0, 0xa0, 0xa0));
1670 } else if (role == Qt::BackgroundRole) {
1671 if (column < numLangs && numLangs != 1) {
1672 QBrush brush = m_data->brushForModel(column);
1673 if (row & 1) {
1674 brush.setColor(brush.color().darker(108));
1675 }
1676 return brush;
1677 }
1678 }
1679 }
1680 return QVariant();
1681}
1682
1683MultiDataIndex MessageModel::dataIndex(const QModelIndex &index, int model) const
1684{
1685 Q_ASSERT(index.isValid());
1686 Q_ASSERT(index.internalId());
1687 return MultiDataIndex(m_translationType, model, index.internalId() - 1, index.row());
1688}
1689
1690QT_END_NAMESPACE
TranslatorSaveMode m_saveMode
Definition translator.h:61
bool m_ignoreUnfinished
Definition translator.h:57
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()
void modifiedChanged()
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 setTranslations(const MultiDataIndex &index, const QStringList &translations)
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
void statsChanged(const StatisticalData &newStats)
void multiGroupDataChanged(const MultiDataIndex &index)
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:124
void setExtras(const ExtraData &extras)
Definition translator.h:156
void append(const TranslatorMessage &msg)
void setLocationsType(LocationsType lt)
Definition translator.h:123
@ RelativeLocations
Definition translator.h:122
@ AbsoluteLocations
Definition translator.h:122
int messageCount() const
Definition translator.h:137
Duplicates resolveDuplicates()
void normalizeTranslations(ConversionData &cd)
const ExtraData & extras() const
Definition translator.h:155
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:224
TranslatorSaveMode