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