11 Commits

Author SHA1 Message Date
hans a1a56cd08c Ja. 2026-08-04 13:00:56 +02:00
hans d9af82d230 Omzetting naar stdio. 2026-08-03 08:22:42 +02:00
hans fb1a293e88 QtWebEngine select popup rendering issue observed with older Qt 6.x.
Symptom: native HTML <select> popup keeps growing/repainting while open;
QtWebEngine process busy, Racket process idle.
Resolved by upgrading to Qt 6.11.1.
2026-07-07 19:23:17 +02:00
hans 803d6edd41 Merge branch 'main' of https://git.dijkewijk.nl/hans/racket-webview-qt 2026-07-07 17:43:46 +02:00
hans 35b7bfe2e9 Migration to Qt 6.11.1 2026-07-07 17:43:27 +02:00
hans c6db000d26 linefeet added 2026-05-06 09:22:44 +02:00
hans 85b5a29192 handle tray menu myself instead by tray icon 2026-04-30 13:42:57 +02:00
hans b53597396c trying to clear the context menu, but that does not work 2026-04-30 11:53:08 +02:00
hans be980dbffd tray 2026-04-29 23:35:40 +02:00
hans 8165ee20cc Adding a tray icon. 2026-04-29 14:33:43 +02:00
hans cacffbcc2a synchronization on finalization finally solved. 2026-04-20 19:07:03 +02:00
78 changed files with 1553 additions and 539 deletions
+10 -21
View File
@@ -12,20 +12,6 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(QT NAMES Qt6 REQUIRED COMPONENTS Widgets WebEngineWidgets)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets WebEngineWidgets)
add_library(rktwebview SHARED
rktwebview_global.h
rktwebview.h
rktwebview.cpp
shm.h shm.cpp
shmqueue.h shmqueue.cpp
rkt_protocol.h
rktwebview_types.h
json.cpp json.h
utils.h
utils.cpp
memqueue.h memqueue.cpp
)
add_executable(rktwebview_prg
main.cpp
@@ -37,22 +23,25 @@ add_executable(rktwebview_prg
rktutils.h rktutils.cpp
command.h command.cpp
shm.h shm.cpp
shmqueue.h shmqueue.cpp
rkt_protocol.h
rktwebview_types.h
utils.cpp utils.h
)
add_executable(rktwebview_test
rktwebview_test.cpp
build_menu_from_json.cpp
build_menu_from_json.h
)
target_link_libraries(rktwebview_prg PRIVATE Qt${QT_VERSION_MAJOR}::Widgets)
target_link_libraries(rktwebview_prg PRIVATE Qt${QT_VERSION_MAJOR}::WebEngineWidgets)
target_compile_definitions(rktwebview PRIVATE RKTWEBVIEW_LIBRARY)
target_compile_definitions(rktwebview_prg PRIVATE RKTWEBVIEW_PRG_EXE)
target_link_Libraries(rktwebview_test PRIVATE rktwebview)
add_executable(rktwebview_test
rktwebview_test.cpp
rkt_protocol.h
)
target_link_libraries(rktwebview_test PRIVATE Qt${QT_VERSION_MAJOR}::Core)
add_dependencies(rktwebview_test rktwebview_prg)
+63 -1
View File
@@ -1,3 +1,65 @@
# racket-webview-qt
The Qt backend of racket-webview
The Qt WebEngine backend of `racket-webview`.
## Version 0.2.3
This version replaces the shared-library/shared-memory transport with a
standalone process interface.
## Transport
`rktwebview_prg` receives line-delimited JSON commands on stdin, writes
line-delimited JSON results and events to stdout, and writes all diagnostics to
stderr. The existing command numbers and command payloads in `rkt_protocol.h`
are unchanged.
See `STDIO-PROTOCOL.md` for the transport envelope and shutdown rules.
The former shared library, shared-memory queue, and callback sources remain in
the repository for comparison, but they are no longer part of the CMake target.
## Build
The CMake target requires Qt Widgets and Qt WebEngineWidgets and builds only:
```text
rktwebview_prg
```
## Previous Qt update
QtWebEngine select popup rendering issues were observed with older Qt 6.x. The
native HTML `<select>` popup could keep growing or repainting while open while
the QtWebEngine process remained busy. Upgrading to Qt 6.11.1 resolved this.
## Integration test
`rktwebview_test` starts the built `rktwebview_prg` with `QProcess` and tests
the stdio handshake, request/result matching, basic webview commands, and clean
shutdown. Run it from the build directory:
```sh
./rktwebview_test
```
For a protocol-only test that does not create a window:
```sh
./rktwebview_test --protocol-only
```
Set `RKT_WEBVIEW_PRG` to test a backend executable outside the test program's
directory.
### Backend logging
Diagnostics written through `utils.h` use one-line structured stderr records:
```text
json:{"topic":"webview-backend","level":"info","elapsed":0.125,"message":"..."}
```
Stdout remains reserved for protocol messages. Unstructured stderr output from
Qt or third-party libraries may still occur and should be treated as diagnostic
text.
+86
View File
@@ -0,0 +1,86 @@
# racket-webview stdio protocol
The Qt backend is a child process of the Racket process. Communication uses
UTF-8 JSON records, one compact JSON object per line.
- stdin of `rktwebview_prg`: commands from Racket
- stdout of `rktwebview_prg`: handshake, results, and events
- stderr of `rktwebview_prg`: diagnostics only
No diagnostic text may be written to stdout.
## Compatibility
The existing numeric command identifiers in `rkt_protocol.h` and their JSON
payload objects are retained. The stdio transport adds only an envelope and a
request identifier. The request identifier allows replies to be matched even
when several Racket threads issue commands concurrently.
## Handshake
The backend writes this record after Qt has been initialized and the command
reader has started:
```json
{"type":"ready","protocol":1}
```
Racket must not send application commands until this record has been received.
## Command
```json
{"type":"command","id":17,"command":6,"data":{"wv":1,"url":"https://example.test"}}
```
Fields:
- `id`: positive request identifier chosen by Racket
- `command`: existing numeric command from `rkt_protocol.h`
- `data`: existing command payload object
## Result
```json
{"type":"result","id":17,"result":0,"data":null}
```
`result` has the same meaning as before. Depending on the command it can be a
`result_t`, a handle, a context identifier, a window-state value, or a metric.
For `CMD_CALL_JS`, `data` contains the JavaScript result string.
Every command now receives a result record, including commands that previously
returned `void`, such as close and setting the OU token. Their public Racket
return values remain unchanged.
## Event
```json
{"type":"event","wv":1,"data":"{\"event\":\"page-loaded\",...}"}
```
The `data` field is the original event JSON string. It is deliberately not
restructured by the transport, so existing event parsing remains unchanged.
Events are independent of command results.
## Shutdown
A normal shutdown sends `CMD_QUIT`, waits for its result, closes the child's
stdin, and waits for the child process. If the Racket process terminates
unexpectedly, the operating system closes the pipe; EOF on stdin makes the Qt
backend close all windows and quit.
## Structured stderr logging
The backend writes diagnostic logging to stderr, never to stdout. Log entries
created through `utils.h` are emitted as one line beginning with `json:` followed
by a compact JSON object:
```text
json:{"topic":"webview-backend","level":"debug","elapsed":1.234,"message":"..."}
```
Newlines and other control characters in the message are JSON escaped, so one
C++ log call always produces one physical stderr line. Consumers should also
accept unstructured stderr lines because Qt, Qt WebEngine, or another library
may write diagnostics directly.
+99
View File
@@ -0,0 +1,99 @@
#include <QJsonArray>
#include <QJsonParseError>
#include <QJsonObject>
#include <QMenu>
#include "rktwebview_qt.h"
#include "utils.h"
static QAction *addMenuItemFromJson(Rktwebview_qt *owner,
QMenu *menu,
const QJsonObject &item,
int source_handle);
static QMenu *buildMenuObjectFromJson(Rktwebview_qt *owner,
const QJsonArray &items,
int source_handle,
QWidget *parent = nullptr)
{
QMenu *menu = new QMenu(parent);
for (const QJsonValue &v : items) {
if (!v.isObject()) {
continue;
}
QJsonObject item = v.toObject();
if (item.value("separator").toBool(false)) {
menu->addSeparator();
continue;
}
addMenuItemFromJson(owner, menu, item, source_handle);
}
return menu;
}
static QAction *addMenuItemFromJson(Rktwebview_qt *owner,
QMenu *menu,
const QJsonObject &item,
int source_handle)
{
QString id = item.value("id").toString();
QString name = item.value("name").toString();
QString icon_file = item.value("icon").toString();
QAction *action = nullptr;
QJsonValue submenu_value = item.value("submenu");
if (submenu_value.isObject()) {
QJsonObject submenu_obj = submenu_value.toObject();
QJsonArray submenu_items = submenu_obj.value("menu").toArray();
QMenu *submenu = buildMenuObjectFromJson(owner,
submenu_items,
source_handle,
menu);
if (!icon_file.isEmpty()) {
submenu->setIcon(QIcon(icon_file));
}
submenu->setTitle(name);
action = menu->addMenu(submenu);
} else {
if (icon_file.isEmpty()) {
action = menu->addAction(name);
} else {
action = menu->addAction(QIcon(icon_file), name);
}
QObject::connect(action, &QAction::triggered,
owner, [owner, source_handle, id]() {
EventContainer e("tray-menu-item-chosen");
e["id"] = id;
e["menu_item"] = id;
owner->triggerEvent(source_handle, e);
});
}
action->setData(id);
return action;
}
QMenu *buildMenuFromJson(Rktwebview_qt *self, const QString &menu_json, int source_handle)
{
QJsonParseError err;
QJsonDocument doc = QJsonDocument::fromJson(menu_json.toUtf8(), &err);
if (err.error != QJsonParseError::NoError || !doc.isObject()) {
ERROR2("Json parse error for menu: %d, '%s'\n", err.error, err.errorString().toUtf8().constData());
return nullptr;
}
QJsonObject root = doc.object();
QJsonArray items = root.value("menu").toArray();
return buildMenuObjectFromJson(self, items, source_handle);
}
+10
View File
@@ -0,0 +1,10 @@
#ifndef BUILD_MENU_FROM_JSON_H
#define BUILD_MENU_FROM_JSON_H
class QMenu;
class Rktwebview_qt;
class QString;
QMenu *buildMenuFromJson(Rktwebview_qt *self, const QString &menu_json, int source_handle);
#endif // BUILD_MENU_FROM_JSON_H
+5
View File
@@ -29,6 +29,11 @@
#define COMMAND_NEW_CONTEXT 23
#define COMMAND_MESSAGE 24
#define COMMAND_SET_ICON 25
#define COMMAND_CREATE_TRAY 26
#define COMMAND_TRAY_SET_ICON 27
#define COMMAND_TRAY_SET_TOOLTIP 28
#define COMMAND_TRAY_SHOW_MESSAGE 29
#define COMMAND_TRAY_SET_MENU 30
class Command
{
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,48 @@
[
{
"name": "qtbase",
"repositories": ["qtbase", "qtactiveqt", "qtimageformats"]
},
{
"name": "qtdeclarative",
"repositories": ["qtdeclarative"]
},
{
"name": "qtmultimedia",
"repositories": ["qtmultimedia"]
},
{
"name": "qtconnectivity",
"repositories": ["qtconnectivity"]
},
{
"name": "qtlocation",
"repositories": ["qtlocation"]
},
{
"name": "qtwebsockets",
"repositories": ["qtwebsockets"]
},
{
"name": "qtserialport",
"repositories": ["qtserialport"]
},
{
"name": "qtwebengine",
"repositories": ["qtwebengine"]
},
{
"name": "designer",
"modules": ["Designer"]
},
{
"name": "linguist"
},
{
"name": "assistant"
},
{
"name": "qt_help",
"modules": ["Help"]
}
]
+360 -332
View File
@@ -1,433 +1,461 @@
#include <QThread>
#include <QFile>
#include <QApplication>
#include "rkt_protocol.h"
#include "shm.h"
#include "shmqueue.h"
#include "command.h"
#include "utils.h"
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
#include <QThread>
#include <iostream>
#include <mutex>
#include <string>
#include "rkt_protocol.h"
#include "rktwebview_qt.h"
#include "utils.h"
static void free_data(rkt_data_t *d)
namespace {
constexpr int STDIO_PROTOCOL_VERSION = 1;
std::mutex output_mutex;
void writeMessage(const QJsonObject &message)
{
do_free_data(d);
const QByteArray json = QJsonDocument(message).toJson(QJsonDocument::Compact);
std::lock_guard<std::mutex> lock(output_mutex);
std::cout.write(json.constData(), json.size());
std::cout.put('\n');
std::cout.flush();
}
void sendReady()
{
QJsonObject message;
message["type"] = "ready";
message["protocol"] = STDIO_PROTOCOL_VERSION;
writeMessage(message);
}
void sendResult(int id,
int result,
const QJsonValue &data = QJsonValue(QJsonValue::Null))
{
QJsonObject message;
message["type"] = "result";
message["id"] = id;
message["result"] = result;
message["data"] = data;
writeMessage(message);
}
void sendProtocolError(const QString &messageText, int id = -1)
{
QJsonObject message;
message["type"] = "protocol-error";
if (id >= 0) {
message["id"] = id;
}
message["message"] = messageText;
writeMessage(message);
}
void freeData(rkt_data_t *data)
{
do_free_data(data);
}
class Handler : public QThread
{
public:
Shm *shm;
ShmQueue *command_queue;
ShmQueue *result_queue;
ShmQueue *event_queue;
Rktwebview_qt *webview_handler;
bool quit;
// QThread interface
protected:
void run();
};
class Alive : public QThread
{
public:
Handler *handler;
ShmQueue *alive_queue;
ShmQueue *alive_ack_queue;
bool alive_timeout;
Rktwebview_qt *webviewHandler = nullptr;
protected:
void run();
void run() override;
};
static Handler *_handler;
static Alive *_alive;
static void event_cb(rkt_data_t *data)
void eventCallback(rkt_data_t *data)
{
if (data->kind != rkt_data_kind_t::event) {
if (data == nullptr || data->kind != rkt_data_kind_t::event) {
freeData(data);
return;
}
int wv = data->data.event.wv;
char *evt = data->data.event.event;
QJsonObject message;
message["type"] = "event";
message["wv"] = data->data.event.wv;
message["data"] = QString::fromUtf8(data->data.event.event);
writeMessage(message);
std::string evt_d(evt);
_handler->event_queue->enqueue(wv, evt_d);
free_data(data);
freeData(data);
}
} // namespace
int main(int argc, char *argv[])
{
const char *me = argv[0];
INFO0("Starting stdio backend\n");
{
time_t my_time = time(NULL);
INFO1("Starting at %s", ctime(&my_time));
}
Handler commandHandler;
commandHandler.webviewHandler = new Rktwebview_qt(argc, argv);
commandHandler.webviewHandler->initApp();
commandHandler.start();
if (argc < 8) {
ERROR1("%s: wrong number of arguments\n", me);
exit(1);
}
sendReady();
commandHandler.webviewHandler->execApp();
const char *shm_name = argv[1];
const char *shm_size_str = argv[2];
const char *cmd_slot_str = argv[3];
const char *res_slot_str = argv[4];
const char *evt_slot_str = argv[5];
const char *alive_slot_str = argv[6];
const char *alive_ack_slot_str = argv[7];
INFO0("Qt event loop stopped; waiting for stdin handler\n");
commandHandler.wait();
size_t shm_size = atoi(shm_size_str);
int cmd_slot = atoi(cmd_slot_str);
int res_slot = atoi(res_slot_str);
int evt_slot = atoi(evt_slot_str);
int alive_slot = atoi(alive_slot_str);
int alive_ack_slot = atoi(alive_ack_slot_str);
MKLOGSTMT(LOG_INFO, fprintf(stderr, "%s %s %s %s %s %s %s %s\n", me, shm_name, shm_size_str,
cmd_slot_str, res_slot_str, evt_slot_str, alive_slot_str, alive_ack_slot_str));
MKLOGSTMT(LOG_INFO, fprintf(stderr, "%s %s %d %d %d %d %d %d\n", me, shm_name, static_cast<int>(shm_size),
cmd_slot, res_slot, evt_slot, alive_slot, alive_ack_slot));
if (!(shm_size > 0 && cmd_slot > 0 && res_slot > 0 && evt_slot > 0 && alive_slot > 0 && alive_ack_slot > 0)) {
ERROR1("%s: Invalid shm size or slots\n", me);
exit(2);
}
Handler *handler = new Handler();
_handler = handler;
handler->shm = new Shm(shm_name, shm_size, false);
handler->command_queue = new ShmQueue(handler->shm, cmd_slot, false);
handler->result_queue = new ShmQueue(handler->shm, res_slot, false);
handler->event_queue = new ShmQueue(handler->shm, evt_slot, false);
handler->webview_handler = new Rktwebview_qt(argc, argv);
handler->start();
Alive *alive = new Alive();
_alive = alive;
alive->handler = handler;
alive->alive_timeout = false;
alive->alive_queue = new ShmQueue(handler->shm, alive_slot, false);
alive->alive_ack_queue = new ShmQueue(handler->shm, alive_ack_slot, false);
alive->start();
handler->webview_handler->initApp();
handler->webview_handler->execApp();
INFO0("waiting for thread to end\n");
handler->wait();
INFO0("Handler thread stopped\n");
alive->wait();
INFO0("Alive thread stopped\n");
if (alive->alive_timeout) {
// take ownership over all queues and SHM, because we didn't get an alive message
// and probably the racket side has crashed because of that.
ERROR0("Taking ownership of shared memory, shared queues and shared semaphores\n");
handler->command_queue->takeOwnership();
handler->result_queue->takeOwnership();
handler->event_queue->takeOwnership();
handler->shm->takeOwnership();
alive->alive_queue->takeOwnership();
alive->alive_ack_queue->takeOwnership();
}
INFO0("cleaning up shm\n");
delete alive->alive_queue;
delete alive;
delete handler->webview_handler;
delete handler->command_queue;
delete handler->result_queue;
delete handler->event_queue;
delete handler->shm;
delete handler;
{
time_t my_time = time(NULL);
INFO1("Exiting at %s", ctime(&my_time));
}
delete commandHandler.webviewHandler;
commandHandler.webviewHandler = nullptr;
INFO0("Exiting stdio backend\n");
return 0;
}
void Handler::run()
{
int wait_ms = 10 * 1000; // 10 seconds.
while (!quit) {
int cmd;
std::string data;
bool something_came = command_queue->dequeue(cmd, data, wait_ms);
if (!something_came) {
DEBUG1("No command received last %d seconds\n", wait_ms / 1000);
cmd = CMD_NOOP;
}
QJsonObject data_obj = QJsonDocument::fromJson(data.c_str()).object();
std::string line;
bool quit = false;
switch(cmd) {
case CMD_NOOP: {
if (quit) {
DEBUG1("Alive timeout, quit = %d\n", quit);
webview_handler->closeAllWindows();
DEBUG0("Closed all windows\n");
webview_handler->rktQuit();
DEBUG0("Quit application\n");
while (!quit && std::getline(std::cin, line)) {
if (line.empty()) {
continue;
}
QJsonParseError parseError;
const QJsonDocument document =
QJsonDocument::fromJson(QByteArray::fromStdString(line), &parseError);
if (parseError.error != QJsonParseError::NoError || !document.isObject()) {
const QString message = QString("Invalid protocol JSON: %1")
.arg(parseError.errorString());
ERROR1("%s\n", message.toUtf8().constData());
sendProtocolError(message);
continue;
}
const QJsonObject request = document.object();
const int id = request["id"].toInt(-1);
if (request["type"].toString() != "command") {
sendProtocolError("Expected a command message", id);
continue;
}
const int command = request["command"].toInt(CMD_NOOP);
const QJsonObject data = request["data"].toObject();
if (id < 0) {
sendProtocolError("Command has no valid request id");
continue;
}
switch (command) {
case CMD_NOOP:
sendResult(id, result_t::oke);
break;
case CMD_QUIT: {
INFO0("Got quit message\n");
webview_handler->rktQuit();
INFO0("Enqueing RESULT_QUIT to result queue\n");
result_queue->enqueue(RESULT_QUIT);
case CMD_QUIT:
INFO0("Got quit command\n");
webviewHandler->rktQuit();
sendResult(id, RESULT_QUIT);
quit = true;
}
break;
case CMD_INFO: {
int open_windows = webview_handler->openWindows();
result_queue->enqueue(open_windows);
}
break;
case CMD_HANDLE_IS_VALID: {
int wv = data_obj["wv"].toInt();
bool oke = webview_handler->rktValid(wv);
result_queue->enqueue(oke);
}
break;
case CMD_SET_LOGLEVEL: {
int ll = data_obj["wv"].toInt();
setLogLevel(ll);
bool oke = true;
result_queue->enqueue(oke);
}
break;
case CMD_CONTEXT_NEW: {
QString boilerplate_js = data_obj["boilerplate_js"].toString();
bool has_pem_cert = data_obj["has_pem_cert"].toBool();
QString pem_cert = data_obj["pem_cert"].toString();
int context = webview_handler->newContext(boilerplate_js, has_pem_cert, pem_cert);
result_queue->enqueue(context);
}
break;
case CMD_CREATE_WV: {
int context = data_obj["context"].toInt();
int parent = data_obj["parent"].toInt();
int wv = webview_handler->rktWebViewCreate(context, parent, event_cb);
result_queue->enqueue(wv);
}
case CMD_INFO:
sendResult(id, webviewHandler->openWindows());
break;
case CMD_HANDLE_IS_VALID: {
const int wv = data["wv"].toInt();
sendResult(id, webviewHandler->rktValid(wv) ? 1 : 0);
break;
}
case CMD_SET_LOGLEVEL: {
const int level = data["wv"].toInt();
setLogLevel(level);
sendResult(id, result_t::oke);
break;
}
case CMD_CONTEXT_NEW: {
const QString boilerplateJs = data["boilerplate_js"].toString();
const bool hasPemCertificate = data["has_pem_cert"].toBool();
const QString pemCertificate = data["pem_cert"].toString();
const int context = webviewHandler->newContext(boilerplateJs,
hasPemCertificate,
pemCertificate);
sendResult(id, context);
break;
}
case CMD_CREATE_WV: {
const int context = data["context"].toInt();
const int parent = data["parent"].toInt();
const int wv = webviewHandler->rktWebViewCreate(context,
parent,
eventCallback);
sendResult(id, wv);
break;
}
case CMD_CLOSE_WV: {
int wv = data_obj["wv"].toInt();
webview_handler->rktWebViewClose(wv);
}
const int wv = data["wv"].toInt();
webviewHandler->rktWebViewClose(wv);
sendResult(id, result_t::oke);
break;
}
case CMD_SET_URL: {
int wv = data_obj["wv"].toInt();
QString url = data_obj["url"].toString();
result_t r = webview_handler->rktSetUrl(wv, url.toUtf8().constData());
result_queue->enqueue(r);
}
const int wv = data["wv"].toInt();
const QString url = data["url"].toString();
sendResult(id, webviewHandler->rktSetUrl(wv, url.toUtf8().constData()));
break;
}
case CMD_SET_HTML: {
int wv = data_obj["wv"].toInt();
QString html = data_obj["html"].toString();
result_t r = webview_handler->rktSetHtml(wv, html.toUtf8().constData());
result_queue->enqueue(r);
}
const int wv = data["wv"].toInt();
const QString html = data["html"].toString();
sendResult(id, webviewHandler->rktSetHtml(wv, html.toUtf8().constData()));
break;
}
case CMD_RUN_JS: {
int wv = data_obj["wv"].toInt();
QString js = data_obj["js"].toString();
result_t r = webview_handler->rktRunJs(wv, js);
result_queue->enqueue(r);
}
const int wv = data["wv"].toInt();
const QString js = data["js"].toString();
sendResult(id, webviewHandler->rktRunJs(wv, js));
break;
}
case CMD_CALL_JS: {
int wv = data_obj["wv"].toInt();
QString js = data_obj["js"].toString();
rkt_data_t *res = webview_handler->rktCallJs(wv, js.toUtf8().constData());
result_queue->enqueue(res->data.js_result.result, res->data.js_result.value);
free_data(res);
const int wv = data["wv"].toInt();
const QString js = data["js"].toString();
rkt_data_t *response = webviewHandler->rktCallJs(wv, js.toUtf8().constData());
if (response == nullptr || response->kind != rkt_data_kind_t::js_result) {
sendResult(id, result_t::failed, "Missing JavaScript result");
} else {
sendResult(id,
response->data.js_result.result,
QString::fromUtf8(response->data.js_result.value));
}
freeData(response);
break;
}
case CMD_OPEN_DEVTOOLS: {
int wv = data_obj["wv"].toInt();
result_t r = webview_handler->rktOpenDevtools(wv);
result_queue->enqueue(r);
}
const int wv = data["wv"].toInt();
sendResult(id, webviewHandler->rktOpenDevtools(wv));
break;
}
case CMD_MOVE: {
int wv = data_obj["wv"].toInt();
int x = data_obj["x"].toInt();
int y = data_obj["y"].toInt();
result_t r = webview_handler->rktMove(wv, x, y);
result_queue->enqueue(r);
}
const int wv = data["wv"].toInt();
sendResult(id,
webviewHandler->rktMove(wv,
data["x"].toInt(),
data["y"].toInt()));
break;
}
case CMD_RESIZE: {
int wv = data_obj["wv"].toInt();
int w = data_obj["w"].toInt();
int h = data_obj["h"].toInt();
result_t r = webview_handler->rktResize(wv, w, h);
result_queue->enqueue(r);
}
const int wv = data["wv"].toInt();
sendResult(id,
webviewHandler->rktResize(wv,
data["w"].toInt(),
data["h"].toInt()));
break;
}
case CMD_HIDE: {
int wv = data_obj["wv"].toInt();
result_t r = webview_handler->rktHideWindow(wv);
result_queue->enqueue(r);
}
const int wv = data["wv"].toInt();
sendResult(id, webviewHandler->rktHideWindow(wv));
break;
}
case CMD_SHOW: {
int wv = data_obj["wv"].toInt();
result_t r = webview_handler->rktShowWindow(wv);
result_queue->enqueue(r);
}
const int wv = data["wv"].toInt();
sendResult(id, webviewHandler->rktShowWindow(wv));
break;
}
case CMD_PRESENT: {
int wv = data_obj["wv"].toInt();
result_t r = webview_handler->rktPresentWindow(wv);
result_queue->enqueue(r);
}
const int wv = data["wv"].toInt();
sendResult(id, webviewHandler->rktPresentWindow(wv));
break;
}
case CMD_MAXIMIZE: {
int wv = data_obj["wv"].toInt();
result_t r = webview_handler->rktMaximizeWindow(wv);
result_queue->enqueue(r);
}
const int wv = data["wv"].toInt();
sendResult(id, webviewHandler->rktMaximizeWindow(wv));
break;
}
case CMD_MINIMIZE: {
int wv = data_obj["wv"].toInt();
result_t r = webview_handler->rktMinimizeWindow(wv);
result_queue->enqueue(r);
}
const int wv = data["wv"].toInt();
sendResult(id, webviewHandler->rktMinimizeWindow(wv));
break;
}
case CMD_SHOW_NORMAL: {
int wv = data_obj["wv"].toInt();
result_t r = webview_handler->rktShowNormalWindow(wv);
result_queue->enqueue(r);
}
const int wv = data["wv"].toInt();
sendResult(id, webviewHandler->rktShowNormalWindow(wv));
break;
}
case CMD_WINDOW_STATE: {
int wv = data_obj["wv"].toInt();
window_state_t r = webview_handler->rktWindowState(wv);
result_queue->enqueue(r);
}
const int wv = data["wv"].toInt();
sendResult(id, webviewHandler->rktWindowState(wv));
break;
}
case CMD_SET_TITLE: {
int wv = data_obj["wv"].toInt();
QString title = data_obj["title"].toString();
result_t r = webview_handler->rktWindowSetTitle(wv, title.toUtf8().constData());
result_queue->enqueue(r);
}
const int wv = data["wv"].toInt();
const QString title = data["title"].toString();
sendResult(id,
webviewHandler->rktWindowSetTitle(wv,
title.toUtf8().constData()));
break;
}
case CMD_SET_ICON: {
int wv = data_obj["wv"].toInt();
QString icon_file = data_obj["icon"].toString();
result_t r = webview_handler->rktWindowSetIcon(wv, icon_file.toUtf8().constData());
result_queue->enqueue(r);
}
const int wv = data["wv"].toInt();
const QString icon = data["icon"].toString();
sendResult(id,
webviewHandler->rktWindowSetIcon(wv,
icon.toUtf8().constData()));
break;
}
case CMD_CREATE_TRAY: {
const QString icon = data["icon"].toString();
const QString tooltip = data["tooltip"].toString();
const int tray = webviewHandler->rktTrayCreate(icon.toUtf8().constData(),
tooltip.toUtf8().constData(),
eventCallback);
sendResult(id, tray);
break;
}
case CMD_TRAY_SET_ICON: {
const int tray = data["wv"].toInt();
const QString icon = data["icon"].toString();
sendResult(id,
webviewHandler->rktTraySetIcon(tray,
icon.toUtf8().constData()));
break;
}
case CMD_TRAY_SET_TOOLTIP: {
const int tray = data["wv"].toInt();
const QString tooltip = data["tooltip"].toString();
sendResult(id,
webviewHandler->rktTraySetTooltip(tray,
tooltip.toUtf8().constData()));
break;
}
case CMD_TRAY_SHOW_MESSAGE: {
const int tray = data["wv"].toInt();
const QString title = data["title"].toString();
const QString message = data["message"].toString();
sendResult(id,
webviewHandler->rktTrayShowMessage(tray,
title.toUtf8().constData(),
message.toUtf8().constData()));
break;
}
case CMD_TRAY_CLEAR_MENU: {
const int tray = data["wv"].toInt();
sendResult(id, webviewHandler->rktTraySetMenu(tray, nullptr));
break;
}
case CMD_TRAY_SET_MENU: {
const int tray = data["wv"].toInt();
const QString menuJson = data["menu_json"].toString();
sendResult(id,
webviewHandler->rktTraySetMenu(tray,
menuJson.toUtf8().constData()));
break;
}
case CMD_CHOOSE_DIR: {
int wv = data_obj["wv"].toInt();
QString title = data_obj["title"].toString();
QString base_dir = data_obj["base_dir"].toString();
result_t r = webview_handler->rktChooseDir(wv,
const int wv = data["wv"].toInt();
const QString title = data["title"].toString();
const QString baseDir = data["base_dir"].toString();
sendResult(id,
webviewHandler->rktChooseDir(wv,
title.toUtf8().constData(),
base_dir.toUtf8().constData()
);
result_queue->enqueue(r);
}
baseDir.toUtf8().constData()));
break;
}
case CMD_FILE_OPEN: {
int wv = data_obj["wv"].toInt();
QString title = data_obj["title"].toString();
QString base_dir = data_obj["base_dir"].toString();
QString permitted_exts = data_obj["permitted_exts"].toString();
result_t r = webview_handler->rktFileOpen(wv,
const int wv = data["wv"].toInt();
const QString title = data["title"].toString();
const QString baseDir = data["base_dir"].toString();
const QString permittedExtensions = data["permitted_exts"].toString();
sendResult(id,
webviewHandler->rktFileOpen(wv,
title.toUtf8().constData(),
base_dir.toUtf8().constData(),
permitted_exts.toUtf8().constData()
);
result_queue->enqueue(r);
}
baseDir.toUtf8().constData(),
permittedExtensions.toUtf8().constData()));
break;
}
case CMD_FILE_SAVE: {
int wv = data_obj["wv"].toInt();
QString title = data_obj["title"].toString();
QString base_dir = data_obj["base_dir"].toString();
QString permitted_exts = data_obj["permitted_exts"].toString();
result_t r = webview_handler->rktFileSave(wv,
const int wv = data["wv"].toInt();
const QString title = data["title"].toString();
const QString baseDir = data["base_dir"].toString();
const QString permittedExtensions = data["permitted_exts"].toString();
sendResult(id,
webviewHandler->rktFileSave(wv,
title.toUtf8().constData(),
base_dir.toUtf8().constData(),
permitted_exts.toUtf8().constData()
);
result_queue->enqueue(r);
}
baseDir.toUtf8().constData(),
permittedExtensions.toUtf8().constData()));
break;
}
case CMD_SET_OU_TOKEN: {
int wv = data_obj["wv"].toInt();
QString token = data_obj["token"].toString();
webview_handler->rktSetOUToken(wv, token.toUtf8().constData());
}
const int wv = data["wv"].toInt();
const QString token = data["token"].toString();
webviewHandler->rktSetOUToken(wv, token.toUtf8().constData());
sendResult(id, result_t::oke);
break;
}
case CMD_MSG_BOX: {
int wv = data_obj["wv"].toInt();
QString title = data_obj["title"].toString();
QString message = data_obj["message"].toString();
QString submsg = data_obj["submessage"].toString();
int type = data_obj["type"].toInt();
result_t r = webview_handler->rktMessageBox(wv,
const int wv = data["wv"].toInt();
const QString title = data["title"].toString();
const QString message = data["message"].toString();
const QString submessage = data["submessage"].toString();
const auto type = static_cast<rkt_messagetype_t>(data["type"].toInt());
sendResult(id,
webviewHandler->rktMessageBox(wv,
title.toUtf8().constData(),
message.toUtf8().constData(),
submsg.toUtf8().constData(),
static_cast<rkt_messagetype_t>(type)
);
result_queue->enqueue(r);
}
submessage.toUtf8().constData(),
type));
break;
default: {
ERROR1("Unknown command: %d\n", cmd);
}
}
}
DEBUG0("Exiting handler thread\n");
}
void Alive::run()
{
int wait_ms = 10 * 1000;
int ping_no;
std::string data;
bool go_on = true;
while (go_on) {
bool something_came = alive_queue->dequeue(ping_no, data, wait_ms);
if (!something_came) {
ERROR0("No alive message received, stopping alive loop and quitting rktwebview_prg\n");
handler->quit = true;
handler->command_queue->enqueue(CMD_NOOP);
alive_timeout = true;
go_on = false;
} else {
if (ping_no == CMD_ALIVE_QUIT) {
DEBUG0("Got Quit ping for alive thread\n");
go_on = false;
} else {
DEBUG1("Got alive ping: %d\n", ping_no);
alive_ack_queue->enqueue(ping_no);
default: {
const QString message = QString("Unknown command: %1").arg(command);
ERROR1("%s\n", message.toUtf8().constData());
sendResult(id, result_t::failed, message);
break;
}
}
}
DEBUG0("Exiting alive thread\n");
if (!quit) {
INFO0("stdin closed; stopping Qt backend\n");
webviewHandler->closeAllWindows();
webviewHandler->rktQuit();
}
DEBUG0("Exiting stdin handler\n");
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

+11
View File
@@ -1,6 +1,10 @@
#ifndef RKT_PROTOCOL_H
#define RKT_PROTOCOL_H
// The numeric command identifiers and payload objects are shared by both
// transports. The active transport wraps each command in one JSON line; see
// STDIO-PROTOCOL.md.
#define CMD_HANDLE_IS_VALID 1
#define CMD_QUIT 2
#define CMD_CONTEXT_NEW 3 // arguments: boilerplate_js: string, has_cert: bool, cert_pem: string -> context: int
@@ -29,6 +33,13 @@
#define CMD_SET_LOGLEVEL 26 // arguments: ll: int
#define CMD_INFO 27 // arguments: none
#define CMD_SET_ICON 28 // arguments: wv: int, icon:string (absolute filename to icon in png/jpg/svg)
#define CMD_CREATE_TRAY 29 // arguments: icon: string (path to icon file), tooltip: string -> tray: int
#define CMD_TRAY_SET_ICON 30 // arguments: tray: int, icon: string -> result_t: int
#define CMD_TRAY_SET_TOOLTIP 31 // arguments: tray: int, tooltip: string -> result_t: int
#define CMD_TRAY_SHOW_MESSAGE 32 // arguments: tray: int, title: string, message: string -> result_t: int
#define CMD_TRAY_SET_MENU 33 // arguments: tray: int, menu_json: string -> result_t: int
#define CMD_TRAY_CLEAR_MENU 34 // arguments: tray: int
#define CMD_NOOP 33621
#define RESULT_QUIT 36379
+124 -28
View File
@@ -38,8 +38,6 @@
#define sleep_ms(ms) Sleep(static_cast<DWORD>(ms))
#endif
//#define DEBUG
#ifdef __linux
static int sleep_ms(long msec)
{
@@ -232,7 +230,12 @@ bool runRktWebview(Handle_t *handler)
#define WAIT_ON_EVENT_MS (2 * 1000)
#define ALIVE_MESSAGE_INTERVAL_S 5 // Should be smaller than 10 seconds
#define MAX_WAIT_RESULT (2 * 1000) // Maximum wait in milliseconds for a result per time
#ifdef DEBUG_RKT_WEBVIEW
#define MAX_ALIVE_ACK_TIME (300 * 1000)
#else
#define MAX_ALIVE_ACK_TIME (10 * 1000)
#endif
void rkt_evt_guard(void)
{
@@ -248,10 +251,9 @@ void rkt_evt_guard(void)
handler->evt_queue->enqueue(wv, data);
handler->evt_cb_mutex.lock();
if (handler->evt_cb != nullptr) { // evt_cb could be cleared to null
INFO2("Calling evt_cb with %d, %s\n", wv, data.c_str());
//INFO2("Calling evt_cb with %d, %s\n", wv, data.c_str());
handler->evt_cb(1);
INFO0("done\n");
//INFO0("done\n");
} else {
INFO0("evt_cb = null\n");
}
@@ -397,7 +399,7 @@ void rkt_webview_init(const char *from)
INFO1("rkt_webview_init called from %s\n", from);
char buf[1024];
#ifdef DEBUG
#ifdef DEBUG_RKT_WEBVIEW
sprintf(buf, "rktwebview-dbg");
#else
#ifdef _WIN32
@@ -427,7 +429,7 @@ void rkt_webview_init(const char *from)
handler->alive_ack_queue = new ShmQueue(handler->shm, ALIVE_ACK_SLOT, true);
// Start rktwebview_prg application with the right information
#ifndef DEBUG
#ifndef DEBUG_RKT_WEBVIEW
handler->rkt_webview_prg_started = runRktWebview(handler);
if (!handler->rkt_webview_prg_started) { handler->valid = false; }
#endif
@@ -552,28 +554,70 @@ void rkt_webview_close(rktwebview_t wv)
handler->command_queue->enqueue(CMD_CLOSE_WV, j.dump());
}
#define CMDRES4(cmd, wv, key, val, key2, val2, key3, val3, key4, val4) \
RKT_WEBVIEW_INIT; \
FAIL_HANDLE \
JSON j; \
j["wv"] = wv; \
j[key] = val; \
j[key2] = val2; \
j[key3] = val3; \
j[key4] = val4; \
handler->command_queue->enqueue(cmd, j.dump()); \
int result; \
std::string json_result; \
while (!handler->command_result_queue->dequeue(result, json_result, MAX_WAIT_RESULT)) { \
if (handler->alive_error) { \
return result_t::failed; \
} \
} \
result_t r = static_cast<result_t>(result); \
static inline result_t DO_CMDRES(int cmd, rktwebview_t wv, const JSON &j)
{
RKT_WEBVIEW_INIT;
FAIL_HANDLE
std::string json_str = j.dump();
handler->command_queue->enqueue(cmd, json_str);
int result;
std::string json_result;
while (!handler->command_result_queue->dequeue(result, json_result, MAX_WAIT_RESULT)) {
if (handler->alive_error) {
return result_t::failed;
}
}
result_t r = static_cast<result_t>(result);
return r;
}
static inline result_t CMDRES4(int cmd, rktwebview_t wv,
const char *key, const char *val,
const char *key2, const char *val2,
const char *key3, const char *val3,
const char *key4, const char *val4)
{
JSON j;
j["wv"] = wv;
j[key] = val;
j[key2] = val2;
j[key3] = val3;
j[key4] = val4;
return DO_CMDRES(cmd, wv, j);
}
static inline result_t CMDRES4(int cmd, rktwebview_t wv,
const char *key, int val,
const char *key2, int val2,
const char *key3, const char *val3,
const char *key4, const char *val4)
{
JSON j;
j["wv"] = wv;
j[key] = val;
j[key2] = val2;
j[key3] = val3;
j[key4] = val4;
return DO_CMDRES(cmd, wv, j);
}
static inline result_t CMDRES4(int cmd, rktwebview_t wv,
const char *key, const char *val,
const char *key2, const char *val2,
const char *key3, const char *val3,
const char *key4, int val4)
{
JSON j;
j["wv"] = wv;
j[key] = val;
j[key2] = val2;
j[key3] = val3;
j[key4] = val4;
return DO_CMDRES(cmd, wv, j);
}
#define CMDRES3(cmd, wv, key, val, key2, val2, key3, val3) \
CMDRES4(cmd, wv, key, val, key2, val2, key3, val3, "nil3", "none")
return CMDRES4(cmd, wv, key, val, key2, val2, key3, val3, "nil3", "none");
#define CMDRES2(cmd, wv, key, val, key2, val2) \
CMDRES3(cmd, wv, key, val, key2, val2, "nil2", "none")
@@ -586,7 +630,7 @@ void rkt_webview_close(rktwebview_t wv)
result_t rkt_webview_set_url(rktwebview_t wv, const char *url)
{
CMDRES(CMD_SET_URL, wv, "url", url)
CMDRES(CMD_SET_URL, wv, "url", url);
}
result_t rkt_webview_set_html(rktwebview_t wv, const char *html)
@@ -765,7 +809,7 @@ rkt_data_t *rkt_webview_version()
result_t rkt_webview_message_box(rktwebview_t w, const char *title, const char *message, const char *submessage, rkt_messagetype_t type)
{
CMDRES4(CMD_MSG_BOX, w, "title", title, "message", message, "submessage", submessage, "type", static_cast<int>(type))
return CMDRES4(CMD_MSG_BOX, w, "title", title, "message", message, "submessage", submessage, "type", static_cast<int>(type));
}
int rkt_webview_events_waiting()
@@ -877,3 +921,55 @@ void rkt_webview_exit_done(int done)
ERROR0("rkt_webview_exit_done called with 'false', i.e. this library did not have a cleanup call\n");
}
}
/////////////////////////////////////////////////////////////////////
// Tray specific functions
/////////////////////////////////////////////////////////////////////
rktwebview_t rkt_webview_tray_create(const char *icon_file,
const char *tooltip)
{
RKT_WEBVIEW_INIT;
FAIL_WV
JSON j;
j["icon"] = icon_file;
j["tooltip"] = tooltip;
handler->command_queue->enqueue(CMD_CREATE_TRAY, j.dump());
int result;
std::string json_result;
while (!handler->command_result_queue->dequeue(result, json_result, MAX_WAIT_RESULT)) {
if (handler->alive_error) {
return result_t::failed;
}
}
return result;
}
result_t rkt_webview_tray_set_icon(rktwebview_t tray, const char *icon_file)
{
CMDRES(CMD_TRAY_SET_ICON, tray, "icon", icon_file)
}
result_t rkt_webview_tray_set_tooltip(rktwebview_t tray, const char *tooltip)
{
CMDRES(CMD_TRAY_SET_TOOLTIP, tray, "tooltip", tooltip)
}
result_t rkt_webview_tray_show_message(rktwebview_t tray, const char *title, const char *message)
{
CMDRES2(CMD_TRAY_SHOW_MESSAGE, tray, "title", title, "message", message)
}
result_t rkt_webview_tray_set_menu(rktwebview_t tray, const char *menu_json)
{
if (menu_json == nullptr) {
CMDRES0(CMD_TRAY_CLEAR_MENU, tray)
} else {
CMDRES(CMD_TRAY_SET_MENU, tray, "menu_json", menu_json)
}
}
+7
View File
@@ -58,6 +58,13 @@ RKTWEBVIEW_EXPORT result_t rkt_webview_file_save(rktwebview_t w, const char *tit
RKTWEBVIEW_EXPORT result_t rkt_webview_message_box(rktwebview_t w, const char *title, const char *message, const char *submessage, rkt_messagetype_t type);
// Tray Icon functions (note, also hide/show/close can be used).
RKTWEBVIEW_EXPORT rktwebview_t rkt_webview_tray_create(const char *icon_file, const char *tooltip);
RKTWEBVIEW_EXPORT result_t rkt_webview_tray_set_icon(rktwebview_t tray, const char *icon_file);
RKTWEBVIEW_EXPORT result_t rkt_webview_tray_set_tooltip(rktwebview_t tray, const char *tooltip);
RKTWEBVIEW_EXPORT result_t rkt_webview_tray_show_message(rktwebview_t tray, const char *title, const char *message);
RKTWEBVIEW_EXPORT result_t rkt_webview_tray_set_menu(rktwebview_t tray, const char *menu_json);
}
#endif // RKTWEBVIEW_H
+290 -5
View File
@@ -18,15 +18,36 @@
#include <QFileDialog>
#include <QMessageBox>
#include <QAbstractButton>
#include <QSystemTrayIcon>
#include <QMenu>
#include <QAction>
#include "build_menu_from_json.h"
static inline char *copyString(const char *s)
{
int l = strlen(s);
int l = static_cast<int>(strlen(s));
char *cpy = static_cast<char *>(malloc(l + 1));
memcpy(cpy, s, l + 1);
return cpy;
}
static QString activationReasonToString(QSystemTrayIcon::ActivationReason r)
{
switch (r) {
case QSystemTrayIcon::Context:
return "context-menu"; // rechtsklik / menu openen
case QSystemTrayIcon::DoubleClick:
return "double-click";
case QSystemTrayIcon::Trigger:
return "click"; // meestal linkerklik
case QSystemTrayIcon::MiddleClick:
return "middle-click";
case QSystemTrayIcon::Unknown:
default:
return "unknown";
}
}
void Rktwebview_qt::processCommand(Command *cmd)
{
switch(cmd->cmd) {
@@ -160,14 +181,27 @@ void Rktwebview_qt::processCommand(Command *cmd)
int wv = cmd->args[0].toInt();
if (_views.contains(wv)) {
WebviewWindow *w = _views[wv];
_views.remove(wv);
// WebviewWindow::closeEvent removes the view administration and
// schedules the window and web view with deleteLater(). Do not
// inspect or delete w after closeView(): processing the close
// event can already have scheduled or performed its destruction.
w->closeView();
cmd->result = true;
while(w->isVisible()) {
_app->processEvents();
} else if (_trays.contains(wv)) {
QSystemTrayIcon *tray = _trays[wv];
_trays.remove(wv);
if (_tray_menus.contains(wv)) {
QMenu *menu = _tray_menus[wv];
_tray_menus.remove(wv);
delete menu;
}
tray->hide();
_view_js_callbacks.remove(wv);
delete w;
delete tray;
cmd->result = true;
} else {
cmd->result = false;
}
@@ -306,6 +340,18 @@ void Rktwebview_qt::processCommand(Command *cmd)
w->activateWindow();
}
cmd->result = true;
} else if (_trays.contains(wv)) {
QSystemTrayIcon *tray = _trays[wv];
if (cmd->cmd == COMMAND_SHOW_WIN) {
tray->show();
cmd->result = true;
} else if (cmd->cmd == COMMAND_HIDE_WIN) {
tray->hide();
cmd->result = true;
} else {
cmd->result = false;
}
} else {
cmd->result = false;
}
@@ -449,6 +495,118 @@ void Rktwebview_qt::processCommand(Command *cmd)
}
cmd->done = true;
}
case COMMAND_CREATE_TRAY: {
QString icon_file = cmd->args[0].toString();
QString tooltip = cmd->args[1].toString();
void *f = cmd->args[2].value<void *>();
event_cb_t js_event_cb = reinterpret_cast <event_cb_t>(f);
int id = nextHandle();
QSystemTrayIcon *tray = new QSystemTrayIcon(this);
tray->setIcon(QIcon(icon_file));
tray->setToolTip(tooltip);
tray->setProperty("tray-id", id);
connect(tray, &QSystemTrayIcon::activated, this, &Rktwebview_qt::handleTrayActivation);
connect(tray, &QSystemTrayIcon::messageClicked, this, &Rktwebview_qt::handleTrayMessageClick);
_trays[id] = tray;
_view_js_callbacks[id] = js_event_cb;
tray->show();
cmd->result = id;
cmd->done = true;
}
break;
case COMMAND_TRAY_SET_ICON: {
int tray_id = cmd->args[0].toInt();
QString icon_file = cmd->args[1].toString();
if (_trays.contains(tray_id)) {
_trays[tray_id]->setIcon(QIcon(icon_file));
cmd->result = true;
} else {
cmd->result = false;
}
cmd->done = true;
}
break;
case COMMAND_TRAY_SET_TOOLTIP: {
int tray_id = cmd->args[0].toInt();
QString tooltip = cmd->args[1].toString();
if (_trays.contains(tray_id)) {
_trays[tray_id]->setToolTip(tooltip);
cmd->result = true;
} else {
cmd->result = false;
}
cmd->done = true;
}
break;
case COMMAND_TRAY_SHOW_MESSAGE: {
int tray_id = cmd->args[0].toInt();
QString title = cmd->args[1].toString();
QString message = cmd->args[2].toString();
if (_trays.contains(tray_id)) {
_trays[tray_id]->showMessage(title, message);
cmd->result = true;
} else {
cmd->result = false;
}
cmd->done = true;
}
break;
case COMMAND_TRAY_SET_MENU: {
int tray_id = cmd->args[0].toInt();
int has_menu = cmd->args[1].toInt();
QString menu_json = cmd->args[2].toString();
if (!_trays.contains(tray_id)) {
cmd->result = false;
cmd->done = true;
break;
}
if (_tray_menus.contains(tray_id)) {
QMenu *old = _tray_menus[tray_id];
_tray_menus.remove(tray_id);
delete old;
}
QMenu *menu = (has_menu) ? buildMenuFromJson(this, menu_json, tray_id) : nullptr;
_tray_menus[tray_id] = menu;
/*
if (menu == nullptr) {
if (!has_menu) {
INFO1("null menu, setting null menu on tray %d\n", tray_id);
_trays[tray_id]->setContextMenu(nullptr);
INFO0("Done setting tray menu to null menu\n");
WARN0("Note: on Linux, this will not clear the context menu\n");
cmd->result = true;
cmd->done = true;
} else {
ERROR2("menu json '%s' for tray %d is not oke\n", menu_json.toUtf8().constData(), tray_id);
cmd->result = false;
cmd->done = true;
}
} else {
_tray_menus[tray_id] = menu;
_trays[tray_id]->setContextMenu(menu);
}
*/
cmd->result = true;
cmd->done = true;
}
break;
default: {
cmd->result = false;
@@ -458,6 +616,50 @@ void Rktwebview_qt::processCommand(Command *cmd)
}
}
void Rktwebview_qt::handleTrayActivation(QSystemTrayIcon::ActivationReason reason)
{
QObject *obj = sender();
int tray_wv = obj->property("tray-id").toInt();
QSystemTrayIcon *icn = qobject_cast<QSystemTrayIcon *>(obj);
// If a context menu has been set, pop it up and leave.
if (reason == QSystemTrayIcon::ActivationReason::Context) {
QMenu *mnu = nullptr;
if (_tray_menus.contains(tray_wv)) {
mnu = _tray_menus[tray_wv];
}
if (mnu != nullptr) {
mnu->popup(QCursor::pos());
return;
}
}
QString evt = "tray-activated";
QString reason_str = activationReasonToString(reason);
//INFO3("triggering event: %s for tray %d with reason %s\n",
// evt.toUtf8().constData(), tray_wv, reason_str.toUtf8().constData());
EventContainer e(evt);
e["reason"] = reason_str;
this->triggerEvent(tray_wv, e);
}
void Rktwebview_qt::handleTrayMessageClick()
{
QObject *obj = sender();
//INFO0("triggering message click\n");
int tray_wv = obj->property("tray-id").toInt();
QString evt = "tray-message-clicked";
EventContainer e(evt);
this->triggerEvent(tray_wv, e);
}
void Rktwebview_qt::removeView(int id)
{
if (_views.contains(id)) {
@@ -888,6 +1090,76 @@ result_t Rktwebview_qt::rktMessageBox(rktwebview_t w, const char *title, const c
return r ? result_t::oke : result_t::failed;
}
rktwebview_t Rktwebview_qt::rktTrayCreate(const char *icon_file, const char *tooltip, event_cb_t evt_cb)
{
Command c(COMMAND_CREATE_TRAY);
c.args.push_back(icon_file);
c.args.push_back(tooltip);
void *function = reinterpret_cast<void *>(evt_cb);
QVariant f(QVariant::fromValue(function));
c.args.push_back(f);
postCommand(&c);
while(!c.done) { doEvents(); }
return c.result.toInt();
}
result_t Rktwebview_qt::rktTraySetIcon(rktwebview_t tray, const char *icon_file)
{
Command c(COMMAND_TRAY_SET_ICON);
c.args.push_back(tray);
c.args.push_back(icon_file);
postCommand(&c);
while(!c.done) { doEvents(); }
return c.result.toBool() ? result_t::oke : result_t::failed;
}
result_t Rktwebview_qt::rktTraySetTooltip(rktwebview_t tray, const char *tooltip)
{
//INFO2("rktTraySetTooltip: %d %s\n", tray, tooltip);
Command c(COMMAND_TRAY_SET_TOOLTIP);
c.args.push_back(tray);
c.args.push_back(tooltip);
postCommand(&c);
while(!c.done) { doEvents(); }
//INFO1("rktTraySetTooltip done: %s\n", tooltip);
return c.result.toBool() ? result_t::oke : result_t::failed;
}
result_t Rktwebview_qt::rktTrayShowMessage(rktwebview_t tray, const char *title, const char *message)
{
Command c(COMMAND_TRAY_SHOW_MESSAGE);
c.args.push_back(tray);
c.args.push_back(title);
c.args.push_back(message);
postCommand(&c);
while(!c.done) { doEvents(); }
return c.result.toBool() ? result_t::oke : result_t::failed;
}
result_t Rktwebview_qt::rktTraySetMenu(rktwebview_t tray, const char *menu_json)
{
Command c(COMMAND_TRAY_SET_MENU);
c.args.push_back(tray);
c.args.push_back(menu_json != nullptr);
c.args.push_back((menu_json == nullptr) ? "" : menu_json);
postCommand(&c);
while(!c.done) { doEvents(); }
return c.result.toBool() ? result_t::oke : result_t::failed;
}
result_t Rktwebview_qt::doWindow(rktwebview_t w, int cmd, result_t on_error)
{
Command c(cmd);
@@ -1085,5 +1357,18 @@ Rktwebview_qt::~Rktwebview_qt()
delete p;
}
QList<int> tray_keys = _trays.keys();
for(i = 0, N = tray_keys.size(); i < N; i++) {
QSystemTrayIcon *tray = _trays[tray_keys[i]];
tray->hide();
delete tray;
}
QList<int> menu_keys = _tray_menus.keys();
for(i = 0, N = menu_keys.size(); i < N; i++) {
QMenu *menu = _tray_menus[menu_keys[i]];
delete menu;
}
delete _app;
}
+14
View File
@@ -13,12 +13,15 @@
#include <QTimer>
#include <QEventLoop>
#include <QFileDialog>
#include <QSystemTrayIcon>
class WebViewQt;
class WebviewWindow;
class Command;
class CommandEvent;
class QSystemTrayIcon;
class QMenu;
class Rktwebview_qt : public QObject
{
@@ -30,6 +33,8 @@ private:
int _context_counter;
QHash<int, QWebEngineProfile *> _contexts;
QHash<int, WebviewWindow *> _views;
QHash<int, QSystemTrayIcon *> _trays;
QHash<int, QMenu *> _tray_menus;
QHash<int, event_cb_t> _view_js_callbacks;
QTimer _process_events;
@@ -46,6 +51,8 @@ private:
private slots:
void stopEventloop();
void handleTrayActivation(QSystemTrayIcon::ActivationReason reason);
void handleTrayMessageClick();
private:
void postCommand(Command *cmd);
@@ -114,6 +121,13 @@ public:
bool rktValid(rktwebview_t wv);
public: // Tray
rktwebview_t rktTrayCreate(const char *icon_file, const char *tooltip, event_cb_t evt_cb);
result_t rktTraySetIcon(rktwebview_t tray, const char *icon_file);
result_t rktTraySetTooltip(rktwebview_t tray, const char *tooltip);
result_t rktTrayShowMessage(rktwebview_t tray, const char *title, const char *message);
result_t rktTraySetMenu(rktwebview_t tray, const char *menu_json);
public:
// Events for the backend
void pageLoaded(rktwebview_t w, bool ok);
+286 -62
View File
@@ -1,77 +1,301 @@
#include "rktwebview.h"
#include <string>
#include <QCoreApplication>
#include <QElapsedTimer>
#include <QFileInfo>
#include <QJsonDocument>
#include <QJsonObject>
#include <QProcess>
#include <QStringList>
#ifdef _WIN32
#include <windows.h>
#else
#include <unistd.h>
#endif
#include "utils.h"
#include <cstdio>
#include <utility>
void evt_cb(int n)
#include "rkt_protocol.h"
namespace {
constexpr int protocolVersion = 1;
constexpr int timeoutMs = 10000;
class ProtocolClient
{
fprintf(stderr, "events waiting: %d - %d\n", n, rkt_webview_events_waiting());
public:
explicit ProtocolClient(QString program)
: program_(std::move(program))
{
process_.setProcessChannelMode(QProcess::SeparateChannels);
}
bool start()
{
process_.start(program_, {});
if (!process_.waitForStarted(timeoutMs)) {
fail(QString("Could not start %1: %2")
.arg(program_, process_.errorString()));
return false;
}
const QJsonObject ready = readMessage("ready", -1);
if (ready.isEmpty()) {
return false;
}
if (ready["protocol"].toInt(-1) != protocolVersion) {
fail(QString("Unexpected protocol version: %1")
.arg(ready["protocol"].toInt(-1)));
return false;
}
std::fprintf(stderr, "PASS: backend ready (protocol %d)\n", protocolVersion);
return true;
}
QJsonObject command(int command, const QJsonObject &data = {})
{
const int id = nextId_++;
QJsonObject request;
request["type"] = "command";
request["id"] = id;
request["command"] = command;
request["data"] = data;
const QByteArray json = QJsonDocument(request).toJson(QJsonDocument::Compact) + '\n';
if (process_.write(json) != json.size() || !process_.waitForBytesWritten(timeoutMs)) {
fail(QString("Could not write command %1: %2")
.arg(command)
.arg(process_.errorString()));
return {};
}
return readMessage("result", id);
}
bool stop()
{
const QJsonObject result = command(CMD_QUIT);
if (result.isEmpty() || result["result"].toInt() != RESULT_QUIT) {
fail("QUIT did not return RESULT_QUIT");
process_.kill();
process_.waitForFinished(timeoutMs);
return false;
}
process_.closeWriteChannel();
if (!process_.waitForFinished(timeoutMs)) {
fail("Backend did not stop after QUIT");
process_.kill();
process_.waitForFinished(timeoutMs);
return false;
}
drainStderr();
std::fprintf(stderr, "PASS: backend stopped cleanly\n");
return process_.exitStatus() == QProcess::NormalExit && process_.exitCode() == 0;
}
void abort()
{
if (process_.state() != QProcess::NotRunning) {
process_.kill();
process_.waitForFinished(timeoutMs);
}
drainStderr();
}
private:
QJsonObject readMessage(const QString &expectedType, int expectedId)
{
QElapsedTimer timer;
timer.start();
while (timer.elapsed() < timeoutMs) {
while (process_.canReadLine()) {
const QByteArray line = process_.readLine().trimmed();
if (line.isEmpty()) {
continue;
}
QJsonParseError error;
const QJsonDocument document = QJsonDocument::fromJson(line, &error);
if (error.error != QJsonParseError::NoError || !document.isObject()) {
fail(QString("Invalid JSON from backend: %1").arg(QString::fromUtf8(line)));
return {};
}
const QJsonObject message = document.object();
const QString type = message["type"].toString();
if (type == "event") {
std::fprintf(stderr,
"EVENT: webview=%d %s\n",
message["wv"].toInt(),
message["data"].toString().toUtf8().constData());
continue;
}
if (type == "protocol-error") {
fail(QString("Protocol error: %1").arg(message["message"].toString()));
return {};
}
if (type != expectedType) {
fail(QString("Expected message type %1, got %2")
.arg(expectedType, type));
return {};
}
if (expectedId >= 0 && message["id"].toInt(-1) != expectedId) {
fail(QString("Expected result id %1, got %2")
.arg(expectedId)
.arg(message["id"].toInt(-1)));
return {};
}
return message;
}
drainStderr();
if (process_.state() == QProcess::NotRunning) {
fail(QString("Backend stopped unexpectedly with exit code %1")
.arg(process_.exitCode()));
return {};
}
process_.waitForReadyRead(100);
}
fail(QString("Timed out waiting for %1").arg(expectedType));
return {};
}
void drainStderr()
{
const QByteArray diagnostics = process_.readAllStandardError();
if (!diagnostics.isEmpty()) {
std::fwrite(diagnostics.constData(), 1, diagnostics.size(), stderr);
}
}
static void fail(const QString &message)
{
std::fprintf(stderr, "FAIL: %s\n", message.toUtf8().constData());
}
QString program_;
QProcess process_;
int nextId_ = 1;
};
bool expectResult(const QJsonObject &message, int expected, const char *description)
{
if (message.isEmpty()) {
return false;
}
const int actual = message["result"].toInt();
if (actual != expected) {
std::fprintf(stderr,
"FAIL: %s returned %d, expected %d\n",
description,
actual,
expected);
return false;
}
std::fprintf(stderr, "PASS: %s\n", description);
return true;
}
QString backendProgram(const QCoreApplication &app)
{
const QString environmentProgram = qEnvironmentVariable("RKT_WEBVIEW_PRG");
if (!environmentProgram.isEmpty()) {
return environmentProgram;
}
#ifdef Q_OS_WIN
return app.applicationDirPath() + "/rktwebview_prg.exe";
#else
return app.applicationDirPath() + "/rktwebview_prg";
#endif
}
} // namespace
int main(int argc, char *argv[])
{
std::string me = argv[0];
QCoreApplication app(argc, argv);
const bool protocolOnly = app.arguments().contains("--protocol-only");
const QString program = backendProgram(app);
std::string loc = basedir(me);
#ifdef _WIN32
std::string prg = loc + "\\rktwebview_prg.exe";
SetDllDirectoryA("C:\\Qt\\6.10.2\\msvc2022_64\\bin");
#else
std::string prg = loc + "/rktwebview_prg";
#endif
#ifdef _WIN32
{
std::string e = std::string("RKT_WEBVIEW_PRG=") + prg;
_putenv(e.c_str());
if (!QFileInfo::exists(program)) {
std::fprintf(stderr,
"FAIL: backend executable not found: %s\n",
program.toUtf8().constData());
return 1;
}
#else
setenv("RKT_WEBVIEW_PRG", prg.c_str(), true);
setenv("LD_LIBRARY_PATH", loc.c_str(), true);
#endif
int context = rkt_webview_new_context("", nullptr);
rkt_webview_set_loglevel(rkt_webview_loglevel_t::log_debug);
rkt_webview_register_evt_callback(evt_cb);
int wv = rkt_webview_create(context, 0);
rkt_webview_set_title(wv, "Hi there, this is a title!");
rkt_webview_set_icon(wv, "../../rktplayer.png");
rkt_data_t *d = rkt_webview_info();
rkt_webview_free_data(d);
rkt_webview_move(wv, 100, 200);
rkt_webview_resize(wv, 800, 600);
rkt_webview_set_url(wv, "https://wikipedia.org");
d = rkt_webview_info();
fprintf(stderr, "%s\n", d->data.metrics.log_file);
rkt_webview_free_data(d);
while(rkt_webview_events_waiting() > 0) {
rkt_data_t *d = rkt_webview_get_event();
rkt_webview_free_data(d);
ProtocolClient client(program);
if (!client.start()) {
client.abort();
return 1;
}
#ifdef _WIN32
Sleep(30000);
#else
sleep(30
);
#endif
d = rkt_webview_info();
rkt_webview_free_data(d);
rkt_webview_close(wv);
bool ok = true;
ok = expectResult(client.command(CMD_NOOP), 0, "NOOP") && ok;
d = rkt_webview_info();
rkt_webview_free_data(d);
const QJsonObject initialInfo = client.command(CMD_INFO);
ok = expectResult(initialInfo, 0, "INFO reports no open windows") && ok;
rkt_webview_cleanup();
if (!protocolOnly && ok) {
QJsonObject contextData;
contextData["boilerplate_js"] = "";
contextData["has_pem_cert"] = false;
contextData["pem_cert"] = "";
const QJsonObject contextResult = client.command(CMD_CONTEXT_NEW, contextData);
const int context = contextResult["result"].toInt(-1);
if (context < 0) {
std::fprintf(stderr, "FAIL: CONTEXT_NEW returned %d\n", context);
ok = false;
} else {
std::fprintf(stderr, "PASS: context created (%d)\n", context);
}
QJsonObject createData;
createData["context"] = context;
createData["parent"] = 0;
const QJsonObject createResult = client.command(CMD_CREATE_WV, createData);
const int webview = createResult["result"].toInt(-1);
if (webview <= 0) {
std::fprintf(stderr, "FAIL: CREATE_WV returned %d\n", webview);
ok = false;
} else {
std::fprintf(stderr, "PASS: webview created (%d)\n", webview);
}
if (ok) {
QJsonObject titleData;
titleData["wv"] = webview;
titleData["title"] = "rktwebview stdio integration test";
ok = expectResult(client.command(CMD_SET_TITLE, titleData), 0, "SET_TITLE") && ok;
QJsonObject htmlData;
htmlData["wv"] = webview;
htmlData["html"] = "<html><head><title>stdio test</title></head>"
"<body><h1>rktwebview stdio test</h1></body></html>";
ok = expectResult(client.command(CMD_SET_HTML, htmlData), 0, "SET_HTML") && ok;
QJsonObject validData;
validData["wv"] = webview;
ok = expectResult(client.command(CMD_HANDLE_IS_VALID, validData), 1,
"HANDLE_IS_VALID before close") && ok;
QJsonObject closeData;
closeData["wv"] = webview;
ok = expectResult(client.command(CMD_CLOSE_WV, closeData), 0, "CLOSE_WV") && ok;
}
}
const bool stopped = client.stop();
return ok && stopped ? 0 : 1;
}
+56 -14
View File
@@ -1,18 +1,38 @@
#include "utils.h"
#include <QJsonDocument>
#include <QJsonObject>
#include <QString>
#include <chrono>
#include <cstdarg>
#include <cstdio>
#include <vector>
typedef struct {
std::chrono::time_point<std::chrono::system_clock> start;
} timer;
static int _log_level = LOG_INFO;
static timer *_timer = nullptr;
static QString formattedMessage(const char *format, va_list args)
{
va_list countArgs;
va_copy(countArgs, args);
const int length = std::vsnprintf(nullptr, 0, format, countArgs);
va_end(countArgs);
if (length < 0) {
return QString::fromUtf8(format);
}
std::vector<char> buffer(static_cast<size_t>(length) + 1);
std::vsnprintf(buffer.data(), buffer.size(), format, args);
return QString::fromUtf8(buffer.data(), length);
}
extern "C" {
int logLevel()
{
return _log_level;
@@ -26,26 +46,48 @@ void setLogLevel(int l)
const char *logIndicator(int l)
{
switch(l) {
case LOG_ERROR: return "ERROR ";
case LOG_INFO: return "INFO ";
case LOG_DEBUG: return "DEBUG ";
case LOG_WARNING: return "WARNING";
case LOG_ERROR: return "error";
case LOG_INFO: return "info";
case LOG_DEBUG: return "debug";
case LOG_WARNING: return "warning";
}
return "UNKNOWN";
return "unknown";
}
void logElapsed()
double logElapsedSeconds()
{
if (_timer == nullptr) {
_timer = new timer;
_timer->start = std::chrono::system_clock::now();
}
auto c = std::chrono::system_clock::now();
auto duration = c - _timer->start;
auto milliseconds
const auto current = std::chrono::system_clock::now();
const auto milliseconds
= std::chrono::duration_cast<std::chrono::milliseconds>(
duration)
current - _timer->start)
.count();
fprintf(stderr, "%8.3lf: ", milliseconds/ 1000.0);
return milliseconds / 1000.0;
}
void logMessage(int level, const char *format, ...)
{
if (logLevel() < level) {
return;
}
va_list args;
va_start(args, format);
const QString message = formattedMessage(format, args);
va_end(args);
QJsonObject object;
object.insert(QStringLiteral("topic"), QStringLiteral("webview-backend"));
object.insert(QStringLiteral("level"), QString::fromLatin1(logIndicator(level)));
object.insert(QStringLiteral("elapsed"), logElapsedSeconds());
object.insert(QStringLiteral("message"), message);
const QByteArray json = QJsonDocument(object).toJson(QJsonDocument::Compact);
std::fprintf(stderr, "json:%s\n", json.constData());
std::fflush(stderr);
}
}
+28 -20
View File
@@ -1,7 +1,10 @@
#ifndef UTILS_H
#define UTILS_H
//#define DEBUG_RKT_WEBVIEW
#include <string>
#include "rktwebview_global.h"
#include "rktwebview_types.h"
inline std::string basedir(const std::string &path)
@@ -27,35 +30,40 @@ inline std::string basedir(const std::string &path)
return r;
}
int logLevel();
void setLogLevel(int l);
void logElapsed();
const char *logIndicator(int l);
extern "C" {
#ifdef RKTWEBVIEW_PRG_EXE
#undef RKTWEBVIEW_EXPORT
#define RKTWEBVIEW_EXPORT
#endif
RKTWEBVIEW_EXPORT int logLevel();
RKTWEBVIEW_EXPORT void setLogLevel(int l);
RKTWEBVIEW_EXPORT double logElapsedSeconds();
RKTWEBVIEW_EXPORT const char *logIndicator(int l);
RKTWEBVIEW_EXPORT void logMessage(int level, const char *format, ...);
}
#define LOG_ERROR 1
#define LOG_WARNING 2
#define LOG_INFO 3
#define LOG_DEBUG 4
#define MKLOGSTMT(level, code) if (logLevel() >= level) { fprintf(stderr, "%s: ", logIndicator(level));logElapsed();code;fflush(stderr); }
#define MKL0(level, msg) MKLOGSTMT(level, fprintf(stderr, msg))
#define MKL1(level, msg, a) MKLOGSTMT(level, fprintf(stderr, msg, a))
#define MKL2(level, msg, a, b) MKLOGSTMT(level, fprintf(stderr, msg, a, b))
#define ERROR0(msg) logMessage(LOG_ERROR, msg)
#define WARN0(msg) logMessage(LOG_WARNING, msg)
#define INFO0(msg) logMessage(LOG_INFO, msg)
#define DEBUG0(msg) logMessage(LOG_DEBUG, msg)
#define ERROR0(msg) MKL0(LOG_ERROR, msg)
#define WARN0(msg) MKL0(LOG_WARNING, msg)
#define INFO0(msg) MKL0(LOG_INFO, msg)
#define DEBUG0(msg) MKL0(LOG_DEBUG, msg)
#define ERROR1(msg, a) logMessage(LOG_ERROR, msg, a)
#define WARN1(msg, a) logMessage(LOG_WARNING, msg, a)
#define INFO1(msg, a) logMessage(LOG_INFO, msg, a)
#define DEBUG1(msg, a) logMessage(LOG_DEBUG, msg, a)
#define ERROR1(msg, a) MKL1(LOG_ERROR, msg, a)
#define WARN1(msg, a) MKL1(LOG_WARNING, msg, a)
#define INFO1(msg, a) MKL1(LOG_INFO, msg, a)
#define DEBUG1(msg, a) MKL1(LOG_DEBUG, msg, a)
#define ERROR2(msg, a, b) logMessage(LOG_ERROR, msg, a, b)
#define WARN2(msg, a, b) logMessage(LOG_WARNING, msg, a, b)
#define INFO2(msg, a, b) logMessage(LOG_INFO, msg, a, b)
#define DEBUG2(msg, a, b) logMessage(LOG_DEBUG, msg, a, b)
#define ERROR2(msg, a, b) MKL2(LOG_ERROR, msg, a, b)
#define WARN2(msg, a, b) MKL2(LOG_WARNING, msg, a, b)
#define INFO2(msg, a, b) MKL2(LOG_INFO, msg, a, b)
#define DEBUG2(msg, a, b) MKL2(LOG_DEBUG, msg, a, b)
#define INFO3(msg, a, b, c) logMessage(LOG_INFO, msg, a, b, c)
inline void do_free_data(rkt_data_t *d)
{