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