]> git.lyx.org Git - lyx.git/blob - src/frontends/qt4/GuiClipboard.cpp
On Mac, moving down a paragraph should place the cursor at the end of the current...
[lyx.git] / src / frontends / qt4 / GuiClipboard.cpp
1 // -*- C++ -*-
2 /**
3  * \file qt4/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 "FileDialog.h"
16
17 #include "support/FileName.h"
18 #include "GuiClipboard.h"
19 #include "qt_helpers.h"
20
21 #include "Buffer.h"
22 #include "BufferView.h"
23 #include "Cursor.h"
24
25 #include "support/lassert.h"
26 #include "support/convert.h"
27 #include "support/debug.h"
28 #include "support/filetools.h"
29 #include "support/gettext.h"
30 #include "support/lstrings.h"
31 #include "support/lyxtime.h"
32
33 #ifdef Q_WS_MACX
34 #include "support/linkback/LinkBackProxy.h"
35 #endif // Q_WS_MACX
36
37 #include "frontends/alert.h"
38
39 #include <QApplication>
40 #include <QBuffer>
41 #include <QClipboard>
42 #include <QDataStream>
43 #include <QFile>
44 #include <QImage>
45 #include <QMimeData>
46 #include <QString>
47 #include <QStringList>
48 #include <QTextDocument>
49
50 #include <boost/crc.hpp>
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::ACTION, "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::ACTION, "Creating CacheMimeData object");
86         cached_formats_ = read_clipboard()->formats();
87
88         // Qt times out after 5 seconds if it does not recieve 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         on_dataChanged();
116 }
117
118
119 string const GuiClipboard::getAsLyX() const
120 {
121         LYXERR(Debug::ACTION, "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::ACTION, s << "'");
129                 return s;
130         }
131         LYXERR(Debug::ACTION, "'");
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 prefered 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 prefered 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::ACTION, "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::ACTION, "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::ACTION, "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_WS_MACX
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_WS_MACX
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 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 }
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::ACTION, "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 & lyx, docstring const & html, docstring const & text)
417 {
418         LYXERR(Debug::ACTION, "GuiClipboard::put(`" << lyx << "' `"
419                               << to_utf8(html) << "' `" << to_utf8(text) << "')");
420         // We don't convert the encoding of lyx since the encoding of the
421         // clipboard contents is specified in the data itself
422         QMimeData * data = new QMimeData;
423         if (!lyx.empty()) {
424                 QByteArray const qlyx(lyx.c_str(), lyx.size());
425                 data->setData(lyxMimeType(), qlyx);
426                 // If the OS has not the concept of clipboard ownership,
427                 // we recognize internal data through its checksum.
428                 if (!hasInternal()) {
429                         boost::crc_32_type crc32;
430                         crc32.process_bytes(lyx.c_str(), lyx.size());
431                         checksum = crc32.checksum();
432                 }
433         }
434         // Don't test for text.empty() since we want to be able to clear the
435         // clipboard.
436         QString const qtext = toqstr(text);
437         data->setText(qtext);
438         QString const qhtml = toqstr(html);
439         data->setHtml(qhtml);
440         qApp->clipboard()->setMimeData(data, QClipboard::Clipboard);
441 }
442
443
444 bool GuiClipboard::hasTextContents(Clipboard::TextType type) const
445 {
446         switch (type) {
447         case AnyTextType:
448                 return cache_.hasFormat(lyxMimeType()) || cache_.hasText() ||
449                        cache_.hasHtml() || cache_.hasFormat(latexMimeType()) ||
450                        cache_.hasFormat(texMimeType());
451         case LyXOrPlainTextType:
452                 return cache_.hasFormat(lyxMimeType()) || cache_.hasText();
453         case LyXTextType:
454                 return cache_.hasFormat(lyxMimeType());
455         case PlainTextType:
456                 return cache_.hasText();       
457         case HtmlTextType:
458                 return cache_.hasHtml();
459         case LaTeXTextType:
460                 return cache_.hasFormat(latexMimeType()) ||
461                        cache_.hasFormat(texMimeType());
462         }
463         // shut up compiler
464         return false;
465 }
466
467
468 bool GuiClipboard::hasGraphicsContents(Clipboard::GraphicsType type) const
469 {
470         if (type == AnyGraphicsType) {
471                 return hasGraphicsContents(PdfGraphicsType)
472                         || hasGraphicsContents(PngGraphicsType)
473                         || hasGraphicsContents(JpegGraphicsType)
474                         || hasGraphicsContents(EmfGraphicsType)
475                         || hasGraphicsContents(WmfGraphicsType)
476                         || hasGraphicsContents(LinkBackGraphicsType);
477         }
478
479         // handle image cases first
480         if (type == PngGraphicsType || type == JpegGraphicsType)
481                 return cache_.hasImage();
482
483         // handle LinkBack for Mac
484         if (type == LinkBackGraphicsType)
485 #ifdef Q_WS_MACX
486                 return isLinkBackDataInPasteboard();
487 #else
488                 return false;
489 #endif // Q_WS_MACX
490         
491         // get mime data
492         QStringList const & formats = cache_.formats();
493         LYXERR(Debug::ACTION, "We found " << formats.size() << " formats");
494         for (int i = 0; i < formats.size(); ++i)
495                 LYXERR(Debug::ACTION, "Found format " << formats[i]);
496
497         // compute mime for type
498         QString mime;
499         switch (type) {
500         case EmfGraphicsType: mime = emfMimeType(); break;
501         case WmfGraphicsType: mime = wmfMimeType(); break;
502         case PdfGraphicsType: mime = pdfMimeType(); break;
503         default: LASSERT(false, return false);
504         }
505         
506         return cache_.hasFormat(mime);
507 }
508
509
510 bool GuiClipboard::isInternal() const
511 {
512         if (!hasTextContents(LyXTextType))
513                 return false;
514
515         // ownsClipboard() is also true for stuff coming from dialogs, e.g.
516         // the preamble dialog. This does only work on X11 and Windows, since
517         // ownsClipboard() is hardwired to return false on OS X.
518         if (hasInternal())
519                 return qApp->clipboard()->ownsClipboard();
520
521         // We are running on OS X: Check whether clipboard data is from
522         // ourself by comparing its checksum with the stored one.
523         QByteArray const ar = cache_.data(lyxMimeType());
524         string const data(ar.data(), ar.count());
525         boost::crc_32_type crc32;
526         crc32.process_bytes(data.c_str(), data.size());
527         return checksum == crc32.checksum();
528 }
529
530
531 bool GuiClipboard::hasInternal() const
532 {
533         // Windows and Mac OS X does not have the concept of ownership;
534         // the clipboard is a fully global resource so all applications 
535         // are notified of changes. However, on Windows ownership is
536         // emulated by Qt through the OleIsCurrentClipboard() API, while
537         // on Mac OS X we deal with this issue by ourself.
538 #if (defined(Q_WS_X11) || defined(Q_WS_WIN))
539         return true;
540 #else
541         return false;
542 #endif
543 }
544
545
546 void GuiClipboard::on_dataChanged()
547 {
548         //Note: we do not really need to run cache_.update() unless the
549         //data has been changed *and* the GuiClipboard has been queried.
550         //However if run cache_.update() the moment a process grabs the
551         //clipboard, the process holding the clipboard presumably won't
552         //yet be frozen, and so we won't need to wait 5 seconds for Qt
553         //to time-out waiting for the clipboard.
554         cache_.update();
555         QStringList l = cache_.formats();
556         LYXERR(Debug::ACTION, "Qt Clipboard changed. We found the following mime types:");
557         for (int i = 0; i < l.count(); i++)
558                 LYXERR(Debug::ACTION, l.value(i));
559
560         plaintext_clipboard_empty_ = qApp->clipboard()->
561                 text(QClipboard::Clipboard).isEmpty();
562
563         has_text_contents_ = hasTextContents();
564         has_graphics_contents_ = hasGraphicsContents();
565 }
566
567
568 bool GuiClipboard::empty() const
569 {
570         // We need to check both the plaintext and the LyX version of the
571         // clipboard. The plaintext version is empty if the LyX version
572         // contains only one inset, and the LyX version is empty if the
573         // clipboard does not come from LyX.
574         if (!plaintext_clipboard_empty_)
575                 return false;
576         return !has_text_contents_ && !has_graphics_contents_;
577 }
578
579 } // namespace frontend
580 } // namespace lyx
581
582 #include "moc_GuiClipboard.cpp"