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
+ %self:octavedoc>
+
+ 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
+%def>
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
|