[education/rkward] rkward: Proof-of-concept implementation of frontend support for rk.menu()

Thomas Friedrichsmeier null at kde.org
Sun Sep 8 20:42:17 BST 2024


Git commit 78ff7a424d26750a66ad50dbd1b22f9de92cc663 by Thomas Friedrichsmeier.
Committed on 07/08/2024 at 20:25.
Pushed by tfry into branch 'master'.

Proof-of-concept implementation of frontend support for rk.menu()

M  +1    -0    rkward/misc/CMakeLists.txt
A  +98   -0    rkward/misc/rkrapimenu.cpp     [License: GPL(v2.0+)]
A  +23   -0    rkward/misc/rkrapimenu.h     [License: GPL(v2.0+)]
M  +2    -27   rkward/rbackend/rkrinterface.cpp
M  +5    -3    rkward/rbackend/rpackages/rkward/R/rk.menu.R
M  +4    -0    rkward/rkward.cpp
M  +3    -0    rkward/rkward.h

https://invent.kde.org/education/rkward/-/commit/78ff7a424d26750a66ad50dbd1b22f9de92cc663

diff --git a/rkward/misc/CMakeLists.txt b/rkward/misc/CMakeLists.txt
index 9287af026..ff0da775d 100644
--- a/rkward/misc/CMakeLists.txt
+++ b/rkward/misc/CMakeLists.txt
@@ -35,6 +35,7 @@ SET(misc_STAT_SRCS
    rkstyle.cpp
    rkparsedversion.cpp
    rkradiogroup.cpp
+   rkrapimenu.cpp
    )
 
 ADD_LIBRARY(misc STATIC ${misc_STAT_SRCS})
diff --git a/rkward/misc/rkrapimenu.cpp b/rkward/misc/rkrapimenu.cpp
new file mode 100644
index 000000000..8e8b9ee52
--- /dev/null
+++ b/rkward/misc/rkrapimenu.cpp
@@ -0,0 +1,98 @@
+/*
+rkrapimenu - This file is part of the RKWard project. Created: Wed Aug 07 2024
+SPDX-FileCopyrightText: 2024 by Thomas Friedrichsmeier <thomas.friedrichsmeier at kdemail.net>
+SPDX-FileContributor: The RKWard Team <rkward-devel at kde.org>
+SPDX-License-Identifier: GPL-2.0-or-later
+*/
+
+#include "rkrapimenu.h"
+
+#include <QDomElement>
+#include <QDomDocument>
+#include <QAction>
+
+#include <KActionCollection>
+#include <KXMLGUIFactory>
+
+#include "../rbackend/rkrinterface.h"
+#include "../core/robject.h"
+#include "../rkward.h"
+
+#include "../debug.h"
+
+RKRApiMenu::RKRApiMenu() : KXMLGUIClient() {
+	RK_TRACE(MISC);
+}
+
+RKRApiMenu::~RKRApiMenu() {
+	RK_TRACE(MISC);
+}
+
+void RKRApiMenu::makeXML(QDomDocument &doc, QDomElement e, const QVariantList &l, const QString &path, QStringList *actionlist) {
+	const auto id = l.value(0).toString();
+	const QString full_id = path.isEmpty() ? id : path + ',' + id;
+	const auto children = l.value(1).toList();
+	const auto label = l.value(2).toString();
+	const auto callable = l.value(3).toList().value(0).toBool();
+	auto s = doc.createElement(callable ? "Action" : "Menu");
+	s.setAttribute("name", callable ? full_id : id);
+	if (!label.isEmpty()) {
+		auto t = doc.createElement("Text");
+		t.appendChild(doc.createTextNode(label));
+		s.appendChild(t);
+	}
+	for (auto it = children.constBegin(); it != children.constEnd(); ++it) {
+		RK_ASSERT(!callable);
+		makeXML(doc, s, (*it).toList(), full_id, actionlist);
+	}
+	e.appendChild(s);
+
+	if (callable) {
+		auto a = action(full_id);
+		if (!a) {
+			a = new QAction();
+			a->setObjectName(full_id);
+			actionCollection()->addAction(full_id, a);
+			QObject::connect(a, &QAction::triggered, a, [full_id]() {
+				QString path;
+				auto segments = full_id.split(',');
+				for (int i = 0; i < segments.size(); ++i) {
+					if (i) path += ',';
+					path += RObject::rQuote(segments[i]);
+				}
+				RInterface::issueCommand(new RCommand("rk.menu()$item(" + path + ")$call()", RCommand::App));
+			});
+		}
+		a->setText(label);
+		actionlist->append(full_id);
+	}
+}
+
+void RKRApiMenu::updateFromR(const QVariantList &rep) {
+	RK_TRACE(MISC);
+
+	auto f = RKWardMainWindow::getMain()->factory();
+	f->removeClient(this);
+	QStringList actionlist;
+	QDomDocument doc;
+	auto menus = rep.value(1).toList();
+	auto r = doc.createElement("kpartgui");
+	r.setAttribute("name", "rapi_menu");  // TODO: version attribute
+	auto mb = doc.createElement("MenuBar");
+	for (auto it = menus.constBegin(); it != menus.constEnd(); ++it) {
+		makeXML(doc, mb, (*it).toList(), QString(), &actionlist);
+	}
+	r.appendChild(mb);
+	doc.appendChild(r);
+	setXMLGUIBuildDocument(doc);
+
+	// delete any actions that are no longer around
+	auto all_actions = actionCollection()->actions();
+	for (int i = 0; i < all_actions.size(); ++i) {
+		if (!actionlist.contains(all_actions[i]->objectName())) {
+			delete (actionCollection()->takeAction(all_actions[i]));
+		}
+	}
+
+	f->addClient(this);
+}
diff --git a/rkward/misc/rkrapimenu.h b/rkward/misc/rkrapimenu.h
new file mode 100644
index 000000000..35bed97ab
--- /dev/null
+++ b/rkward/misc/rkrapimenu.h
@@ -0,0 +1,23 @@
+/*
+rkrapimenu - This file is part of the RKWard project. Created: Wed Aug 07 2024
+SPDX-FileCopyrightText: 2024 by Thomas Friedrichsmeier <thomas.friedrichsmeier at kdemail.net>
+SPDX-FileContributor: The RKWard Team <rkward-devel at kde.org>
+SPDX-License-Identifier: GPL-2.0-or-later
+*/
+#ifndef RKRAPIMENU_H
+#define RKRAPIMENU_H
+
+#include <QVariantList>
+#include <KXMLGUIClient>
+
+/** KXMLGUIClient to represent the menu structure defined by rk.menu() from R API */
+class RKRApiMenu : public KXMLGUIClient {
+public:
+	RKRApiMenu();
+	~RKRApiMenu() override;
+	void updateFromR(const QVariantList &rep);
+private:
+	void makeXML(QDomDocument &doc, QDomElement e, const QVariantList &l, const QString &path, QStringList *actionlist);
+};
+
+#endif
diff --git a/rkward/rbackend/rkrinterface.cpp b/rkward/rbackend/rkrinterface.cpp
index 6ee206cb6..df445914c 100644
--- a/rkward/rbackend/rkrinterface.cpp
+++ b/rkward/rbackend/rkrinterface.cpp
@@ -37,6 +37,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
 #include "../misc/rkcommonfunctions.h"
 #include "../misc/rkmessagecatalog.h"
 #include "../misc/rkoutputdirectory.h"
