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_quota_request(RC!(cef_browser_t), const(cef_string_utf16_t)*, long, RC!(cef_callback_t)) nothrow { 1016 return 0; 1017 } 1018 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 { 1019 return 0; 1020 } 1021 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 { 1022 return 0; 1023 } 1024 override void on_render_view_ready(RC!(cef_browser_t) p) nothrow { 1025 1026 } 1027 override void on_render_process_terminated(RC!(cef_browser_t), cef_termination_status_t) nothrow { 1028 1029 } 1030 override void on_document_available_in_main_frame(RC!(cef_browser_t) browser) nothrow { 1031 browser.runOnWebView(delegate(wv) { 1032 wv.executeJavascript("console.log('here');"); 1033 }); 1034 1035 } 1036 } 1037 1038 class MiniguiContextMenuHandler : CEF!cef_context_menu_handler_t { 1039 private MiniguiCefClient client; 1040 this(MiniguiCefClient client) { 1041 this.client = client; 1042 } 1043 1044 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 { 1045 // FIXME: should i customize these? it is kinda specific to my browser 1046 int itemNo; 1047 1048 void addItem(string label, int commandNo) { 1049 auto lbl = cef_string_t(label); 1050 model.insert_item_at(/* index */ itemNo, /* command id */ cef_menu_id_t.MENU_ID_USER_FIRST + commandNo, &lbl); 1051 itemNo++; 1052 } 1053 1054 void addSeparator() { 1055 model.insert_separator_at(itemNo); 1056 itemNo++; 1057 } 1058 1059 auto flags = params.get_type_flags(); 1060 1061 if(flags & cef_context_menu_type_flags_t.CM_TYPEFLAG_LINK) { 1062 // cef_string_userfree_t linkUrl = params.get_unfiltered_link_url(); 1063 // toGCAndFree 1064 addItem("Open link in new window", 1); 1065 addItem("Copy link URL", 2); 1066 1067 // FIXME: open in other browsers 1068 addSeparator(); 1069 } 1070 1071 if(flags & cef_context_menu_type_flags_t.CM_TYPEFLAG_MEDIA) { 1072 // cef_string_userfree_t linkUrl = params.get_source_url(); 1073 // toGCAndFree 1074 addItem("Open media in new window", 3); 1075 addItem("Copy source URL", 4); 1076 addItem("Download media", 5); 1077 addSeparator(); 1078 } 1079 1080 1081 // get_page_url 1082 // get_title_text 1083 // has_image_contents ??? 1084 // get_source_url 1085 // get_xcoord and get_ycoord 1086 // get_selection_text 1087 1088 } 1089 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 { 1090 // could do a custom display here if i want but i think it is good enough as it is 1091 return 0; 1092 } 1093 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 { 1094 switch(commandId) { 1095 case cef_menu_id_t.MENU_ID_USER_FIRST + 1: // open link in new window 1096 auto what = params.get_unfiltered_link_url().toGCAndFree(); 1097 1098 browser.runOnWebView((widget) { 1099 auto event = new NewWindowRequestedEvent(what, widget); 1100 event.dispatch(); 1101 }); 1102 return 1; 1103 case cef_menu_id_t.MENU_ID_USER_FIRST + 2: // copy link url 1104 auto what = params.get_link_url().toGCAndFree(); 1105 1106 browser.runOnWebView((widget) { 1107 auto event = new CopyRequestedEvent(what, widget); 1108 event.dispatch(); 1109 }); 1110 return 1; 1111 case cef_menu_id_t.MENU_ID_USER_FIRST + 3: // open media in new window 1112 auto what = params.get_source_url().toGCAndFree(); 1113 1114 browser.runOnWebView((widget) { 1115 auto event = new NewWindowRequestedEvent(what, widget); 1116 event.dispatch(); 1117 }); 1118 return 1; 1119 case cef_menu_id_t.MENU_ID_USER_FIRST + 4: // copy source url 1120 auto what = params.get_source_url().toGCAndFree(); 1121 1122 browser.runOnWebView((widget) { 1123 auto event = new CopyRequestedEvent(what, widget); 1124 event.dispatch(); 1125 }); 1126 return 1; 1127 case cef_menu_id_t.MENU_ID_USER_FIRST + 5: // download media 1128 auto str = cef_string_t(params.get_source_url().toGCAndFree()); 1129 browser.get_host().start_download(&str); 1130 return 1; 1131 default: 1132 return 0; 1133 } 1134 } 1135 override void on_context_menu_dismissed(RC!(cef_browser_t), RC!(cef_frame_t)) nothrow { 1136 // to close the custom display 1137 } 1138 1139 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 { 1140 return 0; 1141 } 1142 override int on_quick_menu_command(RC!(cef_browser_t), RC!(cef_frame_t), int, cef_event_flags_t) nothrow { 1143 return 0; 1144 } 1145 override void on_quick_menu_dismissed(RC!(cef_browser_t), RC!(cef_frame_t)) nothrow { 1146 1147 } 1148 } 1149 1150 class MiniguiFocusHandler : CEF!cef_focus_handler_t { 1151 override void on_take_focus(RC!(cef_browser_t) browser, int next) nothrow { 1152 browser.runOnWebView(delegate(wv) { 1153 Widget f; 1154 if(next) { 1155 f = Window.getFirstFocusable(wv.parentWindow); 1156 } else { 1157 foreach(w; &wv.parentWindow.focusableWidgets) { 1158 if(w is wv) 1159 break; 1160 f = w; 1161 } 1162 } 1163 if(f) 1164 f.focus(); 1165 }); 1166 } 1167 override int on_set_focus(RC!(cef_browser_t) browser, cef_focus_source_t source) nothrow { 1168 /+ 1169 browser.runOnWebView((ev) { 1170 ev.focus(); // even this can steal focus from other parts of my application! 1171 }); 1172 +/ 1173 //sdpyPrintDebugString("setting"); 1174 1175 return 1; // otherwise, cancel because this bullshit tends to steal focus from other applications and i never, ever, ever want that to happen. 1176 // seems to happen because of race condition in it getting a focus event and then stealing the focus from the parent 1177 // even though things work fine if i always cancel except 1178 // it still keeps the decoration assuming focus though even though it doesn't have it which is kinda fucked up but meh 1179 // it also breaks its own pop up menus and drop down boxes to allow this! wtf 1180 } 1181 override void on_got_focus(RC!(cef_browser_t) browser) nothrow { 1182 browser.runOnWebView((ev) { 1183 // this sometimes steals from the app too but it is relatively acceptable 1184 // steals when i mouse in from the side of the window quickly, but still 1185 // i want the minigui state to match so i'll allow it 1186 ev.focus(); 1187 }); 1188 } 1189 } 1190 1191 class MiniguiCefClient : CEF!cef_client_t { 1192 1193 void delegate(scope OpenNewWindowParams) openNewWindow; 1194 1195 MiniguiCefLifeSpanHandler lsh; 1196 MiniguiLoadHandler loadHandler; 1197 MiniguiDialogHandler dialogHandler; 1198 MiniguiDisplayHandler displayHandler; 1199 MiniguiDownloadHandler downloadHandler; 1200 MiniguiKeyboardHandler keyboardHandler; 1201 MiniguiFocusHandler focusHandler; 1202 MiniguiRequestHandler requestHandler; 1203 MiniguiContextMenuHandler contextMenuHandler; 1204 this(void delegate(scope OpenNewWindowParams) openNewWindow) { 1205 this.openNewWindow = openNewWindow; 1206 lsh = new MiniguiCefLifeSpanHandler(this); 1207 loadHandler = new MiniguiLoadHandler(); 1208 dialogHandler = new MiniguiDialogHandler(); 1209 displayHandler = new MiniguiDisplayHandler(); 1210 downloadHandler = new MiniguiDownloadHandler(); 1211 keyboardHandler = new MiniguiKeyboardHandler(); 1212 focusHandler = new MiniguiFocusHandler(); 1213 requestHandler = new MiniguiRequestHandler(); 1214 contextMenuHandler = new MiniguiContextMenuHandler(this); 1215 } 1216 1217 override cef_audio_handler_t* get_audio_handler() { 1218 return null; 1219 } 1220 override cef_context_menu_handler_t* get_context_menu_handler() { 1221 return contextMenuHandler.returnable; 1222 } 1223 override cef_dialog_handler_t* get_dialog_handler() { 1224 return dialogHandler.returnable; 1225 } 1226 override cef_display_handler_t* get_display_handler() { 1227 return displayHandler.returnable; 1228 } 1229 override cef_download_handler_t* get_download_handler() { 1230 return downloadHandler.returnable; 1231 } 1232 override cef_drag_handler_t* get_drag_handler() { 1233 return null; 1234 } 1235 override cef_find_handler_t* get_find_handler() { 1236 return null; 1237 } 1238 override cef_focus_handler_t* get_focus_handler() { 1239 return focusHandler.returnable; 1240 } 1241 override cef_jsdialog_handler_t* get_jsdialog_handler() { 1242 // needed for alert etc. 1243 return null; 1244 } 1245 override cef_keyboard_handler_t* get_keyboard_handler() { 1246 // this can handle keyboard shortcuts etc 1247 return keyboardHandler.returnable; 1248 } 1249 override cef_life_span_handler_t* get_life_span_handler() { 1250 return lsh.returnable; 1251 } 1252 override cef_load_handler_t* get_load_handler() { 1253 return loadHandler.returnable; 1254 } 1255 override cef_render_handler_t* get_render_handler() { 1256 // this thing might work for an off-screen thing 1257 // like to an image or to a video stream maybe 1258 // 1259 // might be useful to have it render here then send it over too for remote X sharing a process 1260 return null; 1261 } 1262 override cef_request_handler_t* get_request_handler() { 1263 return requestHandler.returnable; 1264 } 1265 override int on_process_message_received(RC!cef_browser_t, RC!cef_frame_t, cef_process_id_t, RC!cef_process_message_t) { 1266 return 0; // return 1 if you can actually handle the message 1267 } 1268 override cef_frame_handler_t* get_frame_handler() nothrow { 1269 return null; 1270 } 1271 override cef_print_handler_t* get_print_handler() nothrow { 1272 return null; 1273 } 1274 1275 override cef_command_handler_t* get_command_handler() { 1276 return null; 1277 } 1278 1279 override cef_permission_handler_t* get_permission_handler() { 1280 return null; 1281 } 1282 1283 } 1284 } 1285 1286 class BrowserClosedEvent : Event { 1287 enum EventString = "browserclosed"; 1288 1289 this(Widget target) { super(EventString, target); } 1290 override bool cancelable() const { return false; } 1291 } 1292 1293 class CopyRequestedEvent : Event { 1294 enum EventString = "browsercopyrequested"; 1295 1296 string what; 1297 1298 this(string what, Widget target) { this.what = what; super(EventString, target); } 1299 override bool cancelable() const { return false; } 1300 } 1301 1302 class NewWindowRequestedEvent : Event { 1303 enum EventString = "browserwindowrequested"; 1304 1305 string url; 1306 1307 this(string url, Widget target) { this.url = url; super(EventString, target); } 1308 override bool cancelable() const { return false; } 1309 } 1310 1311 1312 1313 /+ 1314 pragma(mangle, "_ZN12CefWindowX115FocusEv") 1315 //pragma(mangle, "_ZN3x116XProto13SetInputFocusERKNS_20SetInputFocusRequestE") 1316 extern(C++) 1317 export void _ZN12CefWindowX115FocusEv() { 1318 sdpyPrintDebugString("OVERRIDDEN"); 1319 } 1320 +/