469 lines
13 KiB
C++
469 lines
13 KiB
C++
// Copyright 2009-2023 The Mumble Developers. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license
|
|
// that can be found in the LICENSE file at the root of the
|
|
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
|
|
|
#include "RichTextEditor.h"
|
|
|
|
#include "Log.h"
|
|
#include "MainWindow.h"
|
|
#include "XMLTools.h"
|
|
#include "Global.h"
|
|
|
|
#include <QtCore/QMimeData>
|
|
#include <QtGui/QImageReader>
|
|
#include <QtGui/QPainter>
|
|
#include <QtWidgets/QColorDialog>
|
|
#include <QtWidgets/QMessageBox>
|
|
#include <QtWidgets/QToolTip>
|
|
|
|
#ifdef Q_OS_WIN
|
|
# include <shlobj.h>
|
|
#endif
|
|
|
|
RichTextHtmlEdit::RichTextHtmlEdit(QWidget *p) : QTextEdit(p) {
|
|
m_document = new LogDocument(this);
|
|
m_document->setDefaultStyleSheet(qApp->styleSheet());
|
|
setDocument(m_document);
|
|
}
|
|
|
|
/* On nix, some programs send utf8, some send wchar_t. Some zeroterminate once, some twice, some not at all.
|
|
*/
|
|
|
|
static QString decodeMimeString(const QByteArray &src) {
|
|
if (src.isEmpty())
|
|
return QString();
|
|
|
|
if ((src.length() >= 4) && ((static_cast< std::size_t >(src.length()) % sizeof(ushort)) == 0)) {
|
|
const ushort *ptr = reinterpret_cast< const ushort * >(src.constData());
|
|
int len = static_cast< int >(static_cast< std::size_t >(src.length()) / sizeof(ushort));
|
|
if ((ptr[0] > 0) && (ptr[0] < 0x7f) && (ptr[1] > 0) && (ptr[1] < 0x7f)) {
|
|
while (len && (ptr[len - 1] == 0))
|
|
--len;
|
|
return QString::fromUtf16(ptr, len);
|
|
}
|
|
}
|
|
|
|
#ifdef _MSVC_LANG
|
|
# pragma warning(push)
|
|
// Disable warning about this if condition being constant
|
|
// TODO: Use if constexpr as soon as we have moved to C++17 (or higher)
|
|
# pragma warning(disable : 4127)
|
|
#endif
|
|
if ((sizeof(wchar_t) != sizeof(ushort)) && (src.length() >= static_cast< int >(sizeof(wchar_t)))
|
|
&& ((static_cast< std::size_t >(src.length()) % sizeof(wchar_t)) == 0)) {
|
|
const wchar_t *ptr = reinterpret_cast< const wchar_t * >(src.constData());
|
|
int len = static_cast< int >(static_cast< std::size_t >(src.length()) / sizeof(wchar_t));
|
|
if (*ptr < 0x7f) {
|
|
while (len && (ptr[len - 1] == 0))
|
|
--len;
|
|
return QString::fromWCharArray(ptr, len);
|
|
}
|
|
}
|
|
#ifdef _MSVC_LANG
|
|
# pragma warning(pop)
|
|
#endif
|
|
const char *ptr = src.constData();
|
|
int len = src.length();
|
|
while (len && (ptr[len - 1] == 0))
|
|
--len;
|
|
return QString::fromUtf8(ptr, len);
|
|
}
|
|
|
|
/* Try really hard to properly decode Mime into something sane.
|
|
*/
|
|
|
|
void RichTextHtmlEdit::insertFromMimeData(const QMimeData *source) {
|
|
QString uri;
|
|
QString title;
|
|
QRegExp newline(QLatin1String("[\\r\\n]"));
|
|
|
|
#ifndef QT_NO_DEBUG
|
|
qWarning() << "RichTextHtmlEdit::insertFromMimeData" << source->formats();
|
|
#endif
|
|
|
|
if (source->hasImage()) {
|
|
QImage img = qvariant_cast< QImage >(source->imageData());
|
|
QString html = Log::imageToImg(img);
|
|
if (!html.isEmpty())
|
|
insertHtml(html);
|
|
return;
|
|
}
|
|
|
|
QString mozurl = decodeMimeString(source->data(QLatin1String("text/x-moz-url")));
|
|
if (!mozurl.isEmpty()) {
|
|
QStringList lines = mozurl.split(newline);
|
|
qWarning() << mozurl << lines;
|
|
if (lines.count() >= 2) {
|
|
uri = lines.at(0);
|
|
title = lines.at(1);
|
|
}
|
|
}
|
|
|
|
if (uri.isEmpty())
|
|
uri = decodeMimeString(source->data(QLatin1String("text/x-moz-url-data")));
|
|
if (title.isEmpty())
|
|
title = decodeMimeString(source->data(QLatin1String("text/x-moz-url-desc")));
|
|
|
|
if (uri.isEmpty()) {
|
|
QStringList urls;
|
|
#ifdef Q_OS_WIN
|
|
urls = decodeMimeString(
|
|
source->data(QLatin1String("application/x-qt-windows-mime;value=\"UniformResourceLocatorW\"")))
|
|
.split(newline);
|
|
if (urls.isEmpty())
|
|
#endif
|
|
urls = decodeMimeString(source->data(QLatin1String("text/uri-list"))).split(newline);
|
|
if (!urls.isEmpty())
|
|
uri = urls.at(0).trimmed();
|
|
}
|
|
|
|
if (uri.isEmpty()) {
|
|
QUrl url(source->text(), QUrl::StrictMode);
|
|
if (url.isValid() && !url.isRelative()) {
|
|
uri = url.toString();
|
|
}
|
|
}
|
|
|
|
#ifdef Q_OS_WIN
|
|
if (title.isEmpty()
|
|
&& source->hasFormat(QLatin1String("application/x-qt-windows-mime;value=\"FileGroupDescriptorW\""))) {
|
|
QByteArray qba = source->data(QLatin1String("application/x-qt-windows-mime;value=\"FileGroupDescriptorW\""));
|
|
if (qba.length() == sizeof(FILEGROUPDESCRIPTORW)) {
|
|
const FILEGROUPDESCRIPTORW *ptr = reinterpret_cast< const FILEGROUPDESCRIPTORW * >(qba.constData());
|
|
title = QString::fromWCharArray(ptr->fgd[0].cFileName);
|
|
if (title.endsWith(QLatin1String(".url"), Qt::CaseInsensitive))
|
|
title = title.left(title.length() - 4);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
if (!uri.isEmpty()) {
|
|
if (title.isEmpty())
|
|
title = uri;
|
|
|
|
uri = uri.toHtmlEscaped();
|
|
title = title.toHtmlEscaped();
|
|
|
|
insertHtml(QString::fromLatin1("<a href=\"%1\">%2</a>").arg(uri, title));
|
|
return;
|
|
}
|
|
|
|
QString html = decodeMimeString(source->data(QLatin1String("text/html")));
|
|
if (!html.isEmpty()) {
|
|
insertHtml(html);
|
|
return;
|
|
}
|
|
|
|
QTextEdit::insertFromMimeData(source);
|
|
}
|
|
|
|
RichTextEditorLink::RichTextEditorLink(const QString &txt, QWidget *p) : QDialog(p) {
|
|
setupUi(this);
|
|
|
|
if (!txt.isEmpty()) {
|
|
qleText->setText(txt);
|
|
}
|
|
}
|
|
|
|
QString RichTextEditorLink::text() const {
|
|
QUrl url(qleUrl->text(), QUrl::StrictMode);
|
|
QString txt = qleText->text();
|
|
|
|
txt = txt.toHtmlEscaped();
|
|
|
|
if (url.isValid() && !url.isRelative() && !txt.isEmpty()) {
|
|
return QString::fromLatin1("<a href=\"%1\">%2</a>").arg(url.toString(), txt);
|
|
}
|
|
|
|
return QString();
|
|
}
|
|
|
|
RichTextEditor::RichTextEditor(QWidget *p) : QTabWidget(p) {
|
|
bChanged = false;
|
|
bModified = false;
|
|
bReadOnly = false;
|
|
|
|
setupUi(this);
|
|
|
|
qtbToolBar->addAction(qaBold);
|
|
qtbToolBar->addAction(qaItalic);
|
|
qtbToolBar->addAction(qaUnderline);
|
|
qtbToolBar->addAction(qaColor);
|
|
qtbToolBar->addSeparator();
|
|
qtbToolBar->addAction(qaLink);
|
|
qtbToolBar->addAction(qaImage);
|
|
|
|
connect(this, SIGNAL(currentChanged(int)), this, SLOT(onCurrentChanged(int)));
|
|
updateActions();
|
|
|
|
qteRichText->setFocus();
|
|
|
|
qteRichText->installEventFilter(this);
|
|
qptePlainText->installEventFilter(this);
|
|
}
|
|
|
|
bool RichTextEditor::isModified() const {
|
|
return bModified;
|
|
}
|
|
|
|
void RichTextEditor::on_qaBold_triggered(bool on) {
|
|
qteRichText->setFontWeight(on ? QFont::Bold : QFont::Normal);
|
|
}
|
|
|
|
void RichTextEditor::on_qaItalic_triggered(bool on) {
|
|
qteRichText->setFontItalic(on);
|
|
}
|
|
|
|
void RichTextEditor::on_qaUnderline_triggered(bool on) {
|
|
qteRichText->setFontUnderline(on);
|
|
}
|
|
|
|
void RichTextEditor::on_qaColor_triggered() {
|
|
QColor c = QColorDialog::getColor();
|
|
if (c.isValid())
|
|
qteRichText->setTextColor(c);
|
|
}
|
|
|
|
void RichTextEditor::on_qaLink_triggered() {
|
|
QTextCursor qtc = qteRichText->textCursor();
|
|
RichTextEditorLink *rtel = new RichTextEditorLink(qtc.selectedText(), this);
|
|
if (rtel->exec() == QDialog::Accepted) {
|
|
QString html = rtel->text();
|
|
if (!html.isEmpty())
|
|
qteRichText->insertHtml(html);
|
|
}
|
|
delete rtel;
|
|
}
|
|
|
|
void RichTextEditor::on_qaImage_triggered() {
|
|
QPair< QByteArray, QImage > choice = Global::get().mw->openImageFile();
|
|
|
|
QByteArray &qba = choice.first;
|
|
|
|
if (qba.isEmpty())
|
|
return;
|
|
|
|
if ((Global::get().uiImageLength > 0)
|
|
&& (static_cast< unsigned int >(qba.length()) > Global::get().uiImageLength)) {
|
|
QMessageBox::warning(this, tr("Failed to load image"),
|
|
tr("Image file too large to embed in document. Please use images smaller than %1 kB.")
|
|
.arg(Global::get().uiImageLength / 1024));
|
|
return;
|
|
}
|
|
|
|
QBuffer qb(&qba);
|
|
qb.open(QIODevice::ReadOnly);
|
|
|
|
QByteArray format = QImageReader::imageFormat(&qb);
|
|
qb.close();
|
|
|
|
qteRichText->insertHtml(Log::imageToImg(format, qba));
|
|
}
|
|
|
|
void RichTextEditor::onCurrentChanged(int index) {
|
|
if (!bChanged)
|
|
return;
|
|
|
|
if (index == 1)
|
|
richToPlain();
|
|
else
|
|
qteRichText->setHtml(qptePlainText->toPlainText());
|
|
|
|
bChanged = false;
|
|
}
|
|
|
|
void RichTextEditor::on_qptePlainText_textChanged() {
|
|
bModified = true;
|
|
bChanged = true;
|
|
}
|
|
|
|
void RichTextEditor::on_qteRichText_textChanged() {
|
|
bModified = true;
|
|
bChanged = true;
|
|
updateActions();
|
|
|
|
if (!Global::get().uiMessageLength)
|
|
return;
|
|
|
|
richToPlain();
|
|
|
|
const QString &plainText = qptePlainText->toPlainText();
|
|
|
|
bool over = true;
|
|
|
|
unsigned int imagelength = static_cast< unsigned int >(plainText.length());
|
|
|
|
|
|
if (Global::get().uiMessageLength && imagelength <= Global::get().uiMessageLength) {
|
|
over = false;
|
|
} else if (Global::get().uiImageLength && imagelength > Global::get().uiImageLength) {
|
|
over = true;
|
|
} else {
|
|
QString qsOut;
|
|
QXmlStreamReader qxsr(QString::fromLatin1("<document>%1</document>").arg(plainText));
|
|
QXmlStreamWriter qxsw(&qsOut);
|
|
while (!qxsr.atEnd()) {
|
|
switch (qxsr.readNext()) {
|
|
case QXmlStreamReader::Invalid:
|
|
return;
|
|
case QXmlStreamReader::StartElement: {
|
|
if (qxsr.name() == QLatin1String("img")) {
|
|
QXmlStreamAttributes attr = qxsr.attributes();
|
|
|
|
qxsw.writeStartElement(qxsr.namespaceUri().toString(), qxsr.name().toString());
|
|
foreach (const QXmlStreamAttribute &a, qxsr.attributes())
|
|
if (a.name() != QLatin1String("src"))
|
|
qxsw.writeAttribute(a);
|
|
} else {
|
|
qxsw.writeCurrentToken(qxsr);
|
|
}
|
|
} break;
|
|
default:
|
|
qxsw.writeCurrentToken(qxsr);
|
|
break;
|
|
}
|
|
}
|
|
over = (static_cast< unsigned int >(qsOut.length()) > Global::get().uiMessageLength);
|
|
}
|
|
|
|
|
|
QString tooltip = tr("Message is too long.");
|
|
|
|
if (!over) {
|
|
if (QToolTip::text() == tooltip)
|
|
QToolTip::hideText();
|
|
} else {
|
|
QPoint p = QCursor::pos();
|
|
const QRect &r = qteRichText->rect();
|
|
if (!r.contains(qteRichText->mapFromGlobal(p)))
|
|
p = qteRichText->mapToGlobal(r.center());
|
|
QToolTip::showText(p, tooltip, qteRichText);
|
|
}
|
|
}
|
|
|
|
void RichTextEditor::on_qteRichText_cursorPositionChanged() {
|
|
updateActions();
|
|
}
|
|
|
|
void RichTextEditor::on_qteRichText_currentCharFormatChanged() {
|
|
updateActions();
|
|
}
|
|
|
|
void RichTextEditor::updateColor(const QColor &col) {
|
|
if (col == qcColor)
|
|
return;
|
|
qcColor = col;
|
|
|
|
QRect r(0, 0, 24, 24);
|
|
|
|
QPixmap qpm(r.size());
|
|
QPainter qp(&qpm);
|
|
qp.fillRect(r, col);
|
|
qp.setPen(col.darker());
|
|
qp.drawRect(r.adjusted(0, 0, -1, -1));
|
|
|
|
qaColor->setIcon(qpm);
|
|
}
|
|
|
|
void RichTextEditor::updateActions() {
|
|
qaBold->setChecked(qteRichText->fontWeight() == QFont::Bold);
|
|
qaItalic->setChecked(qteRichText->fontItalic());
|
|
qaUnderline->setChecked(qteRichText->fontUnderline());
|
|
updateColor(qteRichText->textColor());
|
|
}
|
|
|
|
void RichTextEditor::richToPlain() {
|
|
QXmlStreamReader reader(qteRichText->toHtml());
|
|
|
|
QString qsOutput;
|
|
QXmlStreamWriter writer(&qsOutput);
|
|
|
|
int paragraphs = 0;
|
|
|
|
QMap< QString, QString > def;
|
|
|
|
def.insert(QLatin1String("margin-top"), QLatin1String("0px"));
|
|
def.insert(QLatin1String("margin-bottom"), QLatin1String("0px"));
|
|
def.insert(QLatin1String("margin-left"), QLatin1String("0px"));
|
|
def.insert(QLatin1String("margin-right"), QLatin1String("0px"));
|
|
def.insert(QLatin1String("-qt-block-indent"), QLatin1String("0"));
|
|
def.insert(QLatin1String("text-indent"), QLatin1String("0px"));
|
|
|
|
XMLTools::recurseParse(reader, writer, paragraphs, def);
|
|
|
|
qsOutput = qsOutput.trimmed();
|
|
|
|
bool changed;
|
|
do {
|
|
// Make sure the XML has a root element (would be invalid XML otherwise)
|
|
// The "unduplicate" element will be dropped by XMLTools::unduplciateTags
|
|
qsOutput = QString::fromLatin1("<unduplicate>%1</unduplicate>").arg(qsOutput);
|
|
|
|
QXmlStreamReader r(qsOutput);
|
|
qsOutput = QString();
|
|
QXmlStreamWriter w(&qsOutput);
|
|
changed = XMLTools::unduplicateTags(r, w);
|
|
qsOutput = qsOutput.trimmed();
|
|
} while (changed);
|
|
|
|
qptePlainText->setPlainText(qsOutput);
|
|
}
|
|
|
|
void RichTextEditor::setText(const QString &txt, bool readonly) {
|
|
qtbToolBar->setEnabled(!readonly && Global::get().bAllowHTML);
|
|
qtbToolBar->setVisible(!readonly && Global::get().bAllowHTML);
|
|
qptePlainText->setReadOnly(readonly || !Global::get().bAllowHTML);
|
|
qteRichText->setReadOnly(readonly);
|
|
|
|
qteRichText->setHtml(txt);
|
|
qptePlainText->setPlainText(txt);
|
|
|
|
bChanged = false;
|
|
bModified = false;
|
|
bReadOnly = readonly;
|
|
}
|
|
|
|
QString RichTextEditor::text() {
|
|
if (bChanged) {
|
|
if (currentIndex() == 0)
|
|
richToPlain();
|
|
else
|
|
qteRichText->setHtml(qptePlainText->toPlainText());
|
|
}
|
|
|
|
bChanged = false;
|
|
return qptePlainText->toPlainText();
|
|
}
|
|
|
|
bool RichTextEditor::eventFilter(QObject *obj, QEvent *evt) {
|
|
if (obj != qptePlainText && obj != qteRichText)
|
|
return false;
|
|
if (evt->type() == QEvent::KeyPress) {
|
|
QKeyEvent *keyEvent = static_cast< QKeyEvent * >(evt);
|
|
if (((keyEvent->key() == Qt::Key_Enter) || (keyEvent->key() == Qt::Key_Return))
|
|
&& (keyEvent->modifiers() == Qt::ControlModifier)) {
|
|
emit accept();
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool RichTextImage::isValidImage(const QByteArray &ba, QByteArray &fmt) {
|
|
QBuffer qb;
|
|
qb.setData(ba);
|
|
if (!qb.open(QIODevice::ReadOnly)) {
|
|
return false;
|
|
}
|
|
|
|
QByteArray detectedFormat = QImageReader::imageFormat(&qb).toLower();
|
|
if (detectedFormat == QByteArray("png") || detectedFormat == QByteArray("jpg")
|
|
|| detectedFormat == QByteArray("jpeg") || detectedFormat == QByteArray("gif")) {
|
|
fmt = detectedFormat;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|