[education/rkward] /: Allow the callback in addChangeCommand to be a callable value, directly.

Thomas Friedrichsmeier null at kde.org
Wed May 7 14:21:37 BST 2025


Git commit f70df378a674db997ffa2bfc1887ca67b3236197 by Thomas Friedrichsmeier.
Committed on 07/05/2025 at 13:21.
Pushed by tfry into branch 'master'.

Allow the callback in addChangeCommand to be a callable value, directly.

M  +2    -0    ChangeLog
M  +14   -14   doc/rkwardplugins/index.docbook
M  +10   -11   rkward/plugins/00saveload/save/save.xml
M  +5    -8    rkward/plugins/data/generate_random.xml
M  +2    -4    rkward/plugins/data/recode_categorical.xml
M  +3    -6    rkward/plugins/data/sort.xml
M  +3    -6    rkward/plugins/data/sort2.xml
M  +9    -10   rkward/plugins/testing/test1.xml
M  +16   -24   rkward/scriptbackends/rkcomponentscripting.cpp
M  +1    -6    rkward/scriptbackends/rkcomponentscripting.h

https://invent.kde.org/education/rkward/-/commit/f70df378a674db997ffa2bfc1887ca67b3236197

diff --git a/ChangeLog b/ChangeLog
index 3d25dc454..613c02f98 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,4 +1,6 @@
 --- Version 0.8.2 - UNRELEASED
+- Added: Plugins: Enhance addChangeCommand() to accept callable objects, directly
+- Added: Plugins: Simplify running R commands inside GUI logic code
 - Fixed: Crash on script errors in plugins
 - Fixed: Printing of captured R messages/warnings in plugins was broken
 - Internal: Code cleanups
diff --git a/doc/rkwardplugins/index.docbook b/doc/rkwardplugins/index.docbook
index 885c8cac2..3c3d33da8 100644
--- a/doc/rkwardplugins/index.docbook
+++ b/doc/rkwardplugins/index.docbook
@@ -58,8 +58,8 @@ as Authors, publish date, the abstract, and Keywords -->
      and in the FDL itself on how to use it. -->
 <legalnotice>&FDLNotice;</legalnotice>
 
-<date>2024-09-07</date>
-<releaseinfo>0.8.1</releaseinfo>
+<date>2025-05-03</date>
+<releaseinfo>0.8.2</releaseinfo>
 
 <abstract>
 <para>
@@ -919,16 +919,13 @@ R code.
 '
 	<logic>
 		<script><![CDATA[
-			// ECMAScript code in this block
-			// the top-level statement is only called once
-			gui.addChangeCommand ("mode.string", "modeChanged ()");
-
-			// this function is called whenever the "mode" was changed
-			modeChanged = function () {
-				var varmode = (gui.getString ("mode.string") == "variable");
-				gui.setValue ("y.enabled", varmode);
-				gui.setValue ("constant.enabled", !varmode);
-			}
+			// [...] any code at the top level is called only once
+			gui.addChangeCommand("mode.string", function() {
+				// while this anonymous function will be called, whenever "mode.string" changes
+				var varmode = (gui.getString("mode.string") == "variable");
+				gui.setValue("y.enabled", varmode);
+				gui.setValue("constant.enabled", !varmode);
+			});
 		]]></script>
 	</logic>
 
@@ -936,8 +933,11 @@ R code.
 	[...]
 	</programlisting>
 	<para>
[suppressed due to size limit]
[suppressed due to size limit]
 	</para>
+	<note><para>
+		Alternatively, <command>gui.addChangeCommmand()</command> accepts a string to be evaluated on changes, e.g. <replaceable>"my_update_function()"</replaceable>. This can be useful, if you want to call the same function for changes in several different UI elements.
+	</para></note>
 	<para>
 		The scripted approach to &GUI; logic becomes particularly useful when you want to change the available option according to the type of object that the user has selected. See <link linkend="guilogic_functions">the reference</link> for available functions.
 	</para>
@@ -4477,7 +4477,7 @@ different types, using modifiers may lead to errors. For <replaceable>fixed_valu
 	<varlistentry><term>getList(id)</term><listitem><para>Returns the value of the given child property as an array of strings (if possible). Returns the value of this property, if ID is omitted.</para></listitem></varlistentry>
 	<varlistentry><term>setValue(id, value)</term><listitem><para>Set the value of the given child property to <emphasis>value</emphasis>.</para></listitem></varlistentry>
 	<varlistentry><term>getChild(id)</term><listitem><para>Return an instance of the child-property with the given <emphasis>id</emphasis>.</para></listitem></varlistentry>
