/* * MIT License * * Copyright (c) 2017 Serge Zaitsev * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ #ifndef WEBVIEW_H #define WEBVIEW_H #ifdef __cplusplus extern "C" { #endif #ifdef WEBVIEW_STATIC #define WEBVIEW_API static #else #define WEBVIEW_API extern #endif #include #include #include #include #include #include struct webview_priv { GtkWidget *window; GtkWidget *scroller; GtkWidget *webview; GtkWidget *inspector_window; GAsyncQueue *queue; int ready; int js_busy; int should_exit; }; struct webview; typedef void (*webview_external_invoke_cb_t)(struct webview *w, const char *arg); struct webview { const char *url; const char *title; int width; int height; int resizable; int transparentTitlebar; int debug; webview_external_invoke_cb_t external_invoke_cb; struct webview_priv priv; void *userdata; }; enum webview_dialog_type { WEBVIEW_DIALOG_TYPE_OPEN = 0, WEBVIEW_DIALOG_TYPE_SAVE = 1, WEBVIEW_DIALOG_TYPE_ALERT = 2 }; #define WEBVIEW_DIALOG_FLAG_FILE (0 << 0) #define WEBVIEW_DIALOG_FLAG_DIRECTORY (1 << 0) #define WEBVIEW_DIALOG_FLAG_INFO (1 << 1) #define WEBVIEW_DIALOG_FLAG_WARNING (2 << 1) #define WEBVIEW_DIALOG_FLAG_ERROR (3 << 1) #define WEBVIEW_DIALOG_FLAG_ALERT_MASK (3 << 1) typedef void (*webview_dispatch_fn)(struct webview *w, void *arg); struct webview_dispatch_arg { webview_dispatch_fn fn; struct webview *w; void *arg; }; #define DEFAULT_URL \ "data:text/" \ "html,%3C%21DOCTYPE%20html%3E%0A%3Chtml%20lang=%22en%22%3E%0A%3Chead%3E%" \ "3Cmeta%20charset=%22utf-8%22%3E%3Cmeta%20http-equiv=%22IE=edge%22%" \ "20content=%22IE=edge%22%3E%3C%2Fhead%3E%0A%3Cbody%3E%3Cdiv%20id=%22app%22%" \ "3E%3C%2Fdiv%3E%3Cscript%20type=%22text%2Fjavascript%22%3E%3C%2Fscript%3E%" \ "3C%2Fbody%3E%0A%3C%2Fhtml%3E" #define CSS_INJECT_FUNCTION \ "(function(e){var " \ "t=document.createElement('style'),d=document.head||document." \ "getElementsByTagName('head')[0];t.setAttribute('type','text/" \ "css'),t.styleSheet?t.styleSheet.cssText=e:t.appendChild(document." \ "createTextNode(e)),d.appendChild(t)})" static const char *webview_check_url(const char *url) { if (url == NULL || strlen(url) == 0) { return DEFAULT_URL; } return url; } WEBVIEW_API int webview(const char *title, const char *url, int width, int height, int resizable); WEBVIEW_API int webview_init(struct webview *w); WEBVIEW_API int webview_loop(struct webview *w, int blocking); WEBVIEW_API int webview_eval(struct webview *w, const char *js); WEBVIEW_API int webview_inject_css(struct webview *w, const char *css); WEBVIEW_API void webview_set_title(struct webview *w, const char *title); WEBVIEW_API void webview_set_fullscreen(struct webview *w, int fullscreen); WEBVIEW_API void webview_set_color(struct webview *w, uint8_t r, uint8_t g, uint8_t b, uint8_t a); WEBVIEW_API void webview_dialog(struct webview *w, enum webview_dialog_type dlgtype, int flags, const char *title, const char *arg, char *result, size_t resultsz, char *filter); WEBVIEW_API void webview_dispatch(struct webview *w, webview_dispatch_fn fn, void *arg); WEBVIEW_API void webview_terminate(struct webview *w); WEBVIEW_API void webview_exit(struct webview *w); WEBVIEW_API void webview_debug(const char *format, ...); WEBVIEW_API void webview_print_log(const char *s); WEBVIEW_API int webview(const char *title, const char *url, int width, int height, int resizable) { struct webview webview; memset(&webview, 0, sizeof(webview)); webview.title = title; webview.url = url; webview.width = width; webview.height = height; webview.resizable = resizable; int r = webview_init(&webview); if (r != 0) { return r; } while (webview_loop(&webview, 1) == 0) { } webview_exit(&webview); return 0; } WEBVIEW_API void webview_debug(const char *format, ...) { char buf[4096]; va_list ap; va_start(ap, format); vsnprintf(buf, sizeof(buf), format, ap); webview_print_log(buf); va_end(ap); } static int webview_js_encode(const char *s, char *esc, size_t n) { int r = 1; /* At least one byte for trailing zero */ for (; *s; s++) { const unsigned char c = *s; if (c >= 0x20 && c < 0x80 && strchr("<>\\'\"", c) == NULL) { if (n > 0) { *esc++ = c; n--; } r++; } else { if (n > 0) { snprintf(esc, n, "\\x%02x", (int)c); esc += 4; n -= 4; } r += 4; } } return r; } WEBVIEW_API int webview_inject_css(struct webview *w, const char *css) { int n = webview_js_encode(css, NULL, 0); char *esc = (char *)calloc(1, sizeof(CSS_INJECT_FUNCTION) + n + 4); if (esc == NULL) { return -1; } char *js = (char *)calloc(1, n); webview_js_encode(css, js, n); snprintf(esc, sizeof(CSS_INJECT_FUNCTION) + n + 4, "%s(\"%s\")", CSS_INJECT_FUNCTION, js); int r = webview_eval(w, esc); free(js); free(esc); return r; } static void external_message_received_cb(WebKitUserContentManager *m, WebKitJavascriptResult *r, gpointer arg) { (void)m; struct webview *w = (struct webview *)arg; if (w->external_invoke_cb == NULL) { return; } JSGlobalContextRef context = webkit_javascript_result_get_global_context(r); JSValueRef value = webkit_javascript_result_get_value(r); JSStringRef js = JSValueToStringCopy(context, value, NULL); size_t n = JSStringGetMaximumUTF8CStringSize(js); char *s = g_new(char, n); JSStringGetUTF8CString(js, s, n); w->external_invoke_cb(w, s); JSStringRelease(js); g_free(s); } static void webview_load_changed_cb(WebKitWebView *webview, WebKitLoadEvent event, gpointer arg) { (void)webview; struct webview *w = (struct webview *)arg; if (event == WEBKIT_LOAD_FINISHED) { w->priv.ready = 1; } } static void webview_destroy_cb(GtkWidget *widget, gpointer arg) { (void)widget; struct webview *w = (struct webview *)arg; webview_terminate(w); } static gboolean webview_context_menu_cb(WebKitWebView *webview, GtkWidget *default_menu, WebKitHitTestResult *hit_test_result, gboolean triggered_with_keyboard, gpointer userdata) { (void)webview; (void)default_menu; (void)hit_test_result; (void)triggered_with_keyboard; (void)userdata; return TRUE; } WEBVIEW_API int webview_init(struct webview *w) { if (gtk_init_check(0, NULL) == FALSE) { return -1; } w->priv.ready = 0; w->priv.should_exit = 0; w->priv.queue = g_async_queue_new(); w->priv.window = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_window_set_title(GTK_WINDOW(w->priv.window), w->title); if (w->resizable) { gtk_window_set_default_size(GTK_WINDOW(w->priv.window), w->width, w->height); } else { gtk_widget_set_size_request(w->priv.window, w->width, w->height); } gtk_window_set_resizable(GTK_WINDOW(w->priv.window), !!w->resizable); gtk_window_set_position(GTK_WINDOW(w->priv.window), GTK_WIN_POS_CENTER); w->priv.scroller = gtk_scrolled_window_new(NULL, NULL); gtk_container_add(GTK_CONTAINER(w->priv.window), w->priv.scroller); WebKitUserContentManager *m = webkit_user_content_manager_new(); webkit_user_content_manager_register_script_message_handler(m, "external"); g_signal_connect(m, "script-message-received::external", G_CALLBACK(external_message_received_cb), w); w->priv.webview = webkit_web_view_new_with_user_content_manager(m); webkit_web_view_load_uri(WEBKIT_WEB_VIEW(w->priv.webview), webview_check_url(w->url)); g_signal_connect(G_OBJECT(w->priv.webview), "load-changed", G_CALLBACK(webview_load_changed_cb), w); gtk_container_add(GTK_CONTAINER(w->priv.scroller), w->priv.webview); if (w->debug) { WebKitSettings *settings = webkit_web_view_get_settings(WEBKIT_WEB_VIEW(w->priv.webview)); webkit_settings_set_enable_write_console_messages_to_stdout(settings, true); webkit_settings_set_enable_developer_extras(settings, true); } else { g_signal_connect(G_OBJECT(w->priv.webview), "context-menu", G_CALLBACK(webview_context_menu_cb), w); } gtk_widget_show_all(w->priv.window); webkit_web_view_run_javascript( WEBKIT_WEB_VIEW(w->priv.webview), "window.external={invoke:function(x){" "window.webkit.messageHandlers.external.postMessage(x);}}", NULL, NULL, NULL); g_signal_connect(G_OBJECT(w->priv.window), "destroy", G_CALLBACK(webview_destroy_cb), w); return 0; } WEBVIEW_API int webview_loop(struct webview *w, int blocking) { gtk_main_iteration_do(blocking); return w->priv.should_exit; } WEBVIEW_API void webview_set_title(struct webview *w, const char *title) { gtk_window_set_title(GTK_WINDOW(w->priv.window), title); } WEBVIEW_API void webview_set_fullscreen(struct webview *w, int fullscreen) { if (fullscreen) { gtk_window_fullscreen(GTK_WINDOW(w->priv.window)); } else { gtk_window_unfullscreen(GTK_WINDOW(w->priv.window)); } } WEBVIEW_API void webview_set_color(struct webview *w, uint8_t r, uint8_t g, uint8_t b, uint8_t a) { GdkRGBA color = {r / 255.0, g / 255.0, b / 255.0, a / 255.0}; webkit_web_view_set_background_color(WEBKIT_WEB_VIEW(w->priv.webview), &color); } WEBVIEW_API void webview_dialog(struct webview *w, enum webview_dialog_type dlgtype, int flags, const char *title, const char *arg, char *result, size_t resultsz, char *filter) { GtkWidget *dlg; if (result != NULL) { result[0] = '\0'; } if (dlgtype == WEBVIEW_DIALOG_TYPE_OPEN || dlgtype == WEBVIEW_DIALOG_TYPE_SAVE) { dlg = gtk_file_chooser_dialog_new( title, GTK_WINDOW(w->priv.window), (dlgtype == WEBVIEW_DIALOG_TYPE_OPEN ? (flags & WEBVIEW_DIALOG_FLAG_DIRECTORY ? GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER : GTK_FILE_CHOOSER_ACTION_OPEN) : GTK_FILE_CHOOSER_ACTION_SAVE), "_Cancel", GTK_RESPONSE_CANCEL, (dlgtype == WEBVIEW_DIALOG_TYPE_OPEN ? "_Open" : "_Save"), GTK_RESPONSE_ACCEPT, NULL); if (filter[0] != '\0') { GtkFileFilter *file_filter = gtk_file_filter_new(); gchar **filters = g_strsplit(filter, ",", -1); gint i; for(i = 0; filters && filters[i]; i++) { gtk_file_filter_add_pattern(file_filter, filters[i]); } gtk_file_filter_set_name(file_filter, filter); gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dlg), file_filter); g_strfreev(filters); } gtk_file_chooser_set_local_only(GTK_FILE_CHOOSER(dlg), FALSE); gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(dlg), FALSE); gtk_file_chooser_set_show_hidden(GTK_FILE_CHOOSER(dlg), TRUE); gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(dlg), TRUE); gtk_file_chooser_set_create_folders(GTK_FILE_CHOOSER(dlg), TRUE); gint response = gtk_dialog_run(GTK_DIALOG(dlg)); if (response == GTK_RESPONSE_ACCEPT) { gchar *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dlg)); g_strlcpy(result, filename, resultsz); g_free(filename); } gtk_widget_destroy(dlg); } else if (dlgtype == WEBVIEW_DIALOG_TYPE_ALERT) { GtkMessageType type = GTK_MESSAGE_OTHER; switch (flags & WEBVIEW_DIALOG_FLAG_ALERT_MASK) { case WEBVIEW_DIALOG_FLAG_INFO: type = GTK_MESSAGE_INFO; break; case WEBVIEW_DIALOG_FLAG_WARNING: type = GTK_MESSAGE_WARNING; break; case WEBVIEW_DIALOG_FLAG_ERROR: type = GTK_MESSAGE_ERROR; break; } dlg = gtk_message_dialog_new(GTK_WINDOW(w->priv.window), GTK_DIALOG_MODAL, type, GTK_BUTTONS_OK, "%s", title); gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(dlg), "%s", arg); gtk_dialog_run(GTK_DIALOG(dlg)); gtk_widget_destroy(dlg); } } static void webview_eval_finished(GObject *object, GAsyncResult *result, gpointer userdata) { (void)object; (void)result; struct webview *w = (struct webview *)userdata; w->priv.js_busy = 0; } WEBVIEW_API int webview_eval(struct webview *w, const char *js) { while (w->priv.ready == 0) { g_main_context_iteration(NULL, TRUE); } w->priv.js_busy = 1; webkit_web_view_run_javascript(WEBKIT_WEB_VIEW(w->priv.webview), js, NULL, webview_eval_finished, w); while (w->priv.js_busy) { g_main_context_iteration(NULL, TRUE); } return 0; } static gboolean webview_dispatch_wrapper(gpointer userdata) { struct webview *w = (struct webview *)userdata; for (;;) { struct webview_dispatch_arg *arg = (struct webview_dispatch_arg *)g_async_queue_try_pop(w->priv.queue); if (arg == NULL) { break; } (arg->fn)(w, arg->arg); g_free(arg); } return FALSE; } WEBVIEW_API void webview_dispatch(struct webview *w, webview_dispatch_fn fn, void *arg) { struct webview_dispatch_arg *context = (struct webview_dispatch_arg *)g_new(struct webview_dispatch_arg *, 1); context->w = w; context->arg = arg; context->fn = fn; g_async_queue_lock(w->priv.queue); g_async_queue_push_unlocked(w->priv.queue, context); if (g_async_queue_length_unlocked(w->priv.queue) == 1) { gdk_threads_add_idle(webview_dispatch_wrapper, w); } g_async_queue_unlock(w->priv.queue); } WEBVIEW_API void webview_terminate(struct webview *w) { w->priv.should_exit = 1; } WEBVIEW_API void webview_exit(struct webview *w) { (void)w; } WEBVIEW_API void webview_print_log(const char *s) { fprintf(stderr, "%s\n", s); } #ifdef __cplusplus } #endif #endif /* WEBVIEW_H */