+#include "../misc/rkrapimenu.h"
 #include "rksessionvars.h"
 #include "../windows/rkwindowcatcher.h"
 
@@ -47,8 +48,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
 #include <KLocalizedString>
 
 #include <QDir>
-#include <QDomElement>
-#include <QDomDocument>
 #include <stdlib.h>
 #include <QFileDialog>
 #include <QApplication>
@@ -801,31 +800,7 @@ GenericRRequestResult RInterface::processRCallRequest (const QString &call, cons
 	} else if (call == "switchLanguage") {
 		RKMessageCatalog::switchLanguage(arglist.value(0));
 	} else if (call == "menuupdate") {
-		QDomDocument doc;
-		std::function<void(QDomDocument &, QDomElement, const QVariantList &)> makeXml;
-		makeXml = [&makeXml](QDomDocument &doc, QDomElement e, const QVariantList &l) {
-			const auto id = l.value(0).toString();
-			const auto children = l.value(1).toList();
-			const auto label = l.value(2).toString();
-			const auto callable = l.value(3).toList().value(0).toBool();
-			auto s = doc.createElement(callable ? "Action" : "Menu");
-			s.setAttribute("name", id);
-			auto t = doc.createElement("Text");
-			t.appendChild(doc.createTextNode(label));
-			s.appendChild(t);
-			for (auto it = children.constBegin(); it != children.constEnd(); ++it) {
-				RK_ASSERT(!callable);
-				makeXml(doc, s, (*it).toList());
-			}
-			e.appendChild(s);
-		};
-		auto r = doc.createElement("kpartgui");
-		r.setAttribute("name", "rapi_menu");  // TODO: version attribute
-		auto mb = doc.createElement("MenuBar");
-		makeXml(doc, mb, args.toList());
-		r.appendChild(mb);
-		doc.appendChild(r);
-		qDebug("%s", qPrintable(doc.toString()));
+		RKWardMainWindow::getMain()->rApiMenu()->updateFromR(args.toList());
 	} else {
 		return GenericRRequestResult::makeError(i18n("Error: unrecognized request '%1'", call));
 	}
