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