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 "mergeview.h"
11 
12 #include "cmd.h"
13 #include "mergecatalog.h"
14 #include "project.h"
15 #include "diff.h"
16 
17 #include <klocalizedstring.h>
18 #include <kmessagebox.h>
19 #include <ktextedit.h>
20 #include <knotification.h>
21 
22 #include <QDragEnterEvent>
23 #include <QMimeData>
24 #include <QFile>
25 #include <QToolTip>
26 #include <QStringBuilder>
27 #include <QFileDialog>
28 
MergeView(QWidget * parent,Catalog * catalog,bool primary)29 MergeView::MergeView(QWidget* parent, Catalog* catalog, bool primary)
30     : QDockWidget(primary ? i18nc("@title:window that displays difference between current file and 'merge source'", "Primary Sync") : i18nc("@title:window that displays difference between current file and 'merge source'", "Secondary Sync"), parent)
31     , m_browser(new QTextEdit(this))
32     , m_baseCatalog(catalog)
33     , m_mergeCatalog(nullptr)
34     , m_normTitle(primary ?
35                   i18nc("@title:window that displays difference between current file and 'merge source'", "Primary Sync") :
36                   i18nc("@title:window that displays difference between current file and 'merge source'", "Secondary Sync"))
37     , m_hasInfoTitle(m_normTitle + " [*]")
38     , m_hasInfo(false)
39     , m_primary(primary)
40 {
41     setObjectName(primary ? QStringLiteral("mergeView-primary") : QStringLiteral("mergeView-secondary"));
42     setWidget(m_browser);
43     setToolTip(i18nc("@info:tooltip", "Drop file to be merged into / synced with the current one here, then see context menu options"));
44 
45     hide();
46 
47     setAcceptDrops(true);
48     m_browser->setReadOnly(true);
49     m_browser->setContextMenuPolicy(Qt::NoContextMenu);
50     m_browser->viewport()->setBackgroundRole(QPalette::Window);
51     setContextMenuPolicy(Qt::ActionsContextMenu);
52 }
53 
~MergeView()54 MergeView::~MergeView()
55 {
56     delete m_mergeCatalog;
57     Q_EMIT mergeCatalogPointerChanged(nullptr);
58     Q_EMIT mergeCatalogAvailable(false);
59 }
60 
filePath()61 QString MergeView::filePath()
62 {
63     if (m_mergeCatalog)
64         return m_mergeCatalog->url();
65     return QString();
66 }
67 
dragEnterEvent(QDragEnterEvent * event)68 void MergeView::dragEnterEvent(QDragEnterEvent* event)
69 {
70     if (event->mimeData()->hasUrls() && Catalog::extIsSupported(event->mimeData()->urls().first().path()))
71         event->acceptProposedAction();
72 }
73 
dropEvent(QDropEvent * event)74 void MergeView::dropEvent(QDropEvent *event)
75 {
76     mergeOpen(event->mimeData()->urls().first().toLocalFile());
77     event->acceptProposedAction();
78 }
79 
slotUpdate(const DocPosition & pos)80 void MergeView::slotUpdate(const DocPosition& pos)
81 {
82     if (pos.entry == m_pos.entry)
83         slotNewEntryDisplayed(pos);
84 }
85 
slotNewEntryDisplayed(const DocPosition & pos)86 void MergeView::slotNewEntryDisplayed(const DocPosition& pos)
87 {
88     m_pos = pos;
89 
90     if (!m_mergeCatalog)
91         return;
92 
93     Q_EMIT signalPriorChangedAvailable((pos.entry > m_mergeCatalog->firstChangedIndex())
94                                      || (pluralFormsAvailableBackward() != -1));
95     Q_EMIT signalNextChangedAvailable((pos.entry < m_mergeCatalog->lastChangedIndex())
96                                     || (pluralFormsAvailableForward() != -1));
97 
98     if (!m_mergeCatalog->isPresent(pos.entry)) {
99         //i.e. no corresponding entry, whether changed or not
100         if (m_hasInfo) {
101             m_hasInfo = false;
102             setWindowTitle(m_normTitle);
103             m_browser->clear();
104 //             m_browser->viewport()->setBackgroundRole(QPalette::Base);
105         }
106         Q_EMIT signalEntryWithMergeDisplayed(false);
107 
108         /// no editing at all!  ////////////
109         return;
110     }
111     if (!m_hasInfo) {
112         m_hasInfo = true;
113         setWindowTitle(m_hasInfoTitle);
114     }
115 
116     Q_EMIT signalEntryWithMergeDisplayed(m_mergeCatalog->isDifferent(pos.entry));
117 
118     QString result = userVisibleWordDiff(m_baseCatalog->msgstr(pos),
119                                          m_mergeCatalog->msgstr(pos),
120                                          Project::instance()->accel(),
121                                          Project::instance()->markup(),
122                                          Html);
123 #if 0
124     int i = -1;
125     bool inTag = false;
126     while (++i < result.size()) { //dynamic
127         if (!inTag) {
128             if (result.at(i) == '<')
129                 inTag = true;
130             else if (result.at(i) == ' ')
131                 result.replace(i, 1, "&sp;");
132         } else if (result.at(i) == '>')
133             inTag = false;
134     }
135 #endif
136 
137     if (!m_mergeCatalog->isApproved(pos.entry)) {
138         result.prepend("<i>");
139         result.append("</i>");
140     }
141 
142     if (m_mergeCatalog->isModified(pos)) {
143         result.prepend("<b>");
144         result.append("</b>");
145     }
146     result.replace(' ', QChar::Nbsp);
147     m_browser->setHtml(result);
148     //qCDebug(LOKALIZE_LOG)<<"ELA "<<time.elapsed();
149 }
150 
cleanup()151 void MergeView::cleanup()
152 {
153     delete m_mergeCatalog;
154     m_mergeCatalog = nullptr;
155     Q_EMIT mergeCatalogPointerChanged(nullptr);
156     Q_EMIT mergeCatalogAvailable(false);
157     m_pos = DocPosition();
158 
159     Q_EMIT signalPriorChangedAvailable(false);
160     Q_EMIT signalNextChangedAvailable(false);
161     Q_EMIT signalEntryWithMergeDisplayed(false);
162     m_browser->clear();
163 }
164 
mergeOpen(QString mergeFilePath)165 void MergeView::mergeOpen(QString mergeFilePath)
166 {
167     if (Q_UNLIKELY(!m_baseCatalog->numberOfEntries()))
168         return;
169 
170     if (mergeFilePath == m_baseCatalog->url()) {
171         //(we are likely to be _mergeViewSecondary)
172         //special handling: open corresponding file in the branch
173         //for AutoSync
174 
175         QString path = QFileInfo(mergeFilePath).canonicalFilePath(); //bug 245546 regarding symlinks
176         QString oldPath = path;
177         path.replace(Project::instance()->poDir(), Project::instance()->branchDir());
178 
179         if (oldPath == path) { //if file doesn't exist both are empty
180             cleanup();
181             return;
182         }
183 
184         mergeFilePath = path;
185     }
186 
187     if (mergeFilePath.isEmpty()) {
188         //Project::instance()->model()->weaver()->suspend();
189         //KDE5PORT use mutex if needed
190         mergeFilePath = QFileDialog::getOpenFileName(this, i18nc("@title:window", "Select translation file"), QString(), Catalog::supportedFileTypes(false));
191         //Project::instance()->model()->weaver()->resume();
192     }
193     if (mergeFilePath.isEmpty())
194         return;
195 
196     delete m_mergeCatalog;
197     m_mergeCatalog = new MergeCatalog(this, m_baseCatalog);
198     Q_EMIT mergeCatalogPointerChanged(m_mergeCatalog);
199     Q_EMIT mergeCatalogAvailable(m_mergeCatalog);
200     int errorLine = m_mergeCatalog->loadFromUrl(mergeFilePath);
201     if (Q_LIKELY(errorLine == 0)) {
202         if (m_pos.entry > 0)
203             Q_EMIT signalPriorChangedAvailable(m_pos.entry > m_mergeCatalog->firstChangedIndex());
204 
205         Q_EMIT signalNextChangedAvailable(m_pos.entry < m_mergeCatalog->lastChangedIndex());
206 
207         //a bit hacky :)
208         connect(m_mergeCatalog, &MergeCatalog::signalEntryModified, this, &MergeView::slotUpdate);
209 
210         if (m_pos.entry != -1)
211             slotNewEntryDisplayed(m_pos);
212         show();
213     } else {
214         //KMessageBox::error(this, KIO::NetAccess::lastErrorString() );
215         cleanup();
216         if (errorLine > 0)
217             KMessageBox::error(this, i18nc("@info", "Error opening the file <filename>%1</filename> for synchronization, error line: %2", mergeFilePath, errorLine));
218         else {
219             /* disable this as requested by bug 272587
220             KNotification* notification=new KNotification("MergeFilesOpenError");
221             notification->setWidget(this);
222             notification->setText( i18nc("@info %1 is full filename","Error opening the file <filename>%1</filename> for synchronization",url.pathOrUrl()) );
223             notification->sendEvent();
224             */
225         }
226         //i18nc("@info %1 is w/o path","No branch counterpart for <filename>%1</filename>",url.fileName()),
227     }
228 
229 }
230 
isModified()231 bool MergeView::isModified()
232 {
233     return m_mergeCatalog && m_mergeCatalog->isModified(); //not isClean because mergecatalog doesn't keep history
234 }
235 
pluralFormsAvailableForward()236 int MergeView::pluralFormsAvailableForward()
237 {
238     if (Q_LIKELY(m_pos.entry == -1 || !m_mergeCatalog->isPlural(m_pos.entry)))
239         return -1;
240 
241     int formLimit = qMin(m_baseCatalog->numberOfPluralForms(), m_mergeCatalog->numberOfPluralForms()); //just sanity check
242     DocPosition pos = m_pos;
243     while (++(pos.form) < formLimit) {
244         if (m_baseCatalog->msgstr(pos) != m_mergeCatalog->msgstr(pos))
245             return pos.form;
246     }
247     return -1;
248 }
249 
pluralFormsAvailableBackward()250 int MergeView::pluralFormsAvailableBackward()
251 {
252     if (Q_LIKELY(m_pos.entry == -1 || !m_mergeCatalog->isPlural(m_pos.entry)))
253         return -1;
254 
255     DocPosition pos = m_pos;
256     while (--(pos.form) >= 0) {
257         if (m_baseCatalog->msgstr(pos) != m_mergeCatalog->msgstr(pos))
258             return pos.form;
259     }
260     return -1;
261 }
262 
gotoPrevChanged()263 void MergeView::gotoPrevChanged()
264 {
265     if (Q_UNLIKELY(!m_mergeCatalog))
266         return;
267 
268     DocPosition pos;
269 
270     //first, check if there any plural forms waiting to be synced
271     int form = pluralFormsAvailableBackward();
272     if (Q_UNLIKELY(form != -1)) {
273         pos = m_pos;
274         pos.form = form;
275     } else if (Q_UNLIKELY((pos.entry = m_mergeCatalog->prevChangedIndex(m_pos.entry)) == -1))
276         return;
277 
278     if (Q_UNLIKELY(m_mergeCatalog->isPlural(pos.entry) && form == -1))
279         pos.form = qMin(m_baseCatalog->numberOfPluralForms(), m_mergeCatalog->numberOfPluralForms()) - 1;
280 
281     Q_EMIT gotoEntry(pos, 0);
282 }
283 
gotoNextChangedApproved()284 void MergeView::gotoNextChangedApproved()
285 {
286     gotoNextChanged(true);
287 }
288 
gotoNextChanged(bool approvedOnly)289 void MergeView::gotoNextChanged(bool approvedOnly)
290 {
291     if (Q_UNLIKELY(!m_mergeCatalog))
292         return;
293 
294     DocPosition pos = m_pos;
295 
296     //first, check if there any plural forms waiting to be synced
297     int form = pluralFormsAvailableForward();
298     if (Q_UNLIKELY(form != -1)) {
299         pos = m_pos;
300         pos.form = form;
301     } else if (Q_UNLIKELY((pos.entry = m_mergeCatalog->nextChangedIndex(m_pos.entry)) == -1))
302         return;
303 
304     while (approvedOnly && !m_mergeCatalog->isApproved(pos.entry)) {
305         if (Q_UNLIKELY((pos.entry = m_mergeCatalog->nextChangedIndex(pos.entry)) == -1))
306             return;
307     }
308 
309     Q_EMIT gotoEntry(pos, 0);
310 }
311 
mergeBack()312 void MergeView::mergeBack()
313 {
314     if (m_pos.entry == -1 || !m_mergeCatalog || m_baseCatalog->msgstr(m_pos).isEmpty())
315         return;
316 
317     m_mergeCatalog->copyFromBaseCatalog(m_pos);
318 }
319 
mergeAccept()320 void MergeView::mergeAccept()
321 {
322     if (m_pos.entry == -1
323         || !m_mergeCatalog
324         //||m_baseCatalog->msgstr(m_pos)==m_mergeCatalog->msgstr(m_pos)
325         || m_mergeCatalog->msgstr(m_pos).isEmpty())
326         return;
327 
328     m_mergeCatalog->copyToBaseCatalog(m_pos);
329 
330     Q_EMIT gotoEntry(m_pos, 0);
331 }
332 
mergeAcceptAllForEmpty()333 void MergeView::mergeAcceptAllForEmpty()
334 {
335     if (Q_UNLIKELY(!m_mergeCatalog)) return;
336 
337     bool update = m_mergeCatalog->differentEntries().contains(m_pos.entry);
338 
339     m_mergeCatalog->copyToBaseCatalog(/*MergeCatalog::EmptyOnly*/MergeCatalog::HigherOnly);
340 
341     if (update != m_mergeCatalog->differentEntries().contains(m_pos.entry))
342         Q_EMIT gotoEntry(m_pos, 0);
343 }
344 
345 
event(QEvent * event)346 bool MergeView::event(QEvent *event)
347 {
348     if (event->type() == QEvent::ToolTip && m_mergeCatalog) {
349         QHelpEvent *helpEvent = static_cast<QHelpEvent *>(event);
350         QString text = QStringLiteral("<b>") + QDir::toNativeSeparators(filePath()) + QStringLiteral("</b>\n") + i18nc("@info:tooltip", "Different entries: %1\nUnmatched entries: %2",
351                        m_mergeCatalog->differentEntries().count(), m_mergeCatalog->unmatchedCount());
352         text.replace('\n', QStringLiteral("<br />"));
353         QToolTip::showText(helpEvent->globalPos(), text);
354         return true;
355     }
356     return QWidget::event(event);
357 }
358 
359