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