Warning, file /sdk/lokalize/src/msgctxtview.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).

0001 /*
0002   This file is part of Lokalize
0003 
0004   SPDX-FileCopyrightText: 2007-2014 Nick Shaforostoff <shafff@ukr.net>
0005   SPDX-FileCopyrightText: 2018-2019 Simon Depiets <sdepiets@gmail.com>
0006 
0007   SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0008 */
0009 
0010 #include "msgctxtview.h"
0011 
0012 #include "noteeditor.h"
0013 #include "catalog.h"
0014 #include "cmd.h"
0015 #include "prefs_lokalize.h"
0016 #include "project.h"
0017 
0018 #include "lokalize_debug.h"
0019 
0020 #include <klocalizedstring.h>
0021 #include <ktextedit.h>
0022 #include <kcombobox.h>
0023 
0024 #include <QTime>
0025 #include <QTimer>
0026 #include <QBoxLayout>
0027 #include <QStackedLayout>
0028 #include <QLabel>
0029 #include <QStringListModel>
0030 #include <QLineEdit>
0031 #include <QTextBrowser>
0032 #include <QStringBuilder>
0033 #include <QDesktopServices>
0034 #include <QRegularExpression>
0035 
0036 MsgCtxtView::MsgCtxtView(QWidget* parent, Catalog* catalog)
0037     : QDockWidget(i18nc("@title toolview name", "Unit metadata"), parent)
0038     , m_browser(new QTextBrowser(this))
0039     , m_editor(nullptr)
0040     , m_catalog(catalog)
0041     , m_selection(0)
0042     , m_offset(0)
0043     , m_hasInfo(false)
0044     , m_hasErrorNotes(false)
0045     , m_pologyProcessInProgress(0)
0046     , m_pologyStartedReceivingOutput(false)
0047 {
0048     setObjectName(QStringLiteral("msgCtxtView"));
0049     QWidget* main = new QWidget(this);
0050     setWidget(main);
0051     m_stackedLayout = new QStackedLayout(main);
0052     m_stackedLayout->addWidget(m_browser);
0053 
0054     m_browser->viewport()->setBackgroundRole(QPalette::Window);
0055     m_browser->setOpenLinks(false);
0056     connect(m_browser, &QTextBrowser::anchorClicked, this, &MsgCtxtView::anchorClicked);
0057 }
0058 
0059 MsgCtxtView::~MsgCtxtView()
0060 {
0061 }
0062 
0063 const QString MsgCtxtView::BR = "<br />";
0064 
0065 void MsgCtxtView::cleanup()
0066 {
0067     m_unfinishedNotes.clear();
0068     m_tempNotes.clear();
0069     m_pologyNotes.clear();
0070     m_languageToolNotes.clear();
0071 }
0072 
0073 void MsgCtxtView::gotoEntry(const DocPosition& pos, int selection)
0074 {
0075     m_entry = DocPos(pos);
0076     m_selection = selection;
0077     m_offset = pos.offset;
0078     QTimer::singleShot(0, this, &MsgCtxtView::process);
0079     QTimer::singleShot(0, this, &MsgCtxtView::pology);
0080 }
0081 
0082 void MsgCtxtView::process()
0083 {
0084     if (m_catalog->numberOfEntries() <= m_entry.entry)
0085         return;//because of Qt::QueuedConnection
0086 
0087     if (m_stackedLayout->currentIndex())
0088         m_unfinishedNotes[m_prevEntry] = qMakePair(m_editor->note(), m_editor->noteIndex());
0089 
0090 
0091     if (m_unfinishedNotes.contains(m_entry)) {
0092         addNoteUI();
0093         m_editor->setNote(m_unfinishedNotes.value(m_entry).first, m_unfinishedNotes.value(m_entry).second);
0094     } else
0095         m_stackedLayout->setCurrentIndex(0);
0096 
0097 
0098     m_prevEntry = m_entry;
0099     m_browser->clear();
0100 
0101     if (m_tempNotes.contains(m_entry.entry)) {
0102         QString html = i18nc("@info notes to translation unit which expire when the catalog is closed", "<b>Temporary notes:</b>");
0103         html += MsgCtxtView::BR;
0104         const auto tempNotes = m_tempNotes.values(m_entry.entry);
0105         for (const QString& note : tempNotes)
0106             html += note.toHtmlEscaped() + MsgCtxtView::BR;
0107         html += MsgCtxtView::BR;
0108         m_browser->insertHtml(html.replace('\n', MsgCtxtView::BR));
0109     }
0110     if (m_pologyNotes.contains(m_entry.entry)) {
0111         QString html = i18nc("@info notes generated by the pology check", "<b>Pology notes:</b>");
0112         html += MsgCtxtView::BR;
0113         const auto pologyNotes = m_pologyNotes.values(m_entry.entry);
0114         for (const QString& note : pologyNotes)
0115             html += note.toHtmlEscaped() + MsgCtxtView::BR;
0116         html += MsgCtxtView::BR;
0117         m_browser->insertHtml(html.replace('\n', MsgCtxtView::BR));
0118     }
0119     if (m_languageToolNotes.contains(m_entry.entry)) {
0120         QString html = i18nc("@info notes generated by the languagetool check", "<b>LanguageTool notes:</b>");
0121         html += MsgCtxtView::BR;
0122         const auto languageToolNotes = m_languageToolNotes.values(m_entry.entry);
0123         for (const QString& note : languageToolNotes)
0124             html += note.toHtmlEscaped() + MsgCtxtView::BR;
0125         html += MsgCtxtView::BR;
0126         m_browser->insertHtml(html.replace('\n', MsgCtxtView::BR));
0127     }
0128 
0129     QString phaseName = m_catalog->phase(m_entry.toDocPosition());
0130     if (!phaseName.isEmpty()) {
0131         Phase phase = m_catalog->phase(phaseName);
0132         QString html = i18nc("@info translation unit metadata", "<b>Phase:</b><br>");
0133         if (phase.date.isValid())
0134             html += QString(QStringLiteral("%1: ")).arg(phase.date.toString(Qt::ISODate));
0135         html += phase.process.toHtmlEscaped();
0136         if (!phase.contact.isEmpty())
0137             html += QString(QStringLiteral(" (%1)")).arg(phase.contact.toHtmlEscaped());
0138         m_browser->insertHtml(html + MsgCtxtView::BR);
0139     }
0140 
0141     const QVector<Note> notes = m_catalog->notes(m_entry.toDocPosition());
0142     m_hasErrorNotes = false;
0143     for (const Note& note : notes)
0144         m_hasErrorNotes = m_hasErrorNotes || note.content.contains(QLatin1String("[ERROR]"));
0145 
0146     int realOffset = displayNotes(m_browser, m_catalog->notes(m_entry.toDocPosition()), m_entry.form, m_catalog->capabilities()&MultipleNotes);
0147 
0148     QString html;
0149     const auto developerNotes = m_catalog->developerNotes(m_entry.toDocPosition());
0150     for (const Note& note : developerNotes) {
0151         html += MsgCtxtView::BR + escapeWithLinks(note.content).replace('\n', BR);
0152     }
0153 
0154     const QStringList sourceFiles = m_catalog->sourceFiles(m_entry.toDocPosition());
0155     if (!sourceFiles.isEmpty()) {
0156         html += i18nc("@info PO comment parsing", "<br><b>Files:</b><br>");
0157         for (const QString &sourceFile : sourceFiles)
0158             html += QString(QStringLiteral("<a href=\"src:/%1\">%2</a><br />")).arg(sourceFile, sourceFile);
0159         html.chop(6);
0160     }
0161 
0162     QString msgctxt = m_catalog->context(m_entry.entry).first();
0163     if (!msgctxt.isEmpty())
0164         html += i18nc("@info PO comment parsing", "<br><b>Context:</b><br>") + msgctxt.toHtmlEscaped();
0165 
0166     QTextCursor t = m_browser->textCursor();
0167     t.movePosition(QTextCursor::End);
0168     m_browser->setTextCursor(t);
0169     m_browser->insertHtml(html);
0170 
0171     t.movePosition(QTextCursor::Start);
0172     t.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, realOffset + m_offset);
0173     t.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, m_selection);
0174     m_browser->setTextCursor(t);
0175 }
0176 void MsgCtxtView::languageTool(const QString &text)
0177 {
0178     m_languageToolNotes.insert(m_entry.entry, text);
0179     m_prevEntry.entry = -1;
0180     process();
0181 }
0182 void MsgCtxtView::pology()
0183 {
0184     if (Settings::self()->pologyEnabled() && m_pologyProcessInProgress == 0 && QFile::exists(m_catalog->url())) {
0185         QString command = Settings::self()->pologyCommandEntry();
0186         command = command.replace(QStringLiteral("%u"), QString::number(m_entry.entry + 1)).replace(QStringLiteral("%f"),  QStringLiteral("\"") + m_catalog->url() + QStringLiteral("\"")).replace(QStringLiteral("\n"), QStringLiteral(" "));
0187         m_pologyProcess = new KProcess;
0188         m_pologyProcess->setShellCommand(command);
0189         m_pologyProcess->setOutputChannelMode(KProcess::SeparateChannels);
0190         m_pologyStartedReceivingOutput = false;
0191         connect(m_pologyProcess, &KProcess::readyReadStandardOutput,
0192                 this, &MsgCtxtView::pologyReceivedStandardOutput);
0193         connect(m_pologyProcess, &KProcess::readyReadStandardError,
0194                 this, &MsgCtxtView::pologyReceivedStandardError);
0195         connect(m_pologyProcess, QOverload<int, QProcess::ExitStatus>::of(&KProcess::finished),
0196                 this, &MsgCtxtView::pologyHasFinished);
0197         m_pologyData = QStringLiteral("");
0198         m_pologyProcessInProgress = m_entry.entry + 1;
0199         m_pologyProcess->start();
0200     } else if (Settings::self()->pologyEnabled() && m_pologyProcessInProgress > 0) {
0201         QTimer::singleShot(1000, this, &MsgCtxtView::pology);
0202     }
0203 }
0204 void MsgCtxtView::pologyReceivedStandardOutput()
0205 {
0206     if (m_pologyProcessInProgress == m_entry.entry + 1) {
0207         if (!m_pologyStartedReceivingOutput) {
0208             m_pologyStartedReceivingOutput = true;
0209         }
0210         const QString grossPologyOutput = m_pologyProcess->readAllStandardOutput();
0211         const QStringList pologyTmpLines = grossPologyOutput.split('\n', Qt::SkipEmptyParts);
0212         for (const QString &pologyTmp : pologyTmpLines) {
0213             if (pologyTmp.startsWith(QStringLiteral("[note]")))
0214                 m_pologyData += pologyTmp;
0215         }
0216     }
0217 }
0218 
0219 
0220 void MsgCtxtView::pologyReceivedStandardError()
0221 {
0222     if (m_pologyProcessInProgress == m_entry.entry + 1) {
0223         if (!m_pologyStartedReceivingOutput) {
0224             m_pologyStartedReceivingOutput = true;
0225         }
0226         m_pologyData += m_pologyProcess->readAllStandardError().replace('\n', MsgCtxtView::BR.toLatin1());
0227     }
0228 }
0229 void MsgCtxtView::pologyHasFinished()
0230 {
0231     if (m_pologyProcessInProgress == m_entry.entry + 1) {
0232         if (!m_pologyStartedReceivingOutput) {
0233             m_pologyStartedReceivingOutput = true;
0234             const QString grossPologyOutput = m_pologyProcess->readAllStandardOutput();
0235             const QStringList pologyTmpLines = grossPologyOutput.split('\n', Qt::SkipEmptyParts);
0236             if (pologyTmpLines.count() == 0) {
0237                 m_pologyData += i18nc("@info The pology command didn't return anything", "(empty)");
0238             } else {
0239                 for (const QString &pologyTmp : pologyTmpLines) {
0240                     if (pologyTmp.startsWith(QStringLiteral("[note]")))
0241                         m_pologyData += pologyTmp;
0242                 }
0243             }
0244         }
0245         m_pologyNotes.insert(m_entry.entry, m_pologyData);
0246         m_prevEntry.entry = -1;
0247         process();
0248     }
0249     m_pologyProcess->deleteLater();
0250     m_pologyProcessInProgress = 0;
0251 }
0252 
0253 void MsgCtxtView::addNoteUI()
0254 {
0255     anchorClicked(QUrl(QStringLiteral("note:/add")));
0256 }
0257 
0258 void MsgCtxtView::anchorClicked(const QUrl& link)
0259 {
0260     QString path = link.path().mid(1); // minus '/'
0261 
0262     if (link.scheme() == QLatin1String("note")) {
0263         int capabilities = m_catalog->capabilities();
0264         if (!m_editor) {
0265             m_editor = new NoteEditor(this);
0266             m_stackedLayout->addWidget(m_editor);
0267             connect(m_editor, &NoteEditor::accepted, this, &MsgCtxtView::noteEditAccepted);
0268             connect(m_editor, &NoteEditor::rejected, this, &MsgCtxtView::noteEditRejected);
0269         }
0270         m_editor->setNoteAuthors(m_catalog->noteAuthors());
0271         QVector<Note> notes = m_catalog->notes(m_entry.toDocPosition());
0272         int noteIndex = -1; //means add new note
0273         Note note;
0274         if (!path.endsWith(QLatin1String("add"))) {
0275             noteIndex = path.toInt();
0276             note = notes.at(noteIndex);
0277         } else if (!(capabilities & MultipleNotes) && notes.size()) {
0278             noteIndex = 0; //so we don't overwrite the only possible note
0279             note = notes.first();
0280         }
0281         m_editor->setNote(note, noteIndex);
0282         m_editor->setFromFieldVisible(capabilities & KeepsNoteAuthors);
0283         m_stackedLayout->setCurrentIndex(1);
0284     } else if (link.scheme() == QLatin1String("src")) {
0285         int pos = path.lastIndexOf(':');
0286         Q_EMIT srcFileOpenRequested(path.left(pos), path.midRef(pos + 1).toInt());
0287     } else if (link.scheme().contains(QLatin1String("tp")))
0288         QDesktopServices::openUrl(link);
0289 }
0290 
0291 void MsgCtxtView::noteEditAccepted()
0292 {
0293     DocPosition pos = m_entry.toDocPosition();
0294     pos.form = m_editor->noteIndex();
0295     m_catalog->push(new SetNoteCmd(m_catalog, pos, m_editor->note()));
0296 
0297     m_prevEntry.entry = -1; process();
0298     //m_stackedLayout->setCurrentIndex(0);
0299     //m_unfinishedNotes.remove(m_entry);
0300     noteEditRejected();
0301 }
0302 void MsgCtxtView::noteEditRejected()
0303 {
0304     m_stackedLayout->setCurrentIndex(0);
0305     m_unfinishedNotes.remove(m_entry);
0306     Q_EMIT escaped();
0307 }
0308 
0309 void MsgCtxtView::addNote(DocPosition p, const QString& text)
0310 {
0311     p.form = -1;
0312     m_catalog->push(new SetNoteCmd(m_catalog, p, Note(text)));
0313     if (m_entry.entry == p.entry) {
0314         m_prevEntry.entry = -1;
0315         process();
0316     }
0317 }
0318 
0319 void MsgCtxtView::addTemporaryEntryNote(int entry, const QString& text)
0320 {
0321     m_tempNotes.insertMulti(entry, text);
0322     m_prevEntry.entry = -1;
0323     process();
0324 }
0325 
0326 void MsgCtxtView::removeErrorNotes()
0327 {
0328     if (!m_hasErrorNotes) return;
0329 
0330     DocPosition p = m_entry.toDocPosition();
0331     const QVector<Note> notes = m_catalog->notes(p);
0332     p.form = notes.size();
0333     while (--(p.form) >= 0) {
0334         if (notes.at(p.form).content.contains(QLatin1String("[ERROR]")))
0335             m_catalog->push(new SetNoteCmd(m_catalog, p, Note()));
0336     }
0337 
0338     m_prevEntry.entry = -1;
0339     process();
0340 }
0341 
0342