tether 0.3.5

Windows that are web views.
Documentation
#include <gtk/gtk.h>
#include <webkit2/webkit2.h>

#include "tether.h"

// ===============
// RANDOM NONSENSE
// ===============

struct _tether {
    GtkWindow *window;
    WebKitWebView *webview;
    void *data;
    void (*closed)(void *data);
};

struct dispatch {
    void *data;
    void (*func)(void *data);
};

struct handler {
    void *data;
    void (*func)(void *data, const char *message);
};

static int dispatched(void *ctx) {
    struct dispatch *dispatch = (struct dispatch *)ctx;
    dispatch->func(dispatch->data);
    return G_SOURCE_REMOVE;
}

static void message_received(WebKitUserContentManager *manager, WebKitJavascriptResult *result, void *ctx) {
    (void)manager;
    struct handler *handler = (struct handler *)ctx;
    JSCValue *val = webkit_javascript_result_get_js_value(result);
    char *message = jsc_value_to_string(val);
    handler->func(handler->data, message);
    g_free(message);
}

static void handler_free(void *ctx, GClosure *closure) {
    (void)closure;
    free(ctx);
}

static void window_destroyed(GtkWidget* widget, void *ctx) {
    (void)widget;
    tether self = (tether)ctx;
    self->closed(self->data);
    free(self);
}

gboolean context_menu(WebKitWebView *wv, WebKitContextMenu *cm, GdkEvent *e, WebKitHitTestResult *htr, void *ctx) {
    (void)wv;
    (void)e;
    (void)htr;
    (void)ctx;

    guint i = webkit_context_menu_get_n_items(cm);
    while (i --> 0) {
        WebKitContextMenuItem *item = webkit_context_menu_get_item_at_position(cm, i);
        switch (webkit_context_menu_item_get_stock_action(item)) {
            case WEBKIT_CONTEXT_MENU_ACTION_NO_ACTION:
            case WEBKIT_CONTEXT_MENU_ACTION_OPEN_LINK:
            case WEBKIT_CONTEXT_MENU_ACTION_COPY_LINK_TO_CLIPBOARD:
            case WEBKIT_CONTEXT_MENU_ACTION_COPY_IMAGE_TO_CLIPBOARD:
            case WEBKIT_CONTEXT_MENU_ACTION_COPY_IMAGE_URL_TO_CLIPBOARD:
            case WEBKIT_CONTEXT_MENU_ACTION_COPY:
            case WEBKIT_CONTEXT_MENU_ACTION_CUT:
            case WEBKIT_CONTEXT_MENU_ACTION_PASTE:
            case WEBKIT_CONTEXT_MENU_ACTION_DELETE:
            case WEBKIT_CONTEXT_MENU_ACTION_SELECT_ALL:
            case WEBKIT_CONTEXT_MENU_ACTION_INPUT_METHODS:
            case WEBKIT_CONTEXT_MENU_ACTION_UNICODE:
            case WEBKIT_CONTEXT_MENU_ACTION_SPELLING_GUESS:
            case WEBKIT_CONTEXT_MENU_ACTION_NO_GUESSES_FOUND:
            case WEBKIT_CONTEXT_MENU_ACTION_IGNORE_SPELLING:
            case WEBKIT_CONTEXT_MENU_ACTION_LEARN_SPELLING:
            case WEBKIT_CONTEXT_MENU_ACTION_IGNORE_GRAMMAR:
            case WEBKIT_CONTEXT_MENU_ACTION_FONT_MENU:
            case WEBKIT_CONTEXT_MENU_ACTION_BOLD:
            case WEBKIT_CONTEXT_MENU_ACTION_ITALIC:
            case WEBKIT_CONTEXT_MENU_ACTION_UNDERLINE:
            case WEBKIT_CONTEXT_MENU_ACTION_OUTLINE:
            case WEBKIT_CONTEXT_MENU_ACTION_INSPECT_ELEMENT:
            case WEBKIT_CONTEXT_MENU_ACTION_COPY_VIDEO_LINK_TO_CLIPBOARD:
            case WEBKIT_CONTEXT_MENU_ACTION_COPY_AUDIO_LINK_TO_CLIPBOARD:
            case WEBKIT_CONTEXT_MENU_ACTION_TOGGLE_MEDIA_CONTROLS:
            case WEBKIT_CONTEXT_MENU_ACTION_TOGGLE_MEDIA_LOOP:
            case WEBKIT_CONTEXT_MENU_ACTION_ENTER_VIDEO_FULLSCREEN:
            case WEBKIT_CONTEXT_MENU_ACTION_MEDIA_PLAY:
            case WEBKIT_CONTEXT_MENU_ACTION_MEDIA_PAUSE:
            case WEBKIT_CONTEXT_MENU_ACTION_MEDIA_MUTE:
            case WEBKIT_CONTEXT_MENU_ACTION_CUSTOM:
                break;
            case WEBKIT_CONTEXT_MENU_ACTION_OPEN_LINK_IN_NEW_WINDOW:
            case WEBKIT_CONTEXT_MENU_ACTION_DOWNLOAD_LINK_TO_DISK:
            case WEBKIT_CONTEXT_MENU_ACTION_OPEN_IMAGE_IN_NEW_WINDOW:
            case WEBKIT_CONTEXT_MENU_ACTION_DOWNLOAD_IMAGE_TO_DISK:
            case WEBKIT_CONTEXT_MENU_ACTION_DOWNLOAD_VIDEO_TO_DISK:
            case WEBKIT_CONTEXT_MENU_ACTION_DOWNLOAD_AUDIO_TO_DISK:
            case WEBKIT_CONTEXT_MENU_ACTION_OPEN_FRAME_IN_NEW_WINDOW:
            case WEBKIT_CONTEXT_MENU_ACTION_GO_BACK:
            case WEBKIT_CONTEXT_MENU_ACTION_GO_FORWARD:
            case WEBKIT_CONTEXT_MENU_ACTION_STOP:
            case WEBKIT_CONTEXT_MENU_ACTION_RELOAD:
            case WEBKIT_CONTEXT_MENU_ACTION_OPEN_VIDEO_IN_NEW_WINDOW:
            case WEBKIT_CONTEXT_MENU_ACTION_OPEN_AUDIO_IN_NEW_WINDOW:
                webkit_context_menu_remove(cm, item);
                break;
        }
    }

    return FALSE;
}

