1 | /* |
2 | SPDX-FileCopyrightText: 2010-2018 Dominik Haumann <dhaumann@kde.org> |
3 | SPDX-FileCopyrightText: 2010 Diana-Victoria Tiriplica <diana.tiriplica@gmail.com> |
4 | |
5 | SPDX-License-Identifier: LGPL-2.0-or-later |
6 | */ |
7 | |
8 | #include "config.h" |
9 | |
10 | #include "katebuffer.h" |
11 | #include "kateconfig.h" |
12 | #include "katedocument.h" |
13 | #include "katepartdebug.h" |
14 | #include "kateswapdiffcreator.h" |
15 | #include "kateswapfile.h" |
16 | #include "katetextbuffer.h" |
17 | #include "kateundomanager.h" |
18 | #include "ktexteditor/message.h" |
19 | #include <ktexteditor/view.h> |
20 | |
21 | #include <KLocalizedString> |
22 | #include <KStandardGuiItem> |
23 | |
24 | #include <QApplication> |
25 | #include <QCryptographicHash> |
26 | #include <QDir> |
27 | #include <QFileInfo> |
28 | |
29 | #ifndef Q_OS_WIN |
30 | #include <unistd.h> |
31 | #else |
32 | #include <io.h> |
33 | #endif |
34 | |
35 | // swap file version header |
36 | const static char swapFileVersionString[] = "Kate Swap File 2.0" ; |
37 | |
38 | // tokens for swap files |
39 | const static qint8 EA_StartEditing = 'S'; |
40 | const static qint8 EA_FinishEditing = 'E'; |
41 | const static qint8 EA_WrapLine = 'W'; |
42 | const static qint8 EA_UnwrapLine = 'U'; |
43 | const static qint8 EA_InsertText = 'I'; |
44 | const static qint8 EA_RemoveText = 'R'; |
45 | |
46 | namespace Kate |
47 | { |
48 | QTimer *SwapFile::s_timer = nullptr; |
49 | |
50 | SwapFile::SwapFile(KTextEditor::DocumentPrivate *document) |
51 | : QObject(document) |
52 | , m_document(document) |
53 | , m_trackingEnabled(false) |
54 | , m_recovered(false) |
55 | , m_needSync(false) |
56 | { |
57 | // fixed version of serialisation |
58 | m_stream.setVersion(QDataStream::Qt_4_6); |
59 | |
60 | // connect the timer |
61 | connect(sender: syncTimer(), signal: &QTimer::timeout, context: this, slot: &Kate::SwapFile::writeFileToDisk, type: Qt::DirectConnection); |
62 | |
63 | // connecting the signals |
64 | connect(sender: &m_document->buffer(), signal: &KateBuffer::saved, context: this, slot: &Kate::SwapFile::fileSaved); |
65 | connect(sender: &m_document->buffer(), signal: &KateBuffer::loaded, context: this, slot: &Kate::SwapFile::fileLoaded); |
66 | connect(sender: m_document, signal: &KTextEditor::Document::configChanged, context: this, slot: &SwapFile::configChanged); |
67 | |
68 | // tracking on! |
69 | setTrackingEnabled(true); |
70 | } |
71 | |
72 | SwapFile::~SwapFile() |
73 | { |
74 | // only remove swap file after data recovery (bug #304576) |
75 | if (!shouldRecover()) { |
76 | removeSwapFile(); |
77 | } |
78 | } |
79 | |
80 | void SwapFile::configChanged() |
81 | { |
82 | } |
83 | |
84 | void SwapFile::setTrackingEnabled(bool enable) |
85 | { |
86 | if (m_trackingEnabled == enable) { |
87 | return; |
88 | } |
89 | |
90 | m_trackingEnabled = enable; |
91 | |
92 | if (m_trackingEnabled) { |
93 | connect(sender: m_document, signal: &KTextEditor::Document::editingStarted, context: this, slot: &Kate::SwapFile::startEditing); |
94 | connect(sender: m_document, signal: &KTextEditor::Document::editingFinished, context: this, slot: &Kate::SwapFile::finishEditing); |
95 | connect(sender: m_document, signal: &KTextEditor::DocumentPrivate::modifiedChanged, context: this, slot: &SwapFile::modifiedChanged); |
96 | |
97 | connect(sender: m_document, signal: &KTextEditor::Document::lineWrapped, context: this, slot: &Kate::SwapFile::wrapLine); |
98 | connect(sender: m_document, signal: &KTextEditor::Document::lineUnwrapped, context: this, slot: &Kate::SwapFile::unwrapLine); |
99 | connect(sender: m_document, signal: &KTextEditor::Document::textInserted, context: this, slot: &Kate::SwapFile::insertText); |
100 | connect(sender: m_document, signal: &KTextEditor::Document::textRemoved, context: this, slot: &Kate::SwapFile::removeText); |
101 | } else { |
102 | disconnect(sender: m_document, signal: &KTextEditor::Document::editingStarted, receiver: this, slot: &Kate::SwapFile::startEditing); |
103 | disconnect(sender: m_document, signal: &KTextEditor::Document::editingFinished, receiver: this, slot: &Kate::SwapFile::finishEditing); |
104 | disconnect(sender: m_document, signal: &KTextEditor::DocumentPrivate::modifiedChanged, receiver: this, slot: &SwapFile::modifiedChanged); |
105 | |
106 | disconnect(sender: m_document, signal: &KTextEditor::Document::lineWrapped, receiver: this, slot: &Kate::SwapFile::wrapLine); |
107 | disconnect(sender: m_document, signal: &KTextEditor::Document::lineUnwrapped, receiver: this, slot: &Kate::SwapFile::unwrapLine); |
108 | disconnect(sender: m_document, signal: &KTextEditor::Document::textInserted, receiver: this, slot: &Kate::SwapFile::insertText); |
109 | disconnect(sender: m_document, signal: &KTextEditor::Document::textRemoved, receiver: this, slot: &Kate::SwapFile::removeText); |
110 | } |
111 | } |
112 | |
113 | void SwapFile::fileClosed() |
114 | { |
115 | // remove old swap file, file is now closed |
116 | if (!shouldRecover()) { |
117 | removeSwapFile(); |
118 | } else { |
119 | m_document->setReadWrite(true); |
120 | } |
121 | |
122 | // purge filename |
123 | updateFileName(); |
124 | } |
125 | |
126 | KTextEditor::DocumentPrivate *SwapFile::document() |
127 | { |
128 | return m_document; |
129 | } |
130 | |
131 | bool SwapFile::isValidSwapFile(QDataStream &stream, bool checkDigest) const |
132 | { |
133 | // read and check header |
134 | QByteArray ; |
135 | stream >> header; |
136 | |
137 | if (header != swapFileVersionString) { |
138 | qCWarning(LOG_KTE) << "Can't open swap file, wrong version" ; |
139 | return false; |
140 | } |
141 | |
142 | // read checksum |
143 | QByteArray checksum; |
144 | stream >> checksum; |
145 | // qCDebug(LOG_KTE) << "DIGEST:" << checksum << m_document->checksum(); |
146 | if (checkDigest && checksum != m_document->checksum()) { |
147 | qCWarning(LOG_KTE) << "Can't recover from swap file, checksum of document has changed" ; |
148 | return false; |
149 | } |
150 | |
151 | return true; |
152 | } |
153 | |
154 | void SwapFile::fileLoaded(const QString &) |
155 | { |
156 | // look for swap file |
157 | if (!updateFileName()) { |
158 | return; |
159 | } |
160 | |
161 | if (!m_swapfile.exists()) { |
162 | // qCDebug(LOG_KTE) << "No swap file"; |
163 | return; |
164 | } |
165 | |
166 | if (!QFileInfo(m_swapfile).isReadable()) { |
167 | qCWarning(LOG_KTE) << "Can't open swap file (missing permissions)" ; |
168 | return; |
169 | } |
170 | |
171 | // sanity check |
172 | QFile peekFile(fileName()); |
173 | if (peekFile.open(flags: QIODevice::ReadOnly)) { |
174 | QDataStream stream(&peekFile); |
175 | if (!isValidSwapFile(stream, checkDigest: true)) { |
176 | removeSwapFile(); |
177 | return; |
178 | } |
179 | peekFile.close(); |
180 | } else { |
181 | qCWarning(LOG_KTE) << "Can't open swap file:" << fileName(); |
182 | return; |
183 | } |
184 | |
185 | // show swap file message |
186 | m_document->setReadWrite(false); |
187 | showSwapFileMessage(); |
188 | } |
189 | |
190 | void SwapFile::modifiedChanged() |
191 | { |
192 | if (!m_document->isModified() && !shouldRecover()) { |
193 | // the file is not modified and we are not in recover mode |
194 | removeSwapFile(); |
195 | } |
196 | } |
197 | |
198 | void SwapFile::recover() |
199 | { |
200 | m_document->setReadWrite(true); |
201 | |
202 | // if isOpen() returns true, the swap file likely changed already (appended data) |
203 | // Example: The document was falsely marked as writable and the user changed |
204 | // text even though the recover bar was visible. In this case, a replay of |
205 | // the swap file across wrong document content would happen -> certainly wrong |
206 | if (m_swapfile.isOpen()) { |
207 | qCWarning(LOG_KTE) << "Attempt to recover an already modified document. Aborting" ; |
208 | removeSwapFile(); |
209 | return; |
210 | } |
211 | |
212 | // if the file doesn't exist, abort (user might have deleted it, or use two editor instances) |
213 | if (!m_swapfile.open(flags: QIODevice::ReadOnly)) { |
214 | qCWarning(LOG_KTE) << "Can't open swap file" ; |
215 | return; |
216 | } |
217 | |
218 | // remember that the file has recovered |
219 | m_recovered = true; |
220 | |
221 | // open data stream |
222 | m_stream.setDevice(&m_swapfile); |
223 | |
224 | // replay the swap file |
225 | bool success = recover(m_stream); |
226 | |
227 | // close swap file |
228 | m_stream.setDevice(nullptr); |
229 | m_swapfile.close(); |
230 | |
231 | if (!success) { |
232 | removeSwapFile(); |
233 | } |
234 | |
235 | // recover can also be called through the KTE::RecoveryInterface. |
236 | // Make sure, the message is hidden in this case as well. |
237 | if (m_swapMessage) { |
238 | m_swapMessage->deleteLater(); |
239 | } |
240 | } |
241 | |
242 | bool SwapFile::recover(QDataStream &stream, bool checkDigest) |
243 | { |
244 | if (!isValidSwapFile(stream, checkDigest)) { |
245 | return false; |
246 | } |
247 | |
248 | // disconnect current signals |
249 | setTrackingEnabled(false); |
250 | |
251 | // needed to set undo/redo cursors in a sane way |
252 | bool firstEditInGroup = false; |
253 | KTextEditor::Cursor undoCursor = KTextEditor::Cursor::invalid(); |
254 | KTextEditor::Cursor redoCursor = KTextEditor::Cursor::invalid(); |
255 | |
256 | // replay swapfile |
257 | bool editRunning = false; |
258 | bool brokenSwapFile = false; |
259 | while (!stream.atEnd()) { |
260 | if (brokenSwapFile) { |
261 | break; |
262 | } |
263 | |
264 | qint8 type; |
265 | stream >> type; |
266 | switch (type) { |
267 | case EA_StartEditing: { |
268 | m_document->editStart(); |
269 | editRunning = true; |
270 | firstEditInGroup = true; |
271 | undoCursor = KTextEditor::Cursor::invalid(); |
272 | redoCursor = KTextEditor::Cursor::invalid(); |
273 | break; |
274 | } |
275 | case EA_FinishEditing: { |
276 | m_document->editEnd(); |
277 | |
278 | // empty editStart() / editEnd() groups exist: only set cursor if required |
279 | if (!firstEditInGroup) { |
280 | // set undo/redo cursor of last KateUndoGroup of the undo manager |
281 | m_document->undoManager()->setUndoRedoCursorsOfLastGroup(undoCursor, redoCursor); |
282 | m_document->undoManager()->undoSafePoint(); |
283 | } |
284 | firstEditInGroup = false; |
285 | editRunning = false; |
286 | break; |
287 | } |
288 | case EA_WrapLine: { |
289 | if (!editRunning) { |
290 | brokenSwapFile = true; |
291 | break; |
292 | } |
293 | |
294 | int line = 0; |
295 | int column = 0; |
296 | stream >> line >> column; |
297 | |
298 | // emulate buffer unwrapLine with document |
299 | m_document->editWrapLine(line, col: column, newLine: true); |
300 | |
301 | // track undo/redo cursor |
302 | if (firstEditInGroup) { |
303 | firstEditInGroup = false; |
304 | undoCursor = KTextEditor::Cursor(line, column); |
305 | } |
306 | redoCursor = KTextEditor::Cursor(line + 1, 0); |
307 | |
308 | break; |
309 | } |
310 | case EA_UnwrapLine: { |
311 | if (!editRunning) { |
312 | brokenSwapFile = true; |
313 | break; |
314 | } |
315 | |
316 | int line = 0; |
317 | stream >> line; |
318 | |
319 | // assert valid line |
320 | Q_ASSERT(line > 0); |
321 | |
322 | const int undoColumn = m_document->lineLength(line: line - 1); |
323 | |
324 | // emulate buffer unwrapLine with document |
325 | m_document->editUnWrapLine(line: line - 1, removeLine: true, length: 0); |
326 | |
327 | // track undo/redo cursor |
328 | if (firstEditInGroup) { |
329 | firstEditInGroup = false; |
330 | undoCursor = KTextEditor::Cursor(line, 0); |
331 | } |
332 | redoCursor = KTextEditor::Cursor(line - 1, undoColumn); |
333 | |
334 | break; |
335 | } |
336 | case EA_InsertText: { |
337 | if (!editRunning) { |
338 | brokenSwapFile = true; |
339 | break; |
340 | } |
341 | |
342 | int line; |
343 | int column; |
344 | QByteArray text; |
345 | stream >> line >> column >> text; |
346 | QString textStr = QString::fromUtf8(utf8: text.data(), size: text.size()); |
347 | m_document->insertText(position: KTextEditor::Cursor(line, column), s: textStr); |
348 | |
349 | // track undo/redo cursor |
350 | if (firstEditInGroup) { |
351 | firstEditInGroup = false; |
352 | undoCursor = KTextEditor::Cursor(line, column); |
353 | } |
354 | redoCursor = KTextEditor::Cursor(line, column + textStr.length()); |
355 | |
356 | break; |
357 | } |
358 | case EA_RemoveText: { |
359 | if (!editRunning) { |
360 | brokenSwapFile = true; |
361 | break; |
362 | } |
363 | |
364 | int line; |
365 | int startColumn; |
366 | int endColumn; |
367 | stream >> line >> startColumn >> endColumn; |
368 | m_document->removeText(range: KTextEditor::Range(KTextEditor::Cursor(line, startColumn), KTextEditor::Cursor(line, endColumn))); |
369 | |
370 | // track undo/redo cursor |
371 | if (firstEditInGroup) { |
372 | firstEditInGroup = false; |
373 | undoCursor = KTextEditor::Cursor(line, endColumn); |
374 | } |
375 | redoCursor = KTextEditor::Cursor(line, startColumn); |
376 | |
377 | break; |
378 | } |
379 | default: { |
380 | qCWarning(LOG_KTE) << "Unknown type:" << type; |
381 | } |
382 | } |
383 | } |
384 | |
385 | // balanced editStart and editEnd? |
386 | if (editRunning) { |
387 | brokenSwapFile = true; |
388 | m_document->editEnd(); |
389 | } |
390 | |
391 | // warn the user if the swap file is not complete |
392 | if (brokenSwapFile) { |
393 | qCWarning(LOG_KTE) << "Some data might be lost" ; |
394 | } else { |
395 | // set sane final cursor, if possible |
396 | KTextEditor::View *view = m_document->activeView(); |
397 | redoCursor = m_document->undoManager()->lastRedoCursor(); |
398 | if (view && redoCursor.isValid()) { |
399 | view->setCursorPosition(redoCursor); |
400 | } |
401 | } |
402 | |
403 | // reconnect the signals |
404 | setTrackingEnabled(true); |
405 | |
406 | return true; |
407 | } |
408 | |
409 | void SwapFile::fileSaved(const QString &) |
410 | { |
411 | // remove old swap file (e.g. if a file A was "saved as" B) |
412 | removeSwapFile(); |
413 | |
414 | // set the name for the new swap file |
415 | updateFileName(); |
416 | } |
417 | |
418 | void SwapFile::startEditing() |
419 | { |
420 | // no swap file, no work |
421 | if (m_swapfile.fileName().isEmpty()) { |
422 | return; |
423 | } |
424 | |
425 | // if swap file doesn't exists, open it in WriteOnly mode |
426 | // if it does, append the data to the existing swap file, |
427 | // in case you recover and start editing again |
428 | if (!m_swapfile.exists()) { |
429 | // create path if not there |
430 | if (KateDocumentConfig::global()->swapFileMode() == KateDocumentConfig::SwapFilePresetDirectory |
431 | && !QDir(KateDocumentConfig::global()->swapDirectory()).exists()) { |
432 | QDir().mkpath(dirPath: KateDocumentConfig::global()->swapDirectory()); |
433 | } |
434 | |
435 | m_swapfile.open(flags: QIODevice::WriteOnly); |
436 | m_swapfile.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner); |
437 | m_stream.setDevice(&m_swapfile); |
438 | |
439 | // write file header |
440 | m_stream << QByteArray(swapFileVersionString); |
441 | |
442 | // write checksum |
443 | m_stream << m_document->checksum(); |
444 | } else if (m_stream.device() == nullptr) { |
445 | m_swapfile.open(flags: QIODevice::Append); |
446 | m_swapfile.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner); |
447 | m_stream.setDevice(&m_swapfile); |
448 | } |
449 | |
450 | // format: qint8 |
451 | m_stream << EA_StartEditing; |
452 | m_needSync = true; |
453 | } |
454 | |
455 | void SwapFile::finishEditing() |
456 | { |
457 | // skip if not open |
458 | if (!m_swapfile.isOpen()) { |
459 | return; |
460 | } |
461 | |
462 | // write the file to the disk every 15 seconds (default) |
463 | // skip this if we disabled that |
464 | if (m_document->config()->swapSyncInterval() != 0 && !syncTimer()->isActive()) { |
465 | // important: we store the interval as seconds, start wants milliseconds! |
466 | syncTimer()->start(msec: m_document->config()->swapSyncInterval() * 1000); |
467 | } |
468 | |
469 | // format: qint8 |
470 | m_stream << EA_FinishEditing; |
471 | m_needSync = true; |
472 | } |
473 | |
474 | void SwapFile::wrapLine(KTextEditor::Document *, const KTextEditor::Cursor position) |
475 | { |
476 | // skip if not open |
477 | if (!m_swapfile.isOpen()) { |
478 | return; |
479 | } |
480 | |
481 | // format: qint8, int, int |
482 | m_stream << EA_WrapLine << position.line() << position.column(); |
483 | m_needSync = true; |
484 | } |
485 | |
486 | void SwapFile::unwrapLine(KTextEditor::Document *, int line) |
487 | { |
488 | // skip if not open |
489 | if (!m_swapfile.isOpen()) { |
490 | return; |
491 | } |
492 | |
493 | // format: qint8, int |
494 | m_stream << EA_UnwrapLine << line; |
495 | m_needSync = true; |
496 | } |
497 | |
498 | void SwapFile::insertText(KTextEditor::Document *, const KTextEditor::Cursor position, const QString &text) |
499 | { |
500 | // skip if not open |
501 | if (!m_swapfile.isOpen()) { |
502 | return; |
503 | } |
504 | |
505 | // format: qint8, int, int, bytearray |
506 | m_stream << EA_InsertText << position.line() << position.column() << text.toUtf8(); |
507 | m_needSync = true; |
508 | } |
509 | |
510 | void SwapFile::removeText(KTextEditor::Document *, KTextEditor::Range range, const QString &) |
511 | { |
512 | // skip if not open |
513 | if (!m_swapfile.isOpen()) { |
514 | return; |
515 | } |
516 | |
517 | // format: qint8, int, int, int |
518 | Q_ASSERT(range.start().line() == range.end().line()); |
519 | m_stream << EA_RemoveText << range.start().line() << range.start().column() << range.end().column(); |
520 | m_needSync = true; |
521 | } |
522 | |
523 | bool SwapFile::shouldRecover() const |
524 | { |
525 | // should not recover if the file has already recovered in another view |
526 | if (m_recovered) { |
527 | return false; |
528 | } |
529 | |
530 | return !m_swapfile.fileName().isEmpty() && m_swapfile.exists() && m_stream.device() == nullptr; |
531 | } |
532 | |
533 | void SwapFile::discard() |
534 | { |
535 | m_document->setReadWrite(true); |
536 | removeSwapFile(); |
537 | |
538 | // discard can also be called through the KTE::RecoveryInterface. |
539 | // Make sure, the message is hidden in this case as well. |
540 | if (m_swapMessage) { |
541 | m_swapMessage->deleteLater(); |
542 | } |
543 | } |
544 | |
545 | void SwapFile::removeSwapFile() |
546 | { |
547 | // ensure we have no stray sync |
548 | m_needSync = false; |
549 | |
550 | if (!m_swapfile.fileName().isEmpty() && m_swapfile.exists()) { |
551 | m_stream.setDevice(nullptr); |
552 | m_swapfile.close(); |
553 | m_swapfile.remove(); |
554 | } |
555 | } |
556 | |
557 | bool SwapFile::updateFileName() |
558 | { |
559 | // first clear filename |
560 | m_swapfile.setFileName(QString()); |
561 | |
562 | // get the new path |
563 | QString path = fileName(); |
564 | if (path.isNull()) { |
565 | return false; |
566 | } |
567 | |
568 | m_swapfile.setFileName(path); |
569 | return true; |
570 | } |
571 | |
572 | QString SwapFile::fileName() |
573 | { |
574 | const QUrl &url = m_document->url(); |
575 | if (url.isEmpty() || !url.isLocalFile()) { |
576 | return QString(); |
577 | } |
578 | |
579 | const QString fullLocalPath(url.toLocalFile()); |
580 | QString path; |
581 | if (KateDocumentConfig::global()->swapFileMode() == KateDocumentConfig::SwapFilePresetDirectory) { |
582 | path = KateDocumentConfig::global()->swapDirectory(); |
583 | path.append(c: QLatin1Char('/')); |
584 | |
585 | // append the sha1 sum of the full path + filename, to avoid "too long" paths created |
586 | path.append(s: QString::fromLatin1(ba: QCryptographicHash::hash(data: fullLocalPath.toUtf8(), method: QCryptographicHash::Sha1).toHex())); |
587 | path.append(c: QLatin1Char('-')); |
588 | path.append(s: QFileInfo(fullLocalPath).fileName()); |
589 | |
590 | path.append(s: QLatin1String(".kate-swp" )); |
591 | } else { |
592 | path = fullLocalPath; |
593 | int poz = path.lastIndexOf(c: QLatin1Char('/')); |
594 | path.insert(i: poz + 1, c: QLatin1Char('.')); |
595 | path.append(s: QLatin1String(".kate-swp" )); |
596 | } |
597 | |
598 | return path; |
599 | } |
600 | |
601 | QTimer *SwapFile::syncTimer() |
602 | { |
603 | if (s_timer == nullptr) { |
604 | s_timer = new QTimer(QApplication::instance()); |
605 | s_timer->setSingleShot(true); |
606 | } |
607 | |
608 | return s_timer; |
609 | } |
610 | |
611 | void SwapFile::writeFileToDisk() |
612 | { |
613 | if (m_needSync) { |
614 | m_needSync = false; |
615 | |
616 | // ensure buffers are flushed first |
617 | m_swapfile.flush(); |
618 | |
619 | #ifndef Q_OS_WIN |
620 | // ensure that the file is written to disk |
621 | #if HAVE_FDATASYNC |
622 | fdatasync(fildes: m_swapfile.handle()); |
623 | #else |
624 | fsync(m_swapfile.handle()); |
625 | #endif |
626 | #else |
627 | _commit(m_swapfile.handle()); |
628 | #endif |
629 | } |
630 | } |
631 | |
632 | void SwapFile::showSwapFileMessage() |
633 | { |
634 | m_swapMessage = new KTextEditor::Message(i18n("The file was not closed properly." ), KTextEditor::Message::Warning); |
635 | m_swapMessage->setWordWrap(true); |
636 | |
637 | QAction *diffAction = new QAction(QIcon::fromTheme(QStringLiteral("split" )), i18n("View Changes" ), nullptr); |
638 | QAction *recoverAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-redo" )), i18n("Recover Data" ), nullptr); |
639 | QAction *discardAction = new QAction(KStandardGuiItem::discard().icon(), i18n("Discard" ), nullptr); |
640 | |
641 | m_swapMessage->addAction(action: diffAction, closeOnTrigger: false); |
642 | m_swapMessage->addAction(action: recoverAction); |
643 | m_swapMessage->addAction(action: discardAction); |
644 | |
645 | connect(sender: diffAction, signal: &QAction::triggered, context: this, slot: &SwapFile::showDiff); |
646 | connect(sender: recoverAction, signal: &QAction::triggered, context: this, slot: qOverload<>(&Kate::SwapFile::recover), type: Qt::QueuedConnection); |
647 | connect(sender: discardAction, signal: &QAction::triggered, context: this, slot: &SwapFile::discard, type: Qt::QueuedConnection); |
648 | |
649 | m_document->postMessage(message: m_swapMessage); |
650 | } |
651 | |
652 | void SwapFile::showDiff() |
653 | { |
654 | // the diff creator deletes itself through deleteLater() when it's done |
655 | SwapDiffCreator *diffCreator = new SwapDiffCreator(this); |
656 | diffCreator->viewDiff(); |
657 | } |
658 | |
659 | } |
660 | |