mirror of https://github.com/adamdruppe/arsd.git
more work in progress
This commit is contained in:
parent
49a63f04ef
commit
d177023e44
|
@ -0,0 +1,526 @@
|
|||
/++
|
||||
A webview (based on [arsd.webview]) for minigui.
|
||||
|
||||
For now at least, to use this, you MUST have a CefApp in scope in main for the duration of your gui application.
|
||||
|
||||
History:
|
||||
Added November 5, 2021. NOT YET STABLE.
|
||||
+/
|
||||
module minigui_addons.webview;
|
||||
|
||||
version(linux)
|
||||
version=cef;
|
||||
version(Windows)
|
||||
version=wv2;
|
||||
|
||||
|
||||
class StateChanged(alias field) : Event {
|
||||
enum EventString = __traits(identifier, __traits(parent, field)) ~ "." ~ __traits(identifier, field) ~ ":change";
|
||||
override bool cancelable() const { return false; }
|
||||
this(Widget target, typeof(field) newValue) {
|
||||
this.newValue = newValue;
|
||||
super(EventString, target);
|
||||
}
|
||||
|
||||
typeof(field) newValue;
|
||||
}
|
||||
|
||||
void addWhenTriggered(Widget w, void delegate() dg) {
|
||||
w.addEventListener("triggered", dg);
|
||||
}
|
||||
|
||||
mixin template Observable(T, string name) {
|
||||
private T backing;
|
||||
|
||||
mixin(q{
|
||||
void } ~ name ~ q{_changed (void delegate(T) dg) {
|
||||
this.addEventListener((StateChanged!this_thing ev) {
|
||||
dg(ev.newValue);
|
||||
});
|
||||
}
|
||||
|
||||
@property T } ~ name ~ q{ () {
|
||||
return backing;
|
||||
}
|
||||
|
||||
@property void } ~ name ~ q{ (T t) {
|
||||
backing = t;
|
||||
auto event = new StateChanged!this_thing(this, t);
|
||||
event.dispatch();
|
||||
}
|
||||
});
|
||||
|
||||
mixin("private alias this_thing = " ~ name ~ ";");
|
||||
}
|
||||
|
||||
/+
|
||||
SPA mode: put favicon on top level window, no other user controls at top level, links to different domains always open in new window.
|
||||
+/
|
||||
|
||||
// FIXME: look in /opt/cef for the dll and the locales
|
||||
|
||||
import arsd.minigui;
|
||||
import arsd.webview;
|
||||
|
||||
version(wv2)
|
||||
alias WebViewWidget = WebViewWidget_WV2;
|
||||
else version(cef)
|
||||
alias WebViewWidget = WebViewWidget_CEF;
|
||||
else static assert(0, "no webview available");
|
||||
|
||||
class WebViewWidgetBase : NestedChildWindowWidget {
|
||||
protected SimpleWindow containerWindow;
|
||||
|
||||
protected this(Widget parent) {
|
||||
containerWindow = new SimpleWindow(640, 480, null, OpenGlOptions.no, Resizability.allowResizing, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent));
|
||||
|
||||
super(containerWindow, parent);
|
||||
}
|
||||
|
||||
mixin Observable!(string, "title");
|
||||
mixin Observable!(string, "url");
|
||||
mixin Observable!(string, "status");
|
||||
mixin Observable!(int, "loadingProgress");
|
||||
|
||||
abstract void refresh();
|
||||
abstract void back();
|
||||
abstract void forward();
|
||||
abstract void stop();
|
||||
|
||||
abstract void navigate(string url);
|
||||
|
||||
// the url and line are for error reporting purposes
|
||||
abstract void executeJavascript(string code, string url = null, int line = 0);
|
||||
|
||||
abstract void showDevTools();
|
||||
|
||||
// this is implemented as a do-nothing in the NestedChildWindowWidget base
|
||||
// but you will almost certainly need to override it in implementations.
|
||||
// abstract void registerMovementAdditionalWork();
|
||||
}
|
||||
|
||||
|
||||
version(wv2)
|
||||
class WebViewWidget_WV2 : WebViewWidgetBase {
|
||||
private RC!ICoreWebView2 webview_window;
|
||||
private RC!ICoreWebView2Environment webview_env;
|
||||
private RC!ICoreWebView2Controller controller;
|
||||
|
||||
private bool initialized;
|
||||
|
||||
this(Widget parent) {
|
||||
super(parent);
|
||||
// that ctor sets containerWindow
|
||||
|
||||
Wv2App.useEnvironment((env) {
|
||||
env.CreateCoreWebView2Controller(containerWindow.impl.hwnd,
|
||||
callback!(ICoreWebView2CreateCoreWebView2ControllerCompletedHandler)(delegate(error, controller_raw) {
|
||||
if(error || controller_raw is null)
|
||||
return error;
|
||||
|
||||
// need to keep this beyond the callback or we're doomed.
|
||||
controller = RC!ICoreWebView2Controller(controller_raw);
|
||||
|
||||
webview_window = controller.CoreWebView2;
|
||||
|
||||
RC!ICoreWebView2Settings Settings = webview_window.Settings;
|
||||
Settings.IsScriptEnabled = TRUE;
|
||||
Settings.AreDefaultScriptDialogsEnabled = TRUE;
|
||||
Settings.IsWebMessageEnabled = TRUE;
|
||||
|
||||
|
||||
auto ert = webview_window.add_NavigationStarting(
|
||||
delegate (sender, args) {
|
||||
wchar* t;
|
||||
args.get_Uri(&t);
|
||||
auto ot = t;
|
||||
|
||||
string s;
|
||||
|
||||
while(*t) {
|
||||
s ~= *t;
|
||||
t++;
|
||||
}
|
||||
|
||||
this.url = s;
|
||||
|
||||
CoTaskMemFree(ot);
|
||||
|
||||
return S_OK;
|
||||
});
|
||||
|
||||
RECT bounds;
|
||||
GetClientRect(containerWindow.impl.hwnd, &bounds);
|
||||
controller.Bounds = bounds;
|
||||
error = webview_window.Navigate("http://arsdnet.net/test.html"w.ptr);
|
||||
//error = webview_window.NavigateToString("<html><body>Hello</body></html>"w.ptr);
|
||||
//error = webview_window.Navigate("http://192.168.1.10/"w.ptr);
|
||||
|
||||
controller.IsVisible = true;
|
||||
|
||||
initialized = true;
|
||||
|
||||
return S_OK;
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
override void registerMovementAdditionalWork() {
|
||||
if(initialized) {
|
||||
RECT bounds;
|
||||
GetClientRect(containerWindow.impl.hwnd, &bounds);
|
||||
controller.Bounds = bounds;
|
||||
|
||||
controller.NotifyParentWindowPositionChanged();
|
||||
}
|
||||
}
|
||||
|
||||
override void refresh() {
|
||||
if(!initialized) return;
|
||||
webview_window.Reload();
|
||||
}
|
||||
override void back() {
|
||||
if(!initialized) return;
|
||||
webview_window.GoBack();
|
||||
}
|
||||
override void forward() {
|
||||
if(!initialized) return;
|
||||
webview_window.GoForward();
|
||||
}
|
||||
override void stop() {
|
||||
if(!initialized) return;
|
||||
webview_window.Stop();
|
||||
}
|
||||
|
||||
override void navigate(string url) {
|
||||
if(!initialized) return;
|
||||
import std.utf;
|
||||
auto error = webview_window.Navigate(url.toUTF16z);
|
||||
}
|
||||
|
||||
// the url and line are for error reporting purposes
|
||||
override void executeJavascript(string code, string url = null, int line = 0) {
|
||||
if(!initialized) return;
|
||||
import std.utf;
|
||||
webview_window.ExecuteScript(code.toUTF16z, null);
|
||||
}
|
||||
|
||||
override void showDevTools() {
|
||||
if(!initialized) return;
|
||||
webview_window.OpenDevToolsWindow();
|
||||
}
|
||||
}
|
||||
|
||||
version(cef)
|
||||
class WebViewWidget_CEF : WebViewWidgetBase {
|
||||
this(Widget parent) {
|
||||
//semaphore = new Semaphore;
|
||||
assert(CefApp.active);
|
||||
|
||||
super(parent);
|
||||
|
||||
flushGui();
|
||||
|
||||
mapping[containerWindow.nativeWindowHandle()] = this;
|
||||
|
||||
cef_window_info_t window_info;
|
||||
window_info.parent_window = containerWindow.nativeWindowHandle;
|
||||
|
||||
cef_string_t cef_url = cef_string_t("http://arsdnet.net/test.html");
|
||||
|
||||
cef_browser_settings_t browser_settings;
|
||||
browser_settings.size = cef_browser_settings_t.sizeof;
|
||||
|
||||
client = new MiniguiCefClient();
|
||||
|
||||
auto got = libcef.browser_host_create_browser(&window_info, client.passable, &cef_url, &browser_settings, null, null);
|
||||
|
||||
/+
|
||||
containerWindow.closeQuery = delegate() {
|
||||
browserHandle.get_host.close_browser(true);
|
||||
//containerWindow.close();
|
||||
};
|
||||
+/
|
||||
|
||||
}
|
||||
|
||||
private MiniguiCefClient client;
|
||||
|
||||
/+
|
||||
override void close() {
|
||||
// FIXME: this should prolly be on the onclose event instead
|
||||
mapping.remove[win.nativeWindowHandle()];
|
||||
super.close();
|
||||
}
|
||||
+/
|
||||
|
||||
override void registerMovementAdditionalWork() {
|
||||
if(browserWindow) {
|
||||
static if(UsingSimpledisplayX11)
|
||||
XResizeWindow(XDisplayConnection.get, browserWindow, width, height);
|
||||
// FIXME: do for Windows too
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private NativeWindowHandle browserWindow;
|
||||
private RC!cef_browser_t browserHandle;
|
||||
|
||||
private static WebViewWidget[NativeWindowHandle] mapping;
|
||||
private static WebViewWidget[NativeWindowHandle] browserMapping;
|
||||
|
||||
override void refresh() { if(browserHandle) browserHandle.reload(); }
|
||||
override void back() { if(browserHandle) browserHandle.go_back(); }
|
||||
override void forward() { if(browserHandle) browserHandle.go_forward(); }
|
||||
override void stop() { if(browserHandle) browserHandle.stop_load(); }
|
||||
|
||||
override void navigate(string url) {
|
||||
if(!browserHandle) return;
|
||||
auto s = cef_string_t(url);
|
||||
browserHandle.get_main_frame.load_url(&s);
|
||||
}
|
||||
|
||||
// the url and line are for error reporting purposes
|
||||
override void executeJavascript(string code, string url = null, int line = 0) {
|
||||
if(!browserHandle) return;
|
||||
|
||||
auto c = cef_string_t(code);
|
||||
auto u = cef_string_t(url);
|
||||
browserHandle.get_main_frame.execute_java_script(&c, &u, line);
|
||||
}
|
||||
|
||||
override void showDevTools() {
|
||||
if(!browserHandle) return;
|
||||
browserHandle.get_host.show_dev_tools(null /* window info */, client.passable, null /* settings */, null /* inspect element at coordinates */);
|
||||
}
|
||||
|
||||
// FYI the cef browser host also allows things like custom spelling dictionaries and getting navigation entries.
|
||||
|
||||
// JS on init?
|
||||
// JS bindings?
|
||||
// user styles?
|
||||
// navigate to string? (can just use a data uri maybe?)
|
||||
// custom scheme handlers?
|
||||
|
||||
// navigation callbacks to prohibit certain things or move links to new window etc?
|
||||
}
|
||||
|
||||
version(cef) {
|
||||
|
||||
//import core.sync.semaphore;
|
||||
//__gshared Semaphore semaphore;
|
||||
|
||||
/+
|
||||
Finds the WebViewWidget associated with the given browser, then runs the given code in the gui thread on it.
|
||||
+/
|
||||
void runOnWebView(RC!cef_browser_t browser, void delegate(WebViewWidget) dg) nothrow {
|
||||
auto wh = cast(NativeWindowHandle) browser.get_host.get_window_handle;
|
||||
runInGuiThreadAsync({
|
||||
if(auto wvp = wh in WebViewWidget.browserMapping) {
|
||||
dg(*wvp);
|
||||
} else {
|
||||
//writeln("not found ", wh, WebViewWidget.browserMapping);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
class MiniguiCefLifeSpanHandler : CEF!cef_life_span_handler_t {
|
||||
override int on_before_popup(RC!cef_browser_t, RC!cef_frame_t, const(cef_string_utf16_t)*, const(cef_string_utf16_t)*, cef_window_open_disposition_t, int, const(cef_popup_features_t)*, cef_window_info_t*, cef_client_t**, cef_browser_settings_t*, cef_dictionary_value_t**, int*) {
|
||||
return 0;
|
||||
}
|
||||
override void on_after_created(RC!cef_browser_t browser) {
|
||||
auto handle = cast(NativeWindowHandle) browser.get_host().get_window_handle();
|
||||
auto ptr = browser.passable; // this adds to the refcount until it gets inside
|
||||
|
||||
// the only reliable key (at least as far as i can tell) is the window handle
|
||||
// so gonna look that up and do the sync mapping that way.
|
||||
runInGuiThreadAsync({
|
||||
version(Windows) {
|
||||
auto parent = GetParent(handle);
|
||||
} else static if(UsingSimpledisplayX11) {
|
||||
import arsd.simpledisplay : Window;
|
||||
Window root;
|
||||
Window parent;
|
||||
uint c = 0;
|
||||
auto display = XDisplayConnection.get;
|
||||
Window* children;
|
||||
XQueryTree(display, handle, &root, &parent, &children, &c);
|
||||
XFree(children);
|
||||
} else static assert(0);
|
||||
|
||||
if(auto wvp = parent in WebViewWidget.mapping) {
|
||||
auto wv = *wvp;
|
||||
wv.browserWindow = handle;
|
||||
wv.browserHandle = RC!cef_browser_t(ptr);
|
||||
|
||||
wv.registerMovementAdditionalWork();
|
||||
|
||||
WebViewWidget.browserMapping[handle] = wv;
|
||||
} else assert(0);
|
||||
});
|
||||
}
|
||||
override int do_close(RC!cef_browser_t browser) {
|
||||
return 0;
|
||||
}
|
||||
override void on_before_close(RC!cef_browser_t browser) {
|
||||
/+
|
||||
import std.stdio; debug writeln("notify");
|
||||
try
|
||||
semaphore.notify;
|
||||
catch(Exception e) { assert(0); }
|
||||
+/
|
||||
}
|
||||
}
|
||||
|
||||
class MiniguiLoadHandler : CEF!cef_load_handler_t {
|
||||
override void on_loading_state_change(RC!(cef_browser_t) browser, int isLoading, int canGoBack, int canGoForward) {
|
||||
/+
|
||||
browser.runOnWebView((WebViewWidget wvw) {
|
||||
wvw.parentWindow.win.title = wvw.browserHandle.get_main_frame.get_url.toGCAndFree;
|
||||
});
|
||||
+/
|
||||
}
|
||||
override void on_load_start(RC!(cef_browser_t), RC!(cef_frame_t), cef_transition_type_t) {
|
||||
}
|
||||
override void on_load_error(RC!(cef_browser_t), RC!(cef_frame_t), cef_errorcode_t, const(cef_string_utf16_t)*, const(cef_string_utf16_t)*) {
|
||||
}
|
||||
override void on_load_end(RC!(cef_browser_t), RC!(cef_frame_t), int) {
|
||||
}
|
||||
}
|
||||
|
||||
class MiniguiDialogHandler : CEF!cef_dialog_handler_t {
|
||||
|
||||
override int on_file_dialog(RC!(cef_browser_t) browser, cef_file_dialog_mode_t mode, const(cef_string_utf16_t)* title, const(cef_string_utf16_t)* default_file_path, cef_string_list_t accept_filters, int selected_accept_filter, RC!(cef_file_dialog_callback_t) callback) {
|
||||
try {
|
||||
auto ptr = callback.passable();
|
||||
runInGuiThreadAsync({
|
||||
getOpenFileName((string name) {
|
||||
auto callback = RC!cef_file_dialog_callback_t(ptr);
|
||||
auto list = libcef.string_list_alloc();
|
||||
auto item = cef_string_t(name);
|
||||
libcef.string_list_append(list, &item);
|
||||
callback.cont(selected_accept_filter, list);
|
||||
}, null, null, () {
|
||||
auto callback = RC!cef_file_dialog_callback_t(ptr);
|
||||
callback.cancel();
|
||||
});
|
||||
});
|
||||
} catch(Exception e) {}
|
||||
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
class MiniguiDisplayHandler : CEF!cef_display_handler_t {
|
||||
override void on_address_change(RC!(cef_browser_t) browser, RC!(cef_frame_t), const(cef_string_utf16_t)* address) {
|
||||
auto url = address.toGC;
|
||||
browser.runOnWebView((wv) {
|
||||
wv.url = url;
|
||||
});
|
||||
}
|
||||
override void on_title_change(RC!(cef_browser_t) browser, const(cef_string_utf16_t)* title) {
|
||||
auto t = title.toGC;
|
||||
browser.runOnWebView((wv) {
|
||||
wv.title = t;
|
||||
});
|
||||
}
|
||||
override void on_favicon_urlchange(RC!(cef_browser_t) browser, cef_string_list_t) {
|
||||
}
|
||||
override void on_fullscreen_mode_change(RC!(cef_browser_t) browser, int) {
|
||||
}
|
||||
override int on_tooltip(RC!(cef_browser_t) browser, cef_string_utf16_t*) {
|
||||
return 0;
|
||||
}
|
||||
override void on_status_message(RC!(cef_browser_t) browser, const(cef_string_utf16_t)* msg) {
|
||||
auto status = msg.toGC;
|
||||
browser.runOnWebView((wv) {
|
||||
wv.status = status;
|
||||
});
|
||||
}
|
||||
override void on_loading_progress_change(RC!(cef_browser_t) browser, double progress) {
|
||||
// progress is from 0.0 to 1.0
|
||||
browser.runOnWebView((wv) {
|
||||
wv.loadingProgress = cast(int) (progress * 100);
|
||||
});
|
||||
}
|
||||
override int on_console_message(RC!(cef_browser_t), cef_log_severity_t, const(cef_string_utf16_t)*, const(cef_string_utf16_t)*, int) {
|
||||
return 0; // 1 means to suppress it being automatically output
|
||||
}
|
||||
override int on_auto_resize(RC!(cef_browser_t), const(cef_size_t)*) {
|
||||
return 0;
|
||||
}
|
||||
override int on_cursor_change(RC!(cef_browser_t), cef_cursor_handle_t, cef_cursor_type_t, const(cef_cursor_info_t)*) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
class MiniguiCefClient : CEF!cef_client_t {
|
||||
MiniguiCefLifeSpanHandler lsh;
|
||||
MiniguiLoadHandler loadHandler;
|
||||
MiniguiDialogHandler dialogHandler;
|
||||
MiniguiDisplayHandler displayHandler;
|
||||
this() {
|
||||
lsh = new MiniguiCefLifeSpanHandler();
|
||||
loadHandler = new MiniguiLoadHandler();
|
||||
dialogHandler = new MiniguiDialogHandler();
|
||||
displayHandler = new MiniguiDisplayHandler();
|
||||
}
|
||||
|
||||
override cef_audio_handler_t* get_audio_handler() {
|
||||
return null;
|
||||
}
|
||||
override cef_context_menu_handler_t* get_context_menu_handler() {
|
||||
return null;
|
||||
}
|
||||
override cef_dialog_handler_t* get_dialog_handler() {
|
||||
return dialogHandler.returnable;
|
||||
}
|
||||
override cef_display_handler_t* get_display_handler() {
|
||||
return displayHandler.returnable;
|
||||
}
|
||||
override cef_download_handler_t* get_download_handler() {
|
||||
return null;
|
||||
}
|
||||
override cef_drag_handler_t* get_drag_handler() {
|
||||
return null;
|
||||
}
|
||||
override cef_find_handler_t* get_find_handler() {
|
||||
return null;
|
||||
}
|
||||
override cef_focus_handler_t* get_focus_handler() {
|
||||
return null;
|
||||
}
|
||||
override cef_jsdialog_handler_t* get_jsdialog_handler() {
|
||||
// needed for alert etc.
|
||||
return null;
|
||||
}
|
||||
override cef_keyboard_handler_t* get_keyboard_handler() {
|
||||
// this can handle keyboard shortcuts etc
|
||||
return null;
|
||||
}
|
||||
override cef_life_span_handler_t* get_life_span_handler() {
|
||||
return lsh.returnable;
|
||||
}
|
||||
override cef_load_handler_t* get_load_handler() {
|
||||
return loadHandler.returnable;
|
||||
}
|
||||
override cef_render_handler_t* get_render_handler() {
|
||||
// this thing might work for an off-screen thing
|
||||
// like to an image or to a video stream maybe
|
||||
return null;
|
||||
}
|
||||
override cef_request_handler_t* get_request_handler() {
|
||||
return null;
|
||||
}
|
||||
override int on_process_message_received(RC!cef_browser_t, RC!cef_frame_t, cef_process_id_t, RC!cef_process_message_t) {
|
||||
return 0; // return 1 if you can actually handle the message
|
||||
}
|
||||
override cef_frame_handler_t* get_frame_handler() nothrow {
|
||||
return null;
|
||||
}
|
||||
override cef_print_handler_t* get_print_handler() nothrow {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue