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