]> git.lyx.org Git - lyx.git/blob - src/frontends/qt4/GuiClipboard.cpp
59aa11f4942ff7bc8a25eab99def70c7c6cebe54
[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 "GuiClipboard.h"
18 #include "qt_helpers.h"
19
20 #include "frontends/alert.h"
21
22 #include "Buffer.h"
23 #include "BufferView.h"
24 #include "Cursor.h"
25
26 #include "support/convert.h"
27 #include "support/debug.h"
28 #include "support/filetools.h"
29 #include "support/FileFilterList.h"
30 #include "support/gettext.h"
31 #include "support/lstrings.h"
32
33 #include <QApplication>
34 #include <QBuffer>
35 #include <QClipboard>
36 #include <QDataStream>
37 #include <QFile>
38 #include <QImage>
39 #include <QMacPasteboardMime>
40 #include <QMimeData>
41 #include <QString>
42 #include <QStringList>
43
44 #ifdef Q_WS_WIN
45 #include <QWindowsMime>
46 #ifdef Q_CYGWIN_WIN
47 #include <wtypes.h>
48 #endif
49 #include <objidl.h>
50 #endif // Q_WS_WIN
51
52 #include "boost/assert.hpp"
53
54 #include <map>
55
56 #ifdef Q_WS_MACX
57 #include "support/linkback/LinkBackProxy.h"
58 #endif // Q_WS_MACX
59
60 using namespace std;
61 using namespace lyx::support;
62
63 static char const * const lyx_mime_type = "application/x-lyx";
64 static char const * const pdf_mime_type = "application/pdf";
65 static char const * const emf_mime_type = "image/x-emf";
66 static char const * const wmf_mime_type = "image/x-wmf";
67
68 namespace lyx {
69
70 namespace frontend {
71
72 #ifdef Q_WS_WIN
73
74 static FORMATETC cfFromMime(QString const & mimetype)
75 {
76         FORMATETC formatetc;
77         if (mimetype == emf_mime_type) {
78                 formatetc.cfFormat = CF_ENHMETAFILE;
79                 formatetc.tymed = TYMED_ENHMF;
80         }
81         else if (mimetype == wmf_mime_type) {
82                 formatetc.cfFormat = CF_METAFILEPICT;
83                 formatetc.tymed = TYMED_MFPICT;
84         }
85         else return formatetc;
86         formatetc.ptd = NULL;
87         formatetc.dwAspect = DVASPECT_CONTENT;
88         formatetc.lindex = -1;
89         return formatetc;
90 }
91
92
93 class QWindowsMimeMetafile : public QWindowsMime {
94 public:
95         bool canConvertFromMime(FORMATETC const & formatetc, QMimeData const * mimedata) const;
96         bool canConvertToMime(QString const & mimetype, IDataObject * pDataObj) const;
97         bool convertFromMime(FORMATETC const & formatetc, const QMimeData * mimedata, STGMEDIUM * pmedium) const;
98         QVariant convertToMime(QString const & mimetype, IDataObject * pDataObj, QVariant::Type preferredType) const;
99         QVector<FORMATETC> formatsForMime(QString const & mimeType, QMimeData const * mimeData) const;
100         QString mimeForFormat(FORMATETC const &) const;
101 };
102
103
104 QString QWindowsMimeMetafile::mimeForFormat(FORMATETC const & formatetc) const
105 {
106         QString f;
107         if (formatetc.cfFormat == CF_ENHMETAFILE)
108                 f = emf_mime_type; 
109         else if (formatetc.cfFormat == CF_METAFILEPICT)
110                 f = wmf_mime_type;
111         return f;
112 }
113
114
115 bool QWindowsMimeMetafile::canConvertFromMime(
116         FORMATETC const & formatetc, QMimeData const * mimedata) const
117 {
118         return false;
119 }
120
121
122 bool QWindowsMimeMetafile::canConvertToMime(
123         QString const & mimetype, IDataObject * pDataObj) const
124 {
125         FORMATETC formatetc = cfFromMime(mimetype);
126         return pDataObj->QueryGetData(&formatetc) == S_OK;
127 }
128
129
130 bool QWindowsMimeMetafile::convertFromMime(
131         FORMATETC const & formatetc, QMimeData const * mimedata, 
132         STGMEDIUM * pmedium) const
133 {
134         return false;
135 }
136
137
138 QVariant QWindowsMimeMetafile::convertToMime(QString const & mimetype,
139         IDataObject * pDataObj, QVariant::Type preferredType) const
140 {
141         QByteArray data;
142         if (!canConvertToMime(mimetype, pDataObj))
143                 return data;
144
145         FORMATETC formatetc = cfFromMime(mimetype);
146         STGMEDIUM s;
147         if (pDataObj->GetData(&formatetc, &s) != S_OK)
148                 return data;
149
150         int dataSize;
151         if (s.tymed == TYMED_ENHMF) {
152                 dataSize = GetEnhMetaFileBits(s.hEnhMetaFile, 0, NULL);
153                 data.resize(dataSize);
154                 dataSize = GetEnhMetaFileBits(s.hEnhMetaFile, dataSize, (LPBYTE)data.data());
155         } else if (s.tymed == TYMED_MFPICT) {
156                 dataSize = GetMetaFileBitsEx((HMETAFILE)s.hMetaFilePict, 0, NULL);
157                 data.resize(dataSize);
158                 dataSize = GetMetaFileBitsEx((HMETAFILE)s.hMetaFilePict, dataSize, (LPBYTE)data.data());
159         }
160         data.detach();
161         ReleaseStgMedium(&s);
162
163         return data;
164 }
165
166
167 QVector<FORMATETC> QWindowsMimeMetafile::formatsForMime(
168         QString const & mimetype, QMimeData const * mimedata) const
169 {
170         QVector<FORMATETC> formats;
171         formats += cfFromMime(mimetype);
172         return formats;
173 }
174
175 static QWindowsMimeMetafile * metafileWindowsMime;
176
177 #endif // Q_WS_WIN
178
179 #ifdef Q_WS_MACX
180
181 class QMacPasteboardMimeGraphics : public QMacPasteboardMime {
182 public:
183         QMacPasteboardMimeGraphics()
184                 : QMacPasteboardMime(MIME_QT_CONVERTOR|MIME_ALL)
185         {}
186         ~QMacPasteboardMimeGraphics() {}
187         QString convertorName();
188         QString flavorFor(const QString & mime);
189         QString mimeFor(QString flav);
190         bool canConvert(const QString & mime, QString flav);
191         QVariant convertToMime(const QString & mime, QList<QByteArray> data, QString flav);
192         QList<QByteArray> convertFromMime(const QString &mime, QVariant data, QString flav);
193 };
194
195
196 QString QMacPasteboardMimeGraphics::convertorName()
197 {
198         return "Graphics";
199 }
200
201
202 QString QMacPasteboardMimeGraphics::flavorFor(const QString & mime)
203 {
204         LYXERR(Debug::ACTION, "flavorFor " << fromqstr(mime));
205         if (mime == QLatin1String(pdf_mime_type))
206                 return QLatin1String("com.adobe.pdf");
207         return QString();
208 }
209
210
211 QString QMacPasteboardMimeGraphics::mimeFor(QString flav)
212 {
213         LYXERR(Debug::ACTION, "mimeFor " << fromqstr(flav));
214         if (flav == QLatin1String("com.adobe.pdf"))
215                 return QLatin1String(pdf_mime_type);
216         return QString();
217 }
218
219
220 bool QMacPasteboardMimeGraphics::canConvert(const QString & mime, QString flav)
221 {
222         return mimeFor(flav) == mime;
223 }
224
225
226 QVariant QMacPasteboardMimeGraphics::convertToMime(const QString & mime, QList<QByteArray> data, QString)
227 {
228         if(data.count() > 1)
229                 qWarning("QMacPasteboardMimeGraphics: Cannot handle multiple member data");
230         return data.first();
231 }
232
233
234 QList<QByteArray> QMacPasteboardMimeGraphics::convertFromMime(const QString &mime, QVariant data, QString)
235 {
236         QList<QByteArray> ret;
237         ret.append(data.toByteArray());
238         return ret;
239 }
240
241 static QMacPasteboardMimeGraphics * graphicsPasteboardMime;
242
243 #endif // Q_WS_MACX
244
245
246 GuiClipboard::GuiClipboard()
247 {
248         connect(qApp->clipboard(), SIGNAL(dataChanged()),
249                 this, SLOT(on_dataChanged()));
250         // initialize clipboard status.
251         on_dataChanged();
252         
253 #ifdef Q_WS_MACX
254         if (!graphicsPasteboardMime)
255                 graphicsPasteboardMime = new QMacPasteboardMimeGraphics();
256 #endif // Q_WS_MACX
257
258 #ifdef Q_WS_WIN
259         if (!metafileWindowsMime)
260                 metafileWindowsMime = new QWindowsMimeMetafile();
261 #endif // Q_WS_WIN
262 }
263
264
265 GuiClipboard::~GuiClipboard()
266 {
267 #ifdef Q_WS_MACX
268         closeAllLinkBackLinks();
269 #endif // Q_WS_MACX
270 }
271
272
273 string const GuiClipboard::getAsLyX() const
274 {
275         LYXERR(Debug::ACTION, "GuiClipboard::getAsLyX(): `");
276         // We don't convert encodings here since the encoding of the
277         // clipboard contents is specified in the data itself
278         QMimeData const * source =
279                 qApp->clipboard()->mimeData(QClipboard::Clipboard);
280         if (!source) {
281                 LYXERR(Debug::ACTION, "' (no QMimeData)");
282                 return string();
283         }
284
285         if (source->hasFormat(lyx_mime_type)) {
286                 // data from ourself or some other LyX instance
287                 QByteArray const ar = source->data(lyx_mime_type);
288                 string const s(ar.data(), ar.count());
289                 LYXERR(Debug::ACTION, s << "'");
290                 return s;
291         }
292         LYXERR(Debug::ACTION, "'");
293         return string();
294 }
295
296
297 FileName GuiClipboard::getPastedGraphicsFileName(Cursor const & cur,
298         Clipboard::GraphicsType & type) const
299 {
300         // create file dialog filter according to the existing types in the clipboard
301         vector<Clipboard::GraphicsType> types;
302         if (hasGraphicsContents(Clipboard::EmfGraphicsType))
303                 types.push_back(Clipboard::EmfGraphicsType);
304         if (hasGraphicsContents(Clipboard::WmfGraphicsType))
305                 types.push_back(Clipboard::WmfGraphicsType);
306         if (hasGraphicsContents(Clipboard::LinkBackGraphicsType))
307                 types.push_back(Clipboard::LinkBackGraphicsType);
308         if (hasGraphicsContents(Clipboard::PdfGraphicsType))
309                 types.push_back(Clipboard::PdfGraphicsType);
310         if (hasGraphicsContents(Clipboard::PngGraphicsType))
311                 types.push_back(Clipboard::PngGraphicsType);
312         if (hasGraphicsContents(Clipboard::JpegGraphicsType))
313                 types.push_back(Clipboard::JpegGraphicsType);
314         
315         BOOST_ASSERT(!types.empty());
316         
317         // select prefered type if AnyGraphicsType was passed
318         if (type == Clipboard::AnyGraphicsType)
319                 type = types.front();
320         
321         // which extension?
322         map<Clipboard::GraphicsType, string> extensions;
323         map<Clipboard::GraphicsType, docstring> typeNames;
324         
325         extensions[Clipboard::EmfGraphicsType] = "emf";
326         extensions[Clipboard::WmfGraphicsType] = "wmf";
327         extensions[Clipboard::LinkBackGraphicsType] = "linkback";
328         extensions[Clipboard::PdfGraphicsType] = "pdf";
329         extensions[Clipboard::PngGraphicsType] = "png";
330         extensions[Clipboard::JpegGraphicsType] = "jpeg";
331         
332         typeNames[Clipboard::EmfGraphicsType] = _("Enhanced Metafile");
333         typeNames[Clipboard::WmfGraphicsType] = _("Windows Metafile");
334         typeNames[Clipboard::LinkBackGraphicsType] = _("LinkBack PDF");
335         typeNames[Clipboard::PdfGraphicsType] = _("PDF");
336         typeNames[Clipboard::PngGraphicsType] = _("PNG");
337         typeNames[Clipboard::JpegGraphicsType] = _("JPEG");
338         
339         // find unused filename with primary extension
340         string document_path = cur.buffer().fileName().onlyPath().absFilename();
341         unsigned newfile_number = 0;
342         FileName filename;
343         do {
344                 ++newfile_number;
345                 filename = FileName(addName(document_path,
346                         to_utf8(_("pasted"))
347                         + convert<string>(newfile_number) + "."
348                         + extensions[type]));
349         } while (filename.isReadableFile());
350         
351         while (true) {
352                 // create file type filter, putting the prefered on to the front
353                 docstring filterSpec;
354                 for (size_t i = 0; i != types.size(); ++i) {
355                         docstring s = bformat(_("%1$s Files"), typeNames[types[i]])
356                                 + " (*." + from_ascii(extensions[types[i]]) + ")";
357                         if (types[i] == type)
358                                 filterSpec = s + filterSpec;
359                         else
360                                 filterSpec += ";;" + s;
361                 }
362                 FileFilterList const filter(filterSpec);
363                 
364                 // show save dialog for the graphic
365                 FileDialog dlg(qt_("Choose a filename to save the pasted graphic as"));
366                 FileDialog::Result result =
367                 dlg.save(toqstr(filename.onlyPath().absFilename()), filter,
368                          toqstr(filename.onlyFileName()));
369                 
370                 if (result.first == FileDialog::Later)
371                         return FileName();
372                 
373                 string newFilename = fromqstr(result.second);
374                 if (newFilename.empty()) {
375                         cur.bv().message(_("Canceled."));
376                         return FileName();
377                 }
378                 filename.set(newFilename);
379                 
380                 // check the extension (the user could have changed it)
381                 if (!suffixIs(ascii_lowercase(filename.absFilename()),
382                               "." + extensions[type])) {
383                         // the user changed the extension. Check if the type is available
384                         size_t i;
385                         for (i = 1; i != types.size(); ++i) {
386                                 if (suffixIs(ascii_lowercase(filename.absFilename()),
387                                              "." + extensions[types[i]])) {
388                                         type = types[i];
389                                         break;
390                                 }
391                         }
392                         
393                         // invalid extension found, or none at all. In the latter
394                         // case set the default extensions.
395                         if (i == types.size()
396                             && filename.onlyFileName().find('.') == string::npos) {
397                                 filename.changeExtension("." + extensions[type]);
398                         }
399                 }
400                 
401                 // check whether the file exists and warn the user
402                 if (!filename.exists())
403                         break;
404                 int ret = frontend::Alert::prompt(
405                         _("Overwrite external file?"),
406                         bformat(_("File %1$s already exists, do you want to overwrite it?"),
407                         from_utf8(filename.absFilename())), 1, 1, _("&Overwrite"), _("&Cancel"));
408                 if (ret == 0)
409                         // overwrite, hence break the dialog loop
410                         break;
411                 
412                 // not overwrite, hence show the dialog again (i.e. loop)
413         }
414         
415         return filename;
416 }
417
418
419 FileName GuiClipboard::getAsGraphics(Cursor const & cur, GraphicsType type) const
420 {
421         // get the filename from the user
422         FileName filename = getPastedGraphicsFileName(cur, type);
423         if (filename.empty())
424                 return FileName();
425
426         // handle image cases first
427         if (type == PngGraphicsType || type == JpegGraphicsType) {
428                 // get image from QImage from clipboard
429                 QImage image = qApp->clipboard()->image();
430                 if (image.isNull()) {
431                         LYXERR(Debug::ACTION, "No image in clipboard");
432                         return FileName();
433                 }
434
435                 // convert into graphics format
436                 QByteArray ar;
437                 QBuffer buffer(&ar);
438                 buffer.open(QIODevice::WriteOnly);
439                 if (type == PngGraphicsType)
440                         image.save(toqstr(filename.absFilename()), "PNG");
441                 else if (type == JpegGraphicsType)
442                         image.save(toqstr(filename.absFilename()), "JPEG");
443                 else
444                         BOOST_ASSERT(false);
445                 
446                 return filename;
447         }
448         
449         // get mime data
450         QMimeData const * source =
451         qApp->clipboard()->mimeData(QClipboard::Clipboard);
452         if (!source) {
453                 LYXERR(Debug::ACTION, "0 bytes (no QMimeData)");
454                 return FileName();
455         }
456         
457         // get mime for type
458         QString mime;
459         switch (type) {
460         case PdfGraphicsType: mime = pdf_mime_type; break;
461         case LinkBackGraphicsType: mime = pdf_mime_type; break;
462         case EmfGraphicsType: mime = emf_mime_type; break;
463         case WmfGraphicsType: mime = wmf_mime_type; break;
464         default: BOOST_ASSERT(false);
465         }
466         
467         // get data
468         if (!source->hasFormat(mime))
469                 return FileName();
470         // data from ourself or some other LyX instance
471         QByteArray const ar = source->data(mime);
472         LYXERR(Debug::ACTION, "Getting from clipboard: mime = " << mime.data()
473                << "length = " << ar.count());
474         
475         QFile f(toqstr(filename.absFilename()));
476         if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
477                 LYXERR(Debug::ACTION, "Error opening file "
478                        << filename.absFilename() << " for writing");
479                 return FileName();
480         }
481         
482         // write the (LinkBack) PDF data
483         f.write(ar);
484         if (type == LinkBackGraphicsType) {
485 #ifdef Q_WS_MACX
486                 void const * linkBackData;
487                 unsigned linkBackLen;
488                 getLinkBackData(&linkBackData, &linkBackLen);
489                 f.write((char *)linkBackData, linkBackLen);
490                 quint32 pdfLen = ar.size();
491                 QDataStream ds(&f);
492                 ds << pdfLen; // big endian by default
493 #else
494                 // only non-Mac this should never happen
495                 BOOST_ASSERT(false);
496 #endif // Q_WS_MACX
497         }
498
499         f.close();
500         return filename;
501 }
502
503
504 docstring const GuiClipboard::getAsText() const
505 {
506         // text data from other applications
507         QString const str = qApp->clipboard()->text(QClipboard::Clipboard)
508                                 .normalized(QString::NormalizationForm_C);
509         LYXERR(Debug::ACTION, "GuiClipboard::getAsText(): `" << fromqstr(str) << "'");
510         if (str.isNull())
511                 return docstring();
512
513         return internalLineEnding(qstring_to_ucs4(str));
514 }
515
516
517 void GuiClipboard::put(string const & lyx, docstring const & text)
518 {
519         LYXERR(Debug::ACTION, "GuiClipboard::put(`" << lyx << "' `"
520                               << to_utf8(text) << "')");
521         // We don't convert the encoding of lyx since the encoding of the
522         // clipboard contents is specified in the data itself
523         QMimeData * data = new QMimeData;
524         if (!lyx.empty()) {
525                 QByteArray const qlyx(lyx.c_str(), lyx.size());
526                 data->setData(lyx_mime_type, qlyx);
527         }
528         // Don't test for text.empty() since we want to be able to clear the
529         // clipboard.
530         QString const qtext = toqstr(text);
531         data->setText(qtext);
532         qApp->clipboard()->setMimeData(data, QClipboard::Clipboard);
533 }
534
535
536 bool GuiClipboard::hasLyXContents() const
537 {
538         QMimeData const * const source =
539                 qApp->clipboard()->mimeData(QClipboard::Clipboard);
540         return source && source->hasFormat(lyx_mime_type);
541 }
542
543
544 bool GuiClipboard::hasGraphicsContents(Clipboard::GraphicsType type) const
545 {
546         if (type == AnyGraphicsType) {
547                 return hasGraphicsContents(PdfGraphicsType)
548                         || hasGraphicsContents(PngGraphicsType)
549                         || hasGraphicsContents(JpegGraphicsType)
550                         || hasGraphicsContents(EmfGraphicsType)
551                         || hasGraphicsContents(WmfGraphicsType)
552                         || hasGraphicsContents(LinkBackGraphicsType);
553         }
554
555         QMimeData const * const source =
556         qApp->clipboard()->mimeData(QClipboard::Clipboard);
557
558         // handle image cases first
559         if (type == PngGraphicsType || type == JpegGraphicsType)
560                 return source->hasImage();
561
562         // handle LinkBack for Mac
563 #ifdef Q_WS_MACX
564         if (type == LinkBackGraphicsType)
565                 return isLinkBackDataInPasteboard();
566 #else
567         if (type == LinkBackGraphicsType)
568                 return false;
569 #endif // Q_WS_MACX
570         
571         // get mime data
572         QStringList const & formats = source->formats();
573         LYXERR(Debug::ACTION, "We found " << formats.size() << " formats");
574         for (int i = 0; i < formats.size(); ++i) {
575                 LYXERR(Debug::ACTION, "Found format " << fromqstr(formats[i]));
576         }
577
578         // compute mime for type
579         QString mime;
580         switch (type) {
581         case EmfGraphicsType: mime = emf_mime_type; break;
582         case WmfGraphicsType: mime = wmf_mime_type; break;
583         case PdfGraphicsType: mime = pdf_mime_type; break;
584         default: BOOST_ASSERT(false);
585         }
586         
587         return source && source->hasFormat(mime);
588 }
589
590
591 bool GuiClipboard::isInternal() const
592 {
593         // ownsClipboard() is also true for stuff coming from dialogs, e.g.
594         // the preamble dialog
595         // FIXME: This does only work on X11, since ownsClipboard() is
596         // hardwired to return false on Windows and OS X.
597         return qApp->clipboard()->ownsClipboard() && hasLyXContents();
598 }
599
600
601 bool GuiClipboard::hasInternal() const
602 {
603         // Windows and Mac OS X does not have the concept of ownership;
604         // the clipboard is a fully global resource so all applications 
605         // are notified of changes.
606 #if (defined(Q_WS_X11))
607         return true;
608 #else
609         return false;
610 #endif
611 }
612
613
614 void GuiClipboard::on_dataChanged()
615 {
616         QMimeData const * const source =
617         qApp->clipboard()->mimeData(QClipboard::Clipboard);
618         QStringList l = source->formats();
619         LYXERR(Debug::ACTION, "Qt Clipboard changed. We found the following mime types:");
620         for (int i = 0; i < l.count(); i++) {
621                 LYXERR(Debug::ACTION, fromqstr(l.value(i)));
622         }
623         
624         text_clipboard_empty_ = qApp->clipboard()->
625                 text(QClipboard::Clipboard).isEmpty();
626
627         has_lyx_contents_ = hasLyXContents();
628         has_graphics_contents_ = hasGraphicsContents();
629 }
630
631
632 bool GuiClipboard::empty() const
633 {
634         // We need to check both the plaintext and the LyX version of the
635         // clipboard. The plaintext version is empty if the LyX version
636         // contains only one inset, and the LyX version is empty if the
637         // clipboard does not come from LyX.
638         if (!text_clipboard_empty_)
639                 return false;
640         return !has_lyx_contents_ && !has_graphics_contents_;
641 }
642
643 } // namespace frontend
644 } // namespace lyx
645
646 #include "GuiClipboard_moc.cpp"