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