lyx_mirror/src/frontends/qt/GuiClipboard.cpp
Enrico Forestieri 635a7d77dd Allow compiling with Qt6
This commit allows compiling LyX with Qt6 when using autotools.
For a successful compilation the following 2 conditions must be met.

1) The Qt6 qmake has to come first in PATH, so that the command
   "qmake -v | grep -o 'Qt version .'" returns "Qt version 6".
2) The --enable-qt6 switch has to be passed to the configure command.

If --enable-qt6 is used but Qt6 is not found, Qt5 is tried as a fallback.
If also Qt5 is not found, configuring for Qt4 is attempted.
If --enable-qt6 is not used, then things go as usual. This means that Qt5
is tried first and then Qt4, unless --disable-qt5 is used, in which case
Qt4 is directly attempted. This means that existing scripts should
continue working unmodified.

LyX should compile with Qt6 on windows and linux, and possibly also on
mac, but I could not test that. However, it is not guaranteed that it
works as it should. In particular I am not sure that I got right the
conversion from QRegExp to QRegularExpression. For sure, the syntax
highlighting seems to not work right. Someone in the know should take
a look at that. I am able to load documents and compile them but some
thourough testing is needed. However, when compiling for Qt5 or Qt4,
I tried to make sure that the functionality is preserved.
2021-03-15 17:09:09 +01:00

622 lines
18 KiB
C++

// -*- C++ -*-
/**
* \file qt/GuiClipboard.cpp
* This file is part of LyX, the document processor.
* Licence details can be found in the file COPYING.
*
* \author John Levon
* \author Abdelrazak Younes
*
* Full author contact details are available in file CREDITS.
*/
#include <config.h>
#include "GuiApplication.h"
#include "GuiClipboard.h"
#include "Buffer.h"
#include "BufferView.h"
#include "Cursor.h"
#include "FileDialog.h"
#include "qt_helpers.h"
#include "support/lassert.h"
#include "support/convert.h"
#include "support/debug.h"
#include "support/FileName.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 "support/checksum.h"
#include <QApplication>
#include <QBuffer>
#include <QClipboard>
#include <QDataStream>
#include <QFile>
#include <QImage>
#include <QMimeData>
#include <QString>
#include <QStringList>
#include <QTextDocument>
#include <QTimer>
#include <memory>
#include <map>
#include <iostream>
using namespace std;
using namespace lyx::support;
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 receive 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()));
if(qApp->clipboard()->supportsFindBuffer()) {
connect(qApp->clipboard(), SIGNAL(findBufferChanged()),
this, SLOT(on_findChanged()));
on_findChanged();
}
// initialize clipboard status.
update();
}
string const GuiClipboard::getAsLyX() const
{
LYXERR(Debug::CLIPBOARD, "GuiClipboard::getAsLyX(): `");
// We don't convert encodings here since the encoding of the
// clipboard contents is specified in the data itself
if (cache_.hasFormat(lyxMimeType())) {
// data from ourself or some other LyX instance
QByteArray const ar = cache_.data(lyxMimeType());
string const s(ar.data(), ar.count());
LYXERR(Debug::CLIPBOARD, s << "'");
return s;
}
LYXERR(Debug::CLIPBOARD, "'");
return string();
}
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 preferred 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 preferred 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 const & 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);
#if QT_VERSION < 0x060000
return converter.toHtml("utf-8");
#else
return converter.toHtml();
#endif
}
} // namespace
docstring const GuiClipboard::getAsText(TextType type) const
{
// text data from other applications
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);
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(str);
}
void GuiClipboard::put(string const & text) const
{
qApp->clipboard()->setText(toqstr(text));
}
void GuiClipboard::put(string const & lyx, docstring const & html, docstring const & 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(lyxMimeType(), qlyx);
// If the OS has not the concept of clipboard ownership,
// we recognize internal data through its checksum.
if (!hasInternal())
checksum = support::checksum(lyx);
}
// 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::hasTextContents(Clipboard::TextType type) const
{
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. 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());
return checksum == static_cast<std::uint32_t>(support::checksum(data));
}
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. 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;
#endif
}
void GuiClipboard::setFindBuffer(docstring const & text)
{
LYXERR(Debug::CLIPBOARD, "new findbuffer: " << text);
Clipboard::setFindBuffer(text);
if(qApp->clipboard()->supportsFindBuffer()) {
qApp->clipboard()->setText(toqstr(text), QClipboard::FindBuffer);
}
}
void GuiClipboard::on_findChanged()
{
Clipboard::setFindBuffer(from_utf8(fromqstr(
qApp->clipboard()->text(QClipboard::FindBuffer))));
}
void GuiClipboard::on_dataChanged()
{
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_text_contents_ = hasTextContents();
has_graphics_contents_ = hasGraphicsContents();
}
bool GuiClipboard::empty() const
{
// We need to check both the plaintext and the LyX version of the
// 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 (!plaintext_clipboard_empty_)
return false;
return !has_text_contents_ && !has_graphics_contents_;
}
} // namespace frontend
} // namespace lyx
#include "moc_GuiClipboard.cpp"