1 /*
2   This file is part of Lokalize
3   This file contains parts of KBabel code
4 
5   SPDX-FileCopyrightText: 1999-2000 Matthias Kiefer <matthias.kiefer@gmx.de>
6   SPDX-FileCopyrightText: 2001-2005 Stanislav Visnovsky <visnovsky@kde.org>
7   SPDX-FileCopyrightText: 2006 Nicolas Goutte <goutte@kde.org>
8   SPDX-FileCopyrightText: 2007-2014 Nick Shaforostoff <shafff@ukr.net>
9   SPDX-FileCopyrightText: 2018-2019 Simon Depiets <sdepiets@gmail.com>
10 
11   SPDX-License-Identifier: GPL-2.0-or-later WITH LicenseRef-Qt-Commercial-exception-1.0
12 */
13 
14 #include "catalog.h"
15 #include "catalog_private.h"
16 #include "project.h"
17 #include "projectmodel.h" //to notify about modification
18 
19 #include "catalogstorage.h"
20 #include "gettextstorage.h"
21 #include "gettextimport.h"
22 #include "gettextexport.h"
23 #include "xliffstorage.h"
24 #include "tsstorage.h"
25 
26 #include "mergecatalog.h"
27 
28 #include "version.h"
29 #include "prefs_lokalize.h"
30 #include "jobs.h"
31 #include "dbfilesmodel.h"
32 
33 #include <QString>
34 #include <QStringBuilder>
35 #include <QMap>
36 #include <QBuffer>
37 #include <QFileInfo>
38 #include <QDir>
39 
40 #include <klocalizedstring.h>
41 
42 #ifdef Q_OS_WIN
43 #define U QLatin1String
44 #else
45 #define U QStringLiteral
46 #endif
47 
48 //QString Catalog::supportedMimeFilters("text/x-gettext-translation application/x-xliff application/x-linguist"); //" text/x-gettext-translation-template")
supportedFileTypes(bool includeTemplates)49 QString Catalog::supportedFileTypes(bool includeTemplates)
50 {
51     QString sep = QStringLiteral(";;");
52     QString all = i18n("All supported files (*.po *.pot *.xlf *.xliff *.ts)") + sep;
53     return all + (includeTemplates ? i18n("Gettext (*.po *.pot)") : i18n("Gettext (*.po)")) + sep + i18n("XLIFF (*.xlf *.xliff)") + sep + i18n("Linguist (*.ts)");
54 }
55 
56 static const QString extensions[] = {U(".po"), U(".pot"), U(".xlf"), U(".xliff"), U(".ts")};
57 
58 static const char* const xliff_states[] = {
59     I18N_NOOP("New"), I18N_NOOP("Needs translation"), I18N_NOOP("Needs full localization"), I18N_NOOP("Needs adaptation"), I18N_NOOP("Translated"),
60     I18N_NOOP("Needs translation review"), I18N_NOOP("Needs full localization review"), I18N_NOOP("Needs adaptation review"), I18N_NOOP("Final"),
61     I18N_NOOP("Signed-off")
62 };
63 
states()64 const char* const* Catalog::states()
65 {
66     return xliff_states;
67 }
68 
supportedExtensions()69 QStringList Catalog::supportedExtensions()
70 {
71     QStringList result;
72     int i = sizeof(extensions) / sizeof(QString);
73     while (--i >= 0)
74         result.append(extensions[i]);
75     return result;
76 }
77 
extIsSupported(const QString & path)78 bool Catalog::extIsSupported(const QString& path)
79 {
80     QStringList ext = supportedExtensions();
81     int i = ext.size();
82     while (--i >= 0 && !path.endsWith(ext.at(i)))
83         ;
84     return i != -1;
85 }
86 
Catalog(QObject * parent)87 Catalog::Catalog(QObject *parent)
88     : QUndoStack(parent)
89     , d(this)
90     , m_storage(nullptr)
91 {
92     //cause refresh events for files modified from lokalize itself aint delivered automatically
93     connect(this, QOverload<const QString &>::of(&Catalog::signalFileSaved), Project::instance()->model(), QOverload<const QString &>::of(&ProjectModel::slotFileSaved), Qt::QueuedConnection);
94 
95     QTimer* t = &(d._autoSaveTimer);
96     t->setInterval(2 * 60 * 1000);
97     t->setSingleShot(false);
98     connect(t, &QTimer::timeout, this, &Catalog::doAutoSave);
99     connect(this, QOverload<>::of(&Catalog::signalFileSaved), t, QOverload<>::of(&QTimer::start));
100     connect(this, QOverload<>::of(&Catalog::signalFileLoaded), t, QOverload<>::of(&QTimer::start));
101     connect(this, &Catalog::indexChanged, this, &Catalog::setAutoSaveDirty);
102     connect(Project::local(), &Project::configChanged, this, &Catalog::projectConfigChanged);
103 }
104 
~Catalog()105 Catalog::~Catalog()
106 {
107     clear();
108     //delete m_storage; //deleted in clear();
109 }
110 
111 
clear()112 void Catalog::clear()
113 {
114     setIndex(cleanIndex());//to keep TM in sync
115     QUndoStack::clear();
116     d._errorIndex.clear();
117     d._nonApprovedIndex.clear();
118     d._nonApprovedNonEmptyIndex.clear();
119     d._emptyIndex.clear();
120     delete m_storage; m_storage = nullptr;
121     d._filePath.clear();
122     d._lastModifiedPos = DocPosition();
123     d._modifiedEntries.clear();
124 
125     while (!d._altTransCatalogs.isEmpty())
126         d._altTransCatalogs.takeFirst()->deleteLater();
127 
128     d._altTranslations.clear();
129     /*
130         d.msgidDiffList.clear();
131         d.msgstr2MsgidDiffList.clear();
132         d.diffCache.clear();
133         */
134 }
135 
136 
137 
push(QUndoCommand * cmd)138 void Catalog::push(QUndoCommand* cmd)
139 {
140     generatePhaseForCatalogIfNeeded(this);
141     QUndoStack::push(cmd);
142 }
143 
144 
145 //BEGIN STORAGE TRANSLATION
146 
capabilities() const147 int Catalog::capabilities() const
148 {
149     if (Q_UNLIKELY(!m_storage)) return 0;
150 
151     return m_storage->capabilities();
152 }
153 
numberOfEntries() const154 int Catalog::numberOfEntries() const
155 {
156     if (Q_UNLIKELY(!m_storage)) return 0;
157 
158     return m_storage->size();
159 }
160 
161 
alterForSinglePlural(const Catalog * th,DocPosition pos)162 static DocPosition alterForSinglePlural(const Catalog* th, DocPosition pos)
163 {
164     //if source lang is english (implied) and target lang has only 1 plural form (e.g. Chinese)
165     if (Q_UNLIKELY(th->numberOfPluralForms() == 1 && th->isPlural(pos)))
166         pos.form = 1;
167     return pos;
168 }
169 
msgid(const DocPosition & pos) const170 QString Catalog::msgid(const DocPosition& pos) const
171 {
172     if (Q_UNLIKELY(!m_storage))
173         return QString();
174 
175     return m_storage->source(alterForSinglePlural(this, pos));
176 }
177 
msgidWithPlurals(const DocPosition & pos,bool truncateFirstLine) const178 QString Catalog::msgidWithPlurals(const DocPosition& pos, bool truncateFirstLine) const
179 {
180     if (Q_UNLIKELY(!m_storage))
181         return QString();
182     return m_storage->sourceWithPlurals(pos, truncateFirstLine);
183 }
184 
msgstr(const DocPosition & pos) const185 QString Catalog::msgstr(const DocPosition& pos) const
186 {
187     if (Q_UNLIKELY(!m_storage))
188         return QString();
189 
190     return m_storage->target(pos);
191 }
192 
msgstrWithPlurals(const DocPosition & pos,bool truncateFirstLine) const193 QString Catalog::msgstrWithPlurals(const DocPosition& pos, bool truncateFirstLine) const
194 {
195     if (Q_UNLIKELY(!m_storage))
196         return QString();
197 
198     return m_storage->targetWithPlurals(pos, truncateFirstLine);
199 }
200 
201 
sourceWithTags(const DocPosition & pos) const202 CatalogString Catalog::sourceWithTags(const DocPosition& pos) const
203 {
204     if (Q_UNLIKELY(!m_storage))
205         return CatalogString();
206 
207     return m_storage->sourceWithTags(alterForSinglePlural(this, pos));
208 
209 }
targetWithTags(const DocPosition & pos) const210 CatalogString Catalog::targetWithTags(const DocPosition& pos) const
211 {
212     if (Q_UNLIKELY(!m_storage))
213         return CatalogString();
214 
215     return m_storage->targetWithTags(pos);
216 }
217 
catalogString(const DocPosition & pos) const218 CatalogString Catalog::catalogString(const DocPosition& pos) const
219 {
220     if (Q_UNLIKELY(!m_storage))
221         return CatalogString();
222 
223     return m_storage->catalogString(pos.part == DocPosition::Source ? alterForSinglePlural(this, pos) : pos);
224 }
225 
226 
notes(const DocPosition & pos) const227 QVector<Note> Catalog::notes(const DocPosition& pos) const
228 {
229     if (Q_UNLIKELY(!m_storage))
230         return QVector<Note>();
231 
232     return m_storage->notes(pos);
233 }
234 
developerNotes(const DocPosition & pos) const235 QVector<Note> Catalog::developerNotes(const DocPosition& pos) const
236 {
237     if (Q_UNLIKELY(!m_storage))
238         return QVector<Note>();
239 
240     return m_storage->developerNotes(pos);
241 }
242 
setNote(const DocPosition & pos,const Note & note)243 Note Catalog::setNote(const DocPosition& pos, const Note& note)
244 {
245     if (Q_UNLIKELY(!m_storage))
246         return Note();
247 
248     return m_storage->setNote(pos, note);
249 }
250 
noteAuthors() const251 QStringList Catalog::noteAuthors() const
252 {
253     if (Q_UNLIKELY(!m_storage))
254         return QStringList();
255 
256     return m_storage->noteAuthors();
257 }
258 
attachAltTransCatalog(Catalog * altCat)259 void Catalog::attachAltTransCatalog(Catalog* altCat)
260 {
261     d._altTransCatalogs.append(altCat);
262     if (numberOfEntries() != altCat->numberOfEntries())
263         qCWarning(LOKALIZE_LOG) << altCat->url() << "has different number of entries";
264 }
265 
attachAltTrans(int entry,const AltTrans & trans)266 void Catalog::attachAltTrans(int entry, const AltTrans& trans)
267 {
268     d._altTranslations.insert(entry, trans);
269 }
270 
altTrans(const DocPosition & pos) const271 QVector<AltTrans> Catalog::altTrans(const DocPosition& pos) const
272 {
273     QVector<AltTrans> result;
274     if (m_storage)
275         result = m_storage->altTrans(pos);
276 
277     for (Catalog* altCat : d._altTransCatalogs) {
278         if (pos.entry >= altCat->numberOfEntries()) {
279             qCDebug(LOKALIZE_LOG) << "ignoring" << altCat->url() << "this time because" << pos.entry << "<" << altCat->numberOfEntries();
280             continue;
281         }
282 
283         if (altCat->source(pos) != source(pos)) {
284             qCDebug(LOKALIZE_LOG) << "ignoring" << altCat->url() << "this time because <source>s don't match";
285             continue;
286         }
287 
288         QString target = altCat->msgstr(pos);
289         if (!target.isEmpty() && altCat->isApproved(pos)) {
290             result << AltTrans();
291             result.last().target = target;
292             result.last().type = AltTrans::Reference;
293             result.last().origin = altCat->url();
294         }
295     }
296     if (d._altTranslations.contains(pos.entry))
297         result << d._altTranslations.value(pos.entry);
298     return result;
299 }
300 
sourceFiles(const DocPosition & pos) const301 QStringList Catalog::sourceFiles(const DocPosition& pos) const
302 {
303     if (Q_UNLIKELY(!m_storage))
304         return QStringList();
305 
306     return m_storage->sourceFiles(pos);
307 }
308 
id(const DocPosition & pos) const309 QString Catalog::id(const DocPosition& pos) const
310 {
311     if (Q_UNLIKELY(!m_storage))
312         return QString();
313 
314     return m_storage->id(pos);
315 }
316 
context(const DocPosition & pos) const317 QStringList Catalog::context(const DocPosition& pos) const
318 {
319     if (Q_UNLIKELY(!m_storage))
320         return QStringList();
321 
322     return m_storage->context(pos);
323 }
324 
setPhase(const DocPosition & pos,const QString & phase)325 QString Catalog::setPhase(const DocPosition& pos, const QString& phase)
326 {
327     if (Q_UNLIKELY(!m_storage))
328         return QString();
329 
330     return m_storage->setPhase(pos, phase);
331 }
332 
333 
setActivePhase(const QString & phase,ProjectLocal::PersonRole role)334 void Catalog::setActivePhase(const QString& phase, ProjectLocal::PersonRole role)
335 {
336     d._phase = phase;
337     d._phaseRole = role;
338     updateApprovedEmptyIndexCache();
339     Q_EMIT activePhaseChanged();
340 }
341 
updateApprovedEmptyIndexCache()342 void Catalog::updateApprovedEmptyIndexCache()
343 {
344     if (Q_UNLIKELY(!m_storage))
345         return;
346 
347     //index cache TODO profile?
348     d._nonApprovedIndex.clear();
349     d._nonApprovedNonEmptyIndex.clear();
350     d._emptyIndex.clear();
351 
352     DocPosition pos(0);
353     const int limit = m_storage->size();
354     while (pos.entry < limit) {
355         if (m_storage->isEmpty(pos))
356             d._emptyIndex << pos.entry;
357         if (!isApproved(pos)) {
358             d._nonApprovedIndex << pos.entry;
359             if (!m_storage->isEmpty(pos)) {
360                 d._nonApprovedNonEmptyIndex << pos.entry;
361             }
362         }
363         ++(pos.entry);
364     }
365 
366     Q_EMIT signalNumberOfFuzziesChanged();
367     Q_EMIT signalNumberOfEmptyChanged();
368 }
369 
phase(const DocPosition & pos) const370 QString Catalog::phase(const DocPosition& pos) const
371 {
372     if (Q_UNLIKELY(!m_storage))
373         return QString();
374 
375     return m_storage->phase(pos);
376 }
377 
phase(const QString & name) const378 Phase Catalog::phase(const QString& name) const
379 {
380     return m_storage->phase(name);
381 }
382 
allPhases() const383 QList<Phase> Catalog::allPhases() const
384 {
385     return m_storage->allPhases();
386 }
387 
phaseNotes(const QString & phase) const388 QVector<Note> Catalog::phaseNotes(const QString& phase) const
389 {
390     return m_storage->phaseNotes(phase);
391 }
392 
setPhaseNotes(const QString & phase,QVector<Note> notes)393 QVector<Note> Catalog::setPhaseNotes(const QString& phase, QVector<Note> notes)
394 {
395     return m_storage->setPhaseNotes(phase, notes);
396 }
397 
allTools() const398 QMap<QString, Tool> Catalog::allTools() const
399 {
400     return m_storage->allTools();
401 }
402 
isPlural(uint index) const403 bool Catalog::isPlural(uint index) const
404 {
405     return m_storage && m_storage->isPlural(DocPosition(index));
406 }
407 
isApproved(uint index) const408 bool Catalog::isApproved(uint index) const
409 {
410     if (Q_UNLIKELY(!m_storage))
411         return false;
412 
413     bool extendedStates = m_storage->capabilities()&ExtendedStates;
414 
415     return (extendedStates &&::isApproved(state(DocPosition(index)), activePhaseRole()))
416            || (!extendedStates && m_storage->isApproved(DocPosition(index)));
417 }
418 
state(const DocPosition & pos) const419 TargetState Catalog::state(const DocPosition& pos) const
420 {
421     if (Q_UNLIKELY(!m_storage))
422         return NeedsTranslation;
423 
424     if (m_storage->capabilities()&ExtendedStates)
425         return m_storage->state(pos);
426     else
427         return closestState(m_storage->isApproved(pos), activePhaseRole());
428 }
429 
isEmpty(uint index) const430 bool Catalog::isEmpty(uint index) const
431 {
432     return m_storage && m_storage->isEmpty(DocPosition(index));
433 }
434 
isEmpty(const DocPosition & pos) const435 bool Catalog::isEmpty(const DocPosition& pos) const
436 {
437     return m_storage && m_storage->isEmpty(pos);
438 }
439 
440 
isEquivTrans(const DocPosition & pos) const441 bool Catalog::isEquivTrans(const DocPosition& pos) const
442 {
443     return m_storage && m_storage->isEquivTrans(pos);
444 }
445 
binUnitsCount() const446 int Catalog::binUnitsCount() const
447 {
448     return m_storage ? m_storage->binUnitsCount() : 0;
449 }
450 
unitById(const QString & id) const451 int Catalog::unitById(const QString& id) const
452 {
453     return m_storage ? m_storage->unitById(id) : 0;
454 }
455 
mimetype()456 QString Catalog::mimetype()
457 {
458     if (Q_UNLIKELY(!m_storage))
459         return QString();
460 
461     return m_storage->mimetype();
462 }
463 
fileType()464 QString Catalog::fileType()
465 {
466     if (Q_UNLIKELY(!m_storage))
467         return QString();
468 
469     return m_storage->fileType();
470 }
471 
type()472 CatalogType Catalog::type()
473 {
474     if (Q_UNLIKELY(!m_storage))
475         return Gettext;
476 
477     return m_storage->type();
478 }
479 
sourceLangCode() const480 QString Catalog::sourceLangCode() const
481 {
482     if (Q_UNLIKELY(!m_storage))
483         return QString();
484 
485     return m_storage->sourceLangCode();
486 }
487 
targetLangCode() const488 QString Catalog::targetLangCode() const
489 {
490     if (Q_UNLIKELY(!m_storage))
491         return QString();
492 
493     return m_storage->targetLangCode();
494 }
495 
setTargetLangCode(const QString & targetLangCode)496 void Catalog::setTargetLangCode(const QString& targetLangCode)
497 {
498     if (Q_UNLIKELY(!m_storage))
499         return;
500 
501     bool notify = m_storage->targetLangCode() != targetLangCode;
502     m_storage->setTargetLangCode(targetLangCode);
503     if (notify) Q_EMIT signalFileLoaded();
504 }
505 
506 //END STORAGE TRANSLATION
507 
508 //BEGIN OPEN/SAVE
509 #define DOESNTEXIST -1
510 #define ISNTREADABLE -2
511 #define UNKNOWNFORMAT -3
512 
checkAutoSave(const QString & url)513 KAutoSaveFile* Catalog::checkAutoSave(const QString& url)
514 {
515     KAutoSaveFile* autoSave = nullptr;
516     const QList<KAutoSaveFile*> staleFiles = KAutoSaveFile::staleFiles(QUrl::fromLocalFile(url));
517     for (KAutoSaveFile *stale : staleFiles) {
518         if (stale->open(QIODevice::ReadOnly) && !autoSave) {
519             autoSave = stale;
520             autoSave->setParent(this);
521         } else
522             stale->deleteLater();
523     }
524     if (autoSave)
525         qCInfo(LOKALIZE_LOG) << "autoSave" << autoSave->fileName();
526     return autoSave;
527 }
528 
loadFromUrl(const QString & filePath,const QString & saidUrl,int * fileSize,bool fast)529 int Catalog::loadFromUrl(const QString& filePath, const QString& saidUrl, int* fileSize, bool fast)
530 {
531     QFileInfo info(filePath);
532     if (Q_UNLIKELY(!info.exists() || info.isDir()))
533         return DOESNTEXIST;
534     if (Q_UNLIKELY(!info.isReadable()))
535         return ISNTREADABLE;
536     bool readOnly = !info.isWritable();
537 
538 
539     QElapsedTimer a; a.start();
540 
541     QFile file(filePath);
542     if (!file.open(QIODevice::ReadOnly))
543         return ISNTREADABLE;//TODO
544 
545     CatalogStorage* storage = nullptr;
546     if (filePath.endsWith(QLatin1String(".po")) || filePath.endsWith(QLatin1String(".pot")))
547         storage = new GettextCatalog::GettextStorage;
548     else if (filePath.endsWith(QLatin1String(".xlf")) || filePath.endsWith(QLatin1String(".xliff")))
549         storage = new XliffStorage;
550     else if (filePath.endsWith(QLatin1String(".ts")))
551         storage = new TsStorage;
552     else {
553         //try harder
554         QTextStream in(&file);
555         int i = 0;
556         bool gettext = false;
557         while (!in.atEnd() && ++i < 64 && !gettext)
558             gettext = in.readLine().contains(QLatin1String("msgid"));
559         if (gettext) storage = new GettextCatalog::GettextStorage;
560         else return UNKNOWNFORMAT;
561     }
562 
563     int line = storage->load(&file);
564 
565     file.close();
566 
567     if (Q_UNLIKELY(line != 0 || (!storage->size() && (line == -1)))) {
568         delete storage;
569         return line;
570     }
571 
572     if (a.elapsed() > 100) qCDebug(LOKALIZE_LOG) << filePath << "opened in" << a.elapsed();
573 
574     //ok...
575     clear();
576 
577     //commit transaction
578     m_storage = storage;
579 
580     updateApprovedEmptyIndexCache();
581 
582     d._numberOfPluralForms = storage->numberOfPluralForms();
583     d._autoSaveDirty = true;
584     d._readOnly = readOnly;
585     d._filePath = saidUrl.isEmpty() ? filePath : saidUrl;
586 
587     //set some sane role, a real phase with a nmae will be created later with the first edit command
588     setActivePhase(QString(), Project::local()->role());
589 
590     if (!fast) {
591         KAutoSaveFile* autoSave = checkAutoSave(d._filePath);
592         d._autoSaveRecovered = autoSave;
593         if (autoSave) {
594             d._autoSave->deleteLater();
595             d._autoSave = autoSave;
596 
597             //restore 'modified' status for entries
598             MergeCatalog* mergeCatalog = new MergeCatalog(this, this);
599             int errorLine = mergeCatalog->loadFromUrl(autoSave->fileName());
600             if (Q_LIKELY(errorLine == 0))
601                 mergeCatalog->copyToBaseCatalog();
602             mergeCatalog->deleteLater();
603             d._autoSave->close();
604         } else
605             d._autoSave->setManagedFile(QUrl::fromLocalFile(d._filePath));
606     }
607 
608     if (fileSize)
609         *fileSize = file.size();
610 
611     Q_EMIT signalFileLoaded();
612     Q_EMIT signalFileLoaded(d._filePath);
613     return 0;
614 }
615 
save()616 bool Catalog::save()
617 {
618     return saveToUrl(d._filePath);
619 }
620 
621 //this function is not called if QUndoStack::isClean() !
saveToUrl(QString localFilePath)622 bool Catalog::saveToUrl(QString localFilePath)
623 {
624     if (Q_UNLIKELY(!m_storage))
625         return true;
626 
627     bool nameChanged = localFilePath.length();
628     if (Q_LIKELY(!nameChanged))
629         localFilePath = d._filePath;
630 
631     QString localPath = QFileInfo(localFilePath).absolutePath();
632     if (!QFileInfo::exists(localPath))
633         if (!QDir::root().mkpath(localPath))
634             return false;
635     QFile file(localFilePath);
636     if (Q_UNLIKELY(!file.open(QIODevice::WriteOnly)))   //i18n("Wasn't able to open file %1",filename.ascii());
637         return false;
638 
639     bool belongsToProject = localFilePath.contains(Project::instance()->poDir());
640     if (Q_UNLIKELY(!m_storage->save(&file, belongsToProject)))
641         return false;
642 
643     file.close();
644 
645     d._autoSave->remove();
646     d._autoSaveRecovered = false;
647     setClean(); //undo/redo
648     if (nameChanged) {
649         d._filePath = localFilePath;
650         d._autoSave->setManagedFile(QUrl::fromLocalFile(localFilePath));
651     }
652 
653     //Settings::self()->setCurrentGroup("Bookmarks");
654     //Settings::self()->addItemIntList(d._filePath.url(),d._bookmarkIndex);
655 
656     Q_EMIT signalFileSaved();
657     Q_EMIT signalFileSaved(localFilePath);
658     return true;
659     /*
660         else if (status==NO_PERMISSIONS)
661         {
662             if (KMessageBox::warningContinueCancel(this,
663              i18n("You do not have permission to write to file:\n%1\n"
664               "Do you want to save to another file or cancel?", _currentURL.prettyUrl()),
665              i18n("Error"),KStandardGuiItem::save())==KMessageBox::Continue)
666                 return fileSaveAs();
667 
668         }
669     */
670 }
671 
doAutoSave()672 void Catalog::doAutoSave()
673 {
674     if (isClean() || !(d._autoSaveDirty))
675         return;
676     if (Q_UNLIKELY(!m_storage))
677         return;
678     if (!d._autoSave->open(QIODevice::WriteOnly)) {
679         Q_EMIT signalFileAutoSaveFailed(d._autoSave->fileName());
680         return;
681     }
682     qCInfo(LOKALIZE_LOG) << "doAutoSave" << d._autoSave->fileName();
683     m_storage->save(d._autoSave);
684     d._autoSave->close();
685     d._autoSaveDirty = false;
686 }
687 
projectConfigChanged()688 void Catalog::projectConfigChanged()
689 {
690     setActivePhase(activePhase(), Project::local()->role());
691 }
692 
contents()693 QByteArray Catalog::contents()
694 {
695     QBuffer buf;
696     buf.open(QIODevice::WriteOnly);
697     m_storage->save(&buf);
698     buf.close();
699     return buf.data();
700 }
701 
702 //END OPEN/SAVE
703 
704 
705 
706 /**
707  * helper method to keep db in a good shape :)
708  * called on
709  * 1) entry switch
710  * 2) automatic editing code like replace or undo/redo operation
711 **/
updateDB(const QString & filePath,const QString & ctxt,const CatalogString & english,const CatalogString & newTarget,int form,bool approved,const QString & dbName)712 static void updateDB(
713     const QString& filePath,
714     const QString& ctxt,
715     const CatalogString& english,
716     const CatalogString& newTarget,
717     int form,
718     bool approved,
719     const QString& dbName
720     //const DocPosition&,//for back tracking
721 )
722 {
723     TM::UpdateJob* j = new TM::UpdateJob(filePath, ctxt, english, newTarget, form, approved,
724                                          dbName);
725     TM::threadPool()->start(j);
726 }
727 
728 
729 //BEGIN UNDO/REDO
undo()730 const DocPosition& Catalog::undo()
731 {
732     QUndoStack::undo();
733     return d._lastModifiedPos;
734 }
735 
redo()736 const DocPosition& Catalog::redo()
737 {
738     QUndoStack::redo();
739     return d._lastModifiedPos;
740 }
741 
flushUpdateDBBuffer()742 void Catalog::flushUpdateDBBuffer()
743 {
744     if (!Settings::autoaddTM())
745         return;
746 
747     DocPosition pos = d._lastModifiedPos;
748     if (pos.entry == -1 || pos.entry >= numberOfEntries()) {
749         //nothing to flush
750         //qCWarning(LOKALIZE_LOG)<<"nothing to flush or new file opened";
751         return;
752     }
753     QString dbName;
754     if (Project::instance()->targetLangCode() == targetLangCode()) {
755         dbName = Project::instance()->projectID();
756     } else {
757         dbName = sourceLangCode() + '-' + targetLangCode();
758         qCInfo(LOKALIZE_LOG) << "updating" << dbName << "because target language of project db does not match" << Project::instance()->targetLangCode() << targetLangCode();
759         if (!TM::DBFilesModel::instance()->m_configurations.contains(dbName)) {
760             TM::OpenDBJob* openDBJob = new TM::OpenDBJob(dbName, TM::Local, true);
761             connect(openDBJob, &TM::OpenDBJob::done, TM::DBFilesModel::instance(), &TM::DBFilesModel::updateProjectTmIndex);
762 
763             openDBJob->m_setParams = true;
764             openDBJob->m_tmConfig.markup = Project::instance()->markup();
765             openDBJob->m_tmConfig.accel = Project::instance()->accel();
766             openDBJob->m_tmConfig.sourceLangCode = sourceLangCode();
767             openDBJob->m_tmConfig.targetLangCode = targetLangCode();
768 
769             TM::DBFilesModel::instance()->openDB(openDBJob);
770         }
771     }
772     int form = -1;
773     if (isPlural(pos.entry))
774         form = pos.form;
775     updateDB(url(),
776              context(pos.entry).first(),
777              sourceWithTags(pos),
778              targetWithTags(pos),
779              form,
780              isApproved(pos.entry),
781              dbName);
782 
783     d._lastModifiedPos = DocPosition();
784 }
785 
setLastModifiedPos(const DocPosition & pos)786 void Catalog::setLastModifiedPos(const DocPosition& pos)
787 {
788     if (pos.entry >= numberOfEntries()) //bin-units
789         return;
790 
791     bool entryChanged = DocPos(d._lastModifiedPos) != DocPos(pos);
792     if (entryChanged)
793         flushUpdateDBBuffer();
794 
795     d._lastModifiedPos = pos;
796 }
797 
addToEmptyIndexIfAppropriate(CatalogStorage * storage,const DocPosition & pos,bool alreadyEmpty)798 bool CatalogPrivate::addToEmptyIndexIfAppropriate(CatalogStorage* storage, const DocPosition& pos, bool alreadyEmpty)
799 {
800     if ((!pos.offset) && (storage->target(pos).isEmpty()) && (!alreadyEmpty)) {
801         insertInList(_emptyIndex, pos.entry);
802         return true;
803     }
804     return false;
805 }
806 
targetDelete(const DocPosition & pos,int count)807 void Catalog::targetDelete(const DocPosition& pos, int count)
808 {
809     if (Q_UNLIKELY(!m_storage))
810         return;
811 
812     bool alreadyEmpty = m_storage->isEmpty(pos);
813     m_storage->targetDelete(pos, count);
814 
815     if (d.addToEmptyIndexIfAppropriate(m_storage, pos, alreadyEmpty))
816         Q_EMIT signalNumberOfEmptyChanged();
817     Q_EMIT signalEntryModified(pos);
818 }
819 
removeFromUntransIndexIfAppropriate(CatalogStorage * storage,const DocPosition & pos)820 bool CatalogPrivate::removeFromUntransIndexIfAppropriate(CatalogStorage* storage, const DocPosition& pos)
821 {
822     if ((!pos.offset) && (storage->isEmpty(pos))) {
823         _emptyIndex.removeAll(pos.entry);
824         return true;
825     }
826     return false;
827 }
828 
targetInsert(const DocPosition & pos,const QString & arg)829 void Catalog::targetInsert(const DocPosition& pos, const QString& arg)
830 {
831     if (Q_UNLIKELY(!m_storage))
832         return;
833 
834     if (d.removeFromUntransIndexIfAppropriate(m_storage, pos))
835         Q_EMIT signalNumberOfEmptyChanged();
836 
837     m_storage->targetInsert(pos, arg);
838     Q_EMIT signalEntryModified(pos);
839 }
840 
targetInsertTag(const DocPosition & pos,const InlineTag & tag)841 void Catalog::targetInsertTag(const DocPosition& pos, const InlineTag& tag)
842 {
843     if (Q_UNLIKELY(!m_storage))
844         return;
845 
846     if (d.removeFromUntransIndexIfAppropriate(m_storage, pos))
847         Q_EMIT signalNumberOfEmptyChanged();
848 
849     m_storage->targetInsertTag(pos, tag);
850     Q_EMIT signalEntryModified(pos);
851 }
852 
targetDeleteTag(const DocPosition & pos)853 InlineTag Catalog::targetDeleteTag(const DocPosition& pos)
854 {
855     if (Q_UNLIKELY(!m_storage))
856         return InlineTag();
857 
858     bool alreadyEmpty = m_storage->isEmpty(pos);
859     InlineTag tag = m_storage->targetDeleteTag(pos);
860 
861     if (d.addToEmptyIndexIfAppropriate(m_storage, pos, alreadyEmpty))
862         Q_EMIT signalNumberOfEmptyChanged();
863     Q_EMIT signalEntryModified(pos);
864     return tag;
865 }
866 
setTarget(DocPosition pos,const CatalogString & s)867 void Catalog::setTarget(DocPosition pos, const CatalogString& s)
868 {
869     //TODO for case of markup present
870     m_storage->setTarget(pos, s.string);
871 }
872 
setState(const DocPosition & pos,TargetState state)873 TargetState Catalog::setState(const DocPosition& pos, TargetState state)
874 {
875     bool extendedStates = m_storage && m_storage->capabilities()&ExtendedStates;
876     bool approved =::isApproved(state, activePhaseRole());
877     if (Q_UNLIKELY(!m_storage
878                    || (extendedStates && m_storage->state(pos) == state)
879                    || (!extendedStates && m_storage->isApproved(pos) == approved)))
880         return this->state(pos);
881 
882     TargetState prevState;
883     if (extendedStates) {
884         prevState = m_storage->setState(pos, state);
885         d._statesIndex[prevState].removeAll(pos.entry);
886         insertInList(d._statesIndex[state], pos.entry);
887     } else {
888         prevState = closestState(!approved, activePhaseRole());
889         m_storage->setApproved(pos, approved);
890     }
891 
892     if (!approved) {
893         insertInList(d._nonApprovedIndex, pos.entry);
894         if (!m_storage->isEmpty(pos))
895             insertInList(d._nonApprovedNonEmptyIndex, pos.entry);
896     } else {
897         d._nonApprovedIndex.removeAll(pos.entry);
898         d._nonApprovedNonEmptyIndex.removeAll(pos.entry);
899     }
900 
901     Q_EMIT signalNumberOfFuzziesChanged();
902     Q_EMIT signalEntryModified(pos);
903 
904     return prevState;
905 }
906 
updatePhase(const Phase & phase)907 Phase Catalog::updatePhase(const Phase& phase)
908 {
909     return m_storage->updatePhase(phase);
910 }
911 
setEquivTrans(const DocPosition & pos,bool equivTrans)912 void Catalog::setEquivTrans(const DocPosition& pos, bool equivTrans)
913 {
914     if (m_storage) m_storage->setEquivTrans(pos, equivTrans);
915 }
916 
setModified(DocPos entry,bool modified)917 bool Catalog::setModified(DocPos entry, bool modified)
918 {
919     if (modified) {
920         if (d._modifiedEntries.contains(entry))
921             return false;
922         d._modifiedEntries.insert(entry);
923     } else
924         d._modifiedEntries.remove(entry);
925     return true;
926 }
927 
isModified(DocPos entry) const928 bool Catalog::isModified(DocPos entry) const
929 {
930     return d._modifiedEntries.contains(entry);
931 }
932 
isModified(int entry) const933 bool Catalog::isModified(int entry) const
934 {
935     if (!isPlural(entry))
936         return isModified(DocPos(entry, 0));
937 
938     int f = numberOfPluralForms();
939     while (--f >= 0)
940         if (isModified(DocPos(entry, f)))
941             return true;
942     return false;
943 }
944 
945 //END UNDO/REDO
946 
947 
948 
949 
findNextInList(const QLinkedList<int> & list,int index)950 int findNextInList(const QLinkedList<int>& list, int index)
951 {
952     int nextIndex = -1;
953     for (int key : list) {
954         if (Q_UNLIKELY(key > index)) {
955             nextIndex = key;
956             break;
957         }
958     }
959     return nextIndex;
960 }
961 
findPrevInList(const QLinkedList<int> & list,int index)962 int findPrevInList(const QLinkedList<int>& list, int index)
963 {
964     int prevIndex = -1;
965     for (int key : list) {
966         if (Q_UNLIKELY(key >= index))
967             break;
968         prevIndex = key;
969     }
970     return prevIndex;
971 }
972 
insertInList(QLinkedList<int> & list,int index)973 void insertInList(QLinkedList<int>& list, int index)
974 {
975     QLinkedList<int>::Iterator it = list.begin();
976     while (it != list.end() && index > *it)
977         ++it;
978     list.insert(it, index);
979 }
980 
setBookmark(uint idx,bool set)981 void Catalog::setBookmark(uint idx, bool set)
982 {
983     if (set)
984         insertInList(d._bookmarkIndex, idx);
985     else
986         d._bookmarkIndex.removeAll(idx);
987 }
988 
989 
isApproved(TargetState state,ProjectLocal::PersonRole role)990 bool isApproved(TargetState state, ProjectLocal::PersonRole role)
991 {
992     static const TargetState marginStates[] = {Translated, Final, SignedOff};
993     return state >= marginStates[role];
994 }
995 
isApproved(TargetState state)996 bool isApproved(TargetState state)
997 {
998     static const TargetState marginStates[] = {Translated, Final, SignedOff};
999     return state == marginStates[0] || state == marginStates[1] || state == marginStates[2];
1000 }
1001 
closestState(bool approved,ProjectLocal::PersonRole role)1002 TargetState closestState(bool approved, ProjectLocal::PersonRole role)
1003 {
1004     Q_ASSERT(role != ProjectLocal::Undefined);
1005     static const TargetState approvementStates[][3] = {
1006         {NeedsTranslation, NeedsReviewTranslation, NeedsReviewTranslation},
1007         {Translated, Final, SignedOff}
1008     };
1009     return approvementStates[approved][role];
1010 }
1011 
1012 
isObsolete(int entry) const1013 bool Catalog::isObsolete(int entry) const
1014 {
1015     if (Q_UNLIKELY(!m_storage))
1016         return false;
1017 
1018     return m_storage->isObsolete(entry);
1019 }
1020 
originalOdfFilePath()1021 QString Catalog::originalOdfFilePath()
1022 {
1023     if (Q_UNLIKELY(!m_storage))
1024         return QString();
1025 
1026     return m_storage->originalOdfFilePath();
1027 }
1028 
setOriginalOdfFilePath(const QString & odfFilePath)1029 void Catalog::setOriginalOdfFilePath(const QString& odfFilePath)
1030 {
1031     if (Q_UNLIKELY(!m_storage))
1032         return;
1033 
1034     m_storage->setOriginalOdfFilePath(odfFilePath);
1035 }
1036 
1037 
1038