diff --git a/src/catalog/gettextheader.cpp b/src/catalog/gettextheader.cpp index 2f90930..e36a289 100644 --- a/src/catalog/gettextheader.cpp +++ b/src/catalog/gettextheader.cpp @@ -1,692 +1,690 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2008-2014 by Nick Shaforostoff 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . **************************************************************************** */ #include "gettextheader.h" #include "lokalize_debug.h" #include "project.h" #include "version.h" #include "prefs_lokalize.h" #include "prefs.h" #include #include #include #include #include #include #include #include #include #include /** * this data was obtained by running GNUPluralForms() * on all languages KDE knows of **/ struct langPInfo { const char *lang; const char *plural; }; static const langPInfo langsWithPInfo[] = { { "ar", "nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;" }, { "be@latin", "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);" }, { "be", "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);" }, { "bg", "nplurals=2; plural=(n != 1);" }, { "br", "nplurals=2; plural=(n > 1);" }, { "bs", "nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;" }, { "csb", "nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)" }, { "cs", "nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;" }, { "da", "nplurals=2; plural=(n != 1);" }, { "de", "nplurals=2; plural=(n != 1);" }, { "el", "nplurals=2; plural=(n != 1);" }, { "en", "nplurals=2; plural=(n != 1);" }, { "en_GB", "nplurals=2; plural=(n != 1);" }, { "en_US", "nplurals=2; plural=(n != 1);" }, { "eo", "nplurals=2; plural=(n != 1);" }, { "es", "nplurals=2; plural=(n != 1);" }, { "et", "nplurals=2; plural=(n != 1);" }, { "fa", "nplurals=1; plural=0;" }, { "fi", "nplurals=2; plural=(n != 1);" }, { "fo", "nplurals=2; plural=(n != 1);" }, { "fr", "nplurals=2; plural=(n > 1);" }, { "ga", "nplurals=5; plural=n==1 ? 0 : n==2 ? 1 : n<7 ? 2 : n < 11 ? 3 : 4" }, { "gd", "nplurals=4; plural=(n==1 || n==11) ? 0 : (n==2 || n==12) ? 1 : (n > 2 && n < 20) ? 2 : 3;" }, { "gu", "nplurals=2; plural=(n!=1);" }, { "he", "nplurals=2; plural=(n != 1);" }, { "hi", "nplurals=2; plural=(n!=1);" }, { "hne", "nplurals=2; plural=(n!=1);" }, { "hr", "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);" }, { "hsb", "nplurals=4; plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3;" }, { "hu", "nplurals=2; plural=(n != 1);" }, { "hy", "nplurals=4; plural=n==1 ? 3 : n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;" }, { "id", "nplurals=2; plural=(n != 1);" }, { "it", "nplurals=2; plural=(n != 1);" }, { "ja", "nplurals=1; plural=0;" }, { "ka", "nplurals=1; plural=0;" }, { "kk", "nplurals=1; plural=0;" }, { "km", "nplurals=1; plural=0;" }, { "ko", "nplurals=1; plural=0;" }, { "lt", "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2);" }, { "lv", "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2);" }, { "mai", "nplurals=2; plural=(n!=1);" }, { "mk", "nplurals=3; plural=n%10==1 ? 0 : n%10==2 ? 1 : 2;" }, { "mr", "nplurals=2; plural=(n!=1);" }, { "ms", "nplurals=2; plural=1;" }, { "nb", "nplurals=2; plural=(n != 1);" }, { "nl", "nplurals=2; plural=(n != 1);" }, { "nn", "nplurals=2; plural=(n != 1);" }, { "oc", "nplurals=2; plural=(n > 1);" }, { "or", "nplurals=2; plural=(n!=1);" }, { "pl", "nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);" }, { "pt", "nplurals=2; plural=(n != 1);" }, { "pt_BR", "nplurals=2; plural=(n > 1);" }, { "ro", "nplurals=3; plural=n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < 20)) ? 1 : 2;" }, { "ru", "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);" }, { "sk", "nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;" }, { "sl", "nplurals=4; plural=(n%100==1 ? 1 : n%100==2 ? 2 : n%100==3 || n%100==4 ? 3 : 0);" }, { "sr", "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);" }, { "sr@latin", "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);" }, { "sv", "nplurals=2; plural=(n != 1);" }, { "te", "nplurals=2; plural=(n != 1);" }, { "th", "nplurals=1; plural=0;" }, { "tr", "nplurals=2; plural=(n > 1);" }, { "ug", "nplurals=1; plural=0;" }, { "uk", "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);" }, { "uz", "nplurals=1; plural=0;" }, { "uz@cyrillic", "nplurals=1; plural=0;" }, { "vi", "nplurals=1; plural=0;" }, { "zh_CN", "nplurals=1; plural=0;" }, { "zh_HK", "nplurals=1; plural=0;" }, { "zh_TW", "nplurals=1; plural=0;" } }; static const size_t langsWithPInfoCount = sizeof(langsWithPInfo) / sizeof(langsWithPInfo[0]); int numberOfPluralFormsFromHeader(const QString& header) { QRegExp rxplural(QStringLiteral("Plural-Forms:\\s*nplurals=(.);")); if (rxplural.indexIn(header) == -1) return 0; bool ok; int result = rxplural.cap(1).toShort(&ok); return ok ? result : 0; } int numberOfPluralFormsForLangCode(const QString& langCode) { QString expr = GNUPluralForms(langCode); QRegExp rxplural(QStringLiteral("nplurals=(.);")); if (rxplural.indexIn(expr) == -1) return 0; bool ok; int result = rxplural.cap(1).toShort(&ok); return ok ? result : 0; } QString GNUPluralForms(const QString& lang) { QByteArray l(lang.toUtf8()); int i = langsWithPInfoCount; while (--i >= 0 && l != langsWithPInfo[i].lang) ; if (Q_LIKELY(i >= 0)) return QString::fromLatin1(langsWithPInfo[i].plural); i = langsWithPInfoCount; while (--i >= 0 && !l.startsWith(langsWithPInfo[i].lang)) ; if (Q_LIKELY(i >= 0)) return QString::fromLatin1(langsWithPInfo[i].plural); //BEGIN alternative // NOTE does this work under M$ OS? qCDebug(LOKALIZE_LOG) << "gonna call msginit"; QString def = QStringLiteral("nplurals=2; plural=n != 1;"); QStringList arguments; arguments << QLatin1String("-l") << lang << QLatin1String("-i") << QLatin1String("-") << QLatin1String("-o") << QLatin1String("-") << QLatin1String("--no-translator") << QLatin1String("--no-wrap"); QProcess msginit; msginit.start(QLatin1String("msginit"), arguments); msginit.waitForStarted(5000); if (Q_UNLIKELY(msginit.state() != QProcess::Running)) { //qCWarning(LOKALIZE_LOG)<<"msginit error"; return def; } msginit.write( "# SOME DESCRIPTIVE TITLE.\n" "# Copyright (C) YEAR Free Software Foundation, Inc.\n" "# FIRST AUTHOR , YEAR.\n" "#\n" "#, fuzzy\n" "msgid \"\"\n" "msgstr \"\"\n" "\"Project-Id-Version: PACKAGE VERSION\\n\"\n" "\"POT-Creation-Date: 2002-06-25 03:23+0200\\n\"\n" "\"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n\"\n" "\"Last-Translator: FULL NAME \\n\"\n" "\"Language-Team: LANGUAGE \\n\"\n" "\"Language: LL\\n\"\n" "\"MIME-Version: 1.0\\n\"\n" "\"Content-Type: text/plain; charset=UTF-8\\n\"\n" "\"Content-Transfer-Encoding: ENCODING\\n\"\n" // "\"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\\n\"\n" ); msginit.closeWriteChannel(); if (Q_UNLIKELY(!msginit.waitForFinished(5000))) { qCWarning(LOKALIZE_LOG) << "msginit error"; return def; } QByteArray result = msginit.readAll(); int pos = result.indexOf("Plural-Forms: "); if (Q_UNLIKELY(pos == -1)) { //qCWarning(LOKALIZE_LOG)<<"msginit error"<= 0 ? '+' : '-') + (offset_hours < 10 ? QStringLiteral("0") : QStringLiteral("")) + QString::number(offset_hours) + (offset_minutes < 10 ? QStringLiteral("0") : QStringLiteral("")) + QString::number(offset_minutes); return dateTimeString + zoneOffsetString; } void updateHeader(QString& header, QString& comment, QString& langCode, int& numberOfPluralForms, const QString& CatalogProjectId, bool generatedFromDocbook, bool belongsToProject, bool forSaving, QTextCodec* codec) { askAuthorInfoIfEmpty(); QStringList headerList(header.split('\n', QString::SkipEmptyParts)); QStringList commentList(comment.split('\n', QString::SkipEmptyParts)); //BEGIN header itself QStringList::Iterator it, ait; QString temp; QString authorNameEmail; const QString BACKSLASH_N = QStringLiteral("\\n"); // Unwrap header since the following code // assumes one header item per headerList element it = headerList.begin(); while (it != headerList.end()) { if (!(*it).endsWith(BACKSLASH_N)) { const QString line = *it; it = headerList.erase(it); if (it != headerList.end()) { *it = line + *it; } else { // Something bad happened, put a warning on the command line qCWarning(LOKALIZE_LOG) << "Bad .po header, last header line was" << line; } } else { ++it; } } bool found = false; authorNameEmail = Settings::authorName(); if (!Settings::authorEmail().isEmpty()) authorNameEmail += (QStringLiteral(" <") + Settings::authorEmail() + '>'); temp = QStringLiteral("Last-Translator: ") + authorNameEmail + BACKSLASH_N; QRegExp lt(QStringLiteral("^ *Last-Translator:.*")); for (it = headerList.begin(), found = false; it != headerList.end() && !found; ++it) { if (it->contains(lt)) { if (forSaving) *it = temp; found = true; } } if (Q_UNLIKELY(!found)) headerList.append(temp); temp = QStringLiteral("PO-Revision-Date: ") + formatGettextDate(QDateTime::currentDateTime()) + BACKSLASH_N; QRegExp poRevDate(QStringLiteral("^ *PO-Revision-Date:.*")); for (it = headerList.begin(), found = false; it != headerList.end() && !found; ++it) { found = it->contains(poRevDate); if (found && forSaving) *it = temp; } if (Q_UNLIKELY(!found)) headerList.append(temp); temp = QStringLiteral("Project-Id-Version: ") + CatalogProjectId + BACKSLASH_N; //temp.replace( "@PACKAGE@", packageName()); QRegExp projectIdVer(QStringLiteral("^ *Project-Id-Version:.*")); for (it = headerList.begin(), found = false; it != headerList.end() && !found; ++it) { found = it->contains(projectIdVer); if (found && it->contains(QLatin1String("PACKAGE VERSION"))) *it = temp; } if (Q_UNLIKELY(!found)) headerList.append(temp); langCode = Project::instance()->isLoaded() ? Project::instance()->langCode() : Settings::defaultLangCode(); QString language; //initialized with preexisting value or later QString mailingList; //initialized with preexisting value or later static QMap langEnums; if (!langEnums.size()) for (int l = QLocale::Abkhazian; l <= QLocale::Akoose; ++l) langEnums[QLocale::languageToString((QLocale::Language)l)] = (QLocale::Language)l; static QRegExp langTeamRegExp(QStringLiteral("^ *Language-Team:.*")); for (it = headerList.begin(), found = false; it != headerList.end() && !found; ++it) { found = it->contains(langTeamRegExp); if (found) { //really parse header QRegExp re(QStringLiteral("^ *Language-Team: *(.*) *<([^>]*)>")); if (re.indexIn(*it) != -1) { if (langEnums.contains(re.cap(1).trimmed())) { language = re.cap(1).trimmed(); mailingList = re.cap(2).trimmed(); QList locales = QLocale::matchingLocales(langEnums.value(language), QLocale::AnyScript, QLocale::AnyCountry); if (locales.size()) langCode = locales.first().name().left(2); } } ait = it; } } if (language.isEmpty()) { language = QLocale::languageToString(QLocale(langCode).language()); if (language.isEmpty()) language = langCode; } if (mailingList.isEmpty() || belongsToProject) { if (Project::instance()->isLoaded()) mailingList = Project::instance()->mailingList(); else //if (mailingList.isEmpty()) mailingList = Settings::defaultMailingList(); } Project::LangSource projLangSource = Project::instance()->languageSource(); QString projLT = Project::instance()->projLangTeam(); if (projLangSource == Project::LangSource::Project) { - temp = QStringLiteral("Language-Team: ")+projLT+QStringLiteral("\\n"); - } - else if ((projLangSource == Project::LangSource::Application) && (Settings::overrideLangTeam())) { - temp = QStringLiteral("Language-Team: ")+Settings::userLangTeam()+QStringLiteral("\\n"); - } - else { - temp = QStringLiteral("Language-Team: ")+language+QStringLiteral(" <")+mailingList+QStringLiteral(">\\n"); + temp = QStringLiteral("Language-Team: ") + projLT + QStringLiteral("\\n"); + } else if ((projLangSource == Project::LangSource::Application) && (Settings::overrideLangTeam())) { + temp = QStringLiteral("Language-Team: ") + Settings::userLangTeam() + QStringLiteral("\\n"); + } else { + temp = QStringLiteral("Language-Team: ") + language + QStringLiteral(" <") + mailingList + QStringLiteral(">\\n"); } if (Q_LIKELY(found)) (*ait) = temp; else headerList.append(temp); static QRegExp langCodeRegExp(QStringLiteral("^ *Language: *([^ \\\\]*)")); temp = QStringLiteral("Language: ") + langCode + BACKSLASH_N; for (it = headerList.begin(), found = false; it != headerList.end() && !found; ++it) { found = (langCodeRegExp.indexIn(*it) != -1); if (found && langCodeRegExp.cap(1).isEmpty()) *it = temp; //if (found) qCWarning(LOKALIZE_LOG)<<"got explicit lang code:"<contains(ctRe); if (found) *it = temp; } if (Q_UNLIKELY(!found)) headerList.append(temp); temp = QStringLiteral("Content-Transfer-Encoding: 8bit\\n"); QRegExp cteRe(QStringLiteral("^ *Content-Transfer-Encoding:.*")); for (it = headerList.begin(), found = false; it != headerList.end() && !found; ++it) found = it->contains(cteRe); if (!found) headerList.append(temp); // ensure MIME-Version header temp = QStringLiteral("MIME-Version: 1.0\\n"); QRegExp mvRe(QStringLiteral("^ *MIME-Version:")); for (it = headerList.begin(), found = false; it != headerList.end() && !found; ++it) { found = it->contains(mvRe); if (found) *it = temp; } if (Q_UNLIKELY(!found)) headerList.append(temp); //qCDebug(LOKALIZE_LOG)<<"testing for GNUPluralForms"; // update plural form header QRegExp pfRe(QStringLiteral("^ *Plural-Forms:")); for (it = headerList.begin(), found = false; it != headerList.end() && !found; ++it) found = it->contains(pfRe); if (found) { --it; //qCDebug(LOKALIZE_LOG)<<"GNUPluralForms found"; int num = numberOfPluralFormsFromHeader(header); if (!num) { if (generatedFromDocbook) num = 1; else { qCDebug(LOKALIZE_LOG) << "No plural form info in header, using project-defined one" << langCode; QString t = GNUPluralForms(langCode); //qCWarning(LOKALIZE_LOG)<<"generated: " << t; if (!t.isEmpty()) { static QRegExp pf(QStringLiteral("^ *Plural-Forms:\\s*nplurals.*\\\\n")); pf.setMinimal(true); temp = QStringLiteral("Plural-Forms: %1\\n").arg(t); it->replace(pf, temp); num = numberOfPluralFormsFromHeader(temp); } else { qCWarning(LOKALIZE_LOG) << "no... smth went wrong :(\ncheck your gettext install"; num = 2; } } } numberOfPluralForms = num; } else if (!generatedFromDocbook) { //qCDebug(LOKALIZE_LOG)<<"generating GNUPluralForms"<contains(xgRe); if (found) *it = temp; } if (Q_UNLIKELY(!found)) headerList.append(temp); //m_header.setMsgstr( headerList.join( "\n" ) ); header = headerList.join(QStringLiteral("\n")); //END header itself //BEGIN comment = description, copyrights QLocale cLocale(QLocale::C); // U+00A9 is the Copyright sign QRegExp fsfc(QStringLiteral("^# *Copyright (\\(C\\)|\\x00a9).*Free Software Foundation, Inc")); for (it = commentList.begin(), found = false; it != commentList.end() && !found; ++it) { found = it->contains(fsfc) ; if (found) { it->replace(QStringLiteral("YEAR"), cLocale.toString(QDate::currentDate(), QStringLiteral("yyyy"))); } } /* if( saveOptions.FSFCopyright == ProjectSettingsBase::Update ) { //update years QString cy = cLocale.toString(QDate::currentDate(), "yyyy"); if( !it->contains( QRegExp(cy)) ) // is the year already included? { int index = it->lastIndexOf( QRegExp("[\\d]+[\\d\\-, ]*") ); if( index == -1 ) { KMessageBox::information(nullptr,i18n("Free Software Foundation Copyright does not contain any year. " "It will not be updated.")); } else { it->insert(index+1, QString(", ")+cy); } } }*/ #if 0 if ((!usePrefs || saveOptions.updateDescription) && (!saveOptions.descriptionString.isEmpty())) { temp = "# " + saveOptions.descriptionString; temp.replace("@PACKAGE@", packageName()); temp.replace("@LANGUAGE@", identityOptions.languageName); temp = temp.trimmed(); // The description strings has often buggy variants already in the file, these must be removed QString regexpstr = "^#\\s+" + QRegExp::escape(saveOptions.descriptionString.trimmed()) + "\\s*$"; regexpstr.replace("@PACKAGE@", ".*"); regexpstr.replace("@LANGUAGE@", ".*"); //qCDebug(LOKALIZE_LOG) << "REGEXPSTR: " << regexpstr; QRegExp regexp(regexpstr); // The buggy variants exist in English too (of a time before KBabel got a translation for the corresponding language) QRegExp regexpUntranslated("^#\\s+translation of .* to .*\\s*$"); qCDebug(LOKALIZE_LOG) << "Temp is '" << temp << "'"; found = false; bool foundTemplate = false; it = commentList.begin(); while (it != commentList.end()) { qCDebug(LOKALIZE_LOG) << "testing '" << (*it) << "'"; bool deleteItem = false; if ((*it) == temp) { qCDebug(LOKALIZE_LOG) << "Match "; if (found) deleteItem = true; else found = true; } else if (regexp.indexIn(*it) >= 0) { // We have a similar (translated) string (from another project or another language (perhaps typos)). Remove it. deleteItem = true; } else if (regexpUntranslated.indexIn(*it) >= 0) { // We have a similar (untranslated) string (from another project or another language (perhaps typos)). Remove it. deleteItem = true; } else if ((*it) == "# SOME DESCRIPTIVE TITLE.") { // We have the standard title placeholder, remove it deleteItem = true; } if (deleteItem) it = commentList.erase(it); else ++it; } if (!found) commentList.prepend(temp); } #endif // qCDebug(LOKALIZE_LOG) << "HEADER COMMENT: " << commentList; /* if ( (!usePrefs || saveOptions.updateTranslatorCopyright) && ( ! identityOptions->readEntry("authorName","").isEmpty() ) && ( ! identityOptions->readEntry("Email","").isEmpty() ) ) // An email address can be used as ersatz of a name {*/ // return; QStringList foundAuthors; temp = QStringLiteral("# ") + authorNameEmail + QStringLiteral(", ") + cLocale.toString(QDate::currentDate(), QStringLiteral("yyyy")) + '.'; // ### TODO: it would be nice if the entry could start with "COPYRIGHT" and have the "(C)" symbol (both not mandatory) QRegExp regexpAuthorYear(QStringLiteral("^#.*(<.+@.+>)?,\\s*([\\d]+[\\d\\-, ]*|YEAR)")); QRegExp regexpYearAlone(QStringLiteral("^# , \\d{4}.?\\s*$")); if (commentList.isEmpty()) { commentList.append(temp); commentList.append(QString()); } else { it = commentList.begin(); while (it != commentList.end()) { bool deleteItem = false; if (it->indexOf(QLatin1String("copyright"), 0, Qt::CaseInsensitive) != -1) { // We have a line with a copyright. It should not be moved. } else if (it->contains(QRegExp(QStringLiteral("#, *fuzzy")))) deleteItem = true; else if (it->contains(regexpYearAlone)) { // We have found a year number that is preceded by a comma. // That is typical of KBabel 1.10 (and earlier?) when there is neither an author name nor an email // Remove the entry deleteItem = true; } else if (it->contains(QLatin1String("# FIRST AUTHOR , YEAR."))) deleteItem = true; else if (it->contains(QLatin1String("# SOME DESCRIPTIVE TITLE"))) deleteItem = true; else if (it->contains(regexpAuthorYear)) { // email address followed by year if (!foundAuthors.contains((*it))) { // The author line is new (and not a duplicate), so add it to the author line list foundAuthors.append((*it)); } // Delete also non-duplicated entry, as now all what is needed will be processed in foundAuthors deleteItem = true; } if (deleteItem) it = commentList.erase(it); else ++it; } if (!foundAuthors.isEmpty()) { found = false; bool foundAuthor = false; const QString cy = cLocale.toString(QDate::currentDate(), QStringLiteral("yyyy")); ait = foundAuthors.end(); for (it = foundAuthors.begin() ; it != foundAuthors.end(); ++it) { if (it->contains(Settings::authorName()) || it->contains(Settings::authorEmail())) { foundAuthor = true; if (it->contains(cy)) found = true; else ait = it; } } if (!found) { if (!foundAuthor) foundAuthors.append(temp); else if (ait != foundAuthors.end()) { //update years const int index = (*ait).lastIndexOf(QRegExp(QStringLiteral("[\\d]+[\\d\\-, ]*"))); if (index == -1) (*ait) += QStringLiteral(", ") + cy; else ait->insert(index + 1, QStringLiteral(", ") + cy); } else qCDebug(LOKALIZE_LOG) << "INTERNAL ERROR: author found but iterator dangling!"; } } else foundAuthors.append(temp); foreach (QString author, foundAuthors) { // ensure dot at the end of copyright if (!author.endsWith(QLatin1Char('.'))) author += QLatin1Char('.'); commentList.append(author); } } //m_header.setComment( commentList.join( "\n" ) ); comment = commentList.join(QStringLiteral("\n")); //END comment = description, copyrights } QString fullUserName();// defined in helpers.cpp bool askAuthorInfoIfEmpty() { if (QThread::currentThread() == qApp->thread()) { if (Settings::authorName().isEmpty()) { bool ok; QString contact = QInputDialog::getText( SettingsController::instance()->mainWindowPtr(), i18nc("@window:title", "Author name missing"), i18n("Your name:"), QLineEdit::Normal, fullUserName(), &ok); Settings::self()->authorNameItem()->setValue(ok ? contact : fullUserName()); Settings::self()->save(); } if (Settings::authorEmail().isEmpty()) { bool ok; QString email = QInputDialog::getText( SettingsController::instance()->mainWindowPtr(), i18nc("@window:title", "Author email missing"), i18n("Your email:"), QLineEdit::Normal, QString(), &ok); if (ok) { Settings::self()->authorEmailItem()->setValue(email); Settings::self()->save(); } } } return !Settings::authorName().isEmpty() && !Settings::authorEmail().isEmpty(); } diff --git a/src/cataloglistview/cataloglistview.cpp b/src/cataloglistview/cataloglistview.cpp index f988181..d7ed980 100644 --- a/src/cataloglistview/cataloglistview.cpp +++ b/src/cataloglistview/cataloglistview.cpp @@ -1,331 +1,331 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2007-2009 by Nick Shaforostoff 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . **************************************************************************** */ #include "cataloglistview.h" #include "lokalize_debug.h" #include "catalogmodel.h" #include "catalog.h" #include "project.h" #include "prefs.h" #include "headerviewmenu.h" #include #include #include #include #include #include #include #include #include #include #include #include #include class CatalogTreeView: public QTreeView { public: CatalogTreeView(QWidget * parent) : QTreeView(parent) {} ~CatalogTreeView() {} protected: void keyReleaseEvent(QKeyEvent *e) override { if (e->key() == Qt::Key_Return && currentIndex().isValid()) { emit clicked(currentIndex()); e->accept(); } else { QTreeView::keyReleaseEvent(e); } } }; CatalogView::CatalogView(QWidget* parent, Catalog* catalog) : QDockWidget(i18nc("@title:window aka Message Tree", "Translation Units"), parent) , m_browser(new CatalogTreeView(this)) , m_lineEdit(new QLineEdit(this)) , m_model(new CatalogTreeModel(this, catalog)) , m_proxyModel(new CatalogTreeFilterModel(this)) { setObjectName(QStringLiteral("catalogTreeView")); QWidget* w = new QWidget(this); QVBoxLayout* layout = new QVBoxLayout(w); layout->setContentsMargins(0, 0, 0, 0); QHBoxLayout* l = new QHBoxLayout; l->setContentsMargins(0, 0, 0, 0); l->setSpacing(0); layout->addLayout(l); m_lineEdit->setClearButtonEnabled(true); m_lineEdit->setPlaceholderText(i18n("Quick search...")); m_lineEdit->setToolTip(i18nc("@info:tooltip", "Activated by Ctrl+L.") + ' ' + i18nc("@info:tooltip", "Accepts regular expressions")); connect(m_lineEdit, &QLineEdit::textChanged, this, &CatalogView::setFilterRegExp, Qt::QueuedConnection); // QShortcut* ctrlEsc=new QShortcut(QKeySequence(Qt::META+Qt::Key_Escape),this,SLOT(reset()),0,Qt::WidgetWithChildrenShortcut); QShortcut* esc = new QShortcut(QKeySequence(Qt::Key_Escape), this, 0, 0, Qt::WidgetWithChildrenShortcut); connect(esc, &QShortcut::activated, this, &CatalogView::escaped); QToolButton* btn = new QToolButton(w); btn->setPopupMode(QToolButton::InstantPopup); btn->setText(i18n("options")); //btn->setArrowType(Qt::DownArrow); btn->setMenu(new QMenu(this)); m_filterOptionsMenu = btn->menu(); connect(m_filterOptionsMenu, &QMenu::aboutToShow, this, &CatalogView::fillFilterOptionsMenu); connect(m_filterOptionsMenu, &QMenu::triggered, this, &CatalogView::filterOptionToggled); l->addWidget(m_lineEdit); l->addWidget(btn); layout->addWidget(m_browser); setTabOrder(m_lineEdit, btn); setTabOrder(btn, m_browser); setFocusProxy(m_lineEdit); setWidget(w); connect(m_browser, &CatalogTreeView::clicked, this, &CatalogView::slotItemActivated); m_browser->setRootIsDecorated(false); m_browser->setAllColumnsShowFocus(true); m_browser->setAlternatingRowColors(true); m_browser->viewport()->setBackgroundRole(QPalette::Window); #ifdef Q_OS_DARWIN QPalette p; p.setColor(QPalette::AlternateBase, p.color(QPalette::Window).darker(110)); p.setColor(QPalette::Highlight, p.color(QPalette::Window).darker(150)); m_browser->setPalette(p); #endif m_proxyModel->setSourceModel(m_model); m_browser->setModel(m_proxyModel); m_browser->setColumnWidth(0, m_browser->columnWidth(0) / 3); m_browser->setSortingEnabled(true); m_browser->sortByColumn(0, Qt::AscendingOrder); m_browser->setWordWrap(false); m_browser->setUniformRowHeights(true); m_browser->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); new HeaderViewMenuHandler(m_browser->header()); m_browser->header()->restoreState(readUiState("CatalogTreeViewState")); } CatalogView::~CatalogView() { writeUiState("CatalogTreeViewState", m_browser->header()->saveState()); } void CatalogView::setFocus() { QDockWidget::setFocus(); m_lineEdit->selectAll(); } void CatalogView::slotNewEntryDisplayed(const DocPosition& pos) { QModelIndex item = m_proxyModel->mapFromSource(m_model->index(pos.entry, 0)); m_browser->setCurrentIndex(item); m_browser->scrollTo(item/*,QAbstractItemView::PositionAtCenter*/); m_lastKnownDocPosition = pos.entry; } void CatalogView::setFilterRegExp() { QString expr = m_lineEdit->text(); if (m_proxyModel->filterRegExp().pattern() != expr) m_proxyModel->setFilterRegExp(m_proxyModel->filterOptions()&CatalogTreeFilterModel::IgnoreAccel ? expr.remove(Project::instance()->accel()) : expr); refreshCurrentIndex(); } void CatalogView::refreshCurrentIndex() { QModelIndex newPositionOfSelectedItem = m_proxyModel->mapFromSource(m_model->index(m_lastKnownDocPosition, 0)); m_browser->setCurrentIndex(newPositionOfSelectedItem); m_browser->scrollTo(newPositionOfSelectedItem); } void CatalogView::slotItemActivated(const QModelIndex& idx) { emit gotoEntry(DocPosition(m_proxyModel->mapToSource(idx).row()), 0); } void CatalogView::filterOptionToggled(QAction* action) { if (action->data().isNull()) return; int opt = action->data().toInt(); if (opt > 0) m_proxyModel->setFilterOptions(m_proxyModel->filterOptions()^opt); else { if (opt != -1) opt = -opt - 2; m_proxyModel->setFilterKeyColumn(opt); } m_filterOptionsMenu->clear(); refreshCurrentIndex(); } void CatalogView::fillFilterOptionsMenu() { m_filterOptionsMenu->clear(); if (m_proxyModel->individualRejectFilterEnabled()) m_filterOptionsMenu->addAction(i18n("Reset individual filter"), this, SLOT(setEntriesFilteredOut())); bool extStates = m_model->catalog()->capabilities()&ExtendedStates; const char* const basicTitles[] = { I18N_NOOP("Case insensitive"), I18N_NOOP("Ignore accelerator marks"), I18N_NOOP("Ready"), I18N_NOOP("Non-ready"), I18N_NOOP("Non-empty"), I18N_NOOP("Empty"), I18N_NOOP("Changed since file open"), I18N_NOOP("Unchanged since file open"), I18N_NOOP("Same in sync file"), I18N_NOOP("Different in sync file"), I18N_NOOP("Not in sync file"), I18N_NOOP("Plural"), I18N_NOOP("Non-plural"), }; const char* const* extTitles = Catalog::states(); const char* const* alltitles[2] = {basicTitles, extTitles}; QMenu* basicMenu = m_filterOptionsMenu->addMenu(i18nc("@title:inmenu", "Basic")); QMenu* extMenu = extStates ? m_filterOptionsMenu->addMenu(i18nc("@title:inmenu", "States")) : 0; QMenu* allmenus[2] = {basicMenu, extMenu}; QMenu* columnsMenu = m_filterOptionsMenu->addMenu(i18nc("@title:inmenu", "Searchable column")); QActionGroup* columnsMenuGroup = new QActionGroup(columnsMenu); QAction* txt; txt = m_filterOptionsMenu->addAction(i18nc("@title:inmenu", "Resort and refilter on content change"), m_proxyModel, &CatalogTreeFilterModel::setDynamicSortFilter); txt->setCheckable(true); txt->setChecked(m_proxyModel->dynamicSortFilter()); for (int i = 0; (1 << i) < CatalogTreeFilterModel::MaxOption; ++i) { bool ext = (1 << i) >= CatalogTreeFilterModel::New; if (!extStates && ext) break; txt = allmenus[ext]->addAction(i18n(alltitles[ext][i - ext * FIRSTSTATEPOSITION])); txt->setData(1 << i); txt->setCheckable(true); txt->setChecked(m_proxyModel->filterOptions() & (1 << i)); if ((1 << i) == CatalogTreeFilterModel::IgnoreAccel) basicMenu->addSeparator(); } if (!extStates) m_filterOptionsMenu->addSeparator(); - for (int i = -1; i < CatalogTreeModel::DisplayedColumnCount-1; ++i) { + for (int i = -1; i < CatalogTreeModel::DisplayedColumnCount - 1; ++i) { txt = columnsMenu->addAction((i == -1) ? i18nc("@item:inmenu all columns", "All") : m_model->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString()); txt->setData(-i - 2); txt->setCheckable(true); txt->setChecked(m_proxyModel->filterKeyColumn() == i); txt->setActionGroup(columnsMenuGroup); } refreshCurrentIndex(); } void CatalogView::reset() { m_proxyModel->setFilterKeyColumn(-1); m_proxyModel->setFilterOptions(CatalogTreeFilterModel::AllStates); m_lineEdit->clear(); refreshCurrentIndex(); //emit gotoEntry(DocPosition(m_proxyModel->mapToSource(m_browser->currentIndex()).row()),0); slotItemActivated(m_browser->currentIndex()); } void CatalogView::setMergeCatalogPointer(MergeCatalog* pointer) { m_proxyModel->setMergeCatalogPointer(pointer); } int CatalogView::siblingEntryNumber(int step) { QModelIndex item = m_browser->currentIndex(); int lastRow = m_proxyModel->rowCount() - 1; if (!item.isValid()) { if (lastRow == -1) return -1; item = m_proxyModel->index((step == 1) ? 0 : lastRow, 0); m_browser->setCurrentIndex(item); } else { if (item.row() == ((step == -1) ? 0 : lastRow)) return -1; item = item.sibling(item.row() + step, 0); } return m_proxyModel->mapToSource(item).row(); } int CatalogView::nextEntryNumber() { return siblingEntryNumber(1); } int CatalogView::prevEntryNumber() { return siblingEntryNumber(-1); } static int edgeEntry(CatalogTreeFilterModel* m_proxyModel, int row) { if (!m_proxyModel->rowCount()) return -1; return m_proxyModel->mapToSource(m_proxyModel->index(row, 0)).row(); } int CatalogView::firstEntryNumber() { return edgeEntry(m_proxyModel, 0); } int CatalogView::lastEntryNumber() { return edgeEntry(m_proxyModel, m_proxyModel->rowCount() - 1); } void CatalogView::setEntryFilteredOut(int entry, bool filteredOut) { m_proxyModel->setEntryFilteredOut(entry, filteredOut); refreshCurrentIndex(); } void CatalogView::setEntriesFilteredOut(bool filteredOut) { show(); m_proxyModel->setEntriesFilteredOut(filteredOut); refreshCurrentIndex(); } diff --git a/src/cataloglistview/catalogmodel.cpp b/src/cataloglistview/catalogmodel.cpp index 93ea83b..eba9368 100644 --- a/src/cataloglistview/catalogmodel.cpp +++ b/src/cataloglistview/catalogmodel.cpp @@ -1,367 +1,367 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2007-2014 by Nick Shaforostoff 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . **************************************************************************** */ #include "catalogmodel.h" #include "lokalize_debug.h" #include "catalog.h" #include "project.h" #include #include #include #include #include #include #define DYNAMICFILTER_LIMIT 256 QVector CatalogTreeModel::m_fonts; CatalogTreeModel::CatalogTreeModel(QObject* parent, Catalog* catalog) : QAbstractItemModel(parent) , m_catalog(catalog) , m_ignoreAccel(true) { if (m_fonts.isEmpty()) { QVector fonts(4, QApplication::font()); fonts[1].setItalic(true); //fuzzy fonts[2].setBold(true); //modified fonts[3].setItalic(true); //fuzzy fonts[3].setBold(true); //modified m_fonts.reserve(4); for (int i = 0; i < 4; i++) m_fonts << fonts.at(i); } connect(catalog, &Catalog::signalEntryModified, this, &CatalogTreeModel::reflectChanges); connect(catalog, QOverload<>::of(&Catalog::signalFileLoaded), this, &CatalogTreeModel::fileLoaded); } QModelIndex CatalogTreeModel::index(int row, int column, const QModelIndex& /*parent*/) const { return createIndex(row, column); } QModelIndex CatalogTreeModel::parent(const QModelIndex& /*index*/) const { return QModelIndex(); } int CatalogTreeModel::columnCount(const QModelIndex& parent) const { Q_UNUSED(parent); return DisplayedColumnCount; } void CatalogTreeModel::fileLoaded() { beginResetModel(); endResetModel(); } void CatalogTreeModel::reflectChanges(DocPosition pos) { emit dataChanged(index(pos.entry, 0), index(pos.entry, DisplayedColumnCount - 1)); #if 0 I disabled dynamicSortFilter function //lazy sorting/filtering if (rowCount() < DYNAMICFILTER_LIMIT || m_prevChanged != pos) { qCWarning(LOKALIZE_LOG) << "first dataChanged emitment" << pos.entry; emit dataChanged(index(pos.entry, 0), index(pos.entry, DisplayedColumnCount - 1)); if (!(rowCount() < DYNAMICFILTER_LIMIT)) { qCWarning(LOKALIZE_LOG) << "second dataChanged emitment" << m_prevChanged.entry; emit dataChanged(index(m_prevChanged.entry, 0), index(m_prevChanged.entry, DisplayedColumnCount - 1)); } } m_prevChanged = pos; #endif } int CatalogTreeModel::rowCount(const QModelIndex& parent) const { if (parent.isValid()) return 0; return m_catalog->numberOfEntries(); } QVariant CatalogTreeModel::headerData(int section, Qt::Orientation /*orientation*/, int role) const { if (role != Qt::DisplayRole) return QVariant(); switch (static_cast(section)) { - case CatalogModelColumns::Key: - return i18nc("@title:column", "Entry"); - case CatalogModelColumns::Source: - return i18nc("@title:column Original text", "Source"); - case CatalogModelColumns::Target: - return i18nc("@title:column Text in target language", "Target"); - case CatalogModelColumns::Notes: - return i18nc("@title:column", "Notes"); - case CatalogModelColumns::Context: - return i18nc("@title:column", "Context"); - case CatalogModelColumns::Files: - return i18nc("@title:column", "Files"); - case CatalogModelColumns::TranslationStatus: - return i18nc("@title:column", "Translation Status"); - case CatalogModelColumns::SourceLength: - return i18nc("@title:column Length of the original text", "Source length"); - case CatalogModelColumns::TargetLength: - return i18nc("@title:column Length of the text in target language", "Target length"); - default: - return {}; + case CatalogModelColumns::Key: + return i18nc("@title:column", "Entry"); + case CatalogModelColumns::Source: + return i18nc("@title:column Original text", "Source"); + case CatalogModelColumns::Target: + return i18nc("@title:column Text in target language", "Target"); + case CatalogModelColumns::Notes: + return i18nc("@title:column", "Notes"); + case CatalogModelColumns::Context: + return i18nc("@title:column", "Context"); + case CatalogModelColumns::Files: + return i18nc("@title:column", "Files"); + case CatalogModelColumns::TranslationStatus: + return i18nc("@title:column", "Translation Status"); + case CatalogModelColumns::SourceLength: + return i18nc("@title:column Length of the original text", "Source length"); + case CatalogModelColumns::TargetLength: + return i18nc("@title:column Length of the text in target language", "Target length"); + default: + return {}; } } QVariant CatalogTreeModel::data(const QModelIndex& index, int role) const { if (m_catalog->numberOfEntries() <= index.row()) return QVariant(); const CatalogModelColumns column = static_cast(index.column()); if (role == Qt::SizeHintRole) { //no need to cache because of uniform row heights return QFontMetrics(QApplication::font()).size(Qt::TextSingleLine, QString::fromLatin1(" ")); } else if (role == Qt::FontRole/* && index.column()==Target*/) { bool fuzzy = !m_catalog->isApproved(index.row()); bool modified = m_catalog->isModified(index.row()); return m_fonts.at(fuzzy * 1 | modified * 2); } else if (role == Qt::ForegroundRole) { if (m_catalog->isBookmarked(index.row())) { static KColorScheme colorScheme(QPalette::Normal); return colorScheme.foreground(KColorScheme::LinkText); } if (m_catalog->isObsolete(index.row())) { static KColorScheme colorScheme(QPalette::Normal); return colorScheme.foreground(KColorScheme::InactiveText); } } else if (role == Qt::ToolTipRole) { if (column != CatalogModelColumns::TranslationStatus) { return {}; } switch (getTranslationStatus(index.row())) { - case TranslationStatus::Ready: - return i18nc("@info:status 'non-fuzzy' in gettext terminology", "Ready"); - case TranslationStatus::NeedsReview: - return i18nc("@info:status 'fuzzy' in gettext terminology", "Needs review"); - case TranslationStatus::Untranslated: - return i18nc("@info:status", "Untranslated"); + case TranslationStatus::Ready: + return i18nc("@info:status 'non-fuzzy' in gettext terminology", "Ready"); + case TranslationStatus::NeedsReview: + return i18nc("@info:status 'fuzzy' in gettext terminology", "Needs review"); + case TranslationStatus::Untranslated: + return i18nc("@info:status", "Untranslated"); } } else if (role == Qt::DecorationRole) { if (column != CatalogModelColumns::TranslationStatus) { return {}; } switch (getTranslationStatus(index.row())) { - case TranslationStatus::Ready: - return QIcon::fromTheme("emblem-checked"); - case TranslationStatus::NeedsReview: - return QIcon::fromTheme("emblem-question"); - case TranslationStatus::Untranslated: - return QIcon::fromTheme("emblem-unavailable"); + case TranslationStatus::Ready: + return QIcon::fromTheme("emblem-checked"); + case TranslationStatus::NeedsReview: + return QIcon::fromTheme("emblem-question"); + case TranslationStatus::Untranslated: + return QIcon::fromTheme("emblem-unavailable"); } } else if (role == Qt::UserRole) { switch (column) { - case CatalogModelColumns::TranslationStatus: - return m_catalog->isApproved(index.row()); - case CatalogModelColumns::IsEmpty: - return m_catalog->isEmpty(index.row()); - case CatalogModelColumns::State: - return int(m_catalog->state(index.row())); - case CatalogModelColumns::IsModified: - return m_catalog->isModified(index.row()); - case CatalogModelColumns::IsPlural: - return m_catalog->isPlural(index.row()); - default: - role = Qt::DisplayRole; + case CatalogModelColumns::TranslationStatus: + return m_catalog->isApproved(index.row()); + case CatalogModelColumns::IsEmpty: + return m_catalog->isEmpty(index.row()); + case CatalogModelColumns::State: + return int(m_catalog->state(index.row())); + case CatalogModelColumns::IsModified: + return m_catalog->isModified(index.row()); + case CatalogModelColumns::IsPlural: + return m_catalog->isPlural(index.row()); + default: + role = Qt::DisplayRole; } } else if (role == StringFilterRole) { //exclude UI strings if (column >= CatalogModelColumns::TranslationStatus) return QVariant(); else if (column == CatalogModelColumns::Source || column == CatalogModelColumns::Target) { QString str = column == CatalogModelColumns::Source ? m_catalog->msgidWithPlurals(index.row(), false) : m_catalog->msgstrWithPlurals(index.row(), false); return m_ignoreAccel ? str.remove(Project::instance()->accel()) : str; } role = Qt::DisplayRole; } else if (role == SortRole) { //exclude UI strings if (column == CatalogModelColumns::TranslationStatus) { return static_cast(getTranslationStatus(index.row())); } role = Qt::DisplayRole; } if (role != Qt::DisplayRole) return QVariant(); switch (column) { - case CatalogModelColumns::Key: - return index.row() + 1; - case CatalogModelColumns::Source: - return m_catalog->msgidWithPlurals(index.row(), true); - case CatalogModelColumns::Target: - return m_catalog->msgstrWithPlurals(index.row(), true); - case CatalogModelColumns::Notes: { - QString result; - foreach (const Note ¬e, m_catalog->notes(index.row())) - result += note.content; - return result; - } - case CatalogModelColumns::Context: - return m_catalog->context(index.row()); - case CatalogModelColumns::Files: - return m_catalog->sourceFiles(index.row()).join('|'); - case CatalogModelColumns::SourceLength: - return m_catalog->msgidWithPlurals(index.row(), false).length(); - case CatalogModelColumns::TargetLength: - return m_catalog->msgstrWithPlurals(index.row(), false).length(); - default: - return {}; + case CatalogModelColumns::Key: + return index.row() + 1; + case CatalogModelColumns::Source: + return m_catalog->msgidWithPlurals(index.row(), true); + case CatalogModelColumns::Target: + return m_catalog->msgstrWithPlurals(index.row(), true); + case CatalogModelColumns::Notes: { + QString result; + foreach (const Note ¬e, m_catalog->notes(index.row())) + result += note.content; + return result; + } + case CatalogModelColumns::Context: + return m_catalog->context(index.row()); + case CatalogModelColumns::Files: + return m_catalog->sourceFiles(index.row()).join('|'); + case CatalogModelColumns::SourceLength: + return m_catalog->msgidWithPlurals(index.row(), false).length(); + case CatalogModelColumns::TargetLength: + return m_catalog->msgstrWithPlurals(index.row(), false).length(); + default: + return {}; } } CatalogTreeModel::TranslationStatus CatalogTreeModel::getTranslationStatus(int row) const { if (m_catalog->isEmpty(row)) { return CatalogTreeModel::TranslationStatus::Untranslated; } if (m_catalog->isApproved(row)) { return CatalogTreeModel::TranslationStatus::Ready; } else { return CatalogTreeModel::TranslationStatus::NeedsReview; } } CatalogTreeFilterModel::CatalogTreeFilterModel(QObject* parent) : QSortFilterProxyModel(parent) , m_filterOptions(AllStates) , m_individualRejectFilterEnable(false) , m_mergeCatalog(NULL) { setFilterKeyColumn(-1); setFilterCaseSensitivity(Qt::CaseInsensitive); setFilterRole(CatalogTreeModel::StringFilterRole); setSortRole(CatalogTreeModel::SortRole); setDynamicSortFilter(false); } void CatalogTreeFilterModel::setSourceModel(QAbstractItemModel* sourceModel) { QSortFilterProxyModel::setSourceModel(sourceModel); connect(sourceModel, &QAbstractItemModel::modelReset, this, QOverload<>::of(&CatalogTreeFilterModel::setEntriesFilteredOut)); setEntriesFilteredOut(false); } void CatalogTreeFilterModel::setEntriesFilteredOut() { return setEntriesFilteredOut(false); } void CatalogTreeFilterModel::setEntriesFilteredOut(bool filteredOut) { m_individualRejectFilter.fill(filteredOut, sourceModel()->rowCount()); m_individualRejectFilterEnable = filteredOut; invalidateFilter(); } void CatalogTreeFilterModel::setEntryFilteredOut(int entry, bool filteredOut) { // if (entry>=m_individualRejectFilter.size()) // sourceModelReset(); m_individualRejectFilter[entry] = filteredOut; m_individualRejectFilterEnable = true; invalidateFilter(); } void CatalogTreeFilterModel::setFilterOptions(int o) { m_filterOptions = o; setFilterCaseSensitivity(o & CaseInsensitive ? Qt::CaseInsensitive : Qt::CaseSensitive); static_cast(sourceModel())->setIgnoreAccel(o & IgnoreAccel); invalidateFilter(); } bool CatalogTreeFilterModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const { int filerOptions = m_filterOptions; bool accepts = true; if (bool(filerOptions & Ready) != bool(filerOptions & NotReady)) { bool ready = sourceModel()->index(source_row, static_cast(CatalogTreeModel::CatalogModelColumns::TranslationStatus), source_parent).data(Qt::UserRole).toBool(); accepts = (ready == bool(filerOptions & Ready) || ready != bool(filerOptions & NotReady)); } if (accepts && bool(filerOptions & NonEmpty) != bool(filerOptions & Empty)) { bool untr = sourceModel()->index(source_row, static_cast(CatalogTreeModel::CatalogModelColumns::IsEmpty), source_parent).data(Qt::UserRole).toBool(); accepts = (untr == bool(filerOptions & Empty) || untr != bool(filerOptions & NonEmpty)); } if (accepts && bool(filerOptions & Modified) != bool(filerOptions & NonModified)) { bool modified = sourceModel()->index(source_row, static_cast(CatalogTreeModel::CatalogModelColumns::IsModified), source_parent).data(Qt::UserRole).toBool(); accepts = (modified == bool(filerOptions & Modified) || modified != bool(filerOptions & NonModified)); } if (accepts && bool(filerOptions & Plural) != bool(filerOptions & NonPlural)) { bool modified = sourceModel()->index(source_row, static_cast(CatalogTreeModel::CatalogModelColumns::IsPlural), source_parent).data(Qt::UserRole).toBool(); accepts = (modified == bool(filerOptions & Plural) || modified != bool(filerOptions & NonPlural)); } // These are the possible sync options of a row: // * SameInSync: The sync file contains a row with the same msgid and the same msgstr. // * DifferentInSync: The sync file contains a row with the same msgid and different msgstr. // * NotInSync: The sync file does not contain any row with the same msgid. // // The code below takes care of filtering rows when any of those options is not checked. // const int mask = (SameInSync | DifferentInSync | NotInSync); if (accepts && m_mergeCatalog && (filerOptions & mask) && (filerOptions & mask) != mask) { bool isPresent = m_mergeCatalog->isPresent(source_row); bool isDifferent = m_mergeCatalog->isDifferent(source_row); accepts = ! ((isPresent && !isDifferent && !bool(filerOptions & SameInSync)) || (isPresent && isDifferent && !bool(filerOptions & DifferentInSync)) || (!isPresent && !bool(filerOptions & NotInSync)) ); } if (accepts && (filerOptions & STATES) != STATES) { int state = sourceModel()->index(source_row, static_cast(CatalogTreeModel::CatalogModelColumns::State), source_parent).data(Qt::UserRole).toInt(); accepts = (filerOptions & (1 << (state + FIRSTSTATEPOSITION))); } accepts = accepts && !(m_individualRejectFilterEnable && source_row < m_individualRejectFilter.size() && m_individualRejectFilter.at(source_row)); return accepts && QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent); } void CatalogTreeFilterModel::setMergeCatalogPointer(MergeCatalog* pointer) { m_mergeCatalog = pointer; } diff --git a/src/glossary/glossary.cpp b/src/glossary/glossary.cpp index f1655be..f71c592 100644 --- a/src/glossary/glossary.cpp +++ b/src/glossary/glossary.cpp @@ -1,733 +1,733 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2007-2011 by Nick Shaforostoff 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . **************************************************************************** */ #include "glossary.h" #include "lokalize_debug.h" #include "stemming.h" // #include "tbxparser.h" #include "project.h" #include "prefs_lokalize.h" #include "domroutines.h" #include #include #include #include #include #include #include #include using namespace GlossaryNS; static const QString defaultLang = QStringLiteral("en_US"); static const QString xmlLang = QStringLiteral("xml:lang"); static const QString ntig = QStringLiteral("ntig"); static const QString tig = QStringLiteral("tig"); static const QString termGrp = QStringLiteral("termGrp"); static const QString langSet = QStringLiteral("langSet"); static const QString term = QStringLiteral("term"); static const QString id = QStringLiteral("id"); QList Glossary::idsForLangWord(const QString& lang, const QString& word) const { return idsByLangWord[lang].values(word); } Glossary::Glossary(QObject* parent) : QObject(parent) , m_clean(true) { } //BEGIN DISK bool Glossary::load(const QString& newPath) { QElapsedTimer a; a.start(); //BEGIN NEW QIODevice* device = new QFile(newPath); if (!device->open(QFile::ReadOnly | QFile::Text)) { delete device; //return; device = new QBuffer(); static_cast(device)->setData(QByteArray( "\n" "\n" "\n" " \n" " \n" " \n" " \n" "\n" )); } QXmlSimpleReader reader; - reader.setFeature("http://qt-project.org/xml/features/report-whitespace-only-CharData",true); + reader.setFeature("http://qt-project.org/xml/features/report-whitespace-only-CharData", true); reader.setFeature("http://xml.org/sax/features/namespaces", false); QXmlInputSource source(device); QDomDocument newDoc; QString errorMsg; int errorLine;//+errorColumn; bool success = newDoc.setContent(&source, &reader, &errorMsg, &errorLine/*,errorColumn*/); delete device; if (!success) { qCWarning(LOKALIZE_LOG) << errorMsg; return false; //errorLine+1; } clear();//does setClean(true); m_path = newPath; m_doc = newDoc; //QDomElement file=m_doc.elementsByTagName("file").at(0).toElement(); m_entries = m_doc.elementsByTagName(QStringLiteral("termEntry")); for (int i = 0; i < m_entries.size(); i++) hashTermEntry(m_entries.at(i).toElement()); m_idsForEntriesById = m_entriesById.keys(); //END NEW #if 0 TbxParser parser(this); QXmlSimpleReader reader1; reader1.setContentHandler(&parser); QFile file(p); if (!file.open(QFile::ReadOnly | QFile::Text)) return; QXmlInputSource xmlInputSource(&file); if (!reader1.parse(xmlInputSource)) qCWarning(LOKALIZE_LOG) << "failed to load " << path; #endif emit loaded(); if (a.elapsed() > 50) qCDebug(LOKALIZE_LOG) << "glossary loaded in" << a.elapsed(); return true; } bool Glossary::save() { if (m_path.isEmpty()) return false; QFile* device = new QFile(m_path); if (!device->open(QFile::WriteOnly | QFile::Truncate)) { device->deleteLater(); return false; } QTextStream stream(device); m_doc.save(stream, 2); device->deleteLater(); setClean(true); return true; } void Glossary::setClean(bool clean) { m_clean = clean; emit changed();//may be emitted multiple times in a row. so what? :) } //END DISK //BEGIN MODEL #define FETCH_SIZE 128 void GlossarySortFilterProxyModel::setFilterRegExp(const QString& s) { if (!sourceModel()) return; //static const QRegExp lettersOnly("^[a-z]"); QSortFilterProxyModel::setFilterRegExp(s); fetchMore(QModelIndex()); } void GlossarySortFilterProxyModel::fetchMore(const QModelIndex&) { int expectedCount = rowCount() + FETCH_SIZE / 2; while (rowCount(QModelIndex()) < expectedCount && sourceModel()->canFetchMore(QModelIndex())) { sourceModel()->fetchMore(QModelIndex()); QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents | QEventLoop::ExcludeSocketNotifiers); } } GlossaryModel::GlossaryModel(QObject* parent) : QAbstractListModel(parent) , m_visibleCount(0) , m_glossary(Project::instance()->glossary()) { connect(m_glossary, &Glossary::loaded, this, &GlossaryModel::forceReset); } void GlossaryModel::forceReset() { beginResetModel(); m_visibleCount = 0; endResetModel(); } bool GlossaryModel::canFetchMore(const QModelIndex&) const { return false;//!parent.isValid() && m_glossary->size()!=m_visibleCount; } void GlossaryModel::fetchMore(const QModelIndex& parent) { int newVisibleCount = qMin(m_visibleCount + FETCH_SIZE, m_glossary->size()); beginInsertRows(parent, m_visibleCount, newVisibleCount - 1); m_visibleCount = newVisibleCount; endInsertRows(); } int GlossaryModel::rowCount(const QModelIndex& parent) const { if (parent.isValid()) return 0; return m_glossary->size();//m_visibleCount; } QVariant GlossaryModel::headerData(int section, Qt::Orientation /*orientation*/, int role) const { if (role != Qt::DisplayRole) return QVariant(); switch (section) { //case ID: return i18nc("@title:column","ID"); case English: return i18nc("@title:column Original text", "Source");; case Target: return i18nc("@title:column Text in target language", "Target"); case SubjectField: return i18nc("@title:column", "Subject Field"); } return QVariant(); } QVariant GlossaryModel::data(const QModelIndex& index, int role) const { //if (role==Qt::SizeHintRole) // return QVariant(QSize(50, 30)); if (role != Qt::DisplayRole) return QVariant(); static const QString nl = QStringLiteral(" ") + QChar(0x00B7) + ' '; static Project* project = Project::instance(); Glossary* glossary = m_glossary; QByteArray id = glossary->id(index.row()); switch (index.column()) { case ID: return id; case English: return glossary->terms(id, project->sourceLangCode()).join(nl); case Target: return glossary->terms(id, project->targetLangCode()).join(nl); case SubjectField: return glossary->subjectField(id); } return QVariant(); } /* QModelIndex GlossaryModel::index (int row,int column,const QModelIndex& parent) const { return createIndex (row, column); } */ int GlossaryModel::columnCount(const QModelIndex&) const { return GlossaryModelColumnCount; } /* Qt::ItemFlags GlossaryModel::flags ( const QModelIndex & index ) const { return Qt::ItemIsSelectable|Qt::ItemIsEnabled; //if (index.column()==FuzzyFlag) // return Qt::ItemIsSelectable|Qt::ItemIsUserCheckable|Qt::ItemIsEnabled; //return QAbstractItemModel::flags(index); } */ //END MODEL general (GlossaryModel continues below) //BEGIN EDITING QByteArray Glossary::generateNewId() { // generate unique ID int idNumber = 0; QList busyIdNumbers; QString authorId(Settings::authorName().toLower()); authorId.replace(' ', '_'); QRegExp rx('^' + authorId + QStringLiteral("\\-([0-9]*)$")); foreach (const QByteArray& id, m_idsForEntriesById) { if (rx.exactMatch(QString::fromLatin1(id))) busyIdNumbers.append(rx.cap(1).toInt()); } int i = removedIds.size(); while (--i >= 0) { if (rx.exactMatch(QString::fromLatin1(removedIds.at(i)))) busyIdNumbers.append(rx.cap(1).toInt()); } if (!busyIdNumbers.isEmpty()) { std::sort(busyIdNumbers.begin(), busyIdNumbers.end()); while (busyIdNumbers.contains(idNumber)) ++idNumber; } return authorId.toLatin1() + '-' + QByteArray::number(idNumber); } QStringList Glossary::subjectFields() const { QSet result; foreach (const QByteArray& id, m_idsForEntriesById) result.insert(subjectField(id)); return result.values(); } QByteArray Glossary::id(int index) const { if (index < m_idsForEntriesById.size()) return m_idsForEntriesById.at(index); return QByteArray(); } QStringList Glossary::terms(const QByteArray& id, const QString& language) const { QString minusLang = language; minusLang.replace('_', '-'); QStringRef soleLang = language.leftRef(2); QStringList result; QDomElement n = m_entriesById.value(id).firstChildElement(langSet); while (!n.isNull()) { QString lang = n.attribute(xmlLang, defaultLang); if (language == lang || minusLang == lang || soleLang == lang) { QDomElement ntigElem = n.firstChildElement(ntig); while (!ntigElem.isNull()) { result << ntigElem.firstChildElement(termGrp).firstChildElement(term).text(); ntigElem = ntigElem.nextSiblingElement(ntig); } QDomElement tigElem = n.firstChildElement(tig); while (!tigElem.isNull()) { result << tigElem.firstChildElement(term).text(); tigElem = tigElem.nextSiblingElement(tig); } } n = n.nextSiblingElement(langSet); } return result; } // QDomElement ourLangSetElement will reference the lang tag we want (if it exists) static void getElementsForTermLangIndex(QDomElement termEntry, QString& lang, int index, QDomElement& ourLangSetElement, QDomElement& tigElement, //<-- can point to as well QDomElement& termElement) { QString minusLang = lang; minusLang.replace('_', '-'); QStringRef soleLang = lang.leftRef(2); QDomElement n = termEntry.firstChildElement(langSet); QDomDocument document = n.ownerDocument(); int i = 0; while (!n.isNull()) { QString nLang = n.attribute(xmlLang, defaultLang); if (lang == nLang || minusLang == nLang || soleLang == nLang) { ourLangSetElement = n; QDomElement ntigElem = n.firstChildElement(ntig); while (!ntigElem.isNull()) { if (i == index) { tigElement = ntigElem; termElement = ntigElem.firstChildElement(termGrp).firstChildElement(term); return; } ntigElem = ntigElem.nextSiblingElement(ntig); i++; } QDomElement tigElem = n.firstChildElement(tig); while (!tigElem.isNull()) { //qCDebug(LOKALIZE_LOG)<& wordHash, // const QString& what, // int index) void Glossary::hashTermEntry(const QDomElement& termEntry) { QByteArray entryId = termEntry.attribute(::id).toLatin1(); if (entryId.isEmpty()) return; m_entriesById.insert(entryId, termEntry); QString sourceLangCode = Project::instance()->sourceLangCode(); foreach (const QString& termText, terms(entryId, sourceLangCode)) { foreach (const QString& word, termText.split(' ', QString::SkipEmptyParts)) idsByLangWord[sourceLangCode].insert(stem(sourceLangCode, word), entryId); } } void Glossary::unhashTermEntry(const QDomElement& termEntry) { QByteArray entryId = termEntry.attribute(::id).toLatin1(); m_entriesById.remove(entryId); QString sourceLangCode = Project::instance()->sourceLangCode(); foreach (const QString& termText, terms(entryId, sourceLangCode)) { foreach (const QString& word, termText.split(' ', QString::SkipEmptyParts)) idsByLangWord[sourceLangCode].remove(stem(sourceLangCode, word), entryId); } } #if 0 void Glossary::hashTermEntry(int index) { Q_ASSERT(index < termList.size()); foreach (const QString& term, termList_.at(index).english) { foreach (const QString& word, term.split(' ', QString::SkipEmptyParts)) wordHash_.insert(stem(Project::instance()->sourceLangCode(), word), index); } } void Glossary::unhashTermEntry(int index) { Q_ASSERT(index < termList.size()); foreach (const QString& term, termList_.at(index).english) { foreach (const QString& word, term.split(' ', QString::SkipEmptyParts)) wordHash_.remove(stem(Project::instance()->sourceLangCode(), word), index); } } #endif void Glossary::removeEntry(const QByteArray& id) { if (!m_entriesById.contains(id)) return; QDomElement entry = m_entriesById.value(id); if (entry.nextSibling().isCharacterData()) entry.parentNode().removeChild(entry.nextSibling()); //nice formatting entry.parentNode().removeChild(entry); m_entriesById.remove(id); unhashTermEntry(entry); m_idsForEntriesById = m_entriesById.keys(); removedIds.append(id); //for new id generation goodness setClean(false); } static void appendTerm(QDomElement langSetElem, const QString& termText) { QDomDocument doc = langSetElem.ownerDocument(); /* QDomElement ntigElement=doc.createElement(ntig); langSetElem.appendChild(ntigElement); QDomElement termGrpElement=doc.createElement(termGrp); ntigElement.appendChild(termGrpElement); QDomElement termElement=doc.createElement(term); termGrpElement.appendChild(termElement); termElement.appendChild(doc.createTextNode(termText)); */ QDomElement tigElement = doc.createElement(tig); langSetElem.appendChild(tigElement); QDomElement termElement = doc.createElement(term); tigElement.appendChild(termElement); termElement.appendChild(doc.createTextNode(termText)); } QByteArray Glossary::append(const QStringList& sourceTerms, const QStringList& targetTerms) { if (!m_doc.elementsByTagName(QStringLiteral("body")).count()) return QByteArray(); setClean(false); QDomElement termEntry = m_doc.createElement(QStringLiteral("termEntry")); m_doc.elementsByTagName(QStringLiteral("body")).at(0).appendChild(termEntry); //m_entries=m_doc.elementsByTagName("termEntry"); QByteArray newId = generateNewId(); termEntry.setAttribute(::id, QString::fromLatin1(newId)); QDomElement sourceElem = m_doc.createElement(langSet); termEntry.appendChild(sourceElem); sourceElem.setAttribute(xmlLang, Project::instance()->sourceLangCode().replace('_', '-')); foreach (QString sourceTerm, sourceTerms) appendTerm(sourceElem, sourceTerm); QDomElement targetElem = m_doc.createElement(langSet); termEntry.appendChild(targetElem); targetElem.setAttribute(xmlLang, Project::instance()->targetLangCode().replace('_', '-')); foreach (QString targetTerm, targetTerms) appendTerm(targetElem, targetTerm); hashTermEntry(termEntry); m_idsForEntriesById = m_entriesById.keys(); return newId; } void Glossary::append(const QString& _english, const QString& _target) { append(QStringList(_english), QStringList(_target)); } void Glossary::clear() { setClean(true); //path.clear(); idsByLangWord.clear(); m_entriesById.clear(); m_idsForEntriesById.clear(); removedIds.clear(); changedIds_.clear(); addedIds_.clear(); wordHash_.clear(); termList_.clear(); langWordEntry_.clear(); subjectFields_ = QStringList(QString()); m_doc.clear(); } bool GlossaryModel::removeRows(int row, int count, const QModelIndex& parent) { beginRemoveRows(parent, row, row + count - 1); Glossary* glossary = Project::instance()->glossary(); int i = row + count; while (--i >= row) glossary->removeEntry(glossary->id(i)); endRemoveRows(); return true; } // bool GlossaryModel::insertRows(int row,int count,const QModelIndex& parent) // { // if (row!=rowCount()) // return false; QByteArray GlossaryModel::appendRow(const QString& _english, const QString& _target) { bool notify = !canFetchMore(QModelIndex()); if (notify) beginInsertRows(QModelIndex(), rowCount(), rowCount()); QByteArray id = m_glossary->append(QStringList(_english), QStringList(_target)); if (notify) { m_visibleCount++; endInsertRows(); } return id; } //END EDITING diff --git a/src/languagetool/languagetoolgrammarerror.cpp b/src/languagetool/languagetoolgrammarerror.cpp index 1513390..8af8648 100644 --- a/src/languagetool/languagetoolgrammarerror.cpp +++ b/src/languagetool/languagetoolgrammarerror.cpp @@ -1,81 +1,81 @@ /* Copyright (C) 2019-2020 Laurent Montel This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "languagetoolgrammarerror.h" #include "lokalize_debug.h" #include "languagetoolmanager.h" #include #include LanguageToolGrammarError::LanguageToolGrammarError() { } LanguageToolGrammarError::~LanguageToolGrammarError() { } QString LanguageToolGrammarError::parse(const QJsonObject &obj, const QString &text) { - + QString mError = obj[QStringLiteral("message")].toString(); int mStart = obj[QStringLiteral("offset")].toInt(-1); int mLength = obj[QStringLiteral("length")].toInt(-1); QStringList mSuggestions = parseSuggestion(obj); /* const QJsonObject rulesObj = obj[QStringLiteral("rule")].toObject(); if (!rulesObj.isEmpty()) { QString mRule = rulesObj[QStringLiteral("id")].toString(); const QJsonArray urlArray = rulesObj[QStringLiteral("urls")].toArray(); if (!urlArray.isEmpty()) { if (urlArray.count() > 1) { qCWarning(LOKALIZE_LOG) << "LanguageToolGrammarError::parse : more than 1 url found. Perhaps need to adapt api "; } QString mUrl = urlArray.at(0)[QLatin1String("value")].toString(); //qDebug() << " mUrl" << mUrl; } }*/ QString result = mError; if (mLength > 0) result = result + QStringLiteral(" (") + text.mid(mStart, mLength) + QStringLiteral(")"); if (mSuggestions.count() > 0) result = result + QStringLiteral("\n") + i18n("Suggestions:") + QStringLiteral(" ") + mSuggestions.join(QStringLiteral(", ")); return result + QStringLiteral("\n\n"); } void LanguageToolGrammarError::setTesting(bool b) { mTesting = b; } QStringList LanguageToolGrammarError::parseSuggestion(const QJsonObject &obj) { QStringList lst; const QJsonArray array = obj[QStringLiteral("replacements")].toArray(); for (const QJsonValue ¤t : array) { if (current.type() == QJsonValue::Object) { const QJsonObject suggestionObject = current.toObject(); lst.append(suggestionObject[QLatin1String("value")].toString()); } } //qDebug() << " lst : " << lst; return lst; } diff --git a/src/languagetool/languagetoolmanager.cpp b/src/languagetool/languagetoolmanager.cpp index fc2627d..fb31640 100644 --- a/src/languagetool/languagetoolmanager.cpp +++ b/src/languagetool/languagetoolmanager.cpp @@ -1,59 +1,59 @@ /* Copyright (C) 2019-2020 Laurent Montel This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "languagetoolmanager.h" #include "prefs_lokalize.h" #include #include #include #include #include LanguageToolManager::LanguageToolManager(QObject *parent) : QObject(parent) , mNetworkAccessManager(new QNetworkAccessManager(this)) { mNetworkAccessManager->setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy); mNetworkAccessManager->setStrictTransportSecurityEnabled(true); mNetworkAccessManager->enableStrictTransportSecurityStore(true); } LanguageToolManager::~LanguageToolManager() { } LanguageToolManager *LanguageToolManager::self() { static LanguageToolManager s_self; return &s_self; } QNetworkAccessManager *LanguageToolManager::networkAccessManager() const { return mNetworkAccessManager; } QString LanguageToolManager::languageToolCheckPath() const { return (Settings::self()->languageToolCustom() ? - Settings::self()->languageToolInstancePath() : - QStringLiteral("https://languagetool.org/api/v2") - ) + QStringLiteral("/check"); + Settings::self()->languageToolInstancePath() : + QStringLiteral("https://languagetool.org/api/v2") + ) + QStringLiteral("/check"); } diff --git a/src/languagetool/languagetoolresultjob.cpp b/src/languagetool/languagetoolresultjob.cpp index 8f3f932..0a614a9 100644 --- a/src/languagetool/languagetoolresultjob.cpp +++ b/src/languagetool/languagetoolresultjob.cpp @@ -1,161 +1,161 @@ /* Copyright (C) 2019-2020 Laurent Montel This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "languagetoolresultjob.h" #include "lokalize_debug.h" #include #include LanguageToolResultJob::LanguageToolResultJob(QObject *parent) : QObject(parent) { } LanguageToolResultJob::~LanguageToolResultJob() { } static bool hasNotEmptyText(const QString &text) { for (int i = 0; i < text.length(); ++i) { if (!text.at(i).isSpace()) { return true; } } return false; } bool LanguageToolResultJob::canStart() const { return canStartError() == LanguageToolResultJob::JobError::NotError; } LanguageToolResultJob::JobError LanguageToolResultJob::canStartError() const { if (!mNetworkAccessManager) { return LanguageToolResultJob::JobError::NetworkManagerNotDefined; } if (!hasNotEmptyText(mText)) { return LanguageToolResultJob::JobError::EmptyText; } if (mUrl.isEmpty()) { return LanguageToolResultJob::JobError::UrlNotDefined; } if (mLanguage.isEmpty()) { return LanguageToolResultJob::JobError::LanguageNotDefined; } return LanguageToolResultJob::JobError::NotError; } void LanguageToolResultJob::start() { const LanguageToolResultJob::JobError errorType = canStartError(); switch (errorType) { case LanguageToolResultJob::JobError::EmptyText: return; case LanguageToolResultJob::JobError::UrlNotDefined: case LanguageToolResultJob::JobError::NetworkManagerNotDefined: case LanguageToolResultJob::JobError::LanguageNotDefined: qCWarning(LOKALIZE_LOG) << "Impossible to start language tool"; return; case LanguageToolResultJob::JobError::NotError: break; } QNetworkRequest request(QUrl::fromUserInput(mUrl)); addRequestAttribute(request); const QByteArray ba = "text=" + mText.toUtf8() + "&language=" + mLanguage.toLatin1(); //qCWarning(LOKALIZE_LOG) << "Sending LT query" << ba; QNetworkReply *reply = mNetworkAccessManager->post(request, ba); connect(reply, &QNetworkReply::finished, this, &LanguageToolResultJob::slotCheckGrammarFinished); connect(mNetworkAccessManager, &QNetworkAccessManager::finished, this, &LanguageToolResultJob::slotFinish); } void LanguageToolResultJob::slotFinish(QNetworkReply *reply) { if (reply->error() != QNetworkReply::NoError) { - qCWarning(LOKALIZE_LOG) << " Error reply - "<errorString(); + qCWarning(LOKALIZE_LOG) << " Error reply - " << reply->errorString(); Q_EMIT error(reply->errorString()); } } void LanguageToolResultJob::slotCheckGrammarFinished() { QNetworkReply *reply = qobject_cast(sender()); if (reply) { const QByteArray data = reply->readAll(); Q_EMIT finished(QString::fromUtf8(data)); } reply->deleteLater(); deleteLater(); } void LanguageToolResultJob::addRequestAttribute(QNetworkRequest &request) const { request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/x-www-form-urlencoded")); } QString LanguageToolResultJob::language() const { return mLanguage; } void LanguageToolResultJob::setLanguage(const QString &language) { mLanguage = language; } QString LanguageToolResultJob::url() const { return mUrl; } void LanguageToolResultJob::setUrl(const QString &url) { mUrl = url; } QStringList LanguageToolResultJob::arguments() const { return mArguments; } void LanguageToolResultJob::setArguments(const QStringList &arguments) { mArguments = arguments; } QNetworkAccessManager *LanguageToolResultJob::networkAccessManager() const { return mNetworkAccessManager; } void LanguageToolResultJob::setNetworkAccessManager(QNetworkAccessManager *networkAccessManager) { mNetworkAccessManager = networkAccessManager; } QString LanguageToolResultJob::text() const { return mText; } void LanguageToolResultJob::setText(const QString &text) { mText = text; } diff --git a/src/lokalizemainwindow.cpp b/src/lokalizemainwindow.cpp index 032033a..ca67f6b 100644 --- a/src/lokalizemainwindow.cpp +++ b/src/lokalizemainwindow.cpp @@ -1,1087 +1,1086 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2008-2015 by Nick Shaforostoff 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . **************************************************************************** */ #include "lokalizemainwindow.h" #include "lokalize_debug.h" #include "actionproxy.h" #include "editortab.h" #include "projecttab.h" #include "tmtab.h" #include "jobs.h" #include "filesearchtab.h" #include "prefs_lokalize.h" // #define WEBQUERY_ENABLE #include "project.h" #include "projectmodel.h" #include "projectlocal.h" #include "prefs.h" #include "tools/widgettextcaptureconfig.h" #include "multieditoradaptor.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include LokalizeMainWindow::LokalizeMainWindow() : KXmlGuiWindow() , m_mdiArea(new LokalizeMdiArea) , m_prevSubWindow(0) , m_projectSubWindow(0) , m_translationMemorySubWindow(0) , m_editorActions(new QActionGroup(this)) , m_managerActions(new QActionGroup(this)) , m_spareEditor(new EditorTab(this, false)) , m_multiEditorAdaptor(new MultiEditorAdaptor(m_spareEditor)) , m_projectScriptingPlugin(0) { m_spareEditor->hide(); m_mdiArea->setViewMode(QMdiArea::TabbedView); m_mdiArea->setActivationOrder(QMdiArea::ActivationHistoryOrder); m_mdiArea->setDocumentMode(true); m_mdiArea->setTabsMovable(true); m_mdiArea->setTabsClosable(true); setCentralWidget(m_mdiArea); connect(m_mdiArea, &QMdiArea::subWindowActivated, this, &LokalizeMainWindow::slotSubWindowActivated); setupActions(); //prevent relayout of dockwidgets m_mdiArea->setOption(QMdiArea::DontMaximizeSubWindowOnActivation, true); connect(Project::instance(), QOverload::of(&Project::fileOpenRequested), this, QOverload::of(&LokalizeMainWindow::fileOpen_), Qt::QueuedConnection); connect(Project::instance(), &Project::configChanged, this, &LokalizeMainWindow::projectSettingsChanged); connect(Project::instance(), &Project::closed, this, &LokalizeMainWindow::closeProject); showProjectOverview(); showTranslationMemory(); //fix for #342558 for (int i = ID_STATUS_CURRENT; i <= ID_STATUS_ISFUZZY; i++) { m_statusBarLabels.append(new QLabel()); statusBar()->insertWidget(i, m_statusBarLabels.last(), 2); } setAttribute(Qt::WA_DeleteOnClose, true); if (!qApp->isSessionRestored()) { KConfig config; KConfigGroup stateGroup(&config, "State"); readProperties(stateGroup); } registerDBusAdaptor(); QTimer::singleShot(0, this, &LokalizeMainWindow::initLater); } void LokalizeMainWindow::initLater() { if (!m_prevSubWindow && m_projectSubWindow) slotSubWindowActivated(m_projectSubWindow); if (!Project::instance()->isTmSupported()) { KNotification* notification = new KNotification("NoSqlModulesAvailable", this); notification->setText(i18nc("@info", "No Qt Sql modules were found. Translation memory will not work.")); notification->sendEvent(); } } LokalizeMainWindow::~LokalizeMainWindow() { TM::cancelAllJobs(); KConfig config; KConfigGroup stateGroup(&config, "State"); if (!m_lastEditorState.isEmpty()) { stateGroup.writeEntry("DefaultDockWidgets", m_lastEditorState); } saveProjectState(stateGroup); m_multiEditorAdaptor->deleteLater(); //Disconnect the signals pointing to this MainWindow object QMdiSubWindow* sw; for (int i = 0; i < m_fileToEditor.values().count(); i++) { sw = m_fileToEditor.values().at(i); disconnect(sw, &QMdiSubWindow::destroyed, this, &LokalizeMainWindow::editorClosed); EditorTab* w = static_cast(sw->widget()); disconnect(w, &EditorTab::aboutToBeClosed, this, &LokalizeMainWindow::resetMultiEditorAdaptor); disconnect(w, QOverload::of(&EditorTab::fileOpenRequested), this, QOverload::of(&LokalizeMainWindow::fileOpen)); disconnect(w, QOverload::of(&EditorTab::tmLookupRequested), this, QOverload::of(&LokalizeMainWindow::lookupInTranslationMemory)); } qCWarning(LOKALIZE_LOG) << "MainWindow destroyed"; } void LokalizeMainWindow::slotSubWindowActivated(QMdiSubWindow* w) { //QTime aaa;aaa.start(); if (!w || m_prevSubWindow == w) return; w->setUpdatesEnabled(true); //QTBUG-23289 if (m_prevSubWindow) { m_prevSubWindow->setUpdatesEnabled(false); LokalizeSubwindowBase* prevEditor = static_cast(m_prevSubWindow->widget()); prevEditor->hideDocks(); guiFactory()->removeClient(prevEditor->guiClient()); prevEditor->statusBarItems.unregisterStatusBar(); if (qobject_cast(prevEditor)) { EditorTab* w = static_cast(prevEditor); EditorState state = w->state(); m_lastEditorState = state.dockWidgets.toBase64(); } /* QMenu* projectActions=static_cast(factory()->container("project_actions",this)); QList actionz=projectActions->actions(); int i=actionz.size(); //projectActions->menuAction()->setVisible(i); //qCWarning(LOKALIZE_LOG)<<"adding object"<=0) { disconnect(w, SIGNAL(signalNewEntryDisplayed()),actionz.at(i),SLOT(signalNewEntryDisplayed())); //static_cast(actionz.at(i))->addObject(static_cast( editor )->adaptor(),"Editor",Kross::ChildrenInterface::AutoConnectSignals); //static_cast(actionz.at(i))->trigger(); } } */ } LokalizeSubwindowBase* editor = static_cast(w->widget()); editor->reloadUpdatedXML(); if (qobject_cast(editor)) { EditorTab* w = static_cast(editor); w->setProperFocus(); EditorState state = w->state(); m_lastEditorState = state.dockWidgets.toBase64(); m_multiEditorAdaptor->setEditorTab(w); // connect(m_multiEditorAdaptor,SIGNAL(srcFileOpenRequested(QString,int)),this,SLOT(showTM())); /* QMenu* projectActions=static_cast(factory()->container("project_actions",this)); QList actionz=projectActions->actions(); int i=actionz.size(); //projectActions->menuAction()->setVisible(i); //qCWarning(LOKALIZE_LOG)<<"adding object"<=0) { connect(w, SIGNAL(signalNewEntryDisplayed()),actionz.at(i),SLOT(signalNewEntryDisplayed())); //static_cast(actionz.at(i))->addObject(static_cast( editor )->adaptor(),"Editor",Kross::ChildrenInterface::AutoConnectSignals); //static_cast(actionz.at(i))->trigger(); }*/ QTabBar* tw = m_mdiArea->findChild(); if (tw) tw->setTabToolTip(tw->currentIndex(), w->currentFilePath()); emit editorActivated(); } else if (w == m_projectSubWindow && m_projectSubWindow) { QTabBar* tw = m_mdiArea->findChild(); if (tw) tw->setTabToolTip(tw->currentIndex(), Project::instance()->path()); } editor->showDocks(); editor->statusBarItems.registerStatusBar(statusBar(), m_statusBarLabels); guiFactory()->addClient(editor->guiClient()); m_prevSubWindow = w; //qCWarning(LOKALIZE_LOG)<<"finished"< editors = m_mdiArea->subWindowList(); int i = editors.size(); while (--i >= 0) { //if (editors.at(i)==m_projectSubWindow) if (!qobject_cast(editors.at(i)->widget())) continue; if (!static_cast(editors.at(i)->widget())->queryClose()) return false; } bool ok = Project::instance()->queryCloseForAuxiliaryWindows(); if (ok) { QThreadPool::globalInstance()->clear(); Project::instance()->model()->threadPool()->clear(); } return ok; } EditorTab* LokalizeMainWindow::fileOpen_(QString filePath, const bool setAsActive) { return fileOpen(filePath, 0, setAsActive); } EditorTab* LokalizeMainWindow::fileOpen(QString filePath, int entry, bool setAsActive, const QString& mergeFile, bool silent) { if (filePath.length()) { FileToEditor::const_iterator it = m_fileToEditor.constFind(filePath); if (it != m_fileToEditor.constEnd()) { qCWarning(LOKALIZE_LOG) << "already opened:" << filePath; if (QMdiSubWindow* sw = it.value()) { m_mdiArea->setActiveSubWindow(sw); return static_cast(sw->widget()); } } } QByteArray state = m_lastEditorState; EditorTab* w = new EditorTab(this); QMdiSubWindow* sw = 0; //create QMdiSubWindow BEFORE fileOpen() because it causes some strange QMdiArea behaviour otherwise if (filePath.length()) sw = m_mdiArea->addSubWindow(w); QString suggestedDirPath; QMdiSubWindow* activeSW = m_mdiArea->currentSubWindow(); if (activeSW && qobject_cast(activeSW->widget())) { QString fp = static_cast(activeSW->widget())->currentFilePath(); if (fp.length()) suggestedDirPath = QFileInfo(fp).absolutePath(); } if (!w->fileOpen(filePath, suggestedDirPath, m_fileToEditor, silent)) { if (sw) { m_mdiArea->removeSubWindow(sw); sw->deleteLater(); } w->deleteLater(); return 0; } filePath = w->currentFilePath(); m_openRecentFileAction->addUrl(QUrl::fromLocalFile(filePath));//(w->currentUrl()); if (!sw) sw = m_mdiArea->addSubWindow(w); w->showMaximized(); sw->showMaximized(); if (!state.isEmpty()) { w->restoreState(QByteArray::fromBase64(state)); m_lastEditorState = state; } else { //Dummy restore to "initialize" widgets w->restoreState(w->saveState()); } if (entry/* || offset*/) w->gotoEntry(DocPosition(entry/*, DocPosition::Target, 0, offset*/)); if (setAsActive) { m_toBeActiveSubWindow = sw; QTimer::singleShot(0, this, &LokalizeMainWindow::applyToBeActiveSubWindow); } else { m_mdiArea->setActiveSubWindow(activeSW); sw->setUpdatesEnabled(false); //QTBUG-23289 } if (!mergeFile.isEmpty()) w->mergeOpen(mergeFile); connect(sw, &QMdiSubWindow::destroyed, this, &LokalizeMainWindow::editorClosed); connect(w, &EditorTab::aboutToBeClosed, this, &LokalizeMainWindow::resetMultiEditorAdaptor); connect(w, QOverload::of(&EditorTab::fileOpenRequested), this, QOverload::of(&LokalizeMainWindow::fileOpen)); connect(w, QOverload::of(&EditorTab::tmLookupRequested), this, QOverload::of(&LokalizeMainWindow::lookupInTranslationMemory)); QStringRef fnSlashed = filePath.midRef(filePath.lastIndexOf('/')); FileToEditor::const_iterator i = m_fileToEditor.constBegin(); while (i != m_fileToEditor.constEnd()) { if (i.key().endsWith(fnSlashed)) { static_cast(i.value()->widget())->setFullPathShown(true); w->setFullPathShown(true); } ++i; } m_fileToEditor.insert(filePath, sw); sw->setAttribute(Qt::WA_DeleteOnClose, true); emit editorAdded(); return w; } void LokalizeMainWindow::resetMultiEditorAdaptor() { m_multiEditorAdaptor->setEditorTab(m_spareEditor); //it will be reparented shortly if there are other editors } void LokalizeMainWindow::editorClosed(QObject* obj) { m_fileToEditor.remove(m_fileToEditor.key(static_cast(obj))); } EditorTab* LokalizeMainWindow::fileOpen(const QString& filePath, const QString& source, const QString& ctxt, const bool setAsActive) { EditorTab* w = fileOpen(filePath, 0, setAsActive); if (!w) return 0;//TODO message w->findEntryBySourceContext(source, ctxt); return w; } EditorTab* LokalizeMainWindow::fileOpen(const QString& filePath, DocPosition docPos, int selection, const bool setAsActive) { EditorTab* w = fileOpen(filePath, 0, setAsActive); if (!w) return 0;//TODO message w->gotoEntry(docPos, selection); return w; } QObject* LokalizeMainWindow::projectOverview() { if (!m_projectSubWindow) { ProjectTab* w = new ProjectTab(this); m_projectSubWindow = m_mdiArea->addSubWindow(w); w->showMaximized(); m_projectSubWindow->showMaximized(); connect(w, QOverload::of(&ProjectTab::fileOpenRequested), this, QOverload::of(&LokalizeMainWindow::fileOpen_)); connect(w, QOverload::of(&ProjectTab::projectOpenRequested), this, QOverload::of(&LokalizeMainWindow::openProject)); connect(w, QOverload<>::of(&ProjectTab::projectOpenRequested), this, QOverload<>::of(&LokalizeMainWindow::openProject)); connect(w, QOverload::of(&ProjectTab::searchRequested), this, QOverload::of(&LokalizeMainWindow::addFilesToSearch)); } if (m_mdiArea->currentSubWindow() == m_projectSubWindow) return m_projectSubWindow->widget(); return 0; } void LokalizeMainWindow::showProjectOverview() { projectOverview(); m_mdiArea->setActiveSubWindow(m_projectSubWindow); } TM::TMTab* LokalizeMainWindow::showTM() { if (!Project::instance()->isTmSupported()) { KMessageBox::information(nullptr, i18n("TM facility requires SQLite Qt module."), i18n("No SQLite module available")); return 0; } if (!m_translationMemorySubWindow) { TM::TMTab* w = new TM::TMTab(this); m_translationMemorySubWindow = m_mdiArea->addSubWindow(w); w->showMaximized(); m_translationMemorySubWindow->showMaximized(); connect(w, QOverload::of(&TM::TMTab::fileOpenRequested), this, QOverload::of(&LokalizeMainWindow::fileOpen)); } m_mdiArea->setActiveSubWindow(m_translationMemorySubWindow); return static_cast(m_translationMemorySubWindow->widget()); } FileSearchTab* LokalizeMainWindow::showFileSearch(bool activate) { EditorTab* precedingEditor = qobject_cast(activeEditor()); if (!m_fileSearchSubWindow) { FileSearchTab* w = new FileSearchTab(this); m_fileSearchSubWindow = m_mdiArea->addSubWindow(w); w->showMaximized(); m_fileSearchSubWindow->showMaximized(); connect(w, QOverload::of(&FileSearchTab::fileOpenRequested), this, QOverload::of(&LokalizeMainWindow::fileOpen)); connect(w, QOverload::of(&FileSearchTab::fileOpenRequested), this, QOverload::of(&LokalizeMainWindow::fileOpen_)); } if (activate) { m_mdiArea->setActiveSubWindow(m_fileSearchSubWindow); if (precedingEditor) { if (precedingEditor->selectionInSource().length()) static_cast(m_fileSearchSubWindow->widget())->setSourceQuery(precedingEditor->selectionInSource()); if (precedingEditor->selectionInTarget().length()) static_cast(m_fileSearchSubWindow->widget())->setTargetQuery(precedingEditor->selectionInTarget()); } } return static_cast(m_fileSearchSubWindow->widget()); } void LokalizeMainWindow::fileSearchNext() { FileSearchTab* w = showFileSearch(/*activate=*/false); //TODO fill search params based on current selection w->fileSearchNext(); } void LokalizeMainWindow::addFilesToSearch(const QStringList& files) { FileSearchTab* w = showFileSearch(); w->addFilesToSearch(files); } void LokalizeMainWindow::applyToBeActiveSubWindow() { m_mdiArea->setActiveSubWindow(m_toBeActiveSubWindow); } void LokalizeMainWindow::setupActions() { //all operations that can be done after initial setup //(via QTimer::singleShot) go to initLater() // QElapsedTimer aaa; // aaa.start(); setStandardToolBarMenuEnabled(true); QAction *action; KActionCollection* ac = actionCollection(); KActionCategory* actionCategory; KActionCategory* file = new KActionCategory(i18nc("@title actions category", "File"), ac); //KActionCategory* config=new KActionCategory(i18nc("@title actions category","Settings"), ac); KActionCategory* glossary = new KActionCategory(i18nc("@title actions category", "Glossary"), ac); KActionCategory* tm = new KActionCategory(i18nc("@title actions category", "Translation Memory"), ac); KActionCategory* proj = new KActionCategory(i18nc("@title actions category", "Project"), ac); actionCategory = file; // File //KStandardAction::open(this, SLOT(fileOpen()), ac); file->addAction(KStandardAction::Open, this, SLOT(fileOpen())); m_openRecentFileAction = KStandardAction::openRecent(this, SLOT(fileOpen(QUrl)), ac); file->addAction(KStandardAction::Quit, qApp, SLOT(closeAllWindows())); //Settings SettingsController* sc = SettingsController::instance(); KStandardAction::preferences(sc, &SettingsController::showSettingsDialog, ac); #define ADD_ACTION_SHORTCUT(_name,_text,_shortcut)\ action = actionCategory->addAction(QStringLiteral(_name));\ ac->setDefaultShortcut(action, QKeySequence( _shortcut ));\ action->setText(_text); //Window //documentBack //KStandardAction::close(m_mdiArea, SLOT(closeActiveSubWindow()), ac); actionCategory = file; ADD_ACTION_SHORTCUT("next-tab", i18n("Next tab"), Qt::CTRL + Qt::Key_Tab) connect(action, &QAction::triggered, m_mdiArea, &LokalizeMdiArea::activateNextSubWindow); ADD_ACTION_SHORTCUT("prev-tab", i18n("Previous tab"), Qt::CTRL + Qt::SHIFT + Qt::Key_Tab) connect(action, &QAction::triggered, m_mdiArea, &LokalizeMdiArea::activatePreviousSubWindow); ADD_ACTION_SHORTCUT("prev-active-tab", i18n("Previously active tab"), Qt::CTRL + Qt::Key_BracketLeft) connect(action, &QAction::triggered, m_mdiArea, &QMdiArea::activatePreviousSubWindow); //Tools actionCategory = glossary; Project* project = Project::instance(); ADD_ACTION_SHORTCUT("tools_glossary", i18nc("@action:inmenu", "Glossary"), Qt::CTRL + Qt::ALT + Qt::Key_G) connect(action, &QAction::triggered, project, &Project::showGlossary); actionCategory = tm; ADD_ACTION_SHORTCUT("tools_tm", i18nc("@action:inmenu", "Translation memory"), Qt::Key_F7) connect(action, &QAction::triggered, this, &LokalizeMainWindow::showTM); action = tm->addAction("tools_tm_manage", project, SLOT(showTMManager())); action->setText(i18nc("@action:inmenu", "Manage translation memories")); //Project actionCategory = proj; ADD_ACTION_SHORTCUT("project_overview", i18nc("@action:inmenu", "Project overview"), Qt::Key_F4) connect(action, &QAction::triggered, this, &LokalizeMainWindow::showProjectOverview); action = proj->addAction(QStringLiteral("project_configure"), sc, SLOT(projectConfigure())); action->setText(i18nc("@action:inmenu", "Configure project...")); action = proj->addAction(QStringLiteral("project_create"), sc, SLOT(projectCreate())); action->setText(i18nc("@action:inmenu", "Create software translation project...")); action = proj->addAction(QStringLiteral("project_create_odf"), Project::instance(), SLOT(projectOdfCreate())); action->setText(i18nc("@action:inmenu", "Create OpenDocument translation project...")); action = proj->addAction(QStringLiteral("project_open"), this, SLOT(openProject())); action->setText(i18nc("@action:inmenu", "Open project...")); action->setIcon(QIcon::fromTheme("project-open")); action = proj->addAction(QStringLiteral("project_close"), this, SLOT(closeProject())); action->setText(i18nc("@action:inmenu", "Close project")); action->setIcon(QIcon::fromTheme("project-close")); m_openRecentProjectAction = new KRecentFilesAction(i18nc("@action:inmenu", "Open recent project"), this); action = proj->addAction(QStringLiteral("project_open_recent"), m_openRecentProjectAction); connect(m_openRecentProjectAction, QOverload::of(&KRecentFilesAction::urlSelected), this, QOverload::of(&LokalizeMainWindow::openProject)); //Qt::QueuedConnection: defer until event loop is running to eliminate QWidgetPrivate::showChildren(bool) startup crash connect(Project::instance(), &Project::loaded, this, &LokalizeMainWindow::projectLoaded, Qt::QueuedConnection); ADD_ACTION_SHORTCUT("tools_filesearch", i18nc("@action:inmenu", "Search and replace in files"), Qt::Key_F6) connect(action, &QAction::triggered, this, &LokalizeMainWindow::showFileSearch); ADD_ACTION_SHORTCUT("tools_filesearch_next", i18nc("@action:inmenu", "Find next in files"), Qt::META + Qt::Key_F3) connect(action, &QAction::triggered, this, &LokalizeMainWindow::fileSearchNext); action = ac->addAction(QStringLiteral("tools_widgettextcapture"), this, SLOT(widgetTextCapture())); action->setText(i18nc("@action:inmenu", "Widget text capture")); setupGUI(Default, QStringLiteral("lokalizemainwindowui.rc")); //qCDebug(LOKALIZE_LOG)<<"action setup finished"<subWindowList()) { if (subwindow == m_translationMemorySubWindow && m_translationMemorySubWindow) subwindow->deleteLater(); else if (qobject_cast(subwindow->widget())) { m_fileToEditor.remove(static_cast(subwindow->widget())->currentFilePath());//safety m_mdiArea->removeSubWindow(subwindow); subwindow->deleteLater(); - } - else if (subwindow == m_projectSubWindow && m_projectSubWindow) + } else if (subwindow == m_projectSubWindow && m_projectSubWindow) static_cast(m_projectSubWindow->widget())->showWelcomeScreen(); } Project::instance()->load(QString()); //TODO scripts return true; } void LokalizeMainWindow::openProject(QString path) { path = SettingsController::instance()->projectOpen(path, false); //dry run if (path.isEmpty()) return; if (closeProject()) SettingsController::instance()->projectOpen(path, true);//really open } void LokalizeMainWindow::saveProperties(KConfigGroup& stateGroup) { saveProjectState(stateGroup); } void LokalizeMainWindow::saveProjectState(KConfigGroup& stateGroup) { QList editors = m_mdiArea->subWindowList(); QStringList files; QStringList mergeFiles; QList dockWidgets; //QList offsets; QList entries; QMdiSubWindow* activeSW = m_mdiArea->currentSubWindow(); int activeSWIndex = -1; int i = editors.size(); while (--i >= 0) { //if (editors.at(i)==m_projectSubWindow) if (!editors.at(i) || !qobject_cast(editors.at(i)->widget())) continue; EditorState state = static_cast(editors.at(i)->widget())->state(); if (editors.at(i) == activeSW) { activeSWIndex = files.size(); m_lastEditorState = state.dockWidgets.toBase64(); } files.append(state.filePath); mergeFiles.append(state.mergeFilePath); dockWidgets.append(state.dockWidgets.toBase64()); entries.append(state.entry); //offsets.append(state.offset); //qCWarning(LOKALIZE_LOG)<(editors.at(i)->widget() )->state().url; } //if (activeSWIndex==-1 && activeSW==m_projectSubWindow) if (files.size() == 0 && !m_lastEditorState.isEmpty()) { dockWidgets.append(m_lastEditorState); // save last state if no editor open } if (stateGroup.isValid()) stateGroup.writeEntry("Project", Project::instance()->path()); KConfig config; KConfigGroup projectStateGroup(&config, "State-" + Project::instance()->path()); projectStateGroup.writeEntry("Active", activeSWIndex); projectStateGroup.writeEntry("Files", files); projectStateGroup.writeEntry("MergeFiles", mergeFiles); projectStateGroup.writeEntry("DockWidgets", dockWidgets); //stateGroup.writeEntry("Offsets",offsets); projectStateGroup.writeEntry("Entries", entries); if (m_projectSubWindow) { ProjectTab *w = static_cast(m_projectSubWindow->widget()); if (w->unitsCount() > 0) projectStateGroup.writeEntry("UnitsCount", w->unitsCount()); } QString nameSpecifier = Project::instance()->path(); if (!nameSpecifier.isEmpty()) nameSpecifier.prepend('-'); KConfig* c = stateGroup.isValid() ? stateGroup.config() : &config; m_openRecentFileAction->saveEntries(KConfigGroup(c, "RecentFiles" + nameSpecifier)); m_openRecentProjectAction->saveEntries(KConfigGroup(&config, "RecentProjects")); } void LokalizeMainWindow::readProperties(const KConfigGroup& stateGroup) { KConfig config; m_openRecentProjectAction->loadEntries(KConfigGroup(&config, "RecentProjects")); QString path = stateGroup.readEntry("Project", QString()); if (Project::instance()->isLoaded() || path.isEmpty()) { //1. we weren't existing yet when the signal was emitted //2. defer until event loop is running to eliminate QWidgetPrivate::showChildren(bool) startup crash QTimer::singleShot(0, this, &LokalizeMainWindow::projectLoaded); } else Project::instance()->load(path); } void LokalizeMainWindow::projectLoaded() { QString projectPath = Project::instance()->path(); qCDebug(LOKALIZE_LOG) << "Loaded project : " << projectPath; if (!projectPath.isEmpty()) m_openRecentProjectAction->addUrl(QUrl::fromLocalFile(projectPath)); KConfig config; QString nameSpecifier = projectPath; if (!nameSpecifier.isEmpty()) nameSpecifier.prepend('-'); m_openRecentFileAction->loadEntries(KConfigGroup(&config, "RecentFiles" + nameSpecifier)); //if project isn't loaded, still restore opened files from "State-" KConfigGroup stateGroup(&config, "State"); KConfigGroup projectStateGroup(&config, "State-" + projectPath); QStringList files; QStringList mergeFiles; QList dockWidgets; //QList offsets; QList entries; projectOverview(); if (m_projectSubWindow) { ProjectTab *w = static_cast(m_projectSubWindow->widget()); w->setLegacyUnitsCount(projectStateGroup.readEntry("UnitsCount", 0)); QTabBar* tw = m_mdiArea->findChild(); if (tw) for (int i = 0; i < tw->count(); i++) if (tw->tabText(i) == w->windowTitle()) tw->setTabToolTip(i, Project::instance()->path()); } entries = projectStateGroup.readEntry("Entries", entries); if (Settings::self()->restoreRecentFilesOnStartup()) files = projectStateGroup.readEntry("Files", files); mergeFiles = projectStateGroup.readEntry("MergeFiles", mergeFiles); dockWidgets = projectStateGroup.readEntry("DockWidgets", dockWidgets); int i = files.size(); int activeSWIndex = projectStateGroup.readEntry("Active", -1); QStringList failedFiles; while (--i >= 0) { if (i < dockWidgets.size()) { m_lastEditorState = dockWidgets.at(i); } if (!fileOpen(files.at(i), entries.at(i)/*, offsets.at(i)*//*,&activeSW11*/, activeSWIndex == i, mergeFiles.at(i),/*silent*/true)) failedFiles.append(files.at(i)); } if (!failedFiles.isEmpty()) { qCDebug(LOKALIZE_LOG) << "failedFiles" << failedFiles; // KMessageBox::error(this, i18nc("@info","Error opening the following files:")+ // "
  • "+failedFiles.join("
  • ")+"
  • " ); KNotification* notification = new KNotification("FilesOpenError", this); notification->setText(i18nc("@info", "Error opening the following files:\n\n") + "" + failedFiles.join("
    ") + ""); notification->sendEvent(); } if (files.isEmpty() && dockWidgets.size() > 0) { m_lastEditorState = dockWidgets.last(); // restore last state if no editor open } else { m_lastEditorState = stateGroup.readEntry("DefaultDockWidgets", m_lastEditorState); // restore default state if no last editor for this project } if (activeSWIndex == -1) { m_toBeActiveSubWindow = m_projectSubWindow; QTimer::singleShot(0, this, &LokalizeMainWindow::applyToBeActiveSubWindow); } projectSettingsChanged(); QTimer::singleShot(0, this, &LokalizeMainWindow::loadProjectScripts); } void LokalizeMainWindow::projectSettingsChanged() { //TODO show langs setCaption(Project::instance()->projectID()); } void LokalizeMainWindow::widgetTextCapture() { WidgetTextCaptureConfig* w = new WidgetTextCaptureConfig(this); w->show(); } //BEGIN DBus interface //#include "plugin.h" #include "mainwindowadaptor.h" #include #include using namespace Kross; class MyScriptingPlugin: public Kross::ScriptingPlugin { public: MyScriptingPlugin(QObject* lokalize, QObject* editor) : Kross::ScriptingPlugin(lokalize) { addObject(lokalize, "Lokalize"); addObject(Project::instance(), "Project"); addObject(editor, "Editor"); setXMLFile("scriptsui.rc", true); } ~MyScriptingPlugin() {} }; #define PROJECTRCFILE "scripts.rc" #define PROJECTRCFILEDIR Project::instance()->projectDir()+"/lokalize-scripts" #define PROJECTRCFILEPATH Project::instance()->projectDir()+"/lokalize-scripts" "/" PROJECTRCFILE //TODO be lazy creating scripts dir ProjectScriptingPlugin::ProjectScriptingPlugin(QObject* lokalize, QObject* editor) : Kross::ScriptingPlugin(Project::instance()->kind(), PROJECTRCFILEPATH, Project::instance()->kind(), lokalize) { if (Project::instance()->projectDir().isEmpty()) return; QString filepath = PROJECTRCFILEPATH; // Remove directory "scripts.rc" if it is empty. It could be // mistakenly created by Lokalize 15.04.x. if (QFileInfo(filepath).isDir() && !QDir().rmdir(filepath)) { qCCritical(LOKALIZE_LOG) << "Failed to remove directory" << filepath << "to create scripting configuration file with at the same path. " << "The directory may be not empty."; return; } if (!QFile::exists(filepath)) { QDir().mkdir(QFileInfo(filepath).dir().path()); QFile f(filepath); if (!f.open(QIODevice::WriteOnly)) return; QTextStream out(&f); out << ""; f.close(); } //qCWarning(LOKALIZE_LOG)<collection(Project::instance()->kind()); if (!collection) return; foreach (const QString &collectionname, collection->collections()) { Kross::ActionCollection* c = collection->collection(collectionname); if (!c->isEnabled()) continue; foreach (Kross::Action* action, c->actions()) { if (action->property("autorun").toBool()) action->trigger(); if (action->property("first-run").toBool() && Project::local()->firstRun()) action->trigger(); } } } ProjectScriptingPlugin::~ProjectScriptingPlugin() { Kross::ActionCollection* collection = Kross::Manager::self().actionCollection()->collection(Project::instance()->kind()); if (!collection) return; QString scriptsrc = PROJECTRCFILE; QDir rcdir(PROJECTRCFILEDIR); qCDebug(LOKALIZE_LOG) << rcdir.entryList(QStringList("*.rc"), QDir::Files); foreach (const QString& rc, QDir(PROJECTRCFILEDIR).entryList(QStringList("*.rc"), QDir::Files)) if (rc != scriptsrc) qCWarning(LOKALIZE_LOG) << rc << collection->readXmlFile(rcdir.absoluteFilePath(rc)); } /* void LokalizeMainWindow::checkForProjectAlreadyOpened() { QStringList services=QDBusConnection::sessionBus().interface()->registeredServiceNames().value(); int i=services.size(); while(--i>=0) { if (services.at(i).startsWith("org.kde.lokalize")) //QDBusReply QDBusConnectionInterface::servicePid ( const QString & serviceName ) const; QDBusConnection::callWithCallback(QDBusMessage::createMethodCall(services.at(i),"/ThisIsWhatYouWant","org.kde.Lokalize.MainWindow","currentProject"), this, SLOT(), const char * errorMethod); } } */ void LokalizeMainWindow::registerDBusAdaptor() { new MainWindowAdaptor(this); QDBusConnection::sessionBus().registerObject(QLatin1String("/ThisIsWhatYouWant"), this); //qCWarning(LOKALIZE_LOG)<registeredServiceNames().value(); #ifndef Q_OS_MAC //TODO really fix!!! guiFactory()->addClient(new MyScriptingPlugin(this, m_multiEditorAdaptor)); #endif //QMenu* projectActions=static_cast(factory()->container("project_actions",this)); /* KActionCollection* actionCollection = mWindow->actionCollection(); actionCollection->action("file_save")->setEnabled(canSave); actionCollection->action("file_save_as")->setEnabled(canSave); */ } void LokalizeMainWindow::loadProjectScripts() { if (m_projectScriptingPlugin) { guiFactory()->removeClient(m_projectScriptingPlugin); delete m_projectScriptingPlugin; } //a HACK to get new .rc files shown w/o requiring a restart m_projectScriptingPlugin = new ProjectScriptingPlugin(this, m_multiEditorAdaptor); //guiFactory()->addClient(m_projectScriptingPlugin); //guiFactory()->removeClient(m_projectScriptingPlugin); delete m_projectScriptingPlugin; m_projectScriptingPlugin = new ProjectScriptingPlugin(this, m_multiEditorAdaptor); guiFactory()->addClient(m_projectScriptingPlugin); } int LokalizeMainWindow::lookupInTranslationMemory(DocPosition::Part part, const QString& text) { TM::TMTab* w = showTM(); if (!text.isEmpty()) w->lookup(part == DocPosition::Source ? text : QString(), part == DocPosition::Target ? text : QString()); return w->dbusId(); } int LokalizeMainWindow::lookupInTranslationMemory(const QString& source, const QString& target) { TM::TMTab* w = showTM(); w->lookup(source, target); return w->dbusId(); } int LokalizeMainWindow::showTranslationMemory() { /*activateWindow(); raise(); show();*/ return lookupInTranslationMemory(DocPosition::UndefPart, QString()); } int LokalizeMainWindow::openFileInEditorAt(const QString& path, const QString& source, const QString& ctxt) { EditorTab* w = fileOpen(path, source, ctxt, true); if (!w) return -1; return w->dbusId(); } int LokalizeMainWindow::openFileInEditor(const QString& path) { return openFileInEditorAt(path, QString(), QString()); } QObject* LokalizeMainWindow::activeEditor() { //QList editors=m_mdiArea->subWindowList(); QMdiSubWindow* activeSW = m_mdiArea->currentSubWindow(); if (!activeSW || !qobject_cast(activeSW->widget())) return 0; return activeSW->widget(); } QObject* LokalizeMainWindow::editorForFile(const QString& path) { FileToEditor::const_iterator it = m_fileToEditor.constFind(QFileInfo(path).canonicalFilePath()); if (it == m_fileToEditor.constEnd()) return 0; QMdiSubWindow* w = it.value(); if (!w) return 0; return static_cast(w->widget()); } int LokalizeMainWindow::editorIndexForFile(const QString& path) { EditorTab* editor = static_cast(editorForFile(path)); if (!editor) return -1; return editor->dbusId(); } QString LokalizeMainWindow::currentProject() { return Project::instance()->path(); } #ifdef Q_OS_WIN #include int LokalizeMainWindow::pid() { return GetCurrentProcessId(); } #else #include int LokalizeMainWindow::pid() { return getpid(); } #endif QString LokalizeMainWindow::dbusName() { return QStringLiteral("org.kde.lokalize-%1").arg(pid()); } void LokalizeMainWindow::busyCursor(bool busy) { busy ? QApplication::setOverrideCursor(Qt::WaitCursor) : QApplication::restoreOverrideCursor(); } void LokalizeMdiArea::activateNextSubWindow() { this->setActivationOrder((QMdiArea::WindowOrder)Settings::tabSwitch()); this->QMdiArea::activateNextSubWindow(); this->setActivationOrder(QMdiArea::ActivationHistoryOrder); } void LokalizeMdiArea::activatePreviousSubWindow() { this->setActivationOrder((QMdiArea::WindowOrder)Settings::tabSwitch()); this->QMdiArea::activatePreviousSubWindow(); this->setActivationOrder(QMdiArea::ActivationHistoryOrder); } MultiEditorAdaptor::MultiEditorAdaptor(EditorTab *parent) : EditorAdaptor(parent) { setObjectName(QStringLiteral("MultiEditorAdaptor")); connect(parent, QOverload::of(&EditorTab::destroyed), this, QOverload::of(&MultiEditorAdaptor::handleParentDestroy)); } void MultiEditorAdaptor::setEditorTab(EditorTab* e) { if (parent()) disconnect(parent(), QOverload::of(&EditorTab::destroyed), this, QOverload::of(&MultiEditorAdaptor::handleParentDestroy)); if (e) connect(e, QOverload::of(&EditorTab::destroyed), this, QOverload::of(&MultiEditorAdaptor::handleParentDestroy)); setParent(e); setAutoRelaySignals(false); setAutoRelaySignals(true); } void MultiEditorAdaptor::handleParentDestroy(QObject* p) { Q_UNUSED(p); setParent(0); } //END DBus interface DelayedFileOpener::DelayedFileOpener(const QVector& urls, LokalizeMainWindow* lmw) : QObject() , m_urls(urls) , m_lmw(lmw) { //do the work just after project loading is finished //(i.e. all the files from previous project session are loaded) QTimer::singleShot(1, this, &DelayedFileOpener::doOpen); } void DelayedFileOpener::doOpen() { int lastIndex = m_urls.count() - 1; for (int i = 0; i <= lastIndex; i++) m_lmw->fileOpen(m_urls.at(i), 0, /*set as active*/i == lastIndex); deleteLater(); } #include "moc_lokalizesubwindowbase.cpp" #include "moc_multieditoradaptor.cpp" diff --git a/src/prefs/prefs.cpp b/src/prefs/prefs.cpp index 46a1994..331dee3 100644 --- a/src/prefs/prefs.cpp +++ b/src/prefs/prefs.cpp @@ -1,417 +1,419 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2007-2011 by Nick Shaforostoff 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . **************************************************************************** */ #include "prefs.h" #include "lokalize_debug.h" #include "prefs_lokalize.h" #include "project.h" #include "projectlocal.h" #include "projectmodel.h" #include "languagelistmodel.h" #include "dbfilesmodel.h" #include "ui_prefs_identity.h" #include "ui_prefs_editor.h" #include "ui_prefs_general.h" #include "ui_prefs_appearance.h" #include "ui_prefs_pology.h" #include "ui_prefs_languagetool.h" #include "ui_prefs_tm.h" #include "ui_prefs_projectmain.h" #include "ui_prefs_project_advanced.h" #include "ui_prefs_project_local.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include //#include SettingsController* SettingsController::_instance = 0; void SettingsController::cleanupSettingsController() { delete SettingsController::_instance; SettingsController::_instance = 0; } SettingsController* SettingsController::instance() { if (_instance == 0) { _instance = new SettingsController; qAddPostRoutine(SettingsController::cleanupSettingsController); } return _instance; } SettingsController::SettingsController() : QObject(Project::instance()) , dirty(false) , m_projectActionsView(0) , m_mainWindowPtr(0) {} SettingsController::~SettingsController() {} void SettingsController::showSettingsDialog() { if (KConfigDialog::showDialog("lokalize_settings")) return; KConfigDialog *dialog = new KConfigDialog(m_mainWindowPtr, "lokalize_settings", Settings::self()); dialog->setFaceType(KPageDialog::List); // Identity QWidget *w = new QWidget(dialog); Ui_prefs_identity ui_prefs_identity; ui_prefs_identity.setupUi(w); KConfigGroup grp = Settings::self()->config()->group("Identity"); ui_prefs_identity.DefaultLangCode->setModel(LanguageListModel::instance()->sortModel()); ui_prefs_identity.DefaultLangCode->setCurrentIndex(LanguageListModel::instance()->sortModelRowForLangCode(grp.readEntry("DefaultLangCode", QLocale::system().name()))); connect(ui_prefs_identity.DefaultLangCode, QOverload::of(&KComboBox::activated), ui_prefs_identity.kcfg_DefaultLangCode, &LangCodeSaver::setLangCode); ui_prefs_identity.kcfg_DefaultLangCode->hide(); connect(ui_prefs_identity.kcfg_overrideLangTeam, &QCheckBox::toggled, ui_prefs_identity.kcfg_userLangTeam, &QLineEdit::setEnabled); connect(ui_prefs_identity.kcfg_overrideLangTeam, &QCheckBox::toggled, ui_prefs_identity.kcfg_userLangTeam, &QLineEdit::focusWidget); dialog->addPage(w, i18nc("@title:tab", "Identity"), "preferences-desktop-user"); //General w = new QWidget(dialog); Ui_prefs_general ui_prefs_general; ui_prefs_general.setupUi(w); connect(ui_prefs_general.kcfg_CustomEditorEnabled, &QCheckBox::toggled, ui_prefs_general.kcfg_CustomEditorCommand, &QLineEdit::setEnabled); - ui_prefs_general.kcfg_CustomEditorCommand->setEnabled(Settings::self()->customEditorEnabled()); + ui_prefs_general.kcfg_CustomEditorCommand->setEnabled(Settings::self()->customEditorEnabled()); //Set here to avoid I18N_ARGUMENT_MISSING if set in ui file ui_prefs_general.kcfg_CustomEditorCommand->setToolTip(i18n( - "The following parameters are available\n%1 - Path of the source file\n%2 - Line number" - ,QStringLiteral("%1"),QStringLiteral("%2"))); + "The following parameters are available\n%1 - Path of the source file\n%2 - Line number" + , QStringLiteral("%1"), QStringLiteral("%2"))); dialog->addPage(w, i18nc("@title:tab", "General"), "preferences-system-windows"); //Editor w = new QWidget(dialog); Ui_prefs_editor ui_prefs_editor; ui_prefs_editor.setupUi(w); dialog->addPage(w, i18nc("@title:tab", "Editing"), "accessories-text-editor"); //Font w = new QWidget(dialog); Ui_prefs_appearance ui_prefs_appearance; ui_prefs_appearance.setupUi(w); dialog->addPage(w, i18nc("@title:tab", "Appearance"), "preferences-desktop-font"); //TM w = new QWidget(dialog); Ui_prefs_tm ui_prefs_tm; ui_prefs_tm.setupUi(w); dialog->addPage(w, i18nc("@title:tab", "Translation Memory"), "configure"); //Pology w = new QWidget(dialog); Ui_prefs_pology ui_prefs_pology; ui_prefs_pology.setupUi(w); dialog->addPage(w, i18nc("@title:tab", "Pology"), "preferences-desktop-filetype-association"); //LanguageTool w = new QWidget(dialog); Ui_prefs_languagetool ui_prefs_languagetool; ui_prefs_languagetool.setupUi(w); dialog->addPage(w, i18nc("@title:tab", "LanguageTool"), "lokalize"); connect(dialog, &KConfigDialog::settingsChanged, this, &SettingsController::generalSettingsChanged); //Spellcheck #if 0 w = new Sonnet::ConfigWidget(Settings::self()->config(), dialog); w->setParent(this); dialog->addPage(w, i18nc("@title:tab", "Spellcheck"), "spellcheck_setting"); connect(dialog, SIGNAL(okClicked()), w, SLOT(save())); connect(dialog, SIGNAL(applyClicked()), w, SLOT(save())); connect(dialog, SIGNAL(defaultClicked()), w, SLOT(slotDefault())); #endif //connect(dialog,SIGNAL(settingsChanged(const QString&)),m_view, SLOT(settingsChanged())); dialog->show(); // dialog->addPage(new General(0, "General"), i18n("General") ); // dialog->addPage(new Appearance(0, "Style"), i18n("Appearance") ); // connect(dialog, SIGNAL(settingsChanged(const QString&)), mainWidget, SLOT(loadSettings())); // connect(dialog, SIGNAL(settingsChanged(const QString&)), this, SLOT(loadSettings())); } ScriptsView::ScriptsView(QWidget* parent): Kross::ActionCollectionView(parent) { setAcceptDrops(true); } void ScriptsView::dragEnterEvent(QDragEnterEvent* event) { if (!event->mimeData()->urls().isEmpty() && event->mimeData()->urls().first().path().endsWith(QLatin1String(".rc"))) event->accept(); } void ScriptsView::dropEvent(QDropEvent* event) { Kross::ActionCollectionModel* scriptsModel = static_cast(model()); foreach (const QUrl& url, event->mimeData()->urls()) if (url.path().endsWith(QLatin1String(".rc"))) scriptsModel->rootCollection()->readXmlFile(url.path()); } bool SettingsController::ensureProjectIsLoaded() { if (Project::instance()->isLoaded()) return true; int answer = KMessageBox::questionYesNoCancel(m_mainWindowPtr, i18n("You have accessed a feature that requires a project to be loaded. Do you want to create a new project or open an existing project?"), QString(), KGuiItem(i18nc("@action", "New"), QIcon::fromTheme("document-new")), KGuiItem(i18nc("@action", "Open"), QIcon::fromTheme("project-open")) ); if (answer == KMessageBox::Yes) return projectCreate(); if (answer == KMessageBox::No) return !projectOpen().isEmpty(); return false; } QString SettingsController::projectOpen(QString path, bool doOpen) { if (path.isEmpty()) { //Project::instance()->model()->weaver()->suspend(); //KDE5PORT mutex if needed path = QFileDialog::getOpenFileName(m_mainWindowPtr, QString(), QDir::homePath()/*_catalog->url().directory()*/, i18n("Lokalize translation project (*.lokalize)")/*"text/x-lokalize-project"*/); //Project::instance()->model()->weaver()->resume(); } if (!path.isEmpty() && doOpen) Project::instance()->load(path); return path; } bool SettingsController::projectCreate() { //Project::instance()->model()->weaver()->suspend(); //KDE5PORT mutex if needed QString desirablePath = Project::instance()->desirablePath(); if (desirablePath.isEmpty()) desirablePath = QDir::homePath() + "/index.lokalize"; QString path = QFileDialog::getSaveFileName(m_mainWindowPtr, i18nc("@window:title", "Select folder with Gettext .po files to translate"), desirablePath, i18n("Lokalize translation project (*.lokalize)") /*"text/x-lokalize-project"*/); //Project::instance()->model()->weaver()->resume(); if (path.isEmpty()) return false; if (m_projectActionsView && m_projectActionsView->model()) { //ActionCollectionModel is known to be have bad for the usecase of reinitializing krossplugin m_projectActionsView->model()->deleteLater(); m_projectActionsView->setModel(nullptr); } //TODO ask-n-save QDir projectFolder = QFileInfo(path).absoluteDir(); QString projectId = projectFolder.dirName(); if (projectFolder.cdUp()) projectId = projectFolder.dirName() + '-' + projectId;; Project::instance()->load(path, QString(), projectId); //Project::instance()->setDefaults(); //NOTE will this be an obstacle? //Project::instance()->setProjectID(); QTimer::singleShot(500, this, &SettingsController::projectConfigure); return true; } void SettingsController::projectConfigure() { if (Project::instance()->path().isEmpty()) { KMessageBox::error(mainWindowPtr(), i18n("Create software or OpenDocument translation project first.")); return; } if (KConfigDialog::showDialog("project_settings")) { if (!m_projectActionsView->model()) m_projectActionsView->setModel(new Kross::ActionCollectionModel(m_projectActionsView, Kross::Manager::self().actionCollection()->collection(Project::instance()->kind()))); return; } KConfigDialog *dialog = new KConfigDialog(m_mainWindowPtr, "project_settings", Project::instance()); dialog->setFaceType(KPageDialog::List); // Main QWidget *w = new QWidget(dialog); Ui_prefs_projectmain ui_prefs_projectmain; ui_prefs_projectmain.setupUi(w); dialog->addPage(w, i18nc("@title:tab", "General"), "preferences-desktop-locale"); ui_prefs_projectmain.kcfg_LangCode->hide(); ui_prefs_projectmain.kcfg_PoBaseDir->hide(); ui_prefs_projectmain.kcfg_GlossaryTbx->hide(); Project& p = *(Project::instance()); ui_prefs_projectmain.LangCode->setModel(LanguageListModel::instance()->sortModel()); ui_prefs_projectmain.LangCode->setCurrentIndex(LanguageListModel::instance()->sortModelRowForLangCode(p.langCode())); connect(ui_prefs_projectmain.LangCode, QOverload::of(&KComboBox::activated), ui_prefs_projectmain.kcfg_LangCode, &LangCodeSaver::setLangCode); ui_prefs_projectmain.poBaseDir->setMode(KFile::Directory | KFile::ExistingOnly | KFile::LocalOnly); ui_prefs_projectmain.glossaryTbx->setMode(KFile::File | KFile::ExistingOnly | KFile::LocalOnly); ui_prefs_projectmain.glossaryTbx->setFilter("*.tbx\n*.xml"); connect(ui_prefs_projectmain.poBaseDir, &KUrlRequester::textChanged, ui_prefs_projectmain.kcfg_PoBaseDir, &RelPathSaver::setText); connect(ui_prefs_projectmain.glossaryTbx, &KUrlRequester::textChanged, ui_prefs_projectmain.kcfg_GlossaryTbx, &RelPathSaver::setText); ui_prefs_projectmain.poBaseDir->setUrl(QUrl::fromLocalFile(p.poDir())); ui_prefs_projectmain.glossaryTbx->setUrl(QUrl::fromLocalFile(p.glossaryPath())); auto kcfg_ProjLangTeam = ui_prefs_projectmain.kcfg_ProjLangTeam; connect(ui_prefs_projectmain.kcfg_LanguageSource, static_cast(&KComboBox::currentIndexChanged), - this, [kcfg_ProjLangTeam](int index) { kcfg_ProjLangTeam->setEnabled(static_cast(index) == Project::LangSource::Project); }); + this, [kcfg_ProjLangTeam](int index) { + kcfg_ProjLangTeam->setEnabled(static_cast(index) == Project::LangSource::Project); + }); connect(ui_prefs_projectmain.kcfg_LanguageSource, static_cast(&KComboBox::currentIndexChanged), - this, [kcfg_ProjLangTeam] { kcfg_ProjLangTeam->setFocus(); }); + this, [kcfg_ProjLangTeam] { kcfg_ProjLangTeam->setFocus(); }); // RegExps w = new QWidget(dialog); Ui_project_advanced ui_project_advanced; ui_project_advanced.setupUi(w); ui_project_advanced.kcfg_PotBaseDir->hide(); ui_project_advanced.kcfg_BranchDir->hide(); ui_project_advanced.kcfg_AltDir->hide(); ui_project_advanced.potBaseDir->setMode(KFile::Directory | KFile::ExistingOnly | KFile::LocalOnly); ui_project_advanced.branchDir->setMode(KFile::Directory | KFile::ExistingOnly | KFile::LocalOnly); ui_project_advanced.altDir->setMode(KFile::Directory | KFile::ExistingOnly | KFile::LocalOnly); connect(ui_project_advanced.potBaseDir, &KUrlRequester::textChanged, ui_project_advanced.kcfg_PotBaseDir, &RelPathSaver::setText); connect(ui_project_advanced.branchDir, &KUrlRequester::textChanged, ui_project_advanced.kcfg_BranchDir, &RelPathSaver::setText); connect(ui_project_advanced.altDir, &KUrlRequester::textChanged, ui_project_advanced.kcfg_AltDir, &RelPathSaver::setText); ui_project_advanced.potBaseDir->setUrl(QUrl::fromLocalFile(p.potDir())); ui_project_advanced.branchDir->setUrl(QUrl::fromLocalFile(p.branchDir())); ui_project_advanced.altDir->setUrl(QUrl::fromLocalFile(p.altTransDir())); dialog->addPage(w, i18nc("@title:tab", "Advanced"), "applications-development-translation"); //Scripts w = new QWidget(dialog); QVBoxLayout* layout = new QVBoxLayout(w); layout->setSpacing(6); layout->setContentsMargins(11, 11, 11, 11); //m_projectActionsEditor=new Kross::ActionCollectionEditor(Kross::Manager::self().actionCollection()->collection(Project::instance()->projectID()),w); m_projectActionsView = new ScriptsView(w); layout->addWidget(m_projectActionsView); m_projectActionsView->setModel(new Kross::ActionCollectionModel(w, Kross::Manager::self().actionCollection()->collection(Project::instance()->kind()))); QHBoxLayout* btns = new QHBoxLayout(); layout->addLayout(btns); btns->addWidget(m_projectActionsView->createButton(w, "edit")); dialog->addPage(w, i18nc("@title:tab", "Scripts"), "preferences-system-windows-actions"); w = new QWidget(dialog); Ui_prefs_project_local ui_prefs_project_local; ui_prefs_project_local.setupUi(w); dialog->addPage(w, Project::local(), i18nc("@title:tab", "Personal"), "preferences-desktop-user"); connect(dialog, &KConfigDialog::settingsChanged, Project::instance(), &Project::reinit); connect(dialog, &KConfigDialog::settingsChanged, Project::instance(), &Project::save, Qt::QueuedConnection); connect(dialog, &KConfigDialog::settingsChanged, TM::DBFilesModel::instance(), &TM::DBFilesModel::updateProjectTmIndex); connect(dialog, &KConfigDialog::settingsChanged, this, &SettingsController::reflectProjectConfigChange); dialog->show(); } void SettingsController::reflectProjectConfigChange() { //TODO check target language change: reflect changes in TM and glossary TM::DBFilesModel::instance()->openDB(Project::instance()->projectID()); } void SettingsController::reflectRelativePathsHack() { //m_scriptsRelPrefWidget->clear(); QStringList actionz(m_scriptsPrefWidget->items()); QString projectDir(Project::instance()->projectDir()); int i = actionz.size(); while (--i >= 0) actionz[i] = QDir(projectDir).relativeFilePath(actionz.at(i)); m_scriptsRelPrefWidget->setItems(actionz); } void LangCodeSaver::setLangCode(int index) { setText(LanguageListModel::instance()->langCodeForSortModelRow(index)); } void RelPathSaver::setText(const QString& txt) { QLineEdit::setText(QDir(Project::instance()->projectDir()).relativeFilePath(txt)); } void writeUiState(const char* elementName, const QByteArray& state) { KConfig config; KConfigGroup cg(&config, "MainWindow"); cg.writeEntry(elementName, state.toBase64()); } QByteArray readUiState(const char* elementName) { KConfig config; KConfigGroup cg(&config, "MainWindow"); return QByteArray::fromBase64(cg.readEntry(elementName, QByteArray())); } diff --git a/src/project/projectmodel.cpp b/src/project/projectmodel.cpp index 6a03721..7773a4d 100644 --- a/src/project/projectmodel.cpp +++ b/src/project/projectmodel.cpp @@ -1,1258 +1,1258 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2018 by Karl Ove Hufthammer Copyright (C) 2007-2015 by Nick Shaforostoff Copyright (C) 2009 by Viesturs Zarins Copyright (C) 2018-2019 by Simon Depiets Copyright (C) 2019 by Alexander Potashev This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . **************************************************************************** */ #include "projectmodel.h" #include #include #include #include #include #include #include #include #include #include "lokalize_debug.h" #include "project.h" #include "updatestatsjob.h" static int nodeCounter = 0; ProjectModel::ProjectModel(QObject *parent) : QAbstractItemModel(parent) , m_poModel(this) , m_potModel(this) , m_rootNode(NULL, -1, -1, -1) , m_dirIcon(QIcon::fromTheme(QStringLiteral("inode-directory"))) , m_poIcon(QIcon::fromTheme(QStringLiteral("flag-blue"))) , m_poInvalidIcon(QIcon::fromTheme(QStringLiteral("flag-red"))) , m_poComplIcon(QIcon::fromTheme(QStringLiteral("flag-green"))) , m_poEmptyIcon(QIcon::fromTheme(QStringLiteral("flag-yellow"))) , m_potIcon(QIcon::fromTheme(QStringLiteral("flag-black"))) , m_activeJob(NULL) , m_activeNode(NULL) , m_doneTimer(new QTimer(this)) , m_delayedReloadTimer(new QTimer(this)) , m_threadPool(new QThreadPool(this)) , m_completeScan(true) { m_threadPool->setMaxThreadCount(1); m_threadPool->setExpiryTimeout(-1); m_poModel.dirLister()->setAutoErrorHandlingEnabled(false, NULL); m_poModel.dirLister()->setNameFilter(QStringLiteral("*.po *.pot *.xlf *.xliff *.ts")); m_potModel.dirLister()->setAutoErrorHandlingEnabled(false, NULL); m_potModel.dirLister()->setNameFilter(QStringLiteral("*.pot")); connect(&m_poModel, &KDirModel::dataChanged, this, &ProjectModel::po_dataChanged); connect(&m_poModel, &KDirModel::rowsInserted, this, &ProjectModel::po_rowsInserted); connect(&m_poModel, &KDirModel::rowsRemoved, this, &ProjectModel::po_rowsRemoved); connect(&m_potModel, &KDirModel::dataChanged, this, &ProjectModel::pot_dataChanged); connect(&m_potModel, &KDirModel::rowsInserted, this, &ProjectModel::pot_rowsInserted); connect(&m_potModel, &KDirModel::rowsRemoved, this, &ProjectModel::pot_rowsRemoved); m_delayedReloadTimer->setSingleShot(true); m_doneTimer->setSingleShot(true); connect(m_doneTimer, &QTimer::timeout, this, &ProjectModel::updateTotalsChanged); connect(m_delayedReloadTimer, &QTimer::timeout, this, &ProjectModel::reload); setUrl(QUrl(), QUrl()); } ProjectModel::~ProjectModel() { m_dirsWaitingForMetadata.clear(); if (m_activeJob != NULL) m_activeJob->setStatus(-2); m_activeJob = NULL; for (int pos = 0; pos < m_rootNode.rows.count(); pos ++) deleteSubtree(m_rootNode.rows.at(pos)); } void ProjectModel::setUrl(const QUrl &poUrl, const QUrl &potUrl) { //qCDebug(LOKALIZE_LOG) << "ProjectModel::openUrl("<< poUrl.pathOrUrl() << +", " << potUrl.pathOrUrl() << ")"; emit loadingAboutToStart(); //cleanup old data m_dirsWaitingForMetadata.clear(); if (m_activeJob != NULL) m_activeJob->setStatus(-1); m_activeJob = NULL; if (m_rootNode.rows.count()) { beginRemoveRows(QModelIndex(), 0, m_rootNode.rows.count()); for (int pos = 0; pos < m_rootNode.rows.count(); pos ++) deleteSubtree(m_rootNode.rows.at(pos)); m_rootNode.rows.clear(); m_rootNode.poCount = 0; m_rootNode.resetMetaData(); endRemoveRows(); } //add trailing slashes to base URLs, needed for potToPo and poToPot m_poUrl = poUrl.adjusted(QUrl::StripTrailingSlash); m_potUrl = potUrl.adjusted(QUrl::StripTrailingSlash); if (!poUrl.isEmpty()) m_poModel.dirLister()->openUrl(m_poUrl, KDirLister::Reload); if (!potUrl.isEmpty()) m_potModel.dirLister()->openUrl(m_potUrl, KDirLister::Reload); } QUrl ProjectModel::beginEditing(const QModelIndex& index) { Q_ASSERT(index.isValid()); QModelIndex poIndex = poIndexForOuter(index); QModelIndex potIndex = potIndexForOuter(index); if (poIndex.isValid()) { KFileItem item = m_poModel.itemForIndex(poIndex); return item.url(); } else if (potIndex.isValid()) { //copy over the file QUrl potFile = m_potModel.itemForIndex(potIndex).url(); QUrl poFile = potToPo(potFile); //EditorTab::fileOpen takes care of this //be careful, copy only if file does not exist already. // if (!KIO::NetAccess::exists(poFile, KIO::NetAccess::DestinationSide, NULL)) // KIO::NetAccess::file_copy(potFile, poFile); return poFile; } else { Q_ASSERT(false); return QUrl(); } } void ProjectModel::reload() { setUrl(m_poUrl, m_potUrl); } //Theese methds update the combined model from POT and PO model changes. //Quite complex stuff here, better do not change anything. //TODO A comment from Viesturs Zarins 2009-05-17 20:53:11 UTC: //This is a design issue in projectview.cpp. The same issue happens when creating/deleting any folder in project. //When a node PO item is added, the existing POT node is deleted and new one created to represent both. //When view asks if there is more data in the new node, the POT model answers no, as all the data was already stored in POT node witch is now deleted. //To fix this either reuse the existing POT node or manually repopulate data form POT model. void ProjectModel::po_dataChanged(const QModelIndex& po_topLeft, const QModelIndex& po_bottomRight) { //nothing special here //map from source and propagate QModelIndex topLeft = indexForPoIndex(po_topLeft); QModelIndex bottomRight = indexForPoIndex(po_bottomRight); if (topLeft.row() == bottomRight.row() && itemForIndex(topLeft).isFile()) { //this code works fine only for lonely files //and fails for more complex changes //see bug 342959 emit dataChanged(topLeft, bottomRight); enqueueNodeForMetadataUpdate(nodeForIndex(topLeft.parent())); } else if (topLeft.row() == bottomRight.row() && itemForIndex(topLeft).isDir()) { //Something happened inside this folder, nothing to do on the folder itself } else if (topLeft.row() != bottomRight.row() && itemForIndex(topLeft).isDir() && itemForIndex(bottomRight).isDir()) { //Something happened between two folders, no need to reload them } else { qCWarning(LOKALIZE_LOG) << "Delayed reload triggered in po_dataChanged"; m_delayedReloadTimer->start(1000); } } void ProjectModel::pot_dataChanged(const QModelIndex& pot_topLeft, const QModelIndex& pot_bottomRight) { #if 0 //tricky here - some of the pot items may be represented by po items //let's propagate that all subitems changed QModelIndex pot_parent = pot_topLeft.parent(); QModelIndex parent = indexForPotIndex(pot_parent); ProjectNode* node = nodeForIndex(parent); int count = node->rows.count(); QModelIndex topLeft = index(0, pot_topLeft.column(), parent); QModelIndex bottomRight = index(count - 1, pot_bottomRight.column(), parent); emit dataChanged(topLeft, bottomRight); enqueueNodeForMetadataUpdate(nodeForIndex(topLeft.parent())); #else Q_UNUSED(pot_topLeft) Q_UNUSED(pot_bottomRight) qCWarning(LOKALIZE_LOG) << "Delayed reload triggered in pot_dataChanged"; m_delayedReloadTimer->start(1000); #endif } void ProjectModel::po_rowsInserted(const QModelIndex& po_parent, int first, int last) { QModelIndex parent = indexForPoIndex(po_parent); QModelIndex pot_parent = potIndexForOuter(parent); ProjectNode* node = nodeForIndex(parent); //insert po rows beginInsertRows(parent, first, last); for (int pos = first; pos <= last; pos ++) { ProjectNode * childNode = new ProjectNode(node, pos, pos, -1); node->rows.insert(pos, childNode); } node->poCount += last - first + 1; //update rowNumber for (int pos = last + 1; pos < node->rows.count(); pos++) node->rows[pos]->rowNumber = pos; endInsertRows(); //remove unneeded pot rows, update PO rows if (pot_parent.isValid() || !parent.isValid()) { QVector pot2PoMapping; generatePOTMapping(pot2PoMapping, po_parent, pot_parent); for (int pos = node->poCount; pos < node->rows.count(); pos ++) { ProjectNode* potNode = node->rows.at(pos); int potIndex = potNode->potRowNumber; int poIndex = pot2PoMapping[potIndex]; if (poIndex != -1) { //found pot node, that now has a PO index. //remove the pot node and change the corresponding PO node beginRemoveRows(parent, pos, pos); node->rows.remove(pos); deleteSubtree(potNode); endRemoveRows(); node->rows[poIndex]->potRowNumber = potIndex; //This change does not need notification //dataChanged(index(poIndex, 0, parent), index(poIndex, ProjectModelColumnCount, parent)); pos--; } } } enqueueNodeForMetadataUpdate(node); } void ProjectModel::pot_rowsInserted(const QModelIndex& pot_parent, int start, int end) { QModelIndex parent = indexForPotIndex(pot_parent); QModelIndex po_parent = poIndexForOuter(parent); ProjectNode* node = nodeForIndex(parent); int insertedCount = end + 1 - start; QVector newPotNodes; if (po_parent.isValid() || !parent.isValid()) { //this node containts mixed items - add and merge the stuff QVector pot2PoMapping; generatePOTMapping(pot2PoMapping, po_parent, pot_parent); //reassign affected PO row POT indices for (int pos = 0; pos < node->poCount; pos ++) { ProjectNode* n = node->rows[pos]; if (n->potRowNumber >= start) n->potRowNumber += insertedCount; } //assign new POT indices for (int potIndex = start; potIndex <= end; potIndex ++) { int poIndex = pot2PoMapping[potIndex]; if (poIndex != -1) { //found pot node, that has a PO index. //change the corresponding PO node node->rows[poIndex]->potRowNumber = potIndex; //This change does not need notification //dataChanged(index(poIndex, 0, parent), index(poIndex, ProjectModelColumnCount, parent)); } else newPotNodes.append(potIndex); } } else { for (int pos = start; pos < end; pos ++) newPotNodes.append(pos); } //insert standalone POT rows, preserving POT order int newNodesCount = newPotNodes.count(); if (newNodesCount) { int insertionPoint = node->poCount; while ((insertionPoint < node->rows.count()) && (node->rows[insertionPoint]->potRowNumber < start)) insertionPoint++; beginInsertRows(parent, insertionPoint, insertionPoint + newNodesCount - 1); for (int pos = 0; pos < newNodesCount; pos ++) { int potIndex = newPotNodes.at(pos); ProjectNode * childNode = new ProjectNode(node, insertionPoint, -1, potIndex); node->rows.insert(insertionPoint, childNode); insertionPoint++; } //renumber remaining POT rows for (int pos = insertionPoint; pos < node->rows.count(); pos ++) { node->rows[pos]->rowNumber = pos; node->rows[pos]->potRowNumber += insertedCount; } endInsertRows(); } enqueueNodeForMetadataUpdate(node); //FIXME if templates folder doesn't contain an equivalent of po folder then it's stats will be broken: // one way to fix this is to explicitly force scan of the files of the child folders of the 'node' } void ProjectModel::po_rowsRemoved(const QModelIndex& po_parent, int start, int end) { QModelIndex parent = indexForPoIndex(po_parent); //QModelIndex pot_parent = potIndexForOuter(parent); ProjectNode* node = nodeForIndex(parent); int removedCount = end + 1 - start; if ((!parent.isValid()) && (node->rows.count() == 0)) { qCDebug(LOKALIZE_LOG) << "po_rowsRemoved fail"; //events after removing entire contents return; } //remove PO rows QList potRowsToInsert; beginRemoveRows(parent, start, end); //renumber all rows after removed. for (int pos = end + 1; pos < node->rows.count(); pos ++) { ProjectNode* childNode = node->rows.at(pos); childNode->rowNumber -= removedCount; if (childNode->poRowNumber > end) node->rows[pos]->poRowNumber -= removedCount; } //remove for (int pos = end; pos >= start; pos --) { int potIndex = node->rows.at(pos)->potRowNumber; deleteSubtree(node->rows.at(pos)); node->rows.remove(pos); if (potIndex != -1) potRowsToInsert.append(potIndex); } node->poCount -= removedCount; endRemoveRows(); //< fires removed event - the list has to be consistent now //add back rows that have POT files and fix row order std::sort(potRowsToInsert.begin(), potRowsToInsert.end()); int insertionPoint = node->poCount; for (int pos = 0; pos < potRowsToInsert.count(); pos ++) { int potIndex = potRowsToInsert.at(pos); while (insertionPoint < node->rows.count() && node->rows[insertionPoint]->potRowNumber < potIndex) { node->rows[insertionPoint]->rowNumber = insertionPoint; insertionPoint ++; } beginInsertRows(parent, insertionPoint, insertionPoint); ProjectNode * childNode = new ProjectNode(node, insertionPoint, -1, potIndex); node->rows.insert(insertionPoint, childNode); insertionPoint++; endInsertRows(); } //renumber remaining rows while (insertionPoint < node->rows.count()) { node->rows[insertionPoint]->rowNumber = insertionPoint; insertionPoint++; } enqueueNodeForMetadataUpdate(node); } void ProjectModel::pot_rowsRemoved(const QModelIndex& pot_parent, int start, int end) { QModelIndex parent = indexForPotIndex(pot_parent); QModelIndex po_parent = poIndexForOuter(parent); ProjectNode * node = nodeForIndex(parent); int removedCount = end + 1 - start; if ((!parent.isValid()) && (node->rows.count() == 0)) { //events after removing entire contents return; } //First remove POT nodes int firstPOTToRemove = node->poCount; int lastPOTToRemove = node->rows.count() - 1; while (firstPOTToRemove <= lastPOTToRemove && node->rows[firstPOTToRemove]->potRowNumber < start) firstPOTToRemove ++; while (lastPOTToRemove >= firstPOTToRemove && node->rows[lastPOTToRemove]->potRowNumber > end) lastPOTToRemove --; if (firstPOTToRemove <= lastPOTToRemove) { beginRemoveRows(parent, firstPOTToRemove, lastPOTToRemove); for (int pos = lastPOTToRemove; pos >= firstPOTToRemove; pos --) { ProjectNode* childNode = node->rows.at(pos); Q_ASSERT(childNode->potRowNumber >= start); Q_ASSERT(childNode->potRowNumber <= end); deleteSubtree(childNode); node->rows.remove(pos); } //renumber remaining rows for (int pos = firstPOTToRemove; pos < node->rows.count(); pos ++) { node->rows[pos]->rowNumber = pos; node->rows[pos]->potRowNumber -= removedCount; } endRemoveRows(); } //now remove POT indices form PO rows if (po_parent.isValid() || !parent.isValid()) { for (int poIndex = 0; poIndex < node->poCount; poIndex ++) { ProjectNode * childNode = node->rows[poIndex]; int potIndex = childNode->potRowNumber; if (potIndex >= start && potIndex <= end) { //found PO node, that has a POT index in range. //change the corresponding PO node node->rows[poIndex]->potRowNumber = -1; //this change does not affect the model //dataChanged(index(poIndex, 0, parent), index(poIndex, ProjectModelColumnCount, parent)); } else if (childNode->potRowNumber > end) { //reassign POT indices childNode->potRowNumber -= removedCount; } } } enqueueNodeForMetadataUpdate(node); } int ProjectModel::columnCount(const QModelIndex& /*parent*/)const { return ProjectModelColumnCount; } QVariant ProjectModel::headerData(int section, Qt::Orientation, int role) const { const auto column = static_cast(section); switch (role) { case Qt::TextAlignmentRole: { switch (column) { - // Align numeric columns to the right and other columns to the left - // Qt::AlignAbsolute is needed for RTL languages, ref. https://phabricator.kde.org/D13098 - case ProjectModelColumns::TotalCount: - case ProjectModelColumns::TranslatedCount: - case ProjectModelColumns::FuzzyCount: - case ProjectModelColumns::UntranslatedCount: - case ProjectModelColumns::IncompleteCount: - return QVariant(Qt::AlignRight | Qt::AlignAbsolute); - default: - return QVariant(Qt::AlignLeft); + // Align numeric columns to the right and other columns to the left + // Qt::AlignAbsolute is needed for RTL languages, ref. https://phabricator.kde.org/D13098 + case ProjectModelColumns::TotalCount: + case ProjectModelColumns::TranslatedCount: + case ProjectModelColumns::FuzzyCount: + case ProjectModelColumns::UntranslatedCount: + case ProjectModelColumns::IncompleteCount: + return QVariant(Qt::AlignRight | Qt::AlignAbsolute); + default: + return QVariant(Qt::AlignLeft); } } case Qt::DisplayRole: { switch (column) { - case ProjectModelColumns::FileName: - return i18nc("@title:column File name", "Name"); - case ProjectModelColumns::Graph: - return i18nc("@title:column Graphical representation of Translated/Fuzzy/Untranslated counts", "Graph"); - case ProjectModelColumns::TotalCount: - return i18nc("@title:column Number of entries", "Total"); - case ProjectModelColumns::TranslatedCount: - return i18nc("@title:column Number of entries", "Translated"); - case ProjectModelColumns::FuzzyCount: - return i18nc("@title:column Number of entries", "Not ready"); - case ProjectModelColumns::UntranslatedCount: - return i18nc("@title:column Number of entries", "Untranslated"); - case ProjectModelColumns::IncompleteCount: - return i18nc("@title:column Number of fuzzy or untranslated entries", "Incomplete"); - case ProjectModelColumns::TranslationDate: - return i18nc("@title:column", "Last Translation"); - case ProjectModelColumns::SourceDate: - return i18nc("@title:column", "Template Revision"); - case ProjectModelColumns::LastTranslator: - return i18nc("@title:column", "Last Translator"); - default: - return {}; + case ProjectModelColumns::FileName: + return i18nc("@title:column File name", "Name"); + case ProjectModelColumns::Graph: + return i18nc("@title:column Graphical representation of Translated/Fuzzy/Untranslated counts", "Graph"); + case ProjectModelColumns::TotalCount: + return i18nc("@title:column Number of entries", "Total"); + case ProjectModelColumns::TranslatedCount: + return i18nc("@title:column Number of entries", "Translated"); + case ProjectModelColumns::FuzzyCount: + return i18nc("@title:column Number of entries", "Not ready"); + case ProjectModelColumns::UntranslatedCount: + return i18nc("@title:column Number of entries", "Untranslated"); + case ProjectModelColumns::IncompleteCount: + return i18nc("@title:column Number of fuzzy or untranslated entries", "Incomplete"); + case ProjectModelColumns::TranslationDate: + return i18nc("@title:column", "Last Translation"); + case ProjectModelColumns::SourceDate: + return i18nc("@title:column", "Template Revision"); + case ProjectModelColumns::LastTranslator: + return i18nc("@title:column", "Last Translator"); + default: + return {}; } } default: return {}; } } Qt::ItemFlags ProjectModel::flags(const QModelIndex & index) const { if (static_cast(index.column()) == ProjectModelColumns::FileName) return Qt::ItemIsSelectable | Qt::ItemIsEnabled; else return Qt::ItemIsSelectable; } int ProjectModel::rowCount(const QModelIndex & parent /*= QModelIndex()*/) const { return nodeForIndex(parent)->rows.size(); } bool ProjectModel::hasChildren(const QModelIndex & parent /*= QModelIndex()*/) const { if (!parent.isValid()) return true; QModelIndex poIndex = poIndexForOuter(parent); QModelIndex potIndex = potIndexForOuter(parent); return ((poIndex.isValid() && m_poModel.hasChildren(poIndex)) || (potIndex.isValid() && m_potModel.hasChildren(potIndex))); } bool ProjectModel::canFetchMore(const QModelIndex & parent) const { if (!parent.isValid()) return m_poModel.canFetchMore(QModelIndex()) || m_potModel.canFetchMore(QModelIndex()); QModelIndex poIndex = poIndexForOuter(parent); QModelIndex potIndex = potIndexForOuter(parent); return ((poIndex.isValid() && m_poModel.canFetchMore(poIndex)) || (potIndex.isValid() && m_potModel.canFetchMore(potIndex))); } void ProjectModel::fetchMore(const QModelIndex & parent) { if (!parent.isValid()) { if (m_poModel.canFetchMore(QModelIndex())) m_poModel.fetchMore(QModelIndex()); if (m_potModel.canFetchMore(QModelIndex())) m_potModel.fetchMore(QModelIndex()); } else { QModelIndex poIndex = poIndexForOuter(parent); QModelIndex potIndex = potIndexForOuter(parent); if (poIndex.isValid() && (m_poModel.canFetchMore(poIndex))) m_poModel.fetchMore(poIndex); if (potIndex.isValid() && (m_potModel.canFetchMore(potIndex))) m_potModel.fetchMore(potIndex); } } /** * we use QRect to pass data through QVariant tunnel * * order is tran, untr, fuzzy * left() top() width() * */ QVariant ProjectModel::data(const QModelIndex& index, const int role) const { if (!index.isValid()) return QVariant(); const auto column = static_cast(index.column()); const ProjectNode* node = nodeForIndex(index); const QModelIndex internalIndex = poOrPotIndexForOuter(index); if (!internalIndex.isValid()) return QVariant(); const KFileItem item = itemForIndex(index); const bool isDir = item.isDir(); const bool invalid_file = node->metaDataStatus == ProjectNode::Status::InvalidFile; const bool hasStats = node->metaDataStatus != ProjectNode::Status::NoStats; const int translated = node->translatedAsPerRole(); const int fuzzy = node->fuzzyAsPerRole(); const int untranslated = node->metaData.untranslated; switch (role) { case Qt::TextAlignmentRole: return ProjectModel::headerData(index.column(), Qt::Horizontal, role); // Use same alignment as header case Qt::DisplayRole: switch (column) { - case ProjectModelColumns::FileName: - return item.text(); - case ProjectModelColumns::Graph: - return hasStats ? QRect(translated, untranslated, fuzzy, 0) : QVariant(); - case ProjectModelColumns::TotalCount: - return hasStats ? (translated + untranslated + fuzzy) : QVariant(); - case ProjectModelColumns::TranslatedCount: - return hasStats ? translated : QVariant(); - case ProjectModelColumns::FuzzyCount: - return hasStats ? fuzzy : QVariant(); - case ProjectModelColumns::UntranslatedCount: - return hasStats ? untranslated : QVariant(); - case ProjectModelColumns::IncompleteCount: - return hasStats ? (untranslated + fuzzy) : QVariant(); - case ProjectModelColumns::SourceDate: - return node->metaData.sourceDate; - case ProjectModelColumns::TranslationDate: - return node->metaData.translationDate; - case ProjectModelColumns::LastTranslator: - return node->metaData.lastTranslator; - default: - return {}; + case ProjectModelColumns::FileName: + return item.text(); + case ProjectModelColumns::Graph: + return hasStats ? QRect(translated, untranslated, fuzzy, 0) : QVariant(); + case ProjectModelColumns::TotalCount: + return hasStats ? (translated + untranslated + fuzzy) : QVariant(); + case ProjectModelColumns::TranslatedCount: + return hasStats ? translated : QVariant(); + case ProjectModelColumns::FuzzyCount: + return hasStats ? fuzzy : QVariant(); + case ProjectModelColumns::UntranslatedCount: + return hasStats ? untranslated : QVariant(); + case ProjectModelColumns::IncompleteCount: + return hasStats ? (untranslated + fuzzy) : QVariant(); + case ProjectModelColumns::SourceDate: + return node->metaData.sourceDate; + case ProjectModelColumns::TranslationDate: + return node->metaData.translationDate; + case ProjectModelColumns::LastTranslator: + return node->metaData.lastTranslator; + default: + return {}; } case Qt::ToolTipRole: if (column == ProjectModelColumns::FileName) { return item.text(); } else { return {}; } case KDirModel::FileItemRole: return QVariant::fromValue(item); case Qt::DecorationRole: if (column != ProjectModelColumns::FileName) { return QVariant(); } if (isDir) return m_dirIcon; if (invalid_file) return m_poInvalidIcon; else if (hasStats && fuzzy == 0 && untranslated == 0) { if (translated == 0) return m_poEmptyIcon; else return m_poComplIcon; } else if (node->poRowNumber != -1) return m_poIcon; else if (node->potRowNumber != -1) return m_potIcon; else return QVariant(); case FuzzyUntrCountAllRole: return hasStats ? (fuzzy + untranslated) : 0; case FuzzyUntrCountRole: return item.isFile() ? (fuzzy + untranslated) : 0; case FuzzyCountRole: return item.isFile() ? fuzzy : 0; case UntransCountRole: return item.isFile() ? untranslated : 0; case TemplateOnlyRole: return item.isFile() ? (node->poRowNumber == -1) : 0; case TransOnlyRole: return item.isFile() ? (node->potRowNumber == -1) : 0; case DirectoryRole: return isDir ? 1 : 0; case TotalRole: return hasStats ? (fuzzy + untranslated + translated) : 0; default: return QVariant(); } } QModelIndex ProjectModel::index(int row, int column, const QModelIndex& parent) const { ProjectNode* parentNode = nodeForIndex(parent); //qCWarning(LOKALIZE_LOG)<<(sizeof(ProjectNode))<= parentNode->rows.size()) { qCWarning(LOKALIZE_LOG) << "Issues with indexes" << row << parentNode->rows.size() << itemForIndex(parent).url(); return QModelIndex(); } return createIndex(row, column, parentNode->rows.at(row)); } KFileItem ProjectModel::itemForIndex(const QModelIndex& index) const { if (!index.isValid()) { //file item for root node. return m_poModel.itemForIndex(index); } QModelIndex poIndex = poIndexForOuter(index); if (poIndex.isValid()) return m_poModel.itemForIndex(poIndex); else { QModelIndex potIndex = potIndexForOuter(index); if (potIndex.isValid()) return m_potModel.itemForIndex(potIndex); } qCInfo(LOKALIZE_LOG) << "returning empty KFileItem()" << index.row() << index.column(); qCInfo(LOKALIZE_LOG) << "returning empty KFileItem()" << index.parent().isValid(); qCInfo(LOKALIZE_LOG) << "returning empty KFileItem()" << index.parent().internalPointer(); qCInfo(LOKALIZE_LOG) << "returning empty KFileItem()" << index.parent().data().toString(); qCInfo(LOKALIZE_LOG) << "returning empty KFileItem()" << index.internalPointer(); qCInfo(LOKALIZE_LOG) << "returning empty KFileItem()" << - static_cast(index.internalPointer())->metaData.untranslated << - static_cast(index.internalPointer())->metaData.sourceDate; + static_cast(index.internalPointer())->metaData.untranslated << + static_cast(index.internalPointer())->metaData.sourceDate; return KFileItem(); } ProjectModel::ProjectNode* ProjectModel::nodeForIndex(const QModelIndex& index) const { if (index.isValid()) { ProjectNode * node = static_cast(index.internalPointer()); Q_ASSERT(node != NULL); return node; } else { ProjectNode * node = const_cast(&m_rootNode); Q_ASSERT(node != NULL); return node; } } QModelIndex ProjectModel::indexForNode(const ProjectNode* node) { if (node == &m_rootNode) return QModelIndex(); int row = node->rowNumber; QModelIndex index = createIndex(row, 0, (void*)node); return index; } QModelIndex ProjectModel::indexForUrl(const QUrl& url) { if (m_poUrl.isParentOf(url)) { QModelIndex poIndex = m_poModel.indexForUrl(url); return indexForPoIndex(poIndex); } else if (m_potUrl.isParentOf(url)) { QModelIndex potIndex = m_potModel.indexForUrl(url); return indexForPotIndex(potIndex); } return QModelIndex(); } QModelIndex ProjectModel::parent(const QModelIndex& childIndex) const { if (!childIndex.isValid()) return QModelIndex(); ProjectNode* childNode = nodeForIndex(childIndex); ProjectNode* parentNode = childNode->parent; if (!parentNode || (childNode == &m_rootNode) || (parentNode == &m_rootNode)) return QModelIndex(); return createIndex(parentNode->rowNumber, 0, parentNode); } /** * Theese methods map from project model indices to PO and POT model indices. * In each folder files form PO model comes first, and files from POT that do not exist in PO model come after. */ QModelIndex ProjectModel::indexForOuter(const QModelIndex& outerIndex, IndexType type) const { if (!outerIndex.isValid()) return QModelIndex(); QModelIndex parent = outerIndex.parent(); QModelIndex internalParent; if (parent.isValid()) { internalParent = indexForOuter(parent, type); if (!internalParent.isValid()) return QModelIndex(); } ProjectNode* node = nodeForIndex(outerIndex); short rowNumber = (type == PoIndex ? node->poRowNumber : node->potRowNumber); if (rowNumber == -1) return QModelIndex(); return (type == PoIndex ? m_poModel : m_potModel).index(rowNumber, outerIndex.column(), internalParent); } QModelIndex ProjectModel::poIndexForOuter(const QModelIndex& outerIndex) const { return indexForOuter(outerIndex, PoIndex); } QModelIndex ProjectModel::potIndexForOuter(const QModelIndex& outerIndex) const { return indexForOuter(outerIndex, PotIndex); } QModelIndex ProjectModel::poOrPotIndexForOuter(const QModelIndex& outerIndex) const { if (!outerIndex.isValid()) return QModelIndex(); QModelIndex poIndex = poIndexForOuter(outerIndex); if (poIndex.isValid()) return poIndex; QModelIndex potIndex = potIndexForOuter(outerIndex); if (!potIndex.isValid()) qCWarning(LOKALIZE_LOG) << "error mapping index to PO or POT"; return potIndex; } QModelIndex ProjectModel::indexForPoIndex(const QModelIndex& poIndex) const { if (!poIndex.isValid()) return QModelIndex(); QModelIndex outerParent = indexForPoIndex(poIndex.parent()); int row = poIndex.row(); //keep the same row, no changes return index(row, poIndex.column(), outerParent); } QModelIndex ProjectModel::indexForPotIndex(const QModelIndex& potIndex) const { if (!potIndex.isValid()) return QModelIndex(); QModelIndex outerParent = indexForPotIndex(potIndex.parent()); ProjectNode* node = nodeForIndex(outerParent); int potRow = potIndex.row(); int row = 0; while (row < node->rows.count() && node->rows.at(row)->potRowNumber != potRow) row++; if (row != node->rows.count()) return index(row, potIndex.column(), outerParent); qCWarning(LOKALIZE_LOG) << "error mapping index from POT to outer, searched for potRow:" << potRow; return QModelIndex(); } /** * Makes a list of indices where pot items map to poItems. * result[potRow] = poRow or -1 if the pot entry is not found in po. * Does not use internal pot and po row number cache. */ void ProjectModel::generatePOTMapping(QVector & result, const QModelIndex& poParent, const QModelIndex& potParent) const { result.clear(); int poRows = m_poModel.rowCount(poParent); int potRows = m_potModel.rowCount(potParent); if (potRows == 0) return; QList poOccupiedUrls; for (int poPos = 0; poPos < poRows; poPos ++) { KFileItem file = m_poModel.itemForIndex(m_poModel.index(poPos, 0, poParent)); QUrl potUrl = poToPot(file.url()); poOccupiedUrls.append(potUrl); } for (int potPos = 0; potPos < potRows; potPos ++) { QUrl potUrl = m_potModel.itemForIndex(m_potModel.index(potPos, 0, potParent)).url(); int occupiedPos = -1; //TODO: this is slow for (int poPos = 0; occupiedPos == -1 && poPos < poOccupiedUrls.count(); poPos ++) { QUrl& occupiedUrl = poOccupiedUrls[poPos]; if (potUrl.matches(occupiedUrl, QUrl::StripTrailingSlash)) occupiedPos = poPos; } result.append(occupiedPos); } } QUrl ProjectModel::poToPot(const QUrl& poPath) const { if (!(m_poUrl.isParentOf(poPath) || m_poUrl.matches(poPath, QUrl::StripTrailingSlash))) { qCWarning(LOKALIZE_LOG) << "PO path not in project: " << poPath.url(); return QUrl(); } QString pathToAdd = QDir(m_poUrl.path()).relativeFilePath(poPath.path()); //change ".po" into ".pot" if (pathToAdd.endsWith(QLatin1String(".po"))) //TODO: what about folders ?? pathToAdd += 't'; QUrl potPath = m_potUrl; potPath.setPath(potPath.path() + '/' + pathToAdd); //qCDebug(LOKALIZE_LOG) << "ProjectModel::poToPot("<< poPath.pathOrUrl() << +") = " << potPath.pathOrUrl(); return potPath; } QUrl ProjectModel::potToPo(const QUrl& potPath) const { if (!(m_potUrl.isParentOf(potPath) || m_potUrl.matches(potPath, QUrl::StripTrailingSlash))) { qCWarning(LOKALIZE_LOG) << "POT path not in project: " << potPath.url(); return QUrl(); } QString pathToAdd = QDir(m_potUrl.path()).relativeFilePath(potPath.path()); //change ".pot" into ".po" if (pathToAdd.endsWith(QLatin1String(".pot"))) //TODO: what about folders ?? pathToAdd = pathToAdd.left(pathToAdd.length() - 1); QUrl poPath = m_poUrl; poPath.setPath(poPath.path() + '/' + pathToAdd); //qCDebug(LOKALIZE_LOG) << "ProjectModel::potToPo("<< potPath.pathOrUrl() << +") = " << poPath.pathOrUrl(); return poPath; } //Metadata stuff //For updating translation stats void ProjectModel::enqueueNodeForMetadataUpdate(ProjectNode* node) { //qCWarning(LOKALIZE_LOG) << "Enqueued node for metadata Update : " << node->rowNumber; m_doneTimer->stop(); if (m_dirsWaitingForMetadata.contains(node)) { if ((m_activeJob != NULL) && (m_activeNode == node)) m_activeJob->setStatus(-1); return; } m_dirsWaitingForMetadata.insert(node); if (m_activeJob == NULL) startNewMetadataJob(); } void ProjectModel::deleteSubtree(ProjectNode* node) { for (int row = 0; row < node->rows.count(); row ++) deleteSubtree(node->rows.at(row)); m_dirsWaitingForMetadata.remove(node); if ((m_activeJob != NULL) && (m_activeNode == node)) m_activeJob->setStatus(-1); delete node; } void ProjectModel::startNewMetadataJob() { if (!m_completeScan) //hack for debugging return; m_activeJob = NULL; m_activeNode = NULL; if (m_dirsWaitingForMetadata.isEmpty()) return; ProjectNode* node = *m_dirsWaitingForMetadata.constBegin(); //prepare new work m_activeNode = node; QList files; QModelIndex item = indexForNode(node); for (int row = 0; row < node->rows.count(); row ++) { KFileItem fileItem = itemForIndex(index(row, 0, item)); if (fileItem.isFile())//Do not seek items that are not files files.append(fileItem); } m_activeJob = new UpdateStatsJob(files, this); connect(m_activeJob, &UpdateStatsJob::done, this, &ProjectModel::finishMetadataUpdate); m_threadPool->start(m_activeJob); } void ProjectModel::finishMetadataUpdate(UpdateStatsJob* job) { if (job->m_status == -2) { delete job; return; } if ((m_dirsWaitingForMetadata.contains(m_activeNode)) && (job->m_status == 0)) { m_dirsWaitingForMetadata.remove(m_activeNode); //store the results setMetadataForDir(m_activeNode, m_activeJob->m_info); QModelIndex item = indexForNode(m_activeNode); //scan dubdirs - initiate data loading into the model. for (int row = 0; row < m_activeNode->rows.count(); row++) { QModelIndex child = index(row, 0, item); if (canFetchMore(child)) fetchMore(child); //QCoreApplication::processEvents(); } } delete m_activeJob; m_activeJob = 0; startNewMetadataJob(); } void ProjectModel::slotFileSaved(const QString& filePath) { QModelIndex index = indexForUrl(QUrl::fromLocalFile(filePath)); if (!index.isValid()) return; QList files; files.append(itemForIndex(index)); UpdateStatsJob* j = new UpdateStatsJob(files); connect(j, &UpdateStatsJob::done, this, &ProjectModel::finishSingleMetadataUpdate); m_threadPool->start(j); } void ProjectModel::finishSingleMetadataUpdate(UpdateStatsJob* job) { if (job->m_status != 0) { delete job; return; } const FileMetaData& info = job->m_info.first(); QModelIndex index = indexForUrl(QUrl::fromLocalFile(info.filePath)); if (!index.isValid()) return; ProjectNode* node = nodeForIndex(index); node->setFileStats(job->m_info.first()); updateDirStats(nodeForIndex(index.parent())); QModelIndex topLeft = index.sibling(index.row(), static_cast(ProjectModelColumns::Graph)); QModelIndex bottomRight = index.sibling(index.row(), ProjectModelColumnCount - 1); emit dataChanged(topLeft, bottomRight); delete job; } void ProjectModel::setMetadataForDir(ProjectNode* node, const QList& data) { const QModelIndex item = indexForNode(node); const int dataCount = data.count(); int rowsCount = 0; for (int row = 0; row < node->rows.count(); row++) if (itemForIndex(index(row, 0, item)).isFile()) rowsCount++; //Q_ASSERT(dataCount == rowsCount); if (dataCount != rowsCount) { m_delayedReloadTimer->start(2000); qCWarning(LOKALIZE_LOG) << "dataCount != rowsCount, scheduling full refresh"; return; } int dataId = 0; for (int row = 0; row < node->rows.count(); row++) { if (itemForIndex(index(row, 0, item)).isFile()) { node->rows[row]->setFileStats(data.at(dataId)); dataId++; } } if (!dataCount) return; updateDirStats(node); const QModelIndex topLeft = index(0, static_cast(ProjectModelColumns::Graph), item); const QModelIndex bottomRight = index(rowsCount - 1, ProjectModelColumnCount - 1, item); emit dataChanged(topLeft, bottomRight); } void ProjectModel::updateDirStats(ProjectNode* node) { node->calculateDirStats(); if (node == &m_rootNode) { updateTotalsChanged(); return; } updateDirStats(node->parent); if (node->parent->rows.count() == 0 || node->parent->rows.count() >= node->rowNumber) return; QModelIndex index = indexForNode(node); qCDebug(LOKALIZE_LOG) << index.row() << node->parent->rows.count(); if (index.row() >= node->parent->rows.count()) return; QModelIndex topLeft = index.sibling(index.row(), static_cast(ProjectModelColumns::Graph)); QModelIndex bottomRight = index.sibling(index.row(), ProjectModelColumnCount - 1); emit dataChanged(topLeft, bottomRight); } bool ProjectModel::updateDone(const QModelIndex& index, const KDirModel& model) { if (model.canFetchMore(index)) return false; int row = model.rowCount(index); while (--row >= 0) { if (!updateDone(model.index(row, 0, index), model)) return false; } return true; } void ProjectModel::updateTotalsChanged() { bool done = m_dirsWaitingForMetadata.isEmpty(); if (done) { done = updateDone(m_poModel.indexForUrl(m_poUrl), m_poModel) && updateDone(m_potModel.indexForUrl(m_potUrl), m_potModel); if (m_rootNode.fuzzyAsPerRole() + m_rootNode.translatedAsPerRole() + m_rootNode.metaData.untranslated > 0 && !done) m_doneTimer->start(2000); emit loadingFinished(); } emit totalsChanged(m_rootNode.fuzzyAsPerRole(), m_rootNode.translatedAsPerRole(), m_rootNode.metaData.untranslated, done); } //ProjectNode class ProjectModel::ProjectNode::ProjectNode(ProjectNode* _parent, int _rowNum, int _poIndex, int _potIndex) : parent(_parent) , rowNumber(_rowNum) , poRowNumber(_poIndex) , potRowNumber(_potIndex) , poCount(0) , metaDataStatus(Status::NoStats) , metaData() { ++nodeCounter; } ProjectModel::ProjectNode::~ProjectNode() { --nodeCounter; } void ProjectModel::ProjectNode::calculateDirStats() { metaData.fuzzy = 0; metaData.fuzzy_reviewer = 0; metaData.fuzzy_approver = 0; metaData.translated = 0; metaData.translated_reviewer = 0; metaData.translated_approver = 0; metaData.untranslated = 0; metaDataStatus = ProjectNode::Status::HasStats; for (int pos = 0; pos < rows.count(); pos++) { ProjectNode* child = rows.at(pos); if (child->metaDataStatus == ProjectNode::Status::HasStats) { metaData.fuzzy += child->metaData.fuzzy; metaData.fuzzy_reviewer += child->metaData.fuzzy_reviewer; metaData.fuzzy_approver += child->metaData.fuzzy_approver; metaData.translated += child->metaData.translated; metaData.translated_reviewer += child->metaData.translated_reviewer; metaData.translated_approver += child->metaData.translated_approver; metaData.untranslated += child->metaData.untranslated; } } } void ProjectModel::ProjectNode::setFileStats(const FileMetaData& info) { metaData = info; metaDataStatus = info.invalid_file ? Status::InvalidFile : Status::HasStats; } void ProjectModel::ProjectNode::resetMetaData() { metaDataStatus = Status::NoStats; metaData = FileMetaData(); } diff --git a/src/project/projecttab.cpp b/src/project/projecttab.cpp index bc8205e..4a61bbd 100644 --- a/src/project/projecttab.cpp +++ b/src/project/projecttab.cpp @@ -1,490 +1,490 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2007-2014 by Nick Shaforostoff 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . **************************************************************************** */ #include "projecttab.h" #include "project.h" #include "projectwidget.h" #include "tmscanapi.h" #include "prefs.h" #include "prefs_lokalize.h" #include "catalog.h" #include "lokalize_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include ProjectTab::ProjectTab(QWidget *parent) : LokalizeSubwindowBase2(parent) , m_browser(new ProjectWidget(this)) , m_filterEdit(new QLineEdit(this)) , m_pologyProcessInProgress(false) , m_legacyUnitsCount(-1) , m_currentUnitsCount(0) { setWindowTitle(i18nc("@title:window", "Project Overview")); //setCaption(i18nc("@title:window","Project"),false); //BEGIN setup welcome widget QWidget* welcomeWidget = new QWidget(this); QVBoxLayout* wl = new QVBoxLayout(welcomeWidget); QLabel* about = new QLabel(i18n("" //copied from kaboutkdedialog_p.cpp "You do not have to be a software developer to be a member of the " "KDE team. You can join the national teams that translate " "program interfaces. You can provide graphics, themes, sounds, and " "improved documentation. You decide!" "

    " "Visit " "%1 " "for information on some projects in which you can participate." "

    " "If you need more information or documentation, then a visit to " "%2 " "will provide you with what you need.", QLatin1String("https://community.kde.org/Get_Involved"), QLatin1String("https://techbase.kde.org/")), welcomeWidget); about->setAlignment(Qt::AlignCenter); about->setWordWrap(true); about->setOpenExternalLinks(true); about->setTextInteractionFlags(Qt::TextBrowserInteraction); about->setTextFormat(Qt::RichText); QPushButton* conf = new QPushButton(i18n("&Configure Lokalize"), welcomeWidget); QPushButton* openProject = new QPushButton(i18nc("@action:inmenu", "Open project"), welcomeWidget); QPushButton* createProject = new QPushButton(i18nc("@action:inmenu", "Translate software"), welcomeWidget); QPushButton* createOdfProject = new QPushButton(i18nc("@action:inmenu", "Translate OpenDocument"), welcomeWidget); connect(conf, &QPushButton::clicked, SettingsController::instance(), &SettingsController::showSettingsDialog); connect(openProject, &QPushButton::clicked, this, QOverload<>::of(&ProjectTab::projectOpenRequested)); connect(createProject, &QPushButton::clicked, SettingsController::instance(), &SettingsController::projectCreate); connect(createOdfProject, &QPushButton::clicked, Project::instance(), &Project::projectOdfCreate); QHBoxLayout* wbtnl = new QHBoxLayout(); wbtnl->addStretch(1); wbtnl->addWidget(conf); wbtnl->addWidget(openProject); wbtnl->addWidget(createProject); wbtnl->addWidget(createOdfProject); wbtnl->addStretch(1); wl->addStretch(1); wl->addWidget(about); wl->addStretch(1); wl->addLayout(wbtnl); wl->addStretch(1); //END setup welcome widget QWidget* baseWidget = new QWidget(this); m_stackedLayout = new QStackedLayout(baseWidget); QWidget* w = new QWidget(this); m_stackedLayout->addWidget(welcomeWidget); m_stackedLayout->addWidget(w); connect(Project::instance(), &Project::loaded, this, &ProjectTab::showRealProjectOverview); if (Project::instance()->isLoaded()) //for --project cmd option showRealProjectOverview(); QVBoxLayout* l = new QVBoxLayout(w); m_filterEdit->setClearButtonEnabled(true); m_filterEdit->setPlaceholderText(i18n("Quick search...")); m_filterEdit->setToolTip(i18nc("@info:tooltip", "Activated by Ctrl+L.") + ' ' + i18nc("@info:tooltip", "Accepts regular expressions")); connect(m_filterEdit, &QLineEdit::textChanged, this, &ProjectTab::setFilterRegExp, Qt::QueuedConnection); new QShortcut(Qt::CTRL + Qt::Key_L, this, SLOT(setFocus()), 0, Qt::WidgetWithChildrenShortcut); l->addWidget(m_filterEdit); l->addWidget(m_browser); connect(m_browser, &ProjectWidget::fileOpenRequested, this, &ProjectTab::fileOpenRequested); connect(Project::instance()->model(), &ProjectModel::totalsChanged, this, &ProjectTab::updateStatusBar); connect(Project::instance()->model(), &ProjectModel::loadingAboutToStart, this, &ProjectTab::initStatusBarProgress); setCentralWidget(baseWidget); QStatusBar* statusBar = static_cast(parent)->statusBar(); m_progressBar = new QProgressBar(0); m_progressBar->setVisible(false); statusBar->insertWidget(ID_STATUS_PROGRESS, m_progressBar, 1); setXMLFile(QStringLiteral("projectmanagerui.rc"), true); setUpdatedXMLFile(); //QAction* action = KStandardAction::find(Project::instance(),&ProjectTab::showTM,actionCollection()); #define ADD_ACTION_SHORTCUT_ICON(_name,_text,_shortcut,_icon)\ action = nav->addAction(QStringLiteral(_name));\ action->setText(_text);\ action->setIcon(QIcon::fromTheme(_icon));\ ac->setDefaultShortcut(action, QKeySequence( _shortcut )); QAction *action; KActionCollection* ac = actionCollection(); KActionCategory* nav = new KActionCategory(i18nc("@title actions category", "Navigation"), ac); ADD_ACTION_SHORTCUT_ICON("go_prev_fuzzyUntr", i18nc("@action:inmenu\n'not ready' means 'fuzzy' in gettext terminology", "Previous not ready"), Qt::CTRL + Qt::SHIFT + Qt::Key_PageUp, "prevfuzzyuntrans") connect(action, &QAction::triggered, this, &ProjectTab::gotoPrevFuzzyUntr); ADD_ACTION_SHORTCUT_ICON("go_next_fuzzyUntr", i18nc("@action:inmenu\n'not ready' means 'fuzzy' in gettext terminology", "Next not ready"), Qt::CTRL + Qt::SHIFT + Qt::Key_PageDown, "nextfuzzyuntrans") connect(action, &QAction::triggered, this, &ProjectTab::gotoNextFuzzyUntr); ADD_ACTION_SHORTCUT_ICON("go_prev_fuzzy", i18nc("@action:inmenu\n'not ready' means 'fuzzy' in gettext terminology", "Previous non-empty but not ready"), Qt::CTRL + Qt::Key_PageUp, "prevfuzzy") connect(action, &QAction::triggered, this, &ProjectTab::gotoPrevFuzzy); ADD_ACTION_SHORTCUT_ICON("go_next_fuzzy", i18nc("@action:inmenu\n'not ready' means 'fuzzy' in gettext terminology", "Next non-empty but not ready"), Qt::CTRL + Qt::Key_PageDown, "nextfuzzy") connect(action, &QAction::triggered, this, &ProjectTab::gotoNextFuzzy); ADD_ACTION_SHORTCUT_ICON("go_prev_untrans", i18nc("@action:inmenu", "Previous untranslated"), Qt::ALT + Qt::Key_PageUp, "prevuntranslated") connect(action, &QAction::triggered, this, &ProjectTab::gotoPrevUntranslated); ADD_ACTION_SHORTCUT_ICON("go_next_untrans", i18nc("@action:inmenu", "Next untranslated"), Qt::ALT + Qt::Key_PageDown, "nextuntranslated") connect(action, &QAction::triggered, this, &ProjectTab::gotoNextUntranslated); ADD_ACTION_SHORTCUT_ICON("go_prev_templateOnly", i18nc("@action:inmenu", "Previous template only"), Qt::CTRL + Qt::Key_Up, "prevtemplate") connect(action, &QAction::triggered, this, &ProjectTab::gotoPrevTemplateOnly); ADD_ACTION_SHORTCUT_ICON("go_next_templateOnly", i18nc("@action:inmenu", "Next template only"), Qt::CTRL + Qt::Key_Down, "nexttemplate") connect(action, &QAction::triggered, this, &ProjectTab::gotoNextTemplateOnly); ADD_ACTION_SHORTCUT_ICON("go_prev_transOnly", i18nc("@action:inmenu", "Previous translation only"), Qt::ALT + Qt::Key_Up, "prevpo") connect(action, &QAction::triggered, this, &ProjectTab::gotoPrevTransOnly); ADD_ACTION_SHORTCUT_ICON("go_next_transOnly", i18nc("@action:inmenu", "Next translation only"), Qt::ALT + Qt::Key_Down, "nextpo") connect(action, &QAction::triggered, this, &ProjectTab::gotoNextTransOnly); action = nav->addAction(QStringLiteral("toggle_translated_files")); action->setText(i18nc("@action:inmenu", "Hide completed items")); action->setToolTip(i18nc("@action:inmenu", "Hide fully translated files and folders")); action->setIcon(QIcon::fromTheme("hide_table_row")); action->setCheckable(true); ac->setDefaultShortcut(action, QKeySequence(Qt::CTRL + Qt::Key_T)); connect(action, &QAction::triggered, this, &ProjectTab::toggleTranslatedFiles); // ADD_ACTION_SHORTCUT_ICON("edit_find",i18nc("@action:inmenu","Find in files"),Qt::ALT+Qt::Key_Down,"nextpo") //connect(action, &QAction::triggered, this, &ProjectTab::gotoNextTransOnly); action = nav->addAction(KStandardAction::Find, this, SLOT(searchInFiles())); KActionCategory* proj = new KActionCategory(i18nc("@title actions category", "Project"), ac); action = proj->addAction(QStringLiteral("project_open"), this, SIGNAL(projectOpenRequested())); action->setText(i18nc("@action:inmenu", "Open project")); action->setIcon(QIcon::fromTheme("project-open")); int i = 6; while (--i > ID_STATUS_PROGRESS) statusBarItems.insert(i, QString()); } void ProjectTab::showRealProjectOverview() { m_stackedLayout->setCurrentIndex(1); } void ProjectTab::showWelcomeScreen() { m_stackedLayout->setCurrentIndex(0); } void ProjectTab::toggleTranslatedFiles() { m_browser->toggleTranslatedFiles(); } QString ProjectTab::currentFilePath() { return Project::instance()->path(); } void ProjectTab::setFocus() { m_filterEdit->setFocus(); m_filterEdit->selectAll(); } void ProjectTab::setFilterRegExp() { QString newPattern = m_filterEdit->text(); if (m_browser->proxyModel()->filterRegExp().pattern() == newPattern) return; m_browser->proxyModel()->setFilterRegExp(newPattern); if (newPattern.size() > 2) m_browser->expandItems(); } void ProjectTab::contextMenuEvent(QContextMenuEvent *event) { QMenu* menu = new QMenu(this); connect(menu, &QMenu::aboutToHide, menu, &QMenu::deleteLater); if (m_browser->selectedItems().size() > 1 || (m_browser->selectedItems().size() == 1 && !m_browser->currentIsTranslationFile())) { menu->addAction(i18nc("@action:inmenu", "Open selected files"), this, &ProjectTab::openFile); menu->addSeparator(); } else if (m_browser->currentIsTranslationFile()) { menu->addAction(i18nc("@action:inmenu", "Open"), this, &ProjectTab::openFile); menu->addSeparator(); } /*menu.addAction(i18nc("@action:inmenu","Find in files"),this,&ProjectTab::findInFiles); menu.addAction(i18nc("@action:inmenu","Replace in files"),this,&ProjectTab::replaceInFiles); menu.addAction(i18nc("@action:inmenu","Spellcheck files"),this,&ProjectTab::spellcheckFiles); menu.addSeparator(); menu->addAction(i18nc("@action:inmenu","Get statistics for subfolders"),m_browser,&ProjectTab::expandItems); */ menu->addAction(i18nc("@action:inmenu", "Add to translation memory"), this, &ProjectTab::scanFilesToTM); menu->addAction(i18nc("@action:inmenu", "Search in files"), this, &ProjectTab::searchInFiles); if (Settings::self()->pologyEnabled()) { menu->addAction(i18nc("@action:inmenu", "Launch Pology on files"), this, &ProjectTab::pologyOnFiles); } if (QDir(Project::instance()->templatesRoot()).exists()) menu->addAction(i18nc("@action:inmenu", "Search in files (including templates)"), this, &ProjectTab::searchInFilesInclTempl); // else if (Project::instance()->model()->hasChildren(/*m_proxyModel->mapToSource(*/(m_browser->currentIndex())) // ) // { // menu.addSeparator(); // menu.addAction(i18n("Force Scanning"),this,&ProjectTab::slotForceStats); // // } menu->popup(event->globalPos()); } void ProjectTab::scanFilesToTM() { TM::scanRecursive(m_browser->selectedItems(), Project::instance()->projectID()); } void ProjectTab::searchInFiles(bool templ) { QStringList files = m_browser->selectedItems(); if (!templ) { QString templatesRoot = Project::instance()->templatesRoot(); int i = files.size(); while (--i >= 0) { if (files.at(i).startsWith(templatesRoot)) files.removeAt(i); } } emit searchRequested(files); } void ProjectTab::pologyOnFiles() { if (!m_pologyProcessInProgress) { QStringList files = m_browser->selectedItems(); QString templatesRoot = Project::instance()->templatesRoot(); QString filesAsString; int i = files.size(); while (--i >= 0) { if (files.at(i).endsWith(QStringLiteral(".po"))) filesAsString += QStringLiteral("\"") + files.at(i) + QStringLiteral("\" "); } QString command = Settings::self()->pologyCommandFile().replace(QStringLiteral("%f"), filesAsString); m_pologyProcess = new KProcess; m_pologyProcess->setOutputChannelMode(KProcess::SeparateChannels); qCWarning(LOKALIZE_LOG) << "Launching pology command: " << command; connect(m_pologyProcess, QOverload::of(&KProcess::finished), this, &ProjectTab::pologyHasFinished); m_pologyProcess->setShellCommand(command); m_pologyProcessInProgress = true; m_pologyProcess->start(); } else { KMessageBox::error(this, i18n("A Pology check is already in progress."), i18n("Pology error")); } } void ProjectTab::pologyHasFinished(int exitCode, QProcess::ExitStatus exitStatus) { const QString pologyError = m_pologyProcess->readAllStandardError(); if (exitStatus == QProcess::CrashExit) { KMessageBox::error(this, i18n("The Pology check has crashed unexpectedly:\n%1", pologyError), i18n("Pology error")); } else if (exitCode == 0) { KMessageBox::information(this, i18n("The Pology check has succeeded"), i18n("Pology success")); } else { KMessageBox::error(this, i18n("The Pology check has returned an error:\n%1", pologyError), i18n("Pology error")); } m_pologyProcess->deleteLater(); m_pologyProcessInProgress = false; } void ProjectTab::searchInFilesInclTempl() { searchInFiles(true); } void ProjectTab::openFile() { QStringList files = m_browser->selectedItems(); int i = files.size(); if (i > 50) { QString caption = i18np("You are about to open %1 file", "You are about to open %1 files", i); QString text = i18n("Opening a large number of files at the same time can make Lokalize unresponsive.") + QStringLiteral("\n\n") + i18n("Are you sure you want to open this many files?"); auto yes = KGuiItem( - i18np("&Open %1 File", "&Open %1 Files", i), - QStringLiteral("document-open") - ); + i18np("&Open %1 File", "&Open %1 Files", i), + QStringLiteral("document-open") + ); const int answer = KMessageBox::warningYesNo( - this, text, caption, yes, KStandardGuiItem::cancel() - ); + this, text, caption, yes, KStandardGuiItem::cancel() + ); if (answer != KMessageBox::Yes) { return; } } while (--i >= 0) { if (Catalog::extIsSupported(files.at(i))) { emit fileOpenRequested(files.at(i), true); } } } void ProjectTab::findInFiles() { emit searchRequested(m_browser->selectedItems()); } void ProjectTab::replaceInFiles() { emit replaceRequested(m_browser->selectedItems()); } void ProjectTab::spellcheckFiles() { emit spellcheckRequested(m_browser->selectedItems()); } void ProjectTab::gotoPrevFuzzyUntr() { m_browser->gotoPrevFuzzyUntr(); } void ProjectTab::gotoNextFuzzyUntr() { m_browser->gotoNextFuzzyUntr(); } void ProjectTab::gotoPrevFuzzy() { m_browser->gotoPrevFuzzy(); } void ProjectTab::gotoNextFuzzy() { m_browser->gotoNextFuzzy(); } void ProjectTab::gotoPrevUntranslated() { m_browser->gotoPrevUntranslated(); } void ProjectTab::gotoNextUntranslated() { m_browser->gotoNextUntranslated(); } void ProjectTab::gotoPrevTemplateOnly() { m_browser->gotoPrevTemplateOnly(); } void ProjectTab::gotoNextTemplateOnly() { m_browser->gotoNextTemplateOnly(); } void ProjectTab::gotoPrevTransOnly() { m_browser->gotoPrevTransOnly(); } void ProjectTab::gotoNextTransOnly() { m_browser->gotoNextTransOnly(); } bool ProjectTab::currentItemIsTranslationFile() const { return m_browser->currentIsTranslationFile(); } void ProjectTab::setCurrentItem(const QString& url) { m_browser->setCurrentItem(url); } QString ProjectTab::currentItem() const { return m_browser->currentItem(); } QStringList ProjectTab::selectedItems() const { return m_browser->selectedItems(); } void ProjectTab::updateStatusBar(int fuzzy, int translated, int untranslated, bool done) { int total = fuzzy + translated + untranslated; m_currentUnitsCount = total; if (m_progressBar->value() != total && m_legacyUnitsCount > 0) m_progressBar->setValue(total); if (m_progressBar->maximum() < qMax(total, m_legacyUnitsCount)) m_progressBar->setMaximum(qMax(total, m_legacyUnitsCount)); m_progressBar->setVisible(!done); if (done) m_legacyUnitsCount = total; statusBarItems.insert(ID_STATUS_TOTAL, i18nc("@info:status message entries", "Total: %1", total)); reflectNonApprovedCount(fuzzy, total); reflectUntranslatedCount(untranslated, total); } void ProjectTab::initStatusBarProgress() { if (m_legacyUnitsCount > 0) { if (m_progressBar->value() != 0) m_progressBar->setValue(0); if (m_progressBar->maximum() != m_legacyUnitsCount) m_progressBar->setMaximum(m_legacyUnitsCount); updateStatusBar(); } } void ProjectTab::setLegacyUnitsCount(int to) { m_legacyUnitsCount = to; m_currentUnitsCount = to; initStatusBarProgress(); } //bool ProjectTab::isShown() const {return isVisible();} diff --git a/src/project/projectwidget.cpp b/src/project/projectwidget.cpp index 3505df1..40b4f53 100644 --- a/src/project/projectwidget.cpp +++ b/src/project/projectwidget.cpp @@ -1,557 +1,558 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2007-2015 by Nick Shaforostoff 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . **************************************************************************** */ #include "projectwidget.h" #include "lokalize_debug.h" #include "project.h" #include "catalog.h" #include "headerviewmenu.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include class PoItemDelegate: public QStyledItemDelegate { public: PoItemDelegate(QObject *parent = 0); ~PoItemDelegate() {} void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; QString displayText(const QVariant & value, const QLocale & locale) const override; QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const override; private: KColorScheme m_colorScheme; }; PoItemDelegate::PoItemDelegate(QObject *parent) : QStyledItemDelegate(parent) , m_colorScheme(QPalette::Normal) {} QSize PoItemDelegate::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const { QString text = index.data().toString(); int lineCount = 1; int nPos = text.indexOf('\n'); if (nPos == -1) nPos = text.size(); else lineCount += text.count('\n'); static QFontMetrics metrics(option.font); return QSize(metrics.averageCharWidth() * nPos, metrics.height() * lineCount); } void PoItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { if (static_cast(index.column()) != ProjectModel::ProjectModelColumns::Graph) return QStyledItemDelegate::paint(painter, option, index); QVariant graphData = index.data(Qt::DisplayRole); if (Q_UNLIKELY(!graphData.isValid())) { painter->fillRect(option.rect, Qt::transparent); return; } QRect rect = graphData.toRect(); int translated = rect.left(); int untranslated = rect.top(); int fuzzy = rect.width(); int total = translated + untranslated + fuzzy; if (total > 0) { QBrush brush; painter->setPen(Qt::white); QRect myRect(option.rect); myRect.setWidth(option.rect.width() * translated / total); if (translated) { brush = m_colorScheme.foreground(KColorScheme::PositiveText); painter->fillRect(myRect, brush); } myRect.setLeft(myRect.left() + myRect.width()); myRect.setWidth(option.rect.width() * fuzzy / total); if (fuzzy) { brush = m_colorScheme.foreground(KColorScheme::NeutralText); painter->fillRect(myRect, brush); // painter->drawText(myRect,Qt::AlignRight,QString("%1").arg(data.width())); } myRect.setLeft(myRect.left() + myRect.width()); myRect.setWidth(option.rect.width() - myRect.left() + option.rect.left()); if (untranslated) brush = m_colorScheme.foreground(KColorScheme::NegativeText); //esle: paint what is left with the last brush used - blank, positive or neutral painter->fillRect(myRect, brush); // painter->drawText(myRect,Qt::AlignRight,QString("%1").arg(data.top())); } else if (total == -1) painter->fillRect(option.rect, Qt::transparent); else if (total == 0) painter->fillRect(option.rect, QBrush(Qt::gray)); } // Temporary workaround for Qt bug https://bugreports.qt.io/browse/QTBUG-78094 // to ensure that large numbers are formatted using a thousands separator -QString PoItemDelegate::displayText(const QVariant & value, const QLocale & locale) const { +QString PoItemDelegate::displayText(const QVariant & value, const QLocale & locale) const +{ return QStyledItemDelegate::displayText(value, QLocale::system()); } class SortFilterProxyModel : public KDirSortFilterProxyModel { public: SortFilterProxyModel(QObject* parent = nullptr) : KDirSortFilterProxyModel(parent) { connect(Project::instance()->model(), &ProjectModel::totalsChanged, this, &SortFilterProxyModel::invalidate); } ~SortFilterProxyModel() {} void toggleTranslatedFiles(); bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override; protected: bool lessThan(const QModelIndex& left, const QModelIndex& right) const override; private: bool m_hideTranslatedFiles = false; }; void SortFilterProxyModel::toggleTranslatedFiles() { m_hideTranslatedFiles = !m_hideTranslatedFiles; invalidateFilter(); } bool SortFilterProxyModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const { bool result = false; const QAbstractItemModel* model = sourceModel(); QModelIndex item = model->index(source_row, 0, source_parent); /* if (model->hasChildren(item)) model->fetchMore(item); */ if (item.data(ProjectModel::DirectoryRole) == 1 && item.data(ProjectModel::TotalRole) == 0) return false; // Hide rows with no translations if they are folders if (item.data(ProjectModel::FuzzyUntrCountAllRole) == 0 && m_hideTranslatedFiles) return false; // Hide rows with no untranslated items if the filter is enabled int i = model->rowCount(item); while (--i >= 0 && !result) result = filterAcceptsRow(i, item); return result || QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent); } bool SortFilterProxyModel::lessThan(const QModelIndex& left, const QModelIndex& right) const { static QCollator collator; // qCWarning(LOKALIZE_LOG)<(sourceModel()); const KFileItem leftFileItem = projectModel->itemForIndex(left); const KFileItem rightFileItem = projectModel->itemForIndex(right); //Code taken from KDirSortFilterProxyModel, as it is not compatible with our model. //TODO: make KDirSortFilterProxyModel::subSortLessThan not cast model to KDirModel, but use data() with FileItemRole instead. // Directories and hidden files should always be on the top, independent // from the sort order. const bool isLessThan = (sortOrder() == Qt::AscendingOrder); if (leftFileItem.isNull() || rightFileItem.isNull()) { qCWarning(LOKALIZE_LOG) << ".isNull()"; return false; } // On our priority, folders go above regular files. if (leftFileItem.isDir() && !rightFileItem.isDir()) { return isLessThan; } else if (!leftFileItem.isDir() && rightFileItem.isDir()) { return !isLessThan; } // Hidden elements go before visible ones, if they both are // folders or files. if (leftFileItem.isHidden() && !rightFileItem.isHidden()) { return isLessThan; } else if (!leftFileItem.isHidden() && rightFileItem.isHidden()) { return !isLessThan; } // Hidden elements go before visible ones, if they both are // folders or files. if (leftFileItem.isHidden() && !rightFileItem.isHidden()) { return true; } else if (!leftFileItem.isHidden() && rightFileItem.isHidden()) { return false; } switch (static_cast(left.column())) { case ProjectModel::ProjectModelColumns::FileName: return collator.compare(leftFileItem.name(), rightFileItem.name()) < 0; case ProjectModel::ProjectModelColumns::Graph: { QRect leftRect(left.data(Qt::DisplayRole).toRect()); QRect rightRect(right.data(Qt::DisplayRole).toRect()); int leftAll = leftRect.left() + leftRect.top() + leftRect.width(); int rightAll = rightRect.left() + rightRect.top() + rightRect.width(); if (!leftAll || !rightAll) return false; float leftVal = (float)leftRect.left() / leftAll; float rightVal = (float)rightRect.left() / rightAll; if (leftVal < rightVal) return true; if (leftVal > rightVal) return false; leftVal = (float)leftRect.top() / leftAll; rightVal = (float)rightRect.top() / rightAll; if (leftVal < rightVal) return true; if (leftVal > rightVal) return false; leftVal = (float)leftRect.width() / leftAll; rightVal = (float)rightRect.width() / rightAll; if (leftVal < rightVal) return true; return false; } case ProjectModel::ProjectModelColumns::LastTranslator: case ProjectModel::ProjectModelColumns::SourceDate: case ProjectModel::ProjectModelColumns::TranslationDate: return collator.compare(projectModel->data(left).toString(), projectModel->data(right).toString()) < 0; case ProjectModel::ProjectModelColumns::TotalCount: case ProjectModel::ProjectModelColumns::TranslatedCount: case ProjectModel::ProjectModelColumns::UntranslatedCount: case ProjectModel::ProjectModelColumns::IncompleteCount: case ProjectModel::ProjectModelColumns::FuzzyCount: return projectModel->data(left).toInt() < projectModel->data(right).toInt(); default: return false; } } ProjectWidget::ProjectWidget(/*Catalog* catalog, */QWidget* parent) : QTreeView(parent) , m_proxyModel(new SortFilterProxyModel(this)) // , m_catalog(catalog) { PoItemDelegate* delegate = new PoItemDelegate(this); setItemDelegate(delegate); connect(this, &ProjectWidget::activated, this, &ProjectWidget::slotItemActivated); m_proxyModel->setSourceModel(Project::instance()->model()); //m_proxyModel->setDynamicSortFilter(true); setModel(m_proxyModel); connect(Project::instance()->model(), &ProjectModel::loadingAboutToStart, this, &ProjectWidget::modelAboutToReload); connect(Project::instance()->model(), &ProjectModel::loadingFinished, this, &ProjectWidget::modelReloaded, Qt::QueuedConnection); setUniformRowHeights(true); setAllColumnsShowFocus(true); int widthDefaults[] = {6, 1, 1, 1, 1, 1, 1, 4, 4, 4}; //FileName, Graph, TotalCount, TranslatedCount, FuzzyCount, UntranslatedCount, IncompleteCount, SourceDate, TranslationDate, LastTranslator int i = sizeof(widthDefaults) / sizeof(int); int baseWidth = columnWidth(0); while (--i >= 0) setColumnWidth(i, baseWidth * widthDefaults[i] / 2); setSortingEnabled(true); sortByColumn(0, Qt::AscendingOrder); setSelectionMode(QAbstractItemView::ExtendedSelection); setSelectionBehavior(QAbstractItemView::SelectRows); // QTimer::singleShot(0,this,SLOT(initLater())); new HeaderViewMenuHandler(header()); KConfig config; KConfigGroup stateGroup(&config, "ProjectWindow"); header()->restoreState(QByteArray::fromBase64(stateGroup.readEntry("ListHeaderState", QByteArray()))); i = sizeof(widthDefaults) / sizeof(int); while (--i >= 0) { if (columnWidth(i) > 5 * baseWidth * widthDefaults[i]) { //The column width is more than 5 times its normal width setColumnWidth(i, 5 * baseWidth * widthDefaults[i]); } } } ProjectWidget::~ProjectWidget() { KConfig config; KConfigGroup stateGroup(&config, "ProjectWindow"); stateGroup.writeEntry("ListHeaderState", header()->saveState().toBase64()); } void ProjectWidget::modelAboutToReload() { m_currentItemPathBeforeReload = currentItem(); } void ProjectWidget::modelReloaded() { int i = 10; while (--i >= 0) { QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents | QEventLoop::ExcludeSocketNotifiers | QEventLoop::WaitForMoreEvents, 100); if (setCurrentItem(m_currentItemPathBeforeReload)) break; } if (proxyModel()->filterRegExp().pattern().size() > 2) expandItems(); } bool ProjectWidget::setCurrentItem(const QString& u) { if (u.isEmpty()) return true; QModelIndex index = m_proxyModel->mapFromSource(Project::instance()->model()->indexForUrl(QUrl::fromLocalFile(u))); if (index.isValid()) setCurrentIndex(index); return index.isValid(); } QString ProjectWidget::currentItem() const { if (!currentIndex().isValid()) return QString(); return Project::instance()->model()->itemForIndex( m_proxyModel->mapToSource(currentIndex()) ).localPath(); } bool ProjectWidget::currentIsTranslationFile() const { //remember 'bout empty state return Catalog::extIsSupported(currentItem()); } void ProjectWidget::slotItemActivated(const QModelIndex& index) { if (currentIsTranslationFile()) { ProjectModel * srcModel = static_cast(static_cast(m_proxyModel)->sourceModel()); QModelIndex srcIndex = static_cast(m_proxyModel)->mapToSource(index); QUrl fileUrl = srcModel->beginEditing(srcIndex); emit fileOpenRequested(fileUrl.toLocalFile(), !(QApplication::keyboardModifiers() & Qt::ControlModifier)); } } void ProjectWidget::recursiveAdd(QStringList& list, const QModelIndex& idx) const { if (!m_proxyModel->filterAcceptsRow(idx.row(), idx.parent())) { return; } ProjectModel& model = *(Project::instance()->model()); const KFileItem& item(model.itemForIndex(idx)); if (item.isDir()) { int j = model.rowCount(idx); while (--j >= 0) { const KFileItem& childItem(model.itemForIndex(model.index(j, 0, idx))); if (childItem.isDir()) recursiveAdd(list, model.index(j, 0, idx)); else if (m_proxyModel->filterAcceptsRow(j, idx)) list.prepend(childItem.localPath()); } } else //if (!list.contains(u)) list.prepend(item.localPath()); } QStringList ProjectWidget::selectedItems() const { QStringList list; foreach (const QModelIndex& item, selectedIndexes()) { if (item.column() == 0) recursiveAdd(list, m_proxyModel->mapToSource(item)); } return list; } void ProjectWidget::expandItems(const QModelIndex& parent) { const QAbstractItemModel* m = model(); expand(parent); int i = m->rowCount(parent); while (--i >= 0) expandItems(m->index(i, 0, parent)); } bool ProjectWidget::gotoIndexCheck(const QModelIndex& currentIndex, ProjectModel::AdditionalRoles role) { // Check if role is found for this index if (currentIndex.isValid()) { ProjectModel *srcModel = static_cast(static_cast(m_proxyModel)->sourceModel()); QModelIndex srcIndex = static_cast(m_proxyModel)->mapToSource(currentIndex); QVariant result = srcModel->data(srcIndex, role); return result.isValid() && result.toInt() > 0; } return false; } QModelIndex ProjectWidget::gotoIndexPrevNext(const QModelIndex& currentIndex, int direction) const { QModelIndex index = currentIndex; QModelIndex sibling; // Unless first or last sibling reached, continue with previous or next // sibling, otherwise continue with previous or next parent while (index.isValid()) { sibling = index.sibling(index.row() + direction, index.column()); if (sibling.isValid()) return sibling; index = index.parent(); } return index; } ProjectWidget::gotoIndexResult ProjectWidget::gotoIndexFind( const QModelIndex& currentIndex, ProjectModel::AdditionalRoles role, int direction) { QModelIndex index = currentIndex; while (index.isValid()) { // Set current index and show it if role is found for this index if (gotoIndexCheck(index, role)) { clearSelection(); setCurrentIndex(index); scrollTo(index); return gotoIndex_found; } // Handle child recursively if index is not a leaf QModelIndex child = index.model()->index((direction == 1) ? 0 : (m_proxyModel->rowCount(index) - 1), index.column(), index); if (child.isValid()) { ProjectWidget::gotoIndexResult result = gotoIndexFind(child, role, direction); if (result != gotoIndex_notfound) return result; } // Go to previous or next item index = gotoIndexPrevNext(index, direction); } if (index.parent().isValid()) return gotoIndex_notfound; else return gotoIndex_end; } ProjectWidget::gotoIndexResult ProjectWidget::gotoIndex( const QModelIndex& currentIndex, ProjectModel::AdditionalRoles role, int direction) { QModelIndex index = currentIndex; // Check if current index already found, and if so go to previous or next item if (gotoIndexCheck(index, role)) index = gotoIndexPrevNext(index, direction); return gotoIndexFind(index, role, direction); } void ProjectWidget::gotoPrevFuzzyUntr() { gotoIndex(currentIndex(), ProjectModel::FuzzyUntrCountRole, -1); } void ProjectWidget::gotoNextFuzzyUntr() { gotoIndex(currentIndex(), ProjectModel::FuzzyUntrCountRole, +1); } void ProjectWidget::gotoPrevFuzzy() { gotoIndex(currentIndex(), ProjectModel::FuzzyCountRole, -1); } void ProjectWidget::gotoNextFuzzy() { gotoIndex(currentIndex(), ProjectModel::FuzzyCountRole, +1); } void ProjectWidget::gotoPrevUntranslated() { gotoIndex(currentIndex(), ProjectModel::UntransCountRole, -1); } void ProjectWidget::gotoNextUntranslated() { gotoIndex(currentIndex(), ProjectModel::UntransCountRole, +1); } void ProjectWidget::gotoPrevTemplateOnly() { gotoIndex(currentIndex(), ProjectModel::TemplateOnlyRole, -1); } void ProjectWidget::gotoNextTemplateOnly() { gotoIndex(currentIndex(), ProjectModel::TemplateOnlyRole, +1); } void ProjectWidget::gotoPrevTransOnly() { gotoIndex(currentIndex(), ProjectModel::TransOnlyRole, -1); } void ProjectWidget::gotoNextTransOnly() { gotoIndex(currentIndex(), ProjectModel::TransOnlyRole, +1); } void ProjectWidget::toggleTranslatedFiles() { m_proxyModel->toggleTranslatedFiles(); } QSortFilterProxyModel* ProjectWidget::proxyModel() { return m_proxyModel; } diff --git a/src/tests/gettextheadertest.cpp b/src/tests/gettextheadertest.cpp index 465e577..45ac489 100644 --- a/src/tests/gettextheadertest.cpp +++ b/src/tests/gettextheadertest.cpp @@ -1,56 +1,56 @@ /* * This file is part of Lokalize * * Copyright (c) 2019 Alexander Potashev * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include "gettextheader.h" class GettextHeaderTest : public QObject { Q_OBJECT private Q_SLOTS: void testFormatDate(); }; void GettextHeaderTest::testFormatDate() { QCOMPARE(formatGettextDate( - QDateTime(QDate(2006, 8, 3), QTime(7, 31, 32), Qt::UTC)), "2006-08-03 07:31+0000"); + QDateTime(QDate(2006, 8, 3), QTime(7, 31, 32), Qt::UTC)), "2006-08-03 07:31+0000"); QCOMPARE(formatGettextDate( - QDateTime(QDate(2006, 8, 3), QTime(7, 31, 32), QTimeZone("Europe/Moscow"))), "2006-08-03 07:31+0400"); + QDateTime(QDate(2006, 8, 3), QTime(7, 31, 32), QTimeZone("Europe/Moscow"))), "2006-08-03 07:31+0400"); QCOMPARE(formatGettextDate( - QDateTime(QDate(2018, 8, 3), QTime(7, 31, 32), QTimeZone("Europe/Moscow"))), "2018-08-03 07:31+0300"); + QDateTime(QDate(2018, 8, 3), QTime(7, 31, 32), QTimeZone("Europe/Moscow"))), "2018-08-03 07:31+0300"); QCOMPARE(formatGettextDate( - QDateTime(QDate(2018, 8, 3), QTime(7, 31, 32), QTimeZone("America/New_York"))), "2018-08-03 07:31-0400"); + QDateTime(QDate(2018, 8, 3), QTime(7, 31, 32), QTimeZone("America/New_York"))), "2018-08-03 07:31-0400"); QCOMPARE(formatGettextDate( - QDateTime(QDate(2018, 8, 3), QTime(7, 31, 32), QTimeZone(-14 * 3600))), "2018-08-03 07:31-1400"); + QDateTime(QDate(2018, 8, 3), QTime(7, 31, 32), QTimeZone(-14 * 3600))), "2018-08-03 07:31-1400"); QCOMPARE(formatGettextDate( - QDateTime(QDate(2018, 8, 3), QTime(7, 31, 32), QTimeZone(14 * 3600))), "2018-08-03 07:31+1400"); + QDateTime(QDate(2018, 8, 3), QTime(7, 31, 32), QTimeZone(14 * 3600))), "2018-08-03 07:31+1400"); QCOMPARE(formatGettextDate( - QDateTime(QDate(2018, 8, 3), QTime(7, 31, 32), QTimeZone("Asia/Kolkata"))), "2018-08-03 07:31+0530"); + QDateTime(QDate(2018, 8, 3), QTime(7, 31, 32), QTimeZone("Asia/Kolkata"))), "2018-08-03 07:31+0530"); } QTEST_GUILESS_MAIN(GettextHeaderTest) #include "gettextheadertest.moc" diff --git a/src/tests/projectmodeltest.cpp b/src/tests/projectmodeltest.cpp index b601dd7..f1513b1 100644 --- a/src/tests/projectmodeltest.cpp +++ b/src/tests/projectmodeltest.cpp @@ -1,109 +1,109 @@ /* * This file is part of Lokalize * * Copyright (c) 2019 Alexander Potashev * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include "project/projectmodel.h" class ProjectModelTest : public QObject { -Q_OBJECT + Q_OBJECT private Q_SLOTS: void testInvalid(); void testHalfTranslated(); }; void ProjectModelTest::testInvalid() { QAtomicInt loaded; auto *model = new ProjectModel(this); connect(model, &ProjectModel::loadingFinished, [&loaded]() { loaded.fetchAndAddRelaxed(1); }); - connect(model, &ProjectModel::totalsChanged, [=](int fuzzy, int translated, int untranslated, bool done) { + connect(model, &ProjectModel::totalsChanged, [ = ](int fuzzy, int translated, int untranslated, bool done) { QCOMPARE(fuzzy, 0); QCOMPARE(translated, 0); QCOMPARE(untranslated, 0); QCOMPARE(done, true); }); model->setUrl(QUrl::fromLocalFile(QFINDTESTDATA("data/dir-invalid")), {}); // Wait for signal while (!loaded.load()) { QCoreApplication::processEvents(); } QCOMPARE(model->rowCount(QModelIndex()), 1); QCOMPARE(model->data(model->index(0, 0), Qt::DisplayRole), QStringLiteral("invalid.po")); QCOMPARE(model->data(model->index(0, 1), Qt::DisplayRole), QRect(0, 0, 0, 0)); QCOMPARE(model->data(model->index(0, 2), Qt::DisplayRole), 0); QCOMPARE(model->data(model->index(0, 3), Qt::DisplayRole), 0); QCOMPARE(model->data(model->index(0, 4), Qt::DisplayRole), 0); QCOMPARE(model->data(model->index(0, 5), Qt::DisplayRole), 0); QCOMPARE(model->data(model->index(0, 6), Qt::DisplayRole), 0); QCOMPARE(model->data(model->index(0, 7), Qt::DisplayRole), QString()); QCOMPARE(model->data(model->index(0, 8), Qt::DisplayRole), QString()); QCOMPARE(model->data(model->index(0, 9), Qt::DisplayRole), QString()); } void ProjectModelTest::testHalfTranslated() { QAtomicInt loaded; auto *model = new ProjectModel(this); connect(model, &ProjectModel::loadingFinished, [&loaded]() { loaded.fetchAndAddRelaxed(1); }); - connect(model, &ProjectModel::totalsChanged, [=](int fuzzy, int translated, int untranslated, bool done) { + connect(model, &ProjectModel::totalsChanged, [ = ](int fuzzy, int translated, int untranslated, bool done) { QCOMPARE(fuzzy, 1); QCOMPARE(translated, 3); QCOMPARE(untranslated, 2); QCOMPARE(done, true); }); model->setUrl(QUrl::fromLocalFile(QFINDTESTDATA("data/dir-halftranslated")), {}); // Wait for signal while (!loaded.load()) { QCoreApplication::processEvents(); } QCOMPARE(model->rowCount(QModelIndex()), 1); QCOMPARE(model->data(model->index(0, 0), Qt::DisplayRole), QStringLiteral("halftranslated.po")); QCOMPARE(model->data(model->index(0, 1), Qt::DisplayRole), QRect(3, 2, 1, 0)); QCOMPARE(model->data(model->index(0, 2), Qt::DisplayRole), 6); QCOMPARE(model->data(model->index(0, 3), Qt::DisplayRole), 3); QCOMPARE(model->data(model->index(0, 4), Qt::DisplayRole), 1); QCOMPARE(model->data(model->index(0, 5), Qt::DisplayRole), 2); QCOMPARE(model->data(model->index(0, 6), Qt::DisplayRole), 3); QCOMPARE(model->data(model->index(0, 7), Qt::DisplayRole), QStringLiteral("2019-05-20 03:26+0200")); QCOMPARE(model->data(model->index(0, 8), Qt::DisplayRole), QStringLiteral("2019-06-13 08:53+0300")); QCOMPARE(model->data(model->index(0, 9), Qt::DisplayRole), QStringLiteral("Alexander Potashev ")); } QTEST_GUILESS_MAIN(ProjectModelTest) #include "projectmodeltest.moc" diff --git a/src/xlifftextedit.cpp b/src/xlifftextedit.cpp index 4215064..ede744c 100644 --- a/src/xlifftextedit.cpp +++ b/src/xlifftextedit.cpp @@ -1,1370 +1,1362 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 2007-2014 by Nick Shaforostoff 2018-2019 by Simon Depiets This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . **************************************************************************** */ #include "xlifftextedit.h" #include "lokalize_debug.h" #include "catalog.h" #include "cmd.h" #include "syntaxhighlighter.h" #include "prefs_lokalize.h" #include "prefs.h" #include "project.h" #include "completionstorage.h" #include "languagetoolmanager.h" #include "languagetoolresultjob.h" #include "languagetoolparser.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include inline static QImage generateImage(const QString& str, const QFont& font) { // im_count++; // QTime a;a.start(); QStyleOptionButton opt; opt.fontMetrics = QFontMetrics(font); opt.text = ' ' + str + ' '; opt.rect = opt.fontMetrics.boundingRect(opt.text).adjusted(0, 0, 5, 5); opt.rect.moveTo(0, 0); QImage result(opt.rect.size(), QImage::Format_ARGB32); result.fill(0);//0xAARRGGBB QPainter painter(&result); QApplication::style()->drawControl(QStyle::CE_PushButton, &opt, &painter); // im_time+=a.elapsed(); // qCWarning(LOKALIZE_LOG)<width() + 2 * frameWidth(); return QSize(w, h); } bool MyCompletionBox::eventFilter(QObject* object, QEvent* event) { if (event->type() == QEvent::KeyPress) { QKeyEvent* e = static_cast(event); if (e->key() == Qt::Key_PageDown || e->key() == Qt::Key_PageUp) { hide(); return false; } } return KCompletionBox::eventFilter(object, event); } TranslationUnitTextEdit::~TranslationUnitTextEdit() { disconnect(document(), &QTextDocument::contentsChange, this, &TranslationUnitTextEdit::contentsChanged); } TranslationUnitTextEdit::TranslationUnitTextEdit(Catalog* catalog, DocPosition::Part part, QWidget* parent) : KTextEdit(parent) , m_currentUnicodeNumber(0) , m_langUsesSpaces(true) , m_catalog(catalog) , m_part(part) , m_highlighter(new SyntaxHighlighter(this)) , m_enabled(Settings::autoSpellcheck()) , m_completionBox(0) , m_cursorSelectionStart(0) , m_cursorSelectionEnd(0) , m_languageToolTimer(new QTimer(this)) { setReadOnly(part == DocPosition::Source); setUndoRedoEnabled(false); setAcceptRichText(false); m_highlighter->setActive(m_enabled); setHighlighter(m_highlighter); if (part == DocPosition::Target) { connect(document(), &QTextDocument::contentsChange, this, &TranslationUnitTextEdit::contentsChanged); connect(this, &KTextEdit::cursorPositionChanged, this, &TranslationUnitTextEdit::emitCursorPositionChanged); connect(m_languageToolTimer, &QTimer::timeout, this, &TranslationUnitTextEdit::launchLanguageTool); } connect(catalog, QOverload<>::of(&Catalog::signalFileLoaded), this, &TranslationUnitTextEdit::fileLoaded); //connect (Project::instance(), &Project::configChanged, this, &TranslationUnitTextEdit::projectConfigChanged); m_languageToolTimer->setSingleShot(true); } void TranslationUnitTextEdit::setSpellCheckingEnabled(bool enable) { Settings::setAutoSpellcheck(enable); m_enabled = enable; m_highlighter->setActive(enable); SettingsController::instance()->dirty = true; } void TranslationUnitTextEdit::setVisualizeSeparators(bool enable) { if (enable) { QTextOption textoption = document()->defaultTextOption(); textoption.setFlags(textoption.flags() | QTextOption::ShowLineAndParagraphSeparators | QTextOption::ShowTabsAndSpaces); document()->setDefaultTextOption(textoption); } else { QTextOption textoption = document()->defaultTextOption(); textoption.setFlags(textoption.flags() & (~QTextOption::ShowLineAndParagraphSeparators) & (~QTextOption::ShowTabsAndSpaces)); document()->setDefaultTextOption(textoption); } } void TranslationUnitTextEdit::fileLoaded() { QString langCode = m_part == DocPosition::Source ? m_catalog->sourceLangCode() : m_catalog->targetLangCode(); QLocale langLocale(langCode); // First try to use a locale name derived from the language code m_highlighter->setCurrentLanguage(langLocale.name()); //qCWarning(LOKALIZE_LOG) << "Attempting to set highlighting for " << m_part << " as " << langLocale.name(); // If that fails, try to use the language code directly if (m_highlighter->currentLanguage() != langLocale.name() || m_highlighter->currentLanguage().isEmpty()) { m_highlighter->setCurrentLanguage(langCode); //qCWarning(LOKALIZE_LOG) << "Attempting to set highlighting for " << m_part << " as " << langCode; - if (m_highlighter->currentLanguage() != langCode && langCode.length() > 2) - { + if (m_highlighter->currentLanguage() != langCode && langCode.length() > 2) { m_highlighter->setCurrentLanguage(langCode.left(2)); //qCWarning(LOKALIZE_LOG) << "Attempting to set highlighting for " << m_part << " as " << langCode.left(2); } } //qCWarning(LOKALIZE_LOG) << "Spellchecker found "<spellCheckerFound()<< " as "<currentLanguage(); //setSpellCheckingLanguage(m_highlighter->currentLanguage()); //"i use an english locale while translating kde pot files from english to hebrew" Bug #181989 Qt::LayoutDirection targetLanguageDirection = Qt::LeftToRight; static const QLocale::Language rtlLanguages[] = {QLocale::Arabic, QLocale::Hebrew, QLocale::Urdu, QLocale::Persian, QLocale::Pashto}; int i = sizeof(rtlLanguages) / sizeof(QLocale::Arabic); while (--i >= 0 && langLocale.language() != rtlLanguages[i]) ; if (i != -1) targetLanguageDirection = Qt::RightToLeft; setLayoutDirection(targetLanguageDirection); if (m_part == DocPosition::Source) return; //"Some language do not need space between words. For example Chinese." static const QLocale::Language noSpaceLanguages[] = {QLocale::Chinese}; i = sizeof(noSpaceLanguages) / sizeof(QLocale::Chinese); while (--i >= 0 && langLocale.language() != noSpaceLanguages[i]) ; m_langUsesSpaces = (i == -1); } void TranslationUnitTextEdit::reflectApprovementState() { if (m_part == DocPosition::Source || m_currentPos.entry == -1) return; bool approved = m_catalog->isApproved(m_currentPos.entry); disconnect(document(), &QTextDocument::contentsChange, this, &TranslationUnitTextEdit::contentsChanged); m_highlighter->setApprovementState(approved); m_highlighter->rehighlight(); connect(document(), &QTextDocument::contentsChange, this, &TranslationUnitTextEdit::contentsChanged); viewport()->setBackgroundRole(approved ? QPalette::Base : QPalette::AlternateBase); if (approved) emit approvedEntryDisplayed(); else emit nonApprovedEntryDisplayed(); bool untr = m_catalog->isEmpty(m_currentPos); if (untr) emit untranslatedEntryDisplayed(); else emit translatedEntryDisplayed(); } void TranslationUnitTextEdit::reflectUntranslatedState() { if (m_part == DocPosition::Source || m_currentPos.entry == -1) return; bool untr = m_catalog->isEmpty(m_currentPos); if (untr) emit untranslatedEntryDisplayed(); else emit translatedEntryDisplayed(); } /** * makes MsgEdit reflect current entry **/ CatalogString TranslationUnitTextEdit::showPos(DocPosition docPosition, const CatalogString& refStr, bool keepCursor) { docPosition.part = m_part; m_currentPos = docPosition; CatalogString catalogString = m_catalog->catalogString(m_currentPos); QString target = catalogString.string; _oldMsgstr = target; //_oldMsgstrAscii=document()->toPlainText(); <-- MOVED THIS TO THE END //BEGIN pos QTextCursor cursor = textCursor(); int pos = cursor.position(); int anchor = cursor.anchor(); //qCWarning(LOKALIZE_LOG)<<"called"<<"pos"<sourceWithTags(docPosition) : refStr); connect(document(), &QTextDocument::contentsChange, this, &TranslationUnitTextEdit::contentsChanged); _oldMsgstrAscii = document()->toPlainText(); //BEGIN pos QTextCursor t = textCursor(); t.movePosition(QTextCursor::Start); if (pos || anchor) { //qCWarning(LOKALIZE_LOG)<<"setting"<blockSignals(true); clear(); QTextCursor c = textCursor(); insertContent(c, catStr, refStr); document()->blockSignals(false); if (m_part == DocPosition::Target) m_highlighter->setSourceString(refStr.string); else //reflectApprovementState() does this for Target m_highlighter->rehighlight(); //explicitly because the signals were disabled - if (Settings::self()->languageToolDelay() > 0) - { + if (Settings::self()->languageToolDelay() > 0) { m_languageToolTimer->start(Settings::self()->languageToolDelay() * 1000); } } #if 0 struct SearchFunctor { virtual int operator()(const QString& str, int startingPos); }; int SearchFunctor::operator()(const QString& str, int startingPos) { return str.indexOf(TAGRANGE_IMAGE_SYMBOL, startingPos); } struct AlternativeSearchFunctor: public SearchFunctor { int operator()(const QString& str, int startingPos); }; int AlternativeSearchFunctor::operator()(const QString& str, int startingPos) { int tagPos = str.indexOf(TAGRANGE_IMAGE_SYMBOL, startingPos); int diffStartPos = str.indexOf("{KBABEL", startingPos); int diffEndPos = str.indexOf("{/KBABEL", startingPos); int diffPos = qMin(diffStartPos, diffEndPos); if (diffPos == -1) diffPos = qMax(diffStartPos, diffEndPos); int result = qMin(tagPos, diffPos); if (result == -1) result = qMax(tagPos, diffPos); return result; } #endif void insertContent(QTextCursor& cursor, const CatalogString& catStr, const CatalogString& refStr, bool insertText) { //settings for TMView QTextCharFormat chF = cursor.charFormat(); QFont font = cursor.document()->defaultFont(); //font.setWeight(chF.fontWeight()); QMap posToTag; int i = catStr.tags.size(); while (--i >= 0) { //qCDebug(LOKALIZE_LOG)<<"\t"< sourceTagIdToIndex = refStr.tagIdToIndex(); int refTagIndexOffset = sourceTagIdToIndex.size(); i = 0; int prev = 0; while ((i = catStr.string.indexOf(TAGRANGE_IMAGE_SYMBOL, i)) != -1) { #if 0 SearchFunctor nextStopSymbol = AlternativeSearchFunctor(); char state = '0'; while ((i = nextStopSymbol(catStr.string, i)) != -1) { //handle diff display for TMView if (catStr.string.at(i) != TAGRANGE_IMAGE_SYMBOL) { if (catStr.string.at(i + 1) == '/') state = '0'; else if (catStr.string.at(i + 8) == 'D') state = '-'; else state = '+'; continue; } #endif if (insertText) cursor.insertText(catStr.string.mid(prev, i - prev)); else { cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, i - prev); cursor.deleteChar();//delete TAGRANGE_IMAGE_SYMBOL to insert it properly } if (!posToTag.contains(i)) { prev = ++i; continue; } int tagIndex = posToTag.value(i); InlineTag tag = catStr.tags.at(tagIndex); QString name = tag.id; QString text; if (tag.type == InlineTag::mrk) text = QStringLiteral("*"); else if (!tag.equivText.isEmpty()) text = tag.equivText; //TODO add number? when? -- right now this is done for gettext qt's 156 mark else text = QString::number(sourceTagIdToIndex.contains(tag.id) ? sourceTagIdToIndex.value(tag.id) : (tagIndex + refTagIndexOffset)); if (tag.start != tag.end) { //qCWarning(LOKALIZE_LOG)<<"b"<resource(QTextDocument::ImageResource, QUrl(name)).isNull()) cursor.document()->addResource(QTextDocument::ImageResource, QUrl(name), generateImage(text, font)); cursor.insertImage(name);//NOTE what if twice the same name? cursor.setCharFormat(chF); prev = ++i; } cursor.insertText(catStr.string.mid(prev)); } void TranslationUnitTextEdit::contentsChanged(int offset, int charsRemoved, int charsAdded) { Q_ASSERT(m_catalog->targetLangCode().length()); Q_ASSERT(Project::instance()->targetLangCode().length()); //qCWarning(LOKALIZE_LOG)<<"contentsChanged. offset"<toPlainText(); if (editTextAscii == _oldMsgstrAscii) { //qCWarning(LOKALIZE_LOG)<<"stopping"<targetWithTags(pos).string; const QStringRef addedText = editText.midRef(offset, charsAdded); //BEGIN XLIFF markup handling //protect from tag removal //TODO use midRef when Qt 4.8 is in distros bool markupRemoved = charsRemoved && target.midRef(offset, charsRemoved).contains(TAGRANGE_IMAGE_SYMBOL); bool markupAdded = charsAdded && addedText.contains(TAGRANGE_IMAGE_SYMBOL); if (markupRemoved || markupAdded) { bool modified = false; CatalogString targetWithTags = m_catalog->targetWithTags(m_currentPos); //special case when the user presses Del w/o selection if (!charsAdded && charsRemoved == 1) { int i = targetWithTags.tags.size(); while (--i >= 0) { if (targetWithTags.tags.at(i).start == offset || targetWithTags.tags.at(i).end == offset) { modified = true; pos.offset = targetWithTags.tags.at(i).start; m_catalog->push(new DelTagCmd(m_catalog, pos)); } } } else if (!markupAdded) { //check if all { plus } tags were selected modified = removeTargetSubstring(offset, charsRemoved, /*refresh*/false); if (modified && charsAdded) m_catalog->push(new InsTextCmd(m_catalog, pos, addedText.toString())); } //qCWarning(LOKALIZE_LOG)<<"calling showPos"; showPos(m_currentPos, CatalogString(),/*keepCursor*/true); if (!modified) { //qCWarning(LOKALIZE_LOG)<<"stop"; return; } } //END XLIFF markup handling else { if (charsRemoved) m_catalog->push(new DelTextCmd(m_catalog, pos, _oldMsgstr.mid(offset, charsRemoved))); _oldMsgstr = editText; //newStr becomes OldStr _oldMsgstrAscii = editTextAscii; //qCWarning(LOKALIZE_LOG)<<"char"<push(new InsTextCmd(m_catalog, pos, addedText.toString())); } /* TODO if (_leds) { if (m_catalog->msgstr(pos).isEmpty()) _leds->ledUntr->on(); else _leds->ledUntr->off(); } */ requestToggleApprovement(); reflectUntranslatedState(); // for mergecatalog (remove entry from index) // and for statusbar emit contentsModified(m_currentPos); if (charsAdded == 1) { int sp = target.lastIndexOf(CompletionStorage::instance()->rxSplit, offset - 1); int len = (offset - sp); int wordCompletionLength = Settings::self()->wordCompletionLength(); if (wordCompletionLength >= 3 && len >= wordCompletionLength) doCompletion(offset + 1); else if (m_completionBox) m_completionBox->hide(); } else if (m_completionBox) m_completionBox->hide(); //qCWarning(LOKALIZE_LOG)<<"finish"; //Start LanguageToolTimer - if (Settings::self()->languageToolDelay() > 0) - { + if (Settings::self()->languageToolDelay() > 0) { m_languageToolTimer->start(Settings::self()->languageToolDelay() * 1000); } } bool TranslationUnitTextEdit::removeTargetSubstring(int delStart, int delLen, bool refresh) { if (Q_UNLIKELY(m_currentPos.entry == -1)) return false; if (!::removeTargetSubstring(m_catalog, m_currentPos, delStart, delLen)) return false; requestToggleApprovement(); if (refresh) { //qCWarning(LOKALIZE_LOG)<<"calling showPos"; showPos(m_currentPos, CatalogString(),/*keepCursor*/true/*false*/); } emit contentsModified(m_currentPos.entry); return true; } void TranslationUnitTextEdit::insertCatalogString(CatalogString catStr, int start, bool refresh) { QString REMOVEME = QStringLiteral("REMOVEME"); CatalogString sourceForReferencing = m_catalog->sourceWithTags(m_currentPos); const CatalogString target = m_catalog->targetWithTags(m_currentPos); QHash id2tagIndex; int i = sourceForReferencing.tags.size(); while (--i >= 0) id2tagIndex.insert(sourceForReferencing.tags.at(i).id, i); //remove markup that is already in target, to avoid duplicates if the string being inserted contains it as well foreach (const InlineTag& tag, target.tags) { if (id2tagIndex.contains(tag.id)) sourceForReferencing.tags[id2tagIndex.value(tag.id)].id = REMOVEME; } //iterating from the end is essential i = sourceForReferencing.tags.size(); while (--i >= 0) if (sourceForReferencing.tags.at(i).id == REMOVEME) sourceForReferencing.tags.removeAt(i); adaptCatalogString(catStr, sourceForReferencing); ::insertCatalogString(m_catalog, m_currentPos, catStr, start); if (refresh) { //qCWarning(LOKALIZE_LOG)<<"calling showPos"; showPos(m_currentPos, CatalogString(),/*keepCursor*/true/*false*/); QTextCursor cursor = textCursor(); cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, catStr.string.size()); setTextCursor(cursor); } } const QString LOKALIZE_XLIFF_MIMETYPE = QStringLiteral("application/x-lokalize-xliff+xml"); QMimeData* TranslationUnitTextEdit::createMimeDataFromSelection() const { QMimeData *mimeData = new QMimeData; CatalogString catalogString = m_catalog->catalogString(m_currentPos); QTextCursor cursor = textCursor(); int start = qMin(cursor.anchor(), cursor.position()); int end = qMax(cursor.anchor(), cursor.position()); QMap tagPlaces; if (fillTagPlaces(tagPlaces, catalogString, start, end - start)) { //transform CatalogString //TODO substring method catalogString.string = catalogString.string.mid(start, end - start); QList::iterator it = catalogString.tags.begin(); while (it != catalogString.tags.end()) { if (!tagPlaces.contains(it->start)) it = catalogString.tags.erase(it); else { it->start -= start; it->end -= start; ++it; } } QByteArray a; QDataStream out(&a, QIODevice::WriteOnly); QVariant v; v.setValue(catalogString); out << v; mimeData->setData(LOKALIZE_XLIFF_MIMETYPE, a); } QString text = catalogString.string; text.remove(TAGRANGE_IMAGE_SYMBOL); mimeData->setText(text); return mimeData; } void TranslationUnitTextEdit::dragEnterEvent(QDragEnterEvent * event) { QObject* dragSource = event->source(); if (dragSource->objectName().compare("qt_scrollarea_viewport") == 0) dragSource = dragSource->parent(); //This is a deplacement within the Target area if (m_part == DocPosition::Target && this == dragSource) { QTextCursor cursor = textCursor(); int start = qMin(cursor.anchor(), cursor.position()); int end = qMax(cursor.anchor(), cursor.position()); m_cursorSelectionEnd = end; m_cursorSelectionStart = start; } QTextEdit::dragEnterEvent(event); } void TranslationUnitTextEdit::dropEvent(QDropEvent * event) { //Ensure the cursor moves to the correct location if (m_part == DocPosition::Target) { setTextCursor(cursorForPosition(event->pos())); //This is a copy modifier, disable the selection flags if (event->keyboardModifiers() & Qt::ControlModifier) { m_cursorSelectionEnd = 0; m_cursorSelectionStart = 0; } } QTextEdit::dropEvent(event); } void TranslationUnitTextEdit::insertFromMimeData(const QMimeData * source) { if (m_part == DocPosition::Source) return; if (source->hasFormat(LOKALIZE_XLIFF_MIMETYPE)) { //qCWarning(LOKALIZE_LOG)<<"has"; QVariant v; QByteArray data = source->data(LOKALIZE_XLIFF_MIMETYPE); QDataStream in(&data, QIODevice::ReadOnly); in >> v; //qCWarning(LOKALIZE_LOG)<<"ins"<(v).string<(v).ranges.size(); int start = 0; m_catalog->beginMacro(i18nc("@item Undo action item", "Insert text with markup")); QTextCursor cursor = textCursor(); if (cursor.hasSelection()) { start = qMin(cursor.anchor(), cursor.position()); int end = qMax(cursor.anchor(), cursor.position()); removeTargetSubstring(start, end - start); cursor.setPosition(start); setTextCursor(cursor); } else //sets right cursor position implicitly -- needed for mouse paste { QMimeData mimeData; mimeData.setText(QString()); if (m_cursorSelectionEnd != m_cursorSelectionStart) { int oldCursorPosition = textCursor().position(); removeTargetSubstring(m_cursorSelectionStart, m_cursorSelectionEnd - m_cursorSelectionStart); if (oldCursorPosition >= m_cursorSelectionEnd) { cursor.setPosition(oldCursorPosition - (m_cursorSelectionEnd - m_cursorSelectionStart)); setTextCursor(cursor); } } KTextEdit::insertFromMimeData(&mimeData); start = textCursor().position(); } insertCatalogString(v.value(), start); m_catalog->endMacro(); } else { QString text = source->text(); text.remove(TAGRANGE_IMAGE_SYMBOL); insertPlainTextWithCursorCheck(text); } } static bool isMasked(const QString & str, uint col) { if (col == 0 || str.isEmpty()) return false; uint counter = 0; int pos = col; while (pos >= 0 && str.at(pos) == '\\') { counter++; pos--; } return !(bool)(counter % 2); } void TranslationUnitTextEdit::keyPressEvent(QKeyEvent * keyEvent) { QString spclChars = QStringLiteral("abfnrtv'?\\"); if (keyEvent->matches(QKeySequence::MoveToPreviousPage)) emit gotoPrevRequested(); else if (keyEvent->matches(QKeySequence::MoveToNextPage)) emit gotoNextRequested(); else if (keyEvent->matches(QKeySequence::Undo)) emit undoRequested(); else if (keyEvent->matches(QKeySequence::Redo)) emit redoRequested(); else if (keyEvent->matches(QKeySequence::Find)) emit findRequested(); else if (keyEvent->matches(QKeySequence::FindNext)) emit findNextRequested(); else if (keyEvent->matches(QKeySequence::Replace)) emit replaceRequested(); else if (keyEvent->modifiers() == (Qt::AltModifier | Qt::ControlModifier)) { if (keyEvent->key() == Qt::Key_Home) emit gotoFirstRequested(); else if (keyEvent->key() == Qt::Key_End) emit gotoLastRequested(); } else if (keyEvent->matches(QKeySequence::MoveToNextLine) || keyEvent->matches(QKeySequence::MoveToPreviousLine)) { //static QTime lastUpDownPress; //if (lastUpDownPress.msecsTo(QTime::currentTime())<500) { keyEvent->setAccepted(true); bool up = keyEvent->key() == Qt::Key_Up; QTextCursor c = textCursor(); if (!c.movePosition(up ? QTextCursor::Up : QTextCursor::Down)) { QTextCursor::MoveOperation op; if (up && !c.atStart()) op = QTextCursor::Start; else if (!up && !c.atEnd()) op = QTextCursor::End; else if (up) { emit gotoPrevRequested(); op = QTextCursor::End; } else { emit gotoNextRequested(); op = QTextCursor::Start; } c.movePosition(op); } setTextCursor(c); } //lastUpDownPress=QTime::currentTime(); } else if (m_part == DocPosition::Source) return KTextEdit::keyPressEvent(keyEvent); //BEGIN GENERAL // ALT+123 feature TODO this is general so should be on another level else if ((keyEvent->modifiers()&Qt::AltModifier) && !keyEvent->text().isEmpty() && keyEvent->text().at(0).isDigit()) { QString text = keyEvent->text(); while (!text.isEmpty() && text.at(0).isDigit()) { m_currentUnicodeNumber = 10 * m_currentUnicodeNumber + (text.at(0).digitValue()); text.remove(0, 1); } KTextEdit::keyPressEvent(keyEvent); } //END GENERAL else if (!keyEvent->modifiers() && (keyEvent->key() == Qt::Key_Backspace || keyEvent->key() == Qt::Key_Delete)) { //only for cases when: //-BkSpace was hit and cursor was atStart //-Del was hit and cursor was atEnd if (Q_UNLIKELY(!m_catalog->isApproved(m_currentPos.entry) && !textCursor().hasSelection()) && ((textCursor().atStart() && keyEvent->key() == Qt::Key_Backspace) || (textCursor().atEnd() && keyEvent->key() == Qt::Key_Delete))) requestToggleApprovement(); else KTextEdit::keyPressEvent(keyEvent); } else if (keyEvent->key() == Qt::Key_Space && (keyEvent->modifiers()&Qt::AltModifier)) insertPlainTextWithCursorCheck(QChar(0x00a0U)); else if (keyEvent->key() == Qt::Key_Minus && (keyEvent->modifiers()&Qt::AltModifier)) insertPlainTextWithCursorCheck(QChar(0x0000AD)); //BEGIN clever editing else if (keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter) { if (m_completionBox && m_completionBox->isVisible()) { if (m_completionBox->currentItem()) completionActivated(m_completionBox->currentItem()->text()); else qCWarning(LOKALIZE_LOG) << "avoided a crash. a case for bug 238835!"; m_completionBox->hide(); return; } if (m_catalog->type() != Gettext) return KTextEdit::keyPressEvent(keyEvent); QString str = toPlainText(); QTextCursor t = textCursor(); int pos = t.position(); QString ins; if (keyEvent->modifiers()&Qt::ShiftModifier) { if (pos > 0 && !str.isEmpty() && str.at(pos - 1) == QLatin1Char('\\') && !isMasked(str, pos - 1)) { ins = 'n'; } else { ins = QStringLiteral("\\n"); } } else if (!(keyEvent->modifiers()&Qt::ControlModifier)) { if (m_langUsesSpaces && pos > 0 && !str.isEmpty() && !str.at(pos - 1).isSpace()) { if (str.at(pos - 1) == QLatin1Char('\\') && !isMasked(str, pos - 1)) ins = QLatin1Char('\\'); // if there is no new line at the end if (pos < 2 || str.midRef(pos - 2, 2) != QLatin1String("\\n")) ins += QLatin1Char(' '); } else if (str.isEmpty()) { ins = QStringLiteral("\\n"); } } if (!str.isEmpty()) { ins += '\n'; insertPlainTextWithCursorCheck(ins); } else KTextEdit::keyPressEvent(keyEvent); } else if (m_catalog->type() != Gettext) KTextEdit::keyPressEvent(keyEvent); else if ((keyEvent->modifiers()&Qt::ControlModifier) ? (keyEvent->key() == Qt::Key_D) : (keyEvent->key() == Qt::Key_Delete) && textCursor().atEnd()) { qCWarning(LOKALIZE_LOG) << "workaround for Qt/X11 bug"; QTextCursor t = textCursor(); if (!t.hasSelection()) { int pos = t.position(); QString str = toPlainText(); //workaround for Qt/X11 bug: if Del on NumPad is pressed, then pos is beyond end if (pos == str.size()) --pos; if (!str.isEmpty() && str.at(pos) == '\\' && !isMasked(str, pos) && pos < str.length() - 1 && spclChars.contains(str.at(pos + 1))) { t.deleteChar(); } } t.deleteChar(); setTextCursor(t); } else if ((!keyEvent->modifiers() && keyEvent->key() == Qt::Key_Backspace) || ((keyEvent->modifiers() & Qt::ControlModifier) && keyEvent->key() == Qt::Key_H)) { QTextCursor t = textCursor(); if (!t.hasSelection()) { int pos = t.position(); QString str = toPlainText(); if (!str.isEmpty() && pos > 0 && spclChars.contains(str.at(pos - 1))) { if (pos > 1 && str.at(pos - 2) == QLatin1Char('\\') && !isMasked(str, pos - 2)) { t.deletePreviousChar(); t.deletePreviousChar(); setTextCursor(t); //qCWarning(LOKALIZE_LOG)<<"set-->"<key() == Qt::Key_Tab) insertPlainTextWithCursorCheck(QStringLiteral("\\t")); else KTextEdit::keyPressEvent(keyEvent); //END clever editing } void TranslationUnitTextEdit::keyReleaseEvent(QKeyEvent * e) { if ((e->key() == Qt::Key_Alt) && m_currentUnicodeNumber >= 32) { insertPlainTextWithCursorCheck(QChar(m_currentUnicodeNumber)); m_currentUnicodeNumber = 0; } else KTextEdit::keyReleaseEvent(e); } void TranslationUnitTextEdit::insertPlainTextWithCursorCheck(const QString & text) { insertPlainText(text); KTextEdit::ensureCursorVisible(); } QString TranslationUnitTextEdit::toPlainText() { QTextCursor cursor = textCursor(); cursor.select(QTextCursor::Document); QString text = cursor.selectedText(); text.replace(QChar(8233), '\n'); /* int ii=text.size(); while(--ii>=0) qCWarning(LOKALIZE_LOG)<push(new InsTagCmd(m_catalog, currentPos(), tag)); showPos(currentPos(), CatalogString(),/*keepCursor*/true); cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, tag.end + 1 + tag.isPaired()); setFocus(); } int TranslationUnitTextEdit::strForMicePosIfUnderTag(QPoint mice, CatalogString & str, bool tryHarder) { if (m_currentPos.entry == -1) return -1; QTextCursor cursor = cursorForPosition(mice); int pos = cursor.position(); str = m_catalog->catalogString(m_currentPos); if (pos == -1 || pos >= str.string.size()) return -1; //qCWarning(LOKALIZE_LOG)<<"here1"<0) // { // cursor.movePosition(QTextCursor::Left); // mice.setX(mice.x()+cursorRect(cursor).width()/2); // pos=cursorForPosition(mice).position(); // } if (str.string.at(pos) != TAGRANGE_IMAGE_SYMBOL) { bool cont = tryHarder && --pos >= 0 && str.string.at(pos) == TAGRANGE_IMAGE_SYMBOL; if (!cont) return -1; } int result = str.tags.size(); while (--result >= 0 && str.tags.at(result).start != pos && str.tags.at(result).end != pos) ; return result; } void TranslationUnitTextEdit::mouseReleaseEvent(QMouseEvent * event) { if (event->button() == Qt::LeftButton) { CatalogString str; int pos = strForMicePosIfUnderTag(event->pos(), str); if (pos != -1 && m_part == DocPosition::Source) { emit tagInsertRequested(str.tags.at(pos)); event->accept(); return; } } KTextEdit::mouseReleaseEvent(event); } void TranslationUnitTextEdit::contextMenuEvent(QContextMenuEvent * event) { CatalogString str; int pos = strForMicePosIfUnderTag(event->pos(), str); if (pos != -1) { QString xid = str.tags.at(pos).xid; if (!xid.isEmpty()) { QMenu menu; int entry = m_catalog->unitById(xid); /* QAction* findUnit=menu.addAction(entry>=m_catalog->numberOfEntries()? i18nc("@action:inmenu","Show the binary unit"): i18nc("@action:inmenu","Go to the referenced entry")); */ QAction* result = menu.exec(event->globalPos()); if (result) { if (entry >= m_catalog->numberOfEntries()) emit binaryUnitSelectRequested(xid); else emit gotoEntryRequested(DocPosition(entry)); event->accept(); } return; } } if (textCursor().hasSelection() && m_part == DocPosition::Target) { QMenu menu; menu.addAction(i18nc("@action:inmenu", "Lookup selected text in translation memory")); if (menu.exec(event->globalPos())) emit tmLookupRequested(m_part, textCursor().selectedText()); return; } if (m_part != DocPosition::Source && m_part != DocPosition::Target) return; KTextEdit::contextMenuEvent(event); #if 0 QTextCursor wordSelectCursor = cursorForPosition(event->pos()); wordSelectCursor.select(QTextCursor::WordUnderCursor); if (m_highlighter->isWordMisspelled(wordSelectCursor.selectedText())) { QMenu menu; QMenu suggestions; foreach (const QString& s, m_highlighter->suggestionsForWord(wordSelectCursor.selectedText())) suggestions.addAction(s); if (!suggestions.isEmpty()) { QAction* answer = suggestions.exec(event->globalPos()); if (answer) { m_catalog->beginMacro(i18nc("@item Undo action item", "Replace text")); wordSelectCursor.insertText(answer->text()); m_catalog->endMacro(); } } } #endif // QMenu menu; // QAction* spellchecking=menu.addAction(); // event->accept(); } - void TranslationUnitTextEdit::zoomRequestedSlot(qreal fontSize) - { + void TranslationUnitTextEdit::zoomRequestedSlot(qreal fontSize) { QFont curFont = font(); curFont.setPointSizeF(fontSize); setFont(curFont); } void TranslationUnitTextEdit::wheelEvent(QWheelEvent * event) { //Override default KTextEdit behavior which ignores Ctrl+wheelEvent when the field is not ReadOnly (i/o zooming) if (m_part == DocPosition::Target && !Settings::mouseWheelGo() && (event->modifiers() == Qt::ControlModifier)) { float delta = event->angleDelta().y() / 120.f; zoomInF(delta); //Also zoom in the source emit zoomRequested(font().pointSizeF()); return; } - if (m_part == DocPosition::Source || !Settings::mouseWheelGo()) - { - if (event->modifiers() == Qt::ControlModifier) - { + if (m_part == DocPosition::Source || !Settings::mouseWheelGo()) { + if (event->modifiers() == Qt::ControlModifier) { float delta = event->angleDelta().y() / 120.f; zoomInF(delta); //Also zoom in the target emit zoomRequested(font().pointSizeF()); return; } return KTextEdit::wheelEvent(event); } switch (event->modifiers()) { case Qt::ControlModifier: if (event->angleDelta().y() > 0) emit gotoPrevFuzzyRequested(); else emit gotoNextFuzzyRequested(); break; case Qt::AltModifier: if (event->angleDelta().y() > 0) emit gotoPrevUntranslatedRequested(); else emit gotoNextUntranslatedRequested(); break; case Qt::ControlModifier + Qt::ShiftModifier: if (event->angleDelta().y() > 0) emit gotoPrevFuzzyUntrRequested(); else emit gotoNextFuzzyUntrRequested(); break; case Qt::ShiftModifier: return KTextEdit::wheelEvent(event); default: if (event->angleDelta().y() > 0) emit gotoPrevRequested(); else emit gotoNextRequested(); } } void TranslationUnitTextEdit::spellReplace() { QTextCursor wordSelectCursor = textCursor(); wordSelectCursor.select(QTextCursor::WordUnderCursor); if (!m_highlighter->isWordMisspelled(wordSelectCursor.selectedText())) return; const QStringList& suggestions = m_highlighter->suggestionsForWord(wordSelectCursor.selectedText()); if (suggestions.isEmpty()) return; m_catalog->beginMacro(i18nc("@item Undo action item", "Replace text")); wordSelectCursor.insertText(suggestions.first()); m_catalog->endMacro(); } bool TranslationUnitTextEdit::event(QEvent * event) { #ifdef Q_OS_MAC if (event->type() == QEvent::InputMethod) { QInputMethodEvent* e = static_cast(event); insertPlainTextWithCursorCheck(e->commitString()); e->accept(); return true; } #endif if (event->type() == QEvent::ToolTip) { QHelpEvent *helpEvent = static_cast(event); CatalogString str; int pos = strForMicePosIfUnderTag(helpEvent->pos(), str, true); if (pos != -1) { QString tooltip = str.tags.at(pos).displayName(); QToolTip::showText(helpEvent->globalPos(), tooltip); return true; } QString tip; QString langCode = m_highlighter->currentLanguage(); //qCWarning(LOKALIZE_LOG) << "Spellchecker found "<spellCheckerFound()<< " as "<currentLanguage(); bool nospell = langCode.isEmpty(); if (nospell) langCode = m_part == DocPosition::Source ? m_catalog->sourceLangCode() : m_catalog->targetLangCode(); QLocale l(langCode); if (l.language() != QLocale::C) tip = l.nativeLanguageName() + QLatin1String(" ("); tip += langCode; if (l.language() != QLocale::C) tip += ')'; if (nospell) tip += QLatin1String(" - ") + i18n("no spellcheck available"); QToolTip::showText(helpEvent->globalPos(), tip); } return KTextEdit::event(event); } - void TranslationUnitTextEdit::slotLanguageToolFinished(const QString &result) - { + void TranslationUnitTextEdit::slotLanguageToolFinished(const QString & result) { LanguageToolParser parser; const QJsonDocument doc = QJsonDocument::fromJson(result.toUtf8()); const QJsonObject fields = doc.object(); emit languageToolChanged(parser.parseResult(fields, toPlainText())); } - void TranslationUnitTextEdit::slotLanguageToolError(const QString &str) - { + void TranslationUnitTextEdit::slotLanguageToolError(const QString & str) { emit languageToolChanged(i18n("An error was reported: %1", str)); } void TranslationUnitTextEdit::launchLanguageTool() { if (toPlainText().length() == 0) return; LanguageToolResultJob *job = new LanguageToolResultJob(this); job->setUrl(LanguageToolManager::self()->languageToolCheckPath()); job->setNetworkAccessManager(LanguageToolManager::self()->networkAccessManager()); job->setText(toPlainText().toHtmlEscaped().replace(QStringLiteral("%"), QStringLiteral("%25"))); job->setLanguage(m_catalog->targetLangCode()); connect(job, &LanguageToolResultJob::finished, this, &TranslationUnitTextEdit::slotLanguageToolFinished); connect(job, &LanguageToolResultJob::error, this, &TranslationUnitTextEdit::slotLanguageToolError); job->start(); } void TranslationUnitTextEdit::tagMenu() { doTag(false); } void TranslationUnitTextEdit::tagImmediate() { doTag(true); } void TranslationUnitTextEdit::doTag(bool immediate) { QMenu menu; QAction* txt = 0; CatalogString sourceWithTags = m_catalog->sourceWithTags(m_currentPos); int count = sourceWithTags.tags.size(); if (count) { QMap tagIdToIndex = m_catalog->targetWithTags(m_currentPos).tagIdToIndex(); bool hasActive = false; for (int i = 0; i < count; ++i) { //txt=menu.addAction(sourceWithTags.ranges.at(i)); txt = menu.addAction(QString::number(i)/*+" "+sourceWithTags.ranges.at(i).id*/); txt->setData(QVariant(i)); if (!hasActive && !tagIdToIndex.contains(sourceWithTags.tags.at(i).id)) { if (immediate) { insertTag(sourceWithTags.tags.at(txt->data().toInt())); return; } hasActive = true; menu.setActiveAction(txt); } } if (immediate) return; txt = menu.exec(mapToGlobal(cursorRect().bottomRight())); if (!txt) return; insertTag(sourceWithTags.tags.at(txt->data().toInt())); } else { if (Q_UNLIKELY(Project::instance()->markup().isEmpty())) return; //QRegExp tag("(<[^>]*>)+|\\&\\w+\\;"); QRegExp tag(Project::instance()->markup()); tag.setMinimal(true); QString en = m_catalog->sourceWithTags(m_currentPos).string; QString target(toPlainText()); en.remove('\n'); target.remove('\n'); int pos = 0; //tag.indexIn(en); int posInMsgStr = 0; while ((pos = tag.indexIn(en, pos)) != -1) { /* QString str(tag.cap(0)); str.replace("&","&&");*/ txt = menu.addAction(tag.cap(0)); pos += tag.matchedLength(); if (posInMsgStr != -1 && (posInMsgStr = target.indexOf(tag.cap(0), posInMsgStr)) == -1) { if (immediate) { insertPlainTextWithCursorCheck(txt->text()); return; } menu.setActiveAction(txt); } else if (posInMsgStr != -1) posInMsgStr += tag.matchedLength(); } if (!txt || immediate) return; //txt=menu.exec(_msgidEdit->mapToGlobal(QPoint(0,0))); txt = menu.exec(mapToGlobal(cursorRect().bottomRight())); if (txt) insertPlainTextWithCursorCheck(txt->text()); } } void TranslationUnitTextEdit::source2target() { CatalogString sourceWithTags = m_catalog->sourceWithTags(m_currentPos); QString text = sourceWithTags.string; QString out; QString ctxt = m_catalog->context(m_currentPos.entry).first(); QRegExp delimiter(QStringLiteral("\\s*,\\s*")); //TODO ask for the fillment if the first time. //BEGIN KDE specific part if (ctxt.startsWith(QLatin1String("NAME OF TRANSLATORS")) || text.startsWith(QLatin1String("_: NAME OF TRANSLATORS\\n"))) { if (!document()->toPlainText().split(delimiter).contains(Settings::authorLocalizedName())) { if (!document()->isEmpty()) out = QLatin1String(", "); out += Settings::authorLocalizedName(); } } else if (ctxt.startsWith(QLatin1String("EMAIL OF TRANSLATORS")) || text.startsWith(QLatin1String("_: EMAIL OF TRANSLATORS\\n"))) { if (!document()->toPlainText().split(delimiter).contains(Settings::authorEmail())) { if (!document()->isEmpty()) out = QLatin1String(", "); out += Settings::authorEmail(); } } else if (/*_catalog->isGeneratedFromDocbook() &&*/ text.startsWith(QLatin1String("ROLES_OF_TRANSLATORS"))) { if (!document()->isEmpty()) out = '\n'; out += QLatin1String("\n" "\n" "
    ") + Settings::authorEmail() + QLatin1String("
    \n" "
    "); } else if (text.startsWith(QLatin1String("CREDIT_FOR_TRANSLATORS"))) { if (!document()->isEmpty()) out = '\n'; out += QLatin1String("") + Settings::authorLocalizedName() + '\n' + QLatin1String("") + Settings::authorEmail() + QLatin1String(""); } //END KDE specific part else { m_catalog->beginMacro(i18nc("@item Undo action item", "Copy source to target")); removeTargetSubstring(0, -1,/*refresh*/false); insertCatalogString(sourceWithTags, 0,/*refresh*/false); m_catalog->endMacro(); showPos(m_currentPos, sourceWithTags,/*keepCursor*/false); requestToggleApprovement(); } if (!out.isEmpty()) { QTextCursor t = textCursor(); t.movePosition(QTextCursor::End); t.insertText(out); setTextCursor(t); } } void TranslationUnitTextEdit::requestToggleApprovement() { if (m_catalog->isApproved(m_currentPos.entry) || !Settings::autoApprove()) return; bool skip = m_catalog->isPlural(m_currentPos); if (skip) { skip = false; DocPos pos(m_currentPos); for (pos.form = 0; pos.form < m_catalog->numberOfPluralForms(); ++(pos.form)) skip = skip || !m_catalog->isModified(pos); } if (!skip) emit toggleApprovementRequested(); } void TranslationUnitTextEdit::cursorToStart() { QTextCursor t = textCursor(); t.movePosition(QTextCursor::Start); setTextCursor(t); } void TranslationUnitTextEdit::doCompletion(int pos) { QString target = m_catalog->targetWithTags(m_currentPos).string; int sp = target.lastIndexOf(CompletionStorage::instance()->rxSplit, pos - 1); int len = (pos - sp) - 1; QStringList s = CompletionStorage::instance()->makeCompletion(QString::fromRawData(target.unicode() + sp + 1, len)); if (!m_completionBox) { //BEGIN creation m_completionBox = new MyCompletionBox(this); connect(m_completionBox, &MyCompletionBox::activated, this, &TranslationUnitTextEdit::completionActivated); m_completionBox->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred); //END creation } m_completionBox->setItems(s); if (s.size() && !s.first().isEmpty()) { m_completionBox->setCurrentRow(0); //qApp->removeEventFilter( m_completionBox ); if (!m_completionBox->isVisible()) //NOTE remove the check if kdelibs gets adapted m_completionBox->show(); m_completionBox->resize(m_completionBox->sizeHint()); QPoint p = cursorRect().bottomRight(); if (p.x() < 10) //workaround Qt bug p.rx() += textCursor().verticalMovementX() + QFontMetrics(currentFont()).horizontalAdvance('W'); m_completionBox->move(viewport()->mapToGlobal(p)); } else m_completionBox->hide(); } void TranslationUnitTextEdit::doExplicitCompletion() { doCompletion(textCursor().anchor()); } void TranslationUnitTextEdit::completionActivated(const QString & semiWord) { QTextCursor cursor = textCursor(); cursor.insertText(semiWord); setTextCursor(cursor); }