diff --git a/CMakeLists.txt b/CMakeLists.txt index e17aec8..e5dd464 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ # ################################################################################################## -# Copyright (c) 2022, Giulio Girardi +# Copyright (c) 2023, Giulio Girardi # # Distributed under the terms of the GNU General Public License v3. # @@ -72,6 +72,8 @@ option( # ============ find_package(xeus-zmq REQUIRED) +find_package(xwidgets REQUIRED) +find_package(xproperty REQUIRED) find_package(PNG REQUIRED) find_package(glad REQUIRED) find_package(glfw3) @@ -112,6 +114,7 @@ set( include/xeus-octave/plotstream.hpp include/xeus-octave/tex2html.hpp include/xeus-octave/display.hpp + include/xeus-octave/xwidgets2.hpp ) set( @@ -122,6 +125,7 @@ set( src/xinterpreter.cpp src/input.cpp src/output.cpp + src/xwidgets2.cpp ) set(XEUS_OCTAVE_MAIN_SRC src/main.cpp) @@ -187,7 +191,7 @@ endmacro() # Scripts directory for xeus-octave set( XEUS_OCTAVE_SCRIPTS_BASEDIR - "share" + ${CMAKE_INSTALL_DATADIR} CACHE STRING "Xeus-octave scripts base directory" ) @@ -215,7 +219,8 @@ macro(xeus_octave_create_target target_name linkage output_name) target_compile_definitions( ${target_name} PUBLIC "XEUS_OCTAVE_EXPORTS" - PRIVATE XEUS_OCTAVE_OVERRIDE_PATH="${CMAKE_INSTALL_PREFIX}/share/xeus-octave" + PRIVATE + XEUS_OCTAVE_OVERRIDE_PATH="${CMAKE_INSTALL_PREFIX}/${XEUS_OCTAVE_SCRIPTS_BASEDIR}/xeus-octave" ) target_compile_features(${target_name} PRIVATE cxx_std_17) @@ -226,7 +231,7 @@ macro(xeus_octave_create_target target_name linkage output_name) target_link_libraries( ${target_name} - PUBLIC xtl PkgConfig::octinterp + PUBLIC xtl xwidgets PkgConfig::octinterp PRIVATE glad::glad glfw PNG::PNG ) if(XEUS_OCTAVE_USE_SHARED_XEUS) @@ -276,6 +281,8 @@ if(XEUS_OCTAVE_BUILD_EXECUTABLE) xeus_octave_set_kernel_options(xoctave) endif() +add_subdirectory(share/xeus-octave/+xwidgets) + # Installation # ============ @@ -312,7 +319,7 @@ if(XEUS_OCTAVE_BUILD_EXECUTABLE) # Configuration and data directories for jupyter and xeus-octave set( XJUPYTER_DATA_DIR - "share/jupyter" + "${CMAKE_INSTALL_DATADIR}/jupyter" CACHE STRING "Jupyter data directory" ) @@ -330,6 +337,7 @@ if(XEUS_OCTAVE_BUILD_EXECUTABLE) DIRECTORY ${XOCTAVE_SCRIPTS_DIR} DESTINATION ${XEUS_OCTAVE_SCRIPTS_BASEDIR} PATTERN "*.in" EXCLUDE + PATTERN "+xwidgets" EXCLUDE ) # Extra path for installing Jupyter kernelspec diff --git a/environment-dev.yml b/environment-dev.yml index e7d9815..cf5406a 100644 --- a/environment-dev.yml +++ b/environment-dev.yml @@ -3,6 +3,7 @@ channels: dependencies: # Build dependencies - cxx-compiler + - fortran-compiler - c-compiler - cmake - make @@ -11,6 +12,7 @@ dependencies: - libuuid - xtl - xeus-zmq =1.* + - xwidgets =0.27.3 - nlohmann_json - cppzmq - octave =7.* @@ -36,5 +38,7 @@ dependencies: - ccache - cmake-format - plotly - - ipywidgets + - ipywidgets =8 + - widgetsnbextension - jupyter-dash + - mako diff --git a/include/xeus-octave/xwidgets2.hpp b/include/xeus-octave/xwidgets2.hpp new file mode 100644 index 0000000..32a5bb4 --- /dev/null +++ b/include/xeus-octave/xwidgets2.hpp @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2022 Giulio Girardi. + * + * This file is part of xeus-octave. + * + * xeus-octave is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * xeus-octave is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with xeus-octave. If not, see . + */ + +#ifndef XEUS_OCTAVE_XWIDGETS2_H +#define XEUS_OCTAVE_XWIDGETS2_H + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xeus_octave::widgets +{ + +void register_all2(octave::interpreter&); + +constexpr inline char const* XWIDGET_CLASS_NAME = "__xwidget_internal__"; + +class xwidget : public octave::handle_cdef_object, public xw::xcommon +{ + +public: + + void put(std::string const&, octave_value const&) override; + void mark_as_constructed(octave::cdef_class const& cls) override; + +private: + + xwidget(); + ~xwidget(); + + void open(); + void close(); + + void serialize_state(nl::json&, xeus::buffer_sequence&) const; + void apply_patch(nl::json const&, xeus::buffer_sequence const&); + void handle_message(xeus::xmessage const&); + void handle_custom_message(nl::json const&); + + /** + * @brief call any observers set in the octave interpreter context for the + * specified property name + */ + void notify_backend(std::string const&); + /** + * @brief send to the frontend a new value for the specified property. + * Octave value is automatically converted to a json value + */ + void notify_frontend(std::string const&, octave_value const&); + +private: + + static octave_value_list cdef_constructor(octave::interpreter&, octave_value_list const&, int); + static octave_value_list cdef_observe(octave_value_list const&, int); + static octave_value_list cdef_display(octave_value_list const&, int); + static octave_value_list cdef_id(octave_value_list const&, int); + static octave_value_list cdef_on(octave_value_list const&, int); + + template friend inline void xw::xwidgets_serialize(T const& value, nl::json& j, xeus::buffer_sequence&); + friend void xeus_octave::widgets::register_all2(octave::interpreter&); + +private: + + std::map> m_observerCallbacks; + std::map> m_eventCallbacks; +}; + +xwidget* get_widget(octave_classdef const*); + +} // namespace xeus_octave::widgets +#endif diff --git a/share/xeus-octave/+xwidgets/CMakeLists.txt b/share/xeus-octave/+xwidgets/CMakeLists.txt new file mode 100644 index 0000000..6fbc52e --- /dev/null +++ b/share/xeus-octave/+xwidgets/CMakeLists.txt @@ -0,0 +1,36 @@ +# ################################################################################################## +# Copyright (c) 2023, Giulio Girardi +# +# Distributed under the terms of the GNU General Public License v3. +# +# The full license is in the file LICENSE, distributed with this software. +# ################################################################################################## + +execute_process( + COMMAND python ${CMAKE_CURRENT_SOURCE_DIR}/xwidgets_generate.py + OUTPUT_VARIABLE XEUS_OCTAVE_WIDGETS +) + +set(XEUS_OCTAVE_WIDGETS_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}) + +add_custom_command( + OUTPUT ${XEUS_OCTAVE_WIDGETS} + DEPENDS ${XEUS_OCTAVE_WIDGETS_SOURCE_DIR}/Widget.m + COMMAND + python ${CMAKE_CURRENT_SOURCE_DIR}/xwidgets_generate.py + ${XEUS_OCTAVE_WIDGETS_SOURCE_DIR}/Widget.m + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + VERBATIM +) + +list( + TRANSFORM XEUS_OCTAVE_WIDGETS + PREPEND "${CMAKE_CURRENT_BINARY_DIR}/" OUTPUT_VARIABLE XEUS_OCTAVE_GENERATED_WIDGETS +) + +install( + FILES ${XEUS_OCTAVE_GENERATED_WIDGETS} + DESTINATION ${XEUS_OCTAVE_SCRIPTS_BASEDIR}/xeus-octave/+xwidgets +) + +add_custom_target(widgets ALL DEPENDS ${XEUS_OCTAVE_WIDGETS}) diff --git a/share/xeus-octave/+xwidgets/Widget.m b/share/xeus-octave/+xwidgets/Widget.m new file mode 100644 index 0000000..4b7f186 --- /dev/null +++ b/share/xeus-octave/+xwidgets/Widget.m @@ -0,0 +1,207 @@ +# +# Copyright (C) 2023 Giulio Girardi. +# +# This file is part of xeus-octave. +# +# xeus-octave is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# xeus-octave is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# +# along with xeus-octave. If not, see . +# +<%! +from traitlets import ( + CaselessStrEnum, + Unicode, + Tuple, + List, + Bool, + CFloat, + Float, + CInt, + Int, + Instance, + Dict, + Bytes, + Any, + Union, +) + +from ipywidgets.widgets import Widget +from ipywidgets.widgets.trait_types import TypedTuple, CByteMemoryView, InstanceDict +%> +classdef ${widget_name} < __xwidget_internal__ + <%self:octavedoc> + -*- texinfo -*- + @deftypefn {} {@var{w} =} xwidgets.${widget_name} () + + % if doc: + ${doc.split("Parameters")[0]} + % endif + + % for trait_name, trait in traits: + % if trait.help: + @deftypefn {} {} xwidgets.${widget_name}.${trait_name} + ${trait.help} + @end deftypefn + % endif + % endfor + + @end deftypefn + + + properties (Sync = true) +% for trait_name, trait in traits: + % if trait.help: + ${'##'} ${trait.help} + % endif + % if trait.default() is None: + ${trait_name} = []; # null + % elif type(trait) in (CaselessStrEnum, Unicode, CUnicode, Color, NumberFormat): + ${trait_name} = "${trait.default()}"; + % elif type(trait) in (CFloat, Float, CInt, Int): + ${trait_name} = ${trait.default()}; + % elif type(trait) is Bool: + ${trait_name} = ${str(trait.default()).lower()}; + % else: + ${trait_name} = []; # null + % endif +% endfor + endproperties + + methods + function obj = ${widget_name}() +% for trait_name, trait in traits: + % if type(trait) in (Instance, InstanceDict) and issubclass(trait.klass, Widget): + <% + for data, klass in widget_list: + if klass == trait.klass: + instance_name = data[2].removesuffix("Model") + break + %> + obj.${trait_name} = xwidgets.${instance_name}; + % elif (type(trait) is TypedTuple and type(trait._trait) is Unicode) or (type(trait) is List and type(trait._trait) is Unicode): + % if trait.default() is not None: + obj.${trait_name} = {${','.join([f'"{v}"' for v in trait.default()])}}; + % endif + % elif type(trait) is TypedTuple and type(trait._trait) is Instance and issubclass(trait._trait.klass, Widget): + obj.${trait_name} = {}; + % elif type(trait) is CByteMemoryView: + obj.${trait_name} = uint8([]); + % endif +% endfor + endfunction + +% for trait_name, trait in traits: + function set.${trait_name}(obj, value) + % if not trait.allow_none: + if isnull(value) + error("input must not be null") + end + % endif + % if type(trait) is CaselessStrEnum: + mustBeMember(value, {${'"' + '","'.join(trait.values) + '"'}}); + if true + % elif type(trait) is Unicode: + if !ischar(value) + error("input must be a string"); + else + % elif type(trait) is CUnicode: + if isnumeric(value) + obj.${trait_name} = num2str(value); + elseif !ischar(value) + obj.${trait_name} = disp(value); + else + % elif type(trait) is Float: + if !isreal(value) && !isscalar(value) + error("input must be a real scalar"); + elseif isinteger(value) + obj.${trait_name} = double(value); + else + % elif type(trait) is CFloat: + if ischar(value) + obj.${trait_name} = str2num(value); + elseif !isreal(value) && !isscalar(value) + error("input must be a real scalar"); + elseif isinteger(value) + obj.${trait_name} = double(value); + else + % elif type(trait) is Int: + if round(value) == value + obj.${trait_name} = int64(value); + elseif !isreal(value) && !isscalar(value) && !isinteger(value) + error("input must be a real scalar integer"); + else + % elif type(trait) is CInt: + if !isreal(value) && !isscalar(value) + error("input must be a real scalar"); + elseif isinteger(value) + obj.${trait_name} = int64(value); + else + % elif type(trait) is Bool: + if !islogical(value) + error("input must be a logical value"); + else + % elif type(trait) in (Instance, InstanceDict) and issubclass(trait.klass, Widget): + <% + for data, klass in widget_list: + if klass == trait.klass: + instance_name = data[2].removesuffix("Model") + break + %> + if !isa(value, "xwidgets.${instance_name}") + error("input must be instance of xwidgets.${instance_name}"); + else + % elif type(trait) is TypedTuple and type(trait._trait) is Unicode: + if !iscellstr(value) + error ("input must be an array of strings"); + else + % elif type(trait) is TypedTuple and type(trait._trait) is Instance and issubclass(trait._trait.klass, Widget): + if !iscell(value) || !all(cellfun(@(c) isa(c, "__xwidget_internal__"), value)) + error ("input must be an array of widgets"); + else + % elif type(trait) is CByteMemoryView: + if !strcmp(typeinfo(value), "uint8 matrix") + obj.${trait_name} = uint8(value); + else + % else: + if true + warning("Property of type ${type(trait)} is not validated"); + % endif + obj.${trait_name} = value; + end + endfunction + +% endfor +% for trait_name, trait in traits: + % if type(trait) in (Int, CInt, Float, CFloat): + function value = get.${trait_name}(obj) + if isnull(obj.${trait_name}) + value = []; + else + % if type(trait) in (Int, CInt): + value = int64(obj.${trait_name}); + % elif type(trait) in (Float, CFloat): + value = double(obj.${trait_name}); + % endif + end + endfunction + + % endif +% endfor + endmethods +endclassdef + +<%def name="octavedoc()"> + % for line in capture(caller.body).strip().splitlines(): + ${'##'} ${line.strip()} + % endfor + diff --git a/share/xeus-octave/+xwidgets/xwidgets_generate.py b/share/xeus-octave/+xwidgets/xwidgets_generate.py new file mode 100644 index 0000000..7144536 --- /dev/null +++ b/share/xeus-octave/+xwidgets/xwidgets_generate.py @@ -0,0 +1,56 @@ +# +# Copyright (C) 2023 Giulio Girardi. +# +# This file is part of xeus-octave. +# +# xeus-octave is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# xeus-octave is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# +# along with xeus-octave. If not, see . +# + +import sys + +from mako.template import Template +from ipywidgets import widgets + +if __name__ == "__main__": + widget_list = sorted(widgets.Widget._widget_types.items()) + + if len(sys.argv) == 2: + widget_template = Template(filename=sys.argv[1]) + + for data, klass in widget_list: + widget_name = data[2].removesuffix("Model") + + # Instanciate dummy widget + if issubclass(klass, widgets.widget_link.Link): + widget = klass((widgets.IntSlider(), 'value'), (widgets.IntSlider(), 'value')) + elif issubclass(klass, (widgets.SelectionRangeSlider, widgets.SelectionSlider)): + widget = klass(options=[1]) + else: + widget = klass() + + traits = widget.traits(sync=True) + traits.pop("_view_count") + + with open(f"{widget_name}.m", "w") as out: + out.write(widget_template.render( # type: ignore + widget_list=widget_list, + widget_name=widget_name, + widget=widget, + doc=klass.__doc__, + traits=traits.items() + )) + else: + widget_files = [d[2].removesuffix('Model') + '.m' for d, _ in widget_list] + print(';'.join(widget_files), end='') diff --git a/src/xinterpreter.cpp b/src/xinterpreter.cpp index 0d71b28..5bd9ba1 100644 --- a/src/xinterpreter.cpp +++ b/src/xinterpreter.cpp @@ -61,6 +61,7 @@ #include "xeus-octave/tk_plotly.hpp" #include "xeus-octave/utils.hpp" #include "xeus-octave/xinterpreter.hpp" +#include "xeus-octave/xwidgets2.hpp" namespace nl = nlohmann; namespace oc = octave; @@ -408,6 +409,9 @@ void xoctave_interpreter::configure_impl() // Register the input system xeus_octave::io::register_input(m_stdin); + // Register the widget system + xeus_octave::widgets::register_all2(interpreter); + // Install version variable interpreter.get_symbol_table().install_built_in_function( "XOCTAVE", new octave_builtin([](octave_value_list const&, int) { return ovl(XEUS_OCTAVE_VERSION); }, "XOCTAVE") diff --git a/src/xwidgets2.cpp b/src/xwidgets2.cpp new file mode 100644 index 0000000..942e5e9 --- /dev/null +++ b/src/xwidgets2.cpp @@ -0,0 +1,453 @@ +/* + * Copyright (C) 2023 Giulio Girardi. + * + * This file is part of xeus-octave. + * + * xeus-octave is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * xeus-octave is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with xeus-octave. If not, see . + */ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "xeus-octave/utils.hpp" +#include "xeus-octave/xwidgets2.hpp" + +namespace xw +{ + +inline void xwidgets_serialize(octave_value const& ov, nl::json& j, xeus::buffer_sequence& b); +inline void +xwidgets_deserialize(octave_value& ov, nl::json const& j, xeus::buffer_sequence const& b = xeus::buffer_sequence()); + +namespace +{ + +template inline void xwidgets_serialize_matrix_like(M const& mv, nl::json& j, xeus::buffer_sequence& b) +{ + j = nl::json::array(); + + for (octave_idx_type i = 0; i < mv.numel(); i++) + { + nl::json e; + xwidgets_serialize(mv.elem(i), e, b); + j.push_back(e); + } +} + +template +inline void xwidgets_deserialize_matrix_like(octave_value& ov, nl::json const& j, xeus::buffer_sequence const& b) +{ + T p(dim_vector(static_cast(j.size()), 1)); + octave_idx_type i = 0; + for (auto& e : j) + xwidgets_deserialize(p(i++), e, b); + ov = p; +} + +inline void xwidgets_deserialize_object(octave_value& ov, nl::json const& j, xeus::buffer_sequence const& b) +{ + octave_scalar_map p; + for (auto& [key, val] : j.items()) + { + octave_value e; + xwidgets_deserialize(e, val, b); + p.assign(key, e); + } + ov = p; +} + +} // namespace + +inline void xwidgets_serialize(octave_classdef const& cdv, nl::json& j, xeus::buffer_sequence&) +{ + if (cdv.is_instance_of(xeus_octave::widgets::XWIDGET_CLASS_NAME)) + j = "IPY_MODEL_" + std::string(xeus_octave::widgets::get_widget(&cdv)->id()); + else + warning("xwidget: cannot serialize classdef"); +} + +inline void xwidgets_serialize(Array const& mv, nl::json& j, xeus::buffer_sequence& b) +{ + xwidgets_serialize_matrix_like(mv, j, b); +} + +inline void xwidgets_serialize(Cell const& cv, nl::json& j, xeus::buffer_sequence& b) +{ + xwidgets_serialize_matrix_like(cv, j, b); +} + +inline void xwidgets_serialize(octave_value const& ov, nl::json& j, xeus::buffer_sequence& b) +{ + if (ov.is_bool_scalar()) + xwidgets_serialize(ov.bool_value(), j, b); + else if (ov.is_real_scalar()) + xwidgets_serialize(ov.scalar_value(), j, b); + else if (ov.isinteger() && ov.is_scalar_type()) + xwidgets_serialize(ov.int64_value(), j, b); + else if (ov.is_string()) + xwidgets_serialize(ov.string_value(), j, b); + else if (ov.is_classdef_object()) + xwidgets_serialize(*ov.classdef_object_value(), j, b); + else if (ov.iscell()) + xwidgets_serialize(ov.cell_value(), j, b); + else if (ov.isnull()) + xwidgets_serialize(nullptr, j, b); + else + warning("xwidget: cannot serialize octave value %s", ov.type_name().data()); +} + +inline void xwidgets_deserialize(octave_value& ov, nl::json const& j, xeus::buffer_sequence const& b) +{ + if (j.is_boolean()) + ov = j.get(); + else if (j.is_number_float()) + ov = j.get(); + else if (j.is_number_integer()) + ov = octave_int64(j.get()); + else if (j.is_string()) + ov = j.get(); + // No classdef at the moment + else if (j.is_array()) + xwidgets_deserialize_matrix_like(ov, j, b); + else if (j.is_object()) + xwidgets_deserialize_object(ov, j, b); + else if (j.is_null()) + ov = octave_null_matrix::instance; + else + warning("xwidget: cannot deserialize json value %s", j.type_name()); +} + +} // namespace xw + +namespace xeus_octave::widgets +{ + +xwidget::xwidget() : octave::handle_cdef_object(), xw::xcommon() +{ + this->comm().on_message(std::bind(&xwidget::handle_message, this, std::placeholders::_1)); +} + +xwidget::~xwidget() +{ + std::clog << "Destructing " << get_class().get_name() << std::endl; + this->close(); +} + +void xwidget::open() +{ + // serialize state + nl::json state; + xeus::buffer_sequence buffers; + this->serialize_state(state, buffers); + + // open comm + xw::xcommon::open(std::move(state), std::move(buffers)); +} + +void xwidget::close() +{ + xw::xcommon::close(); +} + +namespace +{ + +/** + * @brief Check if property should be synced with widget model in frontend + * by looking for "Sync" attribute" + * + * The following must be present in classdef definition in .m file + * + * ... + * properties (Sync = true) + * _model_name = "ButtonModel"; + * _view_name = "ButtonView"; + * + * description = ""; + * tooltip = ""; + * end + * ... + * + * We can use a nonstandard attribute because Octave parses all attributes + * of properties regardless of their "correctness". + * + * @param property reference to a property definition object + * @return true if property has attribute "Sync" set to true + */ +inline bool is_sync_property(octave::cdef_property& property) +{ + return !property.get("Sync").isempty() && property.get("Sync").bool_value(); +} + +}; // namespace + +void xwidget::serialize_state(nl::json& state, xeus::buffer_sequence& buffers) const +{ + octave::cdef_class cls = this->get_class(); + auto properties = cls.get_property_map(octave::cdef_class::property_all); + + for (auto property_tuple : properties) + { + octave::cdef_property property = property_tuple.second; + if (is_sync_property(property)) + { + octave_value ov = this->get(property_tuple.first); + xw::xwidgets_serialize(ov, state[property_tuple.first], buffers); + } + } +} + +void xwidget::apply_patch(nl::json const& state, xeus::buffer_sequence const& buffers) +{ + octave::cdef_class cls = this->get_class(); + auto properties = cls.get_property_map(octave::cdef_class::property_all); + + for (auto property_tuple : properties) + { + octave::cdef_property property = property_tuple.second; + if (properties.count(property_tuple.first) && is_sync_property(property) && state.contains(property_tuple.first)) + { + octave_value value; + xw::xwidgets_deserialize(value, state[property_tuple.first], buffers); + // Call superclass put to avoid notifying the view again in a loop + octave::handle_cdef_object::put(property_tuple.first, value); + this->notify_backend(property_tuple.first); + } + } +} + +void xwidget::put(std::string const& pname, octave_value const& val) +{ + octave::handle_cdef_object::put(pname, val); + if (this->is_constructed()) // When default property values are being set + { + octave::cdef_class cls = this->get_class(); + auto properties = cls.get_property_map(octave::cdef_class::property_all); + + if (properties.count(pname) && is_sync_property(properties[pname])) + { + std::clog << "Notify change " << pname << std::endl; + this->notify_frontend(pname, val); + this->notify_backend(pname); + } + } +} + +void xwidget::notify_frontend(std::string const& name, octave_value const& value) +{ + nl::json state; + xeus::buffer_sequence buffers; + xw::xwidgets_serialize(value, state[name], buffers); + send_patch(std::move(state), std::move(buffers)); +} + +void xwidget::notify_backend(std::string const& pname) +{ + if (this->m_observerCallbacks.count(pname)) + { + for (auto callback : this->m_observerCallbacks[pname]) + { + // Object reference + octave::cdef_object obj(this->clone()); + octave::feval(callback, octave::to_ov(obj)); + } + } +} + +void xwidget::handle_message(xeus::xmessage const& message) +{ + nl::json const& content = message.content(); + nl::json const& data = content["data"]; + const std::string method = data["method"]; + + if (method == "update") + { + nl::json const& state = data["state"]; + auto const& buffers = message.buffers(); + nl::json const& buffer_paths = data["buffer_paths"]; + this->hold() = std::addressof(message); + xw::insert_buffer_paths(const_cast(state), buffer_paths); + this->apply_patch(state, buffers); + this->hold() = nullptr; + } + else if (method == "request_state") + { + nl::json state; + xeus::buffer_sequence buffers; + this->serialize_state(state, buffers); + send_patch(std::move(state), std::move(buffers)); + } + else if (method == "custom") + { + auto it = data.find("content"); + if (it != data.end()) + { + this->handle_custom_message(it.value()); + } + } +} + +void xwidget::handle_custom_message(nl::json const& jsonmessage) +{ + auto meth = this->get_class().find_method("handle_custom_message"); + + if (meth.ok()) + { + octave_value message; + xw::xwidgets_deserialize(message, jsonmessage); + octave::cdef_object obj(this->clone()); + meth.execute(obj, ovl(message), 0); + } + else if (jsonmessage.contains("event")) + { + std::string event = jsonmessage["event"]; + if (this->m_eventCallbacks.count(event)) + { + for (auto callback : this->m_eventCallbacks[event]) + { + // Object reference + octave::cdef_object obj(this->clone()); + octave::feval(callback, octave::to_ov(obj)); + } + } + } +} + +void xwidget::mark_as_constructed(octave::cdef_class const& cls) +{ + octave::handle_cdef_object::mark_as_constructed(cls); + + if (m_ctor_list.empty()) + // Open the comm + this->open(); +} + +octave_value_list xwidget::cdef_constructor(octave::interpreter& interpreter, octave_value_list const& args, int) +{ + // Get a reference to the old object + octave::cdef_object& obj = args(0).classdef_object_value()->get_object_ref(); + // Retrieve the class we want to construct + octave::cdef_class cls = obj.get_class(); + + if (get_widget(args(0).classdef_object_value()) == nullptr) + { + std::clog << "Inject xwidget into " << cls.get_name() << std::endl; + + // Create a new object with our widget rep + xwidget* wdg = new xwidget(); + octave::cdef_object new_obj(wdg); + // Set it to the new object + new_obj.set_class(cls); + // Initialize the properties + cls.initialize_object(new_obj); + // Construct superclasses (only handle) + interpreter.get_cdef_manager().find_class("handle").run_constructor(new_obj, ovl()); + // Replace the old object + obj = new_obj; + + return ovl(octave::to_ov(new_obj)); + } + else // If the object rep has already been substituted with an xwidget (this will happen with multiple inheritance) + { + std::clog << "No need to inject xwidget into " << cls.get_name() << std::endl; + + return ovl(args(0)); + } +} + +octave_value_list xwidget::cdef_observe(octave_value_list const& args, int) +{ + // Object reference + octave_classdef* obj = args(0).classdef_object_value(); + // Property to observe + std::string pname = args(1).xstring_value("PNAME must be a string with the property name"); + // Observer callback + octave_value fcn = args(2); + + if (!fcn.is_function_handle()) + error("HANDLE must be a function handle"); + + get_widget(obj)->m_observerCallbacks[pname].push_back(fcn); + + return ovl(); +} + +octave_value_list xwidget::cdef_display(octave_value_list const& args, int) +{ + get_widget(args(0).classdef_object_value())->display(); + return ovl(); +} + +octave_value_list xwidget::cdef_id(octave_value_list const& args, int) +{ + return ovl(std::string(get_widget(args(0).classdef_object_value())->id())); +} + +octave_value_list xwidget::cdef_on(octave_value_list const& args, int) +{ + // Object reference + octave_classdef* obj = args(0).classdef_object_value(); + // Property to observe + std::string event = args(1).xstring_value("EVENT must be a string with the event name"); + // Observer callback + octave_value fcn = args(2); + + if (!fcn.is_function_handle()) + error("HANDLE must be a function handle"); + + get_widget(obj)->m_eventCallbacks[event].push_back(fcn); + + return ovl(); +} + +xwidget* get_widget(octave_classdef const* obj) +{ + octave::cdef_object const& ref = const_cast(obj)->get_object_ref(); + octave::cdef_object_rep* rep = const_cast(ref.get_rep()); + + return dynamic_cast(rep); +} + +void register_all2(octave::interpreter& interpreter) +{ + octave::cdef_manager& cm = interpreter.get_cdef_manager(); + octave::cdef_class cls = cm.make_class(XWIDGET_CLASS_NAME, cm.find_class("handle")); + + cls.install_method(cm.make_method(cls, XWIDGET_CLASS_NAME, xwidget::cdef_constructor)); + cls.install_method(cm.make_method(cls, "observe", xwidget::cdef_observe)); + cls.install_method(cm.make_method(cls, "display", xwidget::cdef_display)); + cls.install_method(cm.make_method(cls, "id", xwidget::cdef_id)); + cls.install_method(cm.make_method(cls, "on", xwidget::cdef_on)); + + interpreter.get_symbol_table().install_built_in_function(XWIDGET_CLASS_NAME, cls.get_constructor_function()); +} + +} // namespace xeus_octave::widgets