diff --git a/CMakeLists.txt b/CMakeLists.txt index 8d837c9..063e241 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -43,6 +43,9 @@ add_executable(rktwebview_prg rkt_protocol.h rktwebview_types.h utils.cpp utils.h + + build_menu_from_json.cpp + build_menu_from_json.h ) add_executable(rktwebview_test diff --git a/build_menu_from_json.cpp b/build_menu_from_json.cpp new file mode 100644 index 0000000..141e344 --- /dev/null +++ b/build_menu_from_json.cpp @@ -0,0 +1,97 @@ +#include +#include +#include +#include +#include "rktwebview_qt.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()) { + return nullptr; + } + + QJsonObject root = doc.object(); + QJsonArray items = root.value("menu").toArray(); + + return buildMenuObjectFromJson(self, items, source_handle); +} diff --git a/build_menu_from_json.h b/build_menu_from_json.h new file mode 100644 index 0000000..d5c1ad3 --- /dev/null +++ b/build_menu_from_json.h @@ -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 diff --git a/command.h b/command.h index a777679..7f38856 100644 --- a/command.h +++ b/command.h @@ -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 { diff --git a/main.cpp b/main.cpp index b20cd83..30ab860 100644 --- a/main.cpp +++ b/main.cpp @@ -339,6 +339,54 @@ void Handler::run() result_queue->enqueue(r); } break; + case CMD_CREATE_TRAY: { + QString icon_file = data_obj["icon"].toString(); + QString tooltip = data_obj["tooltip"].toString(); + + int tray = webview_handler->rktTrayCreate(icon_file.toUtf8().constData(), + tooltip.toUtf8().constData(), + event_cb); + result_queue->enqueue(tray); + } + break; + case CMD_TRAY_SET_ICON: { + int tray = data_obj["tray"].toInt(); + QString icon_file = data_obj["icon"].toString(); + + result_t r = webview_handler->rktTraySetIcon(tray, + icon_file.toUtf8().constData()); + result_queue->enqueue(r); + } + break; + case CMD_TRAY_SET_TOOLTIP: { + int tray = data_obj["tray"].toInt(); + QString tooltip = data_obj["tooltip"].toString(); + + result_t r = webview_handler->rktTraySetTooltip(tray, + tooltip.toUtf8().constData()); + result_queue->enqueue(r); + } + break; + case CMD_TRAY_SHOW_MESSAGE: { + int tray = data_obj["tray"].toInt(); + QString title = data_obj["title"].toString(); + QString message = data_obj["message"].toString(); + + result_t r = webview_handler->rktTrayShowMessage(tray, + title.toUtf8().constData(), + message.toUtf8().constData()); + result_queue->enqueue(r); + } + break; + case CMD_TRAY_SET_MENU: { + int tray = data_obj["tray"].toInt(); + QString menu_json = data_obj["menu_json"].toString(); + + result_t r = webview_handler->rktTraySetMenu(tray, + menu_json.toUtf8().constData()); + result_queue->enqueue(r); + } + break; case CMD_CHOOSE_DIR: { int wv = data_obj["wv"].toInt(); QString title = data_obj["title"].toString(); diff --git a/rkt_protocol.h b/rkt_protocol.h index 08e1cf3..fe579a3 100644 --- a/rkt_protocol.h +++ b/rkt_protocol.h @@ -29,6 +29,12 @@ #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_NOOP 33621 #define RESULT_QUIT 36379 diff --git a/rktwebview.cpp b/rktwebview.cpp index 505cec8..09babf4 100644 --- a/rktwebview.cpp +++ b/rktwebview.cpp @@ -876,3 +876,51 @@ 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) +{ + CMDRES(CMD_TRAY_SET_MENU, tray, "menu", menu_json) +} + diff --git a/rktwebview.h b/rktwebview.h index 20be273..1713d54 100644 --- a/rktwebview.h +++ b/rktwebview.h @@ -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 diff --git a/rktwebview_qt.cpp b/rktwebview_qt.cpp index 78e182a..2a5e4fa 100644 --- a/rktwebview_qt.cpp +++ b/rktwebview_qt.cpp @@ -18,15 +18,36 @@ #include #include #include +#include +#include +#include +#include "build_menu_from_json.h" static inline char *copyString(const char *s) { - int l = strlen(s); + int l = static_cast(strlen(s)); char *cpy = static_cast(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) { @@ -168,6 +189,19 @@ void Rktwebview_qt::processCommand(Command *cmd) } _view_js_callbacks.remove(wv); delete w; + } 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(); + 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,108 @@ 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(); + + int id = nextHandle(); + + QSystemTrayIcon *tray = new QSystemTrayIcon(this); + tray->setIcon(QIcon(icon_file)); + tray->setToolTip(tooltip); + + connect(tray, &QSystemTrayIcon::activated, + this, [this, id](QSystemTrayIcon::ActivationReason reason) { + EventContainer e("tray-activated"); + e["reason"] = activationReasonToString(reason); + this->triggerEvent(id, e); + }); + + connect(tray, &QSystemTrayIcon::messageClicked, + this, [this, id]() { + EventContainer e("tray-message-clicked"); + this->triggerEvent(id, e); + }); + + _trays[id] = tray; + + 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(); + QString menu_json = cmd->args[1].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 = buildMenuFromJson(this, menu_json, tray_id); + if (menu == nullptr) { + 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; @@ -888,6 +1036,71 @@ 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(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) +{ + Command c(COMMAND_TRAY_SET_TOOLTIP); + c.args.push_back(tray); + c.args.push_back(tooltip); + + postCommand(&c); + while(!c.done) { doEvents(); } + + 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); + + 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 +1298,18 @@ Rktwebview_qt::~Rktwebview_qt() delete p; } + QList 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 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; } diff --git a/rktwebview_qt.h b/rktwebview_qt.h index 0069a0e..d978107 100644 --- a/rktwebview_qt.h +++ b/rktwebview_qt.h @@ -19,6 +19,8 @@ class WebViewQt; class WebviewWindow; class Command; class CommandEvent; +class QSystemTrayIcon; +class QMenu; class Rktwebview_qt : public QObject { @@ -30,6 +32,8 @@ private: int _context_counter; QHash _contexts; QHash _views; + QHash _trays; + QHash _tray_menus; QHash _view_js_callbacks; QTimer _process_events; @@ -114,6 +118,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);