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