// ==============
// EXPORTED STUFF
// ==============

void tether_start(void (*func)(void)) {
    gtk_init(0, NULL);
    func();
    gtk_main();
}

void tether_dispatch(void *data, void (*func)(void *data)) {
    struct dispatch *dispatch = malloc(sizeof *dispatch);
    dispatch->data = data;
    dispatch->func = func;

    g_idle_add_full(
        G_PRIORITY_HIGH_IDLE,
        dispatched,
        dispatch,
        free
    );
}

void tether_exit(void) {
    gtk_main_quit();
}

tether tether_new(tether_options opts) {
    tether self = malloc(sizeof *self);
    self->data = opts.data;
    self->closed = opts.closed;

    // Create the window.
    GtkWindow *window = self->window = GTK_WINDOW(gtk_window_new(GTK_WINDOW_TOPLEVEL));
    gtk_window_set_default_size(window, opts.initial_width, opts.initial_height);
    gtk_widget_set_size_request(GTK_WIDGET(window), opts.minimum_width, opts.minimum_height);
    if (opts.borderless) gtk_window_set_decorated(window, FALSE);
    gtk_window_set_position(window, GTK_WIN_POS_CENTER);

    // Free resources and notify the user when the window's been closed.
    g_signal_connect_data(
        window,
        "destroy",
        G_CALLBACK(window_destroyed),
        self,
        NULL,
        0
    );

    // Create the web view.
    WebKitWebView *webview = self->webview = WEBKIT_WEB_VIEW(webkit_web_view_new());
    WebKitSettings *settings = webkit_web_view_get_settings(webview);
    WebKitUserContentManager *manager = webkit_web_view_get_user_content_manager(webview);
    if (opts.debug) webkit_settings_set_enable_developer_extras(settings, TRUE);

    // Listen for messages.
    struct handler *handler = malloc(sizeof *handler);
    handler->data = opts.data;
    handler->func = opts.message;

    WebKitUserScript *script = webkit_user_script_new(
        "window.tether = function (s) { window.webkit.messageHandlers.__tether.postMessage(s); }",
        WEBKIT_USER_CONTENT_INJECT_TOP_FRAME,
        WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START,
        NULL,
        NULL
    );

    g_signal_connect_data(
        manager,
        "script-message-received::__tether",
        G_CALLBACK(message_received),
        handler,
        handler_free,
        0
    );

    webkit_user_content_manager_register_script_message_handler(manager, "__tether");
    webkit_user_content_manager_add_script(manager, script);

    // Remove navigation items from the context menu.
    g_signal_connect(webview, "context-menu", G_CALLBACK(context_menu), NULL);

    // Attach the web view to the window.
    gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(webview));
    gtk_widget_grab_focus(GTK_WIDGET(webview));

    // Show the window.
    gtk_widget_show_all(GTK_WIDGET(window));

    return self;
}

void tether_eval(tether self, const char *js) {
    webkit_web_view_run_javascript(self->webview, js, NULL, NULL, NULL);
}

void tether_load(tether self, const char *html) {
    webkit_web_view_load_html(self->webview, html, NULL);
}

void tether_title(tether self, const char *title) {
    gtk_window_set_title(self->window, title);
}

void tether_focus(tether self) {
    gtk_window_present(self->window);
}

void tether_close(tether self) {
    gtk_window_close(self->window);
}