From 627c0ef6b8cc990a7fe8b9eac6773c26ec9be5e6 Mon Sep 17 00:00:00 2001
From: Dorian Goepp <dorian.goepp@gmail.com>
Date: Tue, 7 May 2019 11:33:33 +0200
Subject: [PATCH] We are getting closer to a wokring proposal

---
 src/Board.js                           |  10 ++-
 src/HorizontalGauge/HozirontalGauge.js |  22 ++---
 src/HorizontalGauge/RosHGauge.js       |  38 ++++++--
 src/HorizontalGauge/index.js           |   7 +-
 src/devel-config.js                    |   8 +-
 src/utils/modeHandler.js               | 118 +++++++++++++++++++++++--
 6 files changed, 163 insertions(+), 40 deletions(-)

diff --git a/src/Board.js b/src/Board.js
index d8dee81..aedbded 100755
--- a/src/Board.js
+++ b/src/Board.js
@@ -14,8 +14,8 @@ import { RosMood, DemoMood } from './Mood'
 import DemoString from './String'
 
 // get the default configuration for the layout
-import {flexlayout_json as json} from './demo-1-config'
-// import {flexlayout_json as json} from './devel-config'
+// import {flexlayout_json as json} from './demo-1-config'
+import {flexlayout_json as json} from './devel-config'
 
 // CSS common to the whole app
 import './Common.css'
