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 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 override void refresh() { if(browserHandle) browserHandle.reload(); } 494 override void back() { if(browserHandle) browserHandle.go_back(); } 495 override void forward() { if(browserHandle) browserHandle.go_forward(); } 496 override void stop() { if(browserHandle) browserHandle.stop_load(); } 497 498 override void navigate(string url) { 499 if(!browserHandle) return; 500 auto s = cef_string_t(url); 501 browserHandle.get_main_frame.load_url(&s); 502 } 503 504 // the url and line are for error reporting purposes 505 override void executeJavascript(string code, string url = null, int line = 0) { 506 if(!browserHandle) return; 507 508 auto c = cef_string_t(code); 509 auto u = cef_string_t(url); 510 browserHandle.get_main_frame.execute_java_script(&c, &u, line); 511 } 512 513 private Window devTools; 514 override void showDevTools() { 515 if(!browserHandle) return; 516 517 if(devTools is null) { 518 auto host = browserHandle.get_host; 519 520 if(host.has_dev_tools()) { 521 host.close_dev_tools(); 522 return; 523 } 524 525 cef_window_info_t windowinfo; 526 version(linux) { 527 auto sw = new Window("DevTools"); 528 //sw.win.beingOpenKeepsAppOpen = false; 529 devTools = sw; 530 531 auto wv = new WebViewWidget_CEF(client, sw, true); 532 533 sw.show(); 534 535 windowinfo.parent_window = wv.containerWindow.nativeWindowHandle; 536 } 537 host.show_dev_tools(&windowinfo, client.passable, null /* settings */, null /* inspect element at coordinates */); 538 } else { 539 if(devTools.hidden) 540 devTools.show(); 541 else 542 devTools.hide(); 543 } 544 } 545 546 // FYI the cef browser host also allows things like custom spelling dictionaries and getting navigation entries. 547 548 // JS on init? 549 // JS bindings? 550 // user styles? 551 // navigate to string? (can just use a data uri maybe?) 552 // custom scheme handlers? 553 554 // navigation callbacks to prohibit certain things or move links to new window etc? 555 } 556 557 version(cef) { 558 559 //import core.sync.semaphore; 560 //__gshared Semaphore semaphore; 561 562 /+ 563 Finds the WebViewWidget associated with the given browser, then runs the given code in the gui thread on it. 564 +/ 565 void runOnWebView(RC!cef_browser_t browser, void delegate(WebViewWidget) dg) nothrow { 566 auto wh = cast(NativeWindowHandle) browser.get_host.get_window_handle; 567 568 import core.thread; 569 try { thread_attachThis(); } catch(Exception e) {} 570 571 runInGuiThreadAsync({ 572 if(auto wvp = wh in WebViewWidget.browserMapping) { 573 dg(*wvp); 574 } else { 575 //writeln("not found ", wh, WebViewWidget.browserMapping); 576 } 577 }); 578 } 579 580 class MiniguiCefLifeSpanHandler : CEF!cef_life_span_handler_t { 581 private MiniguiCefClient client; 582 this(MiniguiCefClient client) { 583 this.client = client; 584 } 585 586 override int on_before_popup( 587 RC!cef_browser_t browser, 588 RC!cef_frame_t frame, 589 const(cef_string_t)* target_url, 590 const(cef_string_t)* target_frame_name, 591 cef_window_open_disposition_t target_disposition, 592 int user_gesture, 593 const(cef_popup_features_t)* popupFeatures, 594 cef_window_info_t* windowInfo, 595 cef_client_t** client, 596 cef_browser_settings_t* browser_settings, 597 cef_dictionary_value_t** extra_info, 598 int* no_javascript_access 599 ) { 600 sdpyPrintDebugString("on_before_popup"); 601 if(this.client.openNewWindow is null) 602 return 1; // new windows disabled 603 604 try { 605 int ret; 606 607 import core.thread; 608 try { thread_attachThis(); } catch(Exception e) {} 609 610 // FIXME: change settings here 611 612 runInGuiThread({ 613 ret = 1; 614 scope WebViewWidget delegate(Widget, BrowserSettings) accept = (parent, passed_settings) { 615 ret = 0; 616 if(parent !is null) { 617 auto widget = new WebViewWidget_CEF(this.client, parent, false); 618 (*windowInfo).parent_window = widget.containerWindow.nativeWindowHandle; 619 620 passed_settings.set(browser_settings); 621 622 return widget; 623 } 624 return null; 625 }; 626 this.client.openNewWindow(OpenNewWindowParams(target_url.toGC, accept)); 627 return; 628 }); 629 630 return ret; 631 } catch(Exception e) { 632 return 1; 633 } 634 /+ 635 if(!user_gesture) 636 return 1; // if not created by the user, cancel it; basic popup blocking 637 +/ 638 } 639 override void on_after_created(RC!cef_browser_t browser) { 640 auto handle = cast(NativeWindowHandle) browser.get_host().get_window_handle(); 641 auto ptr = browser.passable; // this adds to the refcount until it gets inside 642 643 import core.thread; 644 try { thread_attachThis(); } catch(Exception e) {} 645 646 // the only reliable key (at least as far as i can tell) is the window handle 647 // so gonna look that up and do the sync mapping that way. 648 runInGuiThreadAsync({ 649 version(Windows) { 650 auto parent = GetParent(handle); 651 } else static if(UsingSimpledisplayX11) { 652 import arsd.simpledisplay : Window; 653 Window root; 654 Window parent; 655 Window ozone; 656 uint c = 0; 657 auto display = XDisplayConnection.get; 658 Window* children; 659 XQueryTree(display, handle, &root, &parent, &children, &c); 660 if(c == 1) 661 ozone = children[0]; 662 XFree(children); 663 } else static assert(0); 664 665 if(auto wvp = parent in WebViewWidget.mapping) { 666 auto wv = *wvp; 667 wv.browserWindow = handle; 668 wv.browserHandle = RC!cef_browser_t(ptr); 669 wv.ozone = ozone ? ozone : handle; 670 671 wv.browserWindowWrapped = new SimpleWindow(wv.ozone); 672 /+ 673 XSelectInput(XDisplayConnection.get, handle, EventMask.FocusChangeMask); 674 675 wv.browserWindowWrapped.onFocusChange = (bool got) { 676 import std.format; 677 sdpyPrintDebugString(format("focus change %s %x", got, wv.browserWindowWrapped.impl.window)); 678 }; 679 +/ 680 681 wv.registerMovementAdditionalWork(); 682 683 WebViewWidget.browserMapping[handle] = wv; 684 } else assert(0); 685 }); 686 } 687 override int do_close(RC!cef_browser_t browser) { 688 browser.runOnWebView((wv) { 689 auto bce = new BrowserClosedEvent(wv); 690 bce.dispatch(); 691 }); 692 return 1; 693 } 694 override void on_before_close(RC!cef_browser_t browser) { 695 /+ 696 import std.stdio; debug writeln("notify"); 697 try 698 semaphore.notify; 699 catch(Exception e) { assert(0); } 700 +/ 701 } 702 } 703 704 class MiniguiLoadHandler : CEF!cef_load_handler_t { 705 override void on_loading_state_change(RC!(cef_browser_t) browser, int isLoading, int canGoBack, int canGoForward) { 706 /+ 707 browser.runOnWebView((WebViewWidget wvw) { 708 wvw.parentWindow.win.title = wvw.browserHandle.get_main_frame.get_url.toGCAndFree; 709 }); 710 +/ 711 } 712 override void on_load_start(RC!(cef_browser_t), RC!(cef_frame_t), cef_transition_type_t) { 713 } 714 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)*) { 715 } 716 override void on_load_end(RC!(cef_browser_t), RC!(cef_frame_t), int) { 717 } 718 } 719 720 class MiniguiDialogHandler : CEF!cef_dialog_handler_t { 721 722 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) { 723 try { 724 auto ptr = callback.passable(); 725 browser.runOnWebView((wv) { 726 getOpenFileName((string name) { 727 auto callback = RC!cef_file_dialog_callback_t(ptr); 728 auto list = libcef.string_list_alloc(); 729 auto item = cef_string_t(name); 730 libcef.string_list_append(list, &item); 731 callback.cont(selected_accept_filter, list); 732 }, null, null, () { 733 auto callback = RC!cef_file_dialog_callback_t(ptr); 734 callback.cancel(); 735 }); 736 }); 737 } catch(Exception e) {} 738 739 return 1; 740 } 741 } 742 743 class MiniguiDownloadHandler : CEF!cef_download_handler_t { 744 override void on_before_download( 745 RC!cef_browser_t browser, 746 RC!cef_download_item_t download_item, 747 const(cef_string_t)* suggested_name, 748 RC!cef_before_download_callback_t callback 749 ) nothrow 750 { 751 // FIXME: different filename and check if exists for overwrite etc 752 auto fn = cef_string_t(cast(wstring)("/home/me/Downloads/"w ~ suggested_name.str[0..suggested_name.length])); 753 sdpyPrintDebugString(fn.toGC); 754 callback.cont(&fn, false); 755 } 756 757 override void on_download_updated( 758 RC!cef_browser_t browser, 759 RC!cef_download_item_t download_item, 760 RC!cef_download_item_callback_t cancel 761 ) nothrow 762 { 763 sdpyPrintDebugString(download_item.get_percent_complete()); 764 // FIXME 765 } 766 } 767 768 class MiniguiKeyboardHandler : CEF!cef_keyboard_handler_t { 769 override int on_pre_key_event( 770 RC!(cef_browser_t) browser, 771 const(cef_key_event_t)* event, 772 XEvent* osEvent, 773 int* isShortcut 774 ) nothrow { 775 /+ 776 sdpyPrintDebugString("---pre---"); 777 sdpyPrintDebugString(event.focus_on_editable_field); 778 sdpyPrintDebugString(event.windows_key_code); 779 sdpyPrintDebugString(event.modifiers); 780 sdpyPrintDebugString(event.unmodified_character); 781 +/ 782 //*isShortcut = 1; 783 return 0; // 1 if handled, which cancels sending it to browser 784 } 785 786 override int on_key_event( 787 RC!(cef_browser_t) browser, 788 const(cef_key_event_t)* event, 789 XEvent* osEvent 790 ) nothrow { 791 /+ 792 sdpyPrintDebugString("---key---"); 793 sdpyPrintDebugString(event.focus_on_editable_field); 794 sdpyPrintDebugString(event.windows_key_code); 795 sdpyPrintDebugString(event.modifiers); 796 +/ 797 return 0; // 1 if handled 798 } 799 } 800 801 class MiniguiDisplayHandler : CEF!cef_display_handler_t { 802 override void on_address_change(RC!(cef_browser_t) browser, RC!(cef_frame_t), const(cef_string_utf16_t)* address) { 803 auto url = address.toGC; 804 browser.runOnWebView((wv) { 805 wv.url = url; 806 }); 807 } 808 override void on_title_change(RC!(cef_browser_t) browser, const(cef_string_utf16_t)* title) { 809 auto t = title.toGC; 810 browser.runOnWebView((wv) { 811 wv.title = t; 812 }); 813 } 814 override void on_favicon_urlchange(RC!(cef_browser_t) browser, cef_string_list_t urls) { 815 string url; 816 auto size = libcef.string_list_size(urls); 817 if(size > 0) { 818 cef_string_t str; 819 libcef.string_list_value(urls, 0, &str); 820 url = str.toGC; 821 822 static class Thing : CEF!cef_download_image_callback_t { 823 RC!cef_browser_t browserHandle; 824 this(RC!cef_browser_t browser) nothrow { 825 this.browserHandle = browser; 826 } 827 override void on_download_image_finished(const(cef_string_t)* image_url, int http_status_code, RC!cef_image_t image) nothrow { 828 int width; 829 int height; 830 if(image.getRawPointer is null) { 831 browserHandle.runOnWebView((wv) { 832 wv.favicon = null; 833 }); 834 return; 835 } 836 837 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); 838 839 if(data.getRawPointer is null || width == 0 || height == 0) { 840 browserHandle.runOnWebView((wv) { 841 wv.favicon = null; 842 }); 843 } else { 844 auto s = data.get_size(); 845 auto buffer = new ubyte[](s); 846 auto got = data.get_data(buffer.ptr, buffer.length, 0); 847 auto slice = buffer[0 .. got]; 848 849 auto img = new TrueColorImage (width, height, slice); 850 851 browserHandle.runOnWebView((wv) { 852 wv.favicon = img; 853 }); 854 } 855 } 856 } 857 858 if(url.length) { 859 auto callback = new Thing(browser); 860 861 browser.get_host().download_image(&str, true, 16, 0, callback.passable); 862 } else { 863 browser.runOnWebView((wv) { 864 wv.favicon = null; 865 }); 866 } 867 } 868 869 browser.runOnWebView((wv) { 870 wv.favicon_url = url; 871 }); 872 } 873 override void on_fullscreen_mode_change(RC!(cef_browser_t) browser, int) { 874 } 875 override int on_tooltip(RC!(cef_browser_t) browser, cef_string_utf16_t*) { 876 return 0; 877 } 878 override void on_status_message(RC!(cef_browser_t) browser, const(cef_string_utf16_t)* msg) { 879 auto status = msg.toGC; 880 browser.runOnWebView((wv) { 881 wv.status = status; 882 }); 883 } 884 override void on_loading_progress_change(RC!(cef_browser_t) browser, double progress) { 885 // progress is from 0.0 to 1.0 886 browser.runOnWebView((wv) { 887 wv.loadingProgress = cast(int) (progress * 100); 888 }); 889 } 890 override int on_console_message(RC!(cef_browser_t), cef_log_severity_t, const(cef_string_utf16_t)*, const(cef_string_utf16_t)*, int) { 891 return 0; // 1 means to suppress it being automatically output 892 } 893 override int on_auto_resize(RC!(cef_browser_t), const(cef_size_t)*) { 894 return 0; 895 } 896 override int on_cursor_change(RC!(cef_browser_t), cef_cursor_handle_t, cef_cursor_type_t, const(cef_cursor_info_t)*) { 897 return 0; 898 } 899 } 900 901 class MiniguiRequestHandler : CEF!cef_request_handler_t { 902 override int on_before_browse(RC!(cef_browser_t), RC!(cef_frame_t), RC!(cef_request_t), int, int) nothrow { 903 return 0; 904 } 905 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 { 906 return 0; 907 } 908 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 { 909 return null; 910 } 911 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 { 912 // this is for http basic auth popup..... 913 return 0; 914 } 915 override int on_quota_request(RC!(cef_browser_t), const(cef_string_utf16_t)*, long, RC!(cef_callback_t)) nothrow { 916 return 0; 917 } 918 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 { 919 return 0; 920 } 921 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 { 922 return 0; 923 } 924 override void on_plugin_crashed(RC!(cef_browser_t), const(cef_string_utf16_t)*) nothrow { 925 926 } 927 override void on_render_view_ready(RC!(cef_browser_t) p) nothrow { 928 929 } 930 override void on_render_process_terminated(RC!(cef_browser_t), cef_termination_status_t) nothrow { 931 932 } 933 override void on_document_available_in_main_frame(RC!(cef_browser_t) browser) nothrow { 934 browser.runOnWebView(delegate(wv) { 935 wv.executeJavascript("console.log('here');"); 936 }); 937 938 } 939 } 940 941 class MiniguiFocusHandler : CEF!cef_focus_handler_t { 942 override void on_take_focus(RC!(cef_browser_t) browser, int next) nothrow { 943 browser.runOnWebView(delegate(wv) { 944 Widget f; 945 if(next) { 946 f = Window.getFirstFocusable(wv.parentWindow); 947 } else { 948 foreach(w; &wv.parentWindow.focusableWidgets) { 949 if(w is wv) 950 break; 951 f = w; 952 } 953 } 954 if(f) 955 f.focus(); 956 }); 957 } 958 override int on_set_focus(RC!(cef_browser_t) browser, cef_focus_source_t source) nothrow { 959 /+ 960 browser.runOnWebView((ev) { 961 ev.focus(); // even this can steal focus from other parts of my application! 962 }); 963 +/ 964 //sdpyPrintDebugString("setting"); 965 966 return 1; // otherwise, cancel because this bullshit tends to steal focus from other applications and i never, ever, ever want that to happen. 967 // seems to happen because of race condition in it getting a focus event and then stealing the focus from the parent 968 // even though things work fine if i always cancel except 969 // it still keeps the decoration assuming focus though even though it doesn't have it which is kinda fucked up but meh 970 // it also breaks its own pop up menus and drop down boxes to allow this! wtf 971 } 972 override void on_got_focus(RC!(cef_browser_t) browser) nothrow { 973 browser.runOnWebView((ev) { 974 // this sometimes steals from the app too but it is relatively acceptable 975 // steals when i mouse in from the side of the window quickly, but still 976 // i want the minigui state to match so i'll allow it 977 ev.focus(); 978 }); 979 } 980 } 981 982 class MiniguiCefClient : CEF!cef_client_t { 983 984 void delegate(scope OpenNewWindowParams) openNewWindow; 985 986 MiniguiCefLifeSpanHandler lsh; 987 MiniguiLoadHandler loadHandler; 988 MiniguiDialogHandler dialogHandler; 989 MiniguiDisplayHandler displayHandler; 990 MiniguiDownloadHandler downloadHandler; 991 MiniguiKeyboardHandler keyboardHandler; 992 MiniguiFocusHandler focusHandler; 993 MiniguiRequestHandler requestHandler; 994 this(void delegate(scope OpenNewWindowParams) openNewWindow) { 995 this.openNewWindow = openNewWindow; 996 lsh = new MiniguiCefLifeSpanHandler(this); 997 loadHandler = new MiniguiLoadHandler(); 998 dialogHandler = new MiniguiDialogHandler(); 999 displayHandler = new MiniguiDisplayHandler(); 1000 downloadHandler = new MiniguiDownloadHandler(); 1001 keyboardHandler = new MiniguiKeyboardHandler(); 1002 focusHandler = new MiniguiFocusHandler(); 1003 requestHandler = new MiniguiRequestHandler(); 1004 } 1005 1006 override cef_audio_handler_t* get_audio_handler() { 1007 return null; 1008 } 1009 override cef_context_menu_handler_t* get_context_menu_handler() { 1010 return null; 1011 } 1012 override cef_dialog_handler_t* get_dialog_handler() { 1013 return dialogHandler.returnable; 1014 } 1015 override cef_display_handler_t* get_display_handler() { 1016 return displayHandler.returnable; 1017 } 1018 override cef_download_handler_t* get_download_handler() { 1019 return downloadHandler.returnable; 1020 } 1021 override cef_drag_handler_t* get_drag_handler() { 1022 return null; 1023 } 1024 override cef_find_handler_t* get_find_handler() { 1025 return null; 1026 } 1027 override cef_focus_handler_t* get_focus_handler() { 1028 return focusHandler.returnable; 1029 } 1030 override cef_jsdialog_handler_t* get_jsdialog_handler() { 1031 // needed for alert etc. 1032 return null; 1033 } 1034 override cef_keyboard_handler_t* get_keyboard_handler() { 1035 // this can handle keyboard shortcuts etc 1036 return keyboardHandler.returnable; 1037 } 1038 override cef_life_span_handler_t* get_life_span_handler() { 1039 return lsh.returnable; 1040 } 1041 override cef_load_handler_t* get_load_handler() { 1042 return loadHandler.returnable; 1043 } 1044 override cef_render_handler_t* get_render_handler() { 1045 // this thing might work for an off-screen thing 1046 // like to an image or to a video stream maybe 1047 // 1048 // might be useful to have it render here then send it over too for remote X sharing a process 1049 return null; 1050 } 1051 override cef_request_handler_t* get_request_handler() { 1052 return requestHandler.returnable; 1053 } 1054 override int on_process_message_received(RC!cef_browser_t, RC!cef_frame_t, cef_process_id_t, RC!cef_process_message_t) { 1055 return 0; // return 1 if you can actually handle the message 1056 } 1057 override cef_frame_handler_t* get_frame_handler() nothrow { 1058 return null; 1059 } 1060 override cef_print_handler_t* get_print_handler() nothrow { 1061 return null; 1062 } 1063 1064 } 1065 } 1066 1067 class BrowserClosedEvent : Event { 1068 enum EventString = "browserclosed"; 1069 1070 this(Widget target) { super(EventString, target); } 1071 override bool cancelable() const { return false; } 1072 } 1073 1074 /+ 1075 pragma(mangle, "_ZN12CefWindowX115FocusEv") 1076 //pragma(mangle, "_ZN3x116XProto13SetInputFocusERKNS_20SetInputFocusRequestE") 1077 extern(C++) 1078 export void _ZN12CefWindowX115FocusEv() { 1079 sdpyPrintDebugString("OVERRIDDEN"); 1080 } 1081 +/