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