diff --git a/src/alttransview.cpp b/src/alttransview.cpp index 85b446e..adbf459 100644 --- a/src/alttransview.cpp +++ b/src/alttransview.cpp @@ -1,311 +1,307 @@ /* **************************************************************************** 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 "alttransview.h" #include "lokalize_debug.h" #include "diff.h" #include "catalog.h" #include "cmd.h" #include "project.h" #include "xlifftextedit.h" #include "tmview.h" //TextBrowser #include "mergecatalog.h" #include "prefs_lokalize.h" #include #include #include -#include #include #include #include #include #include #include AltTransView::AltTransView(QWidget* parent, Catalog* catalog, const QVector& actions) : QDockWidget(i18nc("@title:window", "Alternate Translations"), parent) , m_browser(new TM::TextBrowser(this)) , m_catalog(catalog) , m_normTitle(i18nc("@title:window", "Alternate Translations")) , m_hasInfoTitle(m_normTitle + QStringLiteral(" [*]")) , m_hasInfo(false) , m_everShown(false) , m_actions(actions) { setObjectName(QStringLiteral("msgIdDiff")); setWidget(m_browser); hide(); m_browser->setReadOnly(true); m_browser->viewport()->setBackgroundRole(QPalette::Background); QTimer::singleShot(0, this, &AltTransView::initLater); } void AltTransView::initLater() { setAcceptDrops(true); KConfig config; KConfigGroup group(&config, "AltTransView"); m_everShown = group.readEntry("EverShown", false); - QSignalMapper* signalMapper = new QSignalMapper(this); int i = m_actions.size(); while (--i >= 0) { - connect(m_actions.at(i), &QAction::triggered, signalMapper, QOverload<>::of(&QSignalMapper::map)); - signalMapper->setMapping(m_actions.at(i), i); + connect(m_actions.at(i), &QAction::triggered, this, [this, i] { slotUseSuggestion(i); }); } - connect(signalMapper, QOverload::of(&QSignalMapper::mapped), this, &AltTransView::slotUseSuggestion); connect(m_browser, &TM::TextBrowser::textInsertRequested, this, &AltTransView::textInsertRequested); //connect(m_browser, &TM::TextBrowser::customContextMenuRequested, this, &AltTransView::contextMenu); } AltTransView::~AltTransView() { } void AltTransView::dragEnterEvent(QDragEnterEvent* event) { if (event->mimeData()->hasUrls() && Catalog::extIsSupported(event->mimeData()->urls().first().path())) event->acceptProposedAction(); } void AltTransView::dropEvent(QDropEvent *event) { event->acceptProposedAction(); attachAltTransFile(event->mimeData()->urls().first().toLocalFile()); //update m_prevEntry.entry = -1; QTimer::singleShot(0, this, &AltTransView::process); } void AltTransView::attachAltTransFile(const QString& path) { MergeCatalog* altCat = new MergeCatalog(m_catalog, m_catalog, /*saveChanges*/false); altCat->loadFromUrl(path); m_catalog->attachAltTransCatalog(altCat); } void AltTransView::addAlternateTranslation(int entry, const QString& trans) { AltTrans altTrans; altTrans.target = trans; m_catalog->attachAltTrans(entry, altTrans); m_prevEntry = DocPos(); QTimer::singleShot(0, this, &AltTransView::process); } void AltTransView::fileLoaded() { m_prevEntry.entry = -1; QString absPath = m_catalog->url(); QString relPath = QDir(Project::instance()->projectDir()).relativeFilePath(absPath); QFileInfo info(Project::instance()->altTransDir() % '/' % relPath); if (info.canonicalFilePath() != absPath && info.exists()) attachAltTransFile(info.canonicalFilePath()); else qCWarning(LOKALIZE_LOG) << "alt trans file doesn't exist:" << Project::instance()->altTransDir() % '/' % relPath; } void AltTransView::slotNewEntryDisplayed(const DocPosition& pos) { m_entry = DocPos(pos); QTimer::singleShot(0, this, &AltTransView::process); } void AltTransView::process() { if (m_entry == m_prevEntry) return; if (m_catalog->numberOfEntries() <= m_entry.entry) return;//because of Qt::QueuedConnection m_prevEntry = m_entry; m_browser->clear(); m_entryPositions.clear(); const QVector& entries = m_catalog->altTrans(m_entry.toDocPosition()); m_entries = entries; if (entries.isEmpty()) { if (m_hasInfo) { m_hasInfo = false; setWindowTitle(m_normTitle); } return; } if (!m_hasInfo) { m_hasInfo = true; setWindowTitle(m_hasInfoTitle); } if (!isVisible() && !Settings::altTransViewEverShownWithData()) { if (KMessageBox::questionYesNo(this, i18n("There is useful data available in Alternate Translations view.\n\n" "For Gettext PO files it displays difference between current source text " "and the source text corresponding to the fuzzy translation found by msgmerge when updating PO based on POT template.\n\n" "Do you want to show the view with the data?"), m_normTitle) == KMessageBox::Yes) show(); Settings::setAltTransViewEverShownWithData(true); } CatalogString source = m_catalog->sourceWithTags(m_entry.toDocPosition()); QTextBlockFormat blockFormatBase; QTextBlockFormat blockFormatAlternate; blockFormatAlternate.setBackground(QPalette().alternateBase()); QTextCharFormat noncloseMatchCharFormat; QTextCharFormat closeMatchCharFormat; closeMatchCharFormat.setFontWeight(QFont::Bold); int i = 0; int limit = entries.size(); forever { const AltTrans& entry = entries.at(i); QTextCursor cur = m_browser->textCursor(); QString html; html.reserve(1024); if (!entry.source.isEmpty()) { html += QStringLiteral("

"); QString result = userVisibleWordDiff(entry.source.string, source.string, Project::instance()->accel(), Project::instance()->markup()).toHtmlEscaped(); //result.replace("&","&"); //result.replace("<","<"); //result.replace(">",">"); result.replace(QStringLiteral("{KBABELADD}"), QStringLiteral("")); result.replace(QStringLiteral("{/KBABELADD}"), QStringLiteral("")); result.replace(QStringLiteral("{KBABELDEL}"), QStringLiteral("")); result.replace(QStringLiteral("{/KBABELDEL}"), QStringLiteral("")); result.replace(QStringLiteral("\\n"), QStringLiteral("\\n

")); html += result; html += QStringLiteral("
"); cur.insertHtml(html); html.clear(); } if (!entry.target.isEmpty()) { if (Q_LIKELY(i < m_actions.size())) { m_actions.at(i)->setStatusTip(entry.target.string); html += QString(QStringLiteral("[%1] ")).arg(m_actions.at(i)->shortcut().toString(QKeySequence::NativeText)); } else html += QStringLiteral("[ - ] "); cur.insertText(html); html.clear(); insertContent(cur, entry.target); } m_entryPositions.insert(cur.anchor(), i); html += i ? QStringLiteral("

") : QStringLiteral("

"); cur.insertHtml(html); if (Q_UNLIKELY(++i >= limit)) break; cur.insertBlock(i % 2 ? blockFormatAlternate : blockFormatBase); } if (!m_everShown) { m_everShown = true; show(); KConfig config; KConfigGroup group(&config, "AltTransView"); group.writeEntry("EverShown", true); } } bool AltTransView::event(QEvent *event) { if (event->type() == QEvent::ToolTip) { QHelpEvent *helpEvent = static_cast(event); if (m_entryPositions.isEmpty()) { QString tooltip = i18nc("@info:tooltip", "

Sometimes, if source text is changed, its translation becomes deprecated and is either marked as needing review (i.e. looses approval status), " "or (only in case of XLIFF file) moved to the alternate translations section accompanying the unit.

" "

This toolview also shows the difference between current source string and the previous source string, so that you can easily see which changes should be applied to existing translation to make it reflect current source.

" "

Double-clicking any word in this toolview inserts it into translation.

" "

Drop translation file onto this toolview to use it as a source for additional alternate translations.

" ); QToolTip::showText(helpEvent->globalPos(), tooltip); return true; } int block1 = m_browser->cursorForPosition(m_browser->viewport()->mapFromGlobal(helpEvent->globalPos())).blockNumber(); int block = *m_entryPositions.lowerBound(m_browser->cursorForPosition(m_browser->viewport()->mapFromGlobal(helpEvent->globalPos())).anchor()); if (block1 != block) qCWarning(LOKALIZE_LOG) << "block numbers don't match"; if (block >= m_entries.size()) return false; QString origin = m_entries.at(block).origin; if (origin.isEmpty()) return false; QString tooltip = i18nc("@info:tooltip", "Origin: %1", origin); QToolTip::showText(helpEvent->globalPos(), tooltip); return true; } return QWidget::event(event); } void AltTransView::slotUseSuggestion(int i) { if (Q_UNLIKELY(i >= m_entries.size())) return; TM::TMEntry tmEntry; tmEntry.target = m_entries.at(i).target; CatalogString source = m_catalog->sourceWithTags(m_entry.toDocPosition()); tmEntry.diff = userVisibleWordDiff(m_entries.at(i).source.string, source.string, Project::instance()->accel(), Project::instance()->markup()); CatalogString target = TM::targetAdapted(tmEntry, source); qCWarning(LOKALIZE_LOG) << "0" << target.string; if (Q_UNLIKELY(target.isEmpty())) return; m_catalog->beginMacro(i18nc("@item Undo action", "Use alternate translation")); QString old = m_catalog->targetWithTags(m_entry.toDocPosition()).string; if (!old.isEmpty()) { //FIXME test! removeTargetSubstring(m_catalog, m_entry.toDocPosition(), 0, old.size()); //m_catalog->push(new DelTextCmd(m_catalog,m_pos,m_catalog->msgstr(m_pos))); } qCWarning(LOKALIZE_LOG) << "1" << target.string; //m_catalog->push(new InsTextCmd(m_catalog,m_pos,target)/*,true*/); insertCatalogString(m_catalog, m_entry.toDocPosition(), target, 0); m_catalog->endMacro(); emit refreshRequested(); } diff --git a/src/catalog/catalogstring.cpp b/src/catalog/catalogstring.cpp index e3bdf80..3c517b6 100644 --- a/src/catalog/catalogstring.cpp +++ b/src/catalog/catalogstring.cpp @@ -1,326 +1,326 @@ /* **************************************************************************** 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 "catalogstring.h" #include "lokalize_debug.h" #include const char* InlineTag::getElementName(InlineElement type) { static const char* inlineElementNames[(int)InlineElementCount] = { "_unknown", "bpt", "ept", "ph", "it", //"_NEVERSHOULDBECHOSEN", "mrk", "g", "sub", "_NEVERSHOULDBECHOSEN", "x", "bx", "ex" }; return inlineElementNames[(int)type]; } InlineTag InlineTag::getPlaceholder() const { InlineTag tagRange = *this; tagRange.start = -1; tagRange.end = -1; return tagRange; } InlineTag::InlineElement InlineTag::getElementType(const QByteArray& tag) { int i = InlineTag::InlineElementCount; while (--i > 0) if (getElementName(InlineElement(i)) == tag) break; return InlineElement(i); } QString InlineTag::displayName() const { static const char* inlineElementNames[(int)InlineElementCount] = { "_unknown", I18N_NOOP2("XLIFF inline tag name", "Start of paired tag"), I18N_NOOP2("XLIFF inline tag name", "End of paired tag"), I18N_NOOP2("XLIFF inline tag name", "Stand-alone tag"), I18N_NOOP2("XLIFF inline tag name", "Isolated tag"), //"_NEVERSHOULDBECHOSEN", I18N_NOOP2("XLIFF inline tag name", "Marker"), I18N_NOOP2("XLIFF inline tag name", "Generic group placeholder"), I18N_NOOP2("XLIFF inline tag name", "Sub-flow"), "_NEVERSHOULDBECHOSEN", I18N_NOOP2("XLIFF inline tag name", "Generic placeholder"), I18N_NOOP2("XLIFF inline tag name", "Start of paired placeholder"), I18N_NOOP2("XLIFF inline tag name", "End of paired placeholder") }; QString result = i18nc("XLIFF inline tag name", inlineElementNames[type]); if (type == mrk) { static const char* mrkTypes[] = { "abbrev", "abbreviated-form", "abbreviation", "acronym", "appellation", "collocation", "common-name", "datetime", "equation", "expanded-form", "formula", "head-term", "initialism", "international-scientific-term", "internationalism", "logical-expression", "materials-management-unit", "name", "near-synonym", "part-number", "phrase", "phraseological-unit", "protected", "romanized-form", "seg", "set-phrase", "short-form", "sku", "standard-text", "symbol", "synonym", "synonymous-phrase", "term", "transcribed-form", "transliterated-form", "truncated-term", "variant" }; static const char* mrkTypeNames[] = { I18N_NOOP2("XLIFF mark type", "abbreviation"), I18N_NOOP2("XLIFF mark type", "abbreviated form: a term resulting from the omission of any part of the full term while designating the same concept"), I18N_NOOP2("XLIFF mark type", "abbreviation: an abbreviated form of a simple term resulting from the omission of some of its letters (e.g. 'adj.' for 'adjective')"), I18N_NOOP2("XLIFF mark type", "acronym: an abbreviated form of a term made up of letters from the full form of a multiword term strung together into a sequence pronounced only syllabically (e.g. 'radar' for 'radio detecting and ranging')"), I18N_NOOP2("XLIFF mark type", "appellation: a proper-name term, such as the name of an agency or other proper entity"), I18N_NOOP2("XLIFF mark type", "collocation: a recurrent word combination characterized by cohesion in that the components of the collocation must co-occur within an utterance or series of utterances, even though they do not necessarily have to maintain immediate proximity to one another"), I18N_NOOP2("XLIFF mark type", "common name: a synonym for an international scientific term that is used in general discourse in a given language"), I18N_NOOP2("XLIFF mark type", "date and/or time"), I18N_NOOP2("XLIFF mark type", "equation: an expression used to represent a concept based on a statement that two mathematical expressions are, for instance, equal as identified by the equal sign (=), or assigned to one another by a similar sign"), I18N_NOOP2("XLIFF mark type", "expanded form: The complete representation of a term for which there is an abbreviated form"), I18N_NOOP2("XLIFF mark type", "formula: figures, symbols or the like used to express a concept briefly, such as a mathematical or chemical formula"), I18N_NOOP2("XLIFF mark type", "head term: the concept designation that has been chosen to head a terminological record"), I18N_NOOP2("XLIFF mark type", "initialism: an abbreviated form of a term consisting of some of the initial letters of the words making up a multiword term or the term elements making up a compound term when these letters are pronounced individually (e.g. 'BSE' for 'bovine spongiform encephalopathy')"), I18N_NOOP2("XLIFF mark type", "international scientific term: a term that is part of an international scientific nomenclature as adopted by an appropriate scientific body"), I18N_NOOP2("XLIFF mark type", "internationalism: a term that has the same or nearly identical orthographic or phonemic form in many languages"), I18N_NOOP2("XLIFF mark type", "logical expression: an expression used to represent a concept based on mathematical or logical relations, such as statements of inequality, set relationships, Boolean operations, and the like"), I18N_NOOP2("XLIFF mark type", "materials management unit: a unit to track object"), I18N_NOOP2("XLIFF mark type", "name"), I18N_NOOP2("XLIFF mark type", "near synonym: a term that represents the same or a very similar concept as another term in the same language, but for which interchangeability is limited to some contexts and inapplicable in others"), I18N_NOOP2("XLIFF mark type", "part number: a unique alphanumeric designation assigned to an object in a manufacturing system"), I18N_NOOP2("XLIFF mark type", "phrase"), I18N_NOOP2("XLIFF mark type", "phraseological: a group of two or more words that form a unit, the meaning of which frequently cannot be deduced based on the combined sense of the words making up the phrase"), I18N_NOOP2("XLIFF mark type", "protected: the marked text should not be translated"), I18N_NOOP2("XLIFF mark type", "romanized form: a form of a term resulting from an operation whereby non-Latin writing systems are converted to the Latin alphabet"), I18N_NOOP2("XLIFF mark type", "segment: the marked text represents a segment"), I18N_NOOP2("XLIFF mark type", "set phrase: a fixed, lexicalized phrase"), I18N_NOOP2("XLIFF mark type", "short form: a variant of a multiword term that includes fewer words than the full form of the term (e.g. 'Group of Twenty-four' for 'Intergovernmental Group of Twenty-four on International Monetary Affairs')"), I18N_NOOP2("XLIFF mark type", "stock keeping unit: an inventory item identified by a unique alphanumeric designation assigned to an object in an inventory control system"), I18N_NOOP2("XLIFF mark type", "standard text: a fixed chunk of recurring text"), I18N_NOOP2("XLIFF mark type", "symbol: a designation of a concept by letters, numerals, pictograms or any combination thereof"), I18N_NOOP2("XLIFF mark type", "synonym: a term that represents the same or a very similar concept as the main entry term in a term entry"), I18N_NOOP2("XLIFF mark type", "synonymous phrase: phraseological unit in a language that expresses the same semantic content as another phrase in that same language"), I18N_NOOP2("XLIFF mark type", "term"), I18N_NOOP2("XLIFF mark type", "transcribed form: a form of a term resulting from an operation whereby the characters of one writing system are represented by characters from another writing system, taking into account the pronunciation of the characters converted"), I18N_NOOP2("XLIFF mark type", "transliterated form: a form of a term resulting from an operation whereby the characters of an alphabetic writing system are represented by characters from another alphabetic writing system"), I18N_NOOP2("XLIFF mark type", "truncated term: an abbreviated form of a term resulting from the omission of one or more term elements or syllables (e.g. 'flu' for 'influenza')"), I18N_NOOP2("XLIFF mark type", "variant: one of the alternate forms of a term") }; int i = sizeof(mrkTypes) / sizeof(char*); while (--i >= 0 && mrkTypes[i] != id) ; if (i != -1) { result = i18nc("XLIFF mark type", mrkTypeNames[i]); if (!result.isEmpty()) result[0] = result.at(0).toUpper(); } } if (!ctype.isEmpty()) result += " (" + ctype + ')'; return result; } QMap CatalogString::tagIdToIndex() const { QMap result; int index = 0; int count = tags.size(); for (int i = 0; i < count; ++i) { if (!result.contains(tags.at(i).id)) result.insert(tags.at(i).id, index++); } return result; } QByteArray CatalogString::tagsAsByteArray()const { QByteArray result; if (tags.size()) { QDataStream stream(&result, QIODevice::WriteOnly); stream << tags; } return result; } CatalogString::CatalogString(QString str, QByteArray tagsByteArray) : string(str) { if (tagsByteArray.size()) { QDataStream stream(tagsByteArray); stream >> tags; } } static void adjustTags(QList& tags, int position, int value) { int i = tags.size(); while (--i >= 0) { InlineTag& t = tags[i]; if (t.start > position) t.start += value; if (t.end >= position) //cases when strict > is needed? t.end += value; } } void CatalogString::remove(int position, int len) { string.remove(position, len); adjustTags(tags, position, -len); } void CatalogString::insert(int position, const QString& str) { string.insert(position, str); adjustTags(tags, position, str.size()); } QDataStream &operator<<(QDataStream &out, const InlineTag &t) { return out << int(t.type) << t.start << t.end << t.id; } QDataStream &operator>>(QDataStream &in, InlineTag &t) { int type; in >> type >> t.start >> t.end >> t.id; t.type = InlineTag::InlineElement(type); return in; } QDataStream &operator<<(QDataStream &out, const CatalogString &myObj) { return out << myObj.string << myObj.tags; } QDataStream &operator>>(QDataStream &in, CatalogString &myObj) { return in >> myObj.string >> myObj.tags; } void adaptCatalogString(CatalogString& target, const CatalogString& ref) { //qCWarning(LOKALIZE_LOG) << "HERE" << target.string; QHash id2tagIndex; QMultiMap tagType2tagIndex; int i = ref.tags.size(); while (--i >= 0) { const InlineTag& t = ref.tags.at(i); id2tagIndex.insert(t.id, i); tagType2tagIndex.insert(t.type, i); qCWarning(LOKALIZE_LOG) << "inserting" << t.id << t.type << i; } QList oldTags = target.tags; target.tags.clear(); //we actually walking from beginning to end: - qSort(oldTags.begin(), oldTags.end(), qGreater()); + std::sort(oldTags.begin(), oldTags.end(), qGreater()); i = oldTags.size(); while (--i >= 0) { const InlineTag& targetTag = oldTags.at(i); if (id2tagIndex.contains(targetTag.id)) { qCWarning(LOKALIZE_LOG) << "matched" << targetTag.id << i; target.tags.append(targetTag); tagType2tagIndex.remove(targetTag.type, id2tagIndex.take(targetTag.id)); oldTags.removeAt(i); } } //qCWarning(LOKALIZE_LOG) << "HERE 0" << target.string; //now all the tags left have to ID (exact) matches i = oldTags.size(); while (--i >= 0) { InlineTag targetTag = oldTags.at(i); if (tagType2tagIndex.contains(targetTag.type)) { //try to match by position //we're _taking_ first so the next one becomes new 'first' for the next time. QList possibleRefMatches; foreach (int i, tagType2tagIndex.values(targetTag.type)) possibleRefMatches << ref.tags.at(i); - qSort(possibleRefMatches); + std::sort(possibleRefMatches.begin(), possibleRefMatches.end()); qCWarning(LOKALIZE_LOG) << "setting id:" << targetTag.id << possibleRefMatches.first().id; targetTag.id = possibleRefMatches.first().id; target.tags.append(targetTag); qCWarning(LOKALIZE_LOG) << "id??:" << targetTag.id << target.tags.first().id; tagType2tagIndex.remove(targetTag.type, id2tagIndex.take(targetTag.id)); oldTags.removeAt(i); } } //qCWarning(LOKALIZE_LOG) << "HERE 1" << target.string; //now walk through unmatched tags and properly remove them. foreach (const InlineTag& tag, oldTags) { if (tag.isPaired()) target.remove(tag.end, 1); target.remove(tag.start, 1); } //qCWarning(LOKALIZE_LOG) << "HERE 2" << target.string; } diff --git a/src/catalog/phase.cpp b/src/catalog/phase.cpp index 216e0a6..9f72be5 100644 --- a/src/catalog/phase.cpp +++ b/src/catalog/phase.cpp @@ -1,96 +1,96 @@ /* **************************************************************************** This file is part of Lokalize Copyright (C) 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 "phase.h" #include "cmd.h" #include "catalog.h" #include "project.h" #include "prefs_lokalize.h" #include "gettextheader.h" #include #include const char* const* processes() { static const char* const processes[] = {"translation", "review", "approval"}; return processes; } //guess role ProjectLocal::PersonRole roleForProcess(const QString& process) { int i = ProjectLocal::Undefined; while (i >= 0 && !process.startsWith(processes()[--i])) ; return (i == -1) ? Project::local()->role() : ProjectLocal::PersonRole(i); } void generatePhaseForCatalogIfNeeded(Catalog* catalog) { if (Q_LIKELY(!(catalog->capabilities()&Phases) || catalog->activePhaseRole() == ProjectLocal::Undefined)) return; Phase phase; phase.process = processes()[Project::local()->role()]; if (initPhaseForCatalog(catalog, phase)) static_cast(catalog)->push(new UpdatePhaseCmd(catalog, phase)); catalog->setActivePhase(phase.name, roleForProcess(phase.process)); } bool initPhaseForCatalog(Catalog* catalog, Phase& phase, int options) { askAuthorInfoIfEmpty(); phase.contact = Settings::authorName(); QSet names; QList phases = catalog->allPhases(); - qSort(phases.begin(), phases.end(), qGreater()); + std::sort(phases.begin(), phases.end(), qGreater()); foreach (const Phase& p, phases) { if (!(options & ForceAdd) && p.contact == phase.contact && p.process == phase.process) { phase = p; break; } names.insert(p.name); } if (phase.name.isEmpty()) { int i = 0; while (names.contains(phase.name = phase.process + QStringLiteral("-%1").arg(++i))) ; phase.date = QDate::currentDate(); phase.email = Settings::authorEmail(); return true; } return false; } Phase::Phase() : date(QDate::currentDate()) , tool(QStringLiteral("lokalize-" LOKALIZE_VERSION)) {} diff --git a/src/catalog/xliff/xliffstorage.cpp b/src/catalog/xliff/xliffstorage.cpp index 2982adf..2ade5ab 100644 --- a/src/catalog/xliff/xliffstorage.cpp +++ b/src/catalog/xliff/xliffstorage.cpp @@ -1,1034 +1,1034 @@ /* Copyright 2008-2009 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 "xliffstorage.h" #include "lokalize_debug.h" #include "gettextheader.h" #include "project.h" #include "version.h" #include "prefs_lokalize.h" #include #include #include #include #include #include #include #include #include #ifdef Q_OS_WIN #define U QLatin1String #else #define U QStringLiteral #endif static const QString noyes[] = {U("no"), U("yes")}; static const QString bintargettarget[] = {U("bin-target"), U("target")}; static const QString binsourcesource[] = {U("bin-source"), U("source")}; static const QString NOTE = U("note"); XliffStorage::XliffStorage() : CatalogStorage() { } int XliffStorage::capabilities() const { return KeepsNoteAuthors | MultipleNotes | Phases | ExtendedStates | Tags; } //BEGIN OPEN/SAVE int XliffStorage::load(QIODevice* device) { QTime chrono; chrono.start(); QXmlSimpleReader reader; reader.setFeature(QStringLiteral("http://qt-project.org/xml/features/report-whitespace-only-CharData"), true); reader.setFeature(QStringLiteral("http://xml.org/sax/features/namespaces"), false); QXmlInputSource source(device); QString errorMsg; int errorLine;//+errorColumn; bool success = m_doc.setContent(&source, &reader, &errorMsg, &errorLine/*,errorColumn*/); QString FILE = QStringLiteral("file"); if (!success || m_doc.elementsByTagName(FILE).isEmpty()) { qCWarning(LOKALIZE_LOG) << errorMsg; return errorLine + 1; } QDomElement file = m_doc.elementsByTagName(FILE).at(0).toElement(); m_sourceLangCode = file.attribute(QStringLiteral("source-language")).replace(u'-', u'_'); m_targetLangCode = file.attribute(QStringLiteral("target-language")).replace(u'-', u'_'); m_numberOfPluralForms = numberOfPluralFormsForLangCode(m_targetLangCode); //Create entry mapping. //Along the way: for langs with more than 2 forms //we create any form-entries additionally needed entries = m_doc.elementsByTagName(QStringLiteral("trans-unit")); int size = entries.size(); m_map.clear(); m_map.reserve(size); for (int i = 0; i < size; ++i) { QDomElement parentElement = entries.at(i).parentNode().toElement(); //if (Q_UNLIKELY( e.isNull() ))//sanity // continue; m_map << i; m_unitsById[entries.at(i).toElement().attribute(QStringLiteral("id"))] = i; if (parentElement.tagName() == QLatin1String("group") && parentElement.attribute(QStringLiteral("restype")) == QLatin1String("x-gettext-plurals")) { m_plurals.insert(i); int localPluralNum = m_numberOfPluralForms; while (--localPluralNum > 0 && (++i) < size) { QDomElement p = entries.at(i).parentNode().toElement(); if (p.tagName() == QLatin1String("group") && p.attribute(QStringLiteral("restype")) == QLatin1String("x-gettext-plurals")) continue; parentElement.appendChild(entries.at(m_map.last()).cloneNode()); } } } binEntries = m_doc.elementsByTagName(QStringLiteral("bin-unit")); size = binEntries.size(); int offset = m_map.size(); for (int i = 0; i < size; ++i) m_unitsById[binEntries.at(i).toElement().attribute(QStringLiteral("id"))] = offset + i; // entries=m_doc.elementsByTagName("body"); // uint i=0; // uint lim=size(); // while (i msgstr(item.msgstrPlural()); // while (msgstr.count() tags; QString stringToInsert; int pos; int lengthOfStringToRemove; ActionType actionType; ///Get ContentEditingData(ActionType type = Get) : pos(-1) , lengthOfStringToRemove(-1) , actionType(type) {} ///DeleteText ContentEditingData(int p, int l) : pos(p) , lengthOfStringToRemove(l) , actionType(DeleteText) {} ///InsertText ContentEditingData(int p, const QString& s) : stringToInsert(s) , pos(p) , lengthOfStringToRemove(-1) , actionType(InsertText) {} ///InsertTag ContentEditingData(int p, const InlineTag& range) : pos(p) , lengthOfStringToRemove(-1) , actionType(InsertTag) { tags.append(range); } ///DeleteTag ContentEditingData(int p) : pos(p) , lengthOfStringToRemove(-1) , actionType(DeleteTag) {} }; static QString doContent(QDomElement elem, int startingPos, ContentEditingData* data); /** * walks through XLIFF XML and performs actions depending on ContentEditingData: * - reads content * - deletes content, or * - inserts content */ static QString content(QDomElement elem, ContentEditingData* data = 0) { return doContent(elem, 0, data); } static QString doContent(QDomElement elem, int startingPos, ContentEditingData* data) { //actually startingPos is current pos QString result; if (elem.isNull() || (!result.isEmpty() && data && data->actionType == ContentEditingData::CheckLength)) return QString(); bool seenCharacterDataAfterElement = false; QDomNode n = elem.firstChild(); while (!n.isNull()) { if (n.isCharacterData()) { seenCharacterDataAfterElement = true; QDomCharacterData c = n.toCharacterData(); QString cData = c.data(); if (data && data->pos != -1 && data->pos >= startingPos && data->pos <= startingPos + cData.size()) { // time to do some action! ;) int localStartPos = data->pos - startingPos; //BEGIN DELETE TEXT if (data->actionType == ContentEditingData::DeleteText) { //(data->lengthOfStringToRemove!=-1) if (localStartPos + data->lengthOfStringToRemove > cData.size()) { //text is fragmented into several QDomCharacterData int localDelLen = cData.size() - localStartPos; //qCWarning(LOKALIZE_LOG)<<"text is fragmented into several QDomCharacterData. localDelLen:"<lengthOfStringToRemove = data->lengthOfStringToRemove - localDelLen; //data->pos=startingPos; //qCWarning(LOKALIZE_LOG)<<"\tsetup:"<pos<lengthOfStringToRemove; } else { //qCWarning(LOKALIZE_LOG)<<"simple delete"<lengthOfStringToRemove; c.deleteData(localStartPos, data->lengthOfStringToRemove); data->actionType = ContentEditingData::CheckLength; return QString('a');//so it exits 100% } } //END DELETE TEXT //INSERT else if (data->actionType == ContentEditingData::InsertText) { c.insertData(localStartPos, data->stringToInsert); data->actionType = ContentEditingData::CheckLength; return QString('a');//so it exits 100% } //BEGIN INSERT TAG else if (data->actionType == ContentEditingData::InsertTag) { const InlineTag& tag = data->tags.first(); QString mid = cData.mid(localStartPos); qCDebug(LOKALIZE_LOG) << "inserting tag" << tag.name() << tag.id << tag.start << tag.end << mid << data->pos << startingPos; if (mid.size()) c.deleteData(localStartPos, mid.size()); QDomElement newNode = elem.insertAfter(elem.ownerDocument().createElement(tag.getElementName()), n).toElement(); newNode.setAttribute(QStringLiteral("id"), tag.id); if (!tag.xid.isEmpty()) newNode.setAttribute(QStringLiteral("xid"), tag.xid); if (tag.isPaired() && tag.end > (tag.start + 1)) { //qCWarning(LOKALIZE_LOG)<<"isPaired"; int len = tag.end - tag.start - 1; //-image symbol int localLen = qMin(len, mid.size()); if (localLen) { //appending text //qCWarning(LOKALIZE_LOG)<<"localLen. appending"<missingLen (or siblings end) int childrenCumulativeLen = 0; QDomNode sibling = newNode.nextSibling(); while (!sibling.isNull()) { //&&(childrenCumulativeLen missingLen) { if (tmp.isCharacterData()) { //divide the last string const QString& endData = tmp.toCharacterData().data(); QString last = endData.left(endData.size() - (childrenCumulativeLen - missingLen)); newNode.appendChild(elem.ownerDocument().createTextNode(last)); tmp.toCharacterData().deleteData(0, last.size()); //qCWarning(LOKALIZE_LOG)<<"end of add"<actionType = ContentEditingData::CheckLength; return QStringLiteral("a");//we're done here } //END INSERT TAG cData = c.data(); } //else // if (data&&data->pos!=-1/*&& n.nextSibling().isNull()*/) // qCWarning(LOKALIZE_LOG)<<"arg!"<pos"<pos; result += cData; startingPos += cData.size(); } else if (n.isElement()) { QDomElement el = n.toElement(); //BEGIN DELETE TAG if (data && data->actionType == ContentEditingData::DeleteTag && data->pos == startingPos) { //qCWarning(LOKALIZE_LOG)<<"start deleting tag"; data->tags.append(InlineTag(startingPos, -1, InlineTag::getElementType(el.tagName().toUtf8()), el.attribute("id"), el.attribute("xid"))); if (data->tags.first().isPaired()) { //get end position ContentEditingData subData(ContentEditingData::Get); QString subContent = doContent(el, startingPos, &subData); data->tags[0].end = 1 + startingPos + subContent.size(); //tagsymbol+text //qCWarning(LOKALIZE_LOG)<<"get end position"<actionType = ContentEditingData::CheckLength; return QStringLiteral("a");//we're done here } //END DELETE TAG if (!seenCharacterDataAfterElement) //add empty charData child so that user could add some text elem.insertBefore(elem.ownerDocument().createTextNode(QString()), n); seenCharacterDataAfterElement = false; if (data) { result += QChar(TAGRANGE_IMAGE_SYMBOL); ++startingPos; } int oldStartingPos = startingPos; //detect type of the tag InlineTag::InlineElement i = InlineTag::getElementType(el.tagName().toUtf8()); //1 or 2 images to represent it? //2 = there may be content inside if (InlineTag::isPaired(i)) { QString recursiveContent = doContent(el, startingPos, data); if (!recursiveContent.isEmpty()) { result += recursiveContent; startingPos += recursiveContent.size(); } if (data) { result += QChar(TAGRANGE_IMAGE_SYMBOL); ++startingPos; } } if (data && data->actionType == ContentEditingData::Get) { QString id = el.attribute(QStringLiteral("id")); if (i == InlineTag::mrk) //TODO attr map id = el.attribute(QStringLiteral("mtype")); //qCWarning(LOKALIZE_LOG)<<"tagName"<tags.append(InlineTag(oldStartingPos - 1, startingPos - 1, i, id, el.attribute(QStringLiteral("xid")))); } } n = n.nextSibling(); } if (!seenCharacterDataAfterElement) { //add empty charData child so that user could add some text elem.appendChild(elem.ownerDocument().createTextNode(QString())); } return result; } //flat-model interface (ignores XLIFF grouping) CatalogString XliffStorage::catalogString(QDomElement unit, DocPosition::Part part) const { static const QString names[] = {U("source"), U("target"), U("seg-source")}; CatalogString catalogString; ContentEditingData data(ContentEditingData::Get); int nameIndex = part == DocPosition::Target; if (nameIndex == 0 && !unit.firstChildElement(names[2]).isNull()) nameIndex = 2; catalogString.string = content(unit.firstChildElement(names[nameIndex]), &data); catalogString.tags = data.tags; return catalogString; } CatalogString XliffStorage::catalogString(const DocPosition& pos) const { return catalogString(unitForPos(pos.entry), pos.part); } CatalogString XliffStorage::targetWithTags(DocPosition pos) const { return catalogString(unitForPos(pos.entry), DocPosition::Target); } CatalogString XliffStorage::sourceWithTags(DocPosition pos) const { return catalogString(unitForPos(pos.entry), DocPosition::Source); } static QString genericContent(QDomElement elem, bool nonbin) { return nonbin ? content(elem) : elem.firstChildElement(QStringLiteral("external-file")).attribute(QStringLiteral("href")); } QString XliffStorage::source(const DocPosition& pos) const { return genericContent(sourceForPos(pos.entry), pos.entry < size()); } QString XliffStorage::target(const DocPosition& pos) const { return genericContent(targetForPos(pos.entry), pos.entry < size()); } QString XliffStorage::sourceWithPlurals(const DocPosition& pos, bool truncateFirstLine) const { QString str = source(pos); if (truncateFirstLine) { int truncatePos = str.indexOf("\n"); if (truncatePos != -1) str.truncate(truncatePos); } return str; } QString XliffStorage::targetWithPlurals(const DocPosition& pos, bool truncateFirstLine) const { QString str = target(pos); if (truncateFirstLine) { int truncatePos = str.indexOf("\n"); if (truncatePos != -1) str.truncate(truncatePos); } return str; } void XliffStorage::targetDelete(const DocPosition& pos, int count) { if (pos.entry < size()) { ContentEditingData data(pos.offset, count); content(targetForPos(pos.entry), &data); } else { //only bulk delete requests are generated targetForPos(pos.entry).firstChildElement(QStringLiteral("external-file")).setAttribute(QStringLiteral("href"), QString()); } } void XliffStorage::targetInsert(const DocPosition& pos, const QString& arg) { //qCWarning(LOKALIZE_LOG)<<"targetinsert"< if (targetEl.isNull()) { QDomNode unitEl = unitForPos(pos.entry); QDomNode refNode = unitEl.firstChildElement(QStringLiteral("seg-source")); //obey standard if (refNode.isNull()) refNode = unitEl.firstChildElement(binsourcesource[pos.entry < size()]); targetEl = unitEl.insertAfter(m_doc.createElement(bintargettarget[pos.entry < size()]), refNode).toElement(); targetEl.setAttribute(QStringLiteral("state"), QStringLiteral("new")); if (pos.entry < size()) { targetEl.appendChild(m_doc.createTextNode(arg));//i bet that pos.offset is 0 ;) return; } } //END add <*target> if (arg.isEmpty()) return; //means we were called just to add tag if (pos.entry >= size()) { QDomElement ef = targetEl.firstChildElement(QStringLiteral("external-file")); if (ef.isNull()) ef = targetEl.appendChild(m_doc.createElement(QStringLiteral("external-file"))).toElement(); ef.setAttribute(QStringLiteral("href"), arg); return; } ContentEditingData data(pos.offset, arg); content(targetEl, &data); } void XliffStorage::targetInsertTag(const DocPosition& pos, const InlineTag& tag) { targetInsert(pos, QString()); //adds if needed ContentEditingData data(tag.start, tag); content(targetForPos(pos.entry), &data); } InlineTag XliffStorage::targetDeleteTag(const DocPosition& pos) { ContentEditingData data(pos.offset); content(targetForPos(pos.entry), &data); if (data.tags[0].end == -1) data.tags[0].end = data.tags[0].start; return data.tags.first(); } void XliffStorage::setTarget(const DocPosition& pos, const QString& arg) { Q_UNUSED(pos); Q_UNUSED(arg); //TODO } QVector XliffStorage::altTrans(const DocPosition& pos) const { QVector result; QDomElement elem = unitForPos(pos.entry).firstChildElement(QStringLiteral("alt-trans")); while (!elem.isNull()) { AltTrans aTrans; aTrans.source = catalogString(elem, DocPosition::Source); aTrans.target = catalogString(elem, DocPosition::Target); aTrans.phase = elem.attribute(QStringLiteral("phase-name")); aTrans.origin = elem.attribute(QStringLiteral("origin")); aTrans.score = elem.attribute(QStringLiteral("match-quality")).toInt(); aTrans.lang = elem.firstChildElement(QStringLiteral("target")).attribute(QStringLiteral("xml:lang")); const char* const types[] = { "proposal", "previous-version", "rejected", "reference", "accepted" }; QString typeStr = elem.attribute(QStringLiteral("alttranstype")); int i = -1; while (++i < int(sizeof(types) / sizeof(char*)) && types[i] != typeStr) ; aTrans.type = AltTrans::Type(i); result << aTrans; elem = elem.nextSiblingElement(QStringLiteral("alt-trans")); } return result; } static QDomElement phaseElement(QDomDocument m_doc, const QString& name, QDomElement& phasegroup) { QDomElement file = m_doc.elementsByTagName(QStringLiteral("file")).at(0).toElement(); QDomElement header = file.firstChildElement(QStringLiteral("header")); phasegroup = header.firstChildElement(QStringLiteral("phase-group")); if (phasegroup.isNull()) { phasegroup = m_doc.createElement(QStringLiteral("phase-group")); //order following XLIFF spec QDomElement skl = header.firstChildElement(QStringLiteral("skl")); if (!skl.isNull()) header.insertAfter(phasegroup, skl); else header.insertBefore(phasegroup, header.firstChildElement()); } QDomElement phaseElem = phasegroup.firstChildElement(QStringLiteral("phase")); while (!phaseElem.isNull() && phaseElem.attribute(QStringLiteral("phase-name")) != name) phaseElem = phaseElem.nextSiblingElement(QStringLiteral("phase")); return phaseElem; } static Phase phaseFromElement(QDomElement phaseElem) { Phase phase; phase.name = phaseElem.attribute(QStringLiteral("phase-name")); phase.process = phaseElem.attribute(QStringLiteral("process-name")); phase.company = phaseElem.attribute(QStringLiteral("company-name")); phase.contact = phaseElem.attribute(QStringLiteral("contact-name")); phase.email = phaseElem.attribute(QStringLiteral("contact-email")); phase.phone = phaseElem.attribute(QStringLiteral("contact-phone")); phase.tool = phaseElem.attribute(QStringLiteral("tool-id")); phase.date = QDate::fromString(phaseElem.attribute(QStringLiteral("date")), Qt::ISODate); return phase; } Phase XliffStorage::updatePhase(const Phase& phase) { QDomElement phasegroup; QDomElement phaseElem = phaseElement(m_doc, phase.name, phasegroup); Phase prev = phaseFromElement(phaseElem); if (phaseElem.isNull() && !phase.name.isEmpty()) { phaseElem = phasegroup.appendChild(m_doc.createElement(QStringLiteral("phase"))).toElement(); phaseElem.setAttribute(QStringLiteral("phase-name"), phase.name); } phaseElem.setAttribute(QStringLiteral("process-name"), phase.process); if (!phase.company.isEmpty()) phaseElem.setAttribute(QStringLiteral("company-name"), phase.company); phaseElem.setAttribute(QStringLiteral("contact-name"), phase.contact); phaseElem.setAttribute(QStringLiteral("contact-email"), phase.email); //Q_ASSERT(phase.contact.length()); //is empty when exiting w/o saving if (!phase.phone.isEmpty()) phaseElem.setAttribute(QLatin1String("contact-phone"), phase.phone); phaseElem.setAttribute(QStringLiteral("tool-id"), phase.tool); if (phase.date.isValid()) phaseElem.setAttribute(QStringLiteral("date"), phase.date.toString(Qt::ISODate)); return prev; } QList XliffStorage::allPhases() const { QList result; QDomElement file = m_doc.elementsByTagName(QStringLiteral("file")).at(0).toElement(); QDomElement header = file.firstChildElement(QStringLiteral("header")); QDomElement phasegroup = header.firstChildElement(QStringLiteral("phase-group")); QDomElement phaseElem = phasegroup.firstChildElement(QStringLiteral("phase")); while (!phaseElem.isNull()) { result.append(phaseFromElement(phaseElem)); phaseElem = phaseElem.nextSiblingElement(QStringLiteral("phase")); } return result; } Phase XliffStorage::phase(const QString& name) const { QDomElement phasegroup; QDomElement phaseElem = phaseElement(m_doc, name, phasegroup); return phaseFromElement(phaseElem); } QMap XliffStorage::allTools() const { QMap result; QDomElement file = m_doc.elementsByTagName(QStringLiteral("file")).at(0).toElement(); QDomElement header = file.firstChildElement(QStringLiteral("header")); QDomElement toolElem = header.firstChildElement(QStringLiteral("tool")); while (!toolElem.isNull()) { Tool tool; tool.tool = toolElem.attribute(QStringLiteral("tool-id")); tool.name = toolElem.attribute(QStringLiteral("tool-name")); tool.version = toolElem.attribute(QStringLiteral("tool-version")); tool.company = toolElem.attribute(QStringLiteral("tool-company")); result.insert(tool.tool, tool); toolElem = toolElem.nextSiblingElement(QStringLiteral("tool")); } return result; } QStringList XliffStorage::sourceFiles(const DocPosition& pos) const { QStringList result; QDomElement elem = unitForPos(pos.entry).firstChildElement(QStringLiteral("context-group")); while (!elem.isNull()) { if (elem.attribute(QStringLiteral("purpose")).contains(QLatin1String("location"))) { QDomElement context = elem.firstChildElement(QStringLiteral("context")); while (!context.isNull()) { QString sourcefile; QString linenumber; const QString contextType = context.attribute(QStringLiteral("context-type")); if (contextType == QLatin1String("sourcefile")) sourcefile = context.text(); else if (contextType == QLatin1String("linenumber")) linenumber = context.text(); if (!(sourcefile.isEmpty() && linenumber.isEmpty())) result.append(sourcefile % ':' % linenumber); context = context.nextSiblingElement(QStringLiteral("context")); } } elem = elem.nextSiblingElement(QStringLiteral("context-group")); } //qSort(result); return result; } static void initNoteFromElement(Note& note, QDomElement elem) { note.content = elem.text(); note.from = elem.attribute(QStringLiteral("from")); note.lang = elem.attribute(QStringLiteral("xml:lang")); if (elem.attribute(QStringLiteral("annotates")) == QLatin1String("source")) note.annotates = Note::Source; else if (elem.attribute(QStringLiteral("annotates")) == QLatin1String("target")) note.annotates = Note::Target; bool ok; note.priority = elem.attribute(QStringLiteral("priority")).toInt(&ok); if (!ok) note.priority = 0; } QVector XliffStorage::notes(const DocPosition& pos) const { QList result; QDomElement elem = entries.at(m_map.at(pos.entry)).firstChildElement(NOTE); while (!elem.isNull()) { Note note; initNoteFromElement(note, elem); result.append(note); elem = elem.nextSiblingElement(NOTE); } - qSort(result); + std::sort(result.begin(), result.end()); return result.toVector(); } QVector XliffStorage::developerNotes(const DocPosition& pos) const { Q_UNUSED(pos); //TODO return QVector(); } Note XliffStorage::setNote(DocPosition pos, const Note& note) { //qCWarning(LOKALIZE_LOG)< result; QDomNodeList notes = m_doc.elementsByTagName(NOTE); int i = notes.size(); while (--i >= 0) { QString from = notes.at(i).toElement().attribute(QStringLiteral("from")); if (!from.isEmpty()) result.insert(from); } return result.toList(); } QVector phaseNotes(QDomDocument m_doc, const QString& phasename, bool remove = false) { QVector result; QDomElement phasegroup; QDomElement phaseElem = phaseElement(m_doc, phasename, phasegroup); QDomElement noteElem = phaseElem.firstChildElement(NOTE); while (!noteElem.isNull()) { Note note; initNoteFromElement(note, noteElem); result.append(note); QDomElement old = noteElem; noteElem = noteElem.nextSiblingElement(NOTE); if (remove) phaseElem.removeChild(old); } return result; } QVector XliffStorage::phaseNotes(const QString& phasename) const { return ::phaseNotes(m_doc, phasename, false); } QVector XliffStorage::setPhaseNotes(const QString& phasename, QVector notes) { QVector result =::phaseNotes(m_doc, phasename, true); QDomElement phasegroup; QDomElement phaseElem = phaseElement(m_doc, phasename, phasegroup); foreach (const Note& note, notes) { QDomElement elem = phaseElem.appendChild(m_doc.createElement(NOTE)).toElement(); elem.appendChild(m_doc.createTextNode(note.content)); if (!note.from.isEmpty()) elem.setAttribute(QStringLiteral("from"), note.from); if (note.priority) elem.setAttribute(QStringLiteral("priority"), note.priority); } return result; } QString XliffStorage::setPhase(const DocPosition& pos, const QString& phase) { QString PHASENAME = QStringLiteral("phase-name"); targetInsert(pos, QString()); //adds if needed QDomElement target = targetForPos(pos.entry); QString result = target.attribute(PHASENAME); if (phase.isEmpty()) target.removeAttribute(PHASENAME); else if (phase != result) target.setAttribute(PHASENAME, phase); return result; } QString XliffStorage::phase(const DocPosition& pos) const { QDomElement target = targetForPos(pos.entry); return target.attribute(QStringLiteral("phase-name")); } QStringList XliffStorage::context(const DocPosition& pos) const { Q_UNUSED(pos); //TODO return QStringList(QString()); } QStringList XliffStorage::matchData(const DocPosition& pos) const { Q_UNUSED(pos); return QStringList(); } QString XliffStorage::id(const DocPosition& pos) const { return unitForPos(pos.entry).attribute(QStringLiteral("id")); } bool XliffStorage::isPlural(const DocPosition& pos) const { return m_plurals.contains(pos.entry); } /* bool XliffStorage::isApproved(const DocPosition& pos) const { return entries.at(m_map.at(pos.entry)).toElement().attribute("approved")=="yes"; } void XliffStorage::setApproved(const DocPosition& pos, bool approved) { static const char* const noyes[]={"no","yes"}; entries.at(m_map.at(pos.entry)).toElement().setAttribute("approved",noyes[approved]); } */ static const QString xliff_states[] = { U("new"), U("needs-translation"), U("needs-l10n"), U("needs-adaptation"), U("translated"), U("needs-review-translation"), U("needs-review-l10n"), U("needs-review-adaptation"), U("final"), U("signed-off") }; TargetState stringToState(const QString& state) { int i = sizeof(xliff_states) / sizeof(QString); while (--i > 0 && state != xliff_states[i]) ; return TargetState(i); } TargetState XliffStorage::setState(const DocPosition& pos, TargetState state) { targetInsert(pos, QString()); //adds if needed QDomElement target = targetForPos(pos.entry); TargetState prev = stringToState(target.attribute(QStringLiteral("state"))); target.setAttribute(QStringLiteral("state"), xliff_states[state]); unitForPos(pos.entry).setAttribute(QStringLiteral("approved"), noyes[state == SignedOff]); return prev; } TargetState XliffStorage::state(const DocPosition& pos) const { QDomElement target = targetForPos(pos.entry); if (!target.hasAttribute(QStringLiteral("state")) && unitForPos(pos.entry).attribute(QStringLiteral("approved")) == QLatin1String("yes")) return SignedOff; return stringToState(target.attribute(QStringLiteral("state"))); } bool XliffStorage::isEmpty(const DocPosition& pos) const { ContentEditingData data(ContentEditingData::CheckLength); return content(targetForPos(pos.entry), &data).isEmpty(); } bool XliffStorage::isEquivTrans(const DocPosition& pos) const { return targetForPos(pos.entry).attribute(QStringLiteral("equiv-trans")) != QLatin1String("no"); } void XliffStorage::setEquivTrans(const DocPosition& pos, bool equivTrans) { targetForPos(pos.entry).setAttribute(QStringLiteral("equiv-trans"), noyes[equivTrans]); } QDomElement XliffStorage::unitForPos(int pos) const { if (pos < size()) return entries.at(m_map.at(pos)).toElement(); return binEntries.at(pos - size()).toElement(); } QDomElement XliffStorage::targetForPos(int pos) const { return unitForPos(pos).firstChildElement(bintargettarget[pos < size()]); } QDomElement XliffStorage::sourceForPos(int pos) const { return unitForPos(pos).firstChildElement(binsourcesource[pos < size()]); } int XliffStorage::binUnitsCount() const { return binEntries.size(); } int XliffStorage::unitById(const QString& id) const { return m_unitsById.contains(id) ? m_unitsById.value(id) : -1; } QString XliffStorage::originalOdfFilePath() { QDomElement file = m_doc.elementsByTagName(QStringLiteral("file")).at(0).toElement(); return file.attribute(QStringLiteral("original")); } void XliffStorage::setOriginalOdfFilePath(const QString& odfFilePath) { QDomElement file = m_doc.elementsByTagName(QStringLiteral("file")).at(0).toElement(); return file.setAttribute(QStringLiteral("original"), odfFilePath); } //END STORAGE TRANSLATION diff --git a/src/glossary/glossary.cpp b/src/glossary/glossary.cpp index 67c9cbd..65f8516 100644 --- a/src/glossary/glossary.cpp +++ b/src/glossary/glossary.cpp @@ -1,736 +1,736 @@ /* **************************************************************************** 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) { QTime 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://qtsoftware.com/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()); //qCDebug(LOKALIZE_LOG)<<"filter:"<rowCount(); 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()) { - qSort(busyIdNumbers); + 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.toList(); } 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); //qCDebug(LOKALIZE_LOG)<<"started walking over"<& 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/glossary/glossarywindow.cpp b/src/glossary/glossarywindow.cpp index c1d948b..90e1155 100644 --- a/src/glossary/glossarywindow.cpp +++ b/src/glossary/glossarywindow.cpp @@ -1,563 +1,563 @@ /* **************************************************************************** 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 "glossarywindow.h" #include "lokalize_debug.h" #include "glossary.h" #include "project.h" #include "languagelistmodel.h" #include "ui_termedit.h" #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace GlossaryNS; //BEGIN GlossaryTreeView GlossaryTreeView::GlossaryTreeView(QWidget *parent) : QTreeView(parent) { setSortingEnabled(true); sortByColumn(GlossaryModel::English, Qt::AscendingOrder); setItemsExpandable(false); setAllColumnsShowFocus(true); /* setSelectionMode(QAbstractItemView::ExtendedSelection); setSelectionBehavior(QAbstractItemView::SelectRows);*/ } static QByteArray modelIndexToId(const QModelIndex& item) { return item.sibling(item.row(), 0).data(Qt::DisplayRole).toByteArray(); } void GlossaryTreeView::currentChanged(const QModelIndex& current, const QModelIndex&/* previous*/) { if (current.isValid()) { //QModelIndex item=static_cast(model())->mapToSource(current); //emit currentChanged(item.row()); emit currentChanged(modelIndexToId(current)); scrollTo(current); } } void GlossaryTreeView::selectRow(int i) { QSortFilterProxyModel* proxyModel = static_cast(model()); GlossaryModel* sourceModel = static_cast(proxyModel->sourceModel()); //sourceModel->forceReset(); setCurrentIndex(proxyModel->mapFromSource(sourceModel->index(i, 0))); } //END GlossaryTreeView //BEGIN SubjectFieldModel //typedef QStringListModel SubjectFieldModel; #if 0 class SubjectFieldModel: public QAbstractItemModel { public: //Q_OBJECT SubjectFieldModel(QObject* parent); ~SubjectFieldModel() {} QModelIndex index(int row, int column, const QModelIndex& parent = QModelIndex()) const; QModelIndex parent(const QModelIndex&) const; int rowCount(const QModelIndex& parent = QModelIndex()) const; int columnCount(const QModelIndex& parent = QModelIndex()) const; QVariant data(const QModelIndex&, int role = Qt::DisplayRole) const; bool setData(const QModelIndex&, const QVariant&, int role = Qt::EditRole); bool setItemData(const QModelIndex& index, const QMap& roles); bool insertRows(int row, int count, const QModelIndex& parent = QModelIndex()); Qt::ItemFlags flags(const QModelIndex&) const; /*private: Catalog* m_catalog;*/ }; inline SubjectFieldModel::SubjectFieldModel(QObject* parent) : QAbstractItemModel(parent) // , m_catalog(catalog) { } QModelIndex SubjectFieldModel::index(int row, int column, const QModelIndex& /*parent*/) const { return createIndex(row, column); } Qt::ItemFlags SubjectFieldModel::flags(const QModelIndex&) const { return Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsEnabled; } QModelIndex SubjectFieldModel::parent(const QModelIndex& /*index*/) const { return QModelIndex(); } int SubjectFieldModel::columnCount(const QModelIndex& parent) const { if (parent.isValid()) return 0; return 1; } /* inline Qt::ItemFlags SubjectFieldModel::flags ( const QModelIndex & index ) const { if (index.column()==FuzzyFlag) return Qt::ItemIsSelectable|Qt::ItemIsUserCheckable|Qt::ItemIsEnabled; return QAbstractItemModel::flags(index); }*/ int SubjectFieldModel::rowCount(const QModelIndex& parent) const { if (parent.isValid()) return 0; return Project::instance()->glossary()->subjectFields.size(); } QVariant SubjectFieldModel::data(const QModelIndex& index, int role) const { if (role == Qt::DisplayRole || role == Qt::EditRole) return Project::instance()->glossary()->subjectFields.at(index.row()); return QVariant(); } bool SubjectFieldModel::insertRows(int row, int count, const QModelIndex& parent) { beginInsertRows(parent, row, row + count - 1); QStringList& subjectFields = Project::instance()->glossary()->subjectFields; while (--count >= 0) subjectFields.insert(row + count, QString()); endInsertRows(); return true; } bool SubjectFieldModel::setData(const QModelIndex& index, const QVariant& value, int role) { qCDebug(LOKALIZE_LOG) << role; QStringList& subjectFields = Project::instance()->glossary()->subjectFields; subjectFields[index.row()] = value.toString(); return true; } bool SubjectFieldModel::setItemData(const QModelIndex& index, const QMap& roles) { if (roles.contains(Qt::EditRole)) { QStringList& subjectFields = Project::instance()->glossary()->subjectFields; subjectFields[index.row()] = roles.value(Qt::EditRole).toString(); } return true; } #endif //END SubjectFieldModel //BEGIN GlossaryWindow GlossaryWindow::GlossaryWindow(QWidget *parent) : KMainWindow(parent) , m_browser(new GlossaryTreeView(this)) , m_proxyModel(new GlossarySortFilterProxyModel(this)) , m_reactOnSignals(true) { //setAttribute(Qt::WA_DeleteOnClose, true); setAttribute(Qt::WA_DeleteOnClose, false); QSplitter* splitter = new QSplitter(Qt::Horizontal, this); setCentralWidget(splitter); m_proxyModel->setFilterKeyColumn(-1); m_proxyModel->setDynamicSortFilter(true);; GlossaryModel* model = new GlossaryModel(this); m_proxyModel->setSourceModel(model); m_browser->setModel(m_proxyModel); m_browser->setUniformRowHeights(true); m_browser->setAutoScroll(true); m_browser->setColumnHidden(GlossaryModel::ID, true); m_browser->setColumnWidth(GlossaryModel::English, m_browser->columnWidth(GlossaryModel::English) * 2); //man this is HACK y m_browser->setColumnWidth(GlossaryModel::Target, m_browser->columnWidth(GlossaryModel::Target) * 2); m_browser->setAlternatingRowColors(true); //left QWidget* w = new QWidget(splitter); QVBoxLayout* layout = new QVBoxLayout(w); m_filterEdit = new QLineEdit(w); m_filterEdit->setClearButtonEnabled(true); m_filterEdit->setPlaceholderText(i18n("Quick search...")); m_filterEdit->setFocus(); m_filterEdit->setToolTip(i18nc("@info:tooltip", "Activated by Ctrl+L.") + ' ' + i18nc("@info:tooltip", "Accepts regular expressions")); new QShortcut(Qt::CTRL + Qt::Key_L, this, SLOT(setFocus()), 0, Qt::WidgetWithChildrenShortcut); connect(m_filterEdit, &QLineEdit::textChanged, m_proxyModel, &GlossaryNS::GlossarySortFilterProxyModel::setFilterRegExp); layout->addWidget(m_filterEdit); layout->addWidget(m_browser); { QPushButton* addBtn = new QPushButton(w); connect(addBtn, &QPushButton::clicked, this, QOverload<>::of(&GlossaryWindow::newTermEntry)); QPushButton* rmBtn = new QPushButton(w); connect(rmBtn, &QPushButton::clicked, this, QOverload<>::of(&GlossaryWindow::rmTermEntry)); KGuiItem::assign(addBtn, KStandardGuiItem::add()); KGuiItem::assign(rmBtn, KStandardGuiItem::remove()); QPushButton* restoreBtn = new QPushButton(i18nc("@action:button reloads glossary from disk", "Restore from disk"), w); restoreBtn->setToolTip(i18nc("@info:tooltip", "Reload glossary from disk, discarding any changes")); connect(restoreBtn, &QPushButton::clicked, this, &GlossaryWindow::restore); QWidget* btns = new QWidget(w); QHBoxLayout* btnsLayout = new QHBoxLayout(btns); btnsLayout->addWidget(addBtn); btnsLayout->addWidget(rmBtn); btnsLayout->addWidget(restoreBtn); layout->addWidget(btns); //QWidget::setTabOrder(m_browser,addBtn); QWidget::setTabOrder(addBtn, rmBtn); QWidget::setTabOrder(rmBtn, restoreBtn); QWidget::setTabOrder(restoreBtn, m_filterEdit); } QWidget::setTabOrder(m_filterEdit, m_browser); splitter->addWidget(w); //right m_editor = new QWidget(splitter); m_editor->hide(); Ui_TermEdit ui_termEdit; ui_termEdit.setupUi(m_editor); splitter->addWidget(m_editor); Project* project = Project::instance(); m_sourceTermsModel = new TermsListModel(project->glossary(), project->sourceLangCode(), this); m_targetTermsModel = new TermsListModel(project->glossary(), project->targetLangCode(), this); ui_termEdit.sourceTermsView->setModel(m_sourceTermsModel); ui_termEdit.targetTermsView->setModel(m_targetTermsModel); connect(ui_termEdit.addEngTerm, &QToolButton::clicked, ui_termEdit.sourceTermsView, &TermListView::addTerm); connect(ui_termEdit.remEngTerm, &QToolButton::clicked, ui_termEdit.sourceTermsView, &TermListView::rmTerms); connect(ui_termEdit.addTargetTerm, &QToolButton::clicked, ui_termEdit.targetTermsView, &TermListView::addTerm); connect(ui_termEdit.remTargetTerm, &QToolButton::clicked, ui_termEdit.targetTermsView, &TermListView::rmTerms); m_sourceTermsView = ui_termEdit.sourceTermsView; m_targetTermsView = ui_termEdit.targetTermsView; m_subjectField = ui_termEdit.subjectField; m_definition = ui_termEdit.definition; m_definitionLang = ui_termEdit.definitionLang; //connect (m_english,SIGNAL(textChanged()), this,SLOT(applyEntryChange())); //connect (m_target,SIGNAL(textChanged()), this,SLOT(applyEntryChange())); //connect (m_definition,SIGNAL(editingFinished()),this,SLOT(applyEntryChange())); //connect (m_definition,SIGNAL(textChanged()),this,SLOT(applyEntryChange())); //connect (m_subjectField,SIGNAL(editTextChanged(QString)),this,SLOT(applyEntryChange())); connect(m_subjectField->lineEdit(), &QLineEdit::editingFinished, this, &GlossaryWindow::applyEntryChange); //m_subjectField->addItems(Project::instance()->glossary()->subjectFields()); //m_subjectField->setModel(new SubjectFieldModel(this)); QStringList subjectFields = Project::instance()->glossary()->subjectFields(); - qSort(subjectFields); + std::sort(subjectFields.begin(), subjectFields.end()); QStringListModel* subjectFieldsModel = new QStringListModel(this); subjectFieldsModel->setStringList(subjectFields); m_subjectField->setModel(subjectFieldsModel); connect(m_browser, QOverload::of(&GlossaryTreeView::currentChanged), this, &GlossaryWindow::currentChanged); connect(m_browser, QOverload::of(&GlossaryTreeView::currentChanged), this, &GlossaryWindow::showEntryInEditor); connect(m_definitionLang, QOverload::of(&KComboBox::activated), this, &GlossaryWindow::showDefinitionForLang); m_definitionLang->setModel(LanguageListModel::emptyLangInstance()->sortModel()); m_definitionLang->setCurrentIndex(LanguageListModel::emptyLangInstance()->sortModelRowForLangCode(m_defLang));//empty lang //TODO //connect(m_targetTermsModel,SIGNAL(dataChanged(QModelIndex,QModelIndex)),m_browser,SLOT(setFocus())); setAutoSaveSettings(QLatin1String("GlossaryWindow"), true); //Glossary* glossary=Project::instance()->glossary(); /*setCaption(i18nc("@title:window","Glossary"), !glossary->changedIds.isEmpty()||!glossary->addedIds.isEmpty()||!glossary->removedIds.isEmpty()); */ } void GlossaryWindow::setFocus() { m_filterEdit->setFocus(); m_filterEdit->selectAll(); } void GlossaryWindow::showEntryInEditor(const QByteArray& id) { if (m_editor->isVisible()) applyEntryChange(); else m_editor->show(); m_id = id; m_reactOnSignals = false; Project* project = Project::instance(); Glossary* glossary = project->glossary(); m_subjectField->setCurrentItem(glossary->subjectField(id),/*insert*/true); QStringList langsToTry = QStringList(m_defLang) << QStringLiteral("en") << QStringLiteral("en_US") << project->targetLangCode(); foreach (const QString& lang, langsToTry) { QString d = glossary->definition(m_id, lang); if (!d.isEmpty()) { if (m_defLang != lang) m_definitionLang->setCurrentIndex(LanguageListModel::emptyLangInstance()->sortModelRowForLangCode(lang)); m_defLang = lang; break; } } m_definition->setPlainText(glossary->definition(m_id, m_defLang)); m_sourceTermsModel->setEntry(id); m_targetTermsModel->setEntry(id); //m_sourceTermsModel->setStringList(glossary->terms(id,project->sourceLangCode())); //m_targetTermsModel->setStringList(glossary->terms(id,project->targetLangCode())); m_reactOnSignals = true; } void GlossaryWindow::currentChanged(int i) { Q_UNUSED(i); m_reactOnSignals = false; m_editor->show(); m_reactOnSignals = true; } void GlossaryWindow::showDefinitionForLang(int langModelIndex) { applyEntryChange(); m_defLang = LanguageListModel::emptyLangInstance()->langCodeForSortModelRow(langModelIndex); m_definition->setPlainText(Project::instance()->glossary()->definition(m_id, m_defLang)); } void GlossaryWindow::applyEntryChange() { if (!m_reactOnSignals || !m_browser->currentIndex().isValid()) return; QByteArray id = m_id; //modelIndexToId(m_browser->currentIndex()); Project* project = Project::instance(); Glossary* glossary = project->glossary(); if (m_subjectField->currentText() != glossary->subjectField(id)) glossary->setSubjectField(id, QString(), m_subjectField->currentText()); if (m_definition->toPlainText() != glossary->definition(id, m_defLang)) glossary->setDefinition(id, m_defLang, m_definition->toPlainText()); //HACK to force finishing of the listview editing QWidget* prevFocusWidget = QApplication::focusWidget(); m_browser->setFocus(); if (prevFocusWidget) prevFocusWidget->setFocus(); // QSortFilterProxyModel* proxyModel=static_cast(model()); //GlossaryModel* sourceModel=static_cast(m_proxyModel->sourceModel()); const QModelIndex& idx = m_proxyModel->mapToSource(m_browser->currentIndex()); if (!idx.isValid()) return; //TODO display filename, optionally stripped like for filetab names setCaption(i18nc("@title:window", "Glossary"), !glossary->isClean()); } void GlossaryWindow::selectEntry(const QByteArray& id) { //let it fetch the rows QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents | QEventLoop::ExcludeSocketNotifiers | QEventLoop::WaitForMoreEvents, 100); QModelIndexList items = m_proxyModel->match(m_proxyModel->index(0, 0), Qt::DisplayRole, QVariant(id), 1, 0); if (items.count()) { m_browser->setCurrentIndex(items.first()); m_browser->scrollTo(items.first(), QAbstractItemView::PositionAtCenter); //qCDebug(LOKALIZE_LOG)<setCurrentIndex(QModelIndex()); showEntryInEditor(id); //qCDebug(LOKALIZE_LOG)<(m_proxyModel->sourceModel()); QByteArray id = sourceModel->appendRow(_english, _target); selectEntry(id); } void GlossaryWindow::rmTermEntry() { rmTermEntry(-1); } void GlossaryWindow::rmTermEntry(int i) { setCaption(i18nc("@title:window", "Glossary"), true); //QSortFilterProxyModel* proxyModel=static_cast(model()); GlossaryModel* sourceModel = static_cast(m_proxyModel->sourceModel()); if (i == -1) { //NOTE actually we should remove selected items, not current one const QModelIndex& current = m_browser->currentIndex(); if (!current.isValid()) return; i = m_proxyModel->mapToSource(current).row(); } sourceModel->removeRow(i); } void GlossaryWindow::restore() { setCaption(i18nc("@title:window", "Glossary"), false); Glossary* glossary = Project::instance()->glossary(); glossary->load(glossary->path()); m_reactOnSignals = false; showEntryInEditor(m_id); m_reactOnSignals = true; } bool GlossaryWindow::save() { //TODO add error message return Project::instance()->glossary()->save(); } bool GlossaryWindow::queryClose() { Glossary* glossary = Project::instance()->glossary(); applyEntryChange(); if (glossary->isClean()) return true; switch (KMessageBox::warningYesNoCancel(this, i18nc("@info", "The glossary contains unsaved changes.\n\ Do you want to save your changes or discard them?"), i18nc("@title:window", "Warning"), KStandardGuiItem::save(), KStandardGuiItem::discard())) { case KMessageBox::Yes: return save(); case KMessageBox::No: restore(); return true; default: return false; } } //END GlossaryWindow void TermsListModel::setEntry(const QByteArray& id) { m_id = id; QStringList terms = m_glossary->terms(m_id, m_lang); terms.append(QString()); //allow adding new terms setStringList(terms); } bool TermsListModel::setData(const QModelIndex& index, const QVariant& value, int role) { Q_UNUSED(role); m_glossary->setTerm(m_id, m_lang, index.row(), value.toString()); setEntry(m_id); //allow adding new terms return true; } bool TermsListModel::removeRows(int row, int count, const QModelIndex& parent) { Q_UNUSED(count) if (row == rowCount() - 1) return false;// cannot delete non-existing item m_glossary->rmTerm(m_id, m_lang, row); return QStringListModel::removeRows(row, 1, parent); } void TermListView::addTerm() { setCurrentIndex(model()->index(model()->rowCount() - 1, 0)); edit(currentIndex()); } void TermListView::rmTerms() { foreach (const QModelIndex& row, selectionModel()->selectedRows()) model()->removeRow(row.row()); } diff --git a/src/mergemode/mergecatalog.cpp b/src/mergemode/mergecatalog.cpp index b6f4d80..3427db4 100644 --- a/src/mergemode/mergecatalog.cpp +++ b/src/mergemode/mergecatalog.cpp @@ -1,334 +1,334 @@ /* **************************************************************************** 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 "mergecatalog.h" #include "lokalize_debug.h" #include "catalog_private.h" #include "catalogstorage.h" #include "cmd.h" #include #include #include MergeCatalog::MergeCatalog(QObject* parent, Catalog* baseCatalog, bool saveChanges) : Catalog(parent) , m_baseCatalog(baseCatalog) , m_unmatchedCount(0) , m_modified(false) { setActivePhase(baseCatalog->activePhase(), baseCatalog->activePhaseRole()); if (saveChanges) { connect(baseCatalog, &Catalog::signalEntryModified, this, &MergeCatalog::copyFromBaseCatalogIfInDiffIndex); connect(baseCatalog, QOverload<>::of(&Catalog::signalFileSaved), this, &MergeCatalog::save); } } void MergeCatalog::copyFromBaseCatalog(const DocPosition& pos, int options) { bool a = m_mergeDiffIndex.contains(pos.entry); if (options & EvenIfNotInDiffIndex || !a) { //sync changes DocPosition ourPos = pos; if ((ourPos.entry = m_map.at(ourPos.entry)) == -1) return; //note the explicit use of map... if (m_storage->isApproved(ourPos) != m_baseCatalog->isApproved(pos)) //qCWarning(LOKALIZE_LOG)<setApproved(ourPos, m_baseCatalog->isApproved(pos)); DocPos p(pos); if (!m_originalHashes.contains(p)) m_originalHashes[p] = qHash(m_storage->target(ourPos)); m_storage->setTarget(ourPos, m_baseCatalog->target(pos)); setModified(ourPos, true); if (options & EvenIfNotInDiffIndex && a) m_mergeDiffIndex.removeAll(pos.entry); m_modified = true; emit signalEntryModified(pos); } } QString MergeCatalog::msgstr(const DocPosition& pos) const { DocPosition us = pos; us.entry = m_map.at(pos.entry); return (us.entry == -1) ? QString() : Catalog::msgstr(us); } bool MergeCatalog::isApproved(uint index) const { return (m_map.at(index) == -1) ? false : Catalog::isApproved(m_map.at(index)); } TargetState MergeCatalog::state(const DocPosition& pos) const { DocPosition us = pos; us.entry = m_map.at(pos.entry); return (us.entry == -1) ? New : Catalog::state(us); } bool MergeCatalog::isPlural(uint index) const { //sanity if (m_map.at(index) == -1) { qCWarning(LOKALIZE_LOG) << "!!! index" << index << "m_map.at(index)" << m_map.at(index) << "numberOfEntries()" << numberOfEntries(); return false; } return Catalog::isPlural(m_map.at(index)); } bool MergeCatalog::isPresent(const int& entry) const { return m_map.at(entry) != -1; } MatchItem MergeCatalog::calcMatchItem(const DocPosition& basePos, const DocPosition& mergePos) { CatalogStorage& baseStorage = *(m_baseCatalog->m_storage); CatalogStorage& mergeStorage = *(m_storage); MatchItem item(mergePos.entry, basePos.entry, true); //TODO make more robust, perhaps after XLIFF? QStringList baseMatchData = baseStorage.matchData(basePos); QStringList mergeMatchData = mergeStorage.matchData(mergePos); //compare ids item.score += 40 * ((baseMatchData.isEmpty() && mergeMatchData.isEmpty()) ? baseStorage.id(basePos) == mergeStorage.id(mergePos) : baseMatchData == mergeMatchData); //TODO look also for changed/new s //translation isn't changed if (baseStorage.targetAllForms(basePos, true) == mergeStorage.targetAllForms(mergePos, true)) { item.translationIsDifferent = baseStorage.isApproved(basePos) != mergeStorage.isApproved(mergePos); item.score += 29 + 1 * item.translationIsDifferent; } #if 0 if (baseStorage.source(basePos) == "%1 (%2)") { qCDebug(LOKALIZE_LOG) << "BASE"; qCDebug(LOKALIZE_LOG) << m_baseCatalog->url(); qCDebug(LOKALIZE_LOG) << basePos.entry; qCDebug(LOKALIZE_LOG) << baseStorage.source(basePos); qCDebug(LOKALIZE_LOG) << baseMatchData.first(); qCDebug(LOKALIZE_LOG) << "MERGE"; qCDebug(LOKALIZE_LOG) << url(); qCDebug(LOKALIZE_LOG) << mergePos.entry; qCDebug(LOKALIZE_LOG) << mergeStorage.source(mergePos); qCDebug(LOKALIZE_LOG) << mergeStorage.matchData(mergePos).first(); qCDebug(LOKALIZE_LOG) << item.score; qCDebug(LOKALIZE_LOG) << ""; } #endif return item; } static QString strip(QString source) { source.remove('\n'); return source; } int MergeCatalog::loadFromUrl(const QString& filePath) { int errorLine = Catalog::loadFromUrl(filePath); if (Q_UNLIKELY(errorLine != 0)) return errorLine; //now calc the entry mapping CatalogStorage& baseStorage = *(m_baseCatalog->m_storage); CatalogStorage& mergeStorage = *(m_storage); DocPosition i(0); int size = baseStorage.size(); int mergeSize = mergeStorage.size(); m_map.fill(-1, size); QMultiMap backMap; //will be used to maintain one-to-one relation //precalc for fast lookup QMultiHash mergeMap; while (i.entry < mergeSize) { mergeMap.insert(strip(mergeStorage.source(i)), i.entry); ++(i.entry); } i.entry = 0; while (i.entry < size) { QString key = strip(baseStorage.source(i)); const QList& entries = mergeMap.values(key); QList scores; int k = entries.size(); if (k) { while (--k >= 0) scores << calcMatchItem(i, DocPosition(entries.at(k))); - qSort(scores.begin(), scores.end(), qGreater()); + std::sort(scores.begin(), scores.end(), qGreater()); m_map[i.entry] = scores.first().mergeEntry; backMap.insert(scores.first().mergeEntry, i.entry); if (scores.first().translationIsDifferent) m_mergeDiffIndex.append(i.entry); } ++(i.entry); } //maintain one-to-one relation const QList& mergePositions = backMap.uniqueKeys(); foreach (int mergePosition, mergePositions) { const QList& basePositions = backMap.values(mergePosition); if (basePositions.size() == 1) continue; //qCDebug(LOKALIZE_LOG)<<"kv"< scores; foreach (int value, basePositions) scores << calcMatchItem(DocPosition(value), mergePosition); - qSort(scores.begin(), scores.end(), qGreater()); + std::sort(scores.begin(), scores.end(), qGreater()); int i = scores.size(); while (--i > 0) { //qCDebug(LOKALIZE_LOG)<<"erasing"<::iterator it = mergeMap.begin(); while (it != mergeMap.end()) { //qCWarning(LOKALIZE_LOG)<msgstr(pos) != msgstr(pos); m_baseCatalog->beginMacro(i18nc("@item Undo action item", "Accept change in translation")); if (m_baseCatalog->state(pos) != state(pos)) SetStateCmd::instantiateAndPush(m_baseCatalog, pos, state(pos)); if (changeContents) { pos.offset = 0; if (!m_baseCatalog->msgstr(pos).isEmpty()) m_baseCatalog->push(new DelTextCmd(m_baseCatalog, pos, m_baseCatalog->msgstr(pos))); m_baseCatalog->push(new InsTextCmd(m_baseCatalog, pos, msgstr(pos))); } ////////this is NOT done automatically by BaseCatalogEntryChanged slot bool remove = true; if (isPlural(pos.entry)) { DocPosition p = pos; p.form = qMin(m_baseCatalog->numberOfPluralForms(), numberOfPluralForms()); //just sanity check p.form = qMax((int)p.form, 1); //just sanity check while ((--(p.form)) >= 0 && remove) remove = m_baseCatalog->msgstr(p) == msgstr(p); } if (remove) removeFromDiffIndex(pos.entry); m_baseCatalog->endMacro(); } void MergeCatalog::copyToBaseCatalog(int options) { DocPosition pos; pos.offset = 0; bool insHappened = false; QLinkedList changed = differentEntries(); foreach (int entry, changed) { pos.entry = entry; if (options & EmptyOnly && !m_baseCatalog->isEmpty(entry)) continue; if (options & HigherOnly && !m_baseCatalog->isEmpty(entry) && m_baseCatalog->state(pos) >= state(pos)) continue; int formsCount = (m_baseCatalog->isPlural(entry)) ? m_baseCatalog->numberOfPluralForms() : 1; pos.form = 0; while (pos.form < formsCount) { //m_baseCatalog->push(new DelTextCmd(m_baseCatalog,pos,m_baseCatalog->msgstr(pos.entry,0))); ? //some forms may still contain translation... if (!(options & EmptyOnly && !m_baseCatalog->isEmpty(pos)) /*&& !(options&HigherOnly && !m_baseCatalog->isEmpty(pos) && m_baseCatalog->state(pos)>=state(pos))*/) { if (!insHappened) { //stop basecatalog from sending signalEntryModified to us //when we are the ones who does the modification disconnect(m_baseCatalog, &Catalog::signalEntryModified, this, &MergeCatalog::copyFromBaseCatalogIfInDiffIndex); insHappened = true; m_baseCatalog->beginMacro(i18nc("@item Undo action item", "Accept all new translations")); } copyToBaseCatalog(pos); /// /// /// m_baseCatalog->push(new InsTextCmd(m_baseCatalog,pos,mergeCatalog.msgstr(pos))); /// /// } ++(pos.form); } /// /// /// removeFromDiffIndex(m_pos.entry); /// /// } if (insHappened) { m_baseCatalog->endMacro(); //reconnect to catch all modifications coming from outside connect(m_baseCatalog, &Catalog::signalEntryModified, this, &MergeCatalog::copyFromBaseCatalogIfInDiffIndex); } } diff --git a/src/msgctxtview.cpp b/src/msgctxtview.cpp index 8219729..3eca5f3 100644 --- a/src/msgctxtview.cpp +++ b/src/msgctxtview.cpp @@ -1,332 +1,332 @@ /* **************************************************************************** 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 "msgctxtview.h" #include "noteeditor.h" #include "catalog.h" #include "cmd.h" #include "prefs_lokalize.h" #include "project.h" #include "lokalize_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include MsgCtxtView::MsgCtxtView(QWidget* parent, Catalog* catalog) : QDockWidget(i18nc("@title toolview name", "Unit metadata"), parent) , m_browser(new QTextBrowser(this)) , m_editor(0) , m_catalog(catalog) , m_selection(0) , m_offset(0) , m_hasInfo(false) , m_hasErrorNotes(false) , m_pologyProcessInProgress(0) , m_pologyStartedReceivingOutput(false) { setObjectName(QStringLiteral("msgCtxtView")); QWidget* main = new QWidget(this); setWidget(main); m_stackedLayout = new QStackedLayout(main); m_stackedLayout->addWidget(m_browser); m_browser->viewport()->setBackgroundRole(QPalette::Background); m_browser->setOpenLinks(false); connect(m_browser, &QTextBrowser::anchorClicked, this, &MsgCtxtView::anchorClicked); } MsgCtxtView::~MsgCtxtView() { } const QString MsgCtxtView::BR = "
"; void MsgCtxtView::cleanup() { m_unfinishedNotes.clear(); m_tempNotes.clear(); } void MsgCtxtView::gotoEntry(const DocPosition& pos, int selection) { m_entry = DocPos(pos); m_selection = selection; m_offset = pos.offset; QTimer::singleShot(0, this, &MsgCtxtView::process); QTimer::singleShot(0, this, &MsgCtxtView::pology); } void MsgCtxtView::process() { if (m_catalog->numberOfEntries() <= m_entry.entry) return;//because of Qt::QueuedConnection if (m_stackedLayout->currentIndex()) m_unfinishedNotes[m_prevEntry] = qMakePair(m_editor->note(), m_editor->noteIndex()); if (m_unfinishedNotes.contains(m_entry)) { addNoteUI(); m_editor->setNote(m_unfinishedNotes.value(m_entry).first, m_unfinishedNotes.value(m_entry).second); } else m_stackedLayout->setCurrentIndex(0); m_prevEntry = m_entry; m_browser->clear(); if (m_tempNotes.contains(m_entry.entry)) { QString html = i18nc("@info notes to translation unit which expire when the catalog is closed", "Temporary notes:"); html += MsgCtxtView::BR; foreach (const QString& note, m_tempNotes.values(m_entry.entry)) html += note.toHtmlEscaped() + MsgCtxtView::BR; html += MsgCtxtView::BR; m_browser->insertHtml(html.replace('\n', MsgCtxtView::BR)); } QString phaseName = m_catalog->phase(m_entry.toDocPosition()); if (!phaseName.isEmpty()) { Phase phase = m_catalog->phase(phaseName); QString html = i18nc("@info translation unit metadata", "Phase:
"); if (phase.date.isValid()) html += QString(QStringLiteral("%1: ")).arg(phase.date.toString(Qt::ISODate)); html += phase.process.toHtmlEscaped(); if (!phase.contact.isEmpty()) html += QString(QStringLiteral(" (%1)")).arg(phase.contact.toHtmlEscaped()); m_browser->insertHtml(html + MsgCtxtView::BR); } const QVector notes = m_catalog->notes(m_entry.toDocPosition()); m_hasErrorNotes = false; foreach (const Note& note, notes) m_hasErrorNotes = m_hasErrorNotes || note.content.contains(QLatin1String("[ERROR]")); int realOffset = displayNotes(m_browser, m_catalog->notes(m_entry.toDocPosition()), m_entry.form, m_catalog->capabilities()&MultipleNotes); QString html; foreach (const Note& note, m_catalog->developerNotes(m_entry.toDocPosition())) { html += MsgCtxtView::BR + escapeWithLinks(note.content).replace('\n', BR); } QStringList sourceFiles = m_catalog->sourceFiles(m_entry.toDocPosition()); if (!sourceFiles.isEmpty()) { html += i18nc("@info PO comment parsing", "
Files:
"); foreach (const QString &sourceFile, sourceFiles) html += QString(QStringLiteral("%2
")).arg(sourceFile, sourceFile); html.chop(6); } QString msgctxt = m_catalog->context(m_entry.entry).first(); if (!msgctxt.isEmpty()) html += i18nc("@info PO comment parsing", "
Context:
") + msgctxt.toHtmlEscaped(); QTextCursor t = m_browser->textCursor(); t.movePosition(QTextCursor::End); m_browser->setTextCursor(t); m_browser->insertHtml(html); t.movePosition(QTextCursor::Start); t.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, realOffset + m_offset); t.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, m_selection); m_browser->setTextCursor(t); } void MsgCtxtView::pology() { if (Settings::self()->pologyEnabled() && m_pologyProcessInProgress == 0 && QFile::exists(m_catalog->url())) { QString command = Settings::self()->pologyCommandEntry(); command = command.replace(QStringLiteral("%u"), QString::number(m_entry.entry + 1)).replace(QStringLiteral("%f"), QStringLiteral("\"") + m_catalog->url() + QStringLiteral("\"")).replace(QStringLiteral("\n"), QStringLiteral(" ")); m_pologyProcess = new KProcess; m_pologyProcess->setShellCommand(command); m_pologyProcess->setOutputChannelMode(KProcess::SeparateChannels); m_pologyStartedReceivingOutput = false; connect(m_pologyProcess, &KProcess::readyReadStandardOutput, this, &MsgCtxtView::pologyReceivedStandardOutput); connect(m_pologyProcess, &KProcess::readyReadStandardError, this, &MsgCtxtView::pologyReceivedStandardError); - connect(m_pologyProcess, QOverload::of(&KProcess::finished), + connect(m_pologyProcess, QOverload::of(&KProcess::finished), this, &MsgCtxtView::pologyHasFinished); m_pologyData = QStringLiteral("[pology] "); m_pologyProcessInProgress = m_entry.entry + 1; m_pologyProcess->start(); } else if (Settings::self()->pologyEnabled() && m_pologyProcessInProgress > 0) { QTimer::singleShot(1000, this, &MsgCtxtView::pology); } } void MsgCtxtView::pologyReceivedStandardOutput() { if (m_pologyProcessInProgress == m_entry.entry + 1) { if (!m_pologyStartedReceivingOutput) { m_pologyStartedReceivingOutput = true; } const QString grossPologyOutput = m_pologyProcess->readAllStandardOutput(); const QStringList pologyTmpLines = grossPologyOutput.split('\n', QString::SkipEmptyParts); foreach (const QString pologyTmp, pologyTmpLines) { if (pologyTmp.startsWith(QStringLiteral("[note]"))) m_pologyData += pologyTmp; } } } void MsgCtxtView::pologyReceivedStandardError() { if (m_pologyProcessInProgress == m_entry.entry + 1) { if (!m_pologyStartedReceivingOutput) { m_pologyStartedReceivingOutput = true; } m_pologyData += m_pologyProcess->readAllStandardError().replace('\n', MsgCtxtView::BR.toLatin1()); } } void MsgCtxtView::pologyHasFinished() { if (m_pologyProcessInProgress == m_entry.entry + 1) { if (!m_pologyStartedReceivingOutput) { m_pologyStartedReceivingOutput = true; const QString grossPologyOutput = m_pologyProcess->readAllStandardOutput(); const QStringList pologyTmpLines = grossPologyOutput.split('\n', QString::SkipEmptyParts); if (pologyTmpLines.count() == 0) { m_pologyData += i18nc("@info The pology command didn't return anything", "(empty)"); } else { foreach (const QString pologyTmp, pologyTmpLines) { if (pologyTmp.startsWith(QStringLiteral("[note]"))) m_pologyData += pologyTmp; } } } if (!m_tempNotes.value(m_entry.entry).startsWith(QStringLiteral("Failed rules:"))) { //This was not opened by pology //Delete the previous pology notes if (m_tempNotes.value(m_entry.entry).startsWith(QStringLiteral("[pology] "))) { m_tempNotes.remove(m_entry.entry); } addTemporaryEntryNote(m_entry.entry, m_pologyData); } } m_pologyProcess->deleteLater(); m_pologyProcessInProgress = 0; } void MsgCtxtView::addNoteUI() { anchorClicked(QUrl(QStringLiteral("note:/add"))); } void MsgCtxtView::anchorClicked(const QUrl& link) { QString path = link.path().mid(1); // minus '/' if (link.scheme() == QLatin1String("note")) { int capabilities = m_catalog->capabilities(); if (!m_editor) { m_editor = new NoteEditor(this); m_stackedLayout->addWidget(m_editor); connect(m_editor, &NoteEditor::accepted, this, &MsgCtxtView::noteEditAccepted); connect(m_editor, &NoteEditor::rejected, this, &MsgCtxtView::noteEditRejected); } m_editor->setNoteAuthors(m_catalog->noteAuthors()); QVector notes = m_catalog->notes(m_entry.toDocPosition()); int noteIndex = -1; //means add new note Note note; if (!path.endsWith(QLatin1String("add"))) { noteIndex = path.toInt(); note = notes.at(noteIndex); } else if (!(capabilities & MultipleNotes) && notes.size()) { noteIndex = 0; //so we don't overwrite the only possible note note = notes.first(); } m_editor->setNote(note, noteIndex); m_editor->setFromFieldVisible(capabilities & KeepsNoteAuthors); m_stackedLayout->setCurrentIndex(1); } else if (link.scheme() == QLatin1String("src")) { int pos = path.lastIndexOf(':'); emit srcFileOpenRequested(path.left(pos), path.midRef(pos + 1).toInt()); } else if (link.scheme().contains(QLatin1String("tp"))) QDesktopServices::openUrl(link); } void MsgCtxtView::noteEditAccepted() { DocPosition pos = m_entry.toDocPosition(); pos.form = m_editor->noteIndex(); m_catalog->push(new SetNoteCmd(m_catalog, pos, m_editor->note())); m_prevEntry.entry = -1; process(); //m_stackedLayout->setCurrentIndex(0); //m_unfinishedNotes.remove(m_entry); noteEditRejected(); } void MsgCtxtView::noteEditRejected() { m_stackedLayout->setCurrentIndex(0); m_unfinishedNotes.remove(m_entry); emit escaped(); } void MsgCtxtView::addNote(DocPosition p, const QString& text) { p.form = -1; m_catalog->push(new SetNoteCmd(m_catalog, p, Note(text))); if (m_entry.entry == p.entry) { m_prevEntry.entry = -1; process(); } } void MsgCtxtView::addTemporaryEntryNote(int entry, const QString& text) { m_tempNotes.insertMulti(entry, text); m_prevEntry.entry = -1; process(); } void MsgCtxtView::removeErrorNotes() { if (!m_hasErrorNotes) return; DocPosition p = m_entry.toDocPosition(); const QVector notes = m_catalog->notes(p); p.form = notes.size(); while (--(p.form) >= 0) { if (notes.at(p.form).content.contains(QLatin1String("[ERROR]"))) m_catalog->push(new SetNoteCmd(m_catalog, p, Note())); } m_prevEntry.entry = -1; process(); } diff --git a/src/project/projectmodel.cpp b/src/project/projectmodel.cpp index f8f815d..4fca735 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 - qSort(potRowsToInsert.begin(), potRowsToInsert.end()); + 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); } } 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 {}; } } 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 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; 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/projectwidget.cpp b/src/project/projectwidget.cpp index 416e426..c4ea285 100644 --- a/src/project/projectwidget.cpp +++ b/src/project/projectwidget.cpp @@ -1,551 +1,551 @@ /* **************************************************************************** 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; 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)); } 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(idx.child(j, 0))); + const KFileItem& childItem(model.itemForIndex(model.index(j, 0, idx))); if (childItem.isDir()) - recursiveAdd(list, idx.child(j, 0)); + 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.child((direction == 1) ? 0 : (m_proxyModel->rowCount(index) - 1), index.column()); 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/tm/jobs.cpp b/src/tm/jobs.cpp index 34fdc57..9ecf211 100644 --- a/src/tm/jobs.cpp +++ b/src/tm/jobs.cpp @@ -1,2135 +1,2135 @@ /* **************************************************************************** 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 "jobs.h" #include "lokalize_debug.h" #include "catalog.h" #include "project.h" #include "diff.h" #include "prefs_lokalize.h" #include "version.h" #include "stemming.h" #include #include #include #include #include #include #include #include #include #include #include using namespace TM; QThreadPool* TM::threadPool() { static QThreadPool* inst = new QThreadPool; return inst; } #ifdef Q_OS_WIN #define U QLatin1String #else #define U QStringLiteral #endif #define TM_DELIMITER '\v' #define TM_SEPARATOR '\b' #define TM_NOTAPPROVED 0x04 static bool stop = false; void TM::cancelAllJobs() { stop = true; } static qlonglong newTMSourceEntryCount = 0; static qlonglong reusedTMSourceEntryCount = 0; /** * splits string into words, removing any markup * * TODO segmentation by sentences... **/ static void doSplit(QString& cleanEn, QStringList& words, QRegExp& rxClean1, const QString& accel ) { static QRegExp rxSplit(QStringLiteral("\\W+|\\d+")); if (!rxClean1.pattern().isEmpty()) cleanEn.replace(rxClean1, QStringLiteral(" ")); cleanEn.remove(accel); words = cleanEn.toLower().split(rxSplit, QString::SkipEmptyParts); if (words.size() > 4) { int i = 0; for (; i < words.size(); ++i) { if (words.at(i).size() < 4) words.removeAt(i--); else if (words.at(i).startsWith('t') && words.at(i).size() == 4) { if (words.at(i) == QLatin1String("then") || words.at(i) == QLatin1String("than") || words.at(i) == QLatin1String("that") || words.at(i) == QLatin1String("this") ) words.removeAt(i--); } } } } static qlonglong getFileId(const QString& path, QSqlDatabase& db) { QSqlQuery query1(db); QString escapedPath = path; escapedPath.replace(QLatin1Char('\''), QLatin1String("''")); QString pathExpr = QStringLiteral("path='") % escapedPath % '\''; if (path.isEmpty()) pathExpr = QStringLiteral("path ISNULL"); if (Q_UNLIKELY(!query1.exec(U("SELECT id FROM files WHERE " "path='") % escapedPath % '\''))) qCWarning(LOKALIZE_LOG) << "select db error: " << query1.lastError().text(); if (Q_LIKELY(query1.next())) { //this is translation of en string that is already present in db qlonglong id = query1.value(0).toLongLong(); query1.clear(); return id; } query1.clear(); //nope, this is new file bool qpsql = (db.driverName() == QLatin1String("QPSQL")); QString sql = QStringLiteral("INSERT INTO files (path) VALUES (?)"); if (qpsql) sql += QLatin1String(" RETURNING id"); query1.prepare(sql); query1.bindValue(0, path); if (Q_LIKELY(query1.exec())) return qpsql ? (query1.next(), query1.value(0).toLongLong()) : query1.lastInsertId().toLongLong(); else qCWarning(LOKALIZE_LOG) << "insert db error: " << query1.lastError().text(); return -1; } static void addToIndex(qlonglong sourceId, QString sourceString, QRegExp& rxClean1, const QString& accel, QSqlDatabase& db) { QStringList words; doSplit(sourceString, words, rxClean1, accel); if (Q_UNLIKELY(words.isEmpty())) return; QSqlQuery query1(db); QByteArray sourceIdStr = QByteArray::number(sourceId, 36); bool isShort = words.size() < 20; int j = words.size(); while (--j >= 0) { // insert word (if we do not have it) if (Q_UNLIKELY(!query1.exec(U("SELECT word, ids_short, ids_long FROM words WHERE " "word='") % words.at(j) % '\''))) qCWarning(LOKALIZE_LOG) << "select error 3: " << query1.lastError().text(); //we _have_ it bool weHaveIt = query1.next(); if (weHaveIt) { //just add new id QByteArray arr; QString field; if (isShort) { arr = query1.value(1).toByteArray(); field = QStringLiteral("ids_short"); } else { arr = query1.value(2).toByteArray(); field = QStringLiteral("ids_long"); } query1.clear(); if (arr.contains(' ' % sourceIdStr % ' ') || arr.startsWith(sourceIdStr + ' ') || arr.endsWith(' ' + sourceIdStr) || arr == sourceIdStr) return;//this string is already indexed query1.prepare(QStringLiteral("UPDATE words SET ") % field % QStringLiteral("=? WHERE word='") % words.at(j) % '\''); if (!arr.isEmpty()) arr += ' '; arr += sourceIdStr; query1.bindValue(0, arr); if (Q_UNLIKELY(!query1.exec())) qCWarning(LOKALIZE_LOG) << "update error 4: " << query1.lastError().text(); } else { query1.clear(); query1.prepare(QStringLiteral("INSERT INTO words (word, ids_short, ids_long) VALUES (?, ?, ?)")); QByteArray idsShort; QByteArray idsLong; if (isShort) idsShort = sourceIdStr; else idsLong = sourceIdStr; query1.bindValue(0, words.at(j)); query1.bindValue(1, idsShort); query1.bindValue(2, idsLong); if (Q_UNLIKELY(!query1.exec())) qCWarning(LOKALIZE_LOG) << "insert error 2: " << query1.lastError().text() ; } } } /** * remove source string from index if there are no other * 'good' entries using it but the entry specified with mainId */ static void removeFromIndex(qlonglong mainId, qlonglong sourceId, QString sourceString, QRegExp& rxClean1, const QString& accel, QSqlDatabase& db) { QStringList words; doSplit(sourceString, words, rxClean1, accel); if (Q_UNLIKELY(words.isEmpty())) return; QSqlQuery query1(db); QByteArray sourceIdStr = QByteArray::number(sourceId, 36); //BEGIN check //TM_NOTAPPROVED=4 if (Q_UNLIKELY(!query1.exec(U("SELECT count(*) FROM main, target_strings WHERE " "main.source=") % QString::number(sourceId) % U(" AND " "main.target=target_strings.id AND " "target_strings.target NOTNULL AND " "main.id!=") % QString::number(mainId) % U(" AND " "(main.bits&4)!=4")))) { qCWarning(LOKALIZE_LOG) << "select error 500: " << query1.lastError().text(); return; } bool exit = query1.next() && (query1.value(0).toLongLong() > 0); query1.clear(); if (exit) return; //END check bool isShort = words.size() < 20; int j = words.size(); while (--j >= 0) { // remove from record for the word (if we do not have it) if (Q_UNLIKELY(!query1.exec(U("SELECT word, ids_short, ids_long FROM words WHERE " "word='") % words.at(j) % '\''))) { qCWarning(LOKALIZE_LOG) << "select error 3: " << query1.lastError().text(); return; } if (!query1.next()) { qCWarning(LOKALIZE_LOG) << "exit here 1"; //we don't have record for the word, so nothing to remove query1.clear(); return; } QByteArray arr; QString field; if (isShort) { arr = query1.value(1).toByteArray(); field = QStringLiteral("ids_short"); } else { arr = query1.value(2).toByteArray(); field = QStringLiteral("ids_long"); } query1.clear(); if (arr.contains(' ' + sourceIdStr + ' ')) arr.replace(' ' + sourceIdStr + ' ', " "); else if (arr.startsWith(sourceIdStr + ' ')) arr.remove(0, sourceIdStr.size() + 1); else if (arr.endsWith(' ' + sourceIdStr)) arr.chop(sourceIdStr.size() + 1); else if (arr == sourceIdStr) arr.clear(); query1.prepare(U("UPDATE words " "SET ") % field % U("=? " "WHERE word='") % words.at(j) % '\''); query1.bindValue(0, arr); if (Q_UNLIKELY(!query1.exec())) qCWarning(LOKALIZE_LOG) << "update error 504: " << query1.lastError().text(); } } static bool doRemoveEntry(qlonglong mainId, QRegExp& rxClean1, const QString& accel, QSqlDatabase& db) { QSqlQuery query1(db); if (Q_UNLIKELY(!query1.exec(U("SELECT source_strings.id, source_strings.source FROM source_strings, main WHERE " "source_strings.id=main.source AND main.id=") + QString::number(mainId)))) return false; if (!query1.next()) return false; const qlonglong sourceId = query1.value(0).toLongLong(); const QString sourceString = query1.value(1).toString(); query1.clear(); if (Q_UNLIKELY(!query1.exec(U("SELECT target_strings.id FROM target_strings, main WHERE target_strings.id=main.target AND main.id=") + QString::number(mainId)))) return false; if (!query1.next()) return false; const qlonglong targetId = query1.value(0).toLongLong(); query1.clear(); query1.exec(QStringLiteral("DELETE FROM main WHERE source=") + QString::number(sourceId) + QStringLiteral(" AND target=") + QString::number(targetId)); if (!query1.exec(QStringLiteral("SELECT count(*) FROM main WHERE source=") + QString::number(sourceId)) || !query1.next()) return false; const bool noSourceLeft = query1.value(0).toInt() == 0; query1.clear(); if (noSourceLeft) { removeFromIndex(mainId, sourceId, sourceString, rxClean1, accel, db); query1.exec(QStringLiteral("DELETE FROM source_strings WHERE id=") + QString::number(sourceId)); } if (!query1.exec(QStringLiteral("SELECT count(*) FROM main WHERE target=") + QString::number(targetId)) || ! query1.next()) return false; const bool noTargetLeft = query1.value(0).toInt() == 0; query1.clear(); if (noTargetLeft) query1.exec(QStringLiteral("DELETE FROM target_strings WHERE id=") + QString::number(targetId)); return true; } static bool doRemoveFile(const QString& filePath, QSqlDatabase& db) { qlonglong fileId = getFileId(filePath, db); QSqlQuery query1(db); if (Q_UNLIKELY(!query1.exec(U("SELECT id FROM files WHERE " "id=") + QString::number(fileId)))) return false; if (!query1.next()) return false; query1.clear(); query1.exec(QStringLiteral("DELETE source_strings FROM source_strings, main WHERE source_strings.id = main.source AND main.file =") + QString::number(fileId)); query1.exec(QStringLiteral("DELETE target_strings FROM target_strings, main WHERE target_strings.id = main.target AND main.file =") + QString::number(fileId)); query1.exec(QStringLiteral("DELETE FROM main WHERE file = ") + QString::number(fileId)); return query1.exec(QStringLiteral("DELETE FROM files WHERE id=") + QString::number(fileId)); } static int doRemoveMissingFiles(QSqlDatabase& db, const QString& dbName, QObject *job) { int deletedFiles = 0; QSqlQuery query1(db); if (Q_UNLIKELY(!query1.exec(U("SELECT files.path FROM files")))) return false; if (!query1.next()) return false; do { QString filePath = query1.value(0).toString(); if (Project::instance()->isFileMissing(filePath)) { qCWarning(LOKALIZE_LOG) << "Removing file " << filePath << " from translation memory"; RemoveFileJob* job_removefile = new RemoveFileJob(filePath, dbName, job); TM::threadPool()->start(job_removefile, REMOVEFILE); deletedFiles++; } } while (query1.next()); return deletedFiles; } static QString escape(QString str) { return str.replace(QLatin1Char('\''), QStringLiteral("''")); } static bool doInsertEntry(CatalogString source, CatalogString target, const QString& ctxt, //TODO QStringList -- after XLIFF bool approved, qlonglong fileId, QSqlDatabase& db, QRegExp& rxClean1,//cleaning regexps for word index update const QString& accel, qlonglong priorId, qlonglong& mainId ) { QTime a; a.start(); mainId = -1; if (Q_UNLIKELY(source.isEmpty())) { qCWarning(LOKALIZE_LOG) << "doInsertEntry: source empty"; return false; } bool qpsql = (db.driverName() == QLatin1String("QPSQL")); //we store non-entranslaed entries to make search over all source parts possible bool untranslated = target.isEmpty(); bool shouldBeInIndex = !untranslated && approved; //remove first occurrence of accel character so that search returns words containing accel mark int sourceAccelPos = source.string.indexOf(accel); if (sourceAccelPos != -1) source.string.remove(sourceAccelPos, accel.size()); int targetAccelPos = target.string.indexOf(accel); if (targetAccelPos != -1) target.string.remove(targetAccelPos, accel.size()); //check if we already have record with the same en string QSqlQuery query1(db); QString escapedCtxt = escape(ctxt); QByteArray sourceTags = source.tagsAsByteArray(); QByteArray targetTags = target.tagsAsByteArray(); //BEGIN get sourceId query1.prepare(QString(U("SELECT id FROM source_strings WHERE " "source=? AND (source_accel%1) AND source_markup%2")).arg (sourceAccelPos != -1 ? QStringLiteral("=?") : QStringLiteral("=-1 OR source_accel ISNULL"), sourceTags.isEmpty() ? QStringLiteral(" ISNULL") : QStringLiteral("=?"))); int paranum = 0; query1.bindValue(paranum++, source.string); if (sourceAccelPos != -1) query1.bindValue(paranum++, sourceAccelPos); if (!sourceTags.isEmpty()) query1.bindValue(paranum++, sourceTags); if (Q_UNLIKELY(!query1.exec())) { qCWarning(LOKALIZE_LOG) << "doInsertEntry: select db source_strings error: " << query1.lastError().text(); return false; } qlonglong sourceId; if (!query1.next()) { //BEGIN insert source anew //qCDebug(LOKALIZE_LOG) <<"insert source anew";; ++newTMSourceEntryCount; QString sql = QStringLiteral("INSERT INTO source_strings (source, source_markup, source_accel) VALUES (?, ?, ?)"); if (qpsql) sql += QLatin1String(" RETURNING id"); query1.clear(); query1.prepare(sql); query1.bindValue(0, source.string); query1.bindValue(1, sourceTags); query1.bindValue(2, sourceAccelPos != -1 ? QVariant(sourceAccelPos) : QVariant()); if (Q_UNLIKELY(!query1.exec())) { qCWarning(LOKALIZE_LOG) << "doInsertEntry: select db source_strings error: " << query1.lastError().text(); return false; } sourceId = qpsql ? (query1.next(), query1.value(0).toLongLong()) : query1.lastInsertId().toLongLong(); query1.clear(); //update index if (shouldBeInIndex) addToIndex(sourceId, source.string, rxClean1, accel, db); //END insert source anew } else { sourceId = query1.value(0).toLongLong(); ++reusedTMSourceEntryCount; //qCDebug(LOKALIZE_LOG)<<"SOURCE ALREADY PRESENT"< there will be new record insertion and main table update below } //qCDebug(LOKALIZE_LOG)< tmConfigCache; static void setConfig(QSqlDatabase& db, const TMConfig& c) { QSqlQuery query(db); query.prepare(QStringLiteral("INSERT INTO tm_config (key, value) VALUES (?, ?)")); query.addBindValue(0); query.addBindValue(c.markup); //qCDebug(LOKALIZE_LOG)<<"setting tm db config:"<setPriority(QThread::IdlePriority); if (m_type == TM::Local) { QSqlDatabase db = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), m_dbName); QString dbFolder = QStandardPaths::writableLocation(QStandardPaths::DataLocation); QFileInfo fileInfo(dbFolder); if (!fileInfo.exists(dbFolder)) fileInfo.absoluteDir().mkpath(fileInfo.fileName()); db.setDatabaseName(dbFolder % QLatin1Char('/') % m_dbName % TM_DATABASE_EXTENSION); m_connectionSuccessful = db.open(); if (Q_UNLIKELY(!m_connectionSuccessful)) { qCDebug(LOKALIZE_LOG) << "failed to open db" << db.databaseName() << db.lastError().text(); QSqlDatabase::removeDatabase(m_dbName); emit done(this); return; } if (!initSqliteDb(db)) { //need to recreate db ;( QString filename = db.databaseName(); db.close(); QSqlDatabase::removeDatabase(m_dbName); qCWarning(LOKALIZE_LOG) << "We need to recreate the database " << filename; QFile::remove(filename); db = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), m_dbName); db.setDatabaseName(filename); m_connectionSuccessful = db.open() && initSqliteDb(db); if (!m_connectionSuccessful) { QSqlDatabase::removeDatabase(m_dbName); emit done(this); return; } } } else { if (QSqlDatabase::contains(m_dbName)) { //reconnect is true QSqlDatabase::database(m_dbName).close(); QSqlDatabase::removeDatabase(m_dbName); } if (!m_connParams.isFilled()) { QFile rdb(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QLatin1Char('/') + m_dbName % REMOTETM_DATABASE_EXTENSION); if (!rdb.open(QIODevice::ReadOnly | QIODevice::Text)) { emit done(this); return; } QTextStream rdbParams(&rdb); m_connParams.driver = rdbParams.readLine(); m_connParams.host = rdbParams.readLine(); m_connParams.db = rdbParams.readLine(); m_connParams.user = rdbParams.readLine(); m_connParams.passwd = rdbParams.readLine(); } QSqlDatabase db = QSqlDatabase::addDatabase(m_connParams.driver, m_dbName); db.setHostName(m_connParams.host); db.setDatabaseName(m_connParams.db); db.setUserName(m_connParams.user); db.setPassword(m_connParams.passwd); m_connectionSuccessful = db.open(); if (Q_UNLIKELY(!m_connectionSuccessful)) { QSqlDatabase::removeDatabase(m_dbName); emit done(this); return; } m_connParams.user = db.userName(); initPgDb(db); } } QSqlDatabase db = QSqlDatabase::database(m_dbName); //if (!m_markup.isEmpty()||!m_accel.isEmpty()) if (m_setParams) setConfig(db, m_tmConfig); else m_tmConfig = getConfig(db); qCDebug(LOKALIZE_LOG) << "db" << m_dbName << "opened" << a.elapsed() << m_tmConfig.targetLangCode; getStats(db, m_stat.pairsCount, m_stat.uniqueSourcesCount, m_stat.uniqueTranslationsCount); if (m_type == TM::Local) { db.close(); db.open(); } emit done(this); } CloseDBJob::CloseDBJob(const QString& name) : QObject(), QRunnable() , m_dbName(name) { setAutoDelete(false); } CloseDBJob::~CloseDBJob() { qCDebug(LOKALIZE_LOG) << "closedb dtor" << m_dbName; } void CloseDBJob::run() { if (m_dbName.length()) QSqlDatabase::removeDatabase(m_dbName); emit done(this); } static QString makeAcceledString(QString source, const QString& accel, const QVariant& accelPos) { if (accelPos.isNull()) return source; int accelPosInt = accelPos.toInt(); if (accelPosInt != -1) source.insert(accelPosInt, accel); return source; } SelectJob* TM::initSelectJob(Catalog* catalog, DocPosition pos, QString db, int opt) { SelectJob* job = new SelectJob(catalog->sourceWithTags(pos), catalog->context(pos.entry).first(), catalog->url(), pos, db.isEmpty() ? Project::instance()->projectID() : db); if (opt & Enqueue) { //deletion should be done by receiver, e.g. slotSuggestionsCame() threadPool()->start(job, SELECT); } return job; } SelectJob::SelectJob(const CatalogString& source, const QString& ctxt, const QString& file, const DocPosition& pos, const QString& dbName) : QObject(), QRunnable() , m_source(source) , m_ctxt(ctxt) , m_file(file) , m_dequeued(false) , m_pos(pos) , m_dbName(dbName) { setAutoDelete(false); //qCDebug(LOKALIZE_LOG)<<"selectjob"< invertMap(const QMap& source) { //uses the fact that map has its keys always sorted QMap sortingMap; for (QMap::const_iterator i = source.constBegin(); i != source.constEnd(); ++i) { sortingMap.insertMulti(i.value(), i.key()); } return sortingMap; } //returns true if seen translation with >85% bool SelectJob::doSelect(QSqlDatabase& db, QStringList& words, //QList& entries, bool isShort) { bool qpsql = (db.driverName() == QLatin1String("QPSQL")); QMap occurencies; QVector idsForWord; QSqlQuery queryWords(db); //TODO ??? not sure. make another loop before to create QList< QList > then reorder it by size static const QString queryC[] = {U("SELECT ids_long FROM words WHERE word='%1'"), U("SELECT ids_short FROM words WHERE word='%1'") }; QString queryString = queryC[isShort]; //for each word... int o = words.size(); while (--o >= 0) { //if this is not the first word occurrence, just readd ids for it if (!(!idsForWord.isEmpty() && words.at(o) == words.at(o + 1))) { idsForWord.clear(); queryWords.exec(queryString.arg(words.at(o))); if (Q_UNLIKELY(!queryWords.exec(queryString.arg(words.at(o))))) qCWarning(LOKALIZE_LOG) << "select error: " << queryWords.lastError().text() << endl; if (queryWords.next()) { QByteArray arr(queryWords.value(0).toByteArray()); queryWords.clear(); QList ids(arr.split(' ')); int p = ids.size(); idsForWord.reserve(p); while (--p >= 0) idsForWord.append(ids.at(p).toLongLong(/*bool ok*/0, 36)); } else { queryWords.clear(); continue; } } //qCWarning(LOKALIZE_LOG) <<"SelectJob: idsForWord.size() "<::const_iterator i = idsForWord.constBegin(); i != idsForWord.constEnd(); i++) occurencies[*i]++; //0 is default value } //accels are removed TMConfig c = getConfig(db); QString tmp = c.markup; if (!c.markup.isEmpty()) tmp += '|'; QRegExp rxSplit(QLatin1Char('(') % tmp % QStringLiteral("\\W+|\\d+)+")); QString sourceClean(m_source.string); sourceClean.remove(c.accel); //split m_english for use in wordDiff later--all words are needed so we cant use list we already have QStringList englishList(sourceClean.toLower().split(rxSplit, QString::SkipEmptyParts)); static QRegExp delPart(QStringLiteral("*"), Qt::CaseSensitive, QRegExp::Wildcard); static QRegExp addPart(QStringLiteral("*"), Qt::CaseSensitive, QRegExp::Wildcard); delPart.setMinimal(true); addPart.setMinimal(true); //QList concordanceLevels=sortedUniqueValues(occurencies); //we start from entries with higher word-concordance level QMap concordanceLevelToIds = invertMap(occurencies); if (concordanceLevelToIds.isEmpty()) return false; bool seen85 = false; int limit = 200; auto clit = concordanceLevelToIds.constEnd(); if (concordanceLevelToIds.size()) --clit; if (concordanceLevelToIds.size()) while (--limit >= 0) { if (Q_UNLIKELY(m_dequeued)) break; //for every concordance level qlonglong level = clit.key(); QString joined; while (level == clit.key()) { joined += QString::number(clit.value()) + ','; if (clit == concordanceLevelToIds.constBegin() || --limit < 0) break; --clit; } joined.chop(1); //get records containing current word QSqlQuery queryFetch(U( "SELECT id, source, source_accel, source_markup FROM source_strings WHERE " "source_strings.id IN (") % joined % ')', db); TMEntry e; while (queryFetch.next()) { e.id = queryFetch.value(0).toLongLong(); if (queryFetch.value(3).toByteArray().size()) qCDebug(LOKALIZE_LOG) << "BA" << queryFetch.value(3).toByteArray(); e.source = CatalogString(makeAcceledString(queryFetch.value(1).toString(), c.accel, queryFetch.value(2)), queryFetch.value(3).toByteArray()); if (e.source.string.contains(TAGRANGE_IMAGE_SYMBOL)) { if (!e.source.tags.size()) qCWarning(LOKALIZE_LOG) << "problem:" << queryFetch.value(3).toByteArray().size() << queryFetch.value(3).toByteArray(); } //e.target=queryFetch.value(2).toString(); //QStringList e_ctxt=queryFetch.value(3).toString().split('\b',QString::SkipEmptyParts); //e.date=queryFetch.value(4).toString(); e.markupExpr = c.markup; e.accelExpr = c.accel; e.dbName = db.connectionName(); //BEGIN calc score QString str = e.source.string; str.remove(c.accel); QStringList englishSuggList(str.toLower().split(rxSplit, QString::SkipEmptyParts)); if (englishSuggList.size() > 10 * englishList.size()) continue; //sugg is 'old' --translator has to adapt its translation to 'new'--current QString result = wordDiff(englishSuggList, englishList); //qCWarning(LOKALIZE_LOG) <<"SelectJob: doin "< 1 so we have decreased it, and increased result: / exp(0.014 * float(addLen) * log10(3.0f + addSubStrCount)); if (delLen) { //qCWarning(LOKALIZE_LOG) <<"SelectJob: delLen:"< 8500; if (seen85 && e.score < 6000) continue; if (e.score < Settings::suggScore() * 100) continue; //BEGIN fetch rest of the data QString change_author_str; QString authors_table_str; if (qpsql) { //change_author_str=", main.change_author "; change_author_str = QStringLiteral(", pg_user.usename "); authors_table_str = QStringLiteral(" JOIN pg_user ON (pg_user.usesysid=main.change_author) "); } QSqlQuery queryRest(U( "SELECT main.id, main.date, main.ctxt, main.bits, " "target_strings.target, target_strings.target_accel, target_strings.target_markup, " "files.path, main.change_date ") % change_author_str % U( "FROM main JOIN target_strings ON (target_strings.id=main.target) JOIN files ON (files.id=main.file) ") % authors_table_str % U("WHERE " "main.source=") % QString::number(e.id) % U(" AND " "(main.bits&4)!=4 AND " "target_strings.target NOTNULL") , db); //ORDER BY tm_main.id ? queryRest.exec(); //qCDebug(LOKALIZE_LOG)<<"main select error"< sortedEntryList; //to eliminate same targets from different files while (queryRest.next()) { e.id = queryRest.value(0).toLongLong(); e.date = queryRest.value(1).toDate(); e.ctxt = queryRest.value(2).toString(); e.target = CatalogString(makeAcceledString(queryRest.value(4).toString(), c.accel, queryRest.value(5)), queryRest.value(6).toByteArray()); QStringList matchData = queryRest.value(2).toString().split(TM_DELIMITER, QString::KeepEmptyParts); //context|plural e.file = queryRest.value(7).toString(); if (e.target.isEmpty()) continue; e.obsolete = queryRest.value(3).toInt() & 1; e.changeDate = queryRest.value(8).toDate(); if (qpsql) e.changeAuthor = queryRest.value(9).toString(); //BEGIN exact match score++ if (possibleExactMatch) { //"exact" match (case insensitive+w/o non-word characters!) if (m_source.string == e.source.string) e.score = 10000; else e.score = 9900; } if (!m_ctxt.isEmpty() && matchData.size() > 0) { //check not needed? if (matchData.at(0) == m_ctxt) e.score += 33; } //qCWarning(LOKALIZE_LOG)<<"m_pos"< 1) { int form = matchData.at(1).toInt(); //pluralMatches=(form&&form==m_pos.form); if (form && form == (int)m_pos.form) { //qCWarning(LOKALIZE_LOG)<<"this"< hash; int oldCount = m_entries.size(); QMap::const_iterator it = sortedEntryList.constEnd(); if (sortedEntryList.size()) while (true) { --it; const TMEntry& e = it.key(); int& hits = hash[e.target.string]; if (!hits) //0 was default value m_entries.append(e); hits++; if (it == sortedEntryList.constBegin()) break; } for (int i = oldCount; i < m_entries.size(); ++i) m_entries[i].hits = hash.value(m_entries.at(i).target.string); //END fetch rest of the data } queryFetch.clear(); if (clit == concordanceLevelToIds.constBegin()) break; if (seen85) limit = qMin(limit, 100); //be more restrictive for the next concordance levels } return seen85; } void SelectJob::run() { //qCDebug(LOKALIZE_LOG)<<"select started"<setPriority(QThread::IdlePriority); QTime a; a.start(); if (Q_UNLIKELY(!QSqlDatabase::contains(m_dbName))) { emit done(this); return; } QSqlDatabase db = QSqlDatabase::database(m_dbName); if (Q_UNLIKELY(!db.isValid() || !db.isOpen())) { emit done(this); return; } //qCDebug(LOKALIZE_LOG)<<"select started 2"<()); + std::sort(m_entries.begin(), m_entries.end(), qGreater()); const int limit = qMin(Settings::suggCount(), m_entries.size()); const int minScore = Settings::suggScore() * 100; int i = m_entries.size() - 1; while (i >= 0 && (i >= limit || m_entries.last().score < minScore)) { m_entries.removeLast(); i--; } if (Q_UNLIKELY(m_dequeued)) { emit done(this); return; } ++i; while (--i >= 0) { m_entries[i].accelExpr = c.accel; m_entries[i].markupExpr = c.markup; m_entries[i].diff = userVisibleWordDiff(m_entries.at(i).source.string, m_source.string, m_entries.at(i).accelExpr, m_entries.at(i).markupExpr); } emit done(this); } ScanJob::ScanJob(const QString& filePath, const QString& dbName) : QRunnable() , m_filePath(filePath) , m_time(0) , m_added(0) , m_newVersions(0) , m_size(0) , m_dbName(dbName) { qCDebug(LOKALIZE_LOG) << m_dbName << m_filePath; } void ScanJob::run() { if (stop || !QSqlDatabase::contains(m_dbName)) { return; } qCDebug(LOKALIZE_LOG) << "scan job started for" << m_filePath << m_dbName << stop << m_dbName; //QThread::currentThread()->setPriority(QThread::IdlePriority); QTime a; a.start(); QSqlDatabase db = QSqlDatabase::database(m_dbName); if (!db.isOpen()) return; //initSqliteDb(db); TMConfig c = getConfig(db, true); QRegExp rxClean1(c.markup); rxClean1.setMinimal(true); Catalog catalog(0); if (Q_LIKELY(catalog.loadFromUrl(m_filePath, QString(), &m_size, /*no auto save*/true) == 0)) { if (c.targetLangCode != catalog.targetLangCode()) { qCWarning(LOKALIZE_LOG) << "not indexing file because target languages don't match:" << c.targetLangCode << "in TM vs" << catalog.targetLangCode() << "in file"; return; } qlonglong priorId = -1; QSqlQuery queryBegin(QStringLiteral("BEGIN"), db); //qCWarning(LOKALIZE_LOG) <<"queryBegin error: " < #include /** @author Nick Shaforostoff */ class TmxParser : public QXmlDefaultHandler { enum State { //localstate for getting chars into right place null = 0, seg, propContext, propFile, propPluralForm, propApproved }; enum Lang { Source, Target, Null }; public: TmxParser(const QString& dbName); ~TmxParser(); private: bool startDocument() override; bool startElement(const QString&, const QString&, const QString&, const QXmlAttributes&) override; bool endElement(const QString&, const QString&, const QString&) override; bool characters(const QString&) override; private: QSqlDatabase db; QRegExp rxClean1; QString accel; int m_hits; CatalogString m_segment[3]; //Lang enum QList m_inlineTags; QString m_context; QString m_pluralForm; QString m_filePath; QString m_approvedString; State m_state: 8; Lang m_lang: 8; ushort m_added; QMap m_fileIds; QString m_dbLangCode; }; TmxParser::TmxParser(const QString& dbName) : m_hits(0) , m_state(null) , m_lang(Null) , m_added(0) , m_dbLangCode(Project::instance()->langCode().toLower()) { db = QSqlDatabase::database(dbName); TMConfig c = getConfig(db); rxClean1.setPattern(c.markup); rxClean1.setMinimal(true); accel = c.accel; } bool TmxParser::startDocument() { //initSqliteDb(db); m_fileIds.clear(); QSqlQuery queryBegin(QLatin1String("BEGIN"), db); m_state = null; m_lang = Null; return true; } TmxParser::~TmxParser() { QSqlQuery queryEnd(QLatin1String("END"), db); } bool TmxParser::startElement(const QString&, const QString&, const QString& qName, const QXmlAttributes& attr) { if (qName == QLatin1String("tu")) { bool ok; m_hits = attr.value(QLatin1String("usagecount")).toInt(&ok); if (!ok) m_hits = -1; m_segment[Source].clear(); m_segment[Target].clear(); m_context.clear(); m_pluralForm.clear(); m_filePath.clear(); m_approvedString.clear(); } else if (qName == QLatin1String("tuv")) { QString attrLang = attr.value(QStringLiteral("xml:lang")).toLower(); if (attrLang == QLatin1String("en")) //TODO startsWith? m_lang = Source; else if (attrLang == m_dbLangCode) m_lang = Target; else { qCWarning(LOKALIZE_LOG) << "skipping lang" << attr.value("xml:lang"); m_lang = Null; } } else if (qName == QLatin1String("prop")) { QString attrType = attr.value(QStringLiteral("type")).toLower(); if (attrType == QLatin1String("x-context")) m_state = propContext; else if (attrType == QLatin1String("x-file")) m_state = propFile; else if (attrType == QLatin1String("x-pluralform")) m_state = propPluralForm; else if (attrType == QLatin1String("x-approved")) m_state = propApproved; else m_state = null; } else if (qName == QLatin1String("seg")) { m_state = seg; } else if (m_state == seg && m_lang != Null) { InlineTag::InlineElement t = InlineTag::getElementType(qName.toLatin1()); if (t != InlineTag::_unknown) { m_segment[m_lang].string += QChar(TAGRANGE_IMAGE_SYMBOL); int pos = m_segment[m_lang].string.size(); m_inlineTags.append(InlineTag(pos, pos, t, attr.value(QStringLiteral("id")))); } } return true; } bool TmxParser::endElement(const QString&, const QString&, const QString& qName) { if (qName == QLatin1String("tu")) { if (m_filePath.isEmpty()) m_filePath = QLatin1String("tmx-import"); if (!m_fileIds.contains(m_filePath)) m_fileIds.insert(m_filePath, getFileId(m_filePath, db)); qlonglong fileId = m_fileIds.value(m_filePath); if (!m_pluralForm.isEmpty()) m_context += TM_DELIMITER + m_pluralForm; qlonglong priorId = -1; bool ok = doInsertEntry(m_segment[Source], m_segment[Target], m_context, m_approvedString != QLatin1String("no"), fileId, db, rxClean1, accel, priorId, priorId); if (Q_LIKELY(ok)) ++m_added; } else if (m_state == seg && m_lang != Null) { InlineTag::InlineElement t = InlineTag::getElementType(qName.toLatin1()); if (t != InlineTag::_unknown) { InlineTag tag = m_inlineTags.takeLast(); qCWarning(LOKALIZE_LOG) << qName << tag.getElementName(); if (tag.isPaired()) { tag.end = m_segment[m_lang].string.size(); m_segment[m_lang].string += QChar(TAGRANGE_IMAGE_SYMBOL); } m_segment[m_lang].tags.append(tag); } } m_state = null; return true; } bool TmxParser::characters(const QString& ch) { if (m_state == seg && m_lang != Null) m_segment[m_lang].string += ch; else if (m_state == propFile) m_filePath += ch; else if (m_state == propContext) m_context += ch; else if (m_state == propPluralForm) m_pluralForm += ch; else if (m_state == propApproved) m_approvedString += ch; return true; } ImportTmxJob::ImportTmxJob(const QString& filename, const QString& dbName) : QRunnable() , m_filename(filename) , m_time(0) , m_dbName(dbName) { } ImportTmxJob::~ImportTmxJob() { qCDebug(LOKALIZE_LOG) << "ImportTmxJob dtor"; } void ImportTmxJob::run() { QTime a; a.start(); QFile file(m_filename); if (!file.open(QFile::ReadOnly | QFile::Text)) return; TmxParser parser(m_dbName); QXmlSimpleReader reader; reader.setContentHandler(&parser); QXmlInputSource xmlInputSource(&file); if (!reader.parse(xmlInputSource)) qCWarning(LOKALIZE_LOG) << "failed to load" << m_filename; //qCWarning(LOKALIZE_LOG) <<"Done scanning "< ExportTmxJob::ExportTmxJob(const QString& filename, const QString& dbName) : QRunnable() , m_filename(filename) , m_time(0) , m_dbName(dbName) { } ExportTmxJob::~ExportTmxJob() { qCDebug(LOKALIZE_LOG) << "ExportTmxJob dtor"; } void ExportTmxJob::run() { QTime a; a.start(); QFile out(m_filename); if (!out.open(QFile::WriteOnly | QFile::Text)) return; QXmlStreamWriter xmlOut(&out); xmlOut.setAutoFormatting(true); xmlOut.writeStartDocument(QStringLiteral("1.0")); xmlOut.writeStartElement(QStringLiteral("tmx")); xmlOut.writeAttribute(QStringLiteral("version"), QStringLiteral("2.0")); xmlOut.writeStartElement(QStringLiteral("header")); xmlOut.writeAttribute(QStringLiteral("creationtool"), QStringLiteral("lokalize")); xmlOut.writeAttribute(QStringLiteral("creationtoolversion"), QStringLiteral(LOKALIZE_VERSION)); xmlOut.writeAttribute(QStringLiteral("segtype"), QStringLiteral("paragraph")); xmlOut.writeAttribute(QStringLiteral("o-encoding"), QStringLiteral("UTF-8")); xmlOut.writeEndElement(); xmlOut.writeStartElement(QStringLiteral("body")); QString dbLangCode = Project::instance()->langCode(); QSqlDatabase db = QSqlDatabase::database(m_dbName); QSqlQuery query1(db); if (Q_UNLIKELY(!query1.exec(U( "SELECT main.id, main.ctxt, main.date, main.bits, " "source_strings.source, source_strings.source_accel, " "target_strings.target, target_strings.target_accel, " "files.path, main.change_date " "FROM main, source_strings, target_strings, files " "WHERE source_strings.id=main.source AND " "target_strings.id=main.target AND " "files.id=main.file")))) qCWarning(LOKALIZE_LOG) << "select error: " << query1.lastError().text(); TMConfig c = getConfig(db); const QString DATE_FORMAT = QStringLiteral("yyyyMMdd"); const QString PROP = QStringLiteral("prop"); const QString TYPE = QStringLiteral("type"); while (query1.next()) { QString source = makeAcceledString(query1.value(4).toString(), c.accel, query1.value(5)); QString target = makeAcceledString(query1.value(6).toString(), c.accel, query1.value(7)); xmlOut.writeStartElement(QStringLiteral("tu")); xmlOut.writeAttribute(QStringLiteral("tuid"), QString::number(query1.value(0).toLongLong())); xmlOut.writeStartElement(QStringLiteral("tuv")); xmlOut.writeAttribute(QStringLiteral("xml:lang"), QStringLiteral("en")); xmlOut.writeStartElement(QStringLiteral("seg")); xmlOut.writeCharacters(source); xmlOut.writeEndElement(); xmlOut.writeEndElement(); xmlOut.writeStartElement(QStringLiteral("tuv")); xmlOut.writeAttribute(QStringLiteral("xml:lang"), dbLangCode); xmlOut.writeAttribute(QStringLiteral("creationdate"), QDate::fromString(query1.value(2).toString(), Qt::ISODate).toString(DATE_FORMAT)); xmlOut.writeAttribute(QStringLiteral("changedate"), QDate::fromString(query1.value(9).toString(), Qt::ISODate).toString(DATE_FORMAT)); QString ctxt = query1.value(1).toString(); if (!ctxt.isEmpty()) { int pos = ctxt.indexOf(TM_DELIMITER); if (pos != -1) { QString plural = ctxt; plural.remove(0, pos + 1); ctxt.remove(pos, plural.size()); xmlOut.writeStartElement(PROP); xmlOut.writeAttribute(TYPE, "x-pluralform"); xmlOut.writeCharacters(plural); xmlOut.writeEndElement(); } if (!ctxt.isEmpty()) { xmlOut.writeStartElement(PROP); xmlOut.writeAttribute(TYPE, "x-context"); xmlOut.writeCharacters(ctxt); xmlOut.writeEndElement(); } } QString filePath = query1.value(8).toString(); if (!filePath.isEmpty()) { xmlOut.writeStartElement(PROP); xmlOut.writeAttribute(TYPE, "x-file"); xmlOut.writeCharacters(filePath); xmlOut.writeEndElement(); } qlonglong bits = query1.value(8).toLongLong(); if (bits & TM_NOTAPPROVED) if (!filePath.isEmpty()) { xmlOut.writeStartElement(PROP); xmlOut.writeAttribute(TYPE, "x-approved"); xmlOut.writeCharacters("no"); xmlOut.writeEndElement(); } xmlOut.writeStartElement(QStringLiteral("seg")); xmlOut.writeCharacters(target); xmlOut.writeEndElement(); xmlOut.writeEndElement(); xmlOut.writeEndElement(); } query1.clear(); xmlOut.writeEndDocument(); out.close(); qCWarning(LOKALIZE_LOG) << "ExportTmxJob done exporting:" << a.elapsed(); m_time = a.elapsed(); } //END TMX ExecQueryJob::ExecQueryJob(const QString& queryString, const QString& dbName, QMutex *dbOperation) : QObject(), QRunnable() , query(0) , m_dbName(dbName) , m_query(queryString) , m_dbOperationMutex(dbOperation) { setAutoDelete(false); //qCDebug(LOKALIZE_LOG)<<"ExecQueryJob"<lock(); delete query; m_dbOperationMutex->unlock(); qCDebug(LOKALIZE_LOG) << "ExecQueryJob dtor"; } void ExecQueryJob::run() { m_dbOperationMutex->lock(); QSqlDatabase db = QSqlDatabase::database(m_dbName); qCDebug(LOKALIZE_LOG) << "ExecQueryJob" << m_dbName << "db.isOpen() =" << db.isOpen(); //temporarily: if (!db.isOpen()) qCWarning(LOKALIZE_LOG) << "ExecQueryJob db.open()=" << db.open(); query = new QSqlQuery(m_query, db); query->exec(); qCDebug(LOKALIZE_LOG) << "ExecQueryJob done" << query->lastError().text(); m_dbOperationMutex->unlock(); emit done(this); } diff --git a/src/tm/tmview.cpp b/src/tm/tmview.cpp index e67a61b..b4c2b06 100644 --- a/src/tm/tmview.cpp +++ b/src/tm/tmview.cpp @@ -1,1029 +1,1022 @@ /* **************************************************************************** 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 "tmview.h" #include "lokalize_debug.h" #include "jobs.h" #include "tmscanapi.h" #include "catalog.h" #include "cmd.h" #include "project.h" #include "prefs_lokalize.h" #include "dbfilesmodel.h" #include "diff.h" #include "xlifftextedit.h" #include #include #include #include #include #include #include #include #include -#include #include #include #include #include #ifdef NDEBUG #undef NDEBUG #endif #define DEBUG using namespace TM; struct DiffInfo { DiffInfo(int reserveSize); QString diffClean; QString old; //Formatting info: QByteArray diffIndex; //Map old string-->d.diffClean QVector old2DiffClean; }; DiffInfo::DiffInfo(int reserveSize) { diffClean.reserve(reserveSize); old.reserve(reserveSize); diffIndex.reserve(reserveSize); old2DiffClean.reserve(reserveSize); } /** * 0 - common + - add - - del M - modified so the string is like 00000MM00+++---000 (M appears afterwards) */ static DiffInfo getDiffInfo(const QString& diff) { DiffInfo d(diff.size()); QChar sep('{'); char state = '0'; //walk through diff string char-by-char //calculate old and others int pos = -1; while (++pos < diff.size()) { if (diff.at(pos) == sep) { if (diff.indexOf(QLatin1String("{KBABELDEL}"), pos) == pos) { state = '-'; pos += 10; } else if (diff.indexOf(QLatin1String("{KBABELADD}"), pos) == pos) { state = '+'; pos += 10; } else if (diff.indexOf(QLatin1String("{/KBABEL"), pos) == pos) { state = '0'; pos += 11; } } else { if (state != '+') { d.old.append(diff.at(pos)); d.old2DiffClean.append(d.diffIndex.count()); } d.diffIndex.append(state); d.diffClean.append(diff.at(pos)); } } return d; } void TextBrowser::mouseDoubleClickEvent(QMouseEvent* event) { QTextBrowser::mouseDoubleClickEvent(event); QString sel = textCursor().selectedText(); if (!(sel.isEmpty() || sel.contains(' '))) emit textInsertRequested(sel); } TMView::TMView(QWidget* parent, Catalog* catalog, const QVector& actions_insert, const QVector& actions_remove) : QDockWidget(i18nc("@title:window", "Translation Memory"), parent) , m_browser(new TextBrowser(this)) , m_catalog(catalog) , m_currentSelectJob(0) , m_actions_insert(actions_insert) , m_actions_remove(actions_remove) , m_normTitle(i18nc("@title:window", "Translation Memory")) , m_hasInfoTitle(m_normTitle + QStringLiteral(" [*]")) , m_hasInfo(false) , m_isBatching(false) , m_markAsFuzzy(false) { setObjectName(QStringLiteral("TMView")); setWidget(m_browser); m_browser->document()->setDefaultStyleSheet(QStringLiteral("p.close_match { font-weight:bold; }")); m_browser->viewport()->setBackgroundRole(QPalette::Background); QTimer::singleShot(0, this, &TMView::initLater); connect(m_catalog, QOverload::of(&Catalog::signalFileLoaded), this, &TMView::slotFileLoaded); } TMView::~TMView() { #if QT_VERSION >= 0x050500 int i = m_jobs.size(); while (--i >= 0) TM::threadPool()->cancel(m_jobs.at(i)); #endif } void TMView::initLater() { setAcceptDrops(true); - QSignalMapper* signalMapper_insert = new QSignalMapper(this); - QSignalMapper* signalMapper_remove = new QSignalMapper(this); int i = m_actions_insert.size(); while (--i >= 0) { - connect(m_actions_insert.at(i), &QAction::triggered, signalMapper_insert, QOverload<>::of(&QSignalMapper::map)); - signalMapper_insert->setMapping(m_actions_insert.at(i), i); + connect(m_actions_insert.at(i), &QAction::triggered, this, [this, i] { slotUseSuggestion(i); }); } i = m_actions_remove.size(); while (--i >= 0) { - connect(m_actions_remove.at(i), &QAction::triggered, signalMapper_remove, QOverload<>::of(&QSignalMapper::map)); - signalMapper_remove->setMapping(m_actions_remove.at(i), i); + connect(m_actions_remove.at(i), &QAction::triggered, this, [this, i] { slotRemoveSuggestion(i); }); } - connect(signalMapper_insert, QOverload::of(&QSignalMapper::mapped), this, &TMView::slotUseSuggestion); - connect(signalMapper_remove, QOverload::of(&QSignalMapper::mapped), this, &TMView::slotRemoveSuggestion); setToolTip(i18nc("@info:tooltip", "Double-click any word to insert it into translation")); DBFilesModel::instance(); connect(m_browser, &TM::TextBrowser::textInsertRequested, this, &TMView::textInsertRequested); connect(m_browser, &TM::TextBrowser::customContextMenuRequested, this, &TMView::contextMenu); //TODO ? kdisplayPaletteChanged // connect(KGlobalSettings::self(),,SIGNAL(kdisplayPaletteChanged()),this,SLOT(slotPaletteChanged())); } void TMView::dragEnterEvent(QDragEnterEvent* event) { if (dragIsAcceptable(event->mimeData()->urls())) event->acceptProposedAction(); } void TMView::dropEvent(QDropEvent *event) { QStringList files; foreach (const QUrl& url, event->mimeData()->urls()) files.append(url.toLocalFile()); if (scanRecursive(files, Project::instance()->projectID())) event->acceptProposedAction(); } void TMView::slotFileLoaded(const QString& filePath) { const QString& pID = Project::instance()->projectID(); if (Settings::scanToTMOnOpen()) TM::threadPool()->start(new ScanJob(filePath, pID), SCAN); if (!Settings::prefetchTM() && !m_isBatching) return; m_cache.clear(); #if QT_VERSION >= 0x050500 int i = m_jobs.size(); while (--i >= 0) TM::threadPool()->cancel(m_jobs.at(i)); #endif m_jobs.clear(); DocPosition pos; while (switchNext(m_catalog, pos)) { if (!m_catalog->isEmpty(pos.entry) && m_catalog->isApproved(pos.entry)) continue; SelectJob* j = initSelectJob(m_catalog, pos, pID); connect(j, &SelectJob::done, this, &TMView::slotCacheSuggestions); m_jobs.append(j); } //dummy job for the finish indication BatchSelectFinishedJob* m_seq = new BatchSelectFinishedJob(this); connect(m_seq, &BatchSelectFinishedJob::done, this, &TMView::slotBatchSelectDone); TM::threadPool()->start(m_seq, BATCHSELECTFINISHED); m_jobs.append(m_seq); } void TMView::slotCacheSuggestions(SelectJob* job) { m_jobs.removeAll(job); qCDebug(LOKALIZE_LOG) << job->m_pos.entry; if (job->m_pos.entry == m_pos.entry) slotSuggestionsCame(job); m_cache[DocPos(job->m_pos)] = job->m_entries.toVector(); } void TMView::slotBatchSelectDone() { m_jobs.clear(); if (!m_isBatching) return; bool insHappened = false; DocPosition pos; while (switchNext(m_catalog, pos)) { if (!(m_catalog->isEmpty(pos.entry) || !m_catalog->isApproved(pos.entry)) ) continue; const QVector& suggList = m_cache.value(DocPos(pos)); if (suggList.isEmpty()) continue; const TMEntry& entry = suggList.first(); if (entry.score < 9900) //hacky continue; { bool forceFuzzy = (suggList.size() > 1 && suggList.at(1).score >= 10000) || entry.score < 10000; bool ctxtMatches = entry.score == 1001; if (!m_catalog->isApproved(pos.entry)) { ///m_catalog->push(new DelTextCmd(m_catalog,pos,m_catalog->msgstr(pos))); removeTargetSubstring(m_catalog, pos, 0, m_catalog->targetWithTags(pos).string.size()); if (ctxtMatches || !(m_markAsFuzzy || forceFuzzy)) SetStateCmd::push(m_catalog, pos, true); } else if ((m_markAsFuzzy && !ctxtMatches) || forceFuzzy) { SetStateCmd::push(m_catalog, pos, false); } ///m_catalog->push(new InsTextCmd(m_catalog,pos,entry.target)); insertCatalogString(m_catalog, pos, entry.target, 0); if (Q_UNLIKELY(m_pos.entry == pos.entry && pos.form == m_pos.form)) emit refreshRequested(); } if (!insHappened) { insHappened = true; m_catalog->beginMacro(i18nc("@item Undo action", "Batch translation memory filling")); } } QString msg = i18nc("@info", "Batch translation has been completed."); if (insHappened) m_catalog->endMacro(); else { // xgettext: no-c-format msg += ' '; msg += i18nc("@info", "No suggestions with exact matches were found."); } KPassivePopup::message(KPassivePopup::Balloon, i18nc("@title", "Batch translation complete"), msg, this); } void TMView::slotBatchTranslate() { m_isBatching = true; m_markAsFuzzy = false; if (!Settings::prefetchTM()) slotFileLoaded(m_catalog->url()); else if (m_jobs.isEmpty()) return slotBatchSelectDone(); KPassivePopup::message(KPassivePopup::Balloon, i18nc("@title", "Batch translation"), i18nc("@info", "Batch translation has been scheduled."), this); } void TMView::slotBatchTranslateFuzzy() { m_isBatching = true; m_markAsFuzzy = true; if (!Settings::prefetchTM()) slotFileLoaded(m_catalog->url()); else if (m_jobs.isEmpty()) slotBatchSelectDone(); KPassivePopup::message(KPassivePopup::Balloon, i18nc("@title", "Batch translation"), i18nc("@info", "Batch translation has been scheduled."), this); } void TMView::slotNewEntryDisplayed() { return slotNewEntryDisplayed(DocPosition()); } void TMView::slotNewEntryDisplayed(const DocPosition& pos) { if (m_catalog->numberOfEntries() <= pos.entry) return;//because of Qt::QueuedConnection #if QT_VERSION >= 0x050500 int i = m_jobs.size(); while (--i >= 0) TM::threadPool()->cancel(m_currentSelectJob); #endif //update DB //m_catalog->flushUpdateDBBuffer(); //this is called via subscribtion if (pos.entry != -1) m_pos = pos; m_browser->clear(); if (Settings::prefetchTM() && m_cache.contains(DocPos(m_pos))) { QTimer::singleShot(0, this, &TMView::displayFromCache); } m_currentSelectJob = initSelectJob(m_catalog, m_pos); connect(m_currentSelectJob, &TM::SelectJob::done, this, &TMView::slotSuggestionsCame); } void TMView::displayFromCache() { if (m_prevCachePos.entry == m_pos.entry && m_prevCachePos.form == m_pos.form) return; SelectJob* temp = initSelectJob(m_catalog, m_pos, QString(), 0); temp->m_entries = m_cache.value(DocPos(m_pos)).toList(); slotSuggestionsCame(temp); temp->deleteLater(); m_prevCachePos = m_pos; } void TMView::slotSuggestionsCame(SelectJob* j) { QTime time; time.start(); SelectJob& job = *j; job.deleteLater(); if (job.m_pos.entry != m_pos.entry) return; Catalog& catalog = *m_catalog; if (catalog.numberOfEntries() <= m_pos.entry) return;//because of Qt::QueuedConnection //BEGIN query other DBs handling Project* project = Project::instance(); const QString& projectID = project->projectID(); //check if this is an additional query, from secondary DBs if (job.m_dbName != projectID) { job.m_entries += m_entries; - qSort(job.m_entries.begin(), job.m_entries.end(), qGreater()); + std::sort(job.m_entries.begin(), job.m_entries.end(), qGreater()); const int limit = qMin(Settings::suggCount(), job.m_entries.size()); const int minScore = Settings::suggScore() * 100; int i = job.m_entries.size() - 1; while (i >= 0 && (i >= limit || job.m_entries.last().score < minScore)) { job.m_entries.removeLast(); i--; } } else if (job.m_entries.isEmpty() || job.m_entries.first().score < 8500) { //be careful, as we switched to QDirModel! DBFilesModel& dbFilesModel = *(DBFilesModel::instance()); QModelIndex root = dbFilesModel.rootIndex(); int i = dbFilesModel.rowCount(root); //qCWarning(LOKALIZE_LOG)<<"query other DBs,"<= 0) { const QString& dbName = dbFilesModel.data(dbFilesModel.index(i, 0, root), DBFilesModel::NameRole).toString(); if (projectID != dbName && dbFilesModel.m_configurations.value(dbName).targetLangCode == catalog.targetLangCode()) { SelectJob* j = initSelectJob(m_catalog, m_pos, dbName); connect(j, &SelectJob::done, this, &TMView::slotSuggestionsCame); m_jobs.append(j); } } } //END query other DBs handling m_entries = job.m_entries; const int limit = job.m_entries.size(); if (!limit) { if (m_hasInfo) { m_hasInfo = false; setWindowTitle(m_normTitle); } return; } if (!m_hasInfo) { m_hasInfo = true; setWindowTitle(m_hasInfoTitle); } setUpdatesEnabled(false); m_browser->clear(); m_entryPositions.clear(); //m_entries=job.m_entries; //m_browser->insertHtml(""); int i = 0; QTextBlockFormat blockFormatBase; QTextBlockFormat blockFormatAlternate; blockFormatAlternate.setBackground(QPalette().alternateBase()); QTextCharFormat noncloseMatchCharFormat; QTextCharFormat closeMatchCharFormat; closeMatchCharFormat.setFontWeight(QFont::Bold); forever { QTextCursor cur = m_browser->textCursor(); QString html; html.reserve(1024); const TMEntry& entry = job.m_entries.at(i); html += (entry.score > 9500) ? QStringLiteral("

") : QStringLiteral("

"); //qCDebug(LOKALIZE_LOG)< 10000 ? 100 : float(entry.score) / 100)); html += QStringLiteral(" "); html += QString(i18ncp("%1 is the number of times this TM entry has been found", "(1 time)", "(%1 times)", entry.hits)); html += QStringLiteral("/ "); //int sourceStartPos=cur.position(); QString result = entry.diff.toHtmlEscaped(); //result.replace("&","&"); //result.replace("<","<"); //result.replace(">",">"); result.replace(QLatin1String("{KBABELADD}"), QStringLiteral("")); result.replace(QLatin1String("{/KBABELADD}"), QLatin1String("")); result.replace(QLatin1String("{KBABELDEL}"), QStringLiteral("")); result.replace(QLatin1String("{/KBABELDEL}"), QLatin1String("")); result.replace(QLatin1String("\\n"), QLatin1String("\\n
")); result.replace(QLatin1String("\\n"), QLatin1String("\\n
")); html += result; #if 0 cur.insertHtml(result); cur.movePosition(QTextCursor::PreviousCharacter, QTextCursor::MoveAnchor, cur.position() - sourceStartPos); CatalogString catStr(entry.diff); catStr.string.remove("{KBABELDEL}"); catStr.string.remove("{/KBABELDEL}"); catStr.string.remove("{KBABELADD}"); catStr.string.remove("{/KBABELADD}"); catStr.tags = entry.source.tags; DiffInfo d = getDiffInfo(entry.diff); int j = catStr.tags.size(); while (--j >= 0) { catStr.tags[j].start = d.old2DiffClean.at(catStr.tags.at(j).start); catStr.tags[j].end = d.old2DiffClean.at(catStr.tags.at(j).end); } insertContent(cur, catStr, job.m_source, false); #endif //str.replace('&',"&"); TODO check html += QLatin1String("
"); if (Q_LIKELY(i < m_actions_insert.size())) { m_actions_insert.at(i)->setStatusTip(entry.target.string); html += QStringLiteral("[%1] ").arg(m_actions_insert.at(i)->shortcut().toString(QKeySequence::NativeText)); } else html += QLatin1String("[ - ] "); /* QString str(entry.target.string); str.replace('<',"<"); str.replace('>',">"); html+=str; */ cur.insertHtml(html); html.clear(); cur.setCharFormat((entry.score > 9500) ? closeMatchCharFormat : noncloseMatchCharFormat); insertContent(cur, entry.target); m_entryPositions.insert(cur.anchor(), i); html += i ? QStringLiteral("

") : QStringLiteral("

"); cur.insertHtml(html); if (Q_UNLIKELY(++i >= limit)) break; cur.insertBlock(i % 2 ? blockFormatAlternate : blockFormatBase); } m_browser->insertHtml(QStringLiteral("")); setUpdatesEnabled(true); // qCWarning(LOKALIZE_LOG)<<"ELA "<document()->blockCount(); } /* void TMView::slotPaletteChanged() { }*/ bool TMView::event(QEvent *event) { if (event->type() == QEvent::ToolTip) { QHelpEvent *helpEvent = static_cast(event); //int block1=m_browser->cursorForPosition(m_browser->viewport()->mapFromGlobal(helpEvent->globalPos())).blockNumber(); QMap::iterator block = m_entryPositions.lowerBound(m_browser->cursorForPosition(m_browser->viewport()->mapFromGlobal(helpEvent->globalPos())).anchor()); if (block != m_entryPositions.end() && *block < m_entries.size()) { const TMEntry& tmEntry = m_entries.at(*block); QString file = tmEntry.file; if (file == m_catalog->url()) file = i18nc("File argument in tooltip, when file is current file", "this"); QString tooltip = i18nc("@info:tooltip", "File: %1
Addition date: %2", file, tmEntry.date.toString(Qt::ISODate)); if (!tmEntry.changeDate.isNull() && tmEntry.changeDate != tmEntry.date) tooltip += i18nc("@info:tooltip on TM entry continues", "
Last change date: %1", tmEntry.changeDate.toString(Qt::ISODate)); if (!tmEntry.changeAuthor.isEmpty()) tooltip += i18nc("@info:tooltip on TM entry continues", "
Last change author: %1", tmEntry.changeAuthor); tooltip += i18nc("@info:tooltip on TM entry continues", "
TM: %1", tmEntry.dbName); if (tmEntry.obsolete) tooltip += i18nc("@info:tooltip on TM entry continues", "
Is not present in the file anymore"); QToolTip::showText(helpEvent->globalPos(), tooltip); return true; } } return QWidget::event(event); } void TMView::removeEntry(const TMEntry& e) { if (KMessageBox::Yes == KMessageBox::questionYesNo(this, i18n("Do you really want to remove this entry:
%1
from translation memory %2?", e.target.string.toHtmlEscaped(), e.dbName), i18nc("@title:window", "Translation Memory Entry Removal"))) { RemoveJob* job = new RemoveJob(e); connect(job, SIGNAL(done()), this, SLOT(slotNewEntryDisplayed())); TM::threadPool()->start(job, REMOVE); } } void TMView::deleteFile(const TMEntry& e, const bool showPopUp) { QString filePath = e.file; if (Project::instance()->isFileMissing(filePath)) { //File doesn't exist RemoveFileJob* job = new RemoveFileJob(e.file, e.dbName); connect(job, SIGNAL(done()), this, SLOT(slotNewEntryDisplayed())); TM::threadPool()->start(job, REMOVEFILE); if (showPopUp) { KMessageBox::information(this, i18nc("@info", "The file %1 does not exist, it has been removed from the translation memory.", e.file)); } return; } } void TMView::contextMenu(const QPoint& pos) { int block = *m_entryPositions.lowerBound(m_browser->cursorForPosition(pos).anchor()); qCWarning(LOKALIZE_LOG) << block; if (block >= m_entries.size()) return; const TMEntry& e = m_entries.at(block); enum {Remove, RemoveFile, Open}; QMenu popup; popup.addAction(i18nc("@action:inmenu", "Remove this entry"))->setData(Remove); if (e.file != m_catalog->url() && QFile::exists(e.file)) popup.addAction(i18nc("@action:inmenu", "Open file containing this entry"))->setData(Open); else { if (Settings::deleteFromTMOnMissing()) { //Automatic deletion deleteFile(e, true); } else if (!QFile::exists(e.file)) { //Still offer manual deletion if this is not the current file popup.addAction(i18nc("@action:inmenu", "Remove this missing file from TM"))->setData(RemoveFile); } } QAction* r = popup.exec(m_browser->mapToGlobal(pos)); if (!r) return; if (r->data().toInt() == Remove) { removeEntry(e); } else if (r->data().toInt() == Open) { emit fileOpenRequested(e.file, e.source.string, e.ctxt, true); } else if ((r->data().toInt() == RemoveFile) && KMessageBox::Yes == KMessageBox::questionYesNo(this, i18n("Do you really want to remove this missing file:
%1
from translation memory %2?", e.file, e.dbName), i18nc("@title:window", "Translation Memory Missing File Removal"))) { deleteFile(e, false); } } /** * helper function: * searches to th nearest rxNum or ABBR * clears rxNum if ABBR is found before rxNum */ static int nextPlacableIn(const QString& old, int start, QString& cap) { static QRegExp rxNum(QStringLiteral("[\\d\\.\\%]+")); static QRegExp rxAbbr(QStringLiteral("\\w+")); int numPos = rxNum.indexIn(old, start); // int abbrPos=rxAbbr.indexIn(old,start); int abbrPos = start; //qCWarning(LOKALIZE_LOG)<<"seeing"<= 0) { if ((c++)->isUpper()) break; } abbrPos += rxAbbr.matchedLength(); } int pos = qMin(numPos, abbrPos); if (pos == -1) pos = qMax(numPos, abbrPos); // if (pos==numPos) // cap=rxNum.cap(0); // else // cap=rxAbbr.cap(0); cap = (pos == numPos ? rxNum : rxAbbr).cap(0); //qCWarning(LOKALIZE_LOG)<]*") % Settings::addColor().name() % QLatin1String("[^>]*\">([^>]*)")); QRegExp rxDel(QLatin1String("]*") % Settings::delColor().name() % QLatin1String("[^>]*\">([^>]*)")); //rxAdd.setMinimal(true); //rxDel.setMinimal(true); //first things first int pos = 0; while ((pos = rxDel.indexIn(diff, pos)) != -1) diff.replace(pos, rxDel.matchedLength(), "\tKBABELDEL\t" % rxDel.cap(1) % "\t/KBABELDEL\t"); pos = 0; while ((pos = rxAdd.indexIn(diff, pos)) != -1) diff.replace(pos, rxAdd.matchedLength(), "\tKBABELADD\t" % rxAdd.cap(1) % "\t/KBABELADD\t"); diff.replace(QStringLiteral("<"), QStringLiteral("<")); diff.replace(QStringLiteral(">"), QStringLiteral(">")); //possible enhancement: search for non-translated words in removedSubstrings... //QStringList removedSubstrings; //QStringList addedSubstrings; /* 0 - common + - add - - del M - modified so the string is like 00000MM00+++---000 */ DiffInfo d = getDiffInfo(diff); bool sameMarkup = Project::instance()->markup() == entry.markupExpr && !entry.markupExpr.isEmpty(); bool tryMarkup = !entry.target.tags.size() && sameMarkup; //search for changed markup if (tryMarkup) { QRegExp rxMarkup(entry.markupExpr); rxMarkup.setMinimal(true); pos = 0; int replacingPos = 0; while ((pos = rxMarkup.indexIn(d.old, pos)) != -1) { //qCWarning(LOKALIZE_LOG)<<"size"<= d.old2DiffClean.at(pos)) d.diffIndex[tmp] = 'M'; //qCWarning(LOKALIZE_LOG)<<"M"< 0) { QByteArray diffMPart(d.diffIndex.left(len)); int m = diffMPart.indexOf('M'); if (m != -1) diffMPart.truncate(m); #if 0 nono //first goes del, then add. so stop on second del sequence bool seenAdd = false; int j = -1; while (++j < diffMPart.size()) { if (diffMPart.at(j) == '+') seenAdd = true; else if (seenAdd && diffMPart.at(j) == '-') { diffMPart.truncate(j); break; } } #endif //form 'oldMarkup' QString oldMarkup; oldMarkup.reserve(diffMPart.size()); int j = -1; while (++j < diffMPart.size()) { if (diffMPart.at(j) != '+') oldMarkup.append(d.diffClean.at(j)); } //qCWarning(LOKALIZE_LOG)<<"old"<= 0) d.diffIndex[j] = 'M'; //qCWarning(LOKALIZE_LOG)<<"M"<= 0) d.diffIndex[len + j] = 'M'; //qCWarning(LOKALIZE_LOG)<<"M"< 500 cases while ((++endPos < d.diffIndex.size()) && (d.diffIndex.at(endPos) == '+') && (-1 != nextPlacableIn(QString(d.diffClean.at(endPos)), 0, _)) ) diffMPart.append('+'); qCDebug(LOKALIZE_LOG) << "diffMPart extended 1" << diffMPart; // if ((pos-1>=0) && (d.old2DiffClean.at(pos)>=0)) // { // qCWarning(LOKALIZE_LOG)<<"d.diffIndex"<= 0) && (d.diffIndex.at(startPos) == '+') //&&(-1!=nextPlacableIn(QString(d.diffClean.at(d.old2DiffClean.at(pos))),0,_)) ) diffMPart.prepend('+'); ++startPos; qCDebug(LOKALIZE_LOG) << "diffMPart extended 2" << diffMPart; if ((diffMPart.contains('-') || diffMPart.contains('+')) && (!diffMPart.contains('M'))) { //form newMarkup QString newMarkup; newMarkup.reserve(diffMPart.size()); int j = -1; while (++j < diffMPart.size()) { if (diffMPart.at(j) != '-') newMarkup.append(d.diffClean.at(startPos + j)); } if (newMarkup.endsWith(' ')) newMarkup.chop(1); //qCWarning(LOKALIZE_LOG)<<"d.old"<= d.old2DiffClean.at(pos)) d.diffIndex[tmp] = 'M'; //qCWarning(LOKALIZE_LOG)<<"M"<= m_entries.size())) return; const TMEntry& e = m_entries.at(i); removeEntry(e); } void TMView::slotUseSuggestion(int i) { if (Q_UNLIKELY(i >= m_entries.size())) return; CatalogString target = targetAdapted(m_entries.at(i), m_catalog->sourceWithTags(m_pos)); #if 0 QString tmp = target.string; tmp.replace(TAGRANGE_IMAGE_SYMBOL, '*'); qCWarning(LOKALIZE_LOG) << "targetAdapted" << tmp; foreach (InlineTag tag, target.tags) qCWarning(LOKALIZE_LOG) << "tag" << tag.start << tag.end; #endif if (Q_UNLIKELY(target.isEmpty())) return; m_catalog->beginMacro(i18nc("@item Undo action", "Use translation memory suggestion")); QString old = m_catalog->targetWithTags(m_pos).string; if (!old.isEmpty()) { m_pos.offset = 0; //FIXME test! removeTargetSubstring(m_catalog, m_pos, 0, old.size()); //m_catalog->push(new DelTextCmd(m_catalog,m_pos,m_catalog->msgstr(m_pos))); } qCWarning(LOKALIZE_LOG) << "1" << target.string; //m_catalog->push(new InsTextCmd(m_catalog,m_pos,target)/*,true*/); insertCatalogString(m_catalog, m_pos, target, 0); if (m_entries.at(i).score > 9900 && !m_catalog->isApproved(m_pos.entry)) SetStateCmd::push(m_catalog, m_pos, true); m_catalog->endMacro(); emit refreshRequested(); } diff --git a/src/webquery/webqueryview.cpp b/src/webquery/webqueryview.cpp index 7198f0a..4964c0d 100644 --- a/src/webquery/webqueryview.cpp +++ b/src/webquery/webqueryview.cpp @@ -1,184 +1,180 @@ /* **************************************************************************** This file is part of KAider Copyright (C) 2007 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) 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; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. In addition, as a special exception, the copyright holders give permission to link the code of this program with any edition of the Qt library by Trolltech AS, Norway (or with modified versions of Qt that use the same license as Qt), and distribute linked combinations including the two. You must obey the GNU General Public License in all respects for all of the code used other than Qt. If you modify this file, you may extend this exception to your version of the file, but you are not obligated to do so. If you do not wish to do so, delete this exception statement from your version. **************************************************************************** */ #include "webqueryview.h" #include "lokalize_debug.h" #include "project.h" #include "catalog.h" #include "flowlayout.h" #include "ui_querycontrol.h" #include #include #include #include #include #include "webquerycontroller.h" #include #include #include #include -#include #include // #include #include "myactioncollectionview.h" using namespace Kross; WebQueryView::WebQueryView(QWidget* parent, Catalog* catalog, const QVector& actions) : QDockWidget(i18n("Web Queries"), parent) , m_catalog(catalog) , m_splitter(new QSplitter(this)) , m_browser(new QTextBrowser(m_splitter)) , ui_queryControl(new Ui_QueryControl) , m_actions(actions) { setObjectName(QStringLiteral("WebQueryView")); setWidget(m_splitter); hide(); m_browser->viewport()->setBackgroundRole(QPalette::Background); m_browser->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); QWidget* w = new QWidget(m_splitter); ui_queryControl->setupUi(w); QTimer::singleShot(0, this, &WebQueryView::initLater); } WebQueryView::~WebQueryView() { delete ui_queryControl; // delete m_flowLayout; } void WebQueryView::initLater() { connect(ui_queryControl->queryBtn, &QPushButton::clicked, ui_queryControl->actionzView, &MyActionCollectionView::triggerSelectedActions); // connect(this, &WebQueryView::addWebQueryResult, m_flowLayout, SLOT(addWebQueryResult(const QString&))); // ActionCollectionModel::Mode mode( // ActionCollectionModel::Icons // | ActionCollectionModel::ToolTips | ActionCollectionModel::UserCheckable );*/ ActionCollectionModel* m = new ActionCollectionModel(ui_queryControl->actionzView, Manager::self().actionCollection()/*, mode*/); ui_queryControl->actionzView->setModel(m); // m_boxLayout->addWidget(w); ui_queryControl->actionzView->data.webQueryView = this; m_browser->setToolTip(i18nc("@info:tooltip", "Double-click any word to insert it into translation")); - QSignalMapper* signalMapper = new QSignalMapper(this); int i = m_actions.size(); while (--i >= 0) { - connect(m_actions.at(i), &QAction::triggered, signalMapper, QOverload<>::of(&QSignalMapper::map)); - signalMapper->setMapping(m_actions.at(i), i); + connect(m_actions.at(i), &QAction::triggered, this, [this, i] { slotUseSuggestion(i); }); } - connect(signalMapper, QOverload::of(&QSignalMapper::mapped), this, &WebQueryView::slotUseSuggestion); connect(m_browser, &QTextBrowser::selectionChanged, this, &WebQueryView::slotSelectionChanged); } void WebQueryView::slotSelectionChanged() { //NOTE works fine only for dbl-click word selection //(actually, quick word insertion is exactly the purpose of this slot:) QString sel(m_browser->textCursor().selectedText()); if (!sel.isEmpty()) { emit textInsertRequested(sel); } } //TODO text may be dragged // void WebQueryView::dragEnterEvent(QDragEnterEvent* event) // { // /* if(event->mimeData()->hasUrls() && event->mimeData()->urls().first().path().endsWith(".po")) // { // //qCWarning(LOKALIZE_LOG) << " " <<; // event->acceptProposedAction(); // };*/ // } // // void WebQueryView::dropEvent(QDropEvent *event) // { // /* emit mergeOpenRequested(event->mimeData()->urls().first()); // event->acceptProposedAction();*/ // } void WebQueryView::slotNewEntryDisplayed(const DocPosition& pos) { //m_flowLayout->clearWebQueryResult(); m_browser->clear(); m_suggestions.clear(); ui_queryControl->actionzView->data.msg = m_catalog->msgid(pos); //TODO pass DocPosition also, as tmview does if (ui_queryControl->autoQuery->isChecked()) ui_queryControl->actionzView->triggerSelectedActions(); } void WebQueryView::slotUseSuggestion(int i) { if (i >= m_suggestions.size()) return; emit textInsertRequested(m_suggestions.at(i)); } void WebQueryView::addWebQueryResult(const QString& name, const QString& str) { QString html(str); html.replace('<', "<"); html.replace('>', ">"); html.append(QString("

")); html.prepend(QString("[%2] /%1/ ").arg(name).arg( (m_suggestions.size() < m_actions.size()) ? m_actions.at(m_suggestions.size())->shortcut().toString() : " - ")); m_browser->insertHtml(html); //m_flowLayout->addWebQueryResult(str); m_suggestions.append(str); }