Source file: src/html_editor.cpp
/* Copyright (C) 2004-2014 Daniel Verite
This file is part of Manitou-Mail (see http://www.manitou-mail.org)
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License version 2 as
published by the Free Software Foundation.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330,
Boston, MA 02111-1307, USA.
*/
#include "html_editor.h"
#include <QToolBar>
#include <QToolButton>
#include <QDebug>
#include <QVariant>
#include <QWebFrame>
#include <QAction>
#include <QLayout>
#include <QPushButton>
#include <QLineEdit>
#include <QLabel>
#include <QDesktopServices>
#include <QUrl>
#include <QFileDialog>
#include <QFile>
#include <QColorDialog>
#include <QMessageBox>
#include <QFontDatabase>
#include <QComboBox>
#include <QApplication>
#include <QDesktopServices>
#include <QDropEvent>
#include <QWebSettings>
#include "icons.h"
struct html_editor::action_type
html_editor::m_action_definitions[] = {
#define NA QWebPage::NoWebAction
// {name, shortcut, icon, WebAction
{QT_TR_NOOP("Bold"), QT_TR_NOOP("Ctrl+B"), "icon-bold.png", SLOT(bold()), NA }
,{QT_TR_NOOP("Italic"), QT_TR_NOOP("Ctrl+I"), "icon-italic.png", SLOT(italic()), NA }
,{QT_TR_NOOP("Underline"), QT_TR_NOOP("Ctrl+U"), "icon-underline.png", SLOT(underline()), NA }
,{QT_TR_NOOP("Strike through"), QT_TR_NOOP("Ctrl+S"), "icon-strikethrough.png", SLOT(strikethrough()) , NA}
,{QT_TR_NOOP("Superscript"), NULL, "icon-superscript.png", SLOT(superscript()), NA }
,{QT_TR_NOOP("Subscript"), NULL, "icon-subscript.png", SLOT(subscript()), NA }
// separator before 6
,{QT_TR_NOOP("Set foreground color"), NULL, "icon-foreground-color.png", SLOT(foreground_color()), NA }
,{QT_TR_NOOP("Set background color"), NULL, "icon-background-color.png", SLOT(background_color()), NA }
,{QT_TR_NOOP("Insert image"), NULL, "icon-image.png", SLOT(insert_image()), NA }
,{QT_TR_NOOP("Insert link"), NULL, "icon-link.png", SLOT(insert_link()), NA }
,{QT_TR_NOOP("Insert horizontal rule"), NULL, "icon-hr.png", SLOT(insert_hr()), NA }
,{QT_TR_NOOP("Insert unordered list"), NULL, "icon-bullet-list.png", SLOT(insert_unordered_list()), NA }
,{QT_TR_NOOP("Insert ordered list"), NULL, "icon-ordered-list.png", SLOT(insert_ordered_list()), NA }
,{QT_TR_NOOP("Remove format"), NULL, "icon-eraser.png", SLOT(remove_format()), NA }
,{QT_TR_NOOP("Cut"), NULL, "icon-cut.png", NULL, QWebPage::Cut }
,{QT_TR_NOOP("Copy"), NULL, "icon-copy.png", NULL, QWebPage::Copy }
,{QT_TR_NOOP("Paste"), NULL, "icon-paste.png", NULL, QWebPage::Paste }
,{QT_TR_NOOP("Undo"), QT_TR_NOOP("Ctrl+Z"), "icon-undo.png", NULL, QWebPage::Undo }
,{QT_TR_NOOP("Redo"), NULL, "icon-redo.png", NULL, QWebPage::Redo }
,{QT_TR_NOOP("Align left"), NULL, "icon-align-left.png", SLOT(align_left()), NA }
,{QT_TR_NOOP("Justify"), NULL, "icon-justify.png", SLOT(justify()), NA }
,{QT_TR_NOOP("Align right"), NULL, "icon-align-right.png", SLOT(align_right()), NA }
,{QT_TR_NOOP("Center"), NULL, "icon-center.png", SLOT(center()), NA }
,{QT_TR_NOOP("Indent right"), NULL, "icon-indent-right.png", SLOT(indent_right()), NA }
,{QT_TR_NOOP("Indent left"), NULL, "icon-indent-left.png", SLOT(indent_left()), NA }
#undef NA
};
link_editor::link_editor(QWidget* parent): QDialog(parent)
{
setWindowTitle(tr("Edit link"));
QVBoxLayout* layout = new QVBoxLayout(this);
QGridLayout* grid = new QGridLayout();
layout->addLayout(grid);
m_link = new QLineEdit();
m_text = new QLineEdit();
m_link->setText("http://");
grid->addWidget(new QLabel(tr("Link:")), 0, 0);
grid->addWidget(m_link, 0, 1);
grid->addWidget(new QLabel(tr("Text:")), 1, 0);
grid->addWidget(m_text, 1, 1);
QHBoxLayout* buttons_layout = new QHBoxLayout();
layout->addLayout(buttons_layout);
QPushButton* preview = new QPushButton(tr("Preview"));
buttons_layout->addWidget(preview);
connect(preview, SIGNAL(clicked()), SLOT(preview()));
buttons_layout->addStretch(3);
QPushButton* wok = new QPushButton(tr("OK"));
buttons_layout->addWidget(wok);
connect(wok, SIGNAL(clicked()), SLOT(accept()));
wok->setDefault(true);
buttons_layout->addStretch(2);
QPushButton* wcancel = new QPushButton(tr("Cancel"));
buttons_layout->addWidget(wcancel);
connect(wcancel, SIGNAL(clicked()), SLOT(reject()));
}
link_editor::~link_editor()
{
}
void
link_editor::preview()
{
if (!url().isEmpty()) {
QDesktopServices::openUrl(QUrl(url(), QUrl::TolerantMode));
}
}
QString
link_editor::url()
{
return m_link->text();
}
QString
link_editor::text()
{
return m_text->text();
}
html_editor::html_editor(QWidget* parent): QWebView(parent)
{
create_actions();
connect(this, SIGNAL(loadFinished(bool)), SLOT(load_finished(bool)));
page()->setLinkDelegationPolicy(QWebPage::DelegateAllLinks);
connect(this, SIGNAL(linkClicked(const QUrl&)), SLOT(link_clicked(const QUrl&)));
QWebSettings* settings = page()->settings();
settings->setFontFamily(QWebSettings::StandardFont, "Arial");
settings->setFontSize(QWebSettings::DefaultFontSize, 14);
}
html_editor::~html_editor()
{
// delete the QActions we allocated
for (uint i=0; i<sizeof(m_action_definitions)/sizeof(m_action_definitions[0]); i++) {
struct action_type* def = &m_action_definitions[i];
if (def->webaction == QWebPage::NoWebAction) {
// we own only the actions that aren't provided by QWebPage
QMap<const char*, QAction*>::const_iterator iter = m_actions.find(def->name);
if (iter != m_actions.end())
delete iter.value();
}
}
}
void
html_editor::insert_link()
{
link_editor le(this);
if (le.exec() == QDialog::Accepted) {
QString url = le.url();
QString text = le.text();
if (text.isEmpty())
text = url;
url.replace("\"", "\\\""); // replace " by \"
QString h = QString("<a href=\"%1\">%2</a>").arg(url).arg(text);
insert_html(h);
}
}
QString
html_editor::html_text() const
{
return page()->mainFrame()->toHtml();
}
void
html_editor::set_html_text(const QString& text)
{
m_load_finished = false;
setHtml(text);
}
void
html_editor::bold()
{
exec_command("Bold");
}
void
html_editor::italic()
{
exec_command("Italic");
}
void
html_editor::underline()
{
exec_command("underline");
}
void
html_editor::subscript()
{
exec_command("subscript");
}
void
html_editor::superscript()
{
exec_command("superscript");
}
void
html_editor::strikethrough()
{
exec_command("strikeThrough");
}
void
html_editor::remove_format()
{
exec_command("RemoveFormat");
}
void html_editor::undo()
{
page()->triggerAction(QWebPage::Undo);
}
void html_editor::redo()
{
page()->triggerAction(QWebPage::Redo);
}
void html_editor::align_right()
{
exec_command("JustifyRight");
}
void html_editor::align_left()
{
exec_command("JustifyLeft");
}
void html_editor::justify()
{
exec_command("JustifyFull");
}
void html_editor::center()
{
exec_command("JustifyCenter");
}
void html_editor::indent_left()
{
exec_command("Outdent");
}
void html_editor::indent_right()
{
exec_command("Indent");
}
void html_editor::insert_hr()
{
exec_command("InsertHorizontalRule");
}
void html_editor::insert_unordered_list()
{
exec_command("InsertUnorderedList");
}
void html_editor::insert_ordered_list()
{
exec_command("InsertOrderedList");
}
void
html_editor::foreground_color()
{
QColor color = QColorDialog::getColor(Qt::black, this);
if (color.isValid())
exec_command("foreColor", color.name());
}
void
html_editor::background_color()
{
QColor color = QColorDialog::getColor(Qt::white, this);
if (color.isValid())
exec_command("hiliteColor", color.name());
}
void
html_editor::insert_image()
{
QString filters;
filters += tr("Image files (*.png *.jpg *.jpeg *.gif)") + ";;";
filters += tr("PNG files (*.png)") + ";;";
filters += tr("JPEG files (*.jpg *.jpeg)") + ";;";
filters += tr("GIF files (*.gif)") + ";;";
filters += tr("All Files (*)");
QString fname = QFileDialog::getOpenFileName(this, tr("Open image..."),
QString(), filters);
if (!fname.isEmpty() && QFile::exists(fname)) {
QUrl url = QUrl::fromLocalFile(fname);
exec_command("insertImage", url.toString());
}
}
void
html_editor::exec_command(const QString cmd, const QString arg)
{
QString jscript;
if (!arg.isEmpty()) {
QString h = arg;
h.replace("'", "\\'");
jscript = QString("document.execCommand('%1',false,'%2');").arg(cmd).arg(h);
}
else
jscript = QString("document.execCommand('%1',false,null);").arg(cmd);
QVariant res = page()->mainFrame()->evaluateJavaScript(jscript);
// qDebug() << res;
}
const char*
html_editor::manitou_files_jscript =
"manitou_files = {"
" get_references : function() {"
" var result = [];"
" manitou_files._process_references(document.getElementsByTagName('body')[0], result, null);"
" return result;"
" },"
" replace_references : function(cids) {"
" manitou_files._process_references(document.getElementsByTagName('body')[0], null, cids);"
" },"
" _process_references: function(node,arr,repl) {"
" if (node.hasChildNodes) {"
" for (var cn=0;cn<node.childNodes.length;cn++) {"
" manitou_files._process_references(node.childNodes[cn], arr, repl);"
" }"
" }"
" if (node.nodeType==1) {"
" a = node.attributes.getNamedItem('src');"
" if (a!=null) {"
" s = a.value;"
" if (s.length > 0 && s.toLowerCase().indexOf('file:///') != -1) {"
" if (arr!=null)"
" arr.push(s);"
" if (repl!=null) {"
" s1 = repl[s];"
" if (s1!=null)"
" node.setAttribute('src', 'cid:'+s1);"
" }"
" }"
" }"
" }"
" }"
"};"
;
QStringList
html_editor::collect_local_references()
{
page()->mainFrame()->evaluateJavaScript(manitou_files_jscript);
QVariant res = page()->mainFrame()->evaluateJavaScript("manitou_files.get_references()");
// TODO: unify "file://filename" and "filename" and remove duplicates
return res.toStringList();
}
void
html_editor::replace_local_references(const QMap<QString,QString>& map)
{
page()->mainFrame()->evaluateJavaScript(manitou_files_jscript);
QString jscript = "var n=[];";
QMap<QString,QString>::const_iterator i = map.constBegin();
/* build a javascript snippet that maps each "source" reference
(typically something like file:///home/user/foobar.gif) to each
mime content id (cid:uniqueref@domain) */
while (i != map.constEnd()) {
QString source = i.key();
source.replace("'", "\\'");
QString dest = i.value();
dest.replace("'", "\\'");
jscript.append(QString("n['%1']='%2';").arg(source).arg(dest));
++i;
}
jscript.append("manitou_files.replace_references(n); 1");
QVariant res=page()->mainFrame()->evaluateJavaScript(jscript);
}
void
html_editor::insert_html(const QString html)
{
exec_command("InsertHTML", html);
}
bool
html_editor::eval_jscript(const QString script)
{
QString fragment = script;
fragment.prepend("try {");
fragment.append("\n} catch(e) { e; }");
QVariant res = page()->mainFrame()->evaluateJavaScript(fragment);
return true; // TODO: peek into result
}
QList<QToolBar*>
html_editor::create_toolbars()
{
QList<QToolBar*> toolbars;
QToolBar* toolbar1 = new QToolBar(tr("HTML input"), this);
toolbar1->setIconSize(QSize(16,16));
QToolBar* toolbar2 = new QToolBar(tr("HTML operations"), this);
toolbar2->setIconSize(QSize(16,16));
QComboBox* fonts = new QComboBox();
m_font_chooser = fonts;
QStringList families = QFontDatabase().families();
fonts->addItems(families);
connect(fonts, SIGNAL(activated(const QString&)),
this, SLOT(change_font(const QString&)));
toolbar1->addWidget(fonts);
QStringList sizes;
sizes << "xx-small";
sizes << "x-small";
sizes << "small";
sizes << "medium";
sizes << "large";
sizes << "x-large";
sizes << "xx-large";
QComboBox* sizes_box = new QComboBox();
m_para_format_chooser = sizes_box;
sizes_box->addItems(sizes);
connect(sizes_box, SIGNAL(activated(int)), this, SLOT(change_font_size(int)));
toolbar1->addWidget(sizes_box);
QStringList headerN;
for (uint i=1; i<=6; i++) {
headerN << QString(tr("Header %1")).arg(i);
}
QComboBox* headers = new QComboBox();
headers->addItems(headerN);
m_headers_chooser = headers;
connect(headers, SIGNAL(activated(int)), this, SLOT(format_header(int)));
toolbar1->addWidget(headers);
for (uint i=0; i<sizeof(m_action_definitions)/sizeof(m_action_definitions[0]); i++) {
struct action_type* def = &m_action_definitions[i];
QMap<const char*, QAction*>::const_iterator iter = m_actions.find(def->name);
if (iter != m_actions.end()) {
if (i <= 5)
toolbar1->addAction(iter.value());
else
toolbar2->addAction(iter.value());
}
}
toolbars.push_back(toolbar1);
toolbars.push_back(toolbar2);
return toolbars;
}
void
html_editor::enable_html_controls(bool enable)
{
QMapIterator<const char*, QAction*> it(m_actions);
while (it.hasNext()) {
it.next();
it.value()->setEnabled(enable);
}
m_font_chooser->setEnabled(enable);
m_para_format_chooser->setEnabled(enable);
m_headers_chooser->setEnabled(enable);
}
void
html_editor::change_font(const QString& family)
{
exec_command("fontName", family);
setFocus();
}
void
html_editor::change_font_size(int size_index)
{
exec_command("fontSize", QString::number(size_index));
setFocus();
}
void
html_editor::format_header(int size)
{
exec_command("formatBlock", QString("h%1").arg(size+1));
setFocus();
}
void
html_editor::create_actions()
{
for (uint i=0; i<sizeof(m_action_definitions)/sizeof(m_action_definitions[0]); i++) {
struct action_type* def = &m_action_definitions[i];
QAction* a;
if (def->webaction == QWebPage::NoWebAction) {
// the action is owned by us
a = new QAction(tr(def->name), this);
}
else {
// action already provided by QWebPage
a = page()->action(def->webaction);
}
if (a) {
a->setIcon(HTML_ICON(def->icon));
if (def->shortcut)
a->setShortcut(tr(def->shortcut));
m_actions[def->name] = a;
if (def->slot)
connect(a, SIGNAL(triggered()), this, def->slot);
}
}
}
void
html_editor::finish_load()
{
while (!m_load_finished) {
QApplication::processEvents();
}
}
void
html_editor::load_finished(bool ok)
{
m_load_finished=true;
if (ok) {
page()->setContentEditable(true);
// let the cursor be visible
// page()->mainFrame()->evaluateJavaScript("window.getSelection().collapse(document.getElementsByTagName('body')[0], 0)");
}
}
void
html_editor::append_paragraph(const QString& fragment)
{
QString h = fragment;
h.replace("'", "\\'");
h.replace("\n", "<br>");
QString jscript = QString("try {var b=document.getElementsByTagName('body')[0]; var p=document.createElement('p'); p.innerHTML='%1'; b.appendChild(p); 1;} catch(e) { e; }").arg(h);
QVariant res = page()->mainFrame()->evaluateJavaScript(jscript);
}
QString
html_editor::to_plain_text() const
{
return page()->mainFrame()->toPlainText();
}
void
html_editor::link_clicked(const QUrl& url)
{
if (!QDesktopServices::openUrl(QUrl(url))) {
QMessageBox::critical(NULL, tr("Error"), tr("Unable to open URL"));
}
}
void
html_editor::dragEnterEvent(QDragEnterEvent* event)
{
/* it's necessary on Windows to explicitly accept the event since
the default implementation doesn't seem to accept it for dragged
files */
if (event->mimeData()->hasUrls())
event->acceptProposedAction();
else
QWebView::dragEnterEvent(event);
}
void
html_editor::dragMoveEvent(QDragMoveEvent* event)
{
/* We don't let the base dragMoveEvent handle URLs to avoid it
taking control over the caret and not releasing it after the drop */
if (!event->mimeData()->hasUrls())
QWebView::dragMoveEvent(event);
}
void
html_editor::dropEvent(QDropEvent* event)
{
if (event->mimeData()->hasUrls()) {
foreach (QUrl url, event->mimeData()->urls()) {
emit attach_file_request(url);
}
event->acceptProposedAction();
}
else
QWebView::dropEvent(event);
}
HTML source code generated by GNU Source-Highlight plus some custom post-processing
List of all available source files