diff options
Diffstat (limited to 'src/linguist/linguist/messagemodel.cpp')
-rw-r--r-- | src/linguist/linguist/messagemodel.cpp | 1426 |
1 files changed, 1426 insertions, 0 deletions
diff --git a/src/linguist/linguist/messagemodel.cpp b/src/linguist/linguist/messagemodel.cpp new file mode 100644 index 000000000..2d126577c --- /dev/null +++ b/src/linguist/linguist/messagemodel.cpp @@ -0,0 +1,1426 @@ +/**************************************************************************** +** +** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). +** All rights reserved. +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** This file is part of the Qt Linguist of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** No Commercial Usage +** This file contains pre-release code and may not be distributed. +** You may use this file in accordance with the terms and conditions +** contained in the Technology Preview License Agreement accompanying +** this package. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** If you have questions regarding the use of this file, please contact +** Nokia at qt-info@nokia.com. +** +** +** +** +** +** +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "messagemodel.h" + +#include <QtCore/QCoreApplication> +#include <QtCore/QDebug> +#include <QtCore/QTextCodec> + +#include <QtGui/QMessageBox> +#include <QtGui/QPainter> +#include <QtGui/QPixmap> +#include <QtGui/QTextDocument> + +#include <private/qtranslator_p.h> + +#include <limits.h> + +QT_BEGIN_NAMESPACE + +/****************************************************************************** + * + * MessageItem + * + *****************************************************************************/ + +MessageItem::MessageItem(const TranslatorMessage &message) + : m_message(message), + m_danger(false) +{ + if (m_message.translation().isEmpty()) + m_message.setTranslation(QString()); +} + + +bool MessageItem::compare(const QString &findText, bool matchSubstring, + Qt::CaseSensitivity cs) const +{ + return matchSubstring + ? text().indexOf(findText, 0, cs) >= 0 + : text().compare(findText, cs) == 0; +} + +/****************************************************************************** + * + * ContextItem + * + *****************************************************************************/ + +ContextItem::ContextItem(const QString &context) + : m_context(context), + m_finishedCount(0), + m_finishedDangerCount(0), + m_unfinishedDangerCount(0), + m_nonobsoleteCount(0) +{} + +void ContextItem::appendToComment(const QString &str) +{ + if (!m_comment.isEmpty()) + m_comment += QLatin1String("\n\n"); + m_comment += str; +} + +MessageItem *ContextItem::messageItem(int i) const +{ + if (i >= 0 && i < msgItemList.count()) + return const_cast<MessageItem *>(&msgItemList[i]); + Q_ASSERT(i >= 0 && i < msgItemList.count()); + return 0; +} + +MessageItem *ContextItem::findMessage(const QString &sourcetext, const QString &comment) const +{ + for (int i = 0; i < messageCount(); ++i) { + MessageItem *mi = messageItem(i); + if (mi->text() == sourcetext && mi->comment() == comment) + return mi; + } + return 0; +} + +/****************************************************************************** + * + * DataModel + * + *****************************************************************************/ + +DataModel::DataModel(QObject *parent) + : QObject(parent), + m_modified(false), + m_numMessages(0), + m_srcWords(0), + m_srcChars(0), + m_srcCharsSpc(0), + m_language(QLocale::Language(-1)), + m_sourceLanguage(QLocale::Language(-1)), + m_country(QLocale::Country(-1)), + m_sourceCountry(QLocale::Country(-1)) +{} + +QStringList DataModel::normalizedTranslations(const MessageItem &m) const +{ + return Translator::normalizedTranslations(m.message(), m_numerusForms.count()); +} + +ContextItem *DataModel::contextItem(int context) const +{ + if (context >= 0 && context < m_contextList.count()) + return const_cast<ContextItem *>(&m_contextList[context]); + Q_ASSERT(context >= 0 && context < m_contextList.count()); + return 0; +} + +MessageItem *DataModel::messageItem(const DataIndex &index) const +{ + if (ContextItem *c = contextItem(index.context())) + return c->messageItem(index.message()); + return 0; +} + +ContextItem *DataModel::findContext(const QString &context) const +{ + for (int c = 0; c < m_contextList.count(); ++c) { + ContextItem *ctx = contextItem(c); + if (ctx->context() == context) + return ctx; + } + return 0; +} + +MessageItem *DataModel::findMessage(const QString &context, + const QString &sourcetext, const QString &comment) const +{ + if (ContextItem *ctx = findContext(context)) + return ctx->findMessage(sourcetext, comment); + return 0; +} + +static int calcMergeScore(const DataModel *one, const DataModel *two) +{ + int inBoth = 0; + for (int i = 0; i < two->contextCount(); ++i) { + ContextItem *oc = two->contextItem(i); + if (ContextItem *c = one->findContext(oc->context())) { + for (int j = 0; j < oc->messageCount(); ++j) { + MessageItem *m = oc->messageItem(j); + if (c->findMessage(m->text(), m->comment()) >= 0) + ++inBoth; + } + } + } + return inBoth * 100 / two->messageCount(); +} + +bool DataModel::isWellMergeable(const DataModel *other) const +{ + if (!other->messageCount() || !messageCount()) + return true; + + return calcMergeScore(this, other) + calcMergeScore(other, this) > 90; +} + +bool DataModel::load(const QString &fileName, bool *langGuessed, QWidget *parent) +{ + Translator tor; + ConversionData cd; + bool ok = tor.load(fileName, cd, QLatin1String("auto")); + if (!ok) { + QMessageBox::warning(parent, QObject::tr("Qt Linguist"), cd.error()); + return false; + } + + if (!tor.messageCount()) { + QMessageBox::warning(parent, QObject::tr("Qt Linguist"), + tr("The translation file '%1' will not be loaded because it is empty.") + .arg(Qt::escape(fileName))); + return false; + } + + Translator::Duplicates dupes = tor.resolveDuplicates(); + if (!dupes.byId.isEmpty() || !dupes.byContents.isEmpty()) { + QString err = tr("<qt>Duplicate messages found in '%1':").arg(Qt::escape(fileName)); + int numdups = 0; + foreach (int i, dupes.byId) { + if (++numdups >= 5) { + err += tr("<p>[more duplicates omitted]"); + goto doWarn; + } + err += tr("<p>* ID: %1").arg(Qt::escape(tor.message(i).id())); + } + foreach (int j, dupes.byContents) { + const TranslatorMessage &msg = tor.message(j); + if (++numdups >= 5) { + err += tr("<p>[more duplicates omitted]"); + break; + } + err += tr("<p>* Context: %1<br>* Source: %2") + .arg(Qt::escape(msg.context()), Qt::escape(msg.sourceText())); + if (!msg.comment().isEmpty()) + err += tr("<br>* Comment: %3").arg(Qt::escape(msg.comment())); + } + doWarn: + QMessageBox::warning(parent, QObject::tr("Qt Linguist"), err); + } + + m_srcFileName = fileName; + m_codecName = tor.codecName(); + m_relativeLocations = (tor.locationsType() == Translator::RelativeLocations); + m_extra = tor.extras(); + m_contextList.clear(); + m_numMessages = 0; + + QHash<QString, int> contexts; + + m_srcWords = 0; + m_srcChars = 0; + m_srcCharsSpc = 0; + + foreach (const TranslatorMessage &msg, tor.messages()) { + if (!contexts.contains(msg.context())) { + contexts.insert(msg.context(), m_contextList.size()); + m_contextList.append(ContextItem(msg.context())); + } + + ContextItem *c = contextItem(contexts.value(msg.context())); + if (msg.sourceText() == QLatin1String(ContextComment)) { + c->appendToComment(msg.comment()); + } else { + MessageItem tmp(msg); + if (msg.type() == TranslatorMessage::Finished) + c->incrementFinishedCount(); + if (msg.type() != TranslatorMessage::Obsolete) { + doCharCounting(tmp.text(), m_srcWords, m_srcChars, m_srcCharsSpc); + doCharCounting(tmp.pluralText(), m_srcWords, m_srcChars, m_srcCharsSpc); + c->incrementNonobsoleteCount(); + } + c->appendMessage(tmp); + ++m_numMessages; + } + } + + // Try to detect the correct language in the following order + // 1. Look for the language attribute in the ts + // if that fails + // 2. Guestimate the language from the filename + // (expecting the qt_{en,de}.ts convention) + // if that fails + // 3. Retrieve the locale from the system. + *langGuessed = false; + QString lang = tor.languageCode(); + if (lang.isEmpty()) { + lang = QFileInfo(fileName).baseName(); + int pos = lang.indexOf(QLatin1Char('_')); + if (pos != -1 && pos + 3 == lang.length()) + lang = fileName.mid(pos + 1); + else + lang.clear(); + *langGuessed = true; + } + QLocale::Language l; + QLocale::Country c; + Translator::languageAndCountry(lang, &l, &c); + if (l == QLocale::C) { + QLocale sys; + l = sys.language(); + c = sys.country(); + *langGuessed = true; + } + if (!setLanguageAndCountry(l, c)) + QMessageBox::warning(parent, QObject::tr("Qt Linguist"), + tr("Linguist does not know the plural rules for '%1'.\n" + "Will assume a single universal form.") + .arg(m_localizedLanguage)); + // Try to detect the correct source language in the following order + // 1. Look for the language attribute in the ts + // if that fails + // 2. Assume English + lang = tor.sourceLanguageCode(); + if (lang.isEmpty()) { + l = QLocale::C; + c = QLocale::AnyCountry; + } else { + Translator::languageAndCountry(lang, &l, &c); + } + setSourceLanguageAndCountry(l, c); + + setModified(false); + + return true; +} + +bool DataModel::save(const QString &fileName, QWidget *parent) +{ + Translator tor; + for (DataModelIterator it(this); it.isValid(); ++it) + tor.append(it.current()->message()); + + tor.setLanguageCode(Translator::makeLanguageCode(m_language, m_country)); + tor.setSourceLanguageCode(Translator::makeLanguageCode(m_sourceLanguage, m_sourceCountry)); + tor.setCodecName(m_codecName); + tor.setLocationsType(m_relativeLocations ? Translator::RelativeLocations + : Translator::AbsoluteLocations); + tor.setExtras(m_extra); + ConversionData cd; + bool ok = tor.save(fileName, cd, QLatin1String("auto")); + if (ok) + setModified(false); + else + QMessageBox::warning(parent, QObject::tr("Qt Linguist"), cd.error()); + return ok; +} + +bool DataModel::saveAs(const QString &newFileName, QWidget *parent) +{ + if (!save(newFileName, parent)) + return false; + m_srcFileName = newFileName; + return true; +} + +bool DataModel::release(const QString &fileName, bool verbose, bool ignoreUnfinished, + TranslatorSaveMode mode, QWidget *parent) +{ + QFile file(fileName); + if (!file.open(QIODevice::WriteOnly)) { + QMessageBox::warning(parent, QObject::tr("Qt Linguist"), + tr("Cannot create '%2': %1").arg(file.errorString()).arg(fileName)); + return false; + } + Translator tor; + QLocale locale(m_language, m_country); + tor.setLanguageCode(locale.name()); + for (DataModelIterator it(this); it.isValid(); ++it) + tor.append(it.current()->message()); + ConversionData cd; + cd.m_verbose = verbose; + cd.m_ignoreUnfinished = ignoreUnfinished; + cd.m_saveMode = mode; + bool ok = tor.release(&file, cd); + if (!ok) + QMessageBox::warning(parent, QObject::tr("Qt Linguist"), cd.error()); + return ok; +} + +void DataModel::doCharCounting(const QString &text, int &trW, int &trC, int &trCS) +{ + trCS += text.length(); + bool inWord = false; + for (int i = 0; i < text.length(); ++i) { + if (text[i].isLetterOrNumber() || text[i] == QLatin1Char('_')) { + if (!inWord) { + ++trW; + inWord = true; + } + } else { + inWord = false; + } + if (!text[i].isSpace()) + trC++; + } +} + +bool DataModel::setLanguageAndCountry(QLocale::Language lang, QLocale::Country country) +{ + if (m_language == lang && m_country == country) + return true; + m_language = lang; + m_country = country; + + if (lang == QLocale::C || uint(lang) > uint(QLocale::LastLanguage)) // XXX does this make any sense? + lang = QLocale::English; + QByteArray rules; + bool ok = getNumerusInfo(lang, country, &rules, &m_numerusForms, 0); + m_localizedLanguage = QCoreApplication::translate("MessageEditor", QLocale::languageToString(lang).toAscii()); + m_countRefNeeds.clear(); + for (int i = 0; i < rules.size(); ++i) { + m_countRefNeeds.append(!(rules.at(i) == Q_EQ && (i == (rules.size() - 2) || rules.at(i + 2) == (char)Q_NEWRULE))); + while (++i < rules.size() && rules.at(i) != (char)Q_NEWRULE) {} + } + m_countRefNeeds.append(true); + if (!ok) { + m_numerusForms.clear(); + m_numerusForms << tr("Universal Form"); + } + emit languageChanged(); + setModified(true); + return ok; +} + +void DataModel::setSourceLanguageAndCountry(QLocale::Language lang, QLocale::Country country) +{ + if (m_sourceLanguage == lang && m_sourceCountry == country) + return; + m_sourceLanguage = lang; + m_sourceCountry = country; + setModified(true); +} + +void DataModel::updateStatistics() +{ + int trW = 0; + int trC = 0; + int trCS = 0; + + for (DataModelIterator it(this); it.isValid(); ++it) { + const MessageItem *mi = it.current(); + if (mi->isFinished()) + foreach (const QString &trnsl, mi->translations()) + doCharCounting(trnsl, trW, trC, trCS); + } + + emit statsChanged(m_srcWords, m_srcChars, m_srcCharsSpc, trW, trC, trCS); +} + +void DataModel::setModified(bool isModified) +{ + if (m_modified == isModified) + return; + m_modified = isModified; + emit modifiedChanged(); +} + +QString DataModel::prettifyPlainFileName(const QString &fn) +{ + static QString workdir = QDir::currentPath() + QLatin1Char('/'); + + return QDir::toNativeSeparators(fn.startsWith(workdir) ? fn.mid(workdir.length()) : fn); +} + +QString DataModel::prettifyFileName(const QString &fn) +{ + if (fn.startsWith(QLatin1Char('='))) + return QLatin1Char('=') + prettifyPlainFileName(fn.mid(1)); + else + return prettifyPlainFileName(fn); +} + +/****************************************************************************** + * + * DataModelIterator + * + *****************************************************************************/ + +DataModelIterator::DataModelIterator(DataModel *model, int context, int message) + : DataIndex(context, message), m_model(model) +{ +} + +bool DataModelIterator::isValid() const +{ + return m_context < m_model->m_contextList.count(); +} + +void DataModelIterator::operator++() +{ + ++m_message; + if (m_message >= m_model->m_contextList.at(m_context).messageCount()) { + ++m_context; + m_message = 0; + } +} + +MessageItem *DataModelIterator::current() const +{ + return m_model->messageItem(*this); +} + + +/****************************************************************************** + * + * MultiMessageItem + * + *****************************************************************************/ + +MultiMessageItem::MultiMessageItem(const MessageItem *m) + : m_text(m->text()), + m_pluralText(m->pluralText()), + m_comment(m->comment()), + m_nonnullCount(0), + m_nonobsoleteCount(0), + m_editableCount(0), + m_unfinishedCount(0) +{ +} + +/****************************************************************************** + * + * MultiContextItem + * + *****************************************************************************/ + +MultiContextItem::MultiContextItem(int oldCount, ContextItem *ctx, bool writable) + : m_context(ctx->context()), + m_comment(ctx->comment()), + m_finishedCount(0), + m_editableCount(0), + m_nonobsoleteCount(0) +{ + QList<MessageItem *> mList; + QList<MessageItem *> eList; + for (int j = 0; j < ctx->messageCount(); ++j) { + MessageItem *m = ctx->messageItem(j); + mList.append(m); + eList.append(0); + m_multiMessageList.append(MultiMessageItem(m)); + } + for (int i = 0; i < oldCount; ++i) { + m_messageLists.append(eList); + m_writableMessageLists.append(0); + m_contextList.append(0); + } + m_messageLists.append(mList); + m_writableMessageLists.append(writable ? &m_messageLists.last() : 0); + m_contextList.append(ctx); +} + +void MultiContextItem::appendEmptyModel() +{ + QList<MessageItem *> eList; + for (int j = 0; j < messageCount(); ++j) + eList.append(0); + m_messageLists.append(eList); + m_writableMessageLists.append(0); + m_contextList.append(0); +} + +void MultiContextItem::assignLastModel(ContextItem *ctx, bool writable) +{ + if (writable) + m_writableMessageLists.last() = &m_messageLists.last(); + m_contextList.last() = ctx; +} + +// XXX this is not needed, yet +void MultiContextItem::moveModel(int oldPos, int newPos) +{ + m_contextList.insert(newPos, m_contextList[oldPos]); + m_messageLists.insert(newPos, m_messageLists[oldPos]); + m_writableMessageLists.insert(newPos, m_writableMessageLists[oldPos]); + removeModel(oldPos < newPos ? oldPos : oldPos + 1); +} + +void MultiContextItem::removeModel(int pos) +{ + m_contextList.removeAt(pos); + m_messageLists.removeAt(pos); + m_writableMessageLists.removeAt(pos); +} + +void MultiContextItem::putMessageItem(int pos, MessageItem *m) +{ + m_messageLists.last()[pos] = m; +} + +void MultiContextItem::appendMessageItems(const QList<MessageItem *> &m) +{ + QList<MessageItem *> nullItems = m; // Basically, just a reservation + for (int i = 0; i < nullItems.count(); ++i) + nullItems[i] = 0; + for (int i = 0; i < m_messageLists.count() - 1; ++i) + m_messageLists[i] += nullItems; + m_messageLists.last() += m; + foreach (MessageItem *mi, m) + m_multiMessageList.append(MultiMessageItem(mi)); +} + +void MultiContextItem::removeMultiMessageItem(int pos) +{ + for (int i = 0; i < m_messageLists.count(); ++i) + m_messageLists[i].removeAt(pos); + m_multiMessageList.removeAt(pos); +} + +int MultiContextItem::firstNonobsoleteMessageIndex(int msgIdx) const +{ + for (int i = 0; i < m_messageLists.size(); ++i) + if (m_messageLists[i][msgIdx] && !m_messageLists[i][msgIdx]->isObsolete()) + return i; + return -1; +} + +int MultiContextItem::findMessage(const QString &sourcetext, const QString &comment) const +{ + for (int i = 0, cnt = messageCount(); i < cnt; ++i) { + MultiMessageItem *m = multiMessageItem(i); + if (m->text() == sourcetext && m->comment() == comment) + return i; + } + return -1; +} + +/****************************************************************************** + * + * MultiDataModel + * + *****************************************************************************/ + +static const uchar paletteRGBs[7][3] = { + { 236, 244, 255 }, // blue + { 236, 255, 255 }, // cyan + { 236, 255, 232 }, // green + { 255, 255, 230 }, // yellow + { 255, 242, 222 }, // orange + { 255, 236, 236 }, // red + { 252, 236, 255 } // purple +}; + +MultiDataModel::MultiDataModel(QObject *parent) : + QObject(parent), + m_numFinished(0), + m_numEditable(0), + m_numMessages(0), + m_modified(false) +{ + for (int i = 0; i < 7; ++i) + m_colors[i] = QColor(paletteRGBs[i][0], paletteRGBs[i][1], paletteRGBs[i][2]); + + m_bitmap = QBitmap(8, 8); + m_bitmap.clear(); + QPainter p(&m_bitmap); + for (int j = 0; j < 8; ++j) + for (int k = 0; k < 8; ++k) + if ((j + k) & 4) + p.drawPoint(j, k); +} + +MultiDataModel::~MultiDataModel() +{ + qDeleteAll(m_dataModels); +} + +QBrush MultiDataModel::brushForModel(int model) const +{ + QBrush brush(m_colors[model % 7]); + if (!isModelWritable(model)) + brush.setTexture(m_bitmap); + return brush; +} + +bool MultiDataModel::isWellMergeable(const DataModel *dm) const +{ + if (!dm->messageCount() || !messageCount()) + return true; + + int inBothNew = 0; + for (int i = 0; i < dm->contextCount(); ++i) { + ContextItem *c = dm->contextItem(i); + if (MultiContextItem *mc = findContext(c->context())) { + for (int j = 0; j < c->messageCount(); ++j) { + MessageItem *m = c->messageItem(j); + if (mc->findMessage(m->text(), m->comment()) >= 0) + ++inBothNew; + } + } + } + int newRatio = inBothNew * 100 / dm->messageCount(); + + int inBothOld = 0; + for (int k = 0; k < contextCount(); ++k) { + MultiContextItem *mc = multiContextItem(k); + if (ContextItem *c = dm->findContext(mc->context())) { + for (int j = 0; j < mc->messageCount(); ++j) { + MultiMessageItem *m = mc->multiMessageItem(j); + if (c->findMessage(m->text(), m->comment())) + ++inBothOld; + } + } + } + int oldRatio = inBothOld * 100 / messageCount(); + + return newRatio + oldRatio > 90; +} + +void MultiDataModel::append(DataModel *dm, bool readWrite) +{ + int insCol = modelCount() + 1; + m_msgModel->beginInsertColumns(QModelIndex(), insCol, insCol); + m_dataModels.append(dm); + for (int j = 0; j < contextCount(); ++j) { + m_msgModel->beginInsertColumns(m_msgModel->createIndex(j, 0, 0), insCol, insCol); + m_multiContextList[j].appendEmptyModel(); + m_msgModel->endInsertColumns(); + } + m_msgModel->endInsertColumns(); + int appendedContexts = 0; + for (int i = 0; i < dm->contextCount(); ++i) { + ContextItem *c = dm->contextItem(i); + int mcx = findContextIndex(c->context()); + if (mcx >= 0) { + MultiContextItem *mc = multiContextItem(mcx); + mc->assignLastModel(c, readWrite); + QList<MessageItem *> appendItems; + for (int j = 0; j < c->messageCount(); ++j) { + MessageItem *m = c->messageItem(j); + int msgIdx = mc->findMessage(m->text(), m->comment()); + if (msgIdx >= 0) + mc->putMessageItem(msgIdx, m); + else + appendItems << m; + } + if (!appendItems.isEmpty()) { + int msgCnt = mc->messageCount(); + m_msgModel->beginInsertRows(m_msgModel->createIndex(mcx, 0, 0), + msgCnt, msgCnt + appendItems.size() - 1); + mc->appendMessageItems(appendItems); + m_msgModel->endInsertRows(); + m_numMessages += appendItems.size(); + } + } else { + m_multiContextList << MultiContextItem(modelCount() - 1, c, readWrite); + m_numMessages += c->messageCount(); + ++appendedContexts; + } + } + if (appendedContexts) { + // Do that en block to avoid itemview inefficiency. It doesn't hurt that we + // announce the availability of the data "long" after it was actually added. + m_msgModel->beginInsertRows(QModelIndex(), + contextCount() - appendedContexts, contextCount() - 1); + m_msgModel->endInsertRows(); + } + dm->setWritable(readWrite); + updateCountsOnAdd(modelCount() - 1, readWrite); + connect(dm, SIGNAL(modifiedChanged()), SLOT(onModifiedChanged())); + connect(dm, SIGNAL(languageChanged()), SLOT(onLanguageChanged())); + connect(dm, SIGNAL(statsChanged(int,int,int,int,int,int)), SIGNAL(statsChanged(int,int,int,int,int,int))); + emit modelAppended(); +} + +void MultiDataModel::close(int model) +{ + if (m_dataModels.count() == 1) { + closeAll(); + } else { + updateCountsOnRemove(model, isModelWritable(model)); + int delCol = model + 1; + m_msgModel->beginRemoveColumns(QModelIndex(), delCol, delCol); + for (int i = m_multiContextList.size(); --i >= 0;) { + m_msgModel->beginRemoveColumns(m_msgModel->createIndex(i, 0, 0), delCol, delCol); + m_multiContextList[i].removeModel(model); + m_msgModel->endRemoveColumns(); + } + delete m_dataModels.takeAt(model); + m_msgModel->endRemoveColumns(); + emit modelDeleted(model); + for (int i = m_multiContextList.size(); --i >= 0;) { + MultiContextItem &mc = m_multiContextList[i]; + QModelIndex contextIdx = m_msgModel->createIndex(i, 0, 0); + for (int j = mc.messageCount(); --j >= 0;) + if (mc.multiMessageItem(j)->isEmpty()) { + m_msgModel->beginRemoveRows(contextIdx, j, j); + mc.removeMultiMessageItem(j); + m_msgModel->endRemoveRows(); + --m_numMessages; + } + if (!mc.messageCount()) { + m_msgModel->beginRemoveRows(QModelIndex(), i, i); + m_multiContextList.removeAt(i); + m_msgModel->endRemoveRows(); + } + } + onModifiedChanged(); + } +} + +void MultiDataModel::closeAll() +{ + m_numFinished = 0; + m_numEditable = 0; + m_numMessages = 0; + qDeleteAll(m_dataModels); + m_dataModels.clear(); + m_multiContextList.clear(); + m_msgModel->reset(); + emit allModelsDeleted(); + onModifiedChanged(); +} + +// XXX this is not needed, yet +void MultiDataModel::moveModel(int oldPos, int newPos) +{ + int delPos = oldPos < newPos ? oldPos : oldPos + 1; + m_dataModels.insert(newPos, m_dataModels[oldPos]); + m_dataModels.removeAt(delPos); + for (int i = 0; i < m_multiContextList.size(); ++i) + m_multiContextList[i].moveModel(oldPos, newPos); +} + +QStringList MultiDataModel::prettifyFileNames(const QStringList &names) +{ + QStringList out; + + foreach (const QString &name, names) + out << DataModel::prettifyFileName(name); + return out; +} + +QString MultiDataModel::condenseFileNames(const QStringList &names) +{ + if (names.isEmpty()) + return QString(); + + if (names.count() < 2) + return names.first(); + + QString prefix = names.first(); + if (prefix.startsWith(QLatin1Char('='))) + prefix.remove(0, 1); + QString suffix = prefix; + for (int i = 1; i < names.count(); ++i) { + QString fn = names[i]; + if (fn.startsWith(QLatin1Char('='))) + fn.remove(0, 1); + for (int j = 0; j < prefix.length(); ++j) + if (fn[j] != prefix[j]) { + if (j < prefix.length()) { + while (j > 0 && prefix[j - 1].isLetterOrNumber()) + --j; + prefix.truncate(j); + } + break; + } + int fnl = fn.length() - 1; + int sxl = suffix.length() - 1; + for (int k = 0; k <= sxl; ++k) + if (fn[fnl - k] != suffix[sxl - k]) { + if (k < sxl) { + while (k > 0 && suffix[sxl - k + 1].isLetterOrNumber()) + --k; + if (prefix.length() + k > fnl) + --k; + suffix.remove(0, sxl - k + 1); + } + break; + } + } + QString ret = prefix + QLatin1Char('{'); + int pxl = prefix.length(); + int sxl = suffix.length(); + for (int j = 0; j < names.count(); ++j) { + if (j) + ret += QLatin1Char(','); + int off = pxl; + QString fn = names[j]; + if (fn.startsWith(QLatin1Char('='))) { + ret += QLatin1Char('='); + ++off; + } + ret += fn.mid(off, fn.length() - sxl - off); + } + ret += QLatin1Char('}') + suffix; + return ret; +} + +QStringList MultiDataModel::srcFileNames(bool pretty) const +{ + QStringList names; + foreach (DataModel *dm, m_dataModels) + names << (dm->isWritable() ? QString() : QString::fromLatin1("=")) + dm->srcFileName(pretty); + return names; +} + +QString MultiDataModel::condensedSrcFileNames(bool pretty) const +{ + return condenseFileNames(srcFileNames(pretty)); +} + +bool MultiDataModel::isModified() const +{ + foreach (const DataModel *mdl, m_dataModels) + if (mdl->isModified()) + return true; + return false; +} + +void MultiDataModel::onModifiedChanged() +{ + bool modified = isModified(); + if (modified != m_modified) { + emit modifiedChanged(modified); + m_modified = modified; + } +} + +void MultiDataModel::onLanguageChanged() +{ + int i = 0; + while (sender() != m_dataModels[i]) + ++i; + emit languageChanged(i); +} + +int MultiDataModel::isFileLoaded(const QString &name) const +{ + for (int i = 0; i < m_dataModels.size(); ++i) + if (m_dataModels[i]->srcFileName() == name) + return i; + return -1; +} + +int MultiDataModel::findContextIndex(const QString &context) const +{ + for (int i = 0; i < m_multiContextList.size(); ++i) { + const MultiContextItem &mc = m_multiContextList[i]; + if (mc.context() == context) + return i; + } + return -1; +} + +MultiContextItem *MultiDataModel::findContext(const QString &context) const +{ + for (int i = 0; i < m_multiContextList.size(); ++i) { + const MultiContextItem &mc = m_multiContextList[i]; + if (mc.context() == context) + return const_cast<MultiContextItem *>(&mc); + } + return 0; +} + +MessageItem *MultiDataModel::messageItem(const MultiDataIndex &index, int model) const +{ + if (index.context() < contextCount() && model >= 0 && model < modelCount()) { + MultiContextItem *mc = multiContextItem(index.context()); + if (index.message() < mc->messageCount()) + return mc->messageItem(model, index.message()); + } + Q_ASSERT(model >= 0 && model < modelCount()); + Q_ASSERT(index.context() < contextCount()); + return 0; +} + +void MultiDataModel::setTranslation(const MultiDataIndex &index, const QString &translation) +{ + MessageItem *m = messageItem(index); + if (translation == m->translation()) + return; + m->setTranslation(translation); + setModified(index.model(), true); + emit translationChanged(index); +} + +void MultiDataModel::setFinished(const MultiDataIndex &index, bool finished) +{ + MultiContextItem *mc = multiContextItem(index.context()); + MultiMessageItem *mm = mc->multiMessageItem(index.message()); + ContextItem *c = contextItem(index); + MessageItem *m = messageItem(index); + TranslatorMessage::Type type = m->type(); + if (type == TranslatorMessage::Unfinished && finished) { + m->setType(TranslatorMessage::Finished); + mm->decrementUnfinishedCount(); + if (!mm->countUnfinished()) { + incrementFinishedCount(); + mc->incrementFinishedCount(); + emit multiContextDataChanged(index); + } + c->incrementFinishedCount(); + if (m->danger()) { + c->incrementFinishedDangerCount(); + c->decrementUnfinishedDangerCount(); + if (!c->unfinishedDangerCount() + || c->finishedCount() == c->nonobsoleteCount()) + emit contextDataChanged(index); + } else if (c->finishedCount() == c->nonobsoleteCount()) { + emit contextDataChanged(index); + } + emit messageDataChanged(index); + setModified(index.model(), true); + } else if (type == TranslatorMessage::Finished && !finished) { + m->setType(TranslatorMessage::Unfinished); + mm->incrementUnfinishedCount(); + if (mm->countUnfinished() == 1) { + decrementFinishedCount(); + mc->decrementFinishedCount(); + emit multiContextDataChanged(index); + } + c->decrementFinishedCount(); + if (m->danger()) { + c->decrementFinishedDangerCount(); + c->incrementUnfinishedDangerCount(); + if (c->unfinishedDangerCount() == 1 + || c->finishedCount() + 1 == c->nonobsoleteCount()) + emit contextDataChanged(index); + } else if (c->finishedCount() + 1 == c->nonobsoleteCount()) { + emit contextDataChanged(index); + } + emit messageDataChanged(index); + setModified(index.model(), true); + } +} + +void MultiDataModel::setDanger(const MultiDataIndex &index, bool danger) +{ + ContextItem *c = contextItem(index); + MessageItem *m = messageItem(index); + if (!m->danger() && danger) { + if (m->isFinished()) { + c->incrementFinishedDangerCount(); + if (c->finishedDangerCount() == 1) + emit contextDataChanged(index); + } else { + c->incrementUnfinishedDangerCount(); + if (c->unfinishedDangerCount() == 1) + emit contextDataChanged(index); + } + emit messageDataChanged(index); + m->setDanger(danger); + } else if (m->danger() && !danger) { + if (m->isFinished()) { + c->decrementFinishedDangerCount(); + if (!c->finishedDangerCount()) + emit contextDataChanged(index); + } else { + c->decrementUnfinishedDangerCount(); + if (!c->unfinishedDangerCount()) + emit contextDataChanged(index); + } + emit messageDataChanged(index); + m->setDanger(danger); + } +} + +void MultiDataModel::updateCountsOnAdd(int model, bool writable) +{ + for (int i = 0; i < m_multiContextList.size(); ++i) { + MultiContextItem &mc = m_multiContextList[i]; + for (int j = 0; j < mc.messageCount(); ++j) + if (MessageItem *m = mc.messageItem(model, j)) { + MultiMessageItem *mm = mc.multiMessageItem(j); + mm->incrementNonnullCount(); + if (!m->isObsolete()) { + if (writable) { + if (!mm->countEditable()) { + mc.incrementEditableCount(); + incrementEditableCount(); + if (m->isFinished()) { + mc.incrementFinishedCount(); + incrementFinishedCount(); + } else { + mm->incrementUnfinishedCount(); + } + } else if (!m->isFinished()) { + if (!mm->isUnfinished()) { + mc.decrementFinishedCount(); + decrementFinishedCount(); + } + mm->incrementUnfinishedCount(); + } + mm->incrementEditableCount(); + } + mc.incrementNonobsoleteCount(); + mm->incrementNonobsoleteCount(); + } + } + } +} + +void MultiDataModel::updateCountsOnRemove(int model, bool writable) +{ + for (int i = 0; i < m_multiContextList.size(); ++i) { + MultiContextItem &mc = m_multiContextList[i]; + for (int j = 0; j < mc.messageCount(); ++j) + if (MessageItem *m = mc.messageItem(model, j)) { + MultiMessageItem *mm = mc.multiMessageItem(j); + mm->decrementNonnullCount(); + if (!m->isObsolete()) { + mm->decrementNonobsoleteCount(); + mc.decrementNonobsoleteCount(); + if (writable) { + mm->decrementEditableCount(); + if (!mm->countEditable()) { + mc.decrementEditableCount(); + decrementEditableCount(); + if (m->isFinished()) { + mc.decrementFinishedCount(); + decrementFinishedCount(); + } else { + mm->decrementUnfinishedCount(); + } + } else if (!m->isFinished()) { + mm->decrementUnfinishedCount(); + if (!mm->isUnfinished()) { + mc.incrementFinishedCount(); + incrementFinishedCount(); + } + } + } + } + } + } +} + +/****************************************************************************** + * + * MultiDataModelIterator + * + *****************************************************************************/ + +MultiDataModelIterator::MultiDataModelIterator(MultiDataModel *dataModel, int model, int context, int message) + : MultiDataIndex(model, context, message), m_dataModel(dataModel) +{ +} + +void MultiDataModelIterator::operator++() +{ + Q_ASSERT(isValid()); + ++m_message; + if (m_message >= m_dataModel->m_multiContextList.at(m_context).messageCount()) { + ++m_context; + m_message = 0; + } +} + +bool MultiDataModelIterator::isValid() const +{ + return m_context < m_dataModel->m_multiContextList.count(); +} + +MessageItem *MultiDataModelIterator::current() const +{ + return m_dataModel->messageItem(*this); +} + + +/****************************************************************************** + * + * MessageModel + * + *****************************************************************************/ + +MessageModel::MessageModel(QObject *parent, MultiDataModel *data) + : QAbstractItemModel(parent), m_data(data) +{ + data->m_msgModel = this; + connect(m_data, SIGNAL(multiContextDataChanged(MultiDataIndex)), + SLOT(multiContextItemChanged(MultiDataIndex))); + connect(m_data, SIGNAL(contextDataChanged(MultiDataIndex)), + SLOT(contextItemChanged(MultiDataIndex))); + connect(m_data, SIGNAL(messageDataChanged(MultiDataIndex)), + SLOT(messageItemChanged(MultiDataIndex))); +} + +QModelIndex MessageModel::index(int row, int column, const QModelIndex &parent) const +{ + if (!parent.isValid()) + return createIndex(row, column, 0); + if (!parent.internalId()) + return createIndex(row, column, parent.row() + 1); + return QModelIndex(); +} + +QModelIndex MessageModel::parent(const QModelIndex& index) const +{ + if (index.internalId()) + return createIndex(index.internalId() - 1, 0, 0); + return QModelIndex(); +} + +void MessageModel::multiContextItemChanged(const MultiDataIndex &index) +{ + QModelIndex idx = createIndex(index.context(), m_data->modelCount() + 2, 0); + emit dataChanged(idx, idx); +} + +void MessageModel::contextItemChanged(const MultiDataIndex &index) +{ + QModelIndex idx = createIndex(index.context(), index.model() + 1, 0); + emit dataChanged(idx, idx); +} + +void MessageModel::messageItemChanged(const MultiDataIndex &index) +{ + QModelIndex idx = createIndex(index.message(), index.model() + 1, index.context() + 1); + emit dataChanged(idx, idx); +} + +QModelIndex MessageModel::modelIndex(const MultiDataIndex &index) +{ + if (index.message() < 0) // Should be unused case + return createIndex(index.context(), index.model() + 1, 0); + return createIndex(index.message(), index.model() + 1, index.context() + 1); +} + +int MessageModel::rowCount(const QModelIndex &parent) const +{ + if (!parent.isValid()) + return m_data->contextCount(); // contexts + if (!parent.internalId()) // messages + return m_data->multiContextItem(parent.row())->messageCount(); + return 0; +} + +int MessageModel::columnCount(const QModelIndex &) const +{ + return m_data->modelCount() + 3; +} + +QVariant MessageModel::data(const QModelIndex &index, int role) const +{ + static QVariant pxOn = + QVariant::fromValue(QPixmap(QLatin1String(":/images/s_check_on.png"))); + static QVariant pxOff = + QVariant::fromValue(QPixmap(QLatin1String(":/images/s_check_off.png"))); + static QVariant pxObsolete = + QVariant::fromValue(QPixmap(QLatin1String(":/images/s_check_obsolete.png"))); + static QVariant pxDanger = + QVariant::fromValue(QPixmap(QLatin1String(":/images/s_check_danger.png"))); + static QVariant pxWarning = + QVariant::fromValue(QPixmap(QLatin1String(":/images/s_check_warning.png"))); + static QVariant pxEmpty = + QVariant::fromValue(QPixmap(QLatin1String(":/images/s_check_empty.png"))); + + int row = index.row(); + int column = index.column() - 1; + if (column < 0) + return QVariant(); + + int numLangs = m_data->modelCount(); + + if (role == Qt::ToolTipRole && column < numLangs) { + return tr("Completion status for %1").arg(m_data->model(column)->localizedLanguage()); + } else if (index.internalId()) { + // this is a message + int crow = index.internalId() - 1; + MultiContextItem *mci = m_data->multiContextItem(crow); + if (row >= mci->messageCount() || !index.isValid()) + return QVariant(); + + if (role == Qt::DisplayRole || (role == Qt::ToolTipRole && column == numLangs)) { + switch (column - numLangs) { + case 0: // Source text + { + MultiMessageItem *msgItem = mci->multiMessageItem(row); + if (msgItem->text().isEmpty()) { + if (mci->context().isEmpty()) + return tr("<file header>"); + else + return tr("<context comment>"); + } + return msgItem->text().simplified(); + } + default: // Status or dummy column => no text + return QVariant(); + } + } + else if (role == Qt::DecorationRole && column < numLangs) { + if (MessageItem *msgItem = mci->messageItem(column, row)) { + switch (msgItem->message().type()) { + case TranslatorMessage::Unfinished: + if (msgItem->translation().isEmpty()) + return pxEmpty; + if (msgItem->danger()) + return pxDanger; + return pxOff; + case TranslatorMessage::Finished: + if (msgItem->danger()) + return pxWarning; + return pxOn; + default: + return pxObsolete; + } + } + return QVariant(); + } + else if (role == SortRole) { + switch (column - numLangs) { + case 0: // Source text + return mci->multiMessageItem(row)->text().simplified().remove(QLatin1Char('&')); + case 1: // Dummy column + return QVariant(); + default: + if (MessageItem *msgItem = mci->messageItem(column, row)) { + int rslt = !msgItem->translation().isEmpty(); + if (!msgItem->danger()) + rslt |= 2; + if (msgItem->isObsolete()) + rslt |= 8; + else if (msgItem->isFinished()) + rslt |= 4; + return rslt; + } + return INT_MAX; + } + } + else if (role == Qt::ForegroundRole && column > 0 + && mci->multiMessageItem(row)->isObsolete()) { + return QBrush(Qt::darkGray); + } + else if (role == Qt::ForegroundRole && column == numLangs + && mci->multiMessageItem(row)->text().isEmpty()) { + return QBrush(QColor(0, 0xa0, 0xa0)); + } + else if (role == Qt::BackgroundRole) { + if (column < numLangs && numLangs != 1) + return m_data->brushForModel(column); + } + } else { + // this is a context + if (row >= m_data->contextCount() || !index.isValid()) + return QVariant(); + + MultiContextItem *mci = m_data->multiContextItem(row); + + if (role == Qt::DisplayRole || (role == Qt::ToolTipRole && column == numLangs)) { + switch (column - numLangs) { + case 0: // Context + { + if (mci->context().isEmpty()) + return tr("<unnamed context>"); + return mci->context().simplified(); + } + case 1: + { + QString s; + s.sprintf("%d/%d", mci->getNumFinished(), mci->getNumEditable()); + return s; + } + default: + return QVariant(); // Status => no text + } + } + else if (role == Qt::DecorationRole && column < numLangs) { + if (ContextItem *contextItem = mci->contextItem(column)) { + if (contextItem->isObsolete()) + return pxObsolete; + if (contextItem->isFinished()) + return contextItem->finishedDangerCount() > 0 ? pxWarning : pxOn; + return contextItem->unfinishedDangerCount() > 0 ? pxDanger : pxOff; + } + return QVariant(); + } + else if (role == SortRole) { + switch (column - numLangs) { + case 0: // Context (same as display role) + return mci->context().simplified(); + case 1: // Items + return mci->getNumEditable(); + default: // Percent + if (ContextItem *contextItem = mci->contextItem(column)) { + int totalItems = contextItem->nonobsoleteCount(); + int percent = totalItems ? (100 * contextItem->finishedCount()) / totalItems : 100; + int rslt = percent * (((1 << 28) - 1) / 100) + totalItems; + if (contextItem->isObsolete()) { + rslt |= (1 << 30); + } else if (contextItem->isFinished()) { + rslt |= (1 << 29); + if (!contextItem->finishedDangerCount()) + rslt |= (1 << 28); + } else { + if (!contextItem->unfinishedDangerCount()) + rslt |= (1 << 28); + } + return rslt; + } + return INT_MAX; + } + } + else if (role == Qt::ForegroundRole && column >= numLangs + && m_data->multiContextItem(row)->isObsolete()) { + return QBrush(Qt::darkGray); + } + else if (role == Qt::ForegroundRole && column == numLangs + && m_data->multiContextItem(row)->context().isEmpty()) { + return QBrush(QColor(0, 0xa0, 0xa0)); + } + else if (role == Qt::BackgroundRole) { + if (column < numLangs && numLangs != 1) { + QBrush brush = m_data->brushForModel(column); + if (row & 1) { + brush.setColor(brush.color().darker(108)); + } + return brush; + } + } + } + return QVariant(); +} + +MultiDataIndex MessageModel::dataIndex(const QModelIndex &index, int model) const +{ + Q_ASSERT(index.isValid()); + Q_ASSERT(index.internalId()); + return MultiDataIndex(model, index.internalId() - 1, index.row()); +} + +QT_END_NAMESPACE |