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("&","&");
465 //result.replace("<","<");
466 //result.replace(">",">");
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('&',"&"); 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('<',"<");
501 str.replace('>',">");
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("<"), QStringLiteral("<"));
690 diff.replace(QStringLiteral(">"), 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