From 96105d32f077cc3f17be3017fcd1db436ce275f9 Mon Sep 17 00:00:00 2001
From: Iiro Rastas <iiro.t.rastas@utu.fi>
Date: Tue, 29 Oct 2019 14:37:31 +0200
Subject: [PATCH] Add local configurations to the GUI

A new class NetConfigurable is created, which enables handling of both
local and remote configurations in a polymorphic way.

In the GUI, configurations can now be changed via the cog icon.
---
 applications/gui/src/config_window.cpp        | 137 ++++++++++++++----
 applications/gui/src/config_window.hpp        |  15 +-
 applications/gui/src/screen.cpp               |  36 ++++-
 .../common/cpp/include/ftl/configurable.hpp   |   5 +-
 components/common/cpp/src/configurable.cpp    |   4 -
 components/net/cpp/CMakeLists.txt             |   3 +-
 .../net/cpp/include/ftl/net_configurable.hpp  |  26 ++++
 components/net/cpp/src/net_configurable.cpp   |  12 ++
 components/net/cpp/test/CMakeLists.txt        |   9 ++
 .../net/cpp/test/net_configurable_unit.cpp    |  43 ++++++
 10 files changed, 245 insertions(+), 45 deletions(-)
 create mode 100644 components/net/cpp/include/ftl/net_configurable.hpp
 create mode 100644 components/net/cpp/src/net_configurable.cpp
 create mode 100644 components/net/cpp/test/net_configurable_unit.cpp

diff --git a/applications/gui/src/config_window.cpp b/applications/gui/src/config_window.cpp
index a9db26b46..fcba8ff7a 100644
--- a/applications/gui/src/config_window.cpp
+++ b/applications/gui/src/config_window.cpp
@@ -15,8 +15,11 @@ using std::string;
 using std::vector;
 using ftl::config::json_t;
 
