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 "xlifftextedit.h"
11 
12 #include "lokalize_debug.h"
13 
14 #include "catalog.h"
15 #include "cmd.h"
16 #include "syntaxhighlighter.h"
17 #include "prefs_lokalize.h"
18 #include "prefs.h"
19 #include "project.h"
20 #include "completionstorage.h"
21 #include "languagetoolmanager.h"
22 #include "languagetoolresultjob.h"
23 #include "languagetoolparser.h"
24 
25 #include <klocalizedstring.h>
26 #include <kcompletionbox.h>
27 
28 #include <QStringBuilder>
29 #include <QPixmap>
30 #include <QPushButton>
31 #include <QPainter>
32 #include <QStyle>
33 #include <QApplication>
34 #include <QStyleOptionButton>
35 #include <QMimeData>
36 #include <QMetaType>
37 #include <QMenu>
38 #include <QMouseEvent>
39 #include <QToolTip>
40 #include <QScrollBar>
41 #include <QElapsedTimer>
42 #include <QJsonDocument>
43 #include <QJsonObject>
44 
generateImage(const QString & str,const QFont & font)45 inline static QImage generateImage(const QString& str, const QFont& font)
46 {
47     //     im_count++;
48     //     QTime a;a.start();
49 
50     QStyleOptionButton opt;
51     opt.fontMetrics = QFontMetrics(font);
52     opt.text = ' ' + str + ' ';
53     opt.rect = opt.fontMetrics.boundingRect(opt.text).adjusted(0, 0, 5, 5);
54     opt.rect.moveTo(0, 0);
55 
56     QImage result(opt.rect.size(), QImage::Format_ARGB32);
57     result.fill(0);//0xAARRGGBB
58     QPainter painter(&result);
59     QApplication::style()->drawControl(QStyle::CE_PushButton, &opt, &painter);
60 
61     //     im_time+=a.elapsed();
62     //     qCWarning(LOKALIZE_LOG)<<im_count<<im_time;
63     return result;
64 }
65 class MyCompletionBox: public KCompletionBox
66 {
67 public:
MyCompletionBox(QWidget * p)68     MyCompletionBox(QWidget* p): KCompletionBox(p) {}
69     ~MyCompletionBox() override = default;
70     QSize sizeHint() const override;
71 
72     bool eventFilter(QObject*, QEvent*) override;   //reimplemented to deliver more keypresses to XliffTextEdit
73 };
74 
sizeHint() const75 QSize MyCompletionBox::sizeHint() const
76 {
77     int h = count() ? (sizeHintForRow(0)) : 0;
78     h = qMin(count() * h, 10 * h) + 2 * frameWidth();
79     int w = sizeHintForColumn(0) + verticalScrollBar()->width() + 2 * frameWidth();
80     return QSize(w, h);
81 }
82 
eventFilter(QObject * object,QEvent * event)83 bool MyCompletionBox::eventFilter(QObject* object, QEvent* event)
84 {
85     if (event->type() == QEvent::KeyPress) {
86         QKeyEvent* e = static_cast<QKeyEvent*>(event);
87         if (e->key() == Qt::Key_PageDown || e->key() == Qt::Key_PageUp) {
88             hide();
89             return false;
90         }
91     }
92     return KCompletionBox::eventFilter(object, event);
93 }
94 
~TranslationUnitTextEdit()95 TranslationUnitTextEdit::~TranslationUnitTextEdit()
96 {
97     disconnect(document(), &QTextDocument::contentsChange, this, &TranslationUnitTextEdit::contentsChanged);
98 }
99 
TranslationUnitTextEdit(Catalog * catalog,DocPosition::Part part,QWidget * parent)100 TranslationUnitTextEdit::TranslationUnitTextEdit(Catalog* catalog, DocPosition::Part part, QWidget* parent)
101     : KTextEdit(parent)
102     , m_currentUnicodeNumber(0)
103     , m_langUsesSpaces(true)
104     , m_catalog(catalog)
105     , m_part(part)
106     , m_highlighter(new SyntaxHighlighter(this))
107     , m_enabled(Settings::autoSpellcheck())
108     , m_completionBox(nullptr)
109     , m_cursorSelectionStart(0)
110     , m_cursorSelectionEnd(0)
111     , m_languageToolTimer(new QTimer(this))
112 {
113     setReadOnly(part == DocPosition::Source);
114     setUndoRedoEnabled(false);
115     setAcceptRichText(false);
116 
117     m_highlighter->setActive(m_enabled);
118     setHighlighter(m_highlighter);
119 
120     if (part == DocPosition::Target) {
121         connect(document(), &QTextDocument::contentsChange, this, &TranslationUnitTextEdit::contentsChanged);
122         connect(this, &KTextEdit::cursorPositionChanged, this, &TranslationUnitTextEdit::emitCursorPositionChanged);
123         connect(m_languageToolTimer, &QTimer::timeout, this, &TranslationUnitTextEdit::launchLanguageTool);
124     }
125     connect(catalog, QOverload<>::of(&Catalog::signalFileLoaded), this, &TranslationUnitTextEdit::fileLoaded);
126     //connect (Project::instance(), &Project::configChanged, this, &TranslationUnitTextEdit::projectConfigChanged);
127     m_languageToolTimer->setSingleShot(true);
128 }
129 
setSpellCheckingEnabled(bool enable)130 void TranslationUnitTextEdit::setSpellCheckingEnabled(bool enable)
131 {
132     Settings::setAutoSpellcheck(enable);
133     m_enabled = enable;
134     m_highlighter->setActive(enable);
135     SettingsController::instance()->dirty = true;
136 }
137 
setVisualizeSeparators(bool enable)138 void TranslationUnitTextEdit::setVisualizeSeparators(bool enable)
139 {
140     if (enable) {
141         QTextOption textoption = document()->defaultTextOption();
142         textoption.setFlags(textoption.flags() | QTextOption::ShowLineAndParagraphSeparators | QTextOption::ShowTabsAndSpaces);
143         document()->setDefaultTextOption(textoption);
144     } else {
145         QTextOption textoption = document()->defaultTextOption();
146         textoption.setFlags(textoption.flags() & (~QTextOption::ShowLineAndParagraphSeparators) & (~QTextOption::ShowTabsAndSpaces));
147         document()->setDefaultTextOption(textoption);
148     }
149 }
150 
151 
fileLoaded()152 void TranslationUnitTextEdit::fileLoaded()
153 {
154     QString langCode = m_part == DocPosition::Source ? m_catalog->sourceLangCode() : m_catalog->targetLangCode();
155 
156     QLocale langLocale(langCode);
157     // First try to use a locale name derived from the language code
158     m_highlighter->setCurrentLanguage(langLocale.name());
159     //qCWarning(LOKALIZE_LOG) << "Attempting to set highlighting for " << m_part << " as " << langLocale.name();
160     // If that fails, try to use the language code directly
161     if (m_highlighter->currentLanguage() != langLocale.name() || m_highlighter->currentLanguage().isEmpty()) {
162         m_highlighter->setCurrentLanguage(langCode);
163         //qCWarning(LOKALIZE_LOG) << "Attempting to set highlighting for " << m_part << " as " << langCode;
164         if (m_highlighter->currentLanguage() != langCode && langCode.length() > 2) {
165             m_highlighter->setCurrentLanguage(langCode.left(2));
166             //qCWarning(LOKALIZE_LOG) << "Attempting to set highlighting for " << m_part << " as " << langCode.left(2);
167         }
168     }
169     m_highlighter->setAutoDetectLanguageDisabled(m_highlighter->spellCheckerFound());
170     //qCWarning(LOKALIZE_LOG) << "Spellchecker found "<<m_highlighter->spellCheckerFound()<< " as "<<m_highlighter->currentLanguage();
171     //setSpellCheckingLanguage(m_highlighter->currentLanguage());
172     //"i use an english locale while translating kde pot files from english to hebrew" Bug #181989
173     Qt::LayoutDirection targetLanguageDirection = Qt::LeftToRight;
174     static const QLocale::Language rtlLanguages[] = {QLocale::Arabic, QLocale::Hebrew, QLocale::Urdu, QLocale::Persian, QLocale::Pashto};
175     int i = sizeof(rtlLanguages) / sizeof(QLocale::Arabic);
176     while (--i >= 0 && langLocale.language() != rtlLanguages[i])
177         ;
178     if (i != -1)
179         targetLanguageDirection = Qt::RightToLeft;
180     setLayoutDirection(targetLanguageDirection);
181 
182     if (m_part == DocPosition::Source)
183         return;
184 
185     //"Some language do not need space between words. For example Chinese."
186     static const QLocale::Language noSpaceLanguages[] = {QLocale::Chinese};
187     i = sizeof(noSpaceLanguages) / sizeof(QLocale::Chinese);
188     while (--i >= 0 && langLocale.language() != noSpaceLanguages[i])
189         ;
190     m_langUsesSpaces = (i == -1);
191 }
192 
reflectApprovementState()193 void TranslationUnitTextEdit::reflectApprovementState()
194 {
195     if (m_part == DocPosition::Source || m_currentPos.entry == -1)
196         return;
197 
198     bool approved = m_catalog->isApproved(m_currentPos.entry);
199 
200     disconnect(document(), &QTextDocument::contentsChange, this, &TranslationUnitTextEdit::contentsChanged);
201     m_highlighter->setApprovementState(approved);
202     m_highlighter->rehighlight();
203     connect(document(), &QTextDocument::contentsChange, this, &TranslationUnitTextEdit::contentsChanged);
204     viewport()->setBackgroundRole(approved ? QPalette::Base : QPalette::AlternateBase);
205 
206 
207     if (approved) Q_EMIT approvedEntryDisplayed();
208     else          Q_EMIT nonApprovedEntryDisplayed();
209 
210     bool untr = m_catalog->isEmpty(m_currentPos);
211     if (untr)     Q_EMIT untranslatedEntryDisplayed();
212     else          Q_EMIT translatedEntryDisplayed();
213 }
214 
reflectUntranslatedState()215 void TranslationUnitTextEdit::reflectUntranslatedState()
216 {
217     if (m_part == DocPosition::Source || m_currentPos.entry == -1)
218         return;
219 
220     bool untr = m_catalog->isEmpty(m_currentPos);
221     if (untr)     Q_EMIT untranslatedEntryDisplayed();
222     else          Q_EMIT translatedEntryDisplayed();
223 }
224 
225 
226 /**
227  * makes MsgEdit reflect current entry
228  **/
showPos(DocPosition docPosition,const CatalogString & refStr,bool keepCursor)229 CatalogString TranslationUnitTextEdit::showPos(DocPosition docPosition, const CatalogString& refStr, bool keepCursor)
230 {
231     docPosition.part = m_part;
232     m_currentPos = docPosition;
233 
234     CatalogString catalogString = m_catalog->catalogString(m_currentPos);
235     QString target = catalogString.string;
236     _oldMsgstr = target;
237     //_oldMsgstrAscii=document()->toPlainText(); <-- MOVED THIS TO THE END
238 
239     //BEGIN pos
240     QTextCursor cursor = textCursor();
241     int pos = cursor.position();
242     int anchor = cursor.anchor();
243     //qCWarning(LOKALIZE_LOG)<<"called"<<"pos"<<pos<<anchor<<"keepCursor"<<keepCursor;
244     if (!keepCursor && toPlainText() != target) {
245         //qCWarning(LOKALIZE_LOG)<<"resetting pos";
246         pos = 0;
247         anchor = 0;
248     }
249     //END pos
250 
251     disconnect(document(), &QTextDocument::contentsChange, this, &TranslationUnitTextEdit::contentsChanged);
252     if (docPosition.part == DocPosition::Source)
253         setContent(catalogString);
254     else
255         setContent(catalogString, refStr.string.isEmpty() ? m_catalog->sourceWithTags(docPosition) : refStr);
256     connect(document(), &QTextDocument::contentsChange, this, &TranslationUnitTextEdit::contentsChanged);
257 
258     _oldMsgstrAscii = document()->toPlainText();
259 
260     //BEGIN pos
261     QTextCursor t = textCursor();
262     t.movePosition(QTextCursor::Start);
263     if (pos || anchor) {
264         //qCWarning(LOKALIZE_LOG)<<"setting"<<anchor<<pos;
265         // I don't know why the following (more correct) code does not work
266         t.setPosition(anchor, QTextCursor::MoveAnchor);
267         int length = pos - anchor;
268         if (length)
269             t.movePosition(length < 0 ? QTextCursor::PreviousCharacter : QTextCursor::NextCharacter, QTextCursor::KeepAnchor, qAbs(length));
270     }
271     setTextCursor(t);
272     //qCWarning(LOKALIZE_LOG)<<"set?"<<textCursor().anchor()<<textCursor().position();
273     //END pos
274 
275     reflectApprovementState();
276     reflectUntranslatedState();
277     return catalogString; //for the sake of not calling XliffStorage/doContent twice
278 }
279 
setContent(const CatalogString & catStr,const CatalogString & refStr)280 void TranslationUnitTextEdit::setContent(const CatalogString& catStr, const CatalogString& refStr)
281 {
282     //qCWarning(LOKALIZE_LOG)<<"";
283     //qCWarning(LOKALIZE_LOG)<<"START";
284     //qCWarning(LOKALIZE_LOG)<<str<<ranges.size();
285     //prevent undo tracking system from recording this 'action'
286     document()->blockSignals(true);
287     clear();
288 
289     QTextCursor c = textCursor();
290     insertContent(c, catStr, refStr);
291 
292     document()->blockSignals(false);
293 
294     if (m_part == DocPosition::Target)
295         m_highlighter->setSourceString(refStr.string);
296     else
297         //reflectApprovementState() does this for Target
298         m_highlighter->rehighlight(); //explicitly because the signals were disabled
299     if (Settings::self()->languageToolDelay() > 0) {
300         m_languageToolTimer->start(Settings::self()->languageToolDelay() * 1000);
301     }
302 }
303 
304 #if 0
305 struct SearchFunctor {
306     virtual int operator()(const QString& str, int startingPos);
307 };
308 
309 int SearchFunctor::operator()(const QString& str, int startingPos)
310 {
311     return str.indexOf(TAGRANGE_IMAGE_SYMBOL, startingPos);
312 }
313 
314 struct AlternativeSearchFunctor: public SearchFunctor {
315     int operator()(const QString& str, int startingPos);
316 };
317 
318 int AlternativeSearchFunctor::operator()(const QString& str, int startingPos)
319 {
320     int tagPos = str.indexOf(TAGRANGE_IMAGE_SYMBOL, startingPos);
321     int diffStartPos = str.indexOf("{KBABEL", startingPos);
322     int diffEndPos = str.indexOf("{/KBABEL", startingPos);
323 
324     int diffPos = qMin(diffStartPos, diffEndPos);
325     if (diffPos == -1)
326         diffPos = qMax(diffStartPos, diffEndPos);
327 
328     int result = qMin(tagPos, diffPos);
329     if (result == -1)
330         result = qMax(tagPos, diffPos);
331     return result;
332 }
333 #endif
334 
insertContent(QTextCursor & cursor,const CatalogString & catStr,const CatalogString & refStr,bool insertText)335 void insertContent(QTextCursor& cursor, const CatalogString& catStr, const CatalogString& refStr, bool insertText)
336 {
337     //settings for TMView
338     QTextCharFormat chF = cursor.charFormat();
339     QFont font = cursor.document()->defaultFont();
340     //font.setWeight(chF.fontWeight());
341 
342     QMap<int, int> posToTag;
343     int i = catStr.tags.size();
344     while (--i >= 0) {
345         //qCDebug(LOKALIZE_LOG)<<"\t"<<catStr.tags.at(i).getElementName()<<catStr.tags.at(i).id<<catStr.tags.at(i).start<<catStr.tags.at(i).end;
346         posToTag.insert(catStr.tags.at(i).start, i);
347         posToTag.insert(catStr.tags.at(i).end, i);
348     }
349 
350     QMap<QString, int> sourceTagIdToIndex = refStr.tagIdToIndex();
351     int refTagIndexOffset = sourceTagIdToIndex.size();
352 
353     i = 0;
354     int prev = 0;
355     while ((i = catStr.string.indexOf(TAGRANGE_IMAGE_SYMBOL, i)) != -1) {
356 #if 0
357         SearchFunctor nextStopSymbol = AlternativeSearchFunctor();
358         char state = '0';
359         while ((i = nextStopSymbol(catStr.string, i)) != -1) {
360             //handle diff display for TMView
361             if (catStr.string.at(i) != TAGRANGE_IMAGE_SYMBOL) {
362                 if (catStr.string.at(i + 1) == '/')
363                     state = '0';
364                 else if (catStr.string.at(i + 8) == 'D')
365                     state = '-';
366                 else
367                     state = '+';
368                 continue;
369             }
370 #endif
371             if (insertText)
372                 cursor.insertText(catStr.string.mid(prev, i - prev));
373             else {
374                 cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, i - prev);
375                 cursor.deleteChar();//delete TAGRANGE_IMAGE_SYMBOL to insert it properly
376             }
377 
378             if (!posToTag.contains(i)) {
379                 prev = ++i;
380                 continue;
381             }
382             int tagIndex = posToTag.value(i);
383             InlineTag tag = catStr.tags.at(tagIndex);
384             QString name = tag.id;
385             QString text;
386             if (tag.type == InlineTag::mrk)
387                 text = QStringLiteral("*");
388             else if (!tag.equivText.isEmpty())
389                 text = tag.equivText; //TODO add number? when? -- right now this is done for gettext qt's 156 mark
390             else
391                 text = QString::number(sourceTagIdToIndex.contains(tag.id) ? sourceTagIdToIndex.value(tag.id) : (tagIndex + refTagIndexOffset));
392             if (tag.start != tag.end) {
393                 //qCWarning(LOKALIZE_LOG)<<"b"<<i;
394                 if (tag.start == i) {
395                     //qCWarning(LOKALIZE_LOG)<<"\t\tstart:"<<tag.getElementName()<<tag.id<<tag.start;
396                     text.append(QLatin1String(" {"));
397                     name.append(QLatin1String("-start"));
398                 } else {
399                     //qCWarning(LOKALIZE_LOG)<<"\t\tend:"<<tag.getElementName()<<tag.id<<tag.end;
400                     text.prepend(QLatin1String("} "));
401                     name.append(QLatin1String("-end"));
402                 }
403             }
404             if (cursor.document()->resource(QTextDocument::ImageResource, QUrl(name)).isNull())
405                 cursor.document()->addResource(QTextDocument::ImageResource, QUrl(name), generateImage(text, font));
406             cursor.insertImage(name);//NOTE what if twice the same name?
407             cursor.setCharFormat(chF);
408 
409             prev = ++i;
410         }
411         cursor.insertText(catStr.string.mid(prev));
412     }
413 
414 
415 
416     void TranslationUnitTextEdit::contentsChanged(int offset, int charsRemoved, int charsAdded) {
417         Q_ASSERT(m_catalog->targetLangCode().length());
418         Q_ASSERT(Project::instance()->targetLangCode().length());
419 
420         //qCWarning(LOKALIZE_LOG)<<"contentsChanged. offset"<<offset<<"charsRemoved"<<charsRemoved<<"charsAdded"<<charsAdded<<"_oldMsgstr"<<_oldMsgstr;
421 
422         //HACK to workaround #218246
423         const QString& editTextAscii = document()->toPlainText();
424         if (editTextAscii == _oldMsgstrAscii) {
425             //qCWarning(LOKALIZE_LOG)<<"stopping"<<editTextAscii<<_oldMsgstrAscii;
426             return;
427         }
428 
429 
430 
431         const QString& editText = toPlainText();
432         if (Q_UNLIKELY(m_currentPos.entry == -1 || editText == _oldMsgstr)) {
433             //qCWarning(LOKALIZE_LOG)<<"stopping"<<m_currentPos.entry<<editText<<_oldMsgstr;
434             return;
435         }
436 
437         //ktextedit spellcheck handling:
438         if (charsRemoved == 0 && editText.isEmpty() && _oldMsgstr.length())
439             charsRemoved = _oldMsgstr.length();
440         if (charsAdded && editText.isEmpty())
441             charsAdded = 0;
442         if (charsRemoved && _oldMsgstr.isEmpty())
443             charsRemoved = 0;
444 
445         DocPosition pos = m_currentPos;
446         pos.offset = offset;
447         //qCWarning(LOKALIZE_LOG)<<"offset"<<offset<<"charsRemoved"<<charsRemoved<<"_oldMsgstr"<<_oldMsgstr;
448 
449         QString target = m_catalog->targetWithTags(pos).string;
450         const QStringRef addedText = editText.midRef(offset, charsAdded);
451 
452 //BEGIN XLIFF markup handling
453         //protect from tag removal
454         //TODO use midRef when Qt 4.8 is in distros
455         bool markupRemoved = charsRemoved && target.midRef(offset, charsRemoved).contains(TAGRANGE_IMAGE_SYMBOL);
456         bool markupAdded = charsAdded && addedText.contains(TAGRANGE_IMAGE_SYMBOL);
457         if (markupRemoved || markupAdded) {
458             bool modified = false;
459             CatalogString targetWithTags = m_catalog->targetWithTags(m_currentPos);
460             //special case when the user presses Del w/o selection
461             if (!charsAdded && charsRemoved == 1) {
462                 int i = targetWithTags.tags.size();
463                 while (--i >= 0) {
464                     if (targetWithTags.tags.at(i).start == offset || targetWithTags.tags.at(i).end == offset) {
465                         modified = true;
466                         pos.offset = targetWithTags.tags.at(i).start;
467                         m_catalog->push(new DelTagCmd(m_catalog, pos));
468                     }
469                 }
470             } else if (!markupAdded) { //check if all { plus } tags were selected
471                 modified = removeTargetSubstring(offset, charsRemoved, /*refresh*/false);
472                 if (modified && charsAdded)
473                     m_catalog->push(new InsTextCmd(m_catalog, pos, addedText.toString()));
474             }
475 
476             //qCWarning(LOKALIZE_LOG)<<"calling showPos";
477             showPos(m_currentPos, CatalogString(),/*keepCursor*/true);
478             if (!modified) {
479                 //qCWarning(LOKALIZE_LOG)<<"stop";
480                 return;
481             }
482         }
483 //END XLIFF markup handling
484         else {
485             if (charsRemoved)
486                 m_catalog->push(new DelTextCmd(m_catalog, pos, _oldMsgstr.mid(offset, charsRemoved)));
487 
488             _oldMsgstr = editText; //newStr becomes OldStr
489             _oldMsgstrAscii = editTextAscii;
490             //qCWarning(LOKALIZE_LOG)<<"char"<<editText[offset].unicode();
491             if (charsAdded)
492                 m_catalog->push(new InsTextCmd(m_catalog, pos, addedText.toString()));
493 
494         }
495 
496         /* TODO
497             if (_leds)
498             {
499                 if (m_catalog->msgstr(pos).isEmpty()) _leds->ledUntr->on();
500                 else _leds->ledUntr->off();
501             }
502         */
503         requestToggleApprovement();
504         reflectUntranslatedState();
505 
506         // for mergecatalog (remove entry from index)
507         // and for statusbar
508         Q_EMIT contentsModified(m_currentPos);
509         if (charsAdded == 1) {
510             int sp = target.lastIndexOf(CompletionStorage::instance()->rxSplit, offset - 1);
511             int len = (offset - sp);
512             int wordCompletionLength = Settings::self()->wordCompletionLength();
513             if (wordCompletionLength >= 3 && len >= wordCompletionLength)
514                 doCompletion(offset + 1);
515             else if (m_completionBox)
516                 m_completionBox->hide();
517         } else if (m_completionBox)
518             m_completionBox->hide();
519         //qCWarning(LOKALIZE_LOG)<<"finish";
520         //Start LanguageToolTimer
521         if (Settings::self()->languageToolDelay() > 0) {
522             m_languageToolTimer->start(Settings::self()->languageToolDelay() * 1000);
523         }
524     }
525 
526 
527     bool TranslationUnitTextEdit::removeTargetSubstring(int delStart, int delLen, bool refresh) {
528         if (Q_UNLIKELY(m_currentPos.entry == -1))
529             return false;
530 
531         if (!::removeTargetSubstring(m_catalog, m_currentPos, delStart, delLen))
532             return false;
533 
534         requestToggleApprovement();
535 
536         if (refresh) {
537             //qCWarning(LOKALIZE_LOG)<<"calling showPos";
538             showPos(m_currentPos, CatalogString(),/*keepCursor*/true/*false*/);
539         }
540         Q_EMIT contentsModified(m_currentPos.entry);
541         return true;
542     }
543 
544     void TranslationUnitTextEdit::insertCatalogString(CatalogString catStr, int start, bool refresh) {
545         QString REMOVEME = QStringLiteral("REMOVEME");
546         CatalogString sourceForReferencing = m_catalog->sourceWithTags(m_currentPos);
547         const CatalogString         target = m_catalog->targetWithTags(m_currentPos);
548 
549 
550         QHash<QString, int> id2tagIndex;
551         int i = sourceForReferencing.tags.size();
552         while (--i >= 0)
553             id2tagIndex.insert(sourceForReferencing.tags.at(i).id, i);
554 
555         //remove markup that is already in target, to avoid duplicates if the string being inserted contains it as well
556         for (const InlineTag& tag : target.tags) {
557             if (id2tagIndex.contains(tag.id))
558                 sourceForReferencing.tags[id2tagIndex.value(tag.id)].id = REMOVEME;
559         }
560 
561         //iterating from the end is essential
562         i = sourceForReferencing.tags.size();
563         while (--i >= 0)
564             if (sourceForReferencing.tags.at(i).id == REMOVEME)
565                 sourceForReferencing.tags.removeAt(i);
566 
567 
568         adaptCatalogString(catStr, sourceForReferencing);
569 
570         ::insertCatalogString(m_catalog, m_currentPos, catStr, start);
571 
572         if (refresh) {
573             //qCWarning(LOKALIZE_LOG)<<"calling showPos";
574             showPos(m_currentPos, CatalogString(),/*keepCursor*/true/*false*/);
575             QTextCursor cursor = textCursor();
576             cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, catStr.string.size());
577             setTextCursor(cursor);
578         }
579     }
580 
581     const QString LOKALIZE_XLIFF_MIMETYPE = QStringLiteral("application/x-lokalize-xliff+xml");
582 
583     QMimeData* TranslationUnitTextEdit::createMimeDataFromSelection() const {
584         QMimeData *mimeData = new QMimeData;
585 
586         CatalogString catalogString = m_catalog->catalogString(m_currentPos);
587 
588         QTextCursor cursor = textCursor();
589         int start = qMin(cursor.anchor(), cursor.position());
590         int end = qMax(cursor.anchor(), cursor.position());
591 
592         QMap<int, int> tagPlaces;
593         if (fillTagPlaces(tagPlaces, catalogString, start, end - start)) {
594             //transform CatalogString
595             //TODO substring method
596             catalogString.string = catalogString.string.mid(start, end - start);
597 
598             QList<InlineTag>::iterator it = catalogString.tags.begin();
599             while (it != catalogString.tags.end()) {
600                 if (!tagPlaces.contains(it->start))
601                     it = catalogString.tags.erase(it);
602                 else {
603                     it->start -= start;
604                     it->end -= start;
605                     ++it;
606                 }
607             }
608 
609             QByteArray a;
610             QDataStream out(&a, QIODevice::WriteOnly);
611             QVariant v;
612             v.setValue<CatalogString>(catalogString);
613             out << v;
614             mimeData->setData(LOKALIZE_XLIFF_MIMETYPE, a);
615         }
616 
617         QString text = catalogString.string;
618         text.remove(TAGRANGE_IMAGE_SYMBOL);
619         mimeData->setText(text);
620         return mimeData;
621     }
622 
623     void TranslationUnitTextEdit::dragEnterEvent(QDragEnterEvent * event) {
624         QObject* dragSource = event->source();
625         if (dragSource->objectName().compare("qt_scrollarea_viewport") == 0)
626             dragSource = dragSource->parent();
627         //This is a deplacement within the Target area
628         if (m_part == DocPosition::Target && this == dragSource) {
629             QTextCursor cursor = textCursor();
630             int start = qMin(cursor.anchor(), cursor.position());
631             int end = qMax(cursor.anchor(), cursor.position());
632 
633             m_cursorSelectionEnd = end;
634             m_cursorSelectionStart = start;
635         }
636         QTextEdit::dragEnterEvent(event);
637     }
638     void TranslationUnitTextEdit::dropEvent(QDropEvent * event) {
639         //Ensure the cursor moves to the correct location
640         if (m_part == DocPosition::Target) {
641             setTextCursor(cursorForPosition(event->pos()));
642             //This is a copy modifier, disable the selection flags
643             if (event->keyboardModifiers() & Qt::ControlModifier) {
644                 m_cursorSelectionEnd = 0;
645                 m_cursorSelectionStart = 0;
646             }
647         }
648         QTextEdit::dropEvent(event);
649     }
650 
651     void TranslationUnitTextEdit::insertFromMimeData(const QMimeData * source) {
652         if (m_part == DocPosition::Source)
653             return;
654 
655         if (source->hasFormat(LOKALIZE_XLIFF_MIMETYPE)) {
656             //qCWarning(LOKALIZE_LOG)<<"has";
657             QVariant v;
658             QByteArray data = source->data(LOKALIZE_XLIFF_MIMETYPE);
659             QDataStream in(&data, QIODevice::ReadOnly);
660             in >> v;
661             //qCWarning(LOKALIZE_LOG)<<"ins"<<qVariantValue<CatalogString>(v).string<<qVariantValue<CatalogString>(v).ranges.size();
662 
663             int start = 0;
664             m_catalog->beginMacro(i18nc("@item Undo action item", "Insert text with markup"));
665             QTextCursor cursor = textCursor();
666             if (cursor.hasSelection()) {
667                 start = qMin(cursor.anchor(), cursor.position());
668                 int end = qMax(cursor.anchor(), cursor.position());
669                 removeTargetSubstring(start, end - start);
670                 cursor.setPosition(start);
671                 setTextCursor(cursor);
672             } else
673                 //sets right cursor position implicitly -- needed for mouse paste
674             {
675                 QMimeData mimeData;
676                 mimeData.setText(QString());
677 
678                 if (m_cursorSelectionEnd != m_cursorSelectionStart) {
679                     int oldCursorPosition = textCursor().position();
680                     removeTargetSubstring(m_cursorSelectionStart, m_cursorSelectionEnd - m_cursorSelectionStart);
681                     if (oldCursorPosition >= m_cursorSelectionEnd) {
682                         cursor.setPosition(oldCursorPosition - (m_cursorSelectionEnd - m_cursorSelectionStart));
683                         setTextCursor(cursor);
684                     }
685                 }
686                 KTextEdit::insertFromMimeData(&mimeData);
687                 start = textCursor().position();
688             }
689 
690             insertCatalogString(v.value<CatalogString>(), start);
691             m_catalog->endMacro();
692         } else {
693             QString text = source->text();
694             text.remove(TAGRANGE_IMAGE_SYMBOL);
695             insertPlainTextWithCursorCheck(text);
696         }
697     }
698 
699     static bool isMasked(const QString & str, uint col) {
700         if (col == 0 || str.isEmpty())
701             return false;
702 
703         uint counter = 0;
704         int pos = col;
705 
706         while (pos >= 0 && str.at(pos) == '\\') {
707             counter++;
708             pos--;
709         }
710 
711         return !(bool)(counter % 2);
712     }
713 
714     void TranslationUnitTextEdit::keyPressEvent(QKeyEvent * keyEvent) {
715         QString spclChars = QStringLiteral("abfnrtv'?\\");
716 
717         if (keyEvent->matches(QKeySequence::MoveToPreviousPage))
718             Q_EMIT gotoPrevRequested();
719         else if (keyEvent->matches(QKeySequence::MoveToNextPage))
720             Q_EMIT gotoNextRequested();
721         else if (keyEvent->matches(QKeySequence::Undo))
722             Q_EMIT undoRequested();
723         else if (keyEvent->matches(QKeySequence::Redo))
724             Q_EMIT redoRequested();
725         else if (keyEvent->matches(QKeySequence::Find))
726             Q_EMIT findRequested();
727         else if (keyEvent->matches(QKeySequence::FindNext))
728             Q_EMIT findNextRequested();
729         else if (keyEvent->matches(QKeySequence::Replace))
730             Q_EMIT replaceRequested();
731         else if (keyEvent->modifiers() == (Qt::AltModifier | Qt::ControlModifier)) {
732             if (keyEvent->key() == Qt::Key_Home)
733                 Q_EMIT gotoFirstRequested();
734             else if (keyEvent->key() == Qt::Key_End)
735                 Q_EMIT gotoLastRequested();
736         } else if (keyEvent->matches(QKeySequence::MoveToNextLine) || keyEvent->matches(QKeySequence::MoveToPreviousLine)) {
737             //static QTime lastUpDownPress;
738             //if (lastUpDownPress.msecsTo(QTime::currentTime())<500)
739             {
740                 keyEvent->setAccepted(true);
741                 bool up = keyEvent->key() == Qt::Key_Up;
742                 QTextCursor c = textCursor();
743                 if (!c.movePosition(up ? QTextCursor::Up : QTextCursor::Down)) {
744                     QTextCursor::MoveOperation op;
745                     if (up && !c.atStart()) op = QTextCursor::Start;
746                     else if (!up && !c.atEnd()) op = QTextCursor::End;
747                     else if (up) {
748                         Q_EMIT gotoPrevRequested();
749                         op = QTextCursor::End;
750                     } else         {
751                         Q_EMIT gotoNextRequested();
752                         op = QTextCursor::Start;
753                     }
754                     c.movePosition(op);
755                 }
756                 setTextCursor(c);
757             }
758             //lastUpDownPress=QTime::currentTime();
759         } else if (m_part == DocPosition::Source)
760             return KTextEdit::keyPressEvent(keyEvent);
761 
762         //BEGIN GENERAL
763         // ALT+123 feature TODO this is general so should be on another level
764         else if ((keyEvent->modifiers()&Qt::AltModifier)
765                  && !keyEvent->text().isEmpty()
766                  && keyEvent->text().at(0).isDigit()) {
767             QString text = keyEvent->text();
768             while (!text.isEmpty() && text.at(0).isDigit()) {
769                 m_currentUnicodeNumber = 10 * m_currentUnicodeNumber + (text.at(0).digitValue());
770                 text.remove(0, 1);
771             }
772             KTextEdit::keyPressEvent(keyEvent);
773         }
774         //END GENERAL
775 
776         else if (!keyEvent->modifiers() && (keyEvent->key() == Qt::Key_Backspace || keyEvent->key() == Qt::Key_Delete)) {
777             //only for cases when:
778             //-BkSpace was hit and cursor was atStart
779             //-Del was hit and cursor was atEnd
780             if (Q_UNLIKELY(!m_catalog->isApproved(m_currentPos.entry) && !textCursor().hasSelection())
781                 && ((textCursor().atStart() && keyEvent->key() == Qt::Key_Backspace)
782                     || (textCursor().atEnd() && keyEvent->key() == Qt::Key_Delete)))
783                 requestToggleApprovement();
784             else
785                 KTextEdit::keyPressEvent(keyEvent);
786         } else if (keyEvent->key() == Qt::Key_Space && (keyEvent->modifiers()&Qt::AltModifier))
787             insertPlainTextWithCursorCheck(QChar(0x00a0U));
788         else if (keyEvent->key() == Qt::Key_Minus && (keyEvent->modifiers()&Qt::AltModifier))
789             insertPlainTextWithCursorCheck(QChar(0x0000AD));
790 //BEGIN clever editing
791         else if (keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter) {
792             if (m_completionBox && m_completionBox->isVisible()) {
793                 if (m_completionBox->currentItem())
794                     completionActivated(m_completionBox->currentItem()->text());
795                 else
796                     qCWarning(LOKALIZE_LOG) << "avoided a crash. a case for bug 238835!";
797                 m_completionBox->hide();
798                 return;
799             }
800             if (m_catalog->type() != Gettext)
801                 return KTextEdit::keyPressEvent(keyEvent);
802 
803             QString str = toPlainText();
804             QTextCursor t = textCursor();
805             int pos = t.position();
806             QString ins;
807             if (keyEvent->modifiers()&Qt::ShiftModifier) {
808                 if (pos > 0
809                     && !str.isEmpty()
810                     && str.at(pos - 1) == QLatin1Char('\\')
811                     && !isMasked(str, pos - 1)) {
812                     ins = 'n';
813                 } else {
814                     ins = QStringLiteral("\\n");
815                 }
816             } else if (!(keyEvent->modifiers()&Qt::ControlModifier)) {
817                 if (m_langUsesSpaces
818                     && pos > 0
819                     && !str.isEmpty()
820                     && !str.at(pos - 1).isSpace()) {
821                     if (str.at(pos - 1) == QLatin1Char('\\')
822                         && !isMasked(str, pos - 1))
823                         ins = QLatin1Char('\\');
824                     // if there is no new line at the end
825                     if (pos < 2 || str.midRef(pos - 2, 2) != QLatin1String("\\n"))
826                         ins += QLatin1Char(' ');
827                 } else if (str.isEmpty()) {
828                     ins = QStringLiteral("\\n");
829                 }
830             }
831             if (!str.isEmpty()) {
832                 ins += '\n';
833                 insertPlainTextWithCursorCheck(ins);
834             } else
835                 KTextEdit::keyPressEvent(keyEvent);
836         } else if (m_catalog->type() != Gettext)
837             KTextEdit::keyPressEvent(keyEvent);
838         else if ((keyEvent->modifiers()&Qt::ControlModifier) ?
839                  (keyEvent->key() == Qt::Key_D) :
840                  (keyEvent->key() == Qt::Key_Delete)
841                  && textCursor().atEnd()) {
842             qCWarning(LOKALIZE_LOG) << "workaround for Qt/X11 bug";
843             QTextCursor t = textCursor();
844             if (!t.hasSelection()) {
845                 int pos = t.position();
846                 QString str = toPlainText();
847                 //workaround for Qt/X11 bug: if Del on NumPad is pressed, then pos is beyond end
848                 if (pos == str.size()) --pos;
849                 if (!str.isEmpty()
850                     && str.at(pos) == '\\'
851                     && !isMasked(str, pos)
852                     && pos < str.length() - 1
853                     && spclChars.contains(str.at(pos + 1))) {
854                     t.deleteChar();
855                 }
856             }
857 
858             t.deleteChar();
859             setTextCursor(t);
860         } else if ((!keyEvent->modifiers() && keyEvent->key() == Qt::Key_Backspace)
861                    || ((keyEvent->modifiers() & Qt::ControlModifier) && keyEvent->key() == Qt::Key_H)) {
862             QTextCursor t = textCursor();
863             if (!t.hasSelection()) {
864                 int pos = t.position();
865                 QString str = toPlainText();
866                 if (!str.isEmpty() && pos > 0 && spclChars.contains(str.at(pos - 1))) {
867                     if (pos > 1 && str.at(pos - 2) == QLatin1Char('\\') && !isMasked(str, pos - 2)) {
868                         t.deletePreviousChar();
869                         t.deletePreviousChar();
870                         setTextCursor(t);
871                         //qCWarning(LOKALIZE_LOG)<<"set-->"<<textCursor().anchor()<<textCursor().position();
872                     }
873                 }
874 
875             }
876             KTextEdit::keyPressEvent(keyEvent);
877         } else if (keyEvent->key() == Qt::Key_Tab)
878             insertPlainTextWithCursorCheck(QStringLiteral("\\t"));
879         else
880             KTextEdit::keyPressEvent(keyEvent);
881 //END clever editing
882     }
883 
884     void TranslationUnitTextEdit::keyReleaseEvent(QKeyEvent * e) {
885         if ((e->key() == Qt::Key_Alt) && m_currentUnicodeNumber >= 32) {
886             insertPlainTextWithCursorCheck(QChar(m_currentUnicodeNumber));
887             m_currentUnicodeNumber = 0;
888         } else
889             KTextEdit::keyReleaseEvent(e);
890     }
891 
892     void TranslationUnitTextEdit::insertPlainTextWithCursorCheck(const QString & text) {
893         insertPlainText(text);
894         KTextEdit::ensureCursorVisible();
895     }
896 
897     QString TranslationUnitTextEdit::toPlainText() {
898         QTextCursor cursor = textCursor();
899         cursor.select(QTextCursor::Document);
900         QString text = cursor.selectedText();
901         text.replace(QChar(8233), '\n');
902         /*
903             int ii=text.size();
904             while(--ii>=0)
905                 qCWarning(LOKALIZE_LOG)<<text.at(ii).unicode();
906         */
907         return text;
908     }
909 
910     void TranslationUnitTextEdit::emitCursorPositionChanged() {
911         Q_EMIT cursorPositionChanged(textCursor().columnNumber());
912     }
913 
914     void TranslationUnitTextEdit::insertTag(InlineTag tag) {
915         QTextCursor cursor = textCursor();
916         tag.start = qMin(cursor.anchor(), cursor.position());
917         tag.end = qMax(cursor.anchor(), cursor.position()) + tag.isPaired();
918         qCDebug(LOKALIZE_LOG) << "insert tag" << (m_part == DocPosition::Source) << tag.start << tag.end;
919         m_catalog->push(new InsTagCmd(m_catalog, currentPos(), tag));
920         showPos(currentPos(), CatalogString(),/*keepCursor*/true);
921         cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, tag.end + 1 + tag.isPaired());
922         setFocus();
923     }
924 
925     int TranslationUnitTextEdit::strForMicePosIfUnderTag(QPoint mice, CatalogString & str, bool tryHarder) {
926         if (m_currentPos.entry == -1) return -1;
927         QTextCursor cursor = cursorForPosition(mice);
928         int pos = cursor.position();
929         str = m_catalog->catalogString(m_currentPos);
930         if (pos == -1 || pos >= str.string.size()) return -1;
931         //qCWarning(LOKALIZE_LOG)<<"here1"<<str.string.at(pos)<<str.string.at(pos-1)<<str.string.at(pos+1);
932 
933 
934 //     if (pos>0)
935 //     {
936 //         cursor.movePosition(QTextCursor::Left);
937 //         mice.setX(mice.x()+cursorRect(cursor).width()/2);
938 //         pos=cursorForPosition(mice).position();
939 //     }
940 
941         if (str.string.at(pos) != TAGRANGE_IMAGE_SYMBOL) {
942             bool cont = tryHarder && --pos >= 0 && str.string.at(pos) == TAGRANGE_IMAGE_SYMBOL;
943             if (!cont)
944                 return -1;
945         }
946 
947         int result = str.tags.size();
948         while (--result >= 0 && str.tags.at(result).start != pos && str.tags.at(result).end != pos)
949             ;
950         return result;
951     }
952 
953     void TranslationUnitTextEdit::mouseReleaseEvent(QMouseEvent * event) {
954         if (event->button() == Qt::LeftButton) {
955             CatalogString str;
956             int pos = strForMicePosIfUnderTag(event->pos(), str);
957             if (pos != -1 && m_part == DocPosition::Source) {
958                 Q_EMIT tagInsertRequested(str.tags.at(pos));
959                 event->accept();
960                 return;
961             }
962         }
963         KTextEdit::mouseReleaseEvent(event);
964     }
965 
966 
967     void TranslationUnitTextEdit::contextMenuEvent(QContextMenuEvent * event) {
968         CatalogString str;
969         int pos = strForMicePosIfUnderTag(event->pos(), str);
970         if (pos != -1) {
971             QString xid = str.tags.at(pos).xid;
972 
973             if (!xid.isEmpty()) {
974                 QMenu menu;
975                 int entry = m_catalog->unitById(xid);
976                 /* QAction* findUnit=menu.addAction(entry>=m_catalog->numberOfEntries()?
977                                 i18nc("@action:inmenu","Show the binary unit"):
978                                 i18nc("@action:inmenu","Go to the referenced entry")); */
979 
980                 QAction* result = menu.exec(event->globalPos());
981                 if (result) {
982                     if (entry >= m_catalog->numberOfEntries())
983                         Q_EMIT binaryUnitSelectRequested(xid);
984                     else
985                         Q_EMIT gotoEntryRequested(DocPosition(entry));
986                     event->accept();
987                 }
988                 return;
989             }
990         }
991         if (textCursor().hasSelection() && m_part == DocPosition::Target) {
992             QMenu menu;
993             menu.addAction(i18nc("@action:inmenu", "Lookup selected text in translation memory"));
994             if (menu.exec(event->globalPos()))
995                 Q_EMIT tmLookupRequested(m_part, textCursor().selectedText());
996             return;
997         }
998 
999         if (m_part != DocPosition::Source && m_part != DocPosition::Target)
1000             return;
1001 
1002         KTextEdit::contextMenuEvent(event);
1003 
1004 #if 0
1005         QTextCursor wordSelectCursor = cursorForPosition(event->pos());
1006         wordSelectCursor.select(QTextCursor::WordUnderCursor);
1007         if (m_highlighter->isWordMisspelled(wordSelectCursor.selectedText())) {
1008             QMenu menu;
1009             QMenu suggestions;
1010             foreach (const QString& s, m_highlighter->suggestionsForWord(wordSelectCursor.selectedText()))
1011                 suggestions.addAction(s);
1012             if (!suggestions.isEmpty()) {
1013                 QAction* answer = suggestions.exec(event->globalPos());
1014                 if (answer) {
1015                     m_catalog->beginMacro(i18nc("@item Undo action item", "Replace text"));
1016                     wordSelectCursor.insertText(answer->text());
1017                     m_catalog->endMacro();
1018                 }
1019             }
1020         }
1021 #endif
1022 
1023 //     QMenu menu;
1024 //     QAction* spellchecking=menu.addAction();
1025 //     event->accept();
1026     }
1027     void TranslationUnitTextEdit::zoomRequestedSlot(qreal fontSize) {
1028         QFont curFont = font();
1029         curFont.setPointSizeF(fontSize);
1030         setFont(curFont);
1031     }
1032 
1033     void TranslationUnitTextEdit::wheelEvent(QWheelEvent * event) {
1034         //Override default KTextEdit behavior which ignores Ctrl+wheelEvent when the field is not ReadOnly (i/o zooming)
1035         if (m_part == DocPosition::Target && !Settings::mouseWheelGo() && (event->modifiers() == Qt::ControlModifier)) {
1036             float delta = event->angleDelta().y() / 120.f;
1037             zoomInF(delta);
1038             //Also zoom in the source
1039             Q_EMIT zoomRequested(font().pointSizeF());
1040             return;
1041         }
1042 
1043         if (m_part == DocPosition::Source || !Settings::mouseWheelGo()) {
1044             if (event->modifiers() == Qt::ControlModifier) {
1045                 float delta = event->angleDelta().y() / 120.f;
1046                 zoomInF(delta);
1047                 //Also zoom in the target
1048                 Q_EMIT zoomRequested(font().pointSizeF());
1049                 return;
1050             }
1051             return KTextEdit::wheelEvent(event);
1052         }
1053 
1054         switch (event->modifiers()) {
1055         case Qt::ControlModifier:
1056             if (event->angleDelta().y() > 0)
1057                 Q_EMIT gotoPrevFuzzyRequested();
1058             else
1059                 Q_EMIT gotoNextFuzzyRequested();
1060             break;
1061         case Qt::AltModifier:
1062             if (event->angleDelta().y() > 0)
1063                 Q_EMIT gotoPrevUntranslatedRequested();
1064             else
1065                 Q_EMIT gotoNextUntranslatedRequested();
1066             break;
1067         case Qt::ControlModifier + Qt::ShiftModifier:
1068             if (event->angleDelta().y() > 0)
1069                 Q_EMIT gotoPrevFuzzyUntrRequested();
1070             else
1071                 Q_EMIT gotoNextFuzzyUntrRequested();
1072             break;
1073         case Qt::ShiftModifier:
1074             return KTextEdit::wheelEvent(event);
1075         default:
1076             if (event->angleDelta().y() > 0)
1077                 Q_EMIT gotoPrevRequested();
1078             else
1079                 Q_EMIT gotoNextRequested();
1080         }
1081     }
1082 
1083     void TranslationUnitTextEdit::spellReplace() {
1084         QTextCursor wordSelectCursor = textCursor();
1085         wordSelectCursor.select(QTextCursor::WordUnderCursor);
1086         if (!m_highlighter->isWordMisspelled(wordSelectCursor.selectedText()))
1087             return;
1088 
1089         const QStringList& suggestions = m_highlighter->suggestionsForWord(wordSelectCursor.selectedText());
1090         if (suggestions.isEmpty())
1091             return;
1092 
1093         m_catalog->beginMacro(i18nc("@item Undo action item", "Replace text"));
1094         wordSelectCursor.insertText(suggestions.first());
1095         m_catalog->endMacro();
1096     }
1097 
1098     bool TranslationUnitTextEdit::event(QEvent * event) {
1099 #ifdef Q_OS_MAC
1100         if (event->type() == QEvent::InputMethod) {
1101             QInputMethodEvent* e = static_cast<QInputMethodEvent*>(event);
1102             insertPlainTextWithCursorCheck(e->commitString());
1103             e->accept();
1104             return true;
1105         }
1106 #endif
1107         if (event->type() == QEvent::ToolTip) {
1108             QHelpEvent *helpEvent = static_cast<QHelpEvent *>(event);
1109             CatalogString str;
1110             int pos = strForMicePosIfUnderTag(helpEvent->pos(), str, true);
1111             if (pos != -1) {
1112                 QString tooltip = str.tags.at(pos).displayName();
1113                 QToolTip::showText(helpEvent->globalPos(), tooltip);
1114                 return true;
1115             }
1116 
1117             QString tip;
1118 
1119             QString langCode = m_highlighter->currentLanguage();
1120 
1121             //qCWarning(LOKALIZE_LOG) << "Spellchecker found "<<m_highlighter->spellCheckerFound()<< " as "<<m_highlighter->currentLanguage();
1122             bool nospell = langCode.isEmpty();
1123             if (nospell)
1124                 langCode = m_part == DocPosition::Source ? m_catalog->sourceLangCode() : m_catalog->targetLangCode();
1125             QLocale l(langCode);
1126             if (l.language() != QLocale::C) tip = l.nativeLanguageName() + QLatin1String(" (");
1127             tip += langCode;
1128             if (l.language() != QLocale::C) tip += ')';
1129             if (nospell)
1130                 tip += QLatin1String(" - ") + i18n("no spellcheck available");
1131             QToolTip::showText(helpEvent->globalPos(), tip);
1132         }
1133         return KTextEdit::event(event);
1134     }
1135 
1136     void TranslationUnitTextEdit::slotLanguageToolFinished(const QString & result) {
1137         LanguageToolParser parser;
1138         const QJsonDocument doc = QJsonDocument::fromJson(result.toUtf8());
1139         const QJsonObject fields = doc.object();
1140         Q_EMIT languageToolChanged(parser.parseResult(fields, toPlainText()));
1141     }
1142 
1143     void TranslationUnitTextEdit::slotLanguageToolError(const QString & str) {
1144         Q_EMIT languageToolChanged(i18n("An error was reported: %1", str));
1145     }
1146 
1147     void TranslationUnitTextEdit::launchLanguageTool()     {
1148         if (toPlainText().length() == 0)
1149             return;
1150 
1151         LanguageToolResultJob *job = new LanguageToolResultJob(this);
1152         job->setUrl(LanguageToolManager::self()->languageToolCheckPath());
1153         job->setNetworkAccessManager(LanguageToolManager::self()->networkAccessManager());
1154         job->setText(toPlainText().toHtmlEscaped().replace(QStringLiteral("%"), QStringLiteral("%25")));
1155         job->setLanguage(m_catalog->targetLangCode());
1156         connect(job, &LanguageToolResultJob::finished, this, &TranslationUnitTextEdit::slotLanguageToolFinished);
1157         connect(job, &LanguageToolResultJob::error, this, &TranslationUnitTextEdit::slotLanguageToolError);
1158         job->start();
1159     }
1160     void TranslationUnitTextEdit::tagMenu()     {
1161         doTag(false);
1162     }
1163     void TranslationUnitTextEdit::tagImmediate() {
1164         doTag(true);
1165     }
1166 
1167     void TranslationUnitTextEdit::doTag(bool immediate) {
1168         QMenu menu;
1169         QAction* txt = nullptr;
1170 
1171         CatalogString sourceWithTags = m_catalog->sourceWithTags(m_currentPos);
1172         int count = sourceWithTags.tags.size();
1173         if (count) {
1174             QMap<QString, int> tagIdToIndex = m_catalog->targetWithTags(m_currentPos).tagIdToIndex();
1175             bool hasActive = false;
1176             for (int i = 0; i < count; ++i) {
1177                 //txt=menu.addAction(sourceWithTags.ranges.at(i));
1178                 txt = menu.addAction(QString::number(i)/*+" "+sourceWithTags.ranges.at(i).id*/);
1179                 txt->setData(QVariant(i));
1180                 if (!hasActive && !tagIdToIndex.contains(sourceWithTags.tags.at(i).id)) {
1181                     if (immediate) {
1182                         insertTag(sourceWithTags.tags.at(txt->data().toInt()));
1183                         return;
1184                     }
1185                     hasActive = true;
1186                     menu.setActiveAction(txt);
1187                 }
1188             }
1189             if (immediate) return;
1190             txt = menu.exec(mapToGlobal(cursorRect().bottomRight()));
1191             if (!txt) return;
1192             insertTag(sourceWithTags.tags.at(txt->data().toInt()));
1193         } else {
1194             if (Q_UNLIKELY(Project::instance()->markup().isEmpty()))
1195                 return;
1196 
1197             //QRegExp tag("(<[^>]*>)+|\\&\\w+\\;");
1198             QRegExp tag(Project::instance()->markup());
1199             tag.setMinimal(true);
1200             QString en = m_catalog->sourceWithTags(m_currentPos).string;
1201             QString target(toPlainText());
1202             en.remove('\n');
1203             target.remove('\n');
1204             int pos = 0;
1205             //tag.indexIn(en);
1206             int posInMsgStr = 0;
1207             while ((pos = tag.indexIn(en, pos)) != -1) {
1208                 /*        QString str(tag.cap(0));
1209                         str.replace("&","&&");*/
1210                 txt = menu.addAction(tag.cap(0));
1211                 pos += tag.matchedLength();
1212 
1213                 if (posInMsgStr != -1 && (posInMsgStr = target.indexOf(tag.cap(0), posInMsgStr)) == -1) {
1214                     if (immediate) {
1215                         insertPlainTextWithCursorCheck(txt->text());
1216                         return;
1217                     }
1218                     menu.setActiveAction(txt);
1219                 } else if (posInMsgStr != -1)
1220                     posInMsgStr += tag.matchedLength();
1221             }
1222             if (!txt || immediate)
1223                 return;
1224 
1225             //txt=menu.exec(_msgidEdit->mapToGlobal(QPoint(0,0)));
1226             txt = menu.exec(mapToGlobal(cursorRect().bottomRight()));
1227             if (txt)
1228                 insertPlainTextWithCursorCheck(txt->text());
1229         }
1230     }
1231 
1232 
1233     void TranslationUnitTextEdit::source2target() {
1234         CatalogString sourceWithTags = m_catalog->sourceWithTags(m_currentPos);
1235         QString text = sourceWithTags.string;
1236         QString out;
1237         QString ctxt = m_catalog->context(m_currentPos.entry).first();
1238         QRegExp delimiter(QStringLiteral("\\s*,\\s*"));
1239 
1240         //TODO ask for the fillment if the first time.
1241         //BEGIN KDE specific part
1242         if (ctxt.startsWith(QLatin1String("NAME OF TRANSLATORS")) || text.startsWith(QLatin1String("_: NAME OF TRANSLATORS\\n"))) {
1243             if (!document()->toPlainText().split(delimiter).contains(Settings::authorLocalizedName())) {
1244                 if (!document()->isEmpty())
1245                     out = QLatin1String(", ");
1246                 out += Settings::authorLocalizedName();
1247             }
1248         } else if (ctxt.startsWith(QLatin1String("EMAIL OF TRANSLATORS")) || text.startsWith(QLatin1String("_: EMAIL OF TRANSLATORS\\n"))) {
1249             if (!document()->toPlainText().split(delimiter).contains(Settings::authorEmail())) {
1250                 if (!document()->isEmpty())
1251                     out = QLatin1String(", ");
1252                 out += Settings::authorEmail();
1253             }
1254         } else if (/*_catalog->isGeneratedFromDocbook() &&*/ text.startsWith(QLatin1String("ROLES_OF_TRANSLATORS"))) {
1255             if (!document()->isEmpty())
1256                 out = '\n';
1257             out += QLatin1String("<othercredit role=\\\"translator\\\">\n"
1258                                  "<firstname></firstname><surname></surname>\n"
1259                                  "<affiliation><address><email>") + Settings::authorEmail() + QLatin1String("</email></address>\n"
1260                                          "</affiliation><contrib></contrib></othercredit>");
1261         } else if (text.startsWith(QLatin1String("CREDIT_FOR_TRANSLATORS"))) {
1262             if (!document()->isEmpty())
1263                 out = '\n';
1264             out += QLatin1String("<para>") + Settings::authorLocalizedName() + '\n' +
1265                    QLatin1String("<email>") + Settings::authorEmail() + QLatin1String("</email></para>");
1266         }
1267         //END KDE specific part
1268 
1269         else {
1270             m_catalog->beginMacro(i18nc("@item Undo action item", "Copy source to target"));
1271             removeTargetSubstring(0, -1,/*refresh*/false);
1272             insertCatalogString(sourceWithTags, 0,/*refresh*/false);
1273             m_catalog->endMacro();
1274 
1275             showPos(m_currentPos, sourceWithTags,/*keepCursor*/false);
1276 
1277             requestToggleApprovement();
1278         }
1279         if (!out.isEmpty()) {
1280             QTextCursor t = textCursor();
1281             t.movePosition(QTextCursor::End);
1282             t.insertText(out);
1283             setTextCursor(t);
1284         }
1285     }
1286 
1287     void TranslationUnitTextEdit::requestToggleApprovement() {
1288         if (m_catalog->isApproved(m_currentPos.entry) || !Settings::autoApprove())
1289             return;
1290 
1291         bool skip = m_catalog->isPlural(m_currentPos);
1292         if (skip) {
1293             skip = false;
1294             DocPos pos(m_currentPos);
1295             for (pos.form = 0; pos.form < m_catalog->numberOfPluralForms(); ++(pos.form))
1296                 skip = skip || !m_catalog->isModified(pos);
1297         }
1298         if (!skip)
1299             Q_EMIT toggleApprovementRequested();
1300     }
1301 
1302 
1303     void TranslationUnitTextEdit::cursorToStart() {
1304         QTextCursor t = textCursor();
1305         t.movePosition(QTextCursor::Start);
1306         setTextCursor(t);
1307     }
1308 
1309 
1310     void TranslationUnitTextEdit::doCompletion(int pos) {
1311         QString target = m_catalog->targetWithTags(m_currentPos).string;
1312         int sp = target.lastIndexOf(CompletionStorage::instance()->rxSplit, pos - 1);
1313         int len = (pos - sp) - 1;
1314 
1315         QStringList s = CompletionStorage::instance()->makeCompletion(QString::fromRawData(target.unicode() + sp + 1, len));
1316 
1317         if (!m_completionBox) {
1318 //BEGIN creation
1319             m_completionBox = new MyCompletionBox(this);
1320             connect(m_completionBox, &MyCompletionBox::activated, this, &TranslationUnitTextEdit::completionActivated);
1321             m_completionBox->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred);
1322 //END creation
1323         }
1324         m_completionBox->setItems(s);
1325         if (s.size() && !s.first().isEmpty()) {
1326             m_completionBox->setCurrentRow(0);
1327             //qApp->removeEventFilter( m_completionBox );
1328             if (!m_completionBox->isVisible()) //NOTE remove the check if kdelibs gets adapted
1329                 m_completionBox->show();
1330             m_completionBox->resize(m_completionBox->sizeHint());
1331             QPoint p = cursorRect().bottomRight();
1332             if (p.x() < 10) //workaround Qt bug
1333                 p.rx() += textCursor().verticalMovementX() + QFontMetrics(currentFont()).horizontalAdvance('W');
1334             m_completionBox->move(viewport()->mapToGlobal(p));
1335         } else
1336             m_completionBox->hide();
1337     }
1338 
1339     void TranslationUnitTextEdit::doExplicitCompletion() {
1340         doCompletion(textCursor().anchor());
1341     }
1342 
1343     void TranslationUnitTextEdit::completionActivated(const QString & semiWord) {
1344         QTextCursor cursor = textCursor();
1345         cursor.insertText(semiWord);
1346         setTextCursor(cursor);
1347     }
1348 
1349