1 /++ 2 A webview (based on [arsd.webview]) for minigui. 3 4 For now at least, to use this, you MUST have a [WebViewApp] in scope in main for the duration of your gui application. 5 6 Warning: CEF spams the current directory with a bunch of files and directories. You might want to run your program in a dedicated location. 7 8 History: 9 Added November 5, 2021. NOT YET STABLE. 10 11 Examples: 12 --- 13 /+ dub.sdl: 14 name "web" 15 dependency "arsd-official:minigui-webview" version="*" 16 +/ 17 18 import arsd.minigui; 19 import arsd.minigui_addons.webview; 20 21 void main() { 22 auto app = WebViewApp(null); 23 auto window = new Window; 24 auto webview = new WebViewWidget("http://dlang.org/", window); 25 window.loop; 26 } 27 --- 28 +/ 29 module arsd.minigui_addons.webview; 30 // FIXME: i think i can download the cef automatically if needed. 31 32 version(linux) 33 version=cef; 34 version(Windows) 35 version=wv2; 36 37 /+ 38 SPA mode: put favicon on top level window, no other user controls at top level, links to different domains always open in new window. 39 +/ 40 41 // FIXME: look in /opt/cef for the dll and the locales 42 43 import arsd.minigui; 44 import arsd.webview; 45 46 version(wv2) { 47 alias WebViewWidget = WebViewWidget_WV2; 48 alias WebViewApp = Wv2App; 49 } else version(cef) { 50 alias WebViewWidget = WebViewWidget_CEF; 51 alias WebViewApp = CefApp; 52 } else static assert(0, "no webview available"); 53 54 class WebViewWidgetBase : NestedChildWindowWidget { 55 protected SimpleWindow containerWindow; 56 57 protected this(Widget parent) { 58 containerWindow = new SimpleWindow(640, 480, null, OpenGlOptions.no, Resizability.allowResizing, WindowTypes.nestedChild, WindowFlags.normal, getParentWindow(parent)); 59 import std.stdio; writefln("container window %d created", containerWindow.window); 60 61 super(containerWindow, parent); 62 } 63 64 mixin Observable!(string, "title"); 65 mixin Observable!(string, "url"); 66 mixin Observable!(string, "status"); 67 68 // not implemented on WV2 69 mixin Observable!(int, "loadingProgress"); 70 71 // not implemented on WV2 72 mixin Observable!(string, "favicon_url"); 73 mixin Observable!(MemoryImage, "favicon"); // please note it can be changed to null! 74 75 abstract void refresh(); 76 abstract void back(); 77 abstract void forward(); 78 abstract void stop(); 79 80 abstract void navigate(string url); 81 82 // the url and line are for error reporting purposes. They might be ignored. 83 // FIXME: add a callback with the reply. this can send a message from the js thread in cef and just ExecuteScript inWV2 84 // FIXME: add AddScriptToExecuteOnDocumentCreated for cef.... 85 abstract void executeJavascript(string code, string url = null, int line = 0); 86 // for injecting stuff into the context 87 // abstract void executeJavascriptBeforeEachLoad(string code); 88 89 abstract void showDevTools(); 90 91 /++ 92 Your communication consists of running Javascript and sending string messages back and forth, 93 kinda similar to your communication with a web server. 94 +/ 95 // these form your communcation channel between the web view and the native world 96 // abstract void sendMessageToHost(string json); 97 // void delegate(string json) receiveMessageFromHost; 98 99 /+ 100 I also need a url filter 101 +/ 102 103 // this is implemented as a do-nothing in the NestedChildWindowWidget base 104 // but you will almost certainly need to override it in implementations. 105 // abstract void registerMovementAdditionalWork(); 106 } 107 108 // AddScriptToExecuteOnDocumentCreated 109 110 111 112 version(wv2) 113 class WebViewWidget_WV2 : WebViewWidgetBase { 114 private RC!ICoreWebView2 webview_window; 115 private RC!ICoreWebView2Environment webview_env; 116 private RC!ICoreWebView2Controller controller; 117 118 private bool initialized; 119 120 this(string url, void delegate(scope OpenNewWindowParams) openNewWindow, BrowserSettings settings, Widget parent) { 121 // FIXME: openNewWindow 122 super(parent); 123 // that ctor sets containerWindow 124 125 Wv2App.useEnvironment((env) { 126 env.CreateCoreWebView2Controller(containerWindow.impl.hwnd, 127 callback!(ICoreWebView2CreateCoreWebView2ControllerCompletedHandler)(delegate(error, controller_raw) { 128 if(error || controller_raw is null) 129 return error; 130 131 // need to keep this beyond the callback or we're doomed. 132 controller = RC!ICoreWebView2Controller(controller_raw); 133 134 webview_window = controller.CoreWebView2; 135 136 webview_window.add_DocumentTitleChanged((sender, args) { 137 this.title = toGC(&sender.get_DocumentTitle); 138 return S_OK; 139 }); 140 141 // add_HistoryChanged 142 // that's where CanGoBack and CanGoForward can be rechecked. 143 144 RC!ICoreWebView2Settings Settings = webview_window.Settings; 145 Settings.IsScriptEnabled = TRUE; 146 Settings.AreDefaultScriptDialogsEnabled = TRUE; 147 Settings.IsWebMessageEnabled = TRUE; 148 149 150 auto ert = webview_window.add_NavigationStarting( 151 delegate (sender, args) { 152 this.url = toGC(&args.get_Uri); 153 return S_OK; 154 }); 155 156 RECT bounds; 157 GetClientRect(containerWindow.impl.hwnd, &bounds); 158 controller.Bounds = bounds; 159 //error = webview_window.Navigate("http://arsdnet.net/test.html"w.ptr); 160 //error = webview_window.NavigateToString("<html><body>Hello</body></html>"w.ptr); 161 //error = webview_window.Navigate("http://192.168.1.10/"w.ptr); 162 163 WCharzBuffer bfr = WCharzBuffer(url); 164 webview_window.Navigate(bfr.ptr); 165 166 controller.IsVisible = true; 167 168 initialized = true; 169 170 return S_OK; 171 })); 172 }); 173 } 174 175 override void registerMovementAdditionalWork() { 176 if(initialized) { 177 RECT bounds; 178 GetClientRect(containerWindow.impl.hwnd, &bounds); 179 controller.Bounds = bounds; 180 181 controller.NotifyParentWindowPositionChanged(); 182 } 183 } 184 185 override void refresh() { 186 if(!initialized) return; 187 webview_window.Reload(); 188 } 189 override void back() { 190 if(!initialized) return; 191 webview_window.GoBack(); 192 } 193 override void forward() { 194 if(!initialized) return; 195 webview_window.GoForward(); 196 } 197 override void stop() { 198 if(!initialized) return; 199 webview_window.Stop(); 200 } 201 202 override void navigate(string url) { 203 if(!initialized) return; 204 import std.utf; 205 auto error = webview_window.Navigate(url.toUTF16z); 206 } 207 208 // the url and line are for error reporting purposes 209 override void executeJavascript(string code, string url = null, int line = 0) { 210 if(!initialized) return; 211 import std.utf; 212 webview_window.ExecuteScript(code.toUTF16z, null); 213 } 214 215 override void showDevTools() { 216 if(!initialized) return; 217 webview_window.OpenDevToolsWindow(); 218 } 219 } 220 221 /++ 222 The openInNewWindow delegate is given these params. 223 224 To accept the new window, call 225 226 params.accept(parent_widget); 227 228 Please note, you can force it to replace the current tab 229 by just navigating your current thing to the given url instead 230 of accepting it. 231 232 If you accept with a null widget, it will create a new window 233 but then return null, since the new window is managed by the 234 underlying webview instead of by minigui. 235 236 If you do not call accept, the pop up will be blocked. 237 238 accept returns an instance to the newly created widget, which will 239 be a parent to the widget you passed. 240 241 accept will be called from the gui thread and it MUST not call into 242 any other webview methods. It should only create windows/widgets 243 and set event handlers etc. 244 245 You MUST not escape references to anything in this structure. It 246 is entirely strictly temporary! 247 +/ 248 struct OpenNewWindowParams { 249 string url; 250 WebViewWidget delegate(Widget parent, BrowserSettings settings = BrowserSettings.init) accept; 251 } 252 253 /++ 254 Represents a browser setting that can be left default or specifically turned on or off. 255 +/ 256 struct SettingValue { 257 private byte value = -1; 258 259 /++ 260 Set it with `= true` or `= false`. 261 +/ 262 void opAssign(bool enable) { 263 value = enable ? 1 : 0; 264 } 265 266 /++ 267 And this resets it to the default value. 268 +/ 269 void setDefault() { 270 value = -1; 271 } 272 273 /// If isDefault, it will use the default setting from the browser. Else, the getValue return value will be used. getValue is invalid if !isDefault. 274 bool isDefault() { 275 return value == -1; 276 } 277 278 /// ditto 279 bool getValue() { 280 return value == 1; 281 } 282 } 283 284 /++ 285 Defines settings for a browser widget. Not all backends will respect all settings. 286 287 The order of members of this struct may change at any time. Refer to its members by 288 name. 289 +/ 290 struct BrowserSettings { 291 /// This is just disabling the automatic positional constructor, since that is not stable here. 292 this(typeof(null)) {} 293 294 string standardFontFamily; 295 string fixedFontFamily; 296 string serifFontFamily; 297 string sansSerifFontFamily; 298 string cursiveFontFamily; 299 string fantasyFontFamily; 300 301 int defaultFontSize; 302 int defaultFixedFontSize; 303 int minimumFontSize; 304 //int minimumLogicalFontSize; 305 306 SettingValue remoteFontsEnabled; 307 SettingValue javascriptEnabled; 308 SettingValue imagesEnabled; 309 SettingValue clipboardAccessEnabled; 310 SettingValue localStorageEnabled; 311 312 version(cef) 313 private void set(cef_browser_settings_t* browser_settings) { 314 alias settings = this; 315 if(settings.standardFontFamily) 316 browser_settings.standard_font_family = cef_string_t(settings.standardFontFamily); 317 if(settings.fixedFontFamily) 318 browser_settings.fixed_font_family = cef_string_t(settings.fixedFontFamily); 319 if(settings.serifFontFamily) 320 browser_settings.serif_font_family = cef_string_t(settings.serifFontFamily); 321 if(settings.sansSerifFontFamily) 322 browser_settings.sans_serif_font_family = cef_string_t(settings.sansSerifFontFamily); 323 if(settings.cursiveFontFamily) 324 browser_settings.cursive_font_family = cef_string_t(settings.cursiveFontFamily); 325 if(settings.fantasyFontFamily) 326 browser_settings.fantasy_font_family = cef_string_t(settings.fantasyFontFamily); 327 if(settings.defaultFontSize) 328 browser_settings.default_font_size = settings.defaultFontSize; 329 if(settings.defaultFixedFontSize) 330 browser_settings.default_fixed_font_size = settings.defaultFixedFontSize; 331 if(settings.minimumFontSize) 332 browser_settings.minimum_font_size = settings.minimumFontSize; 333 334 if(!settings.remoteFontsEnabled.isDefault()) 335 browser_settings.remote_fonts = settings.remoteFontsEnabled.getValue() ? cef_state_t.STATE_ENABLED : cef_state_t.STATE_DISABLED; 336 if(!settings.javascriptEnabled.isDefault()) 337 browser_settings.javascript = settings.javascriptEnabled.getValue() ? cef_state_t.STATE_ENABLED : cef_state_t.STATE_DISABLED; 338 if(!settings.imagesEnabled.isDefault()) 339 browser_settings.image_loading = settings.imagesEnabled.getValue() ? cef_state_t.STATE_ENABLED : cef_state_t.STATE_DISABLED; 340 if(!settings.clipboardAccessEnabled.isDefault()) 341 browser_settings.javascript_access_clipboard = settings.clipboardAccessEnabled.getValue() ? cef_state_t.STATE_ENABLED : cef_state_t.STATE_DISABLED; 342 if(!settings.localStorageEnabled.isDefault()) 343 browser_settings.local_storage = settings.localStorageEnabled.getValue() ? cef_state_t.STATE_ENABLED : cef_state_t.STATE_DISABLED; 344 345 } 346 } 347 348 version(cef) 349 class WebViewWidget_CEF : WebViewWidgetBase { 350 /++ 351 Create a webview that does not support opening links in new windows and uses default settings to load the given url. 352 +/ 353 this(string url, Widget parent) { 354 this(url, null, BrowserSettings.init, parent); 355 } 356 357 /++ 358 Full-featured constructor. 359 +/ 360 this(string url, void delegate(scope OpenNewWindowParams) openNewWindow, BrowserSettings settings, Widget parent) { 361 //semaphore = new Semaphore; 362 assert(CefApp.active); 363 364 this(new MiniguiCefClient(openNewWindow), parent, false); 365 366 cef_window_info_t window_info; 367 window_info.parent_window = containerWindow.nativeWindowHandle; 368 369 cef_string_t cef_url = cef_string_t(url);//"http://arsdnet.net/test.html"); 370 371 cef_browser_settings_t browser_settings; 372 browser_settings.size = cef_browser_settings_t.sizeof; 373 374 settings.set(&browser_settings); 375 376 auto got = libcef.browser_host_create_browser(&window_info, client.passable, &cef_url, &browser_settings, null, null); 377 } 378 379 /+ 380 ~this() { 381 import core.stdc.stdio; 382 import core.memory; 383 printf("CLEANUP %s\n", GC.inFinalizer ? "GC".ptr : "destroy".ptr); 384 } 385 +/ 386 387 override void dispose() { 388 // sdpyPrintDebugString("closed"); 389 // the window is already gone so too late to do this really.... 390 // if(browserHandle) browserHandle.get_host.close_browser(true); 391 392 // sdpyPrintDebugString("DISPOSE"); 393 394 if(win && win.nativeWindowHandle()) 395 mapping.remove(win.nativeWindowHandle()); 396 if(browserWindow) 397 browserMapping.remove(browserWindow); 398 399 .destroy(this); // but this is ok to do some memory management cleanup 400 } 401 402 private this(MiniguiCefClient client, Widget parent, bool isDevTools) { 403 super(parent); 404 405 this.client = client; 406 407 flushGui(); 408 409 mapping[containerWindow.nativeWindowHandle()] = this; 410 411 this.addEventListener(delegate(KeyDownEvent ke) { 412 if(ke.key == Key.Tab) 413 ke.preventDefault(); 414 }); 415 416 this.addEventListener((FocusEvent fe) { 417 if(!browserHandle) return; 418 419 XFocusChangeEvent ev; 420 ev.type = arsd.simpledisplay.EventType.FocusIn; 421 ev.display = XDisplayConnection.get; 422 ev.window = ozone; 423 ev.mode = NotifyModes.NotifyNormal; 424 ev.detail = NotifyDetail.NotifyVirtual; 425 426 trapXErrors( { 427 XSendEvent(XDisplayConnection.get, ozone, false, 0, cast(XEvent*) &ev); 428 }); 429 430 // this also works if the message is buggy and it avoids weirdness from raising window etc 431 //executeJavascript("if(window.__arsdPreviouslyFocusedNode) window.__arsdPreviouslyFocusedNode.focus(); window.dispatchEvent(new FocusEvent(\"focus\"));"); 432 }); 433 this.addEventListener((BlurEvent be) { 434 if(!browserHandle) return; 435 436 XFocusChangeEvent ev; 437 ev.type = arsd.simpledisplay.EventType.FocusOut; 438 ev.display = XDisplayConnection.get; 439 ev.window = ozone; 440 ev.mode = NotifyModes.NotifyNormal; 441 ev.detail = NotifyDetail.NotifyNonlinearVirtual; 442 443 trapXErrors( { 444 XSendEvent(XDisplayConnection.get, ozone, false, 0, cast(XEvent*) &ev); 445 }); 446 447 //executeJavascript("if(document.activeElement) { window.__arsdPreviouslyFocusedNode = document.activeElement; document.activeElement.blur(); } window.dispatchEvent(new FocusEvent(\"blur\"));"); 448 }); 449 450 bool closeAttempted = false; 451 452 if(isDevTools) 453 this.parentWindow.addEventListener((scope ClosingEvent ce) { 454 this.parentWindow.hide(); 455 ce.preventDefault(); 456 }); 457 else 458 this.parentWindow.addEventListener((scope ClosingEvent ce) { 459 if(devTools) 460 devTools.close(); 461 if(browserHandle) { 462 if(!closeAttempted) { 463 closeAttempted = true; 464 browserHandle.get_host.close_browser(false); 465 ce.preventDefault(); 466 sdpyPrintDebugString("closing 1"); 467 } else { 468 browserHandle.get_host.close_browser(true); 469 sdpyPrintDebugString("closing 2"); 470 } 471 } 472 }); 473 } 474 475 private MiniguiCefClient client; 476 477 override void registerMovementAdditionalWork() { 478 if(browserWindow) { 479 // import std.stdio; writeln("new size ", width, "x", height); 480 static if(UsingSimpledisplayX11) { 481 XResizeWindow(XDisplayConnection.get, browserWindow, width, height); 482 if(ozone) XResizeWindow(XDisplayConnection.get, ozone, width, height); 483 } 484 // FIXME: do for Windows too 485 } 486 } 487 488 SimpleWindow browserHostWrapped; 489 SimpleWindow browserWindowWrapped; 490 override SimpleWindow focusableWindow() { 491 if(browserWindowWrapped is null && browserWindow) { 492 browserWindowWrapped = new SimpleWindow(browserWindow); 493 // FIXME: this should never actually happen should it 494 } 495 return browserWindowWrapped; 496 } 497 498 private NativeWindowHandle browserWindow; 499 private NativeWindowHandle ozone; 500 private RC!cef_browser_t browserHandle; 501 502 private static WebViewWidget[NativeWindowHandle] mapping; 503 private static WebViewWidget[NativeWindowHandle] browserMapping; 504 505 private { 506 string findingText; 507 bool findingCase; 508 } 509 510 // might not be stable, webview does this fully integrated 511 void findText(string text, bool forward = true, bool matchCase = false, bool findNext = false) { 512 if(browserHandle) { 513 auto host = browserHandle.get_host(); 514 515 auto txt = cef_string_t(text); 516 host.find(&txt, forward, matchCase, findNext); 517 518 findingText = text; 519 findingCase = matchCase; 520 } 521 } 522 523 // ditto 524 void findPrevious() { 525 if(!browserHandle) 526 return; 527 auto host = browserHandle.get_host(); 528 auto txt = cef_string_t(findingText); 529 host.find(&txt, 0, findingCase, 1); 530 } 531 532 // ditto 533 void findNext() { 534 if(!browserHandle) 535 return; 536 auto host = browserHandle.get_host(); 537 auto txt = cef_string_t(findingText); 538 host.find(&txt, 1, findingCase, 1); 539 } 540 541 // ditto 542 void stopFind() { 543 if(!browserHandle) 544 return; 545 auto host = browserHandle.get_host(); 546 host.stop_finding(1); 547 } 548 549 override void refresh() { if(browserHandle) browserHandle.reload(); } 550 override void back() { if(browserHandle) browserHandle.go_back(); } 551 override void forward() { if(browserHandle) browserHandle.go_forward(); } 552 override void stop() { if(browserHandle) browserHandle.stop_load(); } 553 554 override void navigate(string url) { 555 if(!browserHandle) return; 556 auto s = cef_string_t(url); 557 browserHandle.get_main_frame.load_url(&s); 558 } 559 560 // the url and line are for error reporting purposes 561 override void executeJavascript(string code, string url = null, int line = 0) { 562 if(!browserHandle) return; 563 564 auto c = cef_string_t(code); 565 auto u = cef_string_t(url); 566 browserHandle.get_main_frame.execute_java_script(&c, &u, line); 567 } 568 569 private Window devTools; 570 override void showDevTools() { 571 if(!browserHandle) return; 572 573 if(devTools is null) { 574 auto host = browserHandle.get_host; 575 576 if(host.has_dev_tools()) { 577 host.close_dev_tools(); 578 return; 579 } 580 581 cef_window_info_t windowinfo; 582 version(linux) { 583 auto sw = new Window("DevTools"); 584 //sw.win.beingOpenKeepsAppOpen = false; 585 devTools = sw; 586 587 auto wv = new WebViewWidget_CEF(client, sw, true); 588 589 sw.show(); 590 591 windowinfo.parent_window = wv.containerWindow.nativeWindowHandle; 592 } 593 host.show_dev_tools(&windowinfo, client.passable, null /* settings */, null /* inspect element at coordinates */); 594 } else { 595 if(devTools.hidden) 596 devTools.show(); 597 else 598 devTools.hide(); 599 } 600 } 601 602 // FYI the cef browser host also allows things like custom spelling dictionaries and getting navigation entries. 603 604 // JS on init? 605 // JS bindings? 606 // user styles? 607 // navigate to string? (can just use a data uri maybe?) 608 // custom scheme handlers? 609 610 // navigation callbacks to prohibit certain things or move links to new window etc? 611 } 612 613 version(cef) { 614 615 //import core.sync.semaphore; 616 //__gshared Semaphore semaphore; 617 618 /+ 619 Finds the WebViewWidget associated with the given browser, then runs the given code in the gui thread on it. 620 +/ 621 void runOnWebView(RC!cef_browser_t browser, void delegate(WebViewWidget) dg) nothrow { 622 auto wh = cast(NativeWindowHandle) browser.get_host.get_window_handle; 623 624 import core.thread; 625 try { thread_attachThis(); } catch(Exception e) {} 626 627 runInGuiThreadAsync({ 628 if(auto wvp = wh in WebViewWidget.browserMapping) { 629 dg(*wvp); 630 } else { 631 //writeln("not found ", wh, WebViewWidget.browserMapping); 632 } 633 }); 634 } 635 636 class MiniguiCefLifeSpanHandler : CEF!cef_life_span_handler_t { 637 private MiniguiCefClient client; 638 this(MiniguiCefClient client) { 639 this.client = client; 640 } 641 642 override int on_before_popup( 643 RC!cef_browser_t browser, 644 RC!cef_frame_t frame, 645 const(cef_string_t)* target_url, 646 const(cef_string_t)* target_frame_name, 647 cef_window_open_disposition_t target_disposition, 648 int user_gesture, 649 const(cef_popup_features_t)* popupFeatures, 650 cef_window_info_t* windowInfo, 651 cef_client_t** client, 652 cef_browser_settings_t* browser_settings, 653 cef_dictionary_value_t** extra_info, 654 int* no_javascript_access 655 ) { 656 sdpyPrintDebugString("on_before_popup"); 657 if(this.client.openNewWindow is null) 658 return 1; // new windows disabled 659 660 try { 661 int ret; 662 663 import core.thread; 664 try { thread_attachThis(); } catch(Exception e) {} 665 666 // FIXME: change settings here 667 668 runInGuiThread({ 669 ret = 1; 670 scope WebViewWidget delegate(Widget, BrowserSettings) accept = (parent, passed_settings) { 671 ret = 0; 672 if(parent !is null) { 673 auto widget = new WebViewWidget_CEF(this.client, parent, false); 674 (*windowInfo).parent_window = widget.containerWindow.nativeWindowHandle; 675 676 passed_settings.set(browser_settings); 677 678 return widget; 679 } 680 return null; 681 }; 682 this.client.openNewWindow(OpenNewWindowParams(target_url.toGC, accept)); 683 return; 684 }); 685 686 return ret; 687 } catch(Exception e) { 688 return 1; 689 } 690 /+ 691 if(!user_gesture) 692 return 1; // if not created by the user, cancel it; basic popup blocking 693 +/ 694 } 695 override void on_after_created(RC!cef_browser_t browser) { 696 auto handle = cast(NativeWindowHandle) browser.get_host().get_window_handle(); 697 auto ptr = browser.passable; // this adds to the refcount until it gets inside 698 699 import core.thread; 700 try { thread_attachThis(); } catch(Exception e) {} 701 702 // the only reliable key (at least as far as i can tell) is the window handle 703 // so gonna look that up and do the sync mapping that way. 704 runInGuiThreadAsync({ 705 version(Windows) { 706 auto parent = GetParent(handle); 707 } else static if(UsingSimpledisplayX11) { 708 import arsd.simpledisplay : Window; 709 Window root; 710 Window parent; 711 Window ozone; 712 uint c = 0; 713 auto display = XDisplayConnection.get; 714 Window* children; 715 XQueryTree(display, handle, &root, &parent, &children, &c); 716 if(c == 1) 717 ozone = children[0]; 718 XFree(children); 719 } else static assert(0); 720 721 if(auto wvp = parent in WebViewWidget.mapping) { 722 auto wv = *wvp; 723 wv.browserWindow = handle; 724 wv.browserHandle = RC!cef_browser_t(ptr); 725 wv.ozone = ozone ? ozone : handle; 726 727 wv.browserHostWrapped = new SimpleWindow(handle); 728 // XSelectInput(XDisplayConnection.get, handle, EventMask.StructureNotifyMask); 729 730 wv.browserHostWrapped.onDestroyed = delegate{ 731 import std.stdio; writefln("browser host %d destroyed (handle %d)", wv.browserWindowWrapped.window, wv.browserWindow); 732 733 auto bce = new BrowserClosedEvent(wv); 734 bce.dispatch(); 735 }; 736 737 // need this to forward key events to 738 wv.browserWindowWrapped = new SimpleWindow(wv.ozone); 739 740 /+ 741 XSelectInput(XDisplayConnection.get, wv.ozone, EventMask.StructureNotifyMask); 742 wv.browserWindowWrapped.onDestroyed = delegate{ 743 import std.stdio; writefln("browser core %d destroyed (handle %d)", wv.browserWindowWrapped.window, wv.browserWindow); 744 745 //auto bce = new BrowserClosedEvent(wv); 746 //bce.dispatch(); 747 }; 748 +/ 749 750 /+ 751 XSelectInput(XDisplayConnection.get, ozone, EventMask.FocusChangeMask); 752 wv.browserWindowWrapped.onFocusChange = (bool got) { 753 import std.format; 754 sdpyPrintDebugString(format("focus change %s %x", got, wv.browserWindowWrapped.impl.window)); 755 }; 756 +/ 757 758 wv.registerMovementAdditionalWork(); 759 760 WebViewWidget.browserMapping[handle] = wv; 761 } else assert(0); 762 }); 763 } 764 override int do_close(RC!cef_browser_t browser) { 765 import std.stdio; 766 debug writeln("do_close"); 767 /+ 768 browser.runOnWebView((wv) { 769 wv.browserWindowWrapped.close(); 770 .destroy(wv.browserHandle); 771 }); 772 773 return 1; 774 +/ 775 776 return 0; 777 } 778 override void on_before_close(RC!cef_browser_t browser) { 779 import std.stdio; debug writeln("notify"); 780 browser.runOnWebView((wv) { 781 .destroy(wv.browserHandle); 782 }); 783 /+ 784 try 785 semaphore.notify; 786 catch(Exception e) { assert(0); } 787 +/ 788 } 789 } 790 791 class MiniguiLoadHandler : CEF!cef_load_handler_t { 792 override void on_loading_state_change(RC!(cef_browser_t) browser, int isLoading, int canGoBack, int canGoForward) { 793 /+ 794 browser.runOnWebView((WebViewWidget wvw) { 795 wvw.parentWindow.win.title = wvw.browserHandle.get_main_frame.get_url.toGCAndFree; 796 }); 797 +/ 798 } 799 override void on_load_start(RC!(cef_browser_t), RC!(cef_frame_t), cef_transition_type_t) { 800 } 801 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)*) { 802 } 803 override void on_load_end(RC!(cef_browser_t), RC!(cef_frame_t), int) { 804 } 805 } 806 807 class MiniguiDialogHandler : CEF!cef_dialog_handler_t { 808 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, RC!(cef_file_dialog_callback_t) callback) { 809 try { 810 auto ptr = callback.passable(); 811 browser.runOnWebView((wv) { 812 getOpenFileName((string name) { 813 auto callback = RC!cef_file_dialog_callback_t(ptr); 814 auto list = libcef.string_list_alloc(); 815 auto item = cef_string_t(name); 816 libcef.string_list_append(list, &item); 817 callback.cont(list); 818 }, null, null, () { 819 auto callback = RC!cef_file_dialog_callback_t(ptr); 820 callback.cancel(); 821 }, "/home/me/"); 822 }); 823 } catch(Exception e) {} 824 825 return 1; 826 } 827 } 828 829 class MiniguiDownloadHandler : CEF!cef_download_handler_t { 830 override void on_before_download( 831 RC!cef_browser_t browser, 832 RC!cef_download_item_t download_item, 833 const(cef_string_t)* suggested_name, 834 RC!cef_before_download_callback_t callback 835 ) nothrow 836 { 837 // FIXME: different filename and check if exists for overwrite etc 838 auto fn = cef_string_t(cast(wstring)("/home/me/Downloads/"w ~ suggested_name.str[0..suggested_name.length])); 839 sdpyPrintDebugString(fn.toGC); 840 callback.cont(&fn, false); 841 } 842 843 override void on_download_updated( 844 RC!cef_browser_t browser, 845 RC!cef_download_item_t download_item, 846 RC!cef_download_item_callback_t cancel 847 ) nothrow 848 { 849 sdpyPrintDebugString(download_item.get_percent_complete()); 850 // FIXME 851 } 852 853 override int can_download(RC!(cef_browser_t), const(cef_string_utf16_t)*, const(cef_string_utf16_t)*) { 854 return 1; 855 } 856 } 857 858 class MiniguiKeyboardHandler : CEF!cef_keyboard_handler_t { 859 override int on_pre_key_event( 860 RC!(cef_browser_t) browser, 861 const(cef_key_event_t)* event, 862 XEvent* osEvent, 863 int* isShortcut 864 ) nothrow { 865 /+ 866 sdpyPrintDebugString("---pre---"); 867 sdpyPrintDebugString(event.focus_on_editable_field); 868 sdpyPrintDebugString(event.windows_key_code); 869 sdpyPrintDebugString(event.modifiers); 870 sdpyPrintDebugString(event.unmodified_character); 871 +/ 872 //*isShortcut = 1; 873 return 0; // 1 if handled, which cancels sending it to browser 874 } 875 876 override int on_key_event( 877 RC!(cef_browser_t) browser, 878 const(cef_key_event_t)* event, 879 XEvent* osEvent 880 ) nothrow { 881 /+ 882 sdpyPrintDebugString("---key---"); 883 sdpyPrintDebugString(event.focus_on_editable_field); 884 sdpyPrintDebugString(event.windows_key_code); 885 sdpyPrintDebugString(event.modifiers); 886 +/ 887 return 0; // 1 if handled 888 } 889 } 890 891 class MiniguiDisplayHandler : CEF!cef_display_handler_t { 892 override void on_address_change(RC!(cef_browser_t) browser, RC!(cef_frame_t), const(cef_string_utf16_t)* address) { 893 auto url = address.toGC; 894 browser.runOnWebView((wv) { 895 wv.url = url; 896 }); 897 } 898 override void on_title_change(RC!(cef_browser_t) browser, const(cef_string_utf16_t)* title) { 899 auto t = title.toGC; 900 browser.runOnWebView((wv) { 901 wv.title = t; 902 }); 903 } 904 override void on_favicon_urlchange(RC!(cef_browser_t) browser, cef_string_list_t urls) { 905 string url; 906 auto size = libcef.string_list_size(urls); 907 if(size > 0) { 908 cef_string_t str; 909 libcef.string_list_value(urls, 0, &str); 910 url = str.toGC; 911 912 static class Thing : CEF!cef_download_image_callback_t { 913 RC!cef_browser_t browserHandle; 914 this(RC!cef_browser_t browser) nothrow { 915 this.browserHandle = browser; 916 } 917 override void on_download_image_finished(const(cef_string_t)* image_url, int http_status_code, RC!cef_image_t image) nothrow { 918 int width; 919 int height; 920 if(image.getRawPointer is null) { 921 browserHandle.runOnWebView((wv) { 922 wv.favicon = null; 923 }); 924 return; 925 } 926 927 auto data = image.get_as_bitmap(1.0, cef_color_type_t.CEF_COLOR_TYPE_RGBA_8888, cef_alpha_type_t.CEF_ALPHA_TYPE_POSTMULTIPLIED, &width, &height); 928 929 if(data.getRawPointer is null || width == 0 || height == 0) { 930 browserHandle.runOnWebView((wv) { 931 wv.favicon = null; 932 }); 933 } else { 934 auto s = data.get_size(); 935 auto buffer = new ubyte[](s); 936 auto got = data.get_data(buffer.ptr, buffer.length, 0); 937 auto slice = buffer[0 .. got]; 938 939 auto img = new TrueColorImage (width, height, slice); 940 941 browserHandle.runOnWebView((wv) { 942 wv.favicon = img; 943 }); 944 } 945 } 946 } 947 948 if(url.length) { 949 auto callback = new Thing(browser); 950 951 browser.get_host().download_image(&str, true, 16, 0, callback.passable); 952 } else { 953 browser.runOnWebView((wv) { 954 wv.favicon = null; 955 }); 956 } 957 } 958 959 browser.runOnWebView((wv) { 960 wv.favicon_url = url; 961 }); 962 } 963 override void on_fullscreen_mode_change(RC!(cef_browser_t) browser, int) { 964 } 965 override int on_tooltip(RC!(cef_browser_t) browser, cef_string_utf16_t*) { 966 return 0; 967 } 968 override void on_status_message(RC!(cef_browser_t) browser, const(cef_string_utf16_t)* msg) { 969 auto status = msg.toGC; 970 browser.runOnWebView((wv) { 971 wv.status = status; 972 }); 973 } 974 override void on_loading_progress_change(RC!(cef_browser_t) browser, double progress) { 975 // progress is from 0.0 to 1.0 976 browser.runOnWebView((wv) { 977 wv.loadingProgress = cast(int) (progress * 100); 978 }); 979 } 980 override int on_console_message(RC!(cef_browser_t), cef_log_severity_t, const(cef_string_utf16_t)*, const(cef_string_utf16_t)*, int) { 981 return 1; // 1 means to suppress it being automatically output 982 } 983 override int on_auto_resize(RC!(cef_browser_t), const(cef_size_t)*) { 984 return 0; 985 } 986 override int on_cursor_change(RC!(cef_browser_t), cef_cursor_handle_t, cef_cursor_type_t, const(cef_cursor_info_t)*) { 987 return 0; 988 } 989 override void on_media_access_change(RC!(cef_browser_t), int, int) { 990 991 } 992 } 993 994 class MiniguiRequestHandler : CEF!cef_request_handler_t { 995 override int on_before_browse(RC!(cef_browser_t), RC!(cef_frame_t), RC!(cef_request_t), int, int) nothrow { 996 return 0; 997 } 998 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 { 999 return 0; 1000 } 1001 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 { 1002 return null; 1003 } 1004 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 { 1005 // this is for http basic auth popup..... 1006 return 0; 1007 } 1008 override int on_quota_request(RC!(cef_browser_t), const(cef_string_utf16_t)*, long, RC!(cef_callback_t)) nothrow { 1009 return 0; 1010 } 1011 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 { 1012 return 0; 1013 } 1014 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 { 1015 return 0; 1016 } 1017 override void on_render_view_ready(RC!(cef_browser_t) p) nothrow { 1018 1019 } 1020 override void on_render_process_terminated(RC!(cef_browser_t), cef_termination_status_t) nothrow { 1021 1022 } 1023 override void on_document_available_in_main_frame(RC!(cef_browser_t) browser) nothrow { 1024 browser.runOnWebView(delegate(wv) { 1025 wv.executeJavascript("console.log('here');"); 1026 }); 1027 1028 } 1029 } 1030 1031 class MiniguiFocusHandler : CEF!cef_focus_handler_t { 1032 override void on_take_focus(RC!(cef_browser_t) browser, int next) nothrow { 1033 browser.runOnWebView(delegate(wv) { 1034 Widget f; 1035 if(next) { 1036 f = Window.getFirstFocusable(wv.parentWindow); 1037 } else { 1038 foreach(w; &wv.parentWindow.focusableWidgets) { 1039 if(w is wv) 1040 break; 1041 f = w; 1042 } 1043 } 1044 if(f) 1045 f.focus(); 1046 }); 1047 } 1048 override int on_set_focus(RC!(cef_browser_t) browser, cef_focus_source_t source) nothrow { 1049 /+ 1050 browser.runOnWebView((ev) { 1051 ev.focus(); // even this can steal focus from other parts of my application! 1052 }); 1053 +/ 1054 //sdpyPrintDebugString("setting"); 1055 1056 return 1; // otherwise, cancel because this bullshit tends to steal focus from other applications and i never, ever, ever want that to happen. 1057 // seems to happen because of race condition in it getting a focus event and then stealing the focus from the parent 1058 // even though things work fine if i always cancel except 1059 // it still keeps the decoration assuming focus though even though it doesn't have it which is kinda fucked up but meh 1060 // it also breaks its own pop up menus and drop down boxes to allow this! wtf 1061 } 1062 override void on_got_focus(RC!(cef_browser_t) browser) nothrow { 1063 browser.runOnWebView((ev) { 1064 // this sometimes steals from the app too but it is relatively acceptable 1065 // steals when i mouse in from the side of the window quickly, but still 1066 // i want the minigui state to match so i'll allow it 1067 ev.focus(); 1068 }); 1069 } 1070 } 1071 1072 class MiniguiCefClient : CEF!cef_client_t { 1073 1074 void delegate(scope OpenNewWindowParams) openNewWindow; 1075 1076 MiniguiCefLifeSpanHandler lsh; 1077 MiniguiLoadHandler loadHandler; 1078 MiniguiDialogHandler dialogHandler; 1079 MiniguiDisplayHandler displayHandler; 1080 MiniguiDownloadHandler downloadHandler; 1081 MiniguiKeyboardHandler keyboardHandler; 1082 MiniguiFocusHandler focusHandler; 1083 MiniguiRequestHandler requestHandler; 1084 this(void delegate(scope OpenNewWindowParams) openNewWindow) { 1085 this.openNewWindow = openNewWindow; 1086 lsh = new MiniguiCefLifeSpanHandler(this); 1087 loadHandler = new MiniguiLoadHandler(); 1088 dialogHandler = new MiniguiDialogHandler(); 1089 displayHandler = new MiniguiDisplayHandler(); 1090 downloadHandler = new MiniguiDownloadHandler(); 1091 keyboardHandler = new MiniguiKeyboardHandler(); 1092 focusHandler = new MiniguiFocusHandler(); 1093 requestHandler = new MiniguiRequestHandler(); 1094 } 1095 1096 override cef_audio_handler_t* get_audio_handler() { 1097 return null; 1098 } 1099 override cef_context_menu_handler_t* get_context_menu_handler() { 1100 return null; 1101 } 1102 override cef_dialog_handler_t* get_dialog_handler() { 1103 return dialogHandler.returnable; 1104 } 1105 override cef_display_handler_t* get_display_handler() { 1106 return displayHandler.returnable; 1107 } 1108 override cef_download_handler_t* get_download_handler() { 1109 return downloadHandler.returnable; 1110 } 1111 override cef_drag_handler_t* get_drag_handler() { 1112 return null; 1113 } 1114 override cef_find_handler_t* get_find_handler() { 1115 return null; 1116 } 1117 override cef_focus_handler_t* get_focus_handler() { 1118 return focusHandler.returnable; 1119 } 1120 override cef_jsdialog_handler_t* get_jsdialog_handler() { 1121 // needed for alert etc. 1122 return null; 1123 } 1124 override cef_keyboard_handler_t* get_keyboard_handler() { 1125 // this can handle keyboard shortcuts etc 1126 return keyboardHandler.returnable; 1127 } 1128 override cef_life_span_handler_t* get_life_span_handler() { 1129 return lsh.returnable; 1130 } 1131 override cef_load_handler_t* get_load_handler() { 1132 return loadHandler.returnable; 1133 } 1134 override cef_render_handler_t* get_render_handler() { 1135 // this thing might work for an off-screen thing 1136 // like to an image or to a video stream maybe 1137 // 1138 // might be useful to have it render here then send it over too for remote X sharing a process 1139 return null; 1140 } 1141 override cef_request_handler_t* get_request_handler() { 1142 return requestHandler.returnable; 1143 } 1144 override int on_process_message_received(RC!cef_browser_t, RC!cef_frame_t, cef_process_id_t, RC!cef_process_message_t) { 1145 return 0; // return 1 if you can actually handle the message 1146 } 1147 override cef_frame_handler_t* get_frame_handler() nothrow { 1148 return null; 1149 } 1150 override cef_print_handler_t* get_print_handler() nothrow { 1151 return null; 1152 } 1153 1154 override cef_command_handler_t* get_command_handler() { 1155 return null; 1156 } 1157 1158 override cef_permission_handler_t* get_permission_handler() { 1159 return null; 1160 } 1161 1162 } 1163 } 1164 1165 class BrowserClosedEvent : Event { 1166 enum EventString = "browserclosed"; 1167 1168 this(Widget target) { super(EventString, target); } 1169 override bool cancelable() const { return false; } 1170 } 1171 1172 /+ 1173 pragma(mangle, "_ZN12CefWindowX115FocusEv") 1174 //pragma(mangle, "_ZN3x116XProto13SetInputFocusERKNS_20SetInputFocusRequestE") 1175 extern(C++) 1176 export void _ZN12CefWindowX115FocusEv() { 1177 sdpyPrintDebugString("OVERRIDDEN"); 1178 } 1179 +/