-	<varlistentry><term>addChangeCommand(id, command)</term><listitem><para>Execute <emphasis>command</emphasis> whenever the child property given by <emphasis>id</emphasis> changes.</para></listitem></varlistentry>
+	<varlistentry><term>addChangeCommand(id, command)</term><listitem><para>Execute <emphasis>command</emphasis> whenever the child property given by <emphasis>id</emphasis> changes. Command can either be a string to be evaluated, or callable value (usually a function).</para></listitem></varlistentry>
 	</variablelist></para></listitem>
 </varlistentry>
 <varlistentry><term>Class "RObject"</term>
diff --git a/rkward/plugins/00saveload/save/save.xml b/rkward/plugins/00saveload/save/save.xml
index 7110933f0..a2be7e2b7 100644
--- a/rkward/plugins/00saveload/save/save.xml
+++ b/rkward/plugins/00saveload/save/save.xml
@@ -17,17 +17,16 @@ SPDX-License-Identifier: GPL-2.0-or-later
 		<connect governor="lgc_compress" client="complevel.enabled" />
 		<connect governor="lgc_cmprssxz" client="xzextreme.enabled" />
 		<script>
-			<![CDATA[ gui.addChangeCommand("compress.string", "compressionChanged()");
-					// try to set compression level dynamically
-					// run each time the compression method is changed
-					compressionChanged = function(){
-							var thisObject = gui.getValue("compress.string");
-							 if(thisObject == "bzip2" | thisObject == "xz"){
-								gui.setValue("complevel.int", 9);
-							} else {
-								gui.setValue("complevel.int", 6);
-							}
-						} ]]>
+			<![CDATA[
+				gui.addChangeCommand("compress.string", function() {
+					var thisObject = gui.getValue("compress.string");
+					if(thisObject == "bzip2" | thisObject == "xz"){
+						gui.setValue("complevel.int", 9);
+					} else {
+						gui.setValue("complevel.int", 6);
+					}
+				})
+			]]>
 		</script>
 	</logic>
 	<dialog label="Save R objects">
diff --git a/rkward/plugins/data/generate_random.xml b/rkward/plugins/data/generate_random.xml
index 4eb25adbb..f90d8bf84 100644
--- a/rkward/plugins/data/generate_random.xml
+++ b/rkward/plugins/data/generate_random.xml
@@ -9,14 +9,11 @@ SPDX-License-Identifier: GPL-2.0-or-later
 	<logic>
 		<connect governor="current_object" client="saveto.parent"/>
 		<script><![CDATA[
-			// the top-level block is called only once
-			gui.addChangeCommand ("saveto.parent", "parentChanged ()");
-
-			// this function is called on every change of the saveto's parent
-			parentChanged = function () {
-				parent_object = makeRObject (gui.getValue ("saveto.parent"));
-				gui.setValue ("length.enabled", !parent_object.isDataFrame ());
-			}
+			gui.addChangeCommand("saveto.parent", function() {
+				// this function is called on every change of the saveto's parent
+				parent_object = makeRObject(gui.getValue("saveto.parent"));
+				gui.setValue("length.enabled", !parent_object.isDataFrame());
+			});
 			]]></script>
 	</logic>
 	<dialog label="Generate random data (normal distribution)">
diff --git a/rkward/plugins/data/recode_categorical.xml b/rkward/plugins/data/recode_categorical.xml
index 2ce05e0db..0c46db6d3 100644
--- a/rkward/plugins/data/recode_categorical.xml
+++ b/rkward/plugins/data/recode_categorical.xml
@@ -18,12 +18,10 @@ SPDX-License-Identifier: GPL-2.0-or-later
 		<connect governor="other_values_custom" client="other_custom.enabled"/>
 
 		<script><![CDATA[
-			gui.addChangeCommand ("datamode", "update ()");
-
-			update = function () {
+			gui.addChangeCommand("datamode", function() {
 				var mode = gui.getValue ("datamode.string");
 				gui.setValue ("set.contents.quotation_note.visible", mode == "factor" || mode == "character");
-			}
+			});
 		]]></script>
 	</logic>
 	<dialog label="Recode categorical data"><tabbook>
