Implement paste from LaTeX and HTML (bug #3096)

As discussed on the list. No automatic contents detection is done, the user
needs to use the special paste menu instead. I used the new TempFile class
for safe temporary file handling.
The documentation would go into section 2.2 of UserGuide.lyx, but I am not
allowed to edit that document.
This commit is contained in:
Georg Baum 2013-04-14 19:45:36 +02:00
parent db0ba3a3c6
commit c14b9e67bc
13 changed files with 247 additions and 43 deletions

View File

@ -156,6 +156,8 @@ Menuset
Menu "edit_paste" Menu "edit_paste"
Item "Plain Text|T" "clipboard-paste" Item "Plain Text|T" "clipboard-paste"
Item "Plain Text, Join Lines|J" "clipboard-paste paragraph" Item "Plain Text, Join Lines|J" "clipboard-paste paragraph"
Item "HTML Text|H" "paste html"
Item "LaTeX Text|L" "paste latex"
Separator Separator
Item "Selection|S" "primary-selection-paste" Item "Selection|S" "primary-selection-paste"
Item "Selection, Join Lines|i" "primary-selection-paste paragraph" Item "Selection, Join Lines|i" "primary-selection-paste paragraph"

View File

@ -102,6 +102,7 @@
#include "support/Package.h" #include "support/Package.h"
#include "support/PathChanger.h" #include "support/PathChanger.h"
#include "support/Systemcall.h" #include "support/Systemcall.h"
#include "support/TempFile.h"
#include "support/textutils.h" #include "support/textutils.h"
#include "support/types.h" #include "support/types.h"
@ -978,6 +979,49 @@ bool Buffer::readDocument(Lexer & lex)
} }
bool Buffer::importString(string const & format, docstring const & contents, ErrorList & errorList)
{
Format const * fmt = formats.getFormat(format);
if (!fmt)
return false;
// It is important to use the correct extension here, since some
// converters create a wrong output file otherwise (e.g. html2latex)
TempFile const tempfile("Buffer_importStringXXXXXX." + fmt->extension());
FileName const name(tempfile.name());
ofdocstream os(name.toFilesystemEncoding().c_str());
bool const success = (os << contents);
os.close();
bool converted = false;
if (success) {
params().compressed = false;
// remove dummy empty par
paragraphs().clear();
converted = importFile(format, name, errorList);
}
if (name.exists())
name.removeFile();
return converted;
}
bool Buffer::importFile(string const & format, FileName const & name, ErrorList & errorList)
{
if (!theConverters().isReachable(format, "lyx"))
return false;
TempFile const tempfile("Buffer_importFileXXXXXX.lyx");
FileName const lyx(tempfile.name());
if (theConverters().convert(0, name, lyx, name, format, "lyx", errorList))
return readFile(lyx) == ReadSuccess;
return false;
}
bool Buffer::readString(string const & s) bool Buffer::readString(string const & s)
{ {
params().compressed = false; params().compressed = false;
@ -1125,7 +1169,7 @@ Buffer::ReadStatus Buffer::parseLyXFormat(Lexer & lex,
Buffer::ReadStatus Buffer::convertLyXFormat(FileName const & fn, Buffer::ReadStatus Buffer::convertLyXFormat(FileName const & fn,
FileName & tmpfile, int from_format) FileName & tmpfile, int from_format)
{ {
tmpfile = FileName::tempName("Buffer_convertLyXFormat"); tmpfile = FileName::tempName("Buffer_convertLyXFormatXXXXXX.lyx");
if(tmpfile.empty()) { if(tmpfile.empty()) {
Alert::error(_("Conversion failed"), Alert::error(_("Conversion failed"),
bformat(_("%1$s is from a different" bformat(_("%1$s is from a different"
@ -2765,11 +2809,12 @@ string Buffer::absFileName() const
string Buffer::filePath() const string Buffer::filePath() const
{ {
int last = d->filename.onlyPath().absFileName().length() - 1; string const abs = d->filename.onlyPath().absFileName();
if (abs.empty())
return abs;
int last = abs.length() - 1;
return d->filename.onlyPath().absFileName()[last] == '/' return abs[last] == '/' ? abs : abs + '/';
? d->filename.onlyPath().absFileName()
: d->filename.onlyPath().absFileName() + "/";
} }

View File

@ -222,6 +222,10 @@ public:
/// emergency or autosave files, one should use \c loadLyXFile. /// emergency or autosave files, one should use \c loadLyXFile.
/// /sa loadLyXFile /// /sa loadLyXFile
ReadStatus loadThisLyXFile(support::FileName const & fn); ReadStatus loadThisLyXFile(support::FileName const & fn);
/// import a new document from a string
bool importString(std::string const &, docstring const &, ErrorList &);
/// import a new file
bool importFile(std::string const &, support::FileName const &, ErrorList &);
/// read a new document from a string /// read a new document from a string
bool readString(std::string const &); bool readString(std::string const &);
/// Reloads the LyX file /// Reloads the LyX file

View File

@ -1010,16 +1010,21 @@ void pasteFromStack(Cursor & cur, ErrorList & errorList, size_t sel_index)
} }
void pasteClipboardText(Cursor & cur, ErrorList & errorList, bool asParagraphs) void pasteClipboardText(Cursor & cur, ErrorList & errorList, bool asParagraphs,
Clipboard::TextType type)
{ {
// Use internal clipboard if it is the most recent one // Use internal clipboard if it is the most recent one
// This overrides asParagraphs and type on purpose!
if (theClipboard().isInternal()) { if (theClipboard().isInternal()) {
pasteFromStack(cur, errorList, 0); pasteFromStack(cur, errorList, 0);
return; return;
} }
// First try LyX format // First try LyX format
if (theClipboard().hasLyXContents()) { if ((type == Clipboard::LyXTextType ||
type == Clipboard::LyXOrPlainTextType ||
type == Clipboard::AnyTextType) &&
theClipboard().hasTextContents(Clipboard::LyXTextType)) {
string lyx = theClipboard().getAsLyX(); string lyx = theClipboard().getAsLyX();
if (!lyx.empty()) { if (!lyx.empty()) {
// For some strange reason gcc 3.2 and 3.3 do not accept // For some strange reason gcc 3.2 and 3.3 do not accept
@ -1035,8 +1040,42 @@ void pasteClipboardText(Cursor & cur, ErrorList & errorList, bool asParagraphs)
} }
} }
// Then try TeX and HTML
Clipboard::TextType types[2] = {Clipboard::HtmlTextType, Clipboard::LaTeXTextType};
string names[2] = {"html", "latex"};
for (int i = 0; i < 2; ++i) {
if (type != types[i] && type != Clipboard::AnyTextType)
continue;
bool available = theClipboard().hasTextContents(types[i]);
// If a specific type was explicitly requested, try to
// interpret plain text: The user told us that the clipboard
// contents is in the desired format
if (!available && type == types[i]) {
types[i] = Clipboard::PlainTextType;
available = theClipboard().hasTextContents(types[i]);
}
if (available) {
docstring text = theClipboard().getAsText(types[i]);
available = !text.empty();
if (available) {
// For some strange reason gcc 3.2 and 3.3 do not accept
// Buffer buffer(string(), false);
Buffer buffer("", false);
buffer.setUnnamed(true);
if (buffer.importString(names[i], text, errorList)) {
cur.recordUndo();
pasteParagraphList(cur, buffer.paragraphs(),
buffer.params().documentClassPtr(), errorList);
return;
}
}
}
}
// Then try plain text // Then try plain text
docstring const text = theClipboard().getAsText(); docstring const text = theClipboard().getAsText(Clipboard::PlainTextType);
if (text.empty()) if (text.empty())
return; return;
cur.recordUndo(); cur.recordUndo();
@ -1065,7 +1104,7 @@ void pasteSimpleText(Cursor & cur, bool asParagraphs)
asParagraphs = false; asParagraphs = false;
} else { } else {
// Then try plain text // Then try plain text
text = theClipboard().getAsText(); text = theClipboard().getAsText(Clipboard::PlainTextType);
} }
if (text.empty()) if (text.empty())

View File

@ -87,8 +87,9 @@ void pasteSelection(Cursor & cur, ErrorList &);
/// Replace the current selection with the clipboard contents as text /// Replace the current selection with the clipboard contents as text
/// (internal or external: which is newer). /// (internal or external: which is newer).
/// Does handle undo. Does only work in text, not mathed. /// Does handle undo. Does only work in text, not mathed.
void pasteClipboardText(Cursor & cur, ErrorList & errorList, /// \p asParagraphs is only considered if plain text is pasted.
bool asParagraphs = true); void pasteClipboardText(Cursor & cur, ErrorList & errorList, bool asParagraphs,
Clipboard::TextType preferedType = Clipboard::LyXOrPlainTextType);
/// Replace the current selection with the clipboard contents as graphic. /// Replace the current selection with the clipboard contents as graphic.
/// Does handle undo. Does only work in text, not mathed. /// Does handle undo. Does only work in text, not mathed.
void pasteClipboardGraphics(Cursor & cur, ErrorList & errorList, void pasteClipboardGraphics(Cursor & cur, ErrorList & errorList,

View File

@ -1215,7 +1215,7 @@ void LyXAction::init()
* \var lyx::FuncCode lyx::LFUN_PASTE * \var lyx::FuncCode lyx::LFUN_PASTE
* \li Action: Pastes material (text or picture) from the active clipboard. * \li Action: Pastes material (text or picture) from the active clipboard.
* \li Syntax: paste [<TYPE>|<NUM>] * \li Syntax: paste [<TYPE>|<NUM>]
* \li Params: <TYPE>: emf|pdf|png|jpeg|linkback|wmf \n * \li Params: <TYPE>: emf|pdf|png|jpeg|linkback|wmf|latex|html \n
<NUM>: number of the selection in the internal clipboard stack to be pasted. <NUM>: number of the selection in the internal clipboard stack to be pasted.
* \endvar * \endvar
*/ */

View File

@ -1248,11 +1248,15 @@ void Text::dispatch(Cursor & cur, FuncRequest & cmd)
&& !theClipboard().hasTextContents()) && !theClipboard().hasTextContents())
pasteClipboardGraphics(cur, bv->buffer().errorList("Paste")); pasteClipboardGraphics(cur, bv->buffer().errorList("Paste"));
else else
pasteClipboardText(cur, bv->buffer().errorList("Paste")); pasteClipboardText(cur, bv->buffer().errorList("Paste"), true);
} else if (isStrUnsignedInt(arg)) { } else if (isStrUnsignedInt(arg)) {
// we have a numerical argument // we have a numerical argument
pasteFromStack(cur, bv->buffer().errorList("Paste"), pasteFromStack(cur, bv->buffer().errorList("Paste"),
convert<unsigned int>(arg)); convert<unsigned int>(arg));
} else if (arg == "html" || arg == "latex") {
Clipboard::TextType type = (arg == "html") ?
Clipboard::HtmlTextType : Clipboard::LaTeXTextType;
pasteClipboardText(cur, bv->buffer().errorList("Paste"), true, type);
} else { } else {
Clipboard::GraphicsType type = Clipboard::AnyGraphicsType; Clipboard::GraphicsType type = Clipboard::AnyGraphicsType;
if (arg == "pdf") if (arg == "pdf")
@ -1267,7 +1271,6 @@ void Text::dispatch(Cursor & cur, FuncRequest & cmd)
type = Clipboard::EmfGraphicsType; type = Clipboard::EmfGraphicsType;
else if (arg == "wmf") else if (arg == "wmf")
type = Clipboard::WmfGraphicsType; type = Clipboard::WmfGraphicsType;
else else
LASSERT(false, /**/); LASSERT(false, /**/);
@ -2772,6 +2775,20 @@ bool Text::getStatus(Cursor & cur, FuncRequest const & cmd,
break; break;
} }
// explicit text type?
if (arg == "html") {
// Do not enable for PlainTextType, since some tidying in the
// frontend is needed for HTML, which is too unsafe for plain text.
enable = theClipboard().hasTextContents(Clipboard::HtmlTextType);
break;
} else if (arg == "latex") {
// LaTeX is usually not available on the clipboard with
// the correct MIME type, but in plain text.
enable = theClipboard().hasTextContents(Clipboard::PlainTextType) ||
theClipboard().hasTextContents(Clipboard::LaTeXTextType);
break;
}
// explicit graphics type? // explicit graphics type?
Clipboard::GraphicsType type = Clipboard::AnyGraphicsType; Clipboard::GraphicsType type = Clipboard::AnyGraphicsType;
if ((arg == "pdf" && (type = Clipboard::PdfGraphicsType)) if ((arg == "pdf" && (type = Clipboard::PdfGraphicsType))

View File

@ -42,6 +42,15 @@ public:
AnyGraphicsType AnyGraphicsType
}; };
enum TextType {
AnyTextType,
LyXOrPlainTextType,
PlainTextType,
HtmlTextType,
LaTeXTextType,
LyXTextType,
};
/** /**
* Get the system clipboard contents. The format is as written in * Get the system clipboard contents. The format is as written in
* .lyx files (may even be an older version than ours if it comes * .lyx files (may even be an older version than ours if it comes
@ -51,8 +60,8 @@ public:
* clipboard. * clipboard.
*/ */
virtual std::string const getAsLyX() const = 0; virtual std::string const getAsLyX() const = 0;
/// Get the contents of the window system clipboard in plain text format. /// Get the contents of the window system clipboard in any text format except LyxTextType.
virtual docstring const getAsText() const = 0; virtual docstring const getAsText(TextType type) const = 0;
/// Get the contents of the window system clipboard as graphics file. /// Get the contents of the window system clipboard as graphics file.
virtual FileName getAsGraphics(Cursor const & cur, GraphicsType type) const = 0; virtual FileName getAsGraphics(Cursor const & cur, GraphicsType type) const = 0;
@ -67,10 +76,8 @@ public:
*/ */
virtual void put(std::string const & lyx, docstring const & html, docstring const & text) = 0; virtual void put(std::string const & lyx, docstring const & html, docstring const & text) = 0;
/// Does the clipboard contain LyX contents?
virtual bool hasLyXContents() const = 0;
/// Does the clipboard contain text contents? /// Does the clipboard contain text contents?
virtual bool hasTextContents() const = 0; virtual bool hasTextContents(TextType type = AnyTextType) const = 0;
/// Does the clipboard contain graphics contents of a certain type? /// Does the clipboard contain graphics contents of a certain type?
virtual bool hasGraphicsContents(GraphicsType type = AnyGraphicsType) const = 0; virtual bool hasGraphicsContents(GraphicsType type = AnyGraphicsType) const = 0;
/// state of clipboard. /// state of clipboard.

View File

@ -44,6 +44,7 @@
#include <QMimeData> #include <QMimeData>
#include <QString> #include <QString>
#include <QStringList> #include <QStringList>
#include <QTextDocument>
#include <boost/crc.hpp> #include <boost/crc.hpp>
@ -98,6 +99,8 @@ QByteArray CacheMimeData::data(QString const & mimeType) const
QString const lyxMimeType(){ return "application/x-lyx"; } 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 pdfMimeType(){ return "application/pdf"; }
QString const emfMimeType(){ return "image/x-emf"; } QString const emfMimeType(){ return "image/x-emf"; }
QString const wmfMimeType(){ return "image/x-wmf"; } QString const wmfMimeType(){ return "image/x-wmf"; }
@ -328,12 +331,80 @@ FileName GuiClipboard::getAsGraphics(Cursor const & cur, GraphicsType type) cons
} }
docstring const GuiClipboard::getAsText() const 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 // 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); .normalized(QString::NormalizationForm_C);
LYXERR(Debug::ACTION, "GuiClipboard::getAsText(): `" << 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::ACTION, "GuiClipboard::getAsText(" << type << "): `" << str << "'");
if (str.isNull()) if (str.isNull())
return docstring(); return docstring();
@ -369,15 +440,27 @@ void GuiClipboard::put(string const & lyx, docstring const & html, docstring con
} }
bool GuiClipboard::hasLyXContents() const bool GuiClipboard::hasTextContents(Clipboard::TextType type) const
{ {
return cache_.hasFormat(lyxMimeType()); switch (type) {
} case AnyTextType:
return cache_.hasFormat(lyxMimeType()) || cache_.hasText() ||
cache_.hasHtml() || cache_.hasFormat(latexMimeType()) ||
bool GuiClipboard::hasTextContents() const cache_.hasFormat(texMimeType());
{ case LyXOrPlainTextType:
return cache_.hasText(); 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;
} }
@ -425,7 +508,7 @@ bool GuiClipboard::hasGraphicsContents(Clipboard::GraphicsType type) const
bool GuiClipboard::isInternal() const bool GuiClipboard::isInternal() const
{ {
if (!hasLyXContents()) if (!hasTextContents(LyXTextType))
return false; return false;
// ownsClipboard() is also true for stuff coming from dialogs, e.g. // ownsClipboard() is also true for stuff coming from dialogs, e.g.
@ -473,10 +556,10 @@ void GuiClipboard::on_dataChanged()
for (int i = 0; i < l.count(); i++) for (int i = 0; i < l.count(); i++)
LYXERR(Debug::ACTION, l.value(i)); LYXERR(Debug::ACTION, l.value(i));
text_clipboard_empty_ = qApp->clipboard()-> plaintext_clipboard_empty_ = qApp->clipboard()->
text(QClipboard::Clipboard).isEmpty(); text(QClipboard::Clipboard).isEmpty();
has_lyx_contents_ = hasLyXContents(); has_text_contents_ = hasTextContents();
has_graphics_contents_ = hasGraphicsContents(); has_graphics_contents_ = hasGraphicsContents();
} }
@ -487,9 +570,9 @@ bool GuiClipboard::empty() const
// clipboard. The plaintext version is empty if the LyX version // clipboard. The plaintext version is empty if the LyX version
// contains only one inset, and the LyX version is empty if the // contains only one inset, and the LyX version is empty if the
// clipboard does not come from LyX. // clipboard does not come from LyX.
if (!text_clipboard_empty_) if (!plaintext_clipboard_empty_)
return false; return false;
return !has_lyx_contents_ && !has_graphics_contents_; return !has_text_contents_ && !has_graphics_contents_;
} }
} // namespace frontend } // namespace frontend

View File

@ -69,11 +69,10 @@ public:
//@{ //@{
std::string const getAsLyX() const; std::string const getAsLyX() const;
FileName getAsGraphics(Cursor const & cur, GraphicsType type) const; FileName getAsGraphics(Cursor const & cur, GraphicsType type) const;
docstring const getAsText() const; docstring const getAsText(TextType type) const;
void put(std::string const & lyx, docstring const & html, docstring const & text); void put(std::string const & lyx, docstring const & html, docstring const & text);
bool hasLyXContents() const;
bool hasGraphicsContents(GraphicsType type = AnyGraphicsType) const; bool hasGraphicsContents(GraphicsType type = AnyGraphicsType) const;
bool hasTextContents() const; bool hasTextContents(TextType typetype = AnyTextType) const;
bool isInternal() const; bool isInternal() const;
bool hasInternal() const; bool hasInternal() const;
bool empty() const; bool empty() const;
@ -86,8 +85,8 @@ private Q_SLOTS:
void on_dataChanged(); void on_dataChanged();
private: private:
bool text_clipboard_empty_; bool plaintext_clipboard_empty_;
bool has_lyx_contents_; bool has_text_contents_;
bool has_graphics_contents_; bool has_graphics_contents_;
/// the cached mime data used to describe the information /// the cached mime data used to describe the information
/// that can be stored in the clipboard /// that can be stored in the clipboard

View File

@ -4329,7 +4329,7 @@ void InsetTabular::doDispatch(Cursor & cur, FuncRequest & cmd)
case LFUN_CLIPBOARD_PASTE: case LFUN_CLIPBOARD_PASTE:
case LFUN_PRIMARY_SELECTION_PASTE: { case LFUN_PRIMARY_SELECTION_PASTE: {
docstring const clip = (act == LFUN_CLIPBOARD_PASTE) ? docstring const clip = (act == LFUN_CLIPBOARD_PASTE) ?
theClipboard().getAsText() : theClipboard().getAsText(Clipboard::PlainTextType) :
theSelection().get(); theSelection().get();
if (clip.empty()) if (clip.empty())
break; break;

View File

@ -1334,7 +1334,7 @@ void InsetMathGrid::doDispatch(Cursor & cur, FuncRequest & cmd)
cap::replaceSelection(cur); cap::replaceSelection(cur);
docstring topaste; docstring topaste;
if (cmd.argument().empty() && !theClipboard().isInternal()) if (cmd.argument().empty() && !theClipboard().isInternal())
topaste = theClipboard().getAsText(); topaste = theClipboard().getAsText(Clipboard::PlainTextType);
else { else {
idocstringstream is(cmd.argument()); idocstringstream is(cmd.argument());
int n = 0; int n = 0;

View File

@ -576,7 +576,7 @@ void InsetMathNest::doDispatch(Cursor & cur, FuncRequest & cmd)
replaceSelection(cur); replaceSelection(cur);
docstring topaste; docstring topaste;
if (cmd.argument().empty() && !theClipboard().isInternal()) if (cmd.argument().empty() && !theClipboard().isInternal())
topaste = theClipboard().getAsText(); topaste = theClipboard().getAsText(Clipboard::PlainTextType);
else { else {
size_t n = 0; size_t n = 0;
idocstringstream is(cmd.argument()); idocstringstream is(cmd.argument());
@ -1461,6 +1461,13 @@ bool InsetMathNest::getStatus(Cursor & cur, FuncRequest const & cmd,
flag.setEnabled(!asHullInset()); flag.setEnabled(!asHullInset());
break; break;
case LFUN_PASTE: {
docstring const & name = cmd.argument();
if (name == "html" || name == "latex")
flag.setEnabled(false);
break;
}
default: default:
ret = false; ret = false;
break; break;