+ConfigWindow::ConfigWindow(nanogui::Widget *parent, ftl::ctrl::Master *ctrl, const ftl::UUID &peer) : ConfigWindow(parent, ctrl, std::optional<ftl::UUID>(peer)) {
 
-ConfigWindow::ConfigWindow(nanogui::Widget *parent, ftl::ctrl::Master *ctrl, const ftl::UUID &peer)
+}
+
+ConfigWindow::ConfigWindow(nanogui::Widget *parent, ftl::ctrl::Master *ctrl, const std::optional<ftl::UUID> &peer)
 		: nanogui::Window(parent, "Settings"), ctrl_(ctrl), peer_(peer) {
 	using namespace nanogui;
 
@@ -24,78 +27,115 @@ ConfigWindow::ConfigWindow(nanogui::Widget *parent, ftl::ctrl::Master *ctrl, con
 	setPosition(Vector2i(parent->width()/2.0f - 100.0f, parent->height()/2.0f - 100.0f));
 	//setModal(true);
 
-	configurables_ = ctrl->getConfigurables(peer);
+	if (peer) {
+		configurables_ = ctrl->getConfigurables(peer.value());
+	} else {
+		configurables_ = ftl::config::list();
+	}
 
 	new Label(this, "Select Configurable","sans-bold");
 
-	auto select = new ComboBox(this, configurables_);
-	select->setCallback([this](int ix) {
-		LOG(INFO) << "Change configurable: " << ix;
-		_buildForm(configurables_[ix], ctrl_->get(peer_, configurables_[ix]));
-
-		setVisible(false);
-		//this->parent()->removeChild(this);
-		//delete this;
-		//screen()->removeChild(this);
-	});
+	for (auto c : configurables_) {
+		auto itembutton = new Button(this, c);
+		itembutton->setCallback([this,c]() {
+			LOG(INFO) << "Change configurable: " << c;
+			_buildForm(c);
+			setVisible(false);
+			//this->parent()->removeChild(this);
+			//delete this;
+			//screen()->removeChild(this);
+		});
+	}
 }
 
 ConfigWindow::~ConfigWindow() {
 
 }
 
-void ConfigWindow::_addElements(nanogui::FormHelper *form, const std::string &suri, const ftl::config::json_t &data) {
+class ConfigWindow::References {
+	public:
+	References(ftl::NetConfigurable* nc, ftl::config::json_t* config, const std::string* suri) : nc(nc), config(config), suri(suri) {
+	}
+
+	~References() {
+		delete nc;
+		delete config;
+		delete suri;
+	}
+
+	private:
+	ftl::NetConfigurable* nc;
+	ftl::config::json_t* config;
+	const std::string* suri;
+};
+
+std::vector<ftl::gui::ConfigWindow::References *> ConfigWindow::_addElements(nanogui::FormHelper *form, ftl::Configurable &nc, const std::string &suri, std::function<ftl::Configurable*(const std::string*, std::vector<References *>&)> construct) {
 	using namespace nanogui;
 
+	std::vector<References *> references;
+
+	auto data = nc.getConfig();
+
 	for (auto i=data.begin(); i!=data.end(); ++i) {
 		if (i.key() == "$id") continue;
 
 		if (i.key() == "$ref" && i.value().is_string()) {
 			LOG(INFO) << "Follow $ref: " << i.value();
-			_addElements(form, suri, ctrl_->get(peer_, i.value().get<string>()));
+			const std::string* suri = new std::string(i.value().get<string>());
+			ftl::Configurable* rc = construct(suri, references);
+			auto new_references = _addElements(form, *rc, *suri, construct);
+			references.insert(references.end(), new_references.begin(), new_references.end());
 			continue;
 		}
 
 		if (i.value().is_boolean()) {
 			string key = i.key();
-			form->addVariable<bool>(i.key(), [this,data,key,suri](const bool &b){
-				ctrl_->set(peer_, suri + string("/") + key, json_t(b));
+			form->addVariable<bool>(i.key(), [this,data,key,&nc](const bool &b){
+				nc.set(key, b);
 			}, [data,key]() -> bool {
 				return data[key].get<bool>();
 			});
 		} else if (i.value().is_number_integer()) {
 			string key = i.key();
-			form->addVariable<int>(i.key(), [this,data,key,suri](const int &f){
-				ctrl_->set(peer_, suri + string("/") + key, json_t(f));
+			form->addVariable<int>(i.key(), [this,data,key,&nc](const int &f){
+				nc.set(key, f);
 			}, [data,key]() -> int {
 				return data[key].get<int>();
 			});
 		} else if (i.value().is_number_float()) {
 			string key = i.key();
-			form->addVariable<float>(i.key(), [this,data,key,suri](const float &f){
-				ctrl_->set(peer_, suri + string("/") + key, json_t(f));
+			form->addVariable<float>(i.key(), [this,data,key,&nc](const float &f){
+				nc.set(key, f);
 			}, [data,key]() -> float {
 				return data[key].get<float>();
 			});
 		} else if (i.value().is_string()) {
 			string key = i.key();
-			form->addVariable<string>(i.key(), [this,data,key,suri](const string &f){
-				ctrl_->set(peer_, suri + string("/") + key, json_t(f));
+			form->addVariable<string>(i.key(), [this,data,key,&nc](const string &f){
+				nc.set(key, f);
 			}, [data,key]() -> string {
 				return data[key].get<string>();
 			});
 		} else if (i.value().is_object()) {
 			string key = i.key();
-			const ftl::config::json_t &v = i.value();
 		
-			form->addButton(i.key(), [this,form,suri,key,v]() {
-				_buildForm(suri+string("/")+key, v);
-			})->setIcon(ENTYPO_ICON_FOLDER);
+			// Checking the URI with exists() prevents unloaded local configurations from being shown.
+			if (suri.find('#') != string::npos && exists(suri+string("/")+key)) {
+				form->addButton(key, [this,suri,key]() {
+					_buildForm(suri+string("/")+key);
+				})->setIcon(ENTYPO_ICON_FOLDER);
+			} else if (exists(suri+string("#")+key)) {
+				form->addButton(key, [this,suri,key]() {
+					_buildForm(suri+string("#")+key);
+				})->setIcon(ENTYPO_ICON_FOLDER);
+			}
 		}
 	}
+
+	return references;
 }
 
-void ConfigWindow::_buildForm(const std::string &suri, ftl::config::json_t data) {
+void ConfigWindow::_buildForm(const std::string &suri) {
 	using namespace nanogui;
 
 	ftl::URI uri(suri);
@@ -105,11 +145,50 @@ void ConfigWindow::_buildForm(const std::string &suri, ftl::config::json_t data)
 	form->addWindow(Vector2i(100,50), uri.getFragment());
 	form->window()->setTheme(theme());
 
-	_addElements(form, suri, data);
+	ftl::config::json_t* config;
+	config = new ftl::config::json_t;
+	const std::string* allocated_suri = new std::string(suri);
+	std::vector<ftl::gui::ConfigWindow::References *> references;
+
+	ftl::Configurable* nc;
+
+	if (peer_) {
+		*config = ctrl_->get(peer_.value(), suri);
+		nc = new ftl::NetConfigurable(peer_.value(), *allocated_suri, *ctrl_, *config);
+
+		references = _addElements(form, *nc, *allocated_suri, [this](auto suri, auto &references) {
+			ftl::config::json_t* config = new ftl::config::json_t;
+			*config = ctrl_->get(peer_.value(), *suri);
+			auto nc = new ftl::NetConfigurable(peer_.value(), *suri, *ctrl_, *config);
+			auto r = new References(nc, config, suri);
+			references.push_back(r);
+			return nc;
+		});
+	} else {
+		nc = ftl::config::find(suri);
+		if (nc) {
+			references = _addElements(form, *nc, *allocated_suri, [this](auto suri, auto &references) {
+				return ftl::config::find(*suri);
+			});
+		}
+	}
 
-	auto closebutton = form->addButton("Close", [form]() {
+	auto closebutton = form->addButton("Close", [this,form,config,allocated_suri,nc,references]() {
 		form->window()->setVisible(false);
+		for(auto r : references) {
+			delete r;
+		}
+		if (peer_) {
+			delete nc;
+		}
+		delete config;
+		delete allocated_suri;
 		delete form;
 	});
 	closebutton->setIcon(ENTYPO_ICON_CROSS);
 }
+
+bool ConfigWindow::exists(const std::string &uri) {
+	// If the Configurable is a NetConfigurable, the URI is not checked.
+	return peer_ || ftl::config::find(uri);
+}
\ No newline at end of file
diff --git a/applications/gui/src/config_window.hpp b/applications/gui/src/config_window.hpp
index 87170b1d3..23279d93c 100644
--- a/applications/gui/src/config_window.hpp
+++ b/applications/gui/src/config_window.hpp
@@ -6,6 +6,7 @@
 #include <ftl/uuid.hpp>
 
 #include <nanogui/formhelper.h>
+#include <ftl/net_configurable.hpp>
 
 namespace ftl {
 namespace gui {
@@ -16,15 +17,23 @@ namespace gui {
 class ConfigWindow : public nanogui::Window {
 	public:
 	ConfigWindow(nanogui::Widget *parent, ftl::ctrl::Master *ctrl, const ftl::UUID &peer);
+	ConfigWindow(nanogui::Widget *parent, ftl::ctrl::Master *ctrl, const std::optional<ftl::UUID> &peer = std::nullopt);
 	~ConfigWindow();
 
 	private:
+	/*
+	References holds the pointers to a NetConfigurable and all its members so that
+	they can all be returned from _addElements() and then simultaneously deleted
+	as the form is closed.
+	*/
+	class References;
 	ftl::ctrl::Master *ctrl_;
-	ftl::UUID peer_;
+	std::optional<ftl::UUID> peer_;
 	std::vector<std::string> configurables_;
 	
-	void _buildForm(const std::string &uri, ftl::config::json_t data);
-	void _addElements(nanogui::FormHelper *form, const std::string &suri, const ftl::config::json_t &data);
+	void _buildForm(const std::string &uri);
+	std::vector<References *> _addElements(nanogui::FormHelper *form, ftl::Configurable &nc, const std::string &suri, std::function<ftl::Configurable*(const std::string*, std::vector<References *>&)> construct);
+	bool exists(const std::string &uri);
 };
 
 }
diff --git a/applications/gui/src/screen.cpp b/applications/gui/src/screen.cpp
index 5df535c3b..bb69755fb 100644
--- a/applications/gui/src/screen.cpp
+++ b/applications/gui/src/screen.cpp
@@ -17,6 +17,7 @@
 
 #include "ctrl_window.hpp"
 #include "src_window.hpp"
+#include "config_window.hpp"
 #include "camera.hpp"
 #include "media_panel.hpp"
 
@@ -212,12 +213,35 @@ ftl::gui::Screen::Screen(ftl::Configurable *proot, ftl::net::Universe *pnet, ftl
 		popup->setVisible(false);
 	});
 
-	button = new ToolButton(toolbar, ENTYPO_ICON_COG);
-	button->setIconExtraScale(1.5f);
-	button->setTheme(toolbuttheme);
-	button->setTooltip("Settings");
-	button->setFixedSize(Vector2i(40,40));
-	button->setPosition(Vector2i(5,height()-50));
+	popbutton = new PopupButton(innertool, "", ENTYPO_ICON_COG);
+	popbutton->setIconExtraScale(1.5f);
+	popbutton->setTheme(toolbuttheme);
+	popbutton->setTooltip("Settings");
+	popbutton->setFixedSize(Vector2i(40,40));
+	popbutton->setSide(Popup::Side::Right);
+	popbutton->setChevronIcon(0);
+	// popbutton->setPosition(Vector2i(5,height()-50));
+	popup = popbutton->popup();
+	popup->setLayout(new GroupLayout());
+	popup->setTheme(toolbuttheme);
+
+	auto node_details = ctrl_->getSlaves();
+	std::vector<std::string> node_titles;
+
+	for (auto &d : node_details) {
+		auto peer = ftl::UUID(d["id"].get<std::string>());
+		itembutton = new Button(popup, d["title"].get<std::string>());
+		itembutton->setCallback([this,popup,peer]() {
+			auto config_window = new ConfigWindow(this, ctrl_, peer);
+			config_window->setTheme(windowtheme);
+		});
+	}
+
+	itembutton = new Button(popup, "Local");
+	itembutton->setCallback([this,popup]() {
+		auto config_window = new ConfigWindow(this, ctrl_);
+		config_window->setTheme(windowtheme);
+	});
 
 	//configwindow_ = new ConfigWindow(parent, ctrl_);
 	cwindow_ = new ftl::gui::ControlWindow(this, controller);
diff --git a/components/common/cpp/include/ftl/configurable.hpp b/components/common/cpp/include/ftl/configurable.hpp
index 6593119c7..244b54828 100644
--- a/components/common/cpp/include/ftl/configurable.hpp
+++ b/components/common/cpp/include/ftl/configurable.hpp
@@ -80,6 +80,7 @@ class Configurable {
 	template <typename T>
 	void set(const std::string &name, T value) {
 		(*config_)[name] = value;
+		inject(name, (*config_)[name]);
 		_trigger(name);
 	}
 
@@ -103,12 +104,12 @@ class Configurable {
 	protected:
 	nlohmann::json *config_;
 
+	virtual void inject(const std::string name, nlohmann::json &value) {}
+
 	private:
 	std::map<std::string, std::list<std::function<void(const config::Event&)>>> observers_; 
 
 	void _trigger(const std::string &name);
-
-	static void __changeURI(const std::string &uri, Configurable *cfg);
 };
 
 /*template <>
diff --git a/components/common/cpp/src/configurable.cpp b/components/common/cpp/src/configurable.cpp
index f47e12195..5116292ad 100644
--- a/components/common/cpp/src/configurable.cpp
+++ b/components/common/cpp/src/configurable.cpp
@@ -59,8 +59,4 @@ void Configurable::on(const string &prop, function<void(const ftl::config::Event
 	} else {
 		(*ix).second.push_back(f);
 	}
-}
-
-void Configurable::__changeURI(const string &uri, Configurable *cfg) {
-
 }
\ No newline at end of file
diff --git a/components/net/cpp/CMakeLists.txt b/components/net/cpp/CMakeLists.txt
index c7e8bb341..9df16c0b2 100644
--- a/components/net/cpp/CMakeLists.txt
+++ b/components/net/cpp/CMakeLists.txt
@@ -9,13 +9,14 @@ add_library(ftlnet
 	src/dispatcher.cpp
 	src/universe.cpp
 	src/ws_internal.cpp
+	src/net_configurable.cpp
 )
 
 target_include_directories(ftlnet PUBLIC
 	$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
 	$<INSTALL_INTERFACE:include>
 	PRIVATE src)
-target_link_libraries(ftlnet ftlcommon Threads::Threads glog::glog ${UUID_LIBRARIES})
+target_link_libraries(ftlnet ftlctrl ftlcommon Threads::Threads glog::glog ${UUID_LIBRARIES})
 
 install(TARGETS ftlnet EXPORT ftlnet-config
 	ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
diff --git a/components/net/cpp/include/ftl/net_configurable.hpp b/components/net/cpp/include/ftl/net_configurable.hpp
new file mode 100644
index 000000000..3a31ecc27
--- /dev/null
+++ b/components/net/cpp/include/ftl/net_configurable.hpp
@@ -0,0 +1,26 @@
+#pragma once
+#ifndef _FTL_NETCONFIGURABLE_HPP_
+#define _FTL_NETCONFIGURABLE_HPP_
+
+#include <ftl/configurable.hpp>
+#include <ftl/master.hpp>
+
+namespace ftl {
+
+    class NetConfigurable : public ftl::Configurable {
+    public:
+	NetConfigurable(ftl::UUID peer, const std::string &suri, ftl::ctrl::Master &ctrl, ftl::config::json_t &config);
+	~NetConfigurable();
+
+	protected:
+	void inject(const std::string name, nlohmann::json &value);
+
+    private:
+	ftl::UUID peer;
+	const std::string &suri;
+	ftl::ctrl::Master &ctrl;
+    };
+
+}
+
+#endif // _FTL_NETCONFIGURABLE_HPP_
diff --git a/components/net/cpp/src/net_configurable.cpp b/components/net/cpp/src/net_configurable.cpp
new file mode 100644
index 000000000..edcec65b8
--- /dev/null
+++ b/components/net/cpp/src/net_configurable.cpp
@@ -0,0 +1,12 @@
+#include <ftl/net_configurable.hpp>
+
+#include <string>
+
+ftl::NetConfigurable::NetConfigurable(ftl::UUID peer, const std::string &suri, ftl::ctrl::Master &ctrl, ftl::config::json_t &config) : ftl::Configurable(config), peer(peer), suri(suri), ctrl(ctrl) {
+}
+
+ftl::NetConfigurable::~NetConfigurable(){}
+
+void ftl::NetConfigurable::inject(const std::string name, nlohmann::json &value) {
+    ctrl.set(peer, suri + std::string("/") + name, value);
+}
diff --git a/components/net/cpp/test/CMakeLists.txt b/components/net/cpp/test/CMakeLists.txt
index 34a642fa0..c786f73d8 100644
--- a/components/net/cpp/test/CMakeLists.txt
+++ b/components/net/cpp/test/CMakeLists.txt
@@ -27,11 +27,20 @@ target_link_libraries(net_integration
 	Threads::Threads
 	${UUID_LIBRARIES})
 
+### NetConfigurable Unit #######################################################
+add_executable(net_configurable_unit
+	./tests.cpp
+	./net_configurable_unit.cpp)
+target_link_libraries(net_configurable_unit
+	ftlnet)
+
 
 
 #add_test(ProtocolUnitTest protocol_unit)
 add_test(PeerUnitTest peer_unit)
 add_test(NetIntegrationTest net_integration)
+# Testing of NetConfigurable is disabled.
+#add_test(NetConfigurableUnitTest net_configurable_unit)
 
 add_custom_target(tests)
 add_dependencies(tests peer_unit uri_unit)
diff --git a/components/net/cpp/test/net_configurable_unit.cpp b/components/net/cpp/test/net_configurable_unit.cpp
new file mode 100644
index 000000000..c8bf42247
--- /dev/null
+++ b/components/net/cpp/test/net_configurable_unit.cpp
@@ -0,0 +1,43 @@
+#include "catch.hpp"
+#include <ftl/net_configurable.hpp>
+#include <ftl/slave.hpp>
+
+using ftl::NetConfigurable;
+
+SCENARIO( "NetConfigurable::set()" ) {
+    GIVEN( "valid peer UUID, URI and Master" ) {
+        // Set up Master
+        nlohmann::json json = {{"$id", "root"}, {"test", {{"listen", "tcp://localhost:7077"}}}}; // Check what values are needed
+        ftl::Configurable *root;
+        root = new ftl::Configurable(json);
+        ftl::net::Universe *net = ftl::config::create<ftl::net::Universe>(root, std::string("test"));
+        net->start();
+        ftl::ctrl::Master *controller = new ftl::ctrl::Master(root, net);
+        
+        // Set up a slave, then call getSlaves() to get the UUID string
+        nlohmann::json jsonSlave = {{"$id", "slave"}, {"test", {{"peers", {"tcp://localhost:7077"}}}}};
+        ftl::Configurable *rootSlave;
+        rootSlave = new ftl::Configurable(jsonSlave);
+        ftl::net::Universe *netSlave = ftl::config::create<ftl::net::Universe>(rootSlave, std::string("test"));
+        ftl::ctrl::Slave slave(netSlave, rootSlave);
+        netSlave->start();
+        netSlave->waitConnections();
+        net->waitConnections();
+
+        auto slaves = controller->getSlaves();
+        REQUIRE( slaves.size() == 1 );
+
+        ftl::UUID peer = ftl::UUID(slaves[0]["id"].get<std::string>());
+        const std::string suri = "slave_test";
+        nlohmann::json jsonTest = {{"$id", "slave_test"}, {"test", {{"peers", {"tcp://localhost:7077"}}}}};
+        NetConfigurable nc(peer, suri, *controller, jsonTest);
+        nc.set("test_value", 5);
+        REQUIRE( nc.get<int>("test_value") == 5 );
+    }
+
+    // invalid peer UUID
+
+    // invalid URI
+
+    // null Master
+}
\ No newline at end of file
-- 
GitLab