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 and openUrlInNewWindow 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 ~this() { 478 import core.stdc.stdio; 479 printf("GC'd %p\n", cast(void*) this); 480 } 481 482 private MiniguiCefClient client; 483 484 override void registerMovementAdditionalWork() { 485 if(browserWindow) { 486 // import std.stdio; writeln("new size ", width, "x", height); 487 static if(UsingSimpledisplayX11) { 488 XResizeWindow(XDisplayConnection.get, browserWindow, width, height); 489 if(ozone) XResizeWindow(XDisplayConnection.get, ozone, width, height); 490 } 491 // FIXME: do for Windows too 492 } 493 } 494 495 SimpleWindow browserHostWrapped; 496 SimpleWindow browserWindowWrapped; 497 override SimpleWindow focusableWindow() { 498 if(browserWindowWrapped is null && browserWindow) { 499 browserWindowWrapped = new SimpleWindow(browserWindow); 500 // FIXME: this should never actually happen should it 501 } 502 return browserWindowWrapped; 503 } 504 505 private NativeWindowHandle browserWindow; 506 private NativeWindowHandle ozone; 507 private RC!cef_browser_t browserHandle; 508 509 private static WebViewWidget[NativeWindowHandle] mapping; 510 private static WebViewWidget[NativeWindowHandle] browserMapping; 511 512 private { 513 string findingText; 514 bool findingCase; 515 } 516 517 // might not be stable, webview does this fully integrated 518 void findText(string text, bool forward = true, bool matchCase = false, bool findNext = false) { 519 if(browserHandle) { 520 auto host = browserHandle.get_host(); 521 522 auto txt = cef_string_t(text); 523 host.find(&txt, forward, matchCase, findNext); 524 525 findingText = text; 526 findingCase = matchCase; 527 } 528 } 529 530 // ditto 531 void findPrevious() { 532 if(!browserHandle) 533 return; 534 auto host = browserHandle.get_host(); 535 auto txt = cef_string_t(findingText); 536 host.find(&txt, 0, findingCase, 1); 537 } 538 539 // ditto 540 void findNext() { 541 if(!browserHandle) 542 return; 543 auto host = browserHandle.get_host(); 544 auto txt = cef_string_t(findingText); 545 host.find(&txt, 1, findingCase, 1); 546 } 547 548 // ditto 549 void stopFind() { 550 if(!browserHandle) 551 return; 552 auto host = browserHandle.get_host(); 553 host.stop_finding(1); 554 } 555 556 override void refresh() { if(browserHandle) browserHandle.reload(); } 557 override void back() { if(browserHandle) browserHandle.go_back(); } 558 override void forward() { if(browserHandle) browserHandle.go_forward(); } 559 override void stop() { if(browserHandle) browserHandle.stop_load(); } 560 561 override void navigate(string url) { 562 if(!browserHandle) return; 563 auto s = cef_string_t(url); 564 browserHandle.get_main_frame.load_url(&s); 565 } 566 567 // the url and line are for error reporting purposes 568 override void executeJavascript(string code, string url = null, int line = 0) { 569 if(!browserHandle) return; 570 571 auto c = cef_string_t(code); 572 auto u = cef_string_t(url); 573 browserHandle.get_main_frame.execute_java_script(&c, &u, line); 574 } 575 576 private Window devTools; 577 override void showDevTools() { 578 if(!browserHandle) return; 579 580 if(devTools is null) { 581 auto host = browserHandle.get_host; 582 583 if(host.has_dev_tools()) { 584 host.close_dev_tools(); 585 return; 586 } 587 588 cef_window_info_t windowinfo; 589 version(linux) { 590 auto sw = new Window("DevTools"); 591 //sw.win.beingOpenKeepsAppOpen = false; 592 devTools = sw; 593 594 auto wv = new WebViewWidget_CEF(client, sw, true); 595 596 sw.show(); 597 598 windowinfo.parent_window = wv.containerWindow.nativeWindowHandle; 599 } 600 host.show_dev_tools(&windowinfo, client.passable, null /* settings */, null /* inspect element at coordinates */); 601 } else { 602 if(devTools.hidden) 603 devTools.show(); 604 else 605 devTools.hide(); 606 } 607 } 608 609 // FYI the cef browser host also allows things like custom spelling dictionaries and getting navigation entries. 610 611 // JS on init? 612 // JS bindings? 613 // user styles? 614 // navigate to string? (can just use a data uri maybe?) 615 // custom scheme handlers? 616 617 // navigation callbacks to prohibit certain things or move links to new window etc? 618 } 619 620 version(cef) { 621 622 //import core.sync.semaphore; 623 //__gshared Semaphore semaphore; 624 625 /+ 626 Finds the WebViewWidget associated with the given browser, then runs the given code in the gui thread on it. 627 +/ 628 void runOnWebView(RC!cef_browser_t browser, void delegate(WebViewWidget) dg) nothrow { 629 auto wh = cast(NativeWindowHandle) browser.get_host.get_window_handle; 630 631 import core.thread; 632 try { thread_attachThis(); } catch(Exception e) {} 633 634 runInGuiThreadAsync({ 635 if(auto wvp = wh in WebViewWidget.browserMapping) { 636 dg(*wvp); 637 } else { 638 writeln("not found ", wh, WebViewWidget.browserMapping); 639 } 640 }); 641 } 642 643 class MiniguiCefLifeSpanHandler : CEF!cef_life_span_handler_t { 644 private MiniguiCefClient client; 645 this(MiniguiCefClient client) { 646 this.client = client; 647 } 648 649 override int on_before_popup( 650 RC!cef_browser_t browser, 651 RC!cef_frame_t frame, 652 const(cef_string_t)* target_url, 653 const(cef_string_t)* target_frame_name, 654 cef_window_open_disposition_t target_disposition, 655 int user_gesture, 656 const(cef_popup_features_t)* popupFeatures, 657 cef_window_info_t* windowInfo, 658 cef_client_t** client, 659 cef_browser_settings_t* browser_settings, 660 cef_dictionary_value_t** extra_info, 661 int* no_javascript_access 662 ) { 663 sdpyPrintDebugString("on_before_popup"); 664 if(this.client.openNewWindow is null) 665 return 1; // new windows disabled 666 667 try { 668 int ret; 669 670 import core.thread; 671 try { thread_attachThis(); } catch(Exception e) {} 672 673 // FIXME: change settings here 674 675 runInGuiThread({ 676 ret = 1; 677 scope WebViewWidget delegate(Widget, BrowserSettings) accept = (parent, passed_settings) { 678 ret = 0; 679 if(parent !is null) { 680 auto widget = new WebViewWidget_CEF(this.client, parent, false); 681 (*windowInfo).parent_window = widget.containerWindow.nativeWindowHandle; 682 683 passed_settings.set(browser_settings); 684 685 return widget; 686 } 687 return null; 688 }; 689 this.client.openNewWindow(OpenNewWindowParams(target_url.toGC, accept)); 690 return; 691 }); 692 693 return ret; 694 } catch(Exception e) { 695 return 1; 696 } 697 /+ 698 if(!user_gesture) 699 return 1; // if not created by the user, cancel it; basic popup blocking 700 +/ 701 } 702 override void on_after_created(RC!cef_browser_t browser) { 703 auto handle = cast(NativeWindowHandle) browser.get_host().get_window_handle(); 704 auto ptr = browser.passable; // this adds to the refcount until it gets inside 705 706 import core.thread; 707 try { thread_attachThis(); } catch(Exception e) {} 708 709 // the only reliable key (at least as far as i can tell) is the window handle 710 // so gonna look that up and do the sync mapping that way. 711 runInGuiThreadAsync({ 712 version(Windows) { 713 auto parent = GetParent(handle); 714 } else static if(UsingSimpledisplayX11) { 715 import arsd.simpledisplay : Window; 716 Window root; 717 Window parent; 718 Window ozone; 719 uint c = 0; 720 auto display = XDisplayConnection.get; 721 Window* children; 722 XQueryTree(display, handle, &root, &parent, &children, &c); 723 if(c == 1) 724 ozone = children[0]; 725 XFree(children); 726 } else static assert(0); 727 728 if(auto wvp = parent in WebViewWidget.mapping) { 729 auto wv = *wvp; 730 wv.browserWindow = handle; 731 wv.browserHandle = RC!cef_browser_t(ptr); 732 wv.ozone = ozone ? ozone : handle; 733 734 wv.browserHostWrapped = new SimpleWindow(handle); 735 // XSelectInput(XDisplayConnection.get, handle, EventMask.StructureNotifyMask); 736 737 wv.browserHostWrapped.onDestroyed = delegate{ 738 import std.stdio; writefln("browser host %d destroyed (handle %d)", wv.browserWindowWrapped.window, wv.browserWindow); 739 740 auto bce = new BrowserClosedEvent(wv); 741 bce.dispatch(); 742 }; 743 744 // need this to forward key events to 745 wv.browserWindowWrapped = new SimpleWindow(wv.ozone); 746 747 /+ 748 XSelectInput(XDisplayConnection.get, wv.ozone, EventMask.StructureNotifyMask); 749 wv.browserWindowWrapped.onDestroyed = delegate{ 750 import std.stdio; writefln("browser core %d destroyed (handle %d)", wv.browserWindowWrapped.window, wv.browserWindow); 751 752 //auto bce = new BrowserClosedEvent(wv); 753 //bce.dispatch(); 754 }; 755 +/ 756 757 /+ 758 XSelectInput(XDisplayConnection.get, ozone, EventMask.FocusChangeMask); 759 wv.browserWindowWrapped.onFocusChange = (bool got) { 760 import std.format; 761 sdpyPrintDebugString(format("focus change %s %x", got, wv.browserWindowWrapped.impl.window)); 762 }; 763 +/ 764 765 wv.registerMovementAdditionalWork(); 766 767 WebViewWidget.browserMapping[handle] = wv; 768 } else assert(0); 769 }); 770 } 771 override int do_close(RC!cef_browser_t browser) { 772 import std.stdio; 773 debug writeln("do_close"); 774 /+ 775 browser.runOnWebView((wv) { 776 wv.browserWindowWrapped.close(); 777 .destroy(wv.browserHandle); 778 }); 779 780 return 1; 781 +/ 782 783 return 0; 784 } 785 override void on_before_close(RC!cef_browser_t browser) { 786 import std.stdio; debug writeln("notify"); 787 browser.runOnWebView((wv) { 788 .destroy(wv.browserHandle); 789 }); 790 /+ 791 try 792 semaphore.notify; 793 catch(Exception e) { assert(0); } 794 +/ 795 } 796 } 797 798 class MiniguiLoadHandler : CEF!cef_load_handler_t { 799 override void on_loading_state_change(RC!(cef_browser_t) browser, int isLoading, int canGoBack, int canGoForward) { 800 /+ 801 browser.runOnWebView((WebViewWidget wvw) { 802 wvw.parentWindow.win.title = wvw.browserHandle.get_main_frame.get_url.toGCAndFree; 803 }); 804 +/ 805 } 806 override void on_load_start(RC!(cef_browser_t), RC!(cef_frame_t), cef_transition_type_t) { 807 } 808 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)*) { 809 } 810 override void on_load_end(RC!(cef_browser_t), RC!(cef_frame_t), int) { 811 } 812 } 813 814 class MiniguiDialogHandler : CEF!cef_dialog_handler_t { 815 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) { 816 try { 817 auto ptr = callback.passable(); 818 browser.runOnWebView((wv) { 819 getOpenFileName((string name) { 820 auto callback = RC!cef_file_dialog_callback_t(ptr); 821 auto list = libcef.string_list_alloc(); 822 auto item = cef_string_t(name); 823 libcef.string_list_append(list, &item); 824 callback.cont(list); 825 }, null, null, () { 826 auto callback = RC!cef_file_dialog_callback_t(ptr); 827 callback.cancel(); 828 }, "/home/me/"); 829 }); 830 } catch(Exception e) {} 831 832 return 1; 833 } 834 } 835 836 class MiniguiDownloadHandler : CEF!cef_download_handler_t { 837 override void on_before_download( 838 RC!cef_browser_t browser, 839 RC!cef_download_item_t download_item, 840 const(cef_string_t)* suggested_name, 841 RC!cef_before_download_callback_t callback 842 ) nothrow 843 { 844 // FIXME: different filename and check if exists for overwrite etc 845 auto fn = cef_string_t(cast(wstring)("/home/me/Downloads/"w ~ suggested_name.str[0..suggested_name.length])); 846 sdpyPrintDebugString(fn.toGC); 847 callback.cont(&fn, false); 848 } 849 850 override void on_download_updated( 851 RC!cef_browser_t browser, 852 RC!cef_download_item_t download_item, 853 RC!cef_download_item_callback_t cancel 854 ) nothrow 855 { 856 sdpyPrintDebugString(download_item.get_percent_complete()); 857 // FIXME 858 } 859 860 override int can_download(RC!(cef_browser_t), const(cef_string_utf16_t)*, const(cef_string_utf16_t)*) { 861 return 1; 862 } 863 } 864 865 class MiniguiKeyboardHandler : CEF!cef_keyboard_handler_t { 866 override int on_pre_key_event( 867 RC!(cef_browser_t) browser, 868 const(cef_key_event_t)* event, 869 XEvent* osEvent, 870 int* isShortcut 871 ) nothrow { 872 /+ 873 sdpyPrintDebugString("---pre---"); 874 sdpyPrintDebugString(event.focus_on_editable_field); 875 sdpyPrintDebugString(event.windows_key_code); 876 sdpyPrintDebugString(event.modifiers); 877 sdpyPrintDebugString(event.unmodified_character); 878 +/ 879 //*isShortcut = 1; 880 return 0; // 1 if handled, which cancels sending it to browser 881 } 882 883 override int on_key_event( 884 RC!(cef_browser_t) browser, 885 const(cef_key_event_t)* event, 886 XEvent* osEvent 887 ) nothrow { 888 /+ 889 sdpyPrintDebugString("---key---"); 890 sdpyPrintDebugString(event.focus_on_editable_field); 891 sdpyPrintDebugString(event.windows_key_code); 892 sdpyPrintDebugString(event.modifiers); 893 +/ 894 return 0; // 1 if handled 895 } 896 } 897 898 class MiniguiDisplayHandler : CEF!cef_display_handler_t { 899 override void on_address_change(RC!(cef_browser_t) browser, RC!(cef_frame_t), const(cef_string_utf16_t)* address) { 900 auto url = address.toGC; 901 browser.runOnWebView((wv) { 902 wv.url = url; 903 }); 904 } 905 override void on_title_change(RC!(cef_browser_t) browser, const(cef_string_utf16_t)* title) { 906 auto t = title.toGC; 907 browser.runOnWebView((wv) { 908 wv.title = t; 909 }); 910 } 911 override void on_favicon_urlchange(RC!(cef_browser_t) browser, cef_string_list_t urls) { 912 string url; 913 auto size = libcef.string_list_size(urls); 914 if(size > 0) { 915 cef_string_t str; 916 libcef.string_list_value(urls, 0, &str); 917 url = str.toGC; 918 919 static class Thing : CEF!cef_download_image_callback_t { 920 RC!cef_browser_t browserHandle; 921 this(RC!cef_browser_t browser) nothrow { 922 this.browserHandle = browser; 923 } 924 override void on_download_image_finished(const(cef_string_t)* image_url, int http_status_code, RC!cef_image_t image) nothrow { 925 int width; 926 int height; 927 if(image.getRawPointer is null) { 928 browserHandle.runOnWebView((wv) { 929 wv.favicon = null; 930 }); 931 return; 932 } 933 934 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); 935 936 if(data.getRawPointer is null || width == 0 || height == 0) { 937 browserHandle.runOnWebView((wv) { 938 wv.favicon = null; 939 }); 940 } else { 941 auto s = data.get_size(); 942 auto buffer = new ubyte[](s); 943 auto got = data.get_data(buffer.ptr, buffer.length, 0); 944 auto slice = buffer[0 .. got]; 945 946 auto img = new TrueColorImage (width, height, slice); 947 948 browserHandle.runOnWebView((wv) { 949 wv.favicon = img; 950 }); 951 } 952 } 953 } 954 955 if(url.length) { 956 auto callback = new Thing(browser); 957 958 browser.get_host().download_image(&str, true, 16, 0, callback.passable); 959 } else { 960 browser.runOnWebView((wv) { 961 wv.favicon = null; 962 }); 963 } 964 } 965 966 browser.runOnWebView((wv) { 967 wv.favicon_url = url; 968 }); 969 } 970 override void on_fullscreen_mode_change(RC!(cef_browser_t) browser, int) { 971 } 972 override int on_tooltip(RC!(cef_browser_t) browser, cef_string_utf16_t*) { 973 return 0; 974 } 975 override void on_status_message(RC!(cef_browser_t) browser, const(cef_string_utf16_t)* msg) { 976 auto status = msg.toGC; 977 browser.runOnWebView((wv) { 978 wv.status = status; 979 }); 980 } 981 override void on_loading_progress_change(RC!(cef_browser_t) browser, double progress) { 982 // progress is from 0.0 to 1.0 983 browser.runOnWebView((wv) { 984 wv.loadingProgress = cast(int) (progress * 100); 985 }); 986 } 987 override int on_console_message(RC!(cef_browser_t), cef_log_severity_t, const(cef_string_utf16_t)*, const(cef_string_utf16_t)*, int) { 988 return 1; // 1 means to suppress it being automatically output 989 } 990 override int on_auto_resize(RC!(cef_browser_t), const(cef_size_t)*) { 991 return 0; 992 } 993 override int on_cursor_change(RC!(cef_browser_t), cef_cursor_handle_t, cef_cursor_type_t, const(cef_cursor_info_t)*) { 994 return 0; 995 } 996 override void on_media_access_change(RC!(cef_browser_t), int, int) { 997 998 } 999 } 1000 1001 class MiniguiRequestHandler : CEF!cef_request_handler_t { 1002 override int on_before_browse(RC!(cef_browser_t), RC!(cef_frame_t), RC!(cef_request_t), int, int) nothrow { 1003 return 0; 1004 } 1005 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 { 1006 return 0; 1007 } 1008 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 { 1009 return null; 1010 } 1011 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 { 1012 // this is for http basic auth popup..... 1013 return 0; 1014 } 1015 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 { 1016 return 0; 1017 } 1018 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 { 1019 return 0; 1020 } 1021 override void on_render_view_ready(RC!(cef_browser_t) p) nothrow { 1022 1023 } 1024 override void on_render_process_terminated(RC!(cef_browser_t), cef_termination_status_t) nothrow { 1025 1026 } 1027 override void on_document_available_in_main_frame(RC!(cef_browser_t) browser) nothrow { 1028 browser.runOnWebView(delegate(wv) { 1029 wv.executeJavascript("console.log('here');"); 1030 }); 1031 1032 } 1033 } 1034 1035 class MiniguiContextMenuHandler : CEF!cef_context_menu_handler_t { 1036 private MiniguiCefClient client; 1037 this(MiniguiCefClient client) { 1038 this.client = client; 1039 } 1040 1041 override void on_before_context_menu(RC!(cef_browser_t) browser, RC!(cef_frame_t) frame, RC!(cef_context_menu_params_t) params, RC!(cef_menu_model_t) model) nothrow { 1042 // FIXME: should i customize these? it is kinda specific to my browser 1043 int itemNo; 1044 1045 void addItem(string label, int commandNo) { 1046 auto lbl = cef_string_t(label); 1047 model.insert_item_at(/* index */ itemNo, /* command id */ cef_menu_id_t.MENU_ID_USER_FIRST + commandNo, &lbl); 1048 itemNo++; 1049 } 1050 1051 void addSeparator() { 1052 model.insert_separator_at(itemNo); 1053 itemNo++; 1054 } 1055 1056 auto flags = params.get_type_flags(); 1057 1058 if(flags & cef_context_menu_type_flags_t.CM_TYPEFLAG_LINK) { 1059 // cef_string_userfree_t linkUrl = params.get_unfiltered_link_url(); 1060 // toGCAndFree 1061 addItem("Open link in new window", 1); 1062 addItem("Copy link URL", 2); 1063 1064 // FIXME: open in other browsers 1065 addSeparator(); 1066 } 1067 1068 if(flags & cef_context_menu_type_flags_t.CM_TYPEFLAG_MEDIA) { 1069 // cef_string_userfree_t linkUrl = params.get_source_url(); 1070 // toGCAndFree 1071 addItem("Open media in new window", 3); 1072 addItem("Copy source URL", 4); 1073 addItem("Download media", 5); 1074 addSeparator(); 1075 } 1076 1077 1078 // get_page_url 1079 // get_title_text 1080 // has_image_contents ??? 1081 // get_source_url 1082 // get_xcoord and get_ycoord 1083 // get_selection_text 1084 1085 } 1086 override int run_context_menu(RC!(cef_browser_t), RC!(cef_frame_t), RC!(cef_context_menu_params_t), RC!(cef_menu_model_t), RC!(cef_run_context_menu_callback_t)) nothrow { 1087 // could do a custom display here if i want but i think it is good enough as it is 1088 return 0; 1089 } 1090 override int on_context_menu_command(RC!(cef_browser_t) browser, RC!(cef_frame_t) frame, RC!(cef_context_menu_params_t) params, int commandId, cef_event_flags_t flags) nothrow { 1091 switch(commandId) { 1092 case cef_menu_id_t.MENU_ID_USER_FIRST + 1: // open link in new window 1093 auto what = params.get_unfiltered_link_url().toGCAndFree(); 1094 1095 browser.runOnWebView((widget) { 1096 auto event = new NewWindowRequestedEvent(what, widget); 1097 event.dispatch(); 1098 }); 1099 return 1; 1100 case cef_menu_id_t.MENU_ID_USER_FIRST + 2: // copy link url 1101 auto what = params.get_link_url().toGCAndFree(); 1102 1103 browser.runOnWebView((widget) { 1104 auto event = new CopyRequestedEvent(what, widget); 1105 event.dispatch(); 1106 }); 1107 return 1; 1108 case cef_menu_id_t.MENU_ID_USER_FIRST + 3: // open media in new window 1109 auto what = params.get_source_url().toGCAndFree(); 1110 1111 browser.runOnWebView((widget) { 1112 auto event = new NewWindowRequestedEvent(what, widget); 1113 event.dispatch(); 1114 }); 1115 return 1; 1116 case cef_menu_id_t.MENU_ID_USER_FIRST + 4: // copy source url 1117 auto what = params.get_source_url().toGCAndFree(); 1118 1119 browser.runOnWebView((widget) { 1120 auto event = new CopyRequestedEvent(what, widget); 1121 event.dispatch(); 1122 }); 1123 return 1; 1124 case cef_menu_id_t.MENU_ID_USER_FIRST + 5: // download media 1125 auto str = cef_string_t(params.get_source_url().toGCAndFree()); 1126 browser.get_host().start_download(&str); 1127 return 1; 1128 default: 1129 return 0; 1130 } 1131 } 1132 override void on_context_menu_dismissed(RC!(cef_browser_t), RC!(cef_frame_t)) nothrow { 1133 // to close the custom display 1134 } 1135 1136 override int run_quick_menu(RC!(cef_browser_t), RC!(cef_frame_t), const(cef_point_t)*, const(cef_size_t)*, cef_quick_menu_edit_state_flags_t, RC!(cef_run_quick_menu_callback_t)) nothrow { 1137 return 0; 1138 } 1139 override int on_quick_menu_command(RC!(cef_browser_t), RC!(cef_frame_t), int, cef_event_flags_t) nothrow { 1140 return 0; 1141 } 1142 override void on_quick_menu_dismissed(RC!(cef_browser_t), RC!(cef_frame_t)) nothrow { 1143 1144 } 1145 } 1146 1147 class MiniguiFocusHandler : CEF!cef_focus_handler_t { 1148 override void on_take_focus(RC!(cef_browser_t) browser, int next) nothrow { 1149 browser.runOnWebView(delegate(wv) { 1150 Widget f; 1151 if(next) { 1152 f = Window.getFirstFocusable(wv.parentWindow); 1153 } else { 1154 foreach(w; &wv.parentWindow.focusableWidgets) { 1155 if(w is wv) 1156 break; 1157 f = w; 1158 } 1159 } 1160 if(f) 1161 f.focus(); 1162 }); 1163 } 1164 override int on_set_focus(RC!(cef_browser_t) browser, cef_focus_source_t source) nothrow { 1165 /+ 1166 browser.runOnWebView((ev) { 1167 ev.focus(); // even this can steal focus from other parts of my application! 1168 }); 1169 +/ 1170 //sdpyPrintDebugString("setting"); 1171 1172 return 1; // otherwise, cancel because this bullshit tends to steal focus from other applications and i never, ever, ever want that to happen. 1173 // seems to happen because of race condition in it getting a focus event and then stealing the focus from the parent 1174 // even though things work fine if i always cancel except 1175 // it still keeps the decoration assuming focus though even though it doesn't have it which is kinda fucked up but meh 1176 // it also breaks its own pop up menus and drop down boxes to allow this! wtf 1177 } 1178 override void on_got_focus(RC!(cef_browser_t) browser) nothrow { 1179 browser.runOnWebView((ev) { 1180 // this sometimes steals from the app too but it is relatively acceptable 1181 // steals when i mouse in from the side of the window quickly, but still 1182 // i want the minigui state to match so i'll allow it 1183 ev.focus(); 1184 }); 1185 } 1186 } 1187 1188 class MiniguiCefClient : CEF!cef_client_t { 1189 1190 void delegate(scope OpenNewWindowParams) openNewWindow; 1191 1192 MiniguiCefLifeSpanHandler lsh; 1193 MiniguiLoadHandler loadHandler; 1194 MiniguiDialogHandler dialogHandler; 1195 MiniguiDisplayHandler displayHandler; 1196 MiniguiDownloadHandler downloadHandler; 1197 MiniguiKeyboardHandler keyboardHandler; 1198 MiniguiFocusHandler focusHandler; 1199 MiniguiRequestHandler requestHandler; 1200 MiniguiContextMenuHandler contextMenuHandler; 1201 this(void delegate(scope OpenNewWindowParams) openNewWindow) { 1202 this.openNewWindow = openNewWindow; 1203 lsh = new MiniguiCefLifeSpanHandler(this); 1204 loadHandler = new MiniguiLoadHandler(); 1205 dialogHandler = new MiniguiDialogHandler(); 1206 displayHandler = new MiniguiDisplayHandler(); 1207 downloadHandler = new MiniguiDownloadHandler(); 1208 keyboardHandler = new MiniguiKeyboardHandler(); 1209 focusHandler = new MiniguiFocusHandler(); 1210 requestHandler = new MiniguiRequestHandler(); 1211 contextMenuHandler = new MiniguiContextMenuHandler(this); 1212 } 1213 1214 override cef_audio_handler_t* get_audio_handler() { 1215 return null; 1216 } 1217 override cef_context_menu_handler_t* get_context_menu_handler() { 1218 return contextMenuHandler.returnable; 1219 } 1220 override cef_dialog_handler_t* get_dialog_handler() { 1221 return dialogHandler.returnable; 1222 } 1223 override cef_display_handler_t* get_display_handler() { 1224 return displayHandler.returnable; 1225 } 1226 override cef_download_handler_t* get_download_handler() { 1227 return downloadHandler.returnable; 1228 } 1229 override cef_drag_handler_t* get_drag_handler() { 1230 return null; 1231 } 1232 override cef_find_handler_t* get_find_handler() { 1233 return null; 1234 } 1235 override cef_focus_handler_t* get_focus_handler() { 1236 return focusHandler.returnable; 1237 } 1238 override cef_jsdialog_handler_t* get_jsdialog_handler() { 1239 // needed for alert etc. 1240 return null; 1241 } 1242 override cef_keyboard_handler_t* get_keyboard_handler() { 1243 // this can handle keyboard shortcuts etc 1244 return keyboardHandler.returnable; 1245 } 1246 override cef_life_span_handler_t* get_life_span_handler() { 1247 return lsh.returnable; 1248 } 1249 override cef_load_handler_t* get_load_handler() { 1250 return loadHandler.returnable; 1251 } 1252 override cef_render_handler_t* get_render_handler() { 1253 // this thing might work for an off-screen thing 1254 // like to an image or to a video stream maybe 1255 // 1256 // might be useful to have it render here then send it over too for remote X sharing a process 1257 return null; 1258 } 1259 override cef_request_handler_t* get_request_handler() { 1260 return requestHandler.returnable; 1261 } 1262 override int on_process_message_received(RC!cef_browser_t, RC!cef_frame_t, cef_process_id_t, RC!cef_process_message_t) { 1263 return 0; // return 1 if you can actually handle the message 1264 } 1265 override cef_frame_handler_t* get_frame_handler() nothrow { 1266 return null; 1267 } 1268 override cef_print_handler_t* get_print_handler() nothrow { 1269 return null; 1270 } 1271 1272 override cef_command_handler_t* get_command_handler() { 1273 return null; 1274 } 1275 1276 override cef_permission_handler_t* get_permission_handler() { 1277 return null; 1278 } 1279 1280 } 1281 } 1282 1283 class BrowserClosedEvent : Event { 1284 enum EventString = "browserclosed"; 1285 1286 this(Widget target) { super(EventString, target); } 1287 override bool cancelable() const { return false; } 1288 } 1289 1290 class CopyRequestedEvent : Event { 1291 enum EventString = "browsercopyrequested"; 1292 1293 string what; 1294 1295 this(string what, Widget target) { this.what = what; super(EventString, target); } 1296 override bool cancelable() const { return false; } 1297 } 1298 1299 class NewWindowRequestedEvent : Event { 1300 enum EventString = "browserwindowrequested"; 1301 1302 string url; 1303 1304 this(string url, Widget target) { this.url = url; super(EventString, target); } 1305 override bool cancelable() const { return false; } 1306 } 1307 1308 1309 1310 /+ 1311 pragma(mangle, "_ZN12CefWindowX115FocusEv") 1312 //pragma(mangle, "_ZN3x116XProto13SetInputFocusERKNS_20SetInputFocusRequestE") 1313 extern(C++) 1314 export void _ZN12CefWindowX115FocusEv() { 1315 sdpyPrintDebugString("OVERRIDDEN"); 1316 } 1317 +/