[education/rkward] /: Allow registering user preview functions for script windows

Thomas Friedrichsmeier null at kde.org
Mon Jan 12 19:55:45 GMT 2026


Git commit 3ad06e225072889e11e04481e92bad925bff0ed9 by Thomas Friedrichsmeier.
Committed on 12/01/2026 at 19:55.
Pushed by tfry into branch 'master'.

Allow registering user preview functions for script windows

M  +1    -1    VERSION.cmake
M  +3    -0    rkward/rbackend/rkrinterface.cpp
M  +1    -1    rkward/rbackend/rpackages/rkward/DESCRIPTION
M  +1    -0    rkward/rbackend/rpackages/rkward/NAMESPACE
M  +48   -0    rkward/rbackend/rpackages/rkward/R/rk.preview-functions.R
M  +1    -1    rkward/rbackend/rpackages/rkward/man/rk.eval.as.preview.Rd
A  +57   -0    rkward/rbackend/rpackages/rkward/man/rk.register.script.preview.Rd
A  +20   -0    rkward/rbackend/rpackages/rkward/man/rk.render.markdown.preview.Rd
M  +20   -2    rkward/windows/rkcommandeditorwindow.cpp
M  +2    -0    rkward/windows/rkcommandeditorwindow.h

https://invent.kde.org/education/rkward/-/commit/3ad06e225072889e11e04481e92bad925bff0ed9

diff --git a/VERSION.cmake b/VERSION.cmake
index afb316a9f..f6099e76e 100644
--- a/VERSION.cmake
+++ b/VERSION.cmake
@@ -1,3 +1,3 @@
 # DO NOT CHANGE THIS FILE MANUALLY!
 # It will be overwritten by scripts/set_dist_version.sh
-SET(RKVERSION_NUMBER 0.8.2z+0.8.3+devel3)
+SET(RKVERSION_NUMBER 0.8.2z+0.8.3+devel4)
diff --git a/rkward/rbackend/rkrinterface.cpp b/rkward/rbackend/rkrinterface.cpp
index 583156007..0bae9f0d9 100644
--- a/rkward/rbackend/rkrinterface.cpp
+++ b/rkward/rbackend/rkrinterface.cpp
@@ -34,6 +34,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
 #include "../settings/rksettingsmoduleplugins.h"
 #include "../settings/rksettingsmoduler.h"
 #include "../windows/rcontrolwindow.h"
+#include "../windows/rkcommandeditorwindow.h"
 #include "../windows/rkcommandlog.h"
 #include "../windows/rkhtmlwindow.h"
 #include "../windows/rkwindowcatcher.h"
