]> git.lyx.org Git - lyx.git/blob - src/frontends/qt/GuiClipboard.cpp
Do not forceLTR in InsetHyperlink (#12044)
[lyx.git] / src / frontends / qt / GuiClipboard.cpp
1 // -*- C++ -*-
2 /**
3  * \file qt/GuiClipboard.cpp
4  * This file is part of LyX, the document processor.
5  * Licence details can be found in the file COPYING.
6  *
7  * \author John Levon
8  * \author Abdelrazak Younes
9  *
10  * Full author contact details are available in file CREDITS.
11  */
12
13 #include <config.h>
14
15 #include "GuiClipboard.h"
16
17 #include "Buffer.h"
18 #include "BufferView.h"
19 #include "Cursor.h"
20 #include "FileDialog.h"
21 #include "qt_helpers.h"
22
23 #include "support/lassert.h"
24 #include "support/convert.h"
25 #include "support/debug.h"
26 #include "support/FileName.h"
27 #include "support/filetools.h"
28 #include "support/gettext.h"
29 #include "support/lstrings.h"
30 #include "support/lyxtime.h"
31
32 #ifdef Q_OS_MAC
33 #include "support/linkback/LinkBackProxy.h"
34 #endif // Q_OS_MAC
35
36 #include "frontends/alert.h"
37
38 #include "support/checksum.h"
39
40 #include <QApplication>
41 #include <QBuffer>
42 #include <QClipboard>
43 #include <QDataStream>
44 #include <QFile>
45 #include <QImage>
46 #include <QMimeData>
47 #include <QString>
48 #include <QStringList>
49 #include <QTextDocument>
50 #include <QTimer>
51
52 #include <memory>
53 #include <map>
54 #include <iostream>
55
56 using namespace std;
57 using namespace lyx::support;
58
59
60 namespace lyx {
61
62 namespace frontend {
63
64 static QMimeData const * read_clipboard()
65 {
66         LYXERR(Debug::CLIPBOARD, "Getting Clipboard");
67         QMimeData const * source =
68                 qApp->clipboard()->mimeData(QClipboard::Clipboard);
69         if (!source) {
70                 LYXERR0("0 bytes (no QMimeData)");
71                 return new QMimeData;
72         }
73         // It appears that doing IO between getting a mimeData object
74         // and using it can cause a crash (maybe Qt used IO
75         // as an excuse to free() it? Anyway let's not introduce
76         // any new IO here, so e.g. leave the following line commented.
77         // lyxerr << "Got Clipboard (" << (long) source << ")\n" ;
78         return source;
79 }
80
81
82 void CacheMimeData::update()
83 {
84         time_t const start_time = current_time();
85         LYXERR(Debug::CLIPBOARD, "Creating CacheMimeData object");
86         cached_formats_ = read_clipboard()->formats();
87
88         // Qt times out after 5 seconds if it does not receive a response.
89         if (current_time() - start_time > 3) {
90                 LYXERR0("No timely response from clipboard, perhaps process "
91                         << "holding clipboard is frozen?");
92         }
93 }
94
95
96 QByteArray CacheMimeData::data(QString const & mimeType) const
97 {
98         return read_clipboard()->data(mimeType);
99 }
100
101
102 QString const lyxMimeType(){ return "application/x-lyx"; }
103 QString const texMimeType(){ return "text/x-tex"; }
104 QString const latexMimeType(){ return "application/x-latex"; }
105 QString const pdfMimeType(){ return "application/pdf"; }
106 QString const emfMimeType(){ return "image/x-emf"; }
107 QString const wmfMimeType(){ return "image/x-wmf"; }
108
109
110 GuiClipboard::GuiClipboard()
111 {
112         connect(qApp->clipboard(), SIGNAL(dataChanged()),
113                 this, SLOT(on_dataChanged()));
114         // initialize clipboard status.
115         update();
116 }
117
118
119 string const GuiClipboard::getAsLyX() const
120 {
121         LYXERR(Debug::CLIPBOARD, "GuiClipboard::getAsLyX(): `");
122         // We don't convert encodings here since the encoding of the
123         // clipboard contents is specified in the data itself
124         if (cache_.hasFormat(lyxMimeType())) {
125                 // data from ourself or some other LyX instance
126                 QByteArray const ar = cache_.data(lyxMimeType());
127                 string const s(ar.data(), ar.count());
128                 LYXERR(Debug::CLIPBOARD, s << "'");
129                 return s;
130         }
131         LYXERR(Debug::CLIPBOARD, "'");
132         return string();
133 }
134
135
136 FileName GuiClipboard::getPastedGraphicsFileName(Cursor const & cur,
137         Clipboard::GraphicsType & type) const
138 {
139         // create file dialog filter according to the existing types in the clipboard
140         vector<Clipboard::GraphicsType> types;
141         if (hasGraphicsContents(Clipboard::EmfGraphicsType))
142                 types.push_back(Clipboard::EmfGraphicsType);
143         if (hasGraphicsContents(Clipboard::WmfGraphicsType))
144                 types.push_back(Clipboard::WmfGraphicsType);
145         if (hasGraphicsContents(Clipboard::LinkBackGraphicsType))
146                 types.push_back(Clipboard::LinkBackGraphicsType);
147         if (hasGraphicsContents(Clipboard::PdfGraphicsType))
148                 types.push_back(Clipboard::PdfGraphicsType);
149         if (hasGraphicsContents(Clipboard::PngGraphicsType))
150                 types.push_back(Clipboard::PngGraphicsType);
151         if (hasGraphicsContents(Clipboard::JpegGraphicsType))
152                 types.push_back(Clipboard::JpegGraphicsType);
153
154         LASSERT(!types.empty(), return FileName());
155
156         // select preferred type if AnyGraphicsType was passed
157         if (type == Clipboard::AnyGraphicsType)
158                 type = types.front();
159
160         // which extension?
161         map<Clipboard::GraphicsType, string> extensions;
162         map<Clipboard::GraphicsType, docstring> typeNames;
163
164         extensions[Clipboard::EmfGraphicsType] = "emf";
165         extensions[Clipboard::WmfGraphicsType] = "wmf";
166         extensions[Clipboard::LinkBackGraphicsType] = "linkback";
167         extensions[Clipboard::PdfGraphicsType] = "pdf";
168         extensions[Clipboard::PngGraphicsType] = "png";
169         extensions[Clipboard::JpegGraphicsType] = "jpeg";
170
171         typeNames[Clipboard::EmfGraphicsType] = _("Enhanced Metafile");
172         typeNames[Clipboard::WmfGraphicsType] = _("Windows Metafile");
173         typeNames[Clipboard::LinkBackGraphicsType] = _("LinkBack PDF");
174         typeNames[Clipboard::PdfGraphicsType] = _("PDF");
175         typeNames[Clipboard::PngGraphicsType] = _("PNG");
176         typeNames[Clipboard::JpegGraphicsType] = _("JPEG");
177
178         // find unused filename with primary extension
179         string document_path = cur.buffer()->fileName().onlyPath().absFileName();
180         unsigned newfile_number = 0;
181         FileName filename;
182         do {
183                 ++newfile_number;
184                 filename = FileName(addName(document_path,
185                         to_utf8(_("pasted"))
186                         + convert<string>(newfile_number) + "."
187                         + extensions[type]));
188         } while (filename.isReadableFile());
189
190         while (true) {
191                 // create file type filter, putting the preferred on to the front
192                 QStringList filter;
193                 for (size_t i = 0; i != types.size(); ++i) {
194                         docstring s = bformat(_("%1$s Files"), typeNames[types[i]])
195                                 + " (*." + from_ascii(extensions[types[i]]) + ")";
196                         if (types[i] == type)
197                                 filter.prepend(toqstr(s));
198                         else
199                                 filter.append(toqstr(s));
200                 }
201                 filter = fileFilters(filter.join(";;"));
202
203                 // show save dialog for the graphic
204                 FileDialog dlg(qt_("Choose a filename to save the pasted graphic as"));
205                 FileDialog::Result result =
206                 dlg.save(toqstr(filename.onlyPath().absFileName()), filter,
207                          toqstr(filename.onlyFileName()));
208
209                 if (result.first == FileDialog::Later)
210                         return FileName();
211
212                 string newFilename = fromqstr(result.second);
213                 if (newFilename.empty()) {
214                         cur.bv().message(_("Canceled."));
215                         return FileName();
216                 }
217                 filename.set(newFilename);
218
219                 // check the extension (the user could have changed it)
220                 if (!suffixIs(ascii_lowercase(filename.absFileName()),
221                               "." + extensions[type])) {
222                         // the user changed the extension. Check if the type is available
223                         size_t i;
224                         for (i = 1; i != types.size(); ++i) {
225                                 if (suffixIs(ascii_lowercase(filename.absFileName()),
226                                              "." + extensions[types[i]])) {
227                                         type = types[i];
228                                         break;
229                                 }
230                         }
231
232                         // invalid extension found, or none at all. In the latter
233                         // case set the default extensions.
234                         if (i == types.size()
235                             && filename.onlyFileName().find('.') == string::npos) {
236                                 filename.changeExtension("." + extensions[type]);
237                         }
238                 }
239
240                 // check whether the file exists and warn the user
241                 if (!filename.exists())
242                         break;
243                 int ret = frontend::Alert::prompt(
244                         _("Overwrite external file?"),
245                         bformat(_("File %1$s already exists, do you want to overwrite it?"),
246                         from_utf8(filename.absFileName())), 1, 1, _("&Overwrite"), _("&Cancel"));
247                 if (ret == 0)
248                         // overwrite, hence break the dialog loop
249                         break;
250
251                 // not overwrite, hence show the dialog again (i.e. loop)
252         }
253
254         return filename;
255 }
256
257
258 FileName GuiClipboard::getAsGraphics(Cursor const & cur, GraphicsType type) const
259 {
260         // get the filename from the user
261         FileName filename = getPastedGraphicsFileName(cur, type);
262         if (filename.empty())
263                 return FileName();
264
265         // handle image cases first
266         if (type == PngGraphicsType || type == JpegGraphicsType) {
267                 // get image from QImage from clipboard
268                 QImage image = qApp->clipboard()->image();
269                 if (image.isNull()) {
270                         LYXERR(Debug::CLIPBOARD, "No image in clipboard");
271                         return FileName();
272                 }
273
274                 // convert into graphics format
275                 QByteArray ar;
276                 QBuffer buffer(&ar);
277                 buffer.open(QIODevice::WriteOnly);
278                 if (type == PngGraphicsType)
279                         image.save(toqstr(filename.absFileName()), "PNG");
280                 else if (type == JpegGraphicsType)
281                         image.save(toqstr(filename.absFileName()), "JPEG");
282                 else
283                         LATTEST(false);
284
285                 return filename;
286         }
287
288         // get mime for type
289         QString mime;
290         switch (type) {
291         case PdfGraphicsType: mime = pdfMimeType(); break;
292         case LinkBackGraphicsType: mime = pdfMimeType(); break;
293         case EmfGraphicsType: mime = emfMimeType(); break;
294         case WmfGraphicsType: mime = wmfMimeType(); break;
295         default: LASSERT(false, return FileName());
296         }
297
298         // get data
299         if (!cache_.hasFormat(mime))
300                 return FileName();
301         // data from ourself or some other LyX instance
302         QByteArray const ar = cache_.data(mime);
303         LYXERR(Debug::CLIPBOARD, "Getting from clipboard: mime = " << mime.constData()
304                << "length = " << ar.count());
305
306         QFile f(toqstr(filename.absFileName()));
307         if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
308                 LYXERR(Debug::CLIPBOARD, "Error opening file "
309                        << filename.absFileName() << " for writing");
310                 return FileName();
311         }
312
313         // write the (LinkBack) PDF data
314         f.write(ar);
315         if (type == LinkBackGraphicsType) {
316 #ifdef Q_OS_MAC
317                 void const * linkBackData;
318                 unsigned linkBackLen;
319                 getLinkBackData(&linkBackData, &linkBackLen);
320                 f.write((char *)linkBackData, linkBackLen);
321                 quint32 pdfLen = ar.size();
322                 QDataStream ds(&f);
323                 ds << pdfLen; // big endian by default
324 #else
325                 // only non-Mac this should never happen
326                 LATTEST(false);
327 #endif // Q_OS_MAC
328         }
329
330         f.close();
331         return filename;
332 }
333
334
335 namespace {
336 /**
337  * Tidy up a HTML chunk coming from the clipboard.
338  * This is needed since different applications put different kinds of HTML
339  * on the clipboard:
340  * - With or without the <?xml> tag
341  * - With or without the <!DOCTYPE> tag
342  * - With or without the <html> tag
343  * - With or without the <body> tag
344  * - With or without the <p> tag
345  * Since we are going to write a HTML file for external converters we need
346  * to ensure that it is a well formed HTML file, including all the mentioned tags.
347  */
348 QString tidyHtml(QString const & input)
349 {
350         // Misuse QTextDocument to cleanup the HTML.
351         // As a side effect, all visual markup like <tt> is converted to CSS,
352         // which is ignored by gnuhtml2latex.
353         // While this may be seen as a bug by some people it is actually a
354         // good thing, since we do import structure, but ignore all visual
355         // clutter.
356         QTextDocument converter;
357         converter.setHtml(input);
358         return converter.toHtml("utf-8");
359 }
360 } // namespace
361
362
363 docstring const GuiClipboard::getAsText(TextType type) const
364 {
365         // text data from other applications
366         if ((type == AnyTextType || type == LyXOrPlainTextType) && hasTextContents(LyXTextType))
367                 type = LyXTextType;
368         if (type == AnyTextType && hasTextContents(LaTeXTextType))
369                 type = LaTeXTextType;
370         if (type == AnyTextType && hasTextContents(HtmlTextType))
371                 type = HtmlTextType;
372         QString str;
373         switch (type) {
374         case LyXTextType:
375                 // must not convert to docstring, since file can contain
376                 // mixed encodings (use getAsLyX() instead)
377                 break;
378         case AnyTextType:
379         case LyXOrPlainTextType:
380         case PlainTextType:
381                 str = qApp->clipboard()->text(QClipboard::Clipboard)
382                                 .normalized(QString::NormalizationForm_C);
383                 break;
384         case LaTeXTextType: {
385                 QMimeData const * source =
386                         qApp->clipboard()->mimeData(QClipboard::Clipboard);
387                 if (source) {
388                         // First try LaTeX, then TeX (we do not distinguish
389                         // for clipboard purposes)
390                         if (source->hasFormat(latexMimeType())) {
391                                 str = source->data(latexMimeType());
392                                 str = str.normalized(QString::NormalizationForm_C);
393                         } else if (source->hasFormat(texMimeType())) {
394                                 str = source->data(texMimeType());
395                                 str = str.normalized(QString::NormalizationForm_C);
396                         }
397                 }
398                 break;
399         }
400         case HtmlTextType: {
401                 QString subtype = "html";
402                 str = qApp->clipboard()->text(subtype, QClipboard::Clipboard)
403                                 .normalized(QString::NormalizationForm_C);
404                 str = tidyHtml(str);
405                 break;
406         }
407         }
408         LYXERR(Debug::CLIPBOARD, "GuiClipboard::getAsText(" << type << "): `" << str << "'");
409         if (str.isNull())
410                 return docstring();
411
412         return internalLineEnding(str);
413 }
414
415
416 void GuiClipboard::put(string const & text) const
417 {
418         qApp->clipboard()->setText(toqstr(text));
419 }
420
421
422 void GuiClipboard::put(string const & lyx, docstring const & html, docstring const & text)
423 {
424         LYXERR(Debug::CLIPBOARD, "GuiClipboard::put(`" << lyx << "' `"
425                               << to_utf8(html) << "' `" << to_utf8(text) << "')");
426         // We don't convert the encoding of lyx since the encoding of the
427         // clipboard contents is specified in the data itself
428         QMimeData * data = new QMimeData;
429         if (!lyx.empty()) {
430                 QByteArray const qlyx(lyx.c_str(), lyx.size());
431                 data->setData(lyxMimeType(), qlyx);
432                 // If the OS has not the concept of clipboard ownership,
433                 // we recognize internal data through its checksum.
434                 if (!hasInternal())
435                         checksum = support::checksum(lyx);
436         }
437         // Don't test for text.empty() since we want to be able to clear the
438         // clipboard.
439         QString const qtext = toqstr(text);
440         data->setText(qtext);
441         QString const qhtml = toqstr(html);
442         data->setHtml(qhtml);
443         qApp->clipboard()->setMimeData(data, QClipboard::Clipboard);
444 }
445
446
447 bool GuiClipboard::hasTextContents(Clipboard::TextType type) const
448 {
449         switch (type) {
450         case AnyTextType:
451                 return cache_.hasFormat(lyxMimeType()) || cache_.hasText() ||
452                        cache_.hasHtml() || cache_.hasFormat(latexMimeType()) ||
453                        cache_.hasFormat(texMimeType());
454         case LyXOrPlainTextType:
455                 return cache_.hasFormat(lyxMimeType()) || cache_.hasText();
456         case LyXTextType:
457                 return cache_.hasFormat(lyxMimeType());
458         case PlainTextType:
459                 return cache_.hasText();
460         case HtmlTextType:
461                 return cache_.hasHtml();
462         case LaTeXTextType:
463                 return cache_.hasFormat(latexMimeType()) ||
464                        cache_.hasFormat(texMimeType());
465         }
466         // shut up compiler
467         return false;
468 }
469
470
471 bool GuiClipboard::hasGraphicsContents(Clipboard::GraphicsType type) const
472 {
473         if (type == AnyGraphicsType) {
474                 return hasGraphicsContents(PdfGraphicsType)
475                         || hasGraphicsContents(PngGraphicsType)
476                         || hasGraphicsContents(JpegGraphicsType)
477                         || hasGraphicsContents(EmfGraphicsType)
478                         || hasGraphicsContents(WmfGraphicsType)
479                         || hasGraphicsContents(LinkBackGraphicsType);
480         }
481
482         // handle image cases first
483         if (type == PngGraphicsType || type == JpegGraphicsType)
484                 return cache_.hasImage();
485
486         // handle LinkBack for Mac
487         if (type == LinkBackGraphicsType)
488 #ifdef Q_OS_MAC
489                 return isLinkBackDataInPasteboard();
490 #else
491                 return false;
492 #endif // Q_OS_MAC
493
494         // get mime data
495         QStringList const & formats = cache_.formats();
496         LYXERR(Debug::CLIPBOARD, "We found " << formats.size() << " formats");
497         for (int i = 0; i < formats.size(); ++i)
498                 LYXERR(Debug::CLIPBOARD, "Found format " << formats[i]);
499
500         // compute mime for type
501         QString mime;
502         switch (type) {
503         case EmfGraphicsType: mime = emfMimeType(); break;
504         case WmfGraphicsType: mime = wmfMimeType(); break;
505         case PdfGraphicsType: mime = pdfMimeType(); break;
506         default: LASSERT(false, return false);
507         }
508
509         return cache_.hasFormat(mime);
510 }
511
512
513 bool GuiClipboard::isInternal() const
514 {
515         if (!hasTextContents(LyXTextType))
516                 return false;
517
518         // ownsClipboard() is also true for stuff coming from dialogs, e.g.
519         // the preamble dialog. This does only work on X11 and Windows, since
520         // ownsClipboard() is hardwired to return false on OS X.
521         if (hasInternal())
522                 return qApp->clipboard()->ownsClipboard();
523
524         // We are running on OS X: Check whether clipboard data is from
525         // ourself by comparing its checksum with the stored one.
526         QByteArray const ar = cache_.data(lyxMimeType());
527         string const data(ar.data(), ar.count());
528         return checksum == static_cast<std::uint32_t>(support::checksum(data));
529 }
530
531
532 bool GuiClipboard::hasInternal() const
533 {
534         // Windows and Mac OS X does not have the concept of ownership;
535         // the clipboard is a fully global resource so all applications
536         // are notified of changes. However, on Windows ownership is
537         // emulated by Qt through the OleIsCurrentClipboard() API, while
538         // on Mac OS X we deal with this issue by ourself.
539 #ifndef Q_OS_MAC
540         return true;
541 #else
542         return false;
543 #endif
544 }
545
546
547 void GuiClipboard::on_dataChanged()
548 {
549         update();
550 #if defined(Q_OS_WIN) || defined(Q_CYGWIN_WIN)
551         // Retry on Windows (#10109)
552         if (cache_.formats().count() == 0) {
553                 QTimer::singleShot(100, this, SLOT(update()));
554         }
555 #endif
556 }
557
558 void GuiClipboard::update()
559 {
560         //Note: we do not really need to run cache_.update() unless the
561         //data has been changed *and* the GuiClipboard has been queried.
562         //However if run cache_.update() the moment a process grabs the
563         //clipboard, the process holding the clipboard presumably won't
564         //yet be frozen, and so we won't need to wait 5 seconds for Qt
565         //to time-out waiting for the clipboard.
566         cache_.update();
567         QStringList l = cache_.formats();
568         LYXERR(Debug::CLIPBOARD, "Qt Clipboard changed. We found the following mime types:");
569         for (int i = 0; i < l.count(); i++)
570                 LYXERR(Debug::CLIPBOARD, l.value(i));
571
572         plaintext_clipboard_empty_ = qApp->clipboard()->
573                 text(QClipboard::Clipboard).isEmpty();
574
575         has_text_contents_ = hasTextContents();
576         has_graphics_contents_ = hasGraphicsContents();
577 }
578
579
580 bool GuiClipboard::empty() const
581 {
582         // We need to check both the plaintext and the LyX version of the
583         // clipboard. The plaintext version is empty if the LyX version
584         // contains only one inset, and the LyX version is empty if the
585         // clipboard does not come from LyX.
586         if (!plaintext_clipboard_empty_)
587                 return false;
588         return !has_text_contents_ && !has_graphics_contents_;
589 }
590
591 } // namespace frontend
592 } // namespace lyx
593
594 #include "moc_GuiClipboard.cpp"