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