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