diff --git a/rkward/rbackend/rpackages/rkward/R/rk.menu.R b/rkward/rbackend/rpackages/rkward/R/rk.menu.R
index aa5490728..118e3590b 100644
--- a/rkward/rbackend/rpackages/rkward/R/rk.menu.R
+++ b/rkward/rbackend/rpackages/rkward/R/rk.menu.R
@@ -29,7 +29,8 @@
 #'
 #' @param func Function to call for leaf item
 #'
-#' @returns \code{rk.menu()} and \code{$item()} return a handle. \code{call()} passes on the return value of the associated function. The other methods return \code{NULL}
+#' @returns \code{rk.menu()} and \code{$item()} return a handle. \code{$define() returns the handle it was given (to allow command chaining)}.
+#'          \code{call()} passes on the return value of the associated function. The other methods return \code{NULL}
 #'
 #' @import methods
 #' @export rk.menu
@@ -59,7 +60,7 @@ rk.menu <- setRefClass("rk.menu",
 			x$fun <- func
 		}
 		.rk.call.async("menuupdate", rk.menu()$.list())
-		invisible(NULL)
+		invisible(rk.menu(path=path))
 	},
 	call=function() {
 		"Call the function associated with this menu item"
@@ -69,7 +70,8 @@ rk.menu <- setRefClass("rk.menu",
 	remove=function() {
 		"Remove any registered menu entry at this path from the menu"
 		parent <- rk.menu(path=path[1:length(path)-1])$.retrieve(FALSE)
-		rm(list=path[length(path)], envir=parent$children)
+		parent$children[[path[length(path)]]] <- NULL
+		.rk.call.async("menuupdate", rk.menu()$.list())
 		invisible(NULL)
 	},
 	enable=function(enable=TRUE, show=TRUE) {
diff --git a/rkward/rkward.cpp b/rkward/rkward.cpp
index baeb4b766..a94cf38de 100644
--- a/rkward/rkward.cpp
+++ b/rkward/rkward.cpp
@@ -58,6 +58,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
 #include "misc/rkcommonfunctions.h"
 #include "misc/rkxmlguisyncer.h"
 #include "misc/rkdialogbuttonbox.h"
+#include "misc/rkrapimenu.h"
 #include "misc/rkstyle.h"
 #include "dialogs/rkloadlibsdialog.h"
 #include "dialogs/rkimportdialog.h"
@@ -155,6 +156,7 @@ RKWardMainWindow::RKWardMainWindow() : KParts::MainWindow() {
 	setXMLFile ("rkwardui.rc");
 	insertChildClient (toplevel_actions = new RKTopLevelWindowGUI (this));
 	insertChildClient (katePluginIntegration ()->mainWindow ());
+	insertChildClient(rapimenu = new RKRApiMenu());
 	createShellGUI (true);
 	// This is pretty convoluted, but while loading plugins the katePluginIntegration-client may gain new actions and thus needs
 	// to be reloaded. We cannot - currently, KF5.65 - delay loading the UI defintion(s), because plugins rely on it having a GUI factory.
@@ -194,6 +196,8 @@ RKWardMainWindow::~RKWardMainWindow() {
 	factory ()->removeClient (RKComponentMap::getMap ());
 	delete RKComponentMap::getMap ();
 	delete RKStyle::_view_scheme;
+	factory()->removeClient(rapimenu);
+	delete rapimenu;
 }
 
 KatePluginIntegrationApp* RKWardMainWindow::katePluginIntegration () {
diff --git a/rkward/rkward.h b/rkward/rkward.h
index de7e2a4f2..a6bc9b948 100644
--- a/rkward/rkward.h
+++ b/rkward/rkward.h
@@ -20,6 +20,7 @@ class RKTopLevelWindowGUI;
 class KSqueezedTextLabel;
 class QAction;
 class KatePluginIntegrationApp;
+class RKRApiMenu;
 
 /**
 The main class of rkward. This is where all strings are tied together, controls the initialization, and there are some of the most important slots for user actions. All real work is done elsewhere.
@@ -124,6 +125,7 @@ public Q_SLOTS:
 /** Trigger restart of backend. Does not wait for restart to actually complete.
  *  Return true, if restart was triggered, false if restart has been cancelled */
 	bool triggerBackendRestart(bool promptsave=true);
+	RKRApiMenu* rApiMenu() { return rapimenu; };
 private Q_SLOTS:
 	void partChanged (KParts::Part *new_part);
 private:
@@ -189,6 +191,7 @@ private:
 
 	KatePluginIntegrationApp *katepluginintegration;
 	KXMLGUIClient *active_ui_buddy;
+	RKRApiMenu *rapimenu;
 friend class RKWardCoreTest;
 	bool testmode_suppress_dialogs;
 public:



More information about the rkward-tracker mailing list