From 7d81f250fe98906cbfc6896487afff6339c037f7 Mon Sep 17 00:00:00 2001 From: "Adam D. Ruppe" Date: Tue, 19 Apr 2022 12:07:08 -0400 Subject: [PATCH] more improvements to input handling and web --- minigui.d | 185 ++++++++++++++++++++++++++++++++++----- minigui_addons/webview.d | 167 +++++++++++++++++++++++++++++++---- simpledisplay.d | 50 ++++++++++- 3 files changed, 358 insertions(+), 44 deletions(-) diff --git a/minigui.d b/minigui.d index b6e5b68..80ae350 100644 --- a/minigui.d +++ b/minigui.d @@ -1925,6 +1925,18 @@ class Widget : ReflectableProperties { return StyleInformation(this); } + int focusableWidgets(scope int delegate(Widget) dg) { + foreach(widget; WidgetStream(this)) { + if(widget.tabStop && !widget.hidden) { + int result = dg(widget); + if (result) + return result; + } + } + return 0; + } + + // FIXME: I kinda want to hide events from implementation widgets // so it just catches them all and stops propagation... // i guess i can do it with a event listener on star. @@ -7084,7 +7096,7 @@ class HorizontalLayout : Layout { } -version(Windows) +version(win32_widgets) private extern(Windows) LRESULT DoubleBufferWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) nothrow { @@ -7944,6 +7956,12 @@ class Window : Widget { Window.newWindowCreated(this); } + version(custom_widgets) + override void defaultEventHandler_click(ClickEvent event) { + if(event.target && event.target.tabStop) + event.target.focus(); + } + private static void delegate(Window) newWindowCreated; version(win32_widgets) @@ -8082,13 +8100,13 @@ class Window : Widget { } win = new SimpleWindow(width, height, title, OpenGlOptions.no, Resizability.allowResizing, WindowTypes.normal, WindowFlags.dontAutoShow | WindowFlags.managesChildWindowFocus); - /+ + ///+ // for input proxy auto display = XDisplayConnection.get; auto inputProxy = XCreateSimpleWindow(display, win.window, -1, -1, 1, 1, 0, 0, 0); XSelectInput(display, inputProxy, EventMask.KeyPressMask | EventMask.KeyReleaseMask); XMapWindow(display, inputProxy); - import std.stdio; writefln("input proxy: 0x%0x", inputProxy); + //import std.stdio; writefln("input proxy: 0x%0x", inputProxy); this.inputProxy = new SimpleWindow(inputProxy); XEvent lastEvent; @@ -8108,8 +8126,11 @@ class Window : Widget { if(auto nw = cast(NestedChildWindowWidget) focusedWidget) { auto thing = nw.focusableWindow(); if(thing && thing.window) { - import std.stdio; writeln("sending event ", lastEvent.xkey); - XSendEvent(XDisplayConnection.get, thing.window, false, 0, &lastEvent); + lastEvent.xkey.window = thing.window; + // import std.stdio; writeln("sending event ", lastEvent.xkey); + trapXErrors( { + XSendEvent(XDisplayConnection.get, thing.window, false, 0, &lastEvent); + }); } } } @@ -8121,7 +8142,7 @@ class Window : Widget { }, ); // done - +/ + //+/ @@ -8133,13 +8154,7 @@ class Window : Widget { SimpleWindow inputProxy; private SimpleWindow setRequestedInputFocus() { - // return inputProxy; - - if(auto fw = cast(NestedChildWindowWidget) focusedWidget) { - // sdpyPrintDebugString("heaven"); - return fw.focusableWindow; - } - return win; + return inputProxy; } /// ditto @@ -8173,14 +8188,15 @@ class Window : Widget { event.ctrlKey = (ev.modifierState & ModifierState.ctrl) ? true : false; event.dispatch(); - return !event.defaultPrevented; + return !event.propagationStopped; } + // returns true if propagation should continue into nested things.... prolly not a great thing to do. bool dispatchCharEvent(dchar ch) { if(focusedWidget) { auto event = new CharEvent(focusedWidget, ch); event.dispatch(); - return !event.defaultPrevented; + return !event.propagationStopped; } return true; } @@ -8343,18 +8359,29 @@ class Window : Widget { } static Widget getFirstFocusable(Widget start) { - if(start.tabStop && !start.hidden) - return start; + if(start is null) + return null; - if(!start.hidden) - foreach(child; start.children) { - auto f = getFirstFocusable(child); - if(f !is null) - return f; + foreach(widget; &start.focusableWidgets) { + return widget; } + return null; } + static Widget getLastFocusable(Widget start) { + if(start is null) + return null; + + Widget last; + foreach(widget; &start.focusableWidgets) { + last = widget; + } + + return last; + } + + mixin Emits!ClosingEvent; mixin Emits!ClosedEvent; } @@ -10457,9 +10484,10 @@ class Menu : Window { if(!menuParent.parentWindow.win.closed) { if(auto maw = cast(MouseActivatedWidget) menuParent) { maw.setDynamicState(DynamicState.depressed, false); + maw.setDynamicState(DynamicState.hover, false); maw.redraw(); } - menuParent.parentWindow.win.focus(); + // menuParent.parentWindow.win.focus(); } clickListener.disconnect(); } @@ -14225,6 +14253,117 @@ interface ReflectableProperties { } } +private struct Stack(T) { + this(int maxSize) { + internalLength = 0; + arr = initialBuffer[]; + } + + ///. + void push(T t) { + if(internalLength >= arr.length) { + auto oldarr = arr; + if(arr.length < 4096) + arr = new T[arr.length * 2]; + else + arr = new T[arr.length + 4096]; + arr[0 .. oldarr.length] = oldarr[]; + } + + arr[internalLength] = t; + internalLength++; + } + + ///. + T pop() { + assert(internalLength); + internalLength--; + return arr[internalLength]; + } + + ///. + T peek() { + assert(internalLength); + return arr[internalLength - 1]; + } + + ///. + @property bool empty() { + return internalLength ? false : true; + } + + ///. + private T[] arr; + private size_t internalLength; + private T[64] initialBuffer; + // the static array is allocated with this object, so if we have a small stack (which we prolly do; dom trees usually aren't insanely deep), + // using this saves us a bunch of trips to the GC. In my last profiling, I got about a 50x improvement in the push() + // function thanks to this, and push() was actually one of the slowest individual functions in the code! +} + +/// This is the lazy range that walks the tree for you. It tries to go in the lexical order of the source: node, then children from first to last, each recursively. +private struct WidgetStream { + + ///. + @property Widget front() { + return current.widget; + } + + /// Use Widget.tree instead. + this(Widget start) { + current.widget = start; + current.childPosition = -1; + isEmpty = false; + stack = typeof(stack)(0); + } + + /* + Handle it + handle its children + + */ + + ///. + void popFront() { + more: + if(isEmpty) return; + + // FIXME: the profiler says this function is somewhat slow (noticeable because it can be called a lot of times) + + current.childPosition++; + if(current.childPosition >= current.widget.children.length) { + if(stack.empty()) + isEmpty = true; + else { + current = stack.pop(); + goto more; + } + } else { + stack.push(current); + current.widget = current.widget.children[current.childPosition]; + current.childPosition = -1; + } + } + + ///. + @property bool empty() { + return isEmpty; + } + + private: + + struct Current { + Widget widget; + int childPosition; + } + + Current current; + + Stack!(Current) stack; + + bool isEmpty; +} + /+ diff --git a/minigui_addons/webview.d b/minigui_addons/webview.d index 194743e..d1e74cf 100644 --- a/minigui_addons/webview.d +++ b/minigui_addons/webview.d @@ -360,7 +360,7 @@ class WebViewWidget_CEF : WebViewWidgetBase { //semaphore = new Semaphore; assert(CefApp.active); - this(new MiniguiCefClient(openNewWindow), parent); + this(new MiniguiCefClient(openNewWindow), parent, false); cef_window_info_t window_info; window_info.parent_window = containerWindow.nativeWindowHandle; @@ -398,7 +398,7 @@ class WebViewWidget_CEF : WebViewWidgetBase { .destroy(this); // but this is ok to do some memory management cleanup } - private this(MiniguiCefClient client, Widget parent) { + private this(MiniguiCefClient client, Widget parent, bool isDevTools) { super(parent); this.client = client; @@ -407,22 +407,56 @@ class WebViewWidget_CEF : WebViewWidgetBase { mapping[containerWindow.nativeWindowHandle()] = this; - - this.parentWindow.addEventListener((FocusEvent fe) { - if(!browserHandle) return; - //browserHandle.get_host.set_focus(true); - - executeJavascript("if(window.__arsdPreviouslyFocusedNode) window.__arsdPreviouslyFocusedNode.focus(); window.dispatchEvent(new FocusEvent(\"focus\"));"); + this.addEventListener(delegate(KeyDownEvent ke) { + if(ke.key == Key.Tab) + ke.preventDefault(); }); - this.parentWindow.addEventListener((BlurEvent be) { + + this.addEventListener((FocusEvent fe) { if(!browserHandle) return; - executeJavascript("if(document.activeElement) { window.__arsdPreviouslyFocusedNode = document.activeElement; document.activeElement.blur(); } window.dispatchEvent(new FocusEvent(\"blur\"));"); + XFocusChangeEvent ev; + ev.type = arsd.simpledisplay.EventType.FocusIn; + ev.display = XDisplayConnection.get; + ev.window = ozone; + ev.mode = NotifyModes.NotifyNormal; + ev.detail = NotifyDetail.NotifyVirtual; + + trapXErrors( { + XSendEvent(XDisplayConnection.get, ozone, false, 0, cast(XEvent*) &ev); + }); + + // this also works if the message is buggy and it avoids weirdness from raising window etc + //executeJavascript("if(window.__arsdPreviouslyFocusedNode) window.__arsdPreviouslyFocusedNode.focus(); window.dispatchEvent(new FocusEvent(\"focus\"));"); + }); + this.addEventListener((BlurEvent be) { + if(!browserHandle) return; + + XFocusChangeEvent ev; + ev.type = arsd.simpledisplay.EventType.FocusOut; + ev.display = XDisplayConnection.get; + ev.window = ozone; + ev.mode = NotifyModes.NotifyNormal; + ev.detail = NotifyDetail.NotifyNonlinearVirtual; + + trapXErrors( { + XSendEvent(XDisplayConnection.get, ozone, false, 0, cast(XEvent*) &ev); + }); + + //executeJavascript("if(document.activeElement) { window.__arsdPreviouslyFocusedNode = document.activeElement; document.activeElement.blur(); } window.dispatchEvent(new FocusEvent(\"blur\"));"); }); bool closeAttempted = false; + if(isDevTools) this.parentWindow.addEventListener((scope ClosingEvent ce) { + this.parentWindow.hide(); + ce.preventDefault(); + }); + else + this.parentWindow.addEventListener((scope ClosingEvent ce) { + if(devTools) + devTools.close(); if(!closeAttempted && browserHandle) { browserHandle.get_host.close_browser(true); ce.preventDefault(); @@ -450,6 +484,7 @@ class WebViewWidget_CEF : WebViewWidgetBase { } private NativeWindowHandle browserWindow; + private NativeWindowHandle ozone; private RC!cef_browser_t browserHandle; private static WebViewWidget[NativeWindowHandle] mapping; @@ -475,9 +510,37 @@ class WebViewWidget_CEF : WebViewWidgetBase { browserHandle.get_main_frame.execute_java_script(&c, &u, line); } + private Window devTools; override void showDevTools() { if(!browserHandle) return; - browserHandle.get_host.show_dev_tools(null /* window info */, client.passable, null /* settings */, null /* inspect element at coordinates */); + + if(devTools is null) { + auto host = browserHandle.get_host; + + if(host.has_dev_tools()) { + host.close_dev_tools(); + return; + } + + cef_window_info_t windowinfo; + version(linux) { + auto sw = new Window("DevTools"); + //sw.win.beingOpenKeepsAppOpen = false; + devTools = sw; + + auto wv = new WebViewWidget_CEF(client, sw, true); + + sw.show(); + + windowinfo.parent_window = wv.containerWindow.nativeWindowHandle; + } + host.show_dev_tools(&windowinfo, client.passable, null /* settings */, null /* inspect element at coordinates */); + } else { + if(devTools.hidden) + devTools.show(); + else + devTools.hide(); + } } // FYI the cef browser host also allows things like custom spelling dictionaries and getting navigation entries. @@ -534,7 +597,7 @@ version(cef) { cef_dictionary_value_t** extra_info, int* no_javascript_access ) { - + sdpyPrintDebugString("on_before_popup"); if(this.client.openNewWindow is null) return 1; // new windows disabled @@ -551,7 +614,7 @@ version(cef) { scope WebViewWidget delegate(Widget, BrowserSettings) accept = (parent, passed_settings) { ret = 0; if(parent !is null) { - auto widget = new WebViewWidget_CEF(this.client, parent); + auto widget = new WebViewWidget_CEF(this.client, parent, false); (*windowInfo).parent_window = widget.containerWindow.nativeWindowHandle; passed_settings.set(browser_settings); @@ -589,10 +652,13 @@ version(cef) { import arsd.simpledisplay : Window; Window root; Window parent; + Window ozone; uint c = 0; auto display = XDisplayConnection.get; Window* children; XQueryTree(display, handle, &root, &parent, &children, &c); + if(c == 1) + ozone = children[0]; XFree(children); } else static assert(0); @@ -600,8 +666,9 @@ version(cef) { auto wv = *wvp; wv.browserWindow = handle; wv.browserHandle = RC!cef_browser_t(ptr); + wv.ozone = ozone ? ozone : handle; - wv.browserWindowWrapped = new SimpleWindow(wv.browserWindow); + wv.browserWindowWrapped = new SimpleWindow(wv.ozone); /+ XSelectInput(XDisplayConnection.get, handle, EventMask.FocusChangeMask); @@ -831,17 +898,70 @@ version(cef) { } } + class MiniguiRequestHandler : CEF!cef_request_handler_t { + override int on_before_browse(RC!(cef_browser_t), RC!(cef_frame_t), RC!(cef_request_t), int, int) nothrow { + return 0; + } + override int on_open_urlfrom_tab(RC!(cef_browser_t), RC!(cef_frame_t), const(cef_string_utf16_t)*, cef_window_open_disposition_t, int) nothrow { + return 0; + } + override cef_resource_request_handler_t* get_resource_request_handler(RC!(cef_browser_t), RC!(cef_frame_t), RC!(cef_request_t), int, int, const(cef_string_utf16_t)*, int*) nothrow { + return null; + } + override int get_auth_credentials(RC!(cef_browser_t), const(cef_string_utf16_t)*, int, const(cef_string_utf16_t)*, int, const(cef_string_utf16_t)*, const(cef_string_utf16_t)*, RC!(cef_auth_callback_t)) nothrow { + // this is for http basic auth popup..... + return 0; + } + override int on_quota_request(RC!(cef_browser_t), const(cef_string_utf16_t)*, long, RC!(cef_callback_t)) nothrow { + return 0; + } + override int on_certificate_error(RC!(cef_browser_t), cef_errorcode_t, const(cef_string_utf16_t)*, RC!(cef_sslinfo_t), RC!(cef_callback_t)) nothrow { + return 0; + } + override int on_select_client_certificate(RC!(cef_browser_t), int, const(cef_string_utf16_t)*, int, ulong, cef_x509certificate_t**, RC!(cef_select_client_certificate_callback_t)) nothrow { + return 0; + } + override void on_plugin_crashed(RC!(cef_browser_t), const(cef_string_utf16_t)*) nothrow { + + } + override void on_render_view_ready(RC!(cef_browser_t) p) nothrow { + + } + override void on_render_process_terminated(RC!(cef_browser_t), cef_termination_status_t) nothrow { + + } + override void on_document_available_in_main_frame(RC!(cef_browser_t) browser) nothrow { + browser.runOnWebView(delegate(wv) { + wv.executeJavascript("console.log('here');"); + }); + + } + } + class MiniguiFocusHandler : CEF!cef_focus_handler_t { override void on_take_focus(RC!(cef_browser_t) browser, int next) nothrow { - // sdpyPrintDebugString("take"); + browser.runOnWebView(delegate(wv) { + Widget f; + if(next) { + f = Window.getFirstFocusable(wv.parentWindow); + } else { + foreach(w; &wv.parentWindow.focusableWidgets) { + if(w is wv) + break; + f = w; + } + } + if(f) + f.focus(); + }); } override int on_set_focus(RC!(cef_browser_t) browser, cef_focus_source_t source) nothrow { /+ browser.runOnWebView((ev) { - sdpyPrintDebugString("setting"); - ev.parentWindow.focusedWidget = ev; + ev.focus(); // even this can steal focus from other parts of my application! }); +/ + //sdpyPrintDebugString("setting"); return 1; // otherwise, cancel because this bullshit tends to steal focus from other applications and i never, ever, ever want that to happen. // seems to happen because of race condition in it getting a focus event and then stealing the focus from the parent @@ -850,7 +970,12 @@ version(cef) { // it also breaks its own pop up menus and drop down boxes to allow this! wtf } override void on_got_focus(RC!(cef_browser_t) browser) nothrow { - // sdpyPrintDebugString("got"); + browser.runOnWebView((ev) { + // this sometimes steals from the app too but it is relatively acceptable + // steals when i mouse in from the side of the window quickly, but still + // i want the minigui state to match so i'll allow it + ev.focus(); + }); } } @@ -865,6 +990,7 @@ version(cef) { MiniguiDownloadHandler downloadHandler; MiniguiKeyboardHandler keyboardHandler; MiniguiFocusHandler focusHandler; + MiniguiRequestHandler requestHandler; this(void delegate(scope OpenNewWindowParams) openNewWindow) { this.openNewWindow = openNewWindow; lsh = new MiniguiCefLifeSpanHandler(this); @@ -874,6 +1000,7 @@ version(cef) { downloadHandler = new MiniguiDownloadHandler(); keyboardHandler = new MiniguiKeyboardHandler(); focusHandler = new MiniguiFocusHandler(); + requestHandler = new MiniguiRequestHandler(); } override cef_audio_handler_t* get_audio_handler() { @@ -917,10 +1044,12 @@ version(cef) { 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 + // + // might be useful to have it render here then send it over too for remote X sharing a process return null; } override cef_request_handler_t* get_request_handler() { - return null; + return requestHandler.returnable; } 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 diff --git a/simpledisplay.d b/simpledisplay.d index 00087a9..699f93b 100644 --- a/simpledisplay.d +++ b/simpledisplay.d @@ -12296,6 +12296,13 @@ version(X11) { if(width == 0 || height == 0) { XSetClipMask(display, gc, None); + if(xrenderPicturePainter) { + + XRectangle[1] rects; + rects[0] = XRectangle(short.min, short.min, short.max, short.max); + XRenderSetPictureClipRectangles(display, xrenderPicturePainter, 0, 0, rects.ptr, cast(int) rects.length); + } + version(with_xft) { if(xftFont is null || xftDraw is null) return; @@ -12306,6 +12313,9 @@ version(X11) { rects[0] = XRectangle(cast(short)(x), cast(short)(y), cast(short) width, cast(short) height); XSetClipRectangles(XDisplayConnection.get, gc, 0, 0, rects.ptr, 1, 0); + if(xrenderPicturePainter) + XRenderSetPictureClipRectangles(display, xrenderPicturePainter, 0, 0, rects.ptr, cast(int) rects.length); + version(with_xft) { if(xftFont is null || xftDraw is null) return; @@ -12572,6 +12582,12 @@ version(X11) { XRenderPictureAttributes attrs; // FIXME: I can prolly reuse this as long as the pixmap itself is valid. xrenderPicturePainter = XRenderCreatePicture(display, d, Sprite.RGB24, 0, &attrs); + + // need to initialize the clip + XRectangle[1] rects; + rects[0] = XRectangle(cast(short)(_clipRectangle.left), cast(short)(_clipRectangle.top), cast(short) _clipRectangle.width, cast(short) _clipRectangle.height); + + XRenderSetPictureClipRectangles(display, xrenderPicturePainter, 0, 0, rects.ptr, cast(int) rects.length); } XRenderComposite( @@ -13613,6 +13629,35 @@ mixin DynamicLoad!(XRandr, "Xrandr", 2, XRandrLibrarySuccessfullyLoaded) XRandrL } } + /++ + Platform-specific for X11. Traps errors for the duration of `dg`. Avoid calling this from inside a call to this. + + Please note that it returns + +/ + XErrorEvent[] trapXErrors(scope void delegate() dg) { + + static XErrorEvent[] errorBuffer; + + static extern(C) int handler (Display* dpy, XErrorEvent* evt) nothrow { + errorBuffer ~= *evt; + return 0; + } + + auto savedErrorHandler = XSetErrorHandler(&handler); + + try { + dg(); + } finally { + XSync(XDisplayConnection.get, 0/*False*/); + XSetErrorHandler(savedErrorHandler); + } + + auto bfr = errorBuffer; + errorBuffer = null; + + return bfr; + } + /// Platform-specific for X11. A singleton class (well, all its methods are actually static... so more like a namespace) wrapping a `Display*`. class XDisplayConnection { private __gshared Display* display; @@ -16986,6 +17031,9 @@ extern(C) { extern(C) alias XIOErrorHandler = int function (Display* display); } +extern(C) nothrow +alias XErrorHandler = int function(Display*, XErrorEvent*); + extern(C) nothrow @nogc { struct Screen{ XExtData *ext_data; /* hook for extension to hang data */ @@ -17190,8 +17238,6 @@ struct Visual byte pad; } - alias XErrorHandler = int function(Display*, XErrorEvent*); - struct XRectangle { short x; short y;