Integrating an HTML Editor into Qt using Javascript and QWebView

published at 25.08.2015 16:27 by Jens Weller

Welcome to the 8th installment of my series on writing applications in C++ with Qt and boost. The last post was about signaling and messaging in C++. This time its about integrating an HTML Editor into Qt using QWebView and Javascript! I'll start with text editors in general, and then continue to the integration, based on QWebkit and TinyMCE3. The end result is a little bit of hackery, but it is a working solution. I did not need to use any dirty tricks to make this work, like writing a server running on localhost to supply images and other data as a customization point.

The video, if you rather listen / view then read:

So, I did get this crazy idea of writing my own CMS this summer. I've been looking for about a year on different solutions and approaches to building websites. I even checked the options for writing websites with C++. CppCMS or TreeFrog are frameworks which enable you to do so. But it does not fit my use case, and I want to be able to later also integrate my own tooling for the conference and other needs. One of them is, that of course, I want to keep my work flow which I currently have, part of this is writing HTML like things in a WYSIWYG editor for blog posts etc. I want to avoid writing raw HTML or copy pasta from Open Office etc. So, I need a capable HTML Editor in my Qt Application.

KDE Framework 5 has a text editor framework, which would be interesting to use, even that I'm not sure if it has the HTML capability. Also it would bring a LOT of dependencies into my project. When I'd like to keep my work flow, why not use what drives writing this and all other blog posts well for years? So it turns out integrating tinymce into my Qt application would be the optimal solution for me. The end result:

../../files/blog/tinymceinqt.png

Integrating TinyMCE into Qt

When I started, this nearly drove me crazy. This is also my first real use of javascript, I have never used js so far for anything except basic things in webdev. I did also experiment with CKEditor and started with TinyMCE4. I could not get TinyMCE4 to run in the QWebKit based QWebView, and as TinyMCE3 has worked well for years, I consider its probably the best option for me. I have derived a HTMLTextEditor class from QWebView, as this would allow me to also easily overwrite any behavoir form QWebView, but so far that was not needed. Also, when searching the web for this, I found a project doing something similar, it helped me solve some issues at the start, but adding TinyMCE to the Qt resource system was something I wanted to avoid. Also, the solution is from 2011, and does not derive a class from QWebView, which I prefer in this case. Also with 0 downloads, I didn't want to base such a critical component on obviously un(der)used solution.

One of the problems making this integration tricky, is that the editors are not meant to be used in such an environment. They have evolved as the needed tooling in a web driven environment, and often use customization points which are not easy to mimic from C++ land. Also, as the editor lives inside the QWebView, all dialogs do so too. These dialogs are often fixed in size, and this is a little problem when the web view is not big enough, scroll bars are ugly. Also trying to move such a dialog is not the best user experience, since its caught in its little web view, as the user is focused on the application.

While most of the functionality of the text editor is working right out of the box, there two customization points needed: selecting images and links from the actual model. TinyMCE3 has a customization point for this: external_image_list_url. This is expecting a file system url, I did write a small server based on QTcpServer to test if I could hand over the image list in this way. This did not work, external_image_list_url:127.0.0.1 produced a hit, but sending back a basic HTTP response did not lead to a success. Also, I really don't want to integrate a server for this into my application. There has to be a better way, also, going down this site of the rabbit hole would mean to use the dialogs of TinyMCE3 for images and links, which I'd like to replace with Qt based dialogs in my application.

One other problem is the base uri which the editor accepts as its home, so far I had no luck of setting it manually. As I can't load the editor view setHtml into the QWebView, it currently has to be an html file on the file system. The location of this file is automatically its base uri. My solution to this problem is, to simply copy the editor.html from a template into the correct position for each project when its created. This works.

Connecting C++, Qt and TinyMCE

There are some ways to interact with Javascript from C++. Googles V8 engine has its own library, and there are other libraries build on top of this. Qt also has the ability to connect to Javascript, and offers with QML even its own JS compatible UI framework, which is unique to Qt. This is driven by code, which has its root in the scripting capabilities for Qt and QWebKit. QWebkit has a Javascript/C++ bridge allowing to expose QObject based classes to js, this enables also to emit signals from Javascript, and call methods on such a QObject, if they are marked with Q_INVOKABLE. I went both ways, and currently think that Q_INVOKABLE is a bit better. Also, it is fairly easy to execute Javascript code from C++, so that the full round trip is possible js -> C++ -> js. This is important to select images in C++ and then insert them in the editor via the Javascript API of TinyMCE.

A first look at the HTMLTextEditor class:

class HTMLTextEditor : public QWebView
{
    Q_OBJECT// Qt fun...
    QStringList imagelist, linklist; // lists to select image and links from
    QWebFrame* mainframe; // access to the internal frame containing the editor
    QString basepath,relative; // basepath and relative (to webroot (e.g. .|../) path
public:
    explicit HTMLTextEditor(QWidget *parent = 0); // standard QWigdet constructor
    QString getContent()const;// get HTML from the editor
    void setContent(QString c); // set HTML
    Q_INVOKABLE void insertLink(); // Qt entry point for link insertion
    QString text()const{return getContent();} // interface for the EventFilter classes
void setImagelist(const QStringList &value);// setters void setLinklist(const QStringList &value);
void setBasePath(const QString& bp);
void setRelative(const QString &value); signals: void selectImage();// signal emitted from js private slots: void onSelectImage();// slot connected to the js emitted signal private: QVariant execJS(const QString& js) const;// exec js };

As you see, using a signal creates a bit more noise in the code then just adding Q_INVOKABLE to a method. The setup is split into the constructor and setBasePath:

HTMLTextEditor::HTMLTextEditor(QWidget *p):QWebView(p)
{
    page()->setLinkDelegationPolicy(QWebPage::DelegateExternalLinks);
    connect(this,SIGNAL(selectImage()),this,SLOT(onSelectImage()));
}
void HTMLTextEditor::setBasePath(const QString &bp)
{
    basepath = bp;
    setUrl(QUrl(basepath+"/editor.html")); //load editor
    mainframe = page()->mainFrame(); // get internal QWebFrame which holds the editor
    mainframe->addToJavaScriptWindowObject("hostObject",this); // make us known to js land
}

Via QWebView::page() you get access to the internal QWebPage object, which is not like QWebView a QWidget. Setting the link delegation policy prevents the QWebView of opening external links in the editor. Otherwise any click on an external link (e.g. http://meetingcpp.com) would open that website in the editor. And as the object just has been created, lets make the javascript part of the QWebView know about the object with addToJavaScriptWindowObject. Now, you can emit the signals and call the Q_INVOKABLE methods from Javascript using window.hostObject. In order to do this at the correct point, I needed to implement to tinymce plugins: one for linking and one for images. All they do is calling/emitting insertLink/selectImage. Currently it is not possible to edit inserted images or links, but it would be possible to do this, as parameters are allowed for signals and methods interacting with Javascript. These parameters are restricted to Qt standard types known to the QWebkit C++/js bridge. In this case QString would be enough.

This is the code selecting the image:

void HTMLTextEditor::onSelectImage()// the slot handling the js signal
{
    ImageDialog dlg(basepath + "/img/",imagelist,this);
    if(dlg.exec()!= QDialog::Accepted)return;
    QString alt,img;
    dlg.transferData(alt,img);
    QString js = R"(ed = tinyMCE.activeEditor; ed.execCommand('mceInsertContent',false,ed.dom.createHTML('img',{src : "img/%1",alt : "%2"}), {skip_undo : 1}); ed.undoManager.add();)";
    execJS(js.arg(relative + img,alt));
}

I use the C++11 feature of raw strings, as it makes it a lot easier to embed this js code into the C++ code. All images are stored under /img/, when the user selects an image it gets inserted via the js api of tinymce. execJS is a method executing all Javascript in this class, so that I could add easily logging etc. to one method, instead to many. Currently one can only insert images with src and alt, the ImageDialog is still a prototype:

../../files/blog/imagedialog.jpg

The code doing the C++ part for inserting links is very similar:

void HTMLTextEditor::insertLink()
{
    LinkDialog dlg(linklist,this);
    if(dlg.exec() != QDialog::Accepted)return;
    QString link;
    dlg.transferData(link);
    execJS(QString(R"(ed = tinyMCE.activeEditor;
tinyMCE.execCommand('createlink',false, "%1");
ed.selection.collapse();
ed.nodeChanged();)").arg(relative +link));
}

This time the js code is a bit more complex, but actually the 2nd line of the js code does the work. The rest is just for better integration and canceling the selection. Getting/Setting the content of the editor is fairly easy to:

QString HTMLTextEditor::getContent() const
{
    return execJS("tinyMCE.activeEditor.getContent();").toString();
}

void HTMLTextEditor::setContent(QString c)
{
    execJS(QString(R"(tinyMCE.activeEditor.setContent("%1");)").arg(c.replace("\n","\\n").replace("\"","\\\"");//replace: hack/fix for multilinecontent
}

The getContent method hints, that execJS returns a value which comes from Javascript. But the setContent method deserves some attention. It looks so easy, that when testing I first didn't realize, that the other methods executed in a different context. These methods get executed when called from the editor, which is then already fully loaded. Calling setContent in a different context, e.g. from the constructor or after setUrl in setBasePath will simply not work and show nothing in the editor. As setUrl is asynchron, and also QWebViews loadFinished is not helping here, as thats only for the HTML, not the Javascript now running inside of QWebView. So, currently, I have in the form containing this class a button "load content" which calls setContent when clicked. This is of course just a proof of concept, I probably will replace this with a timer. Also setContent takes QString per value, as replace is not const.

The method execJS is only calling the method to execute the js in the web view, and returns a QVariant, which holds the result, if the js function returns one:

QVariant HTMLTextEditor::execJS(const QString &js)const
{
    return mainframe->evaluateJavaScript(js);
}

And this is the whole code needed to integrate TinyMCE3 into my Qt application. Well, the embedding class has to do some work with connecting to boost::signal2 signals in order to receive the updates for links and images. But this is a different story...

 

Join the Meeting C++ patreon community!
This and other posts on Meeting C++ are enabled by my supporters on patreon!