diff --git a/addons/gdcef/demos/HelloCEF/Control.gd b/addons/gdcef/demos/HelloCEF/Control.gd index 3db9081..a36efa9 100644 --- a/addons/gdcef/demos/HelloCEF/Control.gd +++ b/addons/gdcef/demos/HelloCEF/Control.gd @@ -181,16 +181,16 @@ func _ready(): # Left browser is displaying the first webpage with a 3D scene, we are # enabling webgl. Other default configuration are: - # {"frame_rate", 30} - # {"javascript", true} - # {"javascript_close_windows", false} - # {"javascript_access_clipboard", false} - # {"javascript_dom_paste", false} - # {"image_loading", true} - # {"databases", true} - # {"webgl", true} - var left = $CEF.create_browser(pages[4], $TextRectLeft, {}) - var right = $CEF.create_browser(pages[0], $TextRectRight, {}) + # {"frame_rate": 30} + # {"javascript": true} + # {"javascript_close_windows": false} + # {"javascript_access_clipboard": false} + # {"javascript_dom_paste": false} + # {"image_loading": true} + # {"databases": true} + # {"webgl": true} + var left = $CEF.create_browser(pages[4], $TextRectLeft, {"javascript": true, "webgl": true}) + var right = $CEF.create_browser(pages[0], $TextRectRight, {"javascript": true, "webgl": true}) left.name = "left" right.name = "right" diff --git a/addons/gdcef/demos/JS/Control.gd b/addons/gdcef/demos/JS/Control.gd new file mode 100644 index 0000000..35465f8 --- /dev/null +++ b/addons/gdcef/demos/JS/Control.gd @@ -0,0 +1,218 @@ +# ============================================================================== +# Basic application made in HTML/JS/CSS with interaction with Godot. +# ============================================================================== +extends Control + +# ============================================================================== +# CEF variables +# ============================================================================== +const BROWSER_NAME = "player_stats" +@onready var mouse_pressed: bool = false + +# ============================================================================== +# Variables for character stats +# ============================================================================== +@onready var player_name: String = "Anonymous" +@onready var weapon: String = "sword" +@onready var xp: int = 0 +@onready var level: int = 1 + +# ============================================================================== +# Initial character configuration +# ============================================================================== +func _ready(): + initialize_cef() +# expose_methods() + pass + +# ============================================================================== +# Expose methods to JavaScript via CEF +# ============================================================================== +#func expose_methods(): +# pass + +# ============================================================================== +# Change character's weapon +# ============================================================================== +func change_weapon(new_weapon: String): + print("Weapon changed to: ", new_weapon) + weapon = new_weapon + _update_character_stats() + pass + +# ============================================================================== +# Set character's name +# ============================================================================== +func set_character_name(new_name: String): + print("New name: ", new_name) + player_name = new_name + _update_character_stats() + pass + +# ============================================================================== +# Modify XP (can be positive or negative) +# ============================================================================== +func modify_xp(xp_change: int): + xp += xp_change + _level_up_check() + _update_character_stats() + pass + +# ============================================================================== +# Check for level up +# ============================================================================== +func _level_up_check(): + var previous_level = level + level = 1 + floor(xp / 100) # Simple progression example + + if level > previous_level: + print("Level up! New level: ", level) + pass + +# ============================================================================== +# Update character statistics +# ============================================================================== +func _update_character_stats(): + var character_info = { + "name": player_name, + "weapon": weapon, + "xp": xp, + "level": level + } + print("Character update: ", character_info) + pass + +# ============================================================================== +# Optional method to get complete character state +# ============================================================================== +func get_character_state() -> Dictionary: + return { + "name": player_name, + "weapon": weapon, + "xp": xp, + "level": level + } + +# ============================================================================== +# CEF Callback when a page has ended to load with success. +# ============================================================================== +func _on_page_loaded(browser): + print("The browser " + browser.name + " has loaded " + browser.get_url()) + $CEF.register_method(self, browser, "change_weapon") + $CEF.register_method(self, browser, "set_character_name") + $CEF.register_method(self, browser, "modify_xp") + pass + +# ============================================================================== +# Callback when a page has ended to load with failure. +# Display a load error message using a data: URI. +# ============================================================================== +func _on_page_failed_loading(_err_code, _err_msg, browser): + $AcceptDialog.title = "Alert!" + $AcceptDialog.dialog_text = "The browser " + browser.name + " did not load " + browser.get_url() + $AcceptDialog.popup_centered(Vector2(0, 0)) + $AcceptDialog.show() + pass + +# ============================================================================== +# Split the browser vertically to display two browsers (aka tabs) rendered in +# two separate textures. +# ============================================================================== +func initialize_cef(): + + ### CEF + + if !$CEF.initialize({"incognito": true, "locale": "en-US", + "remote_debugging_port": 7777, "remote_allow_origin": "*"}): + push_error("Failed initializing CEF") + get_tree().quit() + else: + push_warning("CEF version: " + $CEF.get_full_version()) + pass + + ### Browser + + var browser = $CEF.create_browser("", $TextureRect, {"javascript": true}) + browser.name = BROWSER_NAME + browser.connect("on_page_loaded", _on_page_loaded) + browser.connect("on_page_failed_loading", _on_page_failed_loading) + browser.resize($TextureRect.get_size()) + browser.load_data_uri(_load_html_file(), "text/html") + pass + +# ============================================================================== +# Load the HTML file containing the JavaScript code +# ============================================================================== +func _load_html_file(): + var file = FileAccess.open("res://character-management-ui.html", FileAccess.READ) + var content = file.get_as_text() + file.close() + return content + +# ============================================================================== +# Get the browser node interacting with the JavaScript code. +# ============================================================================== +func get_browser(): + var browser = $CEF.get_node(BROWSER_NAME) + if browser == null: + push_error("Failed getting Godot node '" + name + "'") + get_tree().quit() + return browser + +# ============================================================================== +# Get mouse events and broadcast them to CEF +# ============================================================================== +func _on_TextureRect_gui_input(event: InputEvent): + var current_browser = get_browser() + if event is InputEventMouseButton: + if event.button_index == MOUSE_BUTTON_WHEEL_UP: + current_browser.set_mouse_wheel_vertical(2) + elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN: + current_browser.set_mouse_wheel_vertical(-2) + elif event.button_index == MOUSE_BUTTON_LEFT: + mouse_pressed = event.pressed + if mouse_pressed: + current_browser.set_mouse_left_down() + else: + current_browser.set_mouse_left_up() + elif event.button_index == MOUSE_BUTTON_RIGHT: + mouse_pressed = event.pressed + if mouse_pressed: + current_browser.set_mouse_right_down() + else: + current_browser.set_mouse_right_up() + else: + mouse_pressed = event.pressed + if mouse_pressed: + current_browser.set_mouse_middle_down() + else: + current_browser.set_mouse_middle_up() + elif event is InputEventMouseMotion: + if mouse_pressed: + current_browser.set_mouse_left_down() + current_browser.set_mouse_moved(event.position.x, event.position.y) + pass + +# ============================================================================== +# Make the CEF browser reacts from keyboard events. +# ============================================================================== +func _input(event): + if event is InputEventKey: + get_browser().set_key_pressed( + event.unicode if event.unicode != 0 else event.keycode, + event.pressed, event.shift_pressed, event.alt_pressed, + event.is_command_or_control_pressed()) + pass + +# ============================================================================== +# Windows has resized +# ============================================================================== +func _on_texture_rect_resized(): + get_browser().resize($Panel/VBox/TextureRect.get_size()) + pass + +# ============================================================================== +# CEF is implicitly updated by this function. +# ============================================================================== +func _process(_delta): + pass diff --git a/addons/gdcef/demos/JS/Control.tscn b/addons/gdcef/demos/JS/Control.tscn new file mode 100644 index 0000000..8e6ca1c --- /dev/null +++ b/addons/gdcef/demos/JS/Control.tscn @@ -0,0 +1,28 @@ +[gd_scene load_steps=2 format=3 uid="uid://ckewd5tjvl4rj"] + +[ext_resource type="Script" path="res://Control.gd" id="2"] + +[node name="Control" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("2") + +[node name="TextureRect" type="TextureRect" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="CEF" type="GDCef" parent="."] + +[node name="Timer" type="Timer" parent="."] +wait_time = 10.0 +autostart = true + +[connection signal="gui_input" from="TextureRect" to="." method="_on_TextureRect_gui_input"] diff --git a/addons/gdcef/demos/JS/character-management-ui.html b/addons/gdcef/demos/JS/character-management-ui.html new file mode 100644 index 0000000..2d2b18d --- /dev/null +++ b/addons/gdcef/demos/JS/character-management-ui.html @@ -0,0 +1,220 @@ + + + + + + Character Management UI + + + + +
+
+

