1 /*
2   This file is part of Lokalize
3 
4   SPDX-FileCopyrightText: 2007-2014 Nick Shaforostoff <shafff@ukr.net>
5   SPDX-FileCopyrightText: 2018-2019 Simon Depiets <sdepiets@gmail.com>
6 
7   SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
8 */
9 
10 #include "tmview.h"
11 
12 #include "lokalize_debug.h"
13 
14 #include "jobs.h"
15 #include "tmscanapi.h"
16 #include "catalog.h"
17 #include "cmd.h"
18 #include "project.h"
19 #include "prefs_lokalize.h"
20 #include "dbfilesmodel.h"
21 #include "diff.h"
22 #include "xlifftextedit.h"
23 
24 #include <klocalizedstring.h>
25 #include <kmessagebox.h>
26 #include <kpassivepopup.h>
27 
28 #include <QTime>
29 #include <QDragEnterEvent>
30 #include <QMimeData>
31 #include <QFileInfo>
32 #include <QFile>
33 #include <QDir>
34 #include <QTimer>
35 #include <QToolTip>
36 #include <QMenu>
37 #include <QStringBuilder>
38 
39 #ifdef NDEBUG
40 #undef NDEBUG
41 #endif
42 #define DEBUG
43 using namespace TM;
44 
45 
46 struct DiffInfo {
47     DiffInfo(int reserveSize);
48 
49     QString diffClean;
50     QString old;
51     //Formatting info:
52     QByteArray diffIndex;
53     //Map old string-->d.diffClean
54     QVector<int> old2DiffClean;
55 };
56 
DiffInfo(int reserveSize)57 DiffInfo::DiffInfo(int reserveSize)
58 {
59     diffClean.reserve(reserveSize);
60     old.reserve(reserveSize);
61     diffIndex.reserve(reserveSize);
62     old2DiffClean.reserve(reserveSize);
63 }
64 
65 
66 
67 /**
68  * 0 - common
69  + - add
70  - - del
71  M - modified
72 
73  so the string is like 00000MM00+++---000
74  (M appears afterwards)
75 */
getDiffInfo(const QString & diff)76 static DiffInfo getDiffInfo(const QString& diff)
77 {
78     DiffInfo d(diff.size());
79 
80     QChar sep('{');
81     char state = '0';
82     //walk through diff string char-by-char
83     //calculate old and others
84     int pos = -1;
85     while (++pos < diff.size()) {
86         if (diff.at(pos) == sep) {
87             if (diff.indexOf(QLatin1String("{KBABELDEL}"), pos) == pos) {
88                 state = '-';
89                 pos += 10;
90             } else if (diff.indexOf(QLatin1String("{KBABELADD}"), pos) == pos) {
91                 state = '+';
92                 pos += 10;
93             } else if (diff.indexOf(QLatin1String("{/KBABEL"), pos) == pos) {
94                 state = '0';
95                 pos += 11;
96             }
97         } else {
98             if (state != '+') {
99                 d.old.append(diff.at(pos));
100                 d.old2DiffClean.append(d.diffIndex.count());
101             }
102             d.diffIndex.append(state);
103             d.diffClean.append(diff.at(pos));
104         }
105     }
106     return d;
107 }
108 
109 
mouseDoubleClickEvent(QMouseEvent * event)110 void TextBrowser::mouseDoubleClickEvent(QMouseEvent* event)
111 {
112     QTextBrowser::mouseDoubleClickEvent(event);
113 
114     QString sel = textCursor().selectedText();
115     if (!(sel.isEmpty() || sel.contains(' ')))
116         Q_EMIT textInsertRequested(sel);
117 }
118 
119 
TMView(QWidget * parent,Catalog * catalog,const QVector<QAction * > & actions_insert,const QVector<QAction * > & actions_remove)120 TMView::TMView(QWidget* parent, Catalog* catalog, const QVector<QAction*>& actions_insert, const QVector<QAction*>& actions_remove)
121     : QDockWidget(i18nc("@title:window", "Translation Memory"), parent)
122     , m_browser(new TextBrowser(this))
123     , m_catalog(catalog)
124     , m_currentSelectJob(nullptr)
125     , m_actions_insert(actions_insert)
126     , m_actions_remove(actions_remove)
127     , m_normTitle(i18nc("@title:window", "Translation Memory"))
128     , m_hasInfoTitle(m_normTitle + QStringLiteral(" [*]"))
129     , m_hasInfo(false)
130     , m_isBatching(false)
131     , m_markAsFuzzy(false)
132 {
133     setObjectName(QStringLiteral("TMView"));
134     setWidget(m_browser);
135 
136     m_browser->document()->setDefaultStyleSheet(QStringLiteral("p.close_match { font-weight:bold; }"));
137     m_browser->viewport()->setBackgroundRole(QPalette::Window);
138 
139     QTimer::singleShot(0, this, &TMView::initLater);
140     connect(m_catalog, QOverload<const QString &>::of(&Catalog::signalFileLoaded), this, &TMView::slotFileLoaded);
141 }
142 
~TMView()143 TMView::~TMView()
144 {
145 #if QT_VERSION >= 0x050500
146     int i = m_jobs.size();
147     while (--i >= 0)
148         TM::threadPool()->tryTake(m_jobs.at(i));
149 #endif
150 }
151 
initLater()152 void TMView::initLater()
153 {
154     setAcceptDrops(true);
155 
156     int i = m_actions_insert.size();
157     while (--i >= 0) {
158         connect(m_actions_insert.at(i), &QAction::triggered, this, [this, i] { slotUseSuggestion(i); });
159     }
160 
161     i = m_actions_remove.size();
162     while (--i >= 0) {
163         connect(m_actions_remove.at(i), &QAction::triggered, this, [this, i] { slotRemoveSuggestion(i); });
164     }
165 
166     setToolTip(i18nc("@info:tooltip", "Double-click any word to insert it into translation"));
167 
168     DBFilesModel::instance();
169 
170     connect(m_browser, &TM::TextBrowser::textInsertRequested, this, &TMView::textInsertRequested);
171     connect(m_browser, &TM::TextBrowser::customContextMenuRequested, this, &TMView::contextMenu);
172     //TODO ? kdisplayPaletteChanged
173 //     connect(KGlobalSettings::self(),,SIGNAL(kdisplayPaletteChanged()),this,SLOT(slotPaletteChanged()));
174 
175 }
176 
dragEnterEvent(QDragEnterEvent * event)177 void TMView::dragEnterEvent(QDragEnterEvent* event)
178 {
179     if (dragIsAcceptable(event->mimeData()->urls()))
180         event->acceptProposedAction();
181 }
182 
dropEvent(QDropEvent * event)183 void TMView::dropEvent(QDropEvent *event)
184 {
185     QStringList files;
186     const auto urls = event->mimeData()->urls();
187     for (const QUrl& url : urls)
188         files.append(url.toLocalFile());
189     if (scanRecursive(files, Project::instance()->projectID()))
190         event->acceptProposedAction();
191 }
192 
slotFileLoaded(const QString & filePath)193 void TMView::slotFileLoaded(const QString& filePath)
194 {
195     const QString& pID = Project::instance()->projectID();
196 
197     if (Settings::scanToTMOnOpen())
198         TM::threadPool()->start(new ScanJob(filePath, pID), SCAN);
199 
200     if (!Settings::prefetchTM()
201         && !m_isBatching)
202         return;
203 
204     m_cache.clear();
205 #if QT_VERSION >= 0x050500
206     int i = m_jobs.size();
207     while (--i >= 0)
208         TM::threadPool()->tryTake(m_jobs.at(i));
209 #endif
210     m_jobs.clear();
211 
212     DocPosition pos;
213     while (switchNext(m_catalog, pos)) {
214         if (!m_catalog->isEmpty(pos.entry)
215             && m_catalog->isApproved(pos.entry))
216             continue;
217         SelectJob* j = initSelectJob(m_catalog, pos, pID);
218         connect(j, &SelectJob::done, this, &TMView::slotCacheSuggestions);
219         m_jobs.append(j);
220     }
221 
222     //dummy job for the finish indication
223     BatchSelectFinishedJob* m_seq = new BatchSelectFinishedJob(this);
224     connect(m_seq, &BatchSelectFinishedJob::done, this, &TMView::slotBatchSelectDone);
225     TM::threadPool()->start(m_seq, BATCHSELECTFINISHED);
226     m_jobs.append(m_seq);
227 }
228 
slotCacheSuggestions(SelectJob * job)229 void TMView::slotCacheSuggestions(SelectJob* job)
230 {
231     m_jobs.removeAll(job);
232     qCDebug(LOKALIZE_LOG) << job->m_pos.entry;
233     if (job->m_pos.entry == m_pos.entry)
234         slotSuggestionsCame(job);
235 
236     m_cache[DocPos(job->m_pos)] = job->m_entries.toVector();
237 }
238 
slotBatchSelectDone()239 void TMView::slotBatchSelectDone()
240 {
241     m_jobs.clear();
242     if (!m_isBatching)
243         return;
244 
245     bool insHappened = false;
246     DocPosition pos;
247     while (switchNext(m_catalog, pos)) {
248         if (!(m_catalog->isEmpty(pos.entry)
249               || !m_catalog->isApproved(pos.entry))
250            )
251             continue;
252         const QVector<TMEntry>& suggList = m_cache.value(DocPos(pos));
253         if (suggList.isEmpty())
254             continue;
255         const TMEntry& entry = suggList.first();
256         if (entry.score < 9900) //hacky
257             continue;
258         {
259             bool forceFuzzy = (suggList.size() > 1 && suggList.at(1).score >= 10000)
260                               || entry.score < 10000;
261             bool ctxtMatches = entry.score == 1001;
262             if (!m_catalog->isApproved(pos.entry)) {
263                 ///m_catalog->push(new DelTextCmd(m_catalog,pos,m_catalog->msgstr(pos)));
264                 removeTargetSubstring(m_catalog, pos, 0, m_catalog->targetWithTags(pos).string.size());
265                 if (ctxtMatches || !(m_markAsFuzzy || forceFuzzy))
266                     SetStateCmd::push(m_catalog, pos, true);
267             } else if ((m_markAsFuzzy && !ctxtMatches) || forceFuzzy) {
268                 SetStateCmd::push(m_catalog, pos, false);
269             }
270             ///m_catalog->push(new InsTextCmd(m_catalog,pos,entry.target));
271             insertCatalogString(m_catalog, pos, entry.target, 0);
272 
273             if (Q_UNLIKELY(m_pos.entry == pos.entry && pos.form == m_pos.form))
274                 Q_EMIT refreshRequested();
275 
276         }
277         if (!insHappened) {
278             insHappened = true;
279             m_catalog->beginMacro(i18nc("@item Undo action", "Batch translation memory filling"));
280         }
281     }
282     QString msg = i18nc("@info", "Batch translation has been completed.");
283     if (insHappened)
284         m_catalog->endMacro();
285     else {
286         // xgettext: no-c-format
287         msg += ' ';
288         msg += i18nc("@info", "No suggestions with exact matches were found.");
289     }
290 
291     KPassivePopup::message(KPassivePopup::Balloon,
292                            i18nc("@title", "Batch translation complete"),
293                            msg,
294                            this);
295 }
296 
slotBatchTranslate()297 void TMView::slotBatchTranslate()
298 {
299     m_isBatching = true;
300     m_markAsFuzzy = false;
301     if (!Settings::prefetchTM())
302         slotFileLoaded(m_catalog->url());
303     else if (m_jobs.isEmpty())
304         return slotBatchSelectDone();
305     KPassivePopup::message(KPassivePopup::Balloon,
306                            i18nc("@title", "Batch translation"),
307                            i18nc("@info", "Batch translation has been scheduled."),
308                            this);
309 
310 }
311 
slotBatchTranslateFuzzy()312 void TMView::slotBatchTranslateFuzzy()
313 {
314     m_isBatching = true;
315     m_markAsFuzzy = true;
316     if (!Settings::prefetchTM())
317         slotFileLoaded(m_catalog->url());
318     else if (m_jobs.isEmpty())
319         slotBatchSelectDone();
320     KPassivePopup::message(KPassivePopup::Balloon,
321                            i18nc("@title", "Batch translation"),
322                            i18nc("@info", "Batch translation has been scheduled."),
323                            this);
324 
325 }
326 
slotNewEntryDisplayed()327 void TMView::slotNewEntryDisplayed()
328 {
329     return slotNewEntryDisplayed(DocPosition());
330 }
331 
slotNewEntryDisplayed(const DocPosition & pos)332 void TMView::slotNewEntryDisplayed(const DocPosition& pos)
333 {
334     if (m_catalog->numberOfEntries() <= pos.entry)
335         return;//because of Qt::QueuedConnection
336 
337 #if QT_VERSION >= 0x050500
338     int i = m_jobs.size();
339     while (--i >= 0)
340         TM::threadPool()->tryTake(m_currentSelectJob);
341 #endif
342 
343     //update DB
344     //m_catalog->flushUpdateDBBuffer();
345     //this is called via subscribtion
346 
347     if (pos.entry != -1)
348         m_pos = pos;
349     m_browser->clear();
350     if (Settings::prefetchTM()
351         && m_cache.contains(DocPos(m_pos))) {
352         QTimer::singleShot(0, this, &TMView::displayFromCache);
353     }
354     m_currentSelectJob = initSelectJob(m_catalog, m_pos);
355     connect(m_currentSelectJob, &TM::SelectJob::done, this, &TMView::slotSuggestionsCame);
356 }
357 
displayFromCache()358 void TMView::displayFromCache()
359 {
360     if (m_prevCachePos.entry == m_pos.entry
361         && m_prevCachePos.form == m_pos.form)
362         return;
363     SelectJob* temp = initSelectJob(m_catalog, m_pos, QString(), 0);
364     temp->m_entries = m_cache.value(DocPos(m_pos)).toList();
365     slotSuggestionsCame(temp);
366     temp->deleteLater();
367     m_prevCachePos = m_pos;
368 }
369 
slotSuggestionsCame(SelectJob * j)370 void TMView::slotSuggestionsCame(SelectJob* j)
371 {
372 //     QTime time;
373 //     time.start();
374 
375     SelectJob& job = *j;
376     job.deleteLater();
377     if (job.m_pos.entry != m_pos.entry)
378         return;
379 
380     Catalog& catalog = *m_catalog;
381     if (catalog.numberOfEntries() <= m_pos.entry)
382         return;//because of Qt::QueuedConnection
383 
384 
385     //BEGIN query other DBs handling
386     Project* project = Project::instance();
387     const QString& projectID = project->projectID();
388     //check if this is an additional query, from secondary DBs
389     if (job.m_dbName != projectID) {
390         job.m_entries += m_entries;
391         std::sort(job.m_entries.begin(), job.m_entries.end(), std::greater<TMEntry>());
392         const int limit = qMin(Settings::suggCount(), job.m_entries.size());
393         const int minScore = Settings::suggScore() * 100;
394         int i = job.m_entries.size() - 1;
395         while (i >= 0 && (i >= limit || job.m_entries.last().score < minScore)) {
396             job.m_entries.removeLast();
397             i--;
398         }
399     } else if (job.m_entries.isEmpty() || job.m_entries.first().score < 8500) {
400         //be careful, as we switched to QDirModel!
401         DBFilesModel& dbFilesModel = *(DBFilesModel::instance());
402         QModelIndex root = dbFilesModel.rootIndex();
403         int i = dbFilesModel.rowCount(root);
404         //qCWarning(LOKALIZE_LOG)<<"query other DBs,"<<i<<"total";
405         while (--i >= 0) {
406             const QString& dbName = dbFilesModel.data(dbFilesModel.index(i, 0, root), DBFilesModel::NameRole).toString();
407             if (projectID != dbName && dbFilesModel.m_configurations.value(dbName).targetLangCode == catalog.targetLangCode()) {
408                 SelectJob* j = initSelectJob(m_catalog, m_pos, dbName);
409                 connect(j, &SelectJob::done, this, &TMView::slotSuggestionsCame);
410                 m_jobs.append(j);
411             }
412         }
413     }
414     //END query other DBs handling
415 
416     m_entries = job.m_entries;
417 
418     const int limit = job.m_entries.size();
419 
420     if (!limit) {
421         if (m_hasInfo) {
422             m_hasInfo = false;
423             setWindowTitle(m_normTitle);
424         }
425         return;
426     }
427     if (!m_hasInfo) {
428         m_hasInfo = true;
429         setWindowTitle(m_hasInfoTitle);
430     }
431 
432     setUpdatesEnabled(false);
433     m_browser->clear();
434     m_entryPositions.clear();
435 
436     //m_entries=job.m_entries;
437     //m_browser->insertHtml("<html>");
438 
439     int i = 0;
440     QTextBlockFormat blockFormatBase;
441     QTextBlockFormat blockFormatAlternate;
442     blockFormatAlternate.setBackground(QPalette().alternateBase());
443     QTextCharFormat noncloseMatchCharFormat;
444     QTextCharFormat closeMatchCharFormat;
445     closeMatchCharFormat.setFontWeight(QFont::Bold);
446     while (true) {
447         QTextCursor cur = m_browser->textCursor();
448         QString html;
449         html.reserve(1024);
450 
451         const TMEntry& entry = job.m_entries.at(i);
452         html += (entry.score > 9500) ? QStringLiteral("<p class='close_match'>") : QStringLiteral("<p>");
453         //qCDebug(LOKALIZE_LOG)<<entry.target.string<<entry.hits;
454 
455         html += QStringLiteral("/");
456         html += QString(i18nc("%1 is the TM entry score in percentage", "%1%", entry.score > 10000 ? 100 : float(entry.score) / 100));
457         html += QStringLiteral(" ");
458         html += QString(i18ncp("%1 is the number of times this TM entry has been found", "(1 time)", "(%1 times)", entry.hits));
459         html += QStringLiteral("/ ");
460 
461 
462         //int sourceStartPos=cur.position();
463         QString result = entry.diff.toHtmlEscaped();
464         //result.replace("&","&amp;");
465         //result.replace("<","&lt;");
466         //result.replace(">","&gt;");
467         result.replace(QLatin1String("{KBABELADD}"), QStringLiteral("<font style=\"background-color:") + Settings::addColor().name() + QStringLiteral(";color:black\">"));
468         result.replace(QLatin1String("{/KBABELADD}"), QLatin1String("</font>"));
469         result.replace(QLatin1String("{KBABELDEL}"), QStringLiteral("<font style=\"background-color:") + Settings::delColor().name() + QStringLiteral(";color:black\">"));
470         result.replace(QLatin1String("{/KBABELDEL}"), QLatin1String("</font>"));
471         result.replace(QLatin1String("\\n"), QLatin1String("\\n<br>"));
472         result.replace(QLatin1String("\\n"), QLatin1String("\\n<br>"));
473         html += result;
474 #if 0
475         cur.insertHtml(result);
476 
477         cur.movePosition(QTextCursor::PreviousCharacter, QTextCursor::MoveAnchor, cur.position() - sourceStartPos);
478         CatalogString catStr(entry.diff);
479         catStr.string.remove("{KBABELDEL}"); catStr.string.remove("{/KBABELDEL}");
480         catStr.string.remove("{KBABELADD}"); catStr.string.remove("{/KBABELADD}");
481         catStr.tags = entry.source.tags;
482         DiffInfo d = getDiffInfo(entry.diff);
483         int j = catStr.tags.size();
484         while (--j >= 0) {
485             catStr.tags[j].start = d.old2DiffClean.at(catStr.tags.at(j).start);
486             catStr.tags[j].end  = d.old2DiffClean.at(catStr.tags.at(j).end);
487         }
488         insertContent(cur, catStr, job.m_source, false);
489 #endif
490 
491         //str.replace('&',"&amp;"); TODO check
492         html += QLatin1String("<br>");
493         if (Q_LIKELY(i < m_actions_insert.size())) {
494             m_actions_insert.at(i)->setStatusTip(entry.target.string);
495             html += QStringLiteral("[%1] ").arg(m_actions_insert.at(i)->shortcut().toString(QKeySequence::NativeText));
496         } else
497             html += QLatin1String("[ - ] ");
498         /*
499                 QString str(entry.target.string);
500                 str.replace('<',"&lt;");
501                 str.replace('>',"&gt;");
502                 html+=str;
503         */
504         cur.insertHtml(html); html.clear();
505         cur.setCharFormat((entry.score > 9500) ? closeMatchCharFormat : noncloseMatchCharFormat);
506         insertContent(cur, entry.target);
507         m_entryPositions.insert(cur.anchor(), i);
508 
509         html += i ? QStringLiteral("<br></p>") : QStringLiteral("</p>");
510         cur.insertHtml(html);
511 
512         if (Q_UNLIKELY(++i >= limit))
513             break;
514 
515         cur.insertBlock(i % 2 ? blockFormatAlternate : blockFormatBase);
516 
517     }
518     m_browser->insertHtml(QStringLiteral("</html>"));
519     setUpdatesEnabled(true);
520 //    qCWarning(LOKALIZE_LOG)<<"ELA "<<time.elapsed()<<"BLOCK COUNT "<<m_browser->document()->blockCount();
521 }
522 
523 
524 /*
525 void TMView::slotPaletteChanged()
526 {
527 
528 }*/
event(QEvent * event)529 bool TMView::event(QEvent *event)
530 {
531     if (event->type() == QEvent::ToolTip) {
532         QHelpEvent *helpEvent = static_cast<QHelpEvent *>(event);
533         //int block1=m_browser->cursorForPosition(m_browser->viewport()->mapFromGlobal(helpEvent->globalPos())).blockNumber();
534         QMap<int, int>::iterator block = m_entryPositions.lowerBound(m_browser->cursorForPosition(m_browser->viewport()->mapFromGlobal(helpEvent->globalPos())).anchor());
535         if (block != m_entryPositions.end() && *block < m_entries.size()) {
536             const TMEntry& tmEntry = m_entries.at(*block);
537             QString file = tmEntry.file;
538             if (file == m_catalog->url())
539                 file = i18nc("File argument in tooltip, when file is current file", "this");
540             QString tooltip = i18nc("@info:tooltip", "File: %1<br />Addition date: %2", file, tmEntry.date.toString(Qt::ISODate));
541             if (!tmEntry.changeDate.isNull() && tmEntry.changeDate != tmEntry.date)
542                 tooltip += i18nc("@info:tooltip on TM entry continues", "<br />Last change date: %1", tmEntry.changeDate.toString(Qt::ISODate));
543             if (!tmEntry.changeAuthor.isEmpty())
544                 tooltip += i18nc("@info:tooltip on TM entry continues", "<br />Last change author: %1", tmEntry.changeAuthor);
545             tooltip += i18nc("@info:tooltip on TM entry continues", "<br />TM: %1", tmEntry.dbName);
546             if (tmEntry.obsolete)
547                 tooltip += i18nc("@info:tooltip on TM entry continues", "<br />Is not present in the file anymore");
548             QToolTip::showText(helpEvent->globalPos(), tooltip);
549             return true;
550         }
551     }
552     return QWidget::event(event);
553 }
554 
removeEntry(const TMEntry & e)555 void TMView::removeEntry(const TMEntry& e)
556 {
557     if (KMessageBox::Yes == KMessageBox::questionYesNo(this, i18n("<html>Do you really want to remove this entry:<br/><i>%1</i><br/>from translation memory %2?</html>",  e.target.string.toHtmlEscaped(), e.dbName),
558             i18nc("@title:window", "Translation Memory Entry Removal"))) {
559         RemoveJob* job = new RemoveJob(e);
560         connect(job, SIGNAL(done()), this, SLOT(slotNewEntryDisplayed()));
561         TM::threadPool()->start(job, REMOVE);
562     }
563 }
564 
deleteFile(const TMEntry & e,const bool showPopUp)565 void TMView::deleteFile(const TMEntry& e, const bool showPopUp)
566 {
567     QString filePath = e.file;
568     if (Project::instance()->isFileMissing(filePath)) {
569         //File doesn't exist
570         RemoveFileJob* job = new RemoveFileJob(e.file, e.dbName);
571         connect(job, SIGNAL(done()), this, SLOT(slotNewEntryDisplayed()));
572         TM::threadPool()->start(job, REMOVEFILE);
573         if (showPopUp) {
574             KMessageBox::information(this, i18nc("@info", "The file %1 does not exist, it has been removed from the translation memory.", e.file));
575         }
576         return;
577     }
578 }
579 
contextMenu(const QPoint & pos)580 void TMView::contextMenu(const QPoint& pos)
581 {
582     int block = *m_entryPositions.lowerBound(m_browser->cursorForPosition(pos).anchor());
583     qCWarning(LOKALIZE_LOG) << block;
584     if (block >= m_entries.size())
585         return;
586 
587     const TMEntry& e = m_entries.at(block);
588     enum {Remove, RemoveFile, Open};
589     QMenu popup;
590     popup.addAction(i18nc("@action:inmenu", "Remove this entry"))->setData(Remove);
591     if (e.file != m_catalog->url() && QFile::exists(e.file))
592         popup.addAction(i18nc("@action:inmenu", "Open file containing this entry"))->setData(Open);
593     else {
594         if (Settings::deleteFromTMOnMissing()) {
595             //Automatic deletion
596             deleteFile(e, true);
597         } else if (!QFile::exists(e.file)) {
598             //Still offer manual deletion if this is not the current file
599             popup.addAction(i18nc("@action:inmenu", "Remove this missing file from TM"))->setData(RemoveFile);
600         }
601     }
602     QAction* r = popup.exec(m_browser->mapToGlobal(pos));
603     if (!r)
604         return;
605     if (r->data().toInt() == Remove) {
606         removeEntry(e);
607     } else if (r->data().toInt() == Open) {
608         Q_EMIT fileOpenRequested(e.file, e.source.string, e.ctxt, true);
609     } else if ((r->data().toInt() == RemoveFile) &&
610                KMessageBox::Yes == KMessageBox::questionYesNo(this, i18n("<html>Do you really want to remove this missing file:<br/><i>%1</i><br/>from translation memory %2?</html>",  e.file, e.dbName),
611                        i18nc("@title:window", "Translation Memory Missing File Removal"))) {
612         deleteFile(e, false);
613     }
614 }
615 
616 /**
617  * helper function:
618  * searches to th nearest rxNum or ABBR
619  * clears rxNum if ABBR is found before rxNum
620  */
nextPlacableIn(const QString & old,int start,QString & cap)621 static int nextPlacableIn(const QString& old, int start, QString& cap)
622 {
623     static QRegExp rxNum(QStringLiteral("[\\d\\.\\%]+"));
624     static QRegExp rxAbbr(QStringLiteral("\\w+"));
625 
626     int numPos = rxNum.indexIn(old, start);
627 //    int abbrPos=rxAbbr.indexIn(old,start);
628     int abbrPos = start;
629     //qCWarning(LOKALIZE_LOG)<<"seeing"<<old.size()<<old;
630     while (((abbrPos = rxAbbr.indexIn(old, abbrPos)) != -1)) {
631         QString word = rxAbbr.cap(0);
632         //check if tail contains uppoer case characters
633         const QChar* c = word.unicode() + 1;
634         int i = word.size() - 1;
635         while (--i >= 0) {
636             if ((c++)->isUpper())
637                 break;
638         }
639         abbrPos += rxAbbr.matchedLength();
640     }
641 
642     int pos = qMin(numPos, abbrPos);
643     if (pos == -1)
644         pos = qMax(numPos, abbrPos);
645 
646 //     if (pos==numPos)
647 //         cap=rxNum.cap(0);
648 //     else
649 //         cap=rxAbbr.cap(0);
650 
651     cap = (pos == numPos ? rxNum : rxAbbr).cap(0);
652     //qCWarning(LOKALIZE_LOG)<<cap;
653 
654     return pos;
655 }
656 
657 
658 
659 
660 
661 //TODO thorough testing
662 
663 /**
664  * this tries some black magic
665  * naturally, there are many assumptions that might not always be true
666  */
targetAdapted(const TMEntry & entry,const CatalogString & ref)667 CatalogString TM::targetAdapted(const TMEntry& entry, const CatalogString& ref)
668 {
669     qCWarning(LOKALIZE_LOG) << entry.source.string << entry.target.string << entry.diff;
670 
671     QString diff = entry.diff;
672     CatalogString target = entry.target;
673     //QString english=entry.english;
674 
675 
676     QRegExp rxAdd(QLatin1String("<font style=\"background-color:[^>]*") + Settings::addColor().name() + QLatin1String("[^>]*\">([^>]*)</font>"));
677     QRegExp rxDel(QLatin1String("<font style=\"background-color:[^>]*") + Settings::delColor().name() + QLatin1String("[^>]*\">([^>]*)</font>"));
678     //rxAdd.setMinimal(true);
679     //rxDel.setMinimal(true);
680 
681     //first things first
682     int pos = 0;
683     while ((pos = rxDel.indexIn(diff, pos)) != -1)
684         diff.replace(pos, rxDel.matchedLength(), "\tKBABELDEL\t" + rxDel.cap(1) + "\t/KBABELDEL\t");
685     pos = 0;
686     while ((pos = rxAdd.indexIn(diff, pos)) != -1)
687         diff.replace(pos, rxAdd.matchedLength(), "\tKBABELADD\t" + rxAdd.cap(1) + "\t/KBABELADD\t");
688 
689     diff.replace(QStringLiteral("&lt;"), QStringLiteral("<"));
690     diff.replace(QStringLiteral("&gt;"), QStringLiteral(">"));
691 
692     //possible enhancement: search for non-translated words in removedSubstrings...
693     //QStringList removedSubstrings;
694     //QStringList addedSubstrings;
695 
696 
697     /*
698       0 - common
699       + - add
700       - - del
701       M - modified
702 
703       so the string is like 00000MM00+++---000
704     */
705     DiffInfo d = getDiffInfo(diff);
706 
707     bool sameMarkup = Project::instance()->markup() == entry.markupExpr && !entry.markupExpr.isEmpty();
708     bool tryMarkup = !entry.target.tags.size() && sameMarkup;
709     //search for changed markup
710     if (tryMarkup) {
711         QRegExp rxMarkup(entry.markupExpr);
712         rxMarkup.setMinimal(true);
713         pos = 0;
714         int replacingPos = 0;
715         while ((pos = rxMarkup.indexIn(d.old, pos)) != -1) {
716             //qCWarning(LOKALIZE_LOG)<<"size"<<oldM.size()<<pos<<pos+rxMarkup.matchedLength();
717             QByteArray diffIndexPart(d.diffIndex.mid(d.old2DiffClean.at(pos),
718                                      d.old2DiffClean.at(pos + rxMarkup.matchedLength() - 1) + 1 - d.old2DiffClean.at(pos)));
719             //qCWarning(LOKALIZE_LOG)<<"diffMPart"<<diffMPart;
720             if (diffIndexPart.contains('-')
721                 || diffIndexPart.contains('+')) {
722                 //form newMarkup
723                 QString newMarkup;
724                 newMarkup.reserve(diffIndexPart.size());
725                 int j = -1;
726                 while (++j < diffIndexPart.size()) {
727                     if (diffIndexPart.at(j) != '-')
728                         newMarkup.append(d.diffClean.at(d.old2DiffClean.at(pos) + j));
729                 }
730 
731                 //replace first ocurrence
732                 int tmp = target.string.indexOf(rxMarkup.cap(0), replacingPos);
733                 if (tmp != -1) {
734                     target.replace(tmp,
735                                    rxMarkup.cap(0).size(),
736                                    newMarkup);
737                     replacingPos = tmp;
738                     //qCWarning(LOKALIZE_LOG)<<"d.old"<<rxMarkup.cap(0)<<"new"<<newMarkup;
739 
740                     //avoid trying this part again
741                     tmp = d.old2DiffClean.at(pos + rxMarkup.matchedLength() - 1);
742                     while (--tmp >= d.old2DiffClean.at(pos))
743                         d.diffIndex[tmp] = 'M';
744                     //qCWarning(LOKALIZE_LOG)<<"M"<<diffM;
745                 }
746             }
747 
748             pos += rxMarkup.matchedLength();
749         }
750     }
751 
752     //del, add only markup, punct, num
753     //TODO further improvement: spaces, punct marked as 0
754 //BEGIN BEGIN HANDLING
755     QRegExp rxNonTranslatable;
756     if (tryMarkup)
757         rxNonTranslatable.setPattern(QStringLiteral("^((") + entry.markupExpr + QStringLiteral(")|(\\W|\\d)+)+"));
758     else
759         rxNonTranslatable.setPattern(QStringLiteral("^(\\W|\\d)+"));
760 
761     //qCWarning(LOKALIZE_LOG)<<"("+entry.markup+"|(\\W|\\d)+";
762 
763 
764     //handle the beginning
765     int len = d.diffIndex.indexOf('0');
766     if (len > 0) {
767         QByteArray diffMPart(d.diffIndex.left(len));
768         int m = diffMPart.indexOf('M');
769         if (m != -1)
770             diffMPart.truncate(m);
771 
772 #if 0
773         nono
774         //first goes del, then add. so stop on second del sequence
775         bool seenAdd = false;
776         int j = -1;
777         while (++j < diffMPart.size()) {
778             if (diffMPart.at(j) == '+')
779                 seenAdd = true;
780             else if (seenAdd && diffMPart.at(j) == '-') {
781                 diffMPart.truncate(j);
782                 break;
783             }
784         }
785 #endif
786         //form 'oldMarkup'
787         QString oldMarkup;
788         oldMarkup.reserve(diffMPart.size());
789         int j = -1;
790         while (++j < diffMPart.size()) {
791             if (diffMPart.at(j) != '+')
792                 oldMarkup.append(d.diffClean.at(j));
793         }
794 
795         //qCWarning(LOKALIZE_LOG)<<"old"<<oldMarkup;
796         rxNonTranslatable.indexIn(oldMarkup); //FIXME if it fails?
797         oldMarkup = rxNonTranslatable.cap(0);
798         if (target.string.startsWith(oldMarkup)) {
799 
800             //form 'newMarkup'
801             QString newMarkup;
802             newMarkup.reserve(diffMPart.size());
803             j = -1;
804             while (++j < diffMPart.size()) {
805                 if (diffMPart.at(j) != '-')
806                     newMarkup.append(d.diffClean.at(j));
807             }
808             //qCWarning(LOKALIZE_LOG)<<"new"<<newMarkup;
809             rxNonTranslatable.indexIn(newMarkup);
810             newMarkup = rxNonTranslatable.cap(0);
811 
812             //replace
813             qCWarning(LOKALIZE_LOG) << "BEGIN HANDLING. replacing" << target.string.left(oldMarkup.size()) << "with" << newMarkup;
814             target.remove(0, oldMarkup.size());
815             target.insert(0, newMarkup);
816 
817             //avoid trying this part again
818             j = diffMPart.size();
819             while (--j >= 0)
820                 d.diffIndex[j] = 'M';
821             //qCWarning(LOKALIZE_LOG)<<"M"<<diffM;
822         }
823 
824     }
825 //END BEGIN HANDLING
826 //BEGIN END HANDLING
827     if (tryMarkup)
828         rxNonTranslatable.setPattern(QStringLiteral("((") + entry.markupExpr + QStringLiteral(")|(\\W|\\d)+)+$"));
829     else
830         rxNonTranslatable.setPattern(QStringLiteral("(\\W|\\d)+$"));
831 
832     //handle the end
833     if (!d.diffIndex.endsWith('0')) {
834         len = d.diffIndex.lastIndexOf('0') + 1;
835         QByteArray diffMPart(d.diffIndex.mid(len));
836         int m = diffMPart.lastIndexOf('M');
837         if (m != -1) {
838             len = m + 1;
839             diffMPart = diffMPart.mid(len);
840         }
841 
842         //form 'oldMarkup'
843         QString oldMarkup;
844         oldMarkup.reserve(diffMPart.size());
845         int j = -1;
846         while (++j < diffMPart.size()) {
847             if (diffMPart.at(j) != '+')
848                 oldMarkup.append(d.diffClean.at(len + j));
849         }
850         //qCWarning(LOKALIZE_LOG)<<"old-"<<oldMarkup;
851         rxNonTranslatable.indexIn(oldMarkup);
852         oldMarkup = rxNonTranslatable.cap(0);
853         if (target.string.endsWith(oldMarkup)) {
854 
855             //form newMarkup
856             QString newMarkup;
857             newMarkup.reserve(diffMPart.size());
858             j = -1;
859             while (++j < diffMPart.size()) {
860                 if (diffMPart.at(j) != '-')
861                     newMarkup.append(d.diffClean.at(len + j));
862             }
863             //qCWarning(LOKALIZE_LOG)<<"new"<<newMarkup;
864             rxNonTranslatable.indexIn(newMarkup);
865             newMarkup = rxNonTranslatable.cap(0);
866 
867             //replace
868             target.string.chop(oldMarkup.size());
869             target.string.append(newMarkup);
870 
871             //avoid trying this part again
872             j = diffMPart.size();
873             while (--j >= 0)
874                 d.diffIndex[len + j] = 'M';
875             //qCWarning(LOKALIZE_LOG)<<"M"<<diffM;
876         }
877     }
878 //END BEGIN HANDLING
879 
880     //search for numbers and stuff
881     //QRegExp rxNum("[\\d\\.\\%]+");
882     pos = 0;
883     int replacingPos = 0;
884     QString cap;
885     QString _;
886     //while ((pos=rxNum.indexIn(old,pos))!=-1)
887     qCWarning(LOKALIZE_LOG) << "string:" << target.string << "searching for placeables in" << d.old;
888     while ((pos = nextPlacableIn(d.old, pos, cap)) != -1) {
889         qCDebug(LOKALIZE_LOG) << "considering placable" << cap;
890         //save these so we can use rxNum in a body
891         int endPos1 = pos + cap.size() - 1;
892         int endPos = d.old2DiffClean.at(endPos1);
893         int startPos = d.old2DiffClean.at(pos);
894         QByteArray diffMPart = d.diffIndex.mid(startPos,
895                                                endPos + 1 - startPos);
896 
897         qCDebug(LOKALIZE_LOG) << "starting diffMPart" << diffMPart;
898 
899         //the following loop extends replacement text, e.g. for 1 -> 500 cases
900         while ((++endPos < d.diffIndex.size())
901                && (d.diffIndex.at(endPos) == '+')
902                && (-1 != nextPlacableIn(QString(d.diffClean.at(endPos)), 0, _))
903               )
904             diffMPart.append('+');
905 
906         qCDebug(LOKALIZE_LOG) << "diffMPart extended 1" << diffMPart;
907 //         if ((pos-1>=0) && (d.old2DiffClean.at(pos)>=0))
908 //         {
909 //             qCWarning(LOKALIZE_LOG)<<"d.diffIndex"<<d.diffIndex<<d.old2DiffClean.at(pos)-1;
910 //             qCWarning(LOKALIZE_LOG)<<"(d.diffIndex.at(d.old2DiffClean.at(pos-1))=='+')"<<(d.diffIndex.at(d.old2DiffClean.at(pos-1))=='+');
911 //             //qCWarning(LOKALIZE_LOG)<<(-1!=nextPlacableIn(QString(d.diffClean.at(d.old2DiffClean.at(pos))),0,_));
912 //         }
913 
914         //this is for the case when +'s preceed -'s:
915         while ((--startPos >= 0)
916                && (d.diffIndex.at(startPos) == '+')
917                //&&(-1!=nextPlacableIn(QString(d.diffClean.at(d.old2DiffClean.at(pos))),0,_))
918               )
919             diffMPart.prepend('+');
920         ++startPos;
921 
922         qCDebug(LOKALIZE_LOG) << "diffMPart extended 2" << diffMPart;
923 
924         if ((diffMPart.contains('-')
925              || diffMPart.contains('+'))
926             && (!diffMPart.contains('M'))) {
927             //form newMarkup
928             QString newMarkup;
929             newMarkup.reserve(diffMPart.size());
930             int j = -1;
931             while (++j < diffMPart.size()) {
932                 if (diffMPart.at(j) != '-')
933                     newMarkup.append(d.diffClean.at(startPos + j));
934             }
935             if (newMarkup.endsWith(' ')) newMarkup.chop(1);
936             //qCWarning(LOKALIZE_LOG)<<"d.old"<<cap<<"new"<<newMarkup;
937 
938 
939             //replace first ocurrence
940             int tmp = target.string.indexOf(cap, replacingPos);
941             if (tmp != -1) {
942                 qCWarning(LOKALIZE_LOG) << "replacing" << cap << "with" << newMarkup;
943                 target.replace(tmp, cap.size(), newMarkup);
944                 replacingPos = tmp;
945 
946                 //avoid trying this part again
947                 tmp = d.old2DiffClean.at(endPos1) + 1;
948                 while (--tmp >= d.old2DiffClean.at(pos))
949                     d.diffIndex[tmp] = 'M';
950                 //qCWarning(LOKALIZE_LOG)<<"M"<<diffM;
951             } else
952                 qCWarning(LOKALIZE_LOG) << "newMarkup" << newMarkup << "wasn't used";
953         }
954         pos = endPos1 + 1;
955     }
956     adaptCatalogString(target, ref);
957     return target;
958 }
959 
slotRemoveSuggestion(int i)960 void TMView::slotRemoveSuggestion(int i)
961 {
962     if (Q_UNLIKELY(i >= m_entries.size()))
963         return;
964 
965     const TMEntry& e = m_entries.at(i);
966     removeEntry(e);
967 }
968 
slotUseSuggestion(int i)969 void TMView::slotUseSuggestion(int i)
970 {
971     if (Q_UNLIKELY(i >= m_entries.size()))
972         return;
973 
974     CatalogString target = targetAdapted(m_entries.at(i), m_catalog->sourceWithTags(m_pos));
975 
976 #if 0
977     QString tmp = target.string;
978     tmp.replace(TAGRANGE_IMAGE_SYMBOL, '*');
979     qCWarning(LOKALIZE_LOG) << "targetAdapted" << tmp;
980 
981     foreach (InlineTag tag, target.tags)
982         qCWarning(LOKALIZE_LOG) << "tag" << tag.start << tag.end;
983 #endif
984     if (Q_UNLIKELY(target.isEmpty()))
985         return;
986 
987     m_catalog->beginMacro(i18nc("@item Undo action", "Use translation memory suggestion"));
988 
989     QString old = m_catalog->targetWithTags(m_pos).string;
990     if (!old.isEmpty()) {
991         m_pos.offset = 0;
992         //FIXME test!
993         removeTargetSubstring(m_catalog, m_pos, 0, old.size());
994         //m_catalog->push(new DelTextCmd(m_catalog,m_pos,m_catalog->msgstr(m_pos)));
995     }
996     qCWarning(LOKALIZE_LOG) << "1" << target.string;
997 
998     //m_catalog->push(new InsTextCmd(m_catalog,m_pos,target)/*,true*/);
999     insertCatalogString(m_catalog, m_pos, target, 0);
1000 
1001     if (m_entries.at(i).score > 9900 && !m_catalog->isApproved(m_pos.entry))
1002         SetStateCmd::push(m_catalog, m_pos, true);
1003 
1004     m_catalog->endMacro();
1005 
1006     Q_EMIT refreshRequested();
1007 }
1008 
1009