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 "msgctxtview.h"
11 
12 #include "noteeditor.h"
13 #include "catalog.h"
14 #include "cmd.h"
15 #include "prefs_lokalize.h"
16 #include "project.h"
17 
18 #include "lokalize_debug.h"
19 
20 #include <klocalizedstring.h>
21 #include <ktextedit.h>
22 #include <kcombobox.h>
23 
24 #include <QTime>
25 #include <QTimer>
26 #include <QBoxLayout>
27 #include <QStackedLayout>
28 #include <QLabel>
29 #include <QStringListModel>
30 #include <QLineEdit>
31 #include <QTextBrowser>
32 #include <QStringBuilder>
33 #include <QDesktopServices>
34 #include <QRegularExpression>
35 
MsgCtxtView(QWidget * parent,Catalog * catalog)36 MsgCtxtView::MsgCtxtView(QWidget* parent, Catalog* catalog)
37     : QDockWidget(i18nc("@title toolview name", "Unit metadata"), parent)
38     , m_browser(new QTextBrowser(this))
39     , m_editor(nullptr)
40     , m_catalog(catalog)
41     , m_selection(0)
42     , m_offset(0)
43     , m_hasInfo(false)
44     , m_hasErrorNotes(false)
45     , m_pologyProcessInProgress(0)
46     , m_pologyStartedReceivingOutput(false)
47 {
48     setObjectName(QStringLiteral("msgCtxtView"));
49     QWidget* main = new QWidget(this);
50     setWidget(main);
51     m_stackedLayout = new QStackedLayout(main);
52     m_stackedLayout->addWidget(m_browser);
53 
54     m_browser->viewport()->setBackgroundRole(QPalette::Window);
55     m_browser->setOpenLinks(false);
56     connect(m_browser, &QTextBrowser::anchorClicked, this, &MsgCtxtView::anchorClicked);
57 }
58 
~MsgCtxtView()59 MsgCtxtView::~MsgCtxtView()
60 {
61 }
62 
63 const QString MsgCtxtView::BR = "<br />";
64 
cleanup()65 void MsgCtxtView::cleanup()
66 {
67     m_unfinishedNotes.clear();
68     m_tempNotes.clear();
69     m_pologyNotes.clear();
70     m_languageToolNotes.clear();
71 }
72 
gotoEntry(const DocPosition & pos,int selection)73 void MsgCtxtView::gotoEntry(const DocPosition& pos, int selection)
74 {
75     m_entry = DocPos(pos);
76     m_selection = selection;
77     m_offset = pos.offset;
78     QTimer::singleShot(0, this, &MsgCtxtView::process);
79     QTimer::singleShot(0, this, &MsgCtxtView::pology);
80 }
81 
process()82 void MsgCtxtView::process()
83 {
84     if (m_catalog->numberOfEntries() <= m_entry.entry)
85         return;//because of Qt::QueuedConnection
86 
87     if (m_stackedLayout->currentIndex())
88         m_unfinishedNotes[m_prevEntry] = qMakePair(m_editor->note(), m_editor->noteIndex());
89 
90 
91     if (m_unfinishedNotes.contains(m_entry)) {
92         addNoteUI();
93         m_editor->setNote(m_unfinishedNotes.value(m_entry).first, m_unfinishedNotes.value(m_entry).second);
94     } else
95         m_stackedLayout->setCurrentIndex(0);
96 
97 
98     m_prevEntry = m_entry;
99     m_browser->clear();
100 
101     if (m_tempNotes.contains(m_entry.entry)) {
102         QString html = i18nc("@info notes to translation unit which expire when the catalog is closed", "<b>Temporary notes:</b>");
103         html += MsgCtxtView::BR;
104         const auto tempNotes = m_tempNotes.values(m_entry.entry);
105         for (const QString& note : tempNotes)
106             html += note.toHtmlEscaped() + MsgCtxtView::BR;
107         html += MsgCtxtView::BR;
108         m_browser->insertHtml(html.replace('\n', MsgCtxtView::BR));
109     }
110     if (m_pologyNotes.contains(m_entry.entry)) {
111         QString html = i18nc("@info notes generated by the pology check", "<b>Pology notes:</b>");
112         html += MsgCtxtView::BR;
113         const auto pologyNotes = m_pologyNotes.values(m_entry.entry);
114         for (const QString& note : pologyNotes)
115             html += note.toHtmlEscaped() + MsgCtxtView::BR;
116         html += MsgCtxtView::BR;
117         m_browser->insertHtml(html.replace('\n', MsgCtxtView::BR));
118     }
119     if (m_languageToolNotes.contains(m_entry.entry)) {
120         QString html = i18nc("@info notes generated by the languagetool check", "<b>LanguageTool notes:</b>");
121         html += MsgCtxtView::BR;
122         const auto languageToolNotes = m_languageToolNotes.values(m_entry.entry);
123         for (const QString& note : languageToolNotes)
124             html += note.toHtmlEscaped() + MsgCtxtView::BR;
125         html += MsgCtxtView::BR;
126         m_browser->insertHtml(html.replace('\n', MsgCtxtView::BR));
127     }
128 
129     QString phaseName = m_catalog->phase(m_entry.toDocPosition());
130     if (!phaseName.isEmpty()) {
131         Phase phase = m_catalog->phase(phaseName);
132         QString html = i18nc("@info translation unit metadata", "<b>Phase:</b><br>");
133         if (phase.date.isValid())
134             html += QString(QStringLiteral("%1: ")).arg(phase.date.toString(Qt::ISODate));
135         html += phase.process.toHtmlEscaped();
136         if (!phase.contact.isEmpty())
137             html += QString(QStringLiteral(" (%1)")).arg(phase.contact.toHtmlEscaped());
138         m_browser->insertHtml(html + MsgCtxtView::BR);
139     }
140 
141     const QVector<Note> notes = m_catalog->notes(m_entry.toDocPosition());
142     m_hasErrorNotes = false;
143     for (const Note& note : notes)
144         m_hasErrorNotes = m_hasErrorNotes || note.content.contains(QLatin1String("[ERROR]"));
145 
146     int realOffset = displayNotes(m_browser, m_catalog->notes(m_entry.toDocPosition()), m_entry.form, m_catalog->capabilities()&MultipleNotes);
147 
148     QString html;
149     const auto developerNotes = m_catalog->developerNotes(m_entry.toDocPosition());
150     for (const Note& note : developerNotes) {
151         html += MsgCtxtView::BR + escapeWithLinks(note.content).replace('\n', BR);
152     }
153 
154     const QStringList sourceFiles = m_catalog->sourceFiles(m_entry.toDocPosition());
155     if (!sourceFiles.isEmpty()) {
156         html += i18nc("@info PO comment parsing", "<br><b>Files:</b><br>");
157         for (const QString &sourceFile : sourceFiles)
158             html += QString(QStringLiteral("<a href=\"src:/%1\">%2</a><br />")).arg(sourceFile, sourceFile);
159         html.chop(6);
160     }
161 
162     QString msgctxt = m_catalog->context(m_entry.entry).first();
163     if (!msgctxt.isEmpty())
164         html += i18nc("@info PO comment parsing", "<br><b>Context:</b><br>") + msgctxt.toHtmlEscaped();
165 
166     QTextCursor t = m_browser->textCursor();
167     t.movePosition(QTextCursor::End);
168     m_browser->setTextCursor(t);
169     m_browser->insertHtml(html);
170 
171     t.movePosition(QTextCursor::Start);
172     t.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, realOffset + m_offset);
173     t.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, m_selection);
174     m_browser->setTextCursor(t);
175 }
languageTool(const QString & text)176 void MsgCtxtView::languageTool(const QString &text)
177 {
178     m_languageToolNotes.insert(m_entry.entry, text);
179     m_prevEntry.entry = -1;
180     process();
181 }
pology()182 void MsgCtxtView::pology()
183 {
184     if (Settings::self()->pologyEnabled() && m_pologyProcessInProgress == 0 && QFile::exists(m_catalog->url())) {
185         QString command = Settings::self()->pologyCommandEntry();
186         command = command.replace(QStringLiteral("%u"), QString::number(m_entry.entry + 1)).replace(QStringLiteral("%f"),  QStringLiteral("\"") + m_catalog->url() + QStringLiteral("\"")).replace(QStringLiteral("\n"), QStringLiteral(" "));
187         m_pologyProcess = new KProcess;
188         m_pologyProcess->setShellCommand(command);
189         m_pologyProcess->setOutputChannelMode(KProcess::SeparateChannels);
190         m_pologyStartedReceivingOutput = false;
191         connect(m_pologyProcess, &KProcess::readyReadStandardOutput,
192                 this, &MsgCtxtView::pologyReceivedStandardOutput);
193         connect(m_pologyProcess, &KProcess::readyReadStandardError,
194                 this, &MsgCtxtView::pologyReceivedStandardError);
195         connect(m_pologyProcess, QOverload<int, QProcess::ExitStatus>::of(&KProcess::finished),
196                 this, &MsgCtxtView::pologyHasFinished);
197         m_pologyData = QStringLiteral("");
198         m_pologyProcessInProgress = m_entry.entry + 1;
199         m_pologyProcess->start();
200     } else if (Settings::self()->pologyEnabled() && m_pologyProcessInProgress > 0) {
201         QTimer::singleShot(1000, this, &MsgCtxtView::pology);
202     }
203 }
pologyReceivedStandardOutput()204 void MsgCtxtView::pologyReceivedStandardOutput()
205 {
206     if (m_pologyProcessInProgress == m_entry.entry + 1) {
207         if (!m_pologyStartedReceivingOutput) {
208             m_pologyStartedReceivingOutput = true;
209         }
210         const QString grossPologyOutput = m_pologyProcess->readAllStandardOutput();
211         const QStringList pologyTmpLines = grossPologyOutput.split('\n', Qt::SkipEmptyParts);
212         for (const QString &pologyTmp : pologyTmpLines) {
213             if (pologyTmp.startsWith(QStringLiteral("[note]")))
214                 m_pologyData += pologyTmp;
215         }
216     }
217 }
218 
219 
pologyReceivedStandardError()220 void MsgCtxtView::pologyReceivedStandardError()
221 {
222     if (m_pologyProcessInProgress == m_entry.entry + 1) {
223         if (!m_pologyStartedReceivingOutput) {
224             m_pologyStartedReceivingOutput = true;
225         }
226         m_pologyData += m_pologyProcess->readAllStandardError().replace('\n', MsgCtxtView::BR.toLatin1());
227     }
228 }
pologyHasFinished()229 void MsgCtxtView::pologyHasFinished()
230 {
231     if (m_pologyProcessInProgress == m_entry.entry + 1) {
232         if (!m_pologyStartedReceivingOutput) {
233             m_pologyStartedReceivingOutput = true;
234             const QString grossPologyOutput = m_pologyProcess->readAllStandardOutput();
235             const QStringList pologyTmpLines = grossPologyOutput.split('\n', Qt::SkipEmptyParts);
236             if (pologyTmpLines.count() == 0) {
237                 m_pologyData += i18nc("@info The pology command didn't return anything", "(empty)");
238             } else {
239                 for (const QString &pologyTmp : pologyTmpLines) {
240                     if (pologyTmp.startsWith(QStringLiteral("[note]")))
241                         m_pologyData += pologyTmp;
242                 }
243             }
244         }
245         m_pologyNotes.insert(m_entry.entry, m_pologyData);
246         m_prevEntry.entry = -1;
247         process();
248     }
249     m_pologyProcess->deleteLater();
250     m_pologyProcessInProgress = 0;
251 }
252 
addNoteUI()253 void MsgCtxtView::addNoteUI()
254 {
255     anchorClicked(QUrl(QStringLiteral("note:/add")));
256 }
257 
anchorClicked(const QUrl & link)258 void MsgCtxtView::anchorClicked(const QUrl& link)
259 {
260     QString path = link.path().mid(1); // minus '/'
261 
262     if (link.scheme() == QLatin1String("note")) {
263         int capabilities = m_catalog->capabilities();
264         if (!m_editor) {
265             m_editor = new NoteEditor(this);
266             m_stackedLayout->addWidget(m_editor);
267             connect(m_editor, &NoteEditor::accepted, this, &MsgCtxtView::noteEditAccepted);
268             connect(m_editor, &NoteEditor::rejected, this, &MsgCtxtView::noteEditRejected);
269         }
270         m_editor->setNoteAuthors(m_catalog->noteAuthors());
271         QVector<Note> notes = m_catalog->notes(m_entry.toDocPosition());
272         int noteIndex = -1; //means add new note
273         Note note;
274         if (!path.endsWith(QLatin1String("add"))) {
275             noteIndex = path.toInt();
276             note = notes.at(noteIndex);
277         } else if (!(capabilities & MultipleNotes) && notes.size()) {
278             noteIndex = 0; //so we don't overwrite the only possible note
279             note = notes.first();
280         }
281         m_editor->setNote(note, noteIndex);
282         m_editor->setFromFieldVisible(capabilities & KeepsNoteAuthors);
283         m_stackedLayout->setCurrentIndex(1);
284     } else if (link.scheme() == QLatin1String("src")) {
285         int pos = path.lastIndexOf(':');
286         Q_EMIT srcFileOpenRequested(path.left(pos), path.midRef(pos + 1).toInt());
287     } else if (link.scheme().contains(QLatin1String("tp")))
288         QDesktopServices::openUrl(link);
289 }
290 
noteEditAccepted()291 void MsgCtxtView::noteEditAccepted()
292 {
293     DocPosition pos = m_entry.toDocPosition();
294     pos.form = m_editor->noteIndex();
295     m_catalog->push(new SetNoteCmd(m_catalog, pos, m_editor->note()));
296 
297     m_prevEntry.entry = -1; process();
298     //m_stackedLayout->setCurrentIndex(0);
299     //m_unfinishedNotes.remove(m_entry);
300     noteEditRejected();
301 }
noteEditRejected()302 void MsgCtxtView::noteEditRejected()
303 {
304     m_stackedLayout->setCurrentIndex(0);
305     m_unfinishedNotes.remove(m_entry);
306     Q_EMIT escaped();
307 }
308 
addNote(DocPosition p,const QString & text)309 void MsgCtxtView::addNote(DocPosition p, const QString& text)
310 {
311     p.form = -1;
312     m_catalog->push(new SetNoteCmd(m_catalog, p, Note(text)));
313     if (m_entry.entry == p.entry) {
314         m_prevEntry.entry = -1;
315         process();
316     }
317 }
318 
addTemporaryEntryNote(int entry,const QString & text)319 void MsgCtxtView::addTemporaryEntryNote(int entry, const QString& text)
320 {
321     m_tempNotes.insertMulti(entry, text);
322     m_prevEntry.entry = -1;
323     process();
324 }
325 
removeErrorNotes()326 void MsgCtxtView::removeErrorNotes()
327 {
328     if (!m_hasErrorNotes) return;
329 
330     DocPosition p = m_entry.toDocPosition();
331     const QVector<Note> notes = m_catalog->notes(p);
332     p.form = notes.size();
333     while (--(p.form) >= 0) {
334         if (notes.at(p.form).content.contains(QLatin1String("[ERROR]")))
335             m_catalog->push(new SetNoteCmd(m_catalog, p, Note()));
336     }
337 
338     m_prevEntry.entry = -1;
339     process();
340 }
341 
342 
343