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