diff --git a/rkward/plugins/data/sort.xml b/rkward/plugins/data/sort.xml
index 07d3c1138..14975c89a 100644
--- a/rkward/plugins/data/sort.xml
+++ b/rkward/plugins/data/sort.xml
@@ -22,11 +22,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
 		<convert id="custom_conversion" mode="equals" sources="conversion.string" standard="custom"/>
 		<connect governor="custom_conversion" client="conversion_custom.enabled"/>
 		<script><![CDATA[
-			// the top-level block is called only once
-			gui.addChangeCommand ("object.available", "objectChanged ()");
-
-			// this function is called on every change of the saveto's parent
-			objectChanged = function () {
+			gui.addChangeCommand ("object.available", function () {
+				// this function is called on every change of the saveto's parent
 				object = makeRObject (gui.getValue ("object.available"));
 				gui.setValue ("sortby.enabled", object.isDataFrame ());
 				if (object.isDataFrame ()) gui.setValue ("selector.root", object.getName ());
@@ -38,7 +35,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
 					// Not very elegant, but does the trick
 					gui.setValue ("notice.text", i18n ("Sorting this type of object is not supported in this plugin"));
 				}
-			}
+			});
 			]]></script>
 	</logic>
 
diff --git a/rkward/plugins/data/sort2.xml b/rkward/plugins/data/sort2.xml
index c0cd9b4c6..65faca967 100644
--- a/rkward/plugins/data/sort2.xml
+++ b/rkward/plugins/data/sort2.xml
@@ -17,11 +17,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
 		<connect governor="saveto_other_object" client="saveto.enabled"/>
 		<connect governor="saveto_other_object" client="saveto.required"/>
 		<script><![CDATA[
-			// the top-level block is called only once
-			gui.addChangeCommand ("object.available", "objectChanged ()");
-
-			// this function is called on every change of the saveto's parent
-			objectChanged = function () {
+			gui.addChangeCommand ("object.available", function () {
+				// this function is called on every change of the saveto's parent
 				object = makeRObject (gui.getValue ("object.available"));
 				gui.setValue ("sortby_frame.enabled", object.isDataFrame ());
 				gui.setValue ("sortby.required", object.isDataFrame ());
@@ -32,7 +29,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
 					// Not very elegant, but does the trick
 					gui.setValue ("notice.text", i18n ("Sorting this type of object is not supported in this plugin"));
 				}
-			}
+			});
 			]]></script>
 	</logic>
 	<dialog label="Sort data (Variant 2)">
diff --git a/rkward/plugins/testing/test1.xml b/rkward/plugins/testing/test1.xml
index 7ee93a068..dbcd047b5 100644
--- a/rkward/plugins/testing/test1.xml
+++ b/rkward/plugins/testing/test1.xml
@@ -11,15 +11,13 @@ SPDX-License-Identifier: GPL-2.0-or-later
 	<logic>
 		<script><![CDATA[
 			call_num = 0;
- 			last_command_id = -1;
 			gui.setValue ("text.text", i18n (noquote ("Select a dependent variable!")));
 
 			/*f = Kross.module('forms');
 			label = f.createWidget(scripty, 'QLabel', 'Label', {});
 			label.setText ('<b>This label was created by the script. Be sure to read the label above, too.</b>');*/
 
-			gui.addChangeCommand ("x.available", "updateText ()");
-			updateText = function () {
+			gui.addChangeCommand ("x.available", function() {
 				call_num += 1;
 				obj = makeRObject (gui.getValue ("x.available"));
 				text = "So, you think it's '" + obj.objectname + "'?\n";
@@ -27,12 +25,13 @@ SPDX-License-Identifier: GPL-2.0-or-later
 				text += "Updates of this text so far: " + call_num;
 				gui.setValue ("text.text", text);
 
-				if (obj.objectname != "") last_command_id = doRCommand ("levels (" + obj.objectname + ")", "levelsCommandFinished");
-			}
-			levelsCommandFinished = function (value, id) {
-				otext = gui.getValue ("text.text");
-				gui.setValue ("text.text", otext + "\n" + last_command_id + "-" + id + "\nadas" + value.join ("-"));
-			}
+				if (obj.objectname != "") last_command_id = new RCommand("levels (" + obj.objectname + ")", "levelscom").then(result => {
+					otext = gui.getValue ("text.text");
+					gui.setValue ("text.text", otext + "\n" + last_command_id + "-" + id + "\nadas" + value.join ("-"));
+				}).catch(err => {
+					// no error handling
+				});
+			});
 			]]></script>
 	</logic>
 
@@ -59,4 +58,4 @@ SPDX-License-Identifier: GPL-2.0-or-later
 		</tabbook>
 	</dialog>
 
-</document>
\ No newline at end of file
+</document>
diff --git a/rkward/scriptbackends/rkcomponentscripting.cpp b/rkward/scriptbackends/rkcomponentscripting.cpp
index 7d71e65e6..e3b8a9fd2 100644
--- a/rkward/scriptbackends/rkcomponentscripting.cpp
+++ b/rkward/scriptbackends/rkcomponentscripting.cpp
@@ -92,24 +92,33 @@ void RKComponentScriptingProxy::evaluate(const QString &code, const QString &fil
 	handleScriptError(result, filename);
 }
 
