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