@@ -743,6 +744,8 @@ GenericRRequestResult RInterface::processRCallRequest(const QString &call, const
 		dialog->addRCommand(command, true);
 		issueCommand(command, in_chain);
 		dialog->doNonModal(true);
+	} else if (call == QLatin1String("registerScriptPreviewMode")) {
+		RKCommandEditorWindow::registerUserPreviewMode(arglist.value(0), arglist.value(1), arglist.value(2), arglist.value(3));
 	} else if (call == QLatin1String("rstarted")) {
 		// The backend thread has finished basic initialization, but we still have more to do...
 		backend_error.message.append(args.toString());
diff --git a/rkward/rbackend/rpackages/rkward/DESCRIPTION b/rkward/rbackend/rpackages/rkward/DESCRIPTION
index 751b3e55c..686792624 100755
--- a/rkward/rbackend/rpackages/rkward/DESCRIPTION
+++ b/rkward/rbackend/rpackages/rkward/DESCRIPTION
@@ -15,7 +15,7 @@ LazyLoad: yes
 Authors at R: c(person(given="Thomas", family="Friedrichsmeier", email="thomas.friedrichsmeier at kdemail.net", role=c("aut")), person(given="the RKWard", family="team",
                     email="rkward at kde.org", role=c("cre","aut")))
 Version: 0.8.3
-Date: 2025-11-02
+Date: 2025-12-13
 RoxygenNote: 7.3.3
 Collate: 
     'base_overrides.R'
diff --git a/rkward/rbackend/rpackages/rkward/NAMESPACE b/rkward/rbackend/rpackages/rkward/NAMESPACE
index 4fa6f0687..b857046a5 100644
--- a/rkward/rbackend/rpackages/rkward/NAMESPACE
+++ b/rkward/rbackend/rpackages/rkward/NAMESPACE
@@ -100,6 +100,7 @@ export(rk.print.literal)
 export(rk.printer.device)
 export(rk.record.commands)
 export(rk.record.plot)
+export(rk.register.script.preview)
 export(rk.relative.src.line)
 export(rk.removethis.plot)
 export(rk.rename.in.container)
diff --git a/rkward/rbackend/rpackages/rkward/R/rk.preview-functions.R b/rkward/rbackend/rpackages/rkward/R/rk.preview-functions.R
index 957b3a9eb..399150578 100644
--- a/rkward/rbackend/rpackages/rkward/R/rk.preview-functions.R
+++ b/rkward/rbackend/rpackages/rkward/R/rk.preview-functions.R
@@ -214,3 +214,51 @@ function registerPlot(element) {
 	# clean up is done via on.exit handlers, above
 	invisible()
 }
+
+#' Register a custom preview mode for script windows
+#'
+#' @param id An id string to allow subsequent modification or deletion of the preview mode
+#' @param label The label shown in the GUI for this preview mode
+#' @param file.extension The input file (esp. if previously unsaved) will be given this file extension. May be relevant for
+#'        certain external tools.
+#' @param statement The name of a function to run, when the preview needs to be updated, given as a string (see details).
+#'
+#' @details The key parameter is \code{statement}, which is string that will be evaulated as a function, usually the name of a
+#'          custom function inside \code{globalenv()}. If this is an empty string, the preview mode is removed.
+#'
+#'          The given function string needs to be callable with the following function signature:
+#'          \code{my_function(input.file, output.dir, preview.id, ...)}. Here,
+#'
+#'          \itemize{
+#'              \item{\code{input.file} character string specifying the (temporary) input file with the script's contents}
+#'              \item{\code{output.dir} character string specifying a directory suitable for keeping temporary files related
+#'                    to this preview. This will be cleared, automatically, as needed.}
+#'              \item{\code{preview.id} a unique id of the preview window. This could be used to keep temporary data, for instance.}
+#'              \item{\code{...} for future extension}
+#'          }
+#'
+#'          As a final step the preview funtion will generally call either \code{rk.show.html()}, or \code{rk.show.pdf()}, but it could
+#'          also show data using \code{rk.edit}, or produce a plot. Some care has to be taken in order not to interfere with existing
+#'          device windows, or output windows. Looking at the implementation of e.g. \code{rk.eval.as.preview()} may be instructive.
+#'          (This feature is targetted at power users, expecting them to consider and deal with any side-effects, themselves.)
+#'
+#' @examples
+#' \dontrun{
+#' my_csvpreview <- function(infile, outdir, previewid, ...) {
+#'   require("R2HTML")
+#'   suppressWarnings({
+#'     data <- read.csv(infile)
+#'   })
+#'   outfile <- paste0(outdir, "/out.html")
+#'   unlink(outfile) # clear previous output
+#'   HTML(data, file=outfile)
+#'   rk.show.html(outfile)
+#' }
+#' rk.register.script.preview("csv", "Preview CSV data", ".csv", "my_csvpreview"))
+#' }
+#'
+#' @rdname rk.register.script.preview
+#' @export
+rk.register.script.preview <- function(id, label, file.extension=".R", statement) {
+  .rk.call("registerScriptPreviewMode", c(id, label, file.extension, statement))
+}
diff --git a/rkward/rbackend/rpackages/rkward/man/rk.eval.as.preview.Rd b/rkward/rbackend/rpackages/rkward/man/rk.eval.as.preview.Rd
index e9ce26afa..e32cbfd97 100644
--- a/rkward/rbackend/rpackages/rkward/man/rk.eval.as.preview.Rd
+++ b/rkward/rbackend/rpackages/rkward/man/rk.eval.as.preview.Rd
@@ -1,5 +1,5 @@
 % Generated by roxygen2: do not edit by hand
-% Please edit documentation in R/rk.filename-functions.R
+% Please edit documentation in R/rk.preview-functions.R
 \name{rk.eval.as.preview}
 \alias{rk.eval.as.preview}
 \title{Evaluate the given input file, recording a transcript to an HTML output file (including on-screen plots)}
diff --git a/rkward/rbackend/rpackages/rkward/man/rk.register.script.preview.Rd b/rkward/rbackend/rpackages/rkward/man/rk.register.script.preview.Rd
new file mode 100644
index 000000000..9b01acdd9
--- /dev/null
+++ b/rkward/rbackend/rpackages/rkward/man/rk.register.script.preview.Rd
@@ -0,0 +1,57 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/rk.preview-functions.R
+\name{rk.register.script.preview}
+\alias{rk.register.script.preview}
+\title{Register a custom preview mode for script windows}
+\usage{
+rk.register.script.preview(id, label, file.extension = ".R", statement)
+}
+\arguments{
+\item{id}{An id string to allow subsequent modification or deletion of the preview mode}
+
+\item{label}{The label shown in the GUI for this preview mode}
+
+\item{file.extension}{The input file (esp. if previously unsaved) will be given this file extension. May be relevant for
+certain external tools.}
+
+\item{statement}{The name of a function to run, when the preview needs to be updated, given as a string (see details).}
+}
+\description{
+Register a custom preview mode for script windows
+}
+\details{
+The key parameter is \code{statement}, which is string that will be evaulated as a function, usually the name of a
+         custom function inside \code{globalenv()}. If this is an empty string, the preview mode is removed.
+
+         The given function string needs to be callable with the following function signature:
+         \code{my_function(input.file, output.dir, preview.id, ...)}. Here,
+
+         \itemize{
+             \item{\code{input.file} character string specifying the (temporary) input file with the script's contents}
+             \item{\code{output.dir} character string specifying a directory suitable for keeping temporary files related
+                   to this preview. This will be cleared, automatically, as needed.}
+             \item{\code{preview.id} a unique id of the preview window. This could be used to keep temporary data, for instance.}
+             \item{\code{...} for future extension}
+         }
+
+         As a final step the preview funtion will generally call either \code{rk.show.html()}, or \code{rk.show.pdf()}, but it could
+         also show data using \code{rk.edit}, or produce a plot. Some care has to be taken in order not to interfere with existing
+         device windows, or output windows. Looking at the implementation of e.g. \code{rk.eval.as.preview()} may be instructive.
+         (This feature is targetted at power users, expecting them to consider and deal with any side-effects, themselves.)
+}
+\examples{
+\dontrun{
+my_csvpreview <- function(infile, outdir, previewid, ...) {
+  require("R2HTML")
+  suppressWarnings({
+    data <- read.csv(infile)
+  })
+  outfile <- paste0(outdir, "/out.html")
+  unlink(outfile) # clear previous output
+  HTML(data, file=outfile)
+  rk.show.html(outfile)
+}
+rk.register.script.preview("csv", "Preview CSV data", ".csv", "my_csvpreview"))
+}
+
+}
diff --git a/rkward/rbackend/rpackages/rkward/man/rk.render.markdown.preview.Rd b/rkward/rbackend/rpackages/rkward/man/rk.render.markdown.preview.Rd
new file mode 100644
index 000000000..dd8896b50
--- /dev/null
+++ b/rkward/rbackend/rpackages/rkward/man/rk.render.markdown.preview.Rd
@@ -0,0 +1,20 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/rk.preview-functions.R
+\name{rk.render.markdown.preview}
+\alias{rk.render.markdown.preview}
+\title{Render a preview of the given R markdown file}
+\usage{
+rk.render.markdown.preview(infile, outdir, ...)
+}
+\arguments{
+\item{infile}{The input Rmd file, specified as a character string.}
+
+\item{outdir}{The directory for output. The directory is not cleared, but existing files inside it may be overwritten without notice!}
+
+\item{...}{Additional parameters to pass to \link{rmarkdown::render}. Notably this may include \code{utput_format="html_document"} or
+\code{output_format="pdf_document"}.}
+}
+\description{
+This function is mostly targetted at providing a preview inside a script editor window. For other purposes
+it will generally make more sense to call \link{rmarkdown::render}, directly.
+}
diff --git a/rkward/windows/rkcommandeditorwindow.cpp b/rkward/windows/rkcommandeditorwindow.cpp
index 3367e8411..79deb58b9 100644
--- a/rkward/windows/rkcommandeditorwindow.cpp
+++ b/rkward/windows/rkcommandeditorwindow.cpp
@@ -154,8 +154,9 @@ class RKPreviewModeSelector : public QWidgetAction {
 		h->addLayout(r);
 		auto preview_mode_button_group = new QButtonGroup(form);
 
-		for (auto *mode : std::as_const(win->preview_modes)) {
-			auto m = (mode == win->preview_modes.first()) ? nullptr : mode;
+		const auto allmodes = RKCommandEditorWindow::preview_modes + RKCommandEditorWindow::user_preview_modes.values();
+		for (auto *mode : allmodes) {
+			auto m = (mode == allmodes.first()) ? nullptr : mode;
 			auto button = new QRadioButton(mode->label);
 			connect(win->m_doc, &KTextEditor::Document::highlightingModeChanged, button, [button, mode](KTextEditor::Document *doc) {
 				button->setVisible(mode->validator(doc));
@@ -293,6 +294,7 @@ RKCommandEditorWindowPart::~RKCommandEditorWindowPart() {
 // static
 QMap<QString, KTextEditor::Document *> RKCommandEditorWindow::unnamed_documents;
 QList<RKPreviewMode *> RKCommandEditorWindow::preview_modes;
+QHash<QString, RKPreviewMode *> RKCommandEditorWindow::user_preview_modes;
 
 KTextEditor::Document *createDocument(bool with_signals) {
 	KTextEditor::Document *ret = KTextEditor::Editor::instance()->createDocument(RKWardMainWindow::getMain());
@@ -777,6 +779,22 @@ void RKCommandEditorWindow::discardPreview() {
 	}
 }
 
+// static
+void RKCommandEditorWindow::registerUserPreviewMode(const QString &id, const QString &label, const QString &inext, const QString &command) {
+	RK_TRACE(COMMANDEDITOR);
+	delete user_preview_modes.take(id);
+	if (command.isEmpty()) return;
+	auto m = new RKPreviewMode(label, QIcon(), inext);
+	m->command = [command](RKCommandEditorWindow *win, const QString &infile, const QString &outdir, const QString &preview_id) {
+		return command + QStringLiteral("(%1, %2, %3)").arg(RObject::rQuote(infile), RObject::rQuote(outdir), RObject::rQuote(preview_id));
+	};
+	m->validator = [](KTextEditor::Document *doc) -> bool {
+		return true;
+	};
+	user_preview_modes.insert(id, m);
+	// TODO udate/notify existing selector widgets
+}
+
 // static
 void RKCommandEditorWindow::initPreviewModes() {
 	if (!preview_modes.isEmpty()) return;
diff --git a/rkward/windows/rkcommandeditorwindow.h b/rkward/windows/rkcommandeditorwindow.h
index 47384384d..2232aa1d5 100644
--- a/rkward/windows/rkcommandeditorwindow.h
+++ b/rkward/windows/rkcommandeditorwindow.h
@@ -110,6 +110,7 @@ class RKCommandEditorWindow : public RKMDIWindow, public RKScriptContextProvider
 	void highlightLine(int linenum);
 	/** Returns the (main, non-preview) texteditor view in this editor. */
 	KTextEditor::View *getView() const { return m_view; };
+	static void registerUserPreviewMode(const QString &id, const QString &label, const QString &inext, const QString &command);
   public Q_SLOTS:
 	/** update Tab caption according to the current url. Display the filename-component of the URL, or - if not available - a more elaborate description of the url. Also appends a "[modified]" if appropriate */
 	void updateCaption();
@@ -186,6 +187,7 @@ class RKCommandEditorWindow : public RKMDIWindow, public RKScriptContextProvider
 	QAction *action_run_all;
 	QAction *action_run_current;
 	static QList<RKPreviewMode *> preview_modes;
+	static QHash<QString, RKPreviewMode *> user_preview_modes;
 	QAction *action_preview_as_you_type;
 
 	QAction *action_setwd_to_script;



More information about the rkward-tracker mailing list