]> git.lyx.org Git - lyx.git/blob - src/frontends/qt4/GuiClipboard.cpp
Whitespace
[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 #include "support/lyxtime.h"
31
32 #ifdef Q_WS_MACX
33 #include "support/linkback/LinkBackProxy.h"
34 #endif // Q_WS_MACX
35
36 #include "frontends/alert.h"
37
38 #include <QApplication>
39 #include <QBuffer>
40 #include <QClipboard>
41 #include <QDataStream>
42 #include <QFile>
43 #include <QImage>
44 #include <QMimeData>
45 #include <QString>
46 #include <QStringList>
47 #include <QTextDocument>
48
49 #include <boost/crc.hpp>
50
51 #include <memory>
52 #include <map>
53 #include <iostream>
54
55 using namespace std;
56 using namespace lyx::support;
57
58
59 namespace lyx {
60
61 namespace frontend {
62
63 static QMimeData const * read_clipboard() 
64 {
65         LYXERR(Debug::ACTION, "Getting Clipboard");
66         QMimeData const * source =
67                 qApp->clipboard()->mimeData(QClipboard::Clipboard);
68         if (!source) {
69                 LYXERR0("0 bytes (no QMimeData)");
70                 return new QMimeData;
71         }
72         // It appears that doing IO between getting a mimeData object
73         // and using it can cause a crash (maybe Qt used IO
74         // as an excuse to free() it? Anyway let's not introduce
75         // any new IO here, so e.g. leave the following line commented.
76         // lyxerr << "Got Clipboard (" << (long) source << ")\n" ;
77         return source;
78 }
79
80
81 void CacheMimeData::update()
82 {
83         time_t const start_time = current_time();
84         LYXERR(Debug::ACTION, "Creating CacheMimeData object");
85         cached_formats_ = read_clipboard()->formats();
86
87         // Qt times out after 5 seconds if it does not recieve a response.
88         if (current_time() - start_time > 3) {
89                 LYXERR0("No timely response from clipboard, perhaps process "
90                         << "holding clipboard is frozen?");
91         }
92 }
93
94
95 QByteArray CacheMimeData::data(QString const & mimeType) const 
96 {
97         return read_clipboard()->data(mimeType);
98 }
99
100
101 QString const lyxMimeType(){ return "application/x-lyx"; }
102 QString const texMimeType(){ return "text/x-tex"; }
103 QString const latexMimeType(){ return "application/x-latex"; }
104 QString const pdfMimeType(){ return "application/pdf"; }
105 QString const emfMimeType(){ return "image/x-emf"; }
106 QString const wmfMimeType(){ return "image/x-wmf"; }
107
108
109 GuiClipboard::GuiClipboard()
110 {
111         connect(qApp->clipboard(), SIGNAL(dataChanged()),
112                 this, SLOT(on_dataChanged()));
113         // initialize clipboard status.
114         on_dataChanged();
115 }
116
117
118 string const GuiClipboard::getAsLyX() const
119 {
120         LYXERR(Debug::ACTION, "GuiClipboard::getAsLyX(): `");
121         // We don't convert encodings here since the encoding of the
122         // clipboard contents is specified in the data itself
123         if (cache_.hasFormat(lyxMimeType())) {
124                 // data from ourself or some other LyX instance
125                 QByteArray const ar = cache_.data(lyxMimeType());
126                 string const s(ar.data(), ar.count());
127                 LYXERR(Debug::ACTION, s << "'");
128                 return s;
129         }
130         LYXERR(Debug::ACTION, "'");
131         return string();
132 }
133
134
135 FileName GuiClipboard::getPastedGraphicsFileName(Cursor const & cur,
136         Clipboard::GraphicsType & type) const
137 {
138         // create file dialog filter according to the existing types in the clipboard
139         vector<Clipboard::GraphicsType> types;
140         if (hasGraphicsContents(Clipboard::EmfGraphicsType))
141                 types.push_back(Clipboard::EmfGraphicsType);
142         if (hasGraphicsContents(Clipboard::WmfGraphicsType))
143                 types.push_back(Clipboard::WmfGraphicsType);
144         if (hasGraphicsContents(Clipboard::LinkBackGraphicsType))
145                 types.push_back(Clipboard::LinkBackGraphicsType);
146         if (hasGraphicsContents(Clipboard::PdfGraphicsType))
147                 types.push_back(Clipboard::PdfGraphicsType);
148         if (hasGraphicsContents(Clipboard::PngGraphicsType))
149                 types.push_back(Clipboard::PngGraphicsType);
150         if (hasGraphicsContents(Clipboard::JpegGraphicsType))
151                 types.push_back(Clipboard::JpegGraphicsType);
152
153         LASSERT(!types.empty(), /**/);
154
155         // select prefered type if AnyGraphicsType was passed
156         if (type == Clipboard::AnyGraphicsType)
157                 type = types.front();
158
159         // which extension?
160         map<Clipboard::GraphicsType, string> extensions;
161         map<Clipboard::GraphicsType, docstring> typeNames;
162
163         extensions[Clipboard::EmfGraphicsType] = "emf";
164         extensions[Clipboard::WmfGraphicsType] = "wmf";
165         extensions[Clipboard::LinkBackGraphicsType] = "linkback";
166         extensions[Clipboard::PdfGraphicsType] = "pdf";
167         extensions[Clipboard::PngGraphicsType] = "png";
168         extensions[Clipboard::JpegGraphicsType] = "jpeg";
169
170         typeNames[Clipboard::EmfGraphicsType] = _("Enhanced Metafile");
171         typeNames[Clipboard::WmfGraphicsType] = _("Windows Metafile");
172         typeNames[Clipboard::LinkBackGraphicsType] = _("LinkBack PDF");
173         typeNames[Clipboard::PdfGraphicsType] = _("PDF");
174         typeNames[Clipboard::PngGraphicsType] = _("PNG");
175         typeNames[Clipboard::JpegGraphicsType] = _("JPEG");
176
177         // find unused filename with primary extension
178         string document_path = cur.buffer()->fileName().onlyPath().absFileName();
179         unsigned newfile_number = 0;
180         FileName filename;
181         do {
182                 ++newfile_number;
183                 filename = FileName(addName(document_path,
184                         to_utf8(_("pasted"))
185                         + convert<string>(newfile_number) + "."
186                         + extensions[type]));
187         } while (filename.isReadableFile());
188
189         while (true) {
190                 // create file type filter, putting the prefered on to the front
191                 QStringList filter;
192                 for (size_t i = 0; i != types.size(); ++i) {
193                         docstring s = bformat(_("%1$s Files"), typeNames[types[i]])
194                                 + " (*." + from_ascii(extensions[types[i]]) + ")";
195                         if (types[i] == type)
196                                 filter.prepend(toqstr(s));
197                         else
198                                 filter.append(toqstr(s));
199                 }
200                 filter = fileFilters(filter.join(";;"));
201
202                 // show save dialog for the graphic
203                 FileDialog dlg(qt_("Choose a filename to save the pasted graphic as"));
204                 FileDialog::Result result =
205                 dlg.save(toqstr(filename.onlyPath().absFileName()), filter,
206                          toqstr(filename.onlyFileName()));
207
208                 if (result.first == FileDialog::Later)
209                         return FileName();
210
211                 string newFilename = fromqstr(result.second);
212                 if (newFilename.empty()) {
213                         cur.bv().message(_("Canceled."));
214                         return FileName();
215                 }
216                 filename.set(newFilename);
217
218                 // check the extension (the user could have changed it)
219                 if (!suffixIs(ascii_lowercase(filename.absFileName()),
220                               "." + extensions[type])) {
221                         // the user changed the extension. Check if the type is available
222                         size_t i;
223                         for (i = 1; i != types.size(); ++i) {
224                                 if (suffixIs(ascii_lowercase(filename.absFileName()),
225                                              "." + extensions[types[i]])) {
226                                         type = types[i];
227                                         break;
228                                 }
229                         }
230
231                         // invalid extension found, or none at all. In the latter
232                         // case set the default extensions.
233                         if (i == types.size()
234                             && filename.onlyFileName().find('.') == string::npos) {
235                                 filename.changeExtension("." + extensions[type]);
236                         }
237                 }
238
239                 // check whether the file exists and warn the user
240                 if (!filename.exists())
241                         break;
242                 int ret = frontend::Alert::prompt(
243                         _("Overwrite external file?"),
244                         bformat(_("File %1$s already exists, do you want to overwrite it?"),
245                         from_utf8(filename.absFileName())), 1, 1, _("&Overwrite"), _("&Cancel"));
246                 if (ret == 0)
247                         // overwrite, hence break the dialog loop
248                         break;
249
250                 // not overwrite, hence show the dialog again (i.e. loop)
251         }
252
253         return filename;
254 }
255
256
257 FileName GuiClipboard::getAsGraphics(Cursor const & cur, GraphicsType type) const
258 {
259         // get the filename from the user
260         FileName filename = getPastedGraphicsFileName(cur, type);
261         if (filename.empty())
262                 return FileName();
263
264         // handle image cases first
265         if (type == PngGraphicsType || type == JpegGraphicsType) {
266                 // get image from QImage from clipboard
267                 QImage image = qApp->clipboard()->image();
268                 if (image.isNull()) {
269                         LYXERR(Debug::ACTION, "No image in clipboard");
270                         return FileName();
271                 }
272
273                 // convert into graphics format
274                 QByteArray ar;
275                 QBuffer buffer(&ar);
276                 buffer.open(QIODevice::WriteOnly);
277                 if (type == PngGraphicsType)
278                         image.save(toqstr(filename.absFileName()), "PNG");
279                 else if (type == JpegGraphicsType)
280                         image.save(toqstr(filename.absFileName()), "JPEG");
281                 else
282                         LASSERT(false, /**/);
283                 
284                 return filename;
285         }
286         
287         // get mime for type
288         QString mime;
289         switch (type) {
290         case PdfGraphicsType: mime = pdfMimeType(); break;
291         case LinkBackGraphicsType: mime = pdfMimeType(); break;
292         case EmfGraphicsType: mime = emfMimeType(); break;
293         case WmfGraphicsType: mime = wmfMimeType(); break;
294         default: LASSERT(false, /**/);
295         }
296         
297         // get data
298         if (!cache_.hasFormat(mime))
299                 return FileName();
300         // data from ourself or some other LyX instance
301         QByteArray const ar = cache_.data(mime);
302         LYXERR(Debug::ACTION, "Getting from clipboard: mime = " << mime.constData()
303                << "length = " << ar.count());
304
305         QFile f(toqstr(filename.absFileName()));
306         if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
307                 LYXERR(Debug::ACTION, "Error opening file "
308                        << filename.absFileName() << " for writing");
309                 return FileName();
310         }
311         
312         // write the (LinkBack) PDF data
313         f.write(ar);
314         if (type == LinkBackGraphicsType) {
315 #ifdef Q_WS_MACX
316                 void const * linkBackData;
317                 unsigned linkBackLen;
318                 getLinkBackData(&linkBackData, &linkBackLen);
319                 f.write((char *)linkBackData, linkBackLen);
320                 quint32 pdfLen = ar.size();
321                 QDataStream ds(&f);
322                 ds << pdfLen; // big endian by default
323 #else
324                 // only non-Mac this should never happen
325                 LASSERT(false, /**/);
326 #endif // Q_WS_MACX
327         }
328
329         f.close();
330         return filename;
331 }
332
333
334 namespace {
335 /**
336  * Tidy up a HTML chunk coming from the clipboard.
337  * This is needed since different applications put different kinds of HTML
338  * on the clipboard:
339  * - With or without the <?xml> tag
340  * - With or without the <!DOCTYPE> tag
341  * - With or without the <html> tag
342  * - With or without the <body> tag
343  * - With or without the <p> tag
344  * Since we are going to write a HTML file for external converters we need
345  * to ensure that it is a well formed HTML file, including all the mentioned tags.
346  */
347 QString tidyHtml(QString input)
348 {
349         // Misuse QTextDocument to cleanup the HTML.
350         // As a side effect, all visual markup like <tt> is converted to CSS,
351         // which is ignored by gnuhtml2latex.
352         // While this may be seen as a bug by some people it is actually a
353         // good thing, since we do import structure, but ignore all visual
354         // clutter.
355         QTextDocument converter;
356         converter.setHtml(input);
357         return converter.toHtml("utf-8");
358 }
359 }
360
361
362 docstring const GuiClipboard::getAsText(TextType type) const
363 {
364         // text data from other applications
365         if ((type == AnyTextType || type == LyXOrPlainTextType) && hasTextContents(LyXTextType))
366                 type = LyXTextType;
367         if (type == AnyTextType && hasTextContents(LaTeXTextType))
368                 type = LaTeXTextType;
369         if (type == AnyTextType && hasTextContents(HtmlTextType))
370                 type = HtmlTextType;
371         QString str;
372         switch (type) {
373         case LyXTextType:
374                 // must not convert to docstring, since file can contain
375                 // mixed encodings (use getAsLyX() instead)
376                 break;
377         case AnyTextType:
378         case LyXOrPlainTextType:
379         case PlainTextType:
380                 str = qApp->clipboard()->text(QClipboard::Clipboard)
381                                 .normalized(QString::NormalizationForm_C);
382                 break;
383         case LaTeXTextType: {
384                 QMimeData const * source =
385                         qApp->clipboard()->mimeData(QClipboard::Clipboard);
386                 if (source) {
387                         // First try LaTeX, then TeX (we do not distinguish
388                         // for clipboard purposes)
389                         if (source->hasFormat(latexMimeType())) {
390                                 str = source->data(latexMimeType());
391                                 str = str.normalized(QString::NormalizationForm_C);
392                         } else if (source->hasFormat(texMimeType())) {
393                                 str = source->data(texMimeType());
394                                 str = str.normalized(QString::NormalizationForm_C);
395                         }
396                 }
397                 break;
398         }
399         case HtmlTextType: {
400                 QString subtype = "html";
401                 str = qApp->clipboard()->text(subtype, QClipboard::Clipboard)
402                                 .normalized(QString::NormalizationForm_C);
403                 str = tidyHtml(str);
404                 break;
405         }
406         }
407         LYXERR(Debug::ACTION, "GuiClipboard::getAsText(" << type << "): `" << str << "'");
408         if (str.isNull())
409                 return docstring();
410
411         return internalLineEnding(str);
412 }
413
414
415 void GuiClipboard::put(string const & lyx, docstring const & html, docstring const & text)
416 {
417         LYXERR(Debug::ACTION, "GuiClipboard::put(`" << lyx << "' `"
418                               << to_utf8(html) << "' `" << to_utf8(text) << "')");
419         // We don't convert the encoding of lyx since the encoding of the
420         // clipboard contents is specified in the data itself
421         QMimeData * data = new QMimeData;
422         if (!lyx.empty()) {
423                 QByteArray const qlyx(lyx.c_str(), lyx.size());
424                 data->setData(lyxMimeType(), qlyx);
425                 // If the OS has not the concept of clipboard ownership,
426                 // we recognize internal data through its checksum.
427                 if (!hasInternal()) {
428                         boost::crc_32_type crc32;
429                         crc32.process_bytes(lyx.c_str(), lyx.size());
430                         checksum = crc32.checksum();
431                 }
432         }
433         // Don't test for text.empty() since we want to be able to clear the
434         // clipboard.
435         QString const qtext = toqstr(text);
436         data->setText(qtext);
437         QString const qhtml = toqstr(html);
438         data->setHtml(qhtml);
439         qApp->clipboard()->setMimeData(data, QClipboard::Clipboard);
440 }
441
442
443 bool GuiClipboard::hasTextContents(Clipboard::TextType type) const
444 {
445         switch (type) {
446         case AnyTextType:
447                 return cache_.hasFormat(lyxMimeType()) || cache_.hasText() ||
448                        cache_.hasHtml() || cache_.hasFormat(latexMimeType()) ||
449                        cache_.hasFormat(texMimeType());
450         case LyXOrPlainTextType:
451                 return cache_.hasFormat(lyxMimeType()) || cache_.hasText();
452         case LyXTextType:
453                 return cache_.hasFormat(lyxMimeType());
454         case PlainTextType:
455                 return cache_.hasText();       
456         case HtmlTextType:
457                 return cache_.hasHtml();
458         case LaTeXTextType:
459                 return cache_.hasFormat(latexMimeType()) ||
460                        cache_.hasFormat(texMimeType());
461         }
462         // shut up compiler
463         return false;
464 }
465
466
467 bool GuiClipboard::hasGraphicsContents(Clipboard::GraphicsType type) const
468 {
469         if (type == AnyGraphicsType) {
470                 return hasGraphicsContents(PdfGraphicsType)
471                         || hasGraphicsContents(PngGraphicsType)
472                         || hasGraphicsContents(JpegGraphicsType)
473                         || hasGraphicsContents(EmfGraphicsType)
474                         || hasGraphicsContents(WmfGraphicsType)
475                         || hasGraphicsContents(LinkBackGraphicsType);
476         }
477
478         // handle image cases first
479         if (type == PngGraphicsType || type == JpegGraphicsType)
480                 return cache_.hasImage();
481
482         // handle LinkBack for Mac
483         if (type == LinkBackGraphicsType)
484 #ifdef Q_WS_MACX
485                 return isLinkBackDataInPasteboard();
486 #else
487                 return false;
488 #endif // Q_WS_MACX
489         
490         // get mime data
491         QStringList const & formats = cache_.formats();
492         LYXERR(Debug::ACTION, "We found " << formats.size() << " formats");
493         for (int i = 0; i < formats.size(); ++i)
494                 LYXERR(Debug::ACTION, "Found format " << formats[i]);
495
496         // compute mime for type
497         QString mime;
498         switch (type) {
499         case EmfGraphicsType: mime = emfMimeType(); break;
500         case WmfGraphicsType: mime = wmfMimeType(); break;
501         case PdfGraphicsType: mime = pdfMimeType(); break;
502         default: LASSERT(false, /**/);
503         }
504         
505         return cache_.hasFormat(mime);
506 }
507
508
509 bool GuiClipboard::isInternal() const
510 {
511         if (!hasTextContents(LyXTextType))
512                 return false;
513
514         // ownsClipboard() is also true for stuff coming from dialogs, e.g.
515         // the preamble dialog. This does only work on X11 and Windows, since
516         // ownsClipboard() is hardwired to return false on OS X.
517         if (hasInternal())
518                 return qApp->clipboard()->ownsClipboard();
519
520         // We are running on OS X: Check whether clipboard data is from
521         // ourself by comparing its checksum with the stored one.
522         QByteArray const ar = cache_.data(lyxMimeType());
523         string const data(ar.data(), ar.count());
524         boost::crc_32_type crc32;
525         crc32.process_bytes(data.c_str(), data.size());
526         return checksum == crc32.checksum();
527 }
528
529
530 bool GuiClipboard::hasInternal() const
531 {
532         // Windows and Mac OS X does not have the concept of ownership;
533         // the clipboard is a fully global resource so all applications 
534         // are notified of changes. However, on Windows ownership is
535         // emulated by Qt through the OleIsCurrentClipboard() API, while
536         // on Mac OS X we deal with this issue by ourself.
537 #if (defined(Q_WS_X11) || defined(Q_WS_WIN))
538         return true;
539 #else
540         return false;
541 #endif
542 }
543
544
545 void GuiClipboard::on_dataChanged()
546 {
547         //Note: we do not really need to run cache_.update() unless the
548         //data has been changed *and* the GuiClipboard has been queried.
549         //However if run cache_.update() the moment a process grabs the
550         //clipboard, the process holding the clipboard presumably won't
551         //yet be frozen, and so we won't need to wait 5 seconds for Qt
552         //to time-out waiting for the clipboard.
553         cache_.update();
554         QStringList l = cache_.formats();
555         LYXERR(Debug::ACTION, "Qt Clipboard changed. We found the following mime types:");
556         for (int i = 0; i < l.count(); i++)
557                 LYXERR(Debug::ACTION, l.value(i));
558
559         plaintext_clipboard_empty_ = qApp->clipboard()->
560                 text(QClipboard::Clipboard).isEmpty();
561
562         has_text_contents_ = hasTextContents();
563         has_graphics_contents_ = hasGraphicsContents();
564 }
565
566
567 bool GuiClipboard::empty() const
568 {
569         // We need to check both the plaintext and the LyX version of the
570         // clipboard. The plaintext version is empty if the LyX version
571         // contains only one inset, and the LyX version is empty if the
572         // clipboard does not come from LyX.
573         if (!plaintext_clipboard_empty_)
574                 return false;
575         return !has_text_contents_ && !has_graphics_contents_;
576 }
577
578 } // namespace frontend
579 } // namespace lyx
580
581 #include "moc_GuiClipboard.cpp"