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: " <