Saturday, 13 March 2021

Enter non-ascii charaters into the app created by Qt for webassembly

    With the help of wasm, we are able to port the apps written by Qt to the browser, this save us a lot of times in many cases, but same as android/ios, wasm of Qt do have their limitations and the Qt company do not have much interest to fix those issues, I guess this is because major market of Qt are automobile, IOT etc, but not mobile phone or browser. 

    In this post I would like to write down how do I get around the issue 78826--Cannot enter non ascii characters to the TextField and QLineEdit from browser(wasm).


Limitations


1.Cannot access system font


    Download the font you want to display/enter first, unlike desktop/mobile, Qt for wasm cannot access system font.  Today I would use the font download from oppo as an example. After you download the ttf, compress it by qCompress in order to reduce the size.



QFile file("OPPOSans-H.ttf");
if(file.open(QIODevice::ReadOnly)){
    auto const contents = qCompress(file.readAll(), 9);
    QFile fwrite("OPPOSans-H.zip");
    if(fwrite.open(QIODevice::WriteOnly)){
        fwrite.write(contents);
    }else{
        qDebug()<<__func__<<": cannot write file";
    }
}else{
    qDebug()<<__func__<<": cannot open file";
}


    After that, add the zip file into the Qt resource system, load it and set it as the font of the QPlainTextEdit.


//MainWindow.cpp, set font for QPlainTextEdit
QFont const monospace(font_manager().get_font_family());
ui->plainTextEdit->setFont(monospace);

//font_manager.cpp, a class use to register the font and access the font id
font_manager::font_manager()
{
    QFile ifile(":/assets/OPPOSans-H.zip");
    if(ifile.open(QIODevice::ReadOnly)){
        auto const font_id = QFontDatabase::addApplicationFontFromData(qUncompress(ifile.readAll()));
        qDebug()<<__func__<<"font id:"<<font_id;

        font_family_ = QFontDatabase::applicationFontFamilies(font_id).at(0);
    }
}

QFont font_manager::get_font() const
{
    return QFont(get_font_family());
}

QString font_manager::get_font_family() const noexcept
{
    return font_family_;
}


2. Cannot enter Chinese from QPlainTextEdit

    This issue is quite annoying, but we do have a "stupid" solution to overcome this problem. The way I found is use the prompt dialog of js library to input the text. The library I pick called bootbox. You can get around this limit by following steps

a. Use js to enter the text


var _global_text_process_result = ''
//You need to allocate memory before return the array to the c++ side 
function getStringFromJS(targetStr, msg)
{
    console.log(msg + ":" + targetStr);
    var jsString = targetStr;
    var lengthBytes = lengthBytesUTF8(jsString)+1;
    var stringOnWasmHeap = _malloc(lengthBytes);
    stringToUTF8(jsString, stringOnWasmHeap, lengthBytes);
    console.log(msg + " heap:" + stringOnWasmHeap);

    return stringOnWasmHeap;
}
 
//call the prompt dialog of bootbox
function multiLinesPrompt(inputString, inputMode, inputTitle)
{
    bootbox.prompt({        
	    title: inputTitle,
            inputType: "textarea",
            value: inputString,
                
            callback: function (result) { console.log(result); _global_text_process_result = result; }
    });
} 
 
//This function use to avoid the text update repeatly
function globalTextProcessResultIsValid()
{
    return _global_text_process_result !== ""
}

function getGlobalTextProcessResult()
{    
    results = getStringFromJS(_global_text_process_result, "js getGlobalTextProcessResult")
    _global_text_process_result = ""
    
    return results	
}


b. Call the js function from c++


#include "custom_qplain_text_edit.hpp"

#include <QDebug>
#include <QTimer>

#ifdef Q_OS_WASM

#include <emscripten.h>

namespace{

EM_JS(char*, global_text_process_result_is_valid, (), {
          return globalTextProcessResultIsValid();
      })

EM_JS(char*, get_global_text_process_result, (), {
          return getGlobalTextProcessResult();
      })

EM_JS(void, multi_lines_prompt, (char const *input_strings, char const *input_mode, char const *title), {
          multiLinesPrompt(UTF8ToString(input_strings), UTF8ToString(input_mode), UTF8ToString(title));
      })

}

#endif

custom_qplain_text_edit::custom_qplain_text_edit(QWidget *parent) :
    QPlainTextEdit(parent)
{
#ifdef Q_OS_WASM  
    timer_ = new QTimer(this);
    timer_->setInterval(100);

    connect(timer_, &QTimer::timeout, [this]()
    {
        if(global_text_process_result_is_valid()){
            char *msg = get_global_text_process_result();
            setPlainText(QString(msg));
            free(msg);
            timer_->stop();
        }
    });
#endif
}

void custom_qplain_text_edit::mousePressEvent(QMouseEvent *e)
{
#ifdef Q_OS_WASM
    if(e->button() == Qt::RightButton){
        QPlainTextEdit::mousePressEvent(e);
    }else{
        multi_lines_prompt(toPlainText().toUtf8().data(), "textarea", "Input plain text");
        timer_->start();
    }
#else
    QPlainTextEdit::mousePressEvent(e);
#endif
}

c. Include js script and css in generated html file




    To be honest, this is really troublesome, and I hope that one day this shortcoming can be resolved.

Source codes

Github

Full package, include the fonts and js libraries