@@ -132,10 +132,16 @@ class Board extends React.Component {
                     const tab = this.getSelectedNode(); // retrieve the selected tab
                     const config = tab.getConfig(); // get the configuration of this tab
                     if (!("displayMode" in config) || (config.displayMode !== modeName)) {
+                        if (!this.isMaximized()) { // Maximize the tabset
+                            this._model.doAction(Actions.maximizeToggle(this.getId()));
+                        }
                         config.displayMode = modeName;
                         this._model.doAction(
                             FlexLayout.Actions.updateNodeAttributes(tab.getId(), {config: config}));
                     } else {
+                        if (this.isMaximized()) { // Return the tabset to its original size
+                            this._model.doAction(Actions.maximizeToggle(this.getId()));
+                        }
                         delete config.displayMode;
                         this._model.doAction(
                             FlexLayout.Actions.updateNodeAttributes(tab.getId(), {config: config}));
diff --git a/src/HorizontalGauge/HozirontalGauge.js b/src/HorizontalGauge/HozirontalGauge.js
index 65fca8a..9b1d3f9 100644
--- a/src/HorizontalGauge/HozirontalGauge.js
+++ b/src/HorizontalGauge/HozirontalGauge.js
@@ -8,14 +8,16 @@ class HorizontalGauge extends Component {
         const value = ('value' in this.props && this.props.value) ? this.props.value : 0;
 
         // Define the dimensions of the gaug
-        const margin = { left:10, top: 10, right: 10, bottom: 10}
-        , width = this.props.size.width - margin.left - margin.right
-        , height = 20;
+        const margin = { left:10, top: 10, right: 10, bottom: 10},
+            width = this.props.size.width - margin.left - margin.right,
+            height = 20;
         const viewbox = "0 0 " + (width + margin.left + margin.right) +" "+ (height + margin.top + margin.bottom);
+        const min = this.props.min || 0,
+            max = this.props.max || 1;
 
         // Scale to map from values in [0, 1] to values in [0, width] (callable)
         const xScale = scaleLinear()
-        .domain([0, 1])
+        .domain([min, max])
         .range([0, width])
         .clamp(true);
 
@@ -33,18 +35,6 @@ class HorizontalGauge extends Component {
         </g>
         </svg>;
 
-        const config = this.props.node.getConfig()
-
-        if (config && 'displayMode' in config) {
-            if (config.displayMode === "readme") {
-                return <p className="about">This gauge represent a <b>measure</b> ranging from 0 to 1.</p>;
-            } else if (config.displayMode === "settings") {
-                return <div>
-                    <p>config panel</p>
-                </div>;
-            }
-        }
-
         return <div>{svg}</div>;
     }
 }
diff --git a/src/HorizontalGauge/RosHGauge.js b/src/HorizontalGauge/RosHGauge.js
index 874fd0d..07eb69c 100644
--- a/src/HorizontalGauge/RosHGauge.js
+++ b/src/HorizontalGauge/RosHGauge.js
@@ -1,5 +1,6 @@
 import React, {Component} from 'react'
-import RosLib from 'roslib';
+import RosLib from 'roslib'
+import {get} from 'lodash/object'
 
 import HorizontalGauge from './index'
 
@@ -8,20 +9,37 @@ class RosHGauge extends Component {
         super(props)
         
         this.state = {datum: null}
-        this.topic = "/predictions/interaction_involvement"
-        this.type = "april_messages/head_orientation_prediction"
+    }
+
+    static get modes() {
+        return {
+            readme: <p className="about">This gauge represent a <b>measure</b> ranging from 0 to 1.</p>,
+            settingsSchema: {
+                title: "Horizontal Gauge",
+                type: "object",
+                required: ["topic", "field", "min", "max"],
+                properties: {
+                    topic: {type: "string", title: "ROS topic (absolute name)",
+                            default: "/predictions/interaction_involvement"},
+                    field: {type: "string", title: "the field to be read from the message",
+                            default: "prediction_of_head_orientation.list[0].values[1]"},
+                    min: {type: "number", title: "Lower end of the data range", default: 0},
+                    max: {type: "number", title: "Upper end of the data range", default: 1},
+                }
+            },
+        };
     }
     
     componentDidMount() {
         if ('ros' in this.props && this.props.ros) {
+            const config = this.props.node.getConfig();
             this.listener = new RosLib.Topic({
                 ros : this.props.ros,
-                name: this.topic,
-                messageType: this.type
+                name: config.topic,
             });
             
             this.listener.subscribe(function(message) {
-                this.setState({datum: message.prediction_of_head_orientation.list[0].values[1]});
+                this.setState({datum: get(message, config.field)});
             }.bind(this));
         }else {
             console.warn('RosHGauge expects to be passed a valid Ros object as property, got ', this.props.ros);
@@ -35,7 +53,13 @@ class RosHGauge extends Component {
     }
     
     render() {
-        return <HorizontalGauge value={this.state.datum} {...this.props}/>
+        const config = this.props.node.getConfig();
+
+        return <HorizontalGauge
+            value={this.state.datum}
+            min={config.min}
+            max={config.max}
+            {...this.props}/>;
     }
 }
 
diff --git a/src/HorizontalGauge/index.js b/src/HorizontalGauge/index.js
index 7959d5c..aca98b1 100644
--- a/src/HorizontalGauge/index.js
+++ b/src/HorizontalGauge/index.js
@@ -1,6 +1,11 @@
+import React from 'react'
 import HorizontalGauge from './HozirontalGauge'
 import RosHGauge from './RosHGauge'
+import {Modal} from '../utils/modeHandler'
 
 export default HorizontalGauge;
 
-export {RosHGauge};
\ No newline at end of file
+function modal(props){
+  return <Modal component={RosHGauge} {...props}/>;
+}
+export {modal as RosHGauge};
\ No newline at end of file
diff --git a/src/devel-config.js b/src/devel-config.js
index ee0d339..d8ce5f4 100644
--- a/src/devel-config.js
+++ b/src/devel-config.js
@@ -96,9 +96,6 @@ export const flexlayout_json = {
                       {
                         "type": "tab",
                         "component": "mood",
-                        "config": {
-                          "hasReadme": true
-                        },
                       },
                     ]
                   },
@@ -124,10 +121,7 @@ export const flexlayout_json = {
                 "children": [
                   {
                     "type": "tab",
-                    "component": "demo-video",
-                    "config": {
-                      "hasReadme": true
-                    }
+                    "component": "video",
                   }
                 ]
               }
diff --git a/src/utils/modeHandler.js b/src/utils/modeHandler.js
index 8b1afed..898f635 100644
--- a/src/utils/modeHandler.js
+++ b/src/utils/modeHandler.js
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, {Component} from 'react';
 import Form from 'react-jsonschema-form';
 import { merge } from 'lodash/object';
 
@@ -72,19 +72,14 @@ export class modeHandler {
         }
     }
 
-    noop(value) {
-        return value;
-    }
-
     /**
      * Return either the value of the normal parameter, in normal mode, or the content for the `readme` and `settings`
      * modes.
      * 
-     * The display mode is defined by the 'displayMode' attribute of the component's configuraiton.
+     * The display mode is defined by the 'displayMode' attribute of the component's configuration.
      * @param {Jsx} normal The content to return in the normal mode (neither settings nor readme)
      */
     modalDisplay(normal) {
-        // return normal;
         const config = this.configGetter();
         if (config && 'displayMode' in config) {
             if (config.configurable && config.displayMode === "settings") {
@@ -115,4 +110,113 @@ export class modeHandler {
         return config;
     }
     
+}
+
+export class Modal extends Component {
+    constructor(props) {
+        super(props);
+
+        this.modes = this.props.component.modes;
+        this.configGetter = this.props.node.getConfig.bind(this.props.node);
+        this.configSetter = this.props.updateConfig;
+
+        // get the current config; if it does not exist yet, create it
+        let config = this.configGetter() || {};
+        
+        merge(config, this.announceCapabilities(config));
+        if (config.configurable) {
+            merge(config, this.defaultConfigFromSchema(config));
+        }
+        this.configSetter(config);
+    }
+
+    /**
+     * Search for the 'settingsSchema' and 'readme' properties of the managed component. A corresponding field in the
+     * configuration is set accordingly. This is used to display or not the UI buttons to show the Readme or the
+     * configuration for of a component.
+     * @param {Object} config current configuration of the object
+     * @return the new configuration object (possibly unmodified)
+     */
+    announceCapabilities(config) {
+        if (this.modes.settingsSchema) {
+            config.configurable = true;
+        }
+        else {
+            config.configurable = false;
+        }
+        if (this.modes.readme) {
+            config.hasReadme = true;
+        }
+        else {
+            config.hasReadme = false;
+        }
+        return config;
+    }
+
+    /**
+     * Search for properties in a JSON Schema. If they are here, use the default value of each JsonSchema property for
+     * the configuration. This means that if a property is already defined in the configuration, it will not be
+     * altered.
+     * @param {Object} config current configuration of the object
+     * @return the new configuration object (possibly unmodified)
+     */
+    defaultConfigFromSchema(config) {
+        const schema = this.modes.settingsSchema;
+        if (!schema) {
+            return config;
+        }
+        else if (!('properties' in schema)) {
+            console.warn("no properties in the schema");
+            return config;
+        }
+        else {
+            for (let [key, value] of Object.entries(schema.properties)) {
+                if (!(key in config)) {
+                    let message = "The key '" + key + "' is missing in the configuration.";
+                    if ('default' in value) {
+                        config[key] = value.default;
+                        message += " Using default value '" + value.default + "'.";
+                    }
+                    console.debug(message);
+                }
+            }
+            return config;
+        }
+    }
+
+    /**
+     * Render either the value of the normal parameter, in normal mode, or the content for the `readme` and `settings`
+     * modes. The display mode is defined by the 'displayMode' attribute of the component's configuration.
+     */
+    render() {
+        const config = this.configGetter();
+        if (config && 'displayMode' in config) {
+            if (config.configurable && config.displayMode === "settings") {
+                return (
+                <Form
+                    schema={this.modes.settingsSchema}
+                    onSubmit={this.handleSettingsChange.bind(this)}
+                    formData={config} />);
+            }
+            if (config.hasReadme && config.displayMode === "readme") {
+                return this.modes.readme;
+            }
+        }
+        return React.createElement(this.props.component, this.props, null);
+    }
+
+    /**
+     * Given a new configuration object (from the form), update the configuration of the component (in FlexLayout).
+     *
+     * The update is done through Lodash's merge function.
+     * @param {Object} value update for the configuration of the component (we take the field formData)
+     * @return Also return the updated configuration.
+     */
+    handleSettingsChange({ formData }) {
+        let config = this.configGetter();
+        merge(config, formData);
+        this.configSetter(config);
+        return config;
+    }
+    
 }
\ No newline at end of file
-- 
GitLab