]> git.lyx.org Git - lyx.git/blobdiff - src/frontends/qt4/GuiClipboard.cpp
Add missing initialization
[lyx.git] / src / frontends / qt4 / GuiClipboard.cpp
index 8915ab92990fd52b6c67bb1ba20e12de361e2abb..4c8d47e7f8c5eea6c3fb6a8b18acaeacd191f29c 100644 (file)
 
 #include <config.h>
 
+#include "FileDialog.h"
+
+#include "support/FileName.h"
 #include "GuiClipboard.h"
 #include "qt_helpers.h"
 
+#include "Buffer.h"
+#include "BufferView.h"
+#include "Cursor.h"
+
+#include "support/lassert.h"
+#include "support/convert.h"
 #include "support/debug.h"
+#include "support/filetools.h"
+#include "support/gettext.h"
+#include "support/lstrings.h"
+#include "support/lyxtime.h"
+
+#ifdef Q_OS_MAC
+#include "support/linkback/LinkBackProxy.h"
+#endif // Q_OS_MAC
+
+#include "frontends/alert.h"
 
 #include <QApplication>
+#include <QBuffer>
 #include <QClipboard>
+#include <QDataStream>
+#include <QFile>
+#include <QImage>
 #include <QMimeData>
 #include <QString>
+#include <QStringList>
+#include <QTextDocument>
+#include <QTimer>
 
-#include "support/lstrings.h"
+#include <boost/crc.hpp>
+
+#include <memory>
+#include <map>
+#include <iostream>
 
 using namespace std;
 using namespace lyx::support;
 