-void RKComponentScriptingProxy::addChangeCommand(const QString &changed_id, const QString &command) {
+void RKComponentScriptingProxy::addChangeCommand(const QString &changed_id, const QJSValue &command) {
 	RK_TRACE(PHP);
 
 	QString remainder;
 	RKComponentBase *base = component->lookupComponent(changed_id, &remainder);
+	if (!remainder.isEmpty()) {
+		evaluate(QStringLiteral("error ('No such property %1 (failed portion was %2)');\n").arg(changed_id, remainder));
+		return;
+	}
 
-	if (remainder.isEmpty()) {
-		component_commands.insert(base, command);
-		if (base->isComponent()) {
-			connect(static_cast<RKComponent *>(base), &RKComponent::componentChanged, this, &RKComponentScriptingProxy::componentChanged);
+	auto callback = [this, command](RKComponentBase *) {
+		if (command.isCallable()) {
+			auto res = command.call();
+			handleScriptError(res);
 		} else {
-			connect(static_cast<RKComponentPropertyBase *>(base), &RKComponentPropertyBase::valueChanged, this, &RKComponentScriptingProxy::propertyChanged);
+			evaluate(command.toString());
 		}
+	};
+
+	if (base->isComponent()) {
+		connect(static_cast<RKComponent *>(base), &RKComponent::componentChanged, this, callback);
 	} else {
-		evaluate(QStringLiteral("error ('No such property %1 (failed portion was %2)');\n").arg(changed_id, remainder));
+		connect(static_cast<RKComponentPropertyBase *>(base), &RKComponentPropertyBase::valueChanged, this, callback);
 	}
 }
 
+// TODO: retire this function (how do we depracate js-level calls?
 QVariant RKComponentScriptingProxy::doRCommand(const QString &command, const QString &callback) {
 	RK_TRACE(PHP);
 
@@ -206,23 +215,6 @@ void RKComponentScriptingProxy::scriptRCommandFinished(RCommand *command) {
 	handleScriptError(res);
 }
 
-void RKComponentScriptingProxy::componentChanged(RKComponent *changed) {
-	RK_TRACE(PHP);
-	handleChange(changed);
-}
-
-void RKComponentScriptingProxy::propertyChanged(RKComponentPropertyBase *changed) {
-	RK_TRACE(PHP);
-	handleChange(changed);
-}
-
-void RKComponentScriptingProxy::handleChange(RKComponentBase *changed) {
-	RK_TRACE(PHP);
-
-	QString command = component_commands.value(changed);
-	evaluate(command);
-}
-
 QVariant RKComponentScriptingProxy::getValue(const QString &id) const {
 	RK_TRACE(PHP);
 	return (component->fetchValue(id, RKComponent::TraditionalValue));
diff --git a/rkward/scriptbackends/rkcomponentscripting.h b/rkward/scriptbackends/rkcomponentscripting.h
index 015bd90b3..e741befa5 100644
--- a/rkward/scriptbackends/rkcomponentscripting.h
+++ b/rkward/scriptbackends/rkcomponentscripting.h
@@ -32,14 +32,10 @@ class RKComponentScriptingProxy : public QObject {
 	~RKComponentScriptingProxy();
 
 	void initialize(const QString &file, const QString &command);
-  public Q_SLOTS:
-	void componentChanged(RKComponent *changed);
-	void propertyChanged(RKComponentPropertyBase *changed);
-
   public:
 	// these are meant to be called from the script
 	Q_INVOKABLE void include(const QString &filename);
-	Q_INVOKABLE void addChangeCommand(const QString &changed_id, const QString &command);
+	Q_INVOKABLE void addChangeCommand(const QString &changed_id, const QJSValue &command);
 	/** @returns id of the command issued. */
 	Q_INVOKABLE QVariant doRCommand(const QString &command, const QString &callback);
 	Q_INVOKABLE void doRCommand2(const QString &command, const QString &id, const QJSValue resolve, const QJSValue reject);
@@ -72,7 +68,6 @@ class RKComponentScriptingProxy : public QObject {
 	void evaluate(const QString &code, const QString &filename = QString());
 
 	void handleChange(RKComponentBase *changed);
-	QHash<RKComponentBase *, QString> component_commands;
 
 	void handleScriptError(const QJSValue &val, const QString &current_file = QString());
 };



More information about the kde-doc-english mailing list