Character Management UI

+
+ +
+

Name of the character

+
+ + +
+
+ +
+

Weapon Selection

+
+ + + + + + +
+
+ +
+

XP Management

+
+ + + +
+
+ +
+
+ + + + + \ No newline at end of file diff --git a/addons/gdcef/demos/JS/default_env.tres b/addons/gdcef/demos/JS/default_env.tres new file mode 100644 index 0000000..dfc5189 --- /dev/null +++ b/addons/gdcef/demos/JS/default_env.tres @@ -0,0 +1,7 @@ +[gd_resource type="Environment" load_steps=2 format=3 uid="uid://btpjrvhyjvhj2"] + +[sub_resource type="Sky" id="1"] + +[resource] +background_mode = 2 +sky = SubResource("1") diff --git a/addons/gdcef/demos/JS/icon.png b/addons/gdcef/demos/JS/icon.png new file mode 100644 index 0000000..1acbade Binary files /dev/null and b/addons/gdcef/demos/JS/icon.png differ diff --git a/addons/gdcef/demos/JS/project.godot b/addons/gdcef/demos/JS/project.godot new file mode 100644 index 0000000..322cd70 --- /dev/null +++ b/addons/gdcef/demos/JS/project.godot @@ -0,0 +1,25 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=5 + +[application] + +config/name="JS Bindings" +run/main_scene="res://Control.tscn" +config/features=PackedStringArray("4.3") +config/icon="res://icon.png" + +[physics] + +common/enable_pause_aware_picking=true + +[rendering] + +renderer/rendering_method="gl_compatibility" +environment/defaults/default_environment="res://default_env.tres" diff --git a/addons/gdcef/demos/README.md b/addons/gdcef/demos/README.md index 5e54933..878ca2a 100644 --- a/addons/gdcef/demos/README.md +++ b/addons/gdcef/demos/README.md @@ -33,3 +33,9 @@ A demo showing a 3D GUI with a single CEF browser tab showing a radio website. T This demo is based on the asset library: https://godotengine.org/asset-library/asset/127 ![Screenshot](3D/icon.png) + +### Demo 03: CEF browser in 2D with JS bindings + +A demo showing a 2D GUI made in HTML/CSS/JavaScript and loaded in a CEF browser. The demo shows how to bind Godot methods to JavaScript functions using JS Binder. + +![Screenshot](JS/icon.png) diff --git a/addons/gdcef/gdcef/SConstruct b/addons/gdcef/gdcef/SConstruct index b327692..73fd195 100644 --- a/addons/gdcef/gdcef/SConstruct +++ b/addons/gdcef/gdcef/SConstruct @@ -232,7 +232,7 @@ env.Append(CPPPATH=['src/']) env.Append(RPATH=[env['cef_artifacts_folder'][2:-2]]) # Compile the library -sources = ['src/helper_files.cpp', 'src/browser_io.cpp', 'src/gdcef.cpp', 'src/gdbrowser.cpp', 'src/register_types.cpp'] +sources = ['src/helper_files.cpp', 'src/browser_io.cpp', 'src/gdcef.cpp', 'src/gdbrowser.cpp', 'src/register_types.cpp', 'src/godot_js_binder.cpp'] # sources = Glob('src/*.cpp') library = env.SharedLibrary(target=target_path + '/' + target_library, source=sources) Default(library) diff --git a/addons/gdcef/gdcef/src/gdbrowser.cpp b/addons/gdcef/gdcef/src/gdbrowser.cpp index 0ec824f..9b999ec 100644 --- a/addons/gdcef/gdcef/src/gdbrowser.cpp +++ b/addons/gdcef/gdcef/src/gdbrowser.cpp @@ -37,6 +37,10 @@ # include #endif +#ifndef CALL_GODOT_METHOD +# define CALL_GODOT_METHOD "callGodotMethod" +#endif + //------------------------------------------------------------------------------ // Visit the html content of the current page. class Visitor: public CefStringVisitor @@ -886,4 +890,56 @@ void GDBrowserView::onDownloadUpdated( // Emit signal for Godot script emit_signal( "on_download_updated", godot::String(file.c_str()), percentage, this); +} + +//------------------------------------------------------------------------------ +bool GDBrowserView::Impl::OnProcessMessageReceived( + CefRefPtr browser, + CefRefPtr frame, + CefProcessId source_process, + CefRefPtr message) +{ + if (message->GetName() != CALL_GODOT_METHOD) + { + return false; + } + + CefRefPtr args = message->GetArgumentList(); + if (args->GetSize() < 1) + { + return false; + } + + // Get method name from first argument + CefString method_name = args->GetString(0); + + // Convert CEF arguments to Godot arguments + godot::Array godot_args; + for (size_t i = 1; i < args->GetSize(); ++i) + { + switch (args->GetType(i)) + { + case VTYPE_BOOL: + godot_args.push_back(args->GetBool(i)); + break; + case VTYPE_INT: + godot_args.push_back(args->GetInt(i)); + break; + case VTYPE_DOUBLE: + godot_args.push_back(args->GetDouble(i)); + break; + case VTYPE_STRING: + godot_args.push_back( + godot::String(args->GetString(i).ToString().c_str())); + break; + default: + // For unsupported types, pass as string + godot_args.push_back( + godot::String(args->GetString(i).ToString().c_str())); + } + } + + // Call the Godot method + m_owner.call_deferred(method_name.ToString().c_str(), godot_args); + return true; } \ No newline at end of file diff --git a/addons/gdcef/gdcef/src/gdbrowser.hpp b/addons/gdcef/gdcef/src/gdbrowser.hpp index 4cb427e..3bfbc8d 100644 --- a/addons/gdcef/gdcef/src/gdbrowser.hpp +++ b/addons/gdcef/gdcef/src/gdbrowser.hpp @@ -189,6 +189,12 @@ class GDBrowserView: public godot::Node return this; } + virtual bool + OnProcessMessageReceived(CefRefPtr browser, + CefRefPtr frame, + CefProcessId source_process, + CefRefPtr message) override; + private: // CefRenderHandler interfaces // --------------------------------------------------------------------- @@ -285,7 +291,7 @@ class GDBrowserView: public godot::Node { } - private: // CefBrowserProcessHandler interfaces + private: // CefLifeSpanHandler interfaces virtual bool OnBeforePopup(CefRefPtr browser, CefRefPtr frame, diff --git a/addons/gdcef/gdcef/src/gdcef.cpp b/addons/gdcef/gdcef/src/gdcef.cpp index 788693b..1ba1e8d 100644 --- a/addons/gdcef/gdcef/src/gdcef.cpp +++ b/addons/gdcef/gdcef/src/gdcef.cpp @@ -26,6 +26,7 @@ //------------------------------------------------------------------------------ #include "gdcef.hpp" #include "gdbrowser.hpp" +#include "godot_js_binder.hpp" #include "helper_config.hpp" #include "helper_files.hpp" @@ -122,13 +123,15 @@ void GDCef::_bind_methods() GDCEF_DEBUG(""); using namespace godot; - ClassDB::bind_method(D_METHOD("initialize"), &GDCef::initialize); + ClassDB::bind_method(D_METHOD("initialize", "config"), &GDCef::initialize); ClassDB::bind_method(D_METHOD("get_full_version"), &GDCef::version); ClassDB::bind_method(D_METHOD("get_version_part"), &GDCef::versionPart); ClassDB::bind_method(D_METHOD("create_browser"), &GDCef::createBrowser); ClassDB::bind_method(D_METHOD("shutdown"), &GDCef::shutdown); ClassDB::bind_method(D_METHOD("is_alive"), &GDCef::isAlive); ClassDB::bind_method(D_METHOD("get_error"), &GDCef::getError); + ClassDB::bind_method(D_METHOD("register_method"), + &GDCef::registerGodotMethod); } //------------------------------------------------------------------------------ @@ -581,6 +584,61 @@ GDBrowserView* GDCef::createBrowser(godot::String const& url, return browser; } +//------------------------------------------------------------------------------ +//! \brief Method to register Godot methods in the JavaScript context +//! \param [in] godot_object The Godot object containing the method to register +//! \param [in] method_name The name of the method to register +//------------------------------------------------------------------------------ +void GDCef::registerGodotMethod(godot::Object* godot_object, + GDBrowserView* browser, + godot::String const& method_name) +{ + if (godot_object == nullptr) + { + GDCEF_ERROR("Invalid Godot object passed to registerGodotMethod"); + return; + } + + if (browser == nullptr) + { + GDCEF_ERROR("Invalid browser passed to registerGodotMethod"); + return; + } + + // Check that the method exists in the Godot object. + if (!godot_object->has_method(method_name)) + { + GDCEF_ERROR("Method " << method_name.utf8().get_data() + << " does not exist in the Godot object"); + return; + } + + // Handle private GDScript methods (starting with "_"). + // Remove the "_" initial for the JavaScript method name + godot::String js_method_name = method_name; + if (method_name.begins_with("_")) + { + js_method_name = method_name.substr(1); + } + + // Inject the JavaScript code to create the function + std::string js_code = + "window.godot." + std::string(js_method_name.utf8().get_data()) + + " = function() { return window.godot.callGodotMethod('" + + std::string(js_method_name.utf8().get_data()) + "', ...arguments); };"; + + // Get the active browser from the caller + godot::Node* caller = godot::Object::cast_to(godot_object); + if (caller == nullptr) + { + GDCEF_ERROR("Caller object is not a Node"); + return; + } + + // Find the browser in the caller's children + browser->executeJavaScript(js_code.c_str()); +} + //------------------------------------------------------------------------------ void GDCef::Impl::OnAfterCreated(CefRefPtr /*browser*/) { diff --git a/addons/gdcef/gdcef/src/gdcef.hpp b/addons/gdcef/gdcef/src/gdcef.hpp index 30337d6..6e3cf6a 100644 --- a/addons/gdcef/gdcef/src/gdcef.hpp +++ b/addons/gdcef/gdcef/src/gdcef.hpp @@ -108,6 +108,13 @@ class GDCef: public godot::Node // ------------------------------------------------------------------------- bool isAlive(); + // ------------------------------------------------------------------------- + //! \brief Register a Godot method in the JavaScript context + // ------------------------------------------------------------------------- + void registerGodotMethod(godot::Object* godot_object, + GDBrowserView* browser, + const godot::String& method_name); + // ------------------------------------------------------------------------- //! \brief Return the latest error. // ------------------------------------------------------------------------- @@ -273,14 +280,14 @@ class GDCef: public godot::Node //! \param[in] texture_rect the texture container in where to paint the CEF //! output. \param[in] config dictionary of Browser config with default //! values: - //! - {"frame_rate", 30} - //! - {"javascript", STATE_ENABLED} - //! - {"javascript_close_windows", STATE_DISABLED} - //! - {"javascript_access_clipboard", STATE_DISABLED} - //! - {"javascript_dom_paste", STATE_DISABLED} - //! - {"image_loading", STATE_ENABLED} - //! - {"databases", STATE_ENABLED} - //! - {"webgl", STATE_ENABLED} + //! - {"frame_rate": 30} + //! - {"javascript": STATE_ENABLED} + //! - {"javascript_close_windows": STATE_DISABLED} + //! - {"javascript_access_clipboard": STATE_DISABLED} + //! - {"javascript_dom_paste": STATE_DISABLED} + //! - {"image_loading": STATE_ENABLED} + //! - {"databases": STATE_ENABLED} + //! - {"webgl": STATE_ENABLED} //! Wherer STATE_DISABLED / STATE_ENABLED == false / true //! \return the address of the newly created browser (or nullptr in case of //! error). diff --git a/addons/gdcef/gdcef/src/godot_js_binder.cpp b/addons/gdcef/gdcef/src/godot_js_binder.cpp new file mode 100644 index 0000000..5f7900e --- /dev/null +++ b/addons/gdcef/gdcef/src/godot_js_binder.cpp @@ -0,0 +1,174 @@ +//***************************************************************************** +// MIT License +// +// Copyright (c) 2022 Alain Duron +// Copyright (c) 2022 Quentin Quadrat +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +//***************************************************************************** + +#include "godot_js_binder.hpp" + +static CefRefPtr GodotToV8(const godot::Variant& godot_value); +static godot::Variant V8ToGodot(CefRefPtr v8_value); + +//------------------------------------------------------------------------------ +bool GodotMethodInvoker::Execute(const CefString& name, + CefRefPtr object, + const CefV8ValueList& arguments, + CefRefPtr& retval, + CefString& exception) +{ + // Convert arguments JavaScript to Godot arguments + godot::Array godot_args; + for (const auto& arg : arguments) + { + godot_args.append(V8ToGodot(arg)); + } + + // Call the Godot method + godot::Variant result = m_godot_object->call(m_method_name, godot_args); + + // Convert Godot result to V8 value + retval = GodotToV8(result); + return true; +} + +//------------------------------------------------------------------------------ +static CefRefPtr GodotToV8(const godot::Variant& godot_value) +{ + switch (godot_value.get_type()) + { + case godot::Variant::BOOL: + return CefV8Value::CreateBool(godot_value.operator bool()); + + case godot::Variant::INT: + return CefV8Value::CreateInt(godot_value.operator int64_t()); + + case godot::Variant::FLOAT: + return CefV8Value::CreateDouble(godot_value.operator double()); + + case godot::Variant::STRING: + return CefV8Value::CreateString(CefString( + godot_value.operator godot::String().utf8().get_data())); + + case godot::Variant::ARRAY: { + godot::Array godot_array = godot_value.operator godot::Array(); + CefRefPtr js_array = + CefV8Value::CreateArray(godot_array.size()); + + for (int i = 0; i < godot_array.size(); ++i) + { + js_array->SetValue(i, GodotToV8(godot_array[i])); + } + return js_array; + } + + case godot::Variant::DICTIONARY: { + godot::Dictionary godot_dict = + godot_value.operator godot::Dictionary(); + CefRefPtr js_object = + CefV8Value::CreateObject(nullptr, nullptr); + + for (int i = 0; i < godot_dict.size(); ++i) + { + godot::Variant key = godot_dict.keys()[i]; + godot::Variant value = godot_dict.values()[i]; + + js_object->SetValue( + key.operator godot::String().utf8().get_data(), + GodotToV8(value), + V8_PROPERTY_ATTRIBUTE_NONE); + } + return js_object; + } + + default: + return CefV8Value::CreateNull(); + } +} + +//------------------------------------------------------------------------------ +static godot::Variant V8ToGodot(CefRefPtr v8_value) +{ + if (!v8_value.get()) + { + return godot::Variant(); + } + + if (v8_value->IsNull() || v8_value->IsUndefined()) + { + return godot::Variant(); + } + + if (v8_value->IsBool()) + { + return godot::Variant(v8_value->GetBoolValue()); + } + + if (v8_value->IsInt()) + { + return godot::Variant(v8_value->GetIntValue()); + } + + if (v8_value->IsDouble()) + { + return godot::Variant(v8_value->GetDoubleValue()); + } + + if (v8_value->IsString()) + { + return godot::Variant( + godot::String(v8_value->GetStringValue().ToString().c_str())); + } + + if (v8_value->IsArray()) + { + godot::Array godot_array; + int length = v8_value->GetArrayLength(); + + for (int i = 0; i < length; ++i) + { + godot_array.append(V8ToGodot(v8_value->GetValue(i))); + } + + return godot_array; + } + + if (v8_value->IsObject()) + { + godot::Dictionary godot_dict; + std::vector keys; + v8_value->GetKeys(keys); + + for (const auto& key : keys) + { + CefRefPtr property = v8_value->GetValue(key); + if (property.get()) + { + godot_dict[godot::String(key.ToString().c_str())] = + V8ToGodot(property); + } + } + + return godot_dict; + } + + return godot::Variant(); +} \ No newline at end of file diff --git a/addons/gdcef/gdcef/src/godot_js_binder.hpp b/addons/gdcef/gdcef/src/godot_js_binder.hpp new file mode 100644 index 0000000..5e9ada4 --- /dev/null +++ b/addons/gdcef/gdcef/src/godot_js_binder.hpp @@ -0,0 +1,64 @@ +//***************************************************************************** +// MIT License +// +// Copyright (c) 2022 Alain Duron +// Copyright (c) 2022 Quentin Quadrat +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +//***************************************************************************** + +#ifndef GDCEF_GODOT_JS_BINDER_HPP +#define GDCEF_GODOT_JS_BINDER_HPP + +// Godot 4 +#include +#include +#include + +// Chromium Embedded Framework +#include "cef_v8.h" + +// **************************************************************************** +//! \class GodotMethodInvoker +//! \brief Class to handle binding between JavaScript and GDScript methods +// **************************************************************************** +class GodotMethodInvoker: public CefV8Handler +{ +public: + + GodotMethodInvoker(godot::Object* obj, const godot::StringName& method) + : m_godot_object(obj), m_method_name(method) + { + } + + bool Execute(const CefString& name, + CefRefPtr object, + const CefV8ValueList& arguments, + CefRefPtr& retval, + CefString& exception) override; + + IMPLEMENT_REFCOUNTING(GodotMethodInvoker); + +private: + + godot::Object* m_godot_object; + godot::StringName m_method_name; +}; + +#endif // GDCEF_GODOT_JS_BINDER_HPP \ No newline at end of file diff --git a/addons/gdcef/gdcef/src/register_types.cpp b/addons/gdcef/gdcef/src/register_types.cpp index 0cebe34..00f4d91 100644 --- a/addons/gdcef/gdcef/src/register_types.cpp +++ b/addons/gdcef/gdcef/src/register_types.cpp @@ -27,6 +27,7 @@ #include "gdbrowser.hpp" #include "gdcef.hpp" +#include "godot_js_binder.hpp" #include "helper_log.hpp" #include #include diff --git a/addons/gdcef/render_process/src/render_process.cpp b/addons/gdcef/render_process/src/render_process.cpp index 4792d31..54a9c3a 100644 --- a/addons/gdcef/render_process/src/render_process.cpp +++ b/addons/gdcef/render_process/src/render_process.cpp @@ -25,6 +25,7 @@ #include "render_process.hpp" +//------------------------------------------------------------------------------ #define DEBUG_RENDER_PROCESS(txt) \ { \ std::stringstream ss; \ @@ -33,6 +34,7 @@ std::cout << ss.str() << std::endl; \ } +//------------------------------------------------------------------------------ #define DEBUG_BROWSER_PROCESS(txt) \ { \ std::stringstream ss; \ @@ -41,90 +43,102 @@ std::cout << ss.str() << std::endl; \ } -//------------------------------------------------------------------------------ -RenderProcess::~RenderProcess() -{ - DEBUG_RENDER_PROCESS(""); -} +#ifndef CALL_GODOT_METHOD +# define CALL_GODOT_METHOD "callGodotMethod" +#endif -#if 0 //------------------------------------------------------------------------------ -void RenderProcess::OnContextInitialized() +bool GodotMethodHandler::Execute(const CefString& name, + CefRefPtr object, + const CefV8ValueList& arguments, + CefRefPtr& retval, + CefString& exception) { - CEF_REQUIRE_UI_THREAD(); - DEBUG_RENDER_PROCESS(""); + // Function does not exist. + if (name != CALL_GODOT_METHOD) + { + return false; + } + + // No browser created, we cannot call the method. + if (m_browser == nullptr) + { + exception = "Browser pointer at NULL"; + return true; + } - // Information used when creating the native window. - CefWindowInfo window_info; + // Check that there is at least the method name as argument. + if (arguments.size() < 1 || !arguments[0]->IsString()) + { + exception = "First argument must be the method name"; + return false; + } + + // Create and configure the IPC message to the main process. + CefRefPtr msg = + CefProcessMessage::Create(CALL_GODOT_METHOD); + CefRefPtr args = msg->GetArgumentList(); -# if defined(OS_WIN) - // On Windows we need to specify certain flags that will be passed to - // CreateWindowEx(). - window_info.SetAsPopup(NULL, "CEF"); -# endif + // Add the method name as first argument. + args->SetString(0, arguments[0]->GetStringValue()); - // GDCefBrowser implements browser-level callbacks. - DEBUG_RENDER_PROCESS("Create client handler"); - CefRefPtr handler(new GDCefBrowser()); + // Add the arguments directly from V8 types. + for (size_t i = 1; i < arguments.size(); ++i) + { + auto arg = arguments[i]; + if (arg->IsBool()) + { + args->SetBool(i, arg->GetBoolValue()); + } + else if (arg->IsInt()) + { + args->SetInt(i, arg->GetIntValue()); + } + else if (arg->IsDouble()) + { + args->SetDouble(i, arg->GetDoubleValue()); + } + else if (arg->IsString()) + { + args->SetString(i, arg->GetStringValue()); + } + else + { + // For other types, convert them to string + args->SetString(i, arg->GetStringValue()); + } + } - // Specify CEF browser settings here. - CefBrowserSettings browser_settings; + // Send the message to the main process + m_browser->GetMainFrame()->SendProcessMessage(PID_BROWSER, msg); + retval = CefV8Value::CreateBool(true); - // Create the first browser window. - DEBUG_RENDER_PROCESS("Create the browser"); - CefBrowserHost::CreateBrowser( - window_info, handler.get(), "", browser_settings, nullptr, nullptr); + return true; } -#endif //------------------------------------------------------------------------------ +// TODO Faire OnContextReleased ? void RenderProcess::OnContextCreated(CefRefPtr browser, CefRefPtr frame, CefRefPtr context) { - DEBUG_RENDER_PROCESS(""); -} + DEBUG_RENDER_PROCESS(browser->GetIdentifier()); -#if 0 -//------------------------------------------------------------------------------ -GDCefBrowser::~GDCefBrowser() -{ - DEBUG_BROWSER_PROCESS(""); -} - -//------------------------------------------------------------------------------ -void GDCefBrowser::OnAfterCreated(CefRefPtr browser) -{ - CEF_REQUIRE_UI_THREAD(); - DEBUG_BROWSER_PROCESS(""); + // No handler yet, we need to create it first + m_handler = new GodotMethodHandler(browser); - // Add to the list of existing browsers. - m_browser_list.push_back(browser); -} + // Create global JavaScript objects and bind methods + CefRefPtr global = context->GetGlobal(); -//------------------------------------------------------------------------------ -void GDCefBrowser::OnBeforeClose(CefRefPtr browser) -{ - CEF_REQUIRE_UI_THREAD(); - DEBUG_BROWSER_PROCESS(""); + // Create a global Godot bridge object + CefRefPtr godotBridge = + CefV8Value::CreateObject(nullptr, nullptr); - // Remove from the list of existing browsers. - BrowserList::iterator bit = m_browser_list.begin(); - for (; bit != m_browser_list.end(); ++bit) - { - if ((*bit)->IsSame(browser)) - { - m_browser_list.erase(bit); - break; - } - } + // Bind methods from Godot to JavaScript + godotBridge->SetValue( + CALL_GODOT_METHOD, + CefV8Value::CreateFunction(CALL_GODOT_METHOD, m_handler), + V8_PROPERTY_ATTRIBUTE_NONE); - if (m_browser_list.empty()) - { - DEBUG_BROWSER_PROCESS("CefQuitMessageLoop"); - // All browser windows have closed. - // Quit the application message loop. - CefQuitMessageLoop(); - } -} -#endif \ No newline at end of file + global->SetValue("godot", godotBridge, V8_PROPERTY_ATTRIBUTE_NONE); +} \ No newline at end of file diff --git a/addons/gdcef/render_process/src/render_process.hpp b/addons/gdcef/render_process/src/render_process.hpp index 423c72b..46c9653 100644 --- a/addons/gdcef/render_process/src/render_process.hpp +++ b/addons/gdcef/render_process/src/render_process.hpp @@ -63,94 +63,56 @@ #endif // ***************************************************************************** -//! \brief Entry point for the render process +//! \brief JavaScript Method Handler // ***************************************************************************** -class RenderProcess: public CefApp, - // public CefBrowserProcessHandler, - public CefRenderProcessHandler +class GodotMethodHandler: public CefV8Handler { public: - ~RenderProcess(); + GodotMethodHandler(CefRefPtr browser) : m_browser(browser) {} -private: // CefApp methods + bool Execute(const CefString& name, + CefRefPtr object, + const CefV8ValueList& arguments, + CefRefPtr& retval, + CefString& exception) override; - // ------------------------------------------------------------------------- - // virtual CefRefPtr - // GetBrowserProcessHandler() override - // { - // return this; - //} - - virtual CefRefPtr - GetRenderProcessHandler() override - { - return this; - } - -private: // CefBrowserProcessHandler methods - - // ------------------------------------------------------------------------- - // virtual void OnContextInitialized() override; - -private: // CefRenderProcessHandler methods - - // ------------------------------------------------------------------------- - virtual void OnContextCreated(CefRefPtr browser, - CefRefPtr frame, - CefRefPtr context) override; + IMPLEMENT_REFCOUNTING(GodotMethodHandler); private: - IMPLEMENT_REFCOUNTING(RenderProcess); + CefRefPtr m_browser; }; -#if 0 // ***************************************************************************** -//! \brief Browser process handler +//! \brief Entry point for the render process // ***************************************************************************** -class GDCefBrowser: public CefClient, - public CefLifeSpanHandler, - public CefDisplayHandler +class RenderProcess: public CefApp, public CefRenderProcessHandler { public: - ~GDCefBrowser(); - -private: // CefDisplayHandler methods - - // ------------------------------------------------------------------------- - virtual CefRefPtr GetDisplayHandler() override - { - return this; - } + IMPLEMENT_REFCOUNTING(RenderProcess); -private: // CefLifeSpanHandler methods +private: // CefApp methods // ------------------------------------------------------------------------- - virtual CefRefPtr GetLifeSpanHandler() override + virtual CefRefPtr + GetRenderProcessHandler() override { return this; } - // ------------------------------------------------------------------------- - virtual void OnAfterCreated(CefRefPtr browser) override; - - // ------------------------------------------------------------------------- - virtual void OnBeforeClose(CefRefPtr browser) override; - -private: +private: // CefRenderProcessHandler methods // ------------------------------------------------------------------------- - // Include the default reference counting implementation. - IMPLEMENT_REFCOUNTING(GDCefBrowser); + virtual void OnContextCreated(CefRefPtr browser, + CefRefPtr frame, + CefRefPtr context) override; private: - using BrowserList = std::list>; - BrowserList m_browser_list; + CefRefPtr m_handler; }; -#endif #if !defined(_WIN32) # if defined(__clang__)