-static char const * const mime_type = "application/x-lyx";
 
 namespace lyx {
 
 namespace frontend {
 
+static QMimeData const * read_clipboard()
+{
+       LYXERR(Debug::CLIPBOARD, "Getting Clipboard");
+       QMimeData const * source =
+               qApp->clipboard()->mimeData(QClipboard::Clipboard);
+       if (!source) {
+               LYXERR0("0 bytes (no QMimeData)");
+               return new QMimeData;
+       }
+       // It appears that doing IO between getting a mimeData object
+       // and using it can cause a crash (maybe Qt used IO
+       // as an excuse to free() it? Anyway let's not introduce
+       // any new IO here, so e.g. leave the following line commented.
+       // lyxerr << "Got Clipboard (" << (long) source << ")\n" ;
+       return source;
+}
+
+
+void CacheMimeData::update()
+{
+       time_t const start_time = current_time();
+       LYXERR(Debug::CLIPBOARD, "Creating CacheMimeData object");
+       cached_formats_ = read_clipboard()->formats();
+
+       // Qt times out after 5 seconds if it does not recieve a response.
+       if (current_time() - start_time > 3) {
+               LYXERR0("No timely response from clipboard, perhaps process "
+                       << "holding clipboard is frozen?");
+       }
+}
+
+
+QByteArray CacheMimeData::data(QString const & mimeType) const
+{
+       return read_clipboard()->data(mimeType);
+}
+
+
+QString const lyxMimeType(){ return "application/x-lyx"; }
+QString const texMimeType(){ return "text/x-tex"; }
+QString const latexMimeType(){ return "application/x-latex"; }
+QString const pdfMimeType(){ return "application/pdf"; }
+QString const emfMimeType(){ return "image/x-emf"; }
+QString const wmfMimeType(){ return "image/x-wmf"; }
+
+
 GuiClipboard::GuiClipboard()
 {
        connect(qApp->clipboard(), SIGNAL(dataChanged()),
                this, SLOT(on_dataChanged()));
        // initialize clipboard status.
-       on_dataChanged();
+       update();
 }
 
 
 string const GuiClipboard::getAsLyX() const
 {
-       LYXERR(Debug::ACTION, "GuiClipboard::getAsLyX(): `");
+       LYXERR(Debug::CLIPBOARD, "GuiClipboard::getAsLyX(): `");
        // We don't convert encodings here since the encoding of the
        // clipboard contents is specified in the data itself
-       QMimeData const * source =
-               qApp->clipboard()->mimeData(QClipboard::Clipboard);
-       if (!source) {
-               LYXERR(Debug::ACTION, "' (no QMimeData)");
-               return string();
-       }
-       if (source->hasFormat(mime_type)) {
+       if (cache_.hasFormat(lyxMimeType())) {
                // data from ourself or some other LyX instance
-               QByteArray const ar = source->data(mime_type);
+               QByteArray const ar = cache_.data(lyxMimeType());
                string const s(ar.data(), ar.count());
-               LYXERR(Debug::ACTION, s << "'");
+               LYXERR(Debug::CLIPBOARD, s << "'");
                return s;
        }
-       LYXERR(Debug::ACTION, "'");
+       LYXERR(Debug::CLIPBOARD, "'");
        return string();
 }
 
 
-docstring const GuiClipboard::getAsText() const
+FileName GuiClipboard::getPastedGraphicsFileName(Cursor const & cur,
+       Clipboard::GraphicsType & type) const
+{
+       // create file dialog filter according to the existing types in the clipboard
+       vector<Clipboard::GraphicsType> types;
+       if (hasGraphicsContents(Clipboard::EmfGraphicsType))
+               types.push_back(Clipboard::EmfGraphicsType);
+       if (hasGraphicsContents(Clipboard::WmfGraphicsType))
+               types.push_back(Clipboard::WmfGraphicsType);
+       if (hasGraphicsContents(Clipboard::LinkBackGraphicsType))
+               types.push_back(Clipboard::LinkBackGraphicsType);
+       if (hasGraphicsContents(Clipboard::PdfGraphicsType))
+               types.push_back(Clipboard::PdfGraphicsType);
+       if (hasGraphicsContents(Clipboard::PngGraphicsType))
+               types.push_back(Clipboard::PngGraphicsType);
+       if (hasGraphicsContents(Clipboard::JpegGraphicsType))
+               types.push_back(Clipboard::JpegGraphicsType);
+
+       LASSERT(!types.empty(), return FileName());
+
+       // select prefered type if AnyGraphicsType was passed
+       if (type == Clipboard::AnyGraphicsType)
+               type = types.front();
+
+       // which extension?
+       map<Clipboard::GraphicsType, string> extensions;
+       map<Clipboard::GraphicsType, docstring> typeNames;
+
+       extensions[Clipboard::EmfGraphicsType] = "emf";
+       extensions[Clipboard::WmfGraphicsType] = "wmf";
+       extensions[Clipboard::LinkBackGraphicsType] = "linkback";
+       extensions[Clipboard::PdfGraphicsType] = "pdf";
+       extensions[Clipboard::PngGraphicsType] = "png";
+       extensions[Clipboard::JpegGraphicsType] = "jpeg";
+
+       typeNames[Clipboard::EmfGraphicsType] = _("Enhanced Metafile");
+       typeNames[Clipboard::WmfGraphicsType] = _("Windows Metafile");
+       typeNames[Clipboard::LinkBackGraphicsType] = _("LinkBack PDF");
+       typeNames[Clipboard::PdfGraphicsType] = _("PDF");
+       typeNames[Clipboard::PngGraphicsType] = _("PNG");
+       typeNames[Clipboard::JpegGraphicsType] = _("JPEG");
+
+       // find unused filename with primary extension
+       string document_path = cur.buffer()->fileName().onlyPath().absFileName();
+       unsigned newfile_number = 0;
+       FileName filename;
+       do {
+               ++newfile_number;
+               filename = FileName(addName(document_path,
+                       to_utf8(_("pasted"))
+                       + convert<string>(newfile_number) + "."
+                       + extensions[type]));
+       } while (filename.isReadableFile());
+
+       while (true) {
+               // create file type filter, putting the prefered on to the front
+               QStringList filter;
+               for (size_t i = 0; i != types.size(); ++i) {
+                       docstring s = bformat(_("%1$s Files"), typeNames[types[i]])
+                               + " (*." + from_ascii(extensions[types[i]]) + ")";
+                       if (types[i] == type)
+                               filter.prepend(toqstr(s));
+                       else
+                               filter.append(toqstr(s));
+               }
+               filter = fileFilters(filter.join(";;"));
+
+               // show save dialog for the graphic
+               FileDialog dlg(qt_("Choose a filename to save the pasted graphic as"));
+               FileDialog::Result result =
+               dlg.save(toqstr(filename.onlyPath().absFileName()), filter,
+                        toqstr(filename.onlyFileName()));
+
+               if (result.first == FileDialog::Later)
+                       return FileName();
+
+               string newFilename = fromqstr(result.second);
+               if (newFilename.empty()) {
+                       cur.bv().message(_("Canceled."));
+                       return FileName();
+               }
+               filename.set(newFilename);
+
+               // check the extension (the user could have changed it)
+               if (!suffixIs(ascii_lowercase(filename.absFileName()),
+                             "." + extensions[type])) {
+                       // the user changed the extension. Check if the type is available
+                       size_t i;
+                       for (i = 1; i != types.size(); ++i) {
+                               if (suffixIs(ascii_lowercase(filename.absFileName()),
+                                            "." + extensions[types[i]])) {
+                                       type = types[i];
+                                       break;
+                               }
+                       }
+
+                       // invalid extension found, or none at all. In the latter
+                       // case set the default extensions.
+                       if (i == types.size()
+                           && filename.onlyFileName().find('.') == string::npos) {
+                               filename.changeExtension("." + extensions[type]);
+                       }
+               }
+
+               // check whether the file exists and warn the user
+               if (!filename.exists())
+                       break;
+               int ret = frontend::Alert::prompt(
+                       _("Overwrite external file?"),
+                       bformat(_("File %1$s already exists, do you want to overwrite it?"),
+                       from_utf8(filename.absFileName())), 1, 1, _("&Overwrite"), _("&Cancel"));
+               if (ret == 0)
+                       // overwrite, hence break the dialog loop
+                       break;
+
+               // not overwrite, hence show the dialog again (i.e. loop)
+       }
+
+       return filename;
+}
+
+
+FileName GuiClipboard::getAsGraphics(Cursor const & cur, GraphicsType type) const
+{
+       // get the filename from the user
+       FileName filename = getPastedGraphicsFileName(cur, type);
+       if (filename.empty())
+               return FileName();
+
+       // handle image cases first
+       if (type == PngGraphicsType || type == JpegGraphicsType) {
+               // get image from QImage from clipboard
+               QImage image = qApp->clipboard()->image();
+               if (image.isNull()) {
+                       LYXERR(Debug::CLIPBOARD, "No image in clipboard");
+                       return FileName();
+               }
+
+               // convert into graphics format
+               QByteArray ar;
+               QBuffer buffer(&ar);
+               buffer.open(QIODevice::WriteOnly);
+               if (type == PngGraphicsType)
+                       image.save(toqstr(filename.absFileName()), "PNG");
+               else if (type == JpegGraphicsType)
+                       image.save(toqstr(filename.absFileName()), "JPEG");
+               else
+                       LATTEST(false);
+
+               return filename;
+       }
+
+       // get mime for type
+       QString mime;
+       switch (type) {
+       case PdfGraphicsType: mime = pdfMimeType(); break;
+       case LinkBackGraphicsType: mime = pdfMimeType(); break;
+       case EmfGraphicsType: mime = emfMimeType(); break;
+       case WmfGraphicsType: mime = wmfMimeType(); break;
+       default: LASSERT(false, return FileName());
+       }
+
+       // get data
+       if (!cache_.hasFormat(mime))
+               return FileName();
+       // data from ourself or some other LyX instance
+       QByteArray const ar = cache_.data(mime);
+       LYXERR(Debug::CLIPBOARD, "Getting from clipboard: mime = " << mime.constData()
+              << "length = " << ar.count());
+
+       QFile f(toqstr(filename.absFileName()));
+       if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
+               LYXERR(Debug::CLIPBOARD, "Error opening file "
+                      << filename.absFileName() << " for writing");
+               return FileName();
+       }
+
+       // write the (LinkBack) PDF data
+       f.write(ar);
+       if (type == LinkBackGraphicsType) {
+#ifdef Q_OS_MAC
+               void const * linkBackData;
+               unsigned linkBackLen;
+               getLinkBackData(&linkBackData, &linkBackLen);
+               f.write((char *)linkBackData, linkBackLen);
+               quint32 pdfLen = ar.size();
+               QDataStream ds(&f);
+               ds << pdfLen; // big endian by default
+#else
+               // only non-Mac this should never happen
+               LATTEST(false);
+#endif // Q_OS_MAC
+       }
+
+       f.close();
+       return filename;
+}
+
+
+namespace {
+/**
+ * Tidy up a HTML chunk coming from the clipboard.
+ * This is needed since different applications put different kinds of HTML
+ * on the clipboard:
+ * - With or without the <?xml> tag
+ * - With or without the <!DOCTYPE> tag
+ * - With or without the <html> tag
+ * - With or without the <body> tag
+ * - With or without the <p> tag
+ * Since we are going to write a HTML file for external converters we need
+ * to ensure that it is a well formed HTML file, including all the mentioned tags.
+ */
+QString tidyHtml(QString input)
+{
+       // Misuse QTextDocument to cleanup the HTML.
+       // As a side effect, all visual markup like <tt> is converted to CSS,
+       // which is ignored by gnuhtml2latex.
+       // While this may be seen as a bug by some people it is actually a
+       // good thing, since we do import structure, but ignore all visual
+       // clutter.
+       QTextDocument converter;
+       converter.setHtml(input);
+       return converter.toHtml("utf-8");
+}
+}
+
+
+docstring const GuiClipboard::getAsText(TextType type) const
 {
        // text data from other applications
-       QString const str = qApp->clipboard()->text(QClipboard::Clipboard)
+       if ((type == AnyTextType || type == LyXOrPlainTextType) && hasTextContents(LyXTextType))
+               type = LyXTextType;
+       if (type == AnyTextType && hasTextContents(LaTeXTextType))
+               type = LaTeXTextType;
+       if (type == AnyTextType && hasTextContents(HtmlTextType))
+               type = HtmlTextType;
+       QString str;
+       switch (type) {
+       case LyXTextType:
+               // must not convert to docstring, since file can contain
+               // mixed encodings (use getAsLyX() instead)
+               break;
+       case AnyTextType:
+       case LyXOrPlainTextType:
+       case PlainTextType:
+               str = qApp->clipboard()->text(QClipboard::Clipboard)
                                .normalized(QString::NormalizationForm_C);
-       LYXERR(Debug::ACTION, "GuiClipboard::getAsText(): `" << fromqstr(str) << "'");
+               break;
+       case LaTeXTextType: {
+               QMimeData const * source =
+                       qApp->clipboard()->mimeData(QClipboard::Clipboard);
+               if (source) {
+                       // First try LaTeX, then TeX (we do not distinguish
+                       // for clipboard purposes)
+                       if (source->hasFormat(latexMimeType())) {
+                               str = source->data(latexMimeType());
+                               str = str.normalized(QString::NormalizationForm_C);
+                       } else if (source->hasFormat(texMimeType())) {
+                               str = source->data(texMimeType());
+                               str = str.normalized(QString::NormalizationForm_C);
+                       }
+               }
+               break;
+       }
+       case HtmlTextType: {
+               QString subtype = "html";
+               str = qApp->clipboard()->text(subtype, QClipboard::Clipboard)
+                               .normalized(QString::NormalizationForm_C);
+               str = tidyHtml(str);
+               break;
+       }
+       }
+       LYXERR(Debug::CLIPBOARD, "GuiClipboard::getAsText(" << type << "): `" << str << "'");
        if (str.isNull())
                return docstring();
 
-       return internalLineEnding(qstring_to_ucs4(str));
+       return internalLineEnding(str);
+}
+
+
+void GuiClipboard::put(string const & text) const
+{
+       qApp->clipboard()->setText(toqstr(text));
 }
 
 
-void GuiClipboard::put(string const & lyx, docstring const & text)
+void GuiClipboard::put(string const & lyx, docstring const & html, docstring const & text)
 {
-       LYXERR(Debug::ACTION, "GuiClipboard::put(`" << lyx << "' `"
-                             << to_utf8(text) << "')");
+       LYXERR(Debug::CLIPBOARD, "GuiClipboard::put(`" << lyx << "' `"
+                             << to_utf8(html) << "' `" << to_utf8(text) << "')");
        // We don't convert the encoding of lyx since the encoding of the
        // clipboard contents is specified in the data itself
        QMimeData * data = new QMimeData;
        if (!lyx.empty()) {
                QByteArray const qlyx(lyx.c_str(), lyx.size());
-               data->setData(mime_type, qlyx);
+               data->setData(lyxMimeType(), qlyx);
+               // If the OS has not the concept of clipboard ownership,
+               // we recognize internal data through its checksum.
+               if (!hasInternal()) {
+                       boost::crc_32_type crc32;
+                       crc32.process_bytes(lyx.c_str(), lyx.size());
+                       checksum = crc32.checksum();
+               }
        }
        // Don't test for text.empty() since we want to be able to clear the
        // clipboard.
        QString const qtext = toqstr(text);
        data->setText(qtext);
+       QString const qhtml = toqstr(html);
+       data->setHtml(qhtml);
        qApp->clipboard()->setMimeData(data, QClipboard::Clipboard);
 }
 
 
-bool GuiClipboard::hasLyXContents() const
+bool GuiClipboard::hasTextContents(Clipboard::TextType type) const
 {
-       QMimeData const * const source =
-               qApp->clipboard()->mimeData(QClipboard::Clipboard);
-       return source && source->hasFormat(mime_type);
+       switch (type) {
+       case AnyTextType:
+               return cache_.hasFormat(lyxMimeType()) || cache_.hasText() ||
+                      cache_.hasHtml() || cache_.hasFormat(latexMimeType()) ||
+                      cache_.hasFormat(texMimeType());
+       case LyXOrPlainTextType:
+               return cache_.hasFormat(lyxMimeType()) || cache_.hasText();
+       case LyXTextType:
+               return cache_.hasFormat(lyxMimeType());
+       case PlainTextType:
+               return cache_.hasText();
+       case HtmlTextType:
+               return cache_.hasHtml();
+       case LaTeXTextType:
+               return cache_.hasFormat(latexMimeType()) ||
+                      cache_.hasFormat(texMimeType());
+       }
+       // shut up compiler
+       return false;
+}
+
+
+bool GuiClipboard::hasGraphicsContents(Clipboard::GraphicsType type) const
+{
+       if (type == AnyGraphicsType) {
+               return hasGraphicsContents(PdfGraphicsType)
+                       || hasGraphicsContents(PngGraphicsType)
+                       || hasGraphicsContents(JpegGraphicsType)
+                       || hasGraphicsContents(EmfGraphicsType)
+                       || hasGraphicsContents(WmfGraphicsType)
+                       || hasGraphicsContents(LinkBackGraphicsType);
+       }
+
+       // handle image cases first
+       if (type == PngGraphicsType || type == JpegGraphicsType)
+               return cache_.hasImage();
+
+       // handle LinkBack for Mac
+       if (type == LinkBackGraphicsType)
+#ifdef Q_OS_MAC
+               return isLinkBackDataInPasteboard();
+#else
+               return false;
+#endif // Q_OS_MAC
+
+       // get mime data
+       QStringList const & formats = cache_.formats();
+       LYXERR(Debug::CLIPBOARD, "We found " << formats.size() << " formats");
+       for (int i = 0; i < formats.size(); ++i)
+               LYXERR(Debug::CLIPBOARD, "Found format " << formats[i]);
+
+       // compute mime for type
+       QString mime;
+       switch (type) {
+       case EmfGraphicsType: mime = emfMimeType(); break;
+       case WmfGraphicsType: mime = wmfMimeType(); break;
+       case PdfGraphicsType: mime = pdfMimeType(); break;
+       default: LASSERT(false, return false);
+       }
+
+       return cache_.hasFormat(mime);
 }
 
 
 bool GuiClipboard::isInternal() const
 {
+       if (!hasTextContents(LyXTextType))
+               return false;
+
        // ownsClipboard() is also true for stuff coming from dialogs, e.g.
-       // the preamble dialog
-       // FIXME: This does only work on X11, since ownsClipboard() is
-       // hardwired to return false on Windows and OS X.
-       return qApp->clipboard()->ownsClipboard() && hasLyXContents();
+       // the preamble dialog. This does only work on X11 and Windows, since
+       // ownsClipboard() is hardwired to return false on OS X.
+       if (hasInternal())
+               return qApp->clipboard()->ownsClipboard();
+
+       // We are running on OS X: Check whether clipboard data is from
+       // ourself by comparing its checksum with the stored one.
+       QByteArray const ar = cache_.data(lyxMimeType());
+       string const data(ar.data(), ar.count());
+       boost::crc_32_type crc32;
+       crc32.process_bytes(data.c_str(), data.size());
+       return checksum == crc32.checksum();
 }
 
 
 bool GuiClipboard::hasInternal() const
 {
        // Windows and Mac OS X does not have the concept of ownership;
-       // the clipboard is a fully global resource so all applications 
-       // are notified of changes.
-#if (defined(Q_WS_X11))
+       // the clipboard is a fully global resource so all applications
+       // are notified of changes. However, on Windows ownership is
+       // emulated by Qt through the OleIsCurrentClipboard() API, while
+       // on Mac OS X we deal with this issue by ourself.
+#ifndef Q_OS_MAC
        return true;
 #else
        return false;
@@ -130,10 +552,34 @@ bool GuiClipboard::hasInternal() const
 
 void GuiClipboard::on_dataChanged()
 {
-       text_clipboard_empty_ = qApp->clipboard()->
+       update();
+#if defined(Q_OS_WIN) || defined(Q_CYGWIN_WIN)
+       // Retry on Windows (#10109)
+       if (cache_.formats().count() == 0) {
+               QTimer::singleShot(100, this, SLOT(update()));
+       }
+#endif
+}
+
+void GuiClipboard::update()
+{
+       //Note: we do not really need to run cache_.update() unless the
+       //data has been changed *and* the GuiClipboard has been queried.
+       //However if run cache_.update() the moment a process grabs the
+       //clipboard, the process holding the clipboard presumably won't
+       //yet be frozen, and so we won't need to wait 5 seconds for Qt
+       //to time-out waiting for the clipboard.
+       cache_.update();
+       QStringList l = cache_.formats();
+       LYXERR(Debug::CLIPBOARD, "Qt Clipboard changed. We found the following mime types:");
+       for (int i = 0; i < l.count(); i++)
+               LYXERR(Debug::CLIPBOARD, l.value(i));
+
+       plaintext_clipboard_empty_ = qApp->clipboard()->
                text(QClipboard::Clipboard).isEmpty();
 
-       has_lyx_contents_ = hasLyXContents();
+       has_text_contents_ = hasTextContents();
+       has_graphics_contents_ = hasGraphicsContents();
 }
 
 
@@ -143,12 +589,12 @@ bool GuiClipboard::empty() const
        // clipboard. The plaintext version is empty if the LyX version
        // contains only one inset, and the LyX version is empty if the
        // clipboard does not come from LyX.
-       if (!text_clipboard_empty_)
+       if (!plaintext_clipboard_empty_)
                return false;
-       return !has_lyx_contents_;
+       return !has_text_contents_ && !has_graphics_contents_;
 }
 
 } // namespace frontend
 } // namespace lyx
 
-#include "GuiClipboard_moc.cpp"
+#include "moc_GuiClipboard.cpp"