[education/rkward] rkward: Add auto-format R Markdown preview

Thomas Friedrichsmeier null at kde.org
Sun Jul 7 17:33:10 BST 2024


Git commit 8800c15d404bc1eac2b709b56024afcd342daec0 by Thomas Friedrichsmeier.
Committed on 07/07/2024 at 16:32.
Pushed by tfry into branch 'master'.

Add auto-format R Markdown preview

M  +3    -3    rkward/autotests/core_test.cpp
M  +6    -4    rkward/rbackend/rkrinterface.cpp
M  +140  -141  rkward/windows/rkcommandeditorwindow.cpp
M  +2    -2    rkward/windows/rkworkplace.cpp

https://invent.kde.org/education/rkward/-/commit/8800c15d404bc1eac2b709b56024afcd342daec0

diff --git a/rkward/autotests/core_test.cpp b/rkward/autotests/core_test.cpp
index 5a22b6f28..6c8aa1f8d 100644
--- a/rkward/autotests/core_test.cpp
+++ b/rkward/autotests/core_test.cpp
@@ -73,7 +73,7 @@ class RKWardCoreTest: public QObject {
 		connect(command->notifier(), &RCommandNotifier::commandFinished, this, [_done, callback](RCommand *command) { *_done = true; callback(command); });
 		RInterface::issueCommand(command, chain);
 		while (!done && (t.elapsed() < timeoutms)) {
-			qApp->processEvents();
+			qApp->processEvents(QEventLoop::AllEvents, 500);
 		}
 		if (!done) {
 			testLog("Command timed out: %s", qPrintable(ccopy));
@@ -346,7 +346,7 @@ private Q_SLOTS:
 				QElapsedTimer t;
 				t.start();
 				while (commands_out <= i) {
-					qApp->processEvents();
+					qApp->processEvents(QEventLoop::AllEvents, 500);
 					if (t.elapsed() > 10000) {
 						testLog("Timeout waiting for backend");
 						listBackendLog();
@@ -474,7 +474,7 @@ private Q_SLOTS:
 		QElapsedTimer t;
 		t.start();
 		while (oldiface) {  // action may be delayed until next event processing
-			qApp->processEvents();
+			qApp->processEvents(QEventLoop::AllEvents, 500);
 			if (t.elapsed() > 30000) {
 				testLog("Backend shutdown timed out");
 				auto m = QApplication::activeModalWidget();
diff --git a/rkward/rbackend/rkrinterface.cpp b/rkward/rbackend/rkrinterface.cpp
index 9302567a1..c0f7c6da3 100644
--- a/rkward/rbackend/rkrinterface.cpp
+++ b/rkward/rbackend/rkrinterface.cpp
@@ -646,9 +646,11 @@ GenericRRequestResult RInterface::processRCallRequest (const QString &call, cons
 		new RKEditObjectAgent (arglist, in_chain);
 	} else if (call == "require") {
 		RK_ASSERT (!arglist.isEmpty());
-		QString lib_name = arglist.value(0);
-		KMessageBox::information(nullptr, i18n("The R-backend has indicated that in order to carry out the current task it needs the package '%1', which is not currently installed. We will open the package-management tool, and there you can try to locate and install the needed package.", lib_name), i18n("Require package '%1'", lib_name));
-		RKLoadLibsDialog::showInstallPackagesModal(nullptr, in_chain, QStringList(lib_name));
+		if (!RKWardMainWindow::suppressModalDialogsForTesting()) {
+			QString lib_name = arglist.value(0);
+			KMessageBox::information(nullptr, i18n("The R-backend has indicated that in order to carry out the current task it needs the package '%1', which is not currently installed. We will open the package-management tool, and there you can try to locate and install the needed package.", lib_name), i18n("Require package '%1'", lib_name));
+			RKLoadLibsDialog::showInstallPackagesModal(nullptr, in_chain, QStringList(lib_name));
+		}
 	} else if (call == "doPlugin") {
 		if (arglist.count () >= 2) {
 			QString message;
@@ -700,7 +702,7 @@ GenericRRequestResult RInterface::processRCallRequest (const QString &call, cons
 	} else if (call == "updateInstalledPackagesList") {
 		RKSessionVars::instance ()->setInstalledPackages(arglist);
 	} else if (call == "showHTML") {
-		if (arglist.size() == 1)	RKWorkplace::mainWorkplace()->openHelpWindow(QUrl::fromUserInput(arglist.value(0), QDir::currentPath(), QUrl::AssumeLocalFile));
+		if (arglist.size() == 1) RKWorkplace::mainWorkplace()->openHelpWindow(QUrl::fromUserInput(arglist.value(0), QDir::currentPath(), QUrl::AssumeLocalFile));
 		else {
 			auto win = qobject_cast<RKHTMLWindow*>(RKWorkplace::mainWorkplace()->openHelpWindow(QUrl()));
 			RK_ASSERT(win);
diff --git a/rkward/windows/rkcommandeditorwindow.cpp b/rkward/windows/rkcommandeditorwindow.cpp
index 9364890cf..14fa82ba0 100644
--- a/rkward/windows/rkcommandeditorwindow.cpp
+++ b/rkward/windows/rkcommandeditorwindow.cpp
@@ -404,125 +404,6 @@ void RKCommandEditorWindow::initializeActions (KActionCollection* ac) {
 	if (file_save_as_action) file_save_as_action->setText (i18n ("Save Script As..."));
 }
 
-struct SoftwareCheck {
-	const QString exe;
-	const QString header;
-	const QString message;
-};
-
-QString withCheckForSoftware(const QString &command, const SoftwareCheck check, const QString &outfile) {
-	QLatin1String ret(
-		"if (!nzchar(Sys.which(\"%1\"))) {\n"
-		"	if(!file.exists(%2)) {\n"
-		"		output <- rk.set.output.html.file(%2, silent=TRUE)\n"
-		"		rk.header(%3)\n"
-		"		rk.print(%4)\n"
-		"		rk.set.output.html.file(output, silent=TRUE)\n"
-		"		rk.show.html(%2)\n"
-		"	}\n"
-		"} else {\n"
-		"%5"
-		"}\n"
-	);
-	return ret.arg(check.exe, RObject::rQuote(outfile + "._" + check.exe + "_.html"), RObject::rQuote(check.header), RObject::rQuote(check.message), command);
-};
-
-QString withCheckForPandoc(const QString &command, const QString &outfile) {
-	return withCheckForSoftware(command, {
-		"pandoc",
-		i18n("Pandoc is not installed"),
-		i18n("The software <tt>pandoc</tt>, required to rendering R markdown files, is not installed, or not in the system path of "
-		"the running R session. You will need to install pandoc from <a href=\"https://pandoc.org/\">https://pandoc.org/</a>.</br>"
-		"If it is installed, but cannot be found, try adding it to the system path of the running R session at "
-		"<a href=\"rkward://settings/rbackend\">Settings->Configure RKward->R-backend</a>.")
-	}, outfile);
-}
-
-void RKCommandEditorWindow::initPreviewModes() {
-	RK_TRACE(COMMANDEDITOR);
-	preview_mode_list.append(PreviewMode{
-		QIcon::fromTheme("preview_math"), i18n("R Markdown (HTML)"), i18n("Preview of rendered R Markdown"),
-		i18n("Preview the script as rendered from RMarkdown format (.Rmd) to HTML."),
-		QLatin1String(".Rmd"), QLatin1String(".html"),
-		[](const QString& infile, const QString& outfile, const QString& /*preview_id*/) {
-			auto command = QStringLiteral(
-				"	require(rmarkdown)\n"
-				"	rmarkdown::render(%1, output_format=\"html_document\", output_file=%2, quiet=TRUE)\n"
-				"	rk.show.html(%2)\n"
-			).arg(RObject::rQuote(infile), RObject::rQuote(outfile));
-			return withCheckForPandoc(command, outfile);
-		}
-	});
-	preview_mode_list.append(PreviewMode{
-		QIcon::fromTheme("preview_math"), i18n("R Markdown (PDF)"), i18n("Preview of rendered R Markdown"),
-		i18n("Preview the script as rendered from RMarkdown format (.Rmd) to PDF."),
-		QLatin1String(".Rmd"), QLatin1String(".pdf"),
-		[](const QString& infile, const QString& outfile, const QString& /*preview_id*/) {
-			auto pdflatex = SoftwareCheck{"pdflatex",
-				i18n("pdflatex is not installed"),
-				i18n("pdflatex is required for rendering PDF previews. The easiest way to install it is by running <tt>install.packages(\"tinytex\"); install_tinytex()</tt>```")};
-			auto command = QStringLiteral(
-				"	require(rmarkdown)\n"
-				"	rmarkdown::render(%1, output_format=\"pdf_document\", output_file=%2, quiet=TRUE)\n"
-				"	rk.show.pdf(%2)\n"
-			).arg(RObject::rQuote(infile), RObject::rQuote(outfile));
-			return withCheckForPandoc(withCheckForSoftware(command, pdflatex, outfile), outfile);
-		}
-	});
-	preview_mode_list.append(PreviewMode{
-		RKStandardIcons::getIcon(RKStandardIcons::WindowOutput), i18n("RKWard Output"), i18n("Preview of generated RKWard output"),
-		i18n("Preview any output to the RKWard Output Window. This preview will be empty, if there is no call to <i>rk.print()</i> or other RKWard output commands."),
-		QLatin1String(".R"), QLatin1String(".html"),
-		[](const QString& infile, const QString& outfile, const QString& /*preview_id*/) {
-			auto command = QLatin1String("output <- rk.set.output.html.file(%2, silent=TRUE)\n"
-				"try(rk.flush.output(ask=FALSE, style=\"preview\", silent=TRUE))\n"
-				"try(source(%1, local=TRUE))\n"
-				"rk.set.output.html.file(output, silent=TRUE)\n"
-				"rk.show.html(%2)\n");
-			return command.arg(RObject::rQuote(infile), RObject::rQuote(outfile));
-		}
-	});
-	preview_mode_list.append(PreviewMode{
-		RKStandardIcons::getIcon(RKStandardIcons::WindowConsole), i18n("R Console"), i18n("Preview of script running in interactive R Console"),
-		i18n("Preview the script as if it was run in the interactive R Console"),
-		QLatin1String(".R"), QLatin1String(".html"),
-		[](const QString& infile, const QString& outfile, const QString& /*preview_id*/) {
-			auto command = QLatin1String("output <- rk.set.output.html.file(%2, silent=TRUE)\n"
-				"on.exit(rk.set.output.html.file(output, silent=TRUE))\n"
-				"try(rk.flush.output(ask=FALSE, style=\"preview\", silent=TRUE))\n"
-				"exprs <- expression(NULL)\n"
-				"rk.capture.output(suppress.messages=TRUE)\n"
-				"try({\n"
-				"    exprs <- parse (%1, keep.source=TRUE)\n"
-				"})\n"
-				".rk.cat.output(rk.end.capture.output(TRUE))\n"
-				"for (i in seq_len(length(exprs))) {\n"
-				"    rk.print.code(as.character(attr(exprs, \"srcref\")[[i]]))\n"
-				"    rk.capture.output(suppress.messages=TRUE)\n"
-				"    try({\n"
-				"        withAutoprint(exprs[[i]], evaluated=TRUE, echo=FALSE)\n"
-				"    })\n"
-				"    .rk.cat.output(rk.end.capture.output(TRUE))\n"
-				"}\n"
-				"rk.set.output.html.file(output, silent=TRUE)\n"
-				"rk.show.html(%2)\n");
-			return command.arg(RObject::rQuote(infile), RObject::rQuote(outfile));
-		}
-	});
-	preview_mode_list.append(PreviewMode{
-		RKStandardIcons::getIcon(RKStandardIcons::WindowX11), i18n("Plot"), i18n("Preview of generated plot"),
-		i18n("Preview any onscreen graphics produced by running this script. This preview will be empty, if there is no call to <i>plot()</i> or other graphics commands."),
-		QLatin1String(".R"), QLatin1String(),
-		[](const QString& infile, const QString& /*outfile*/, const QString& preview_id) {
-			auto command = QLatin1String("olddev <- dev.cur()\n"
-				".rk.startPreviewDevice(%2)\n"
-				"try(source(%1, local=TRUE, print.eval=TRUE))\n"
-				"if (olddev != 1) dev.set(olddev)\n");
-			return command.arg(RObject::rQuote(infile), RObject::rQuote(preview_id));
-		}
-	});
-}
-
 void RKCommandEditorWindow::initBlocks () {
 	RK_TRACE (COMMANDEDITOR);
 	RK_ASSERT (block_records.isEmpty ());
@@ -738,6 +619,146 @@ void RKCommandEditorWindow::discardPreview () {
 	action_no_preview->setChecked (true);
 }
 
+// TODO: This would probably better go into the R package. The missing ingredient, there, is i18n(), however.
+QString RmarkDownRender(const QString &infile, const QString &outdir, const QString &mode_arg) {
+	return QStringLiteral(
+	".check.for.software <- function(command, message) {\n"
+	"	output <- ''\n"
+	"	for (i in 1:length(command)) {\n"
+	"		if (!nzchar(Sys.which(command[i]))) output <- paste0(output, '<h2>%1</h2><p>', message[i], '</p>\n')\n"
+	"	}\n"
+	"	output\n"
+	"}\n"
+	"require(rmarkdown)\n"
+	"res <- try({\n"
+	"	rmarkdown::render(%6, output_dir=%7,%8 quiet=TRUE)\n"
+	"})\n"
+	"if (inherits(res, 'try-error')) {\n"
+	"	msg <- attr(res, 'condition')$message\n"
+	"	out <- '<h1>%2</h1>'\n"
+	"	if (length(grep('pandoc', msg))) {\n"
+	"		out <- paste0(out, .check.for.software('pandoc', %3))\n"
+	"	}\n"
+	"	if (length(grep('pdflatex', msg))) {\n"
+	"		out <- paste0(out, .check.for.software('pdflatex', %4))\n"
+	"	}\n"
+	"	rk.show.html(content=out)\n"
+	"} else {\n"
+	"	if (endsWith(toupper(res), '.PDF')) {\n"
+	"		rk.show.pdf(res)\n"
+	"	} else if (endsWith(toupper(res), '.HTML')) {\n"
+	"		rk.show.html(res)\n"
+	"	} else {\n"
+	"		rk.show.html(content=paste0(%5, '<p><a href=\"', res, '\">', res, '</a></p>'))\n"
+	"	}\n"
+	"}\n").arg(
+		i18nc("Caption: Some software is missing.", "Missing software"),
+		i18n("Rendering the preview failed"),
+		RObject::rQuote(i18n("The software <tt>pandoc</tt>, required to rendering R markdown files, is not installed, or not in the system path of the running "
+		"R session. You will need to install pandoc from <a href=\"https://pandoc.org/\">https://pandoc.org/</a>.</br>If it is installed, but cannot be found, "
+		"try adding it to the system path of the running R session at <a href=\"rkward://settings/rbackend\">Settings->Configure RKward->R-backend</a>.")),
+		RObject::rQuote(i18n("The software <tt>pdflatex</tt> is required for rendering PDF previews. The easiest way to install it is by running <tt>install.packages(\"tinytex\"); library(\"tinytex\"); install_tinytex()</tt>")),
+		RObject::rQuote(i18n("<h1>Unsupported format</h1><p>The preview cannot be shown, here, because the output format is neither HTML, nor PDF. You can try opening it in an external application, using the link, below, or you can change the preview mode to 'R Markdown (HTML)'.</p>")),
+		RObject::rQuote(infile),
+		RObject::rQuote(outdir),
+		mode_arg
+	);
+}
+
+void RKCommandEditorWindow::initPreviewModes() {
+	RK_TRACE(COMMANDEDITOR);
+	preview_mode_list.append(PreviewMode{
+		QIcon::fromTheme("preview_math"), i18n("R Markdown (HTML)"), i18n("Preview of rendered R Markdown"),
+		i18n("Preview the script as rendered from RMarkdown format (.Rmd) to HTML."),
+		QLatin1String(".Rmd"), QLatin1String(".html"),
+		[](const QString& infile, const QString& outfile, const QString& /*preview_id*/) {
+			return RmarkDownRender(infile, QFileInfo(outfile).absolutePath(), "output_format=\"html_document\", ");
+		}
+	});
+	preview_mode_list.append(PreviewMode{
+		QIcon::fromTheme("preview_math"), i18n("R Markdown (auto)"), i18n("Preview of rendered R Markdown"),
+		i18n("Preview the script as rendered from RMarkdown format (.Rmd) to the format specified in the document header."),
+		QLatin1String(".Rmd"), QLatin1String(".pdf"),
+		[](const QString& infile, const QString& outfile, const QString& /*preview_id*/) {
+			return RmarkDownRender(infile, QFileInfo(outfile).absolutePath(), QString());
+		}
+	});
+	preview_mode_list.append(PreviewMode{
+		RKStandardIcons::getIcon(RKStandardIcons::WindowOutput), i18n("RKWard Output"), i18n("Preview of generated RKWard output"),
+		i18n("Preview any output to the RKWard Output Window. This preview will be empty, if there is no call to <i>rk.print()</i> or other RKWard output commands."),
+		QLatin1String(".R"), QLatin1String(".html"),
+		[](const QString& infile, const QString& outfile, const QString& /*preview_id*/) {
+			auto command = QLatin1String("output <- rk.set.output.html.file(%2, silent=TRUE)\n"
+				"try(rk.flush.output(ask=FALSE, style=\"preview\", silent=TRUE))\n"
+				"try(source(%1, local=TRUE))\n"
+				"rk.set.output.html.file(output, silent=TRUE)\n"
+				"rk.show.html(%2)\n");
+			return command.arg(RObject::rQuote(infile), RObject::rQuote(outfile));
+		}
+	});
+	preview_mode_list.append(PreviewMode{
+		RKStandardIcons::getIcon(RKStandardIcons::WindowConsole), i18n("R Console"), i18n("Preview of script running in interactive R Console"),
+		i18n("Preview the script as if it was run in the interactive R Console"),
+		QLatin1String(".R"), QLatin1String(".html"),
+		[](const QString& infile, const QString& outfile, const QString& /*preview_id*/) {
+			auto command = QLatin1String("output <- rk.set.output.html.file(%2, silent=TRUE)\n"
+				"on.exit(rk.set.output.html.file(output, silent=TRUE))\n"
+				"try(rk.flush.output(ask=FALSE, style=\"preview\", silent=TRUE))\n"
+				"exprs <- expression(NULL)\n"
+				"rk.capture.output(suppress.messages=TRUE)\n"
+				"try({\n"
+				"    exprs <- parse (%1, keep.source=TRUE)\n"
+				"})\n"
+				".rk.cat.output(rk.end.capture.output(TRUE))\n"
+				"for (i in seq_len(length(exprs))) {\n"
+				"    rk.print.code(as.character(attr(exprs, \"srcref\")[[i]]))\n"
+				"    rk.capture.output(suppress.messages=TRUE)\n"
+				"    try({\n"
+				"        withAutoprint(exprs[[i]], evaluated=TRUE, echo=FALSE)\n"
+				"    })\n"
+				"    .rk.cat.output(rk.end.capture.output(TRUE))\n"
+				"}\n"
+				"rk.set.output.html.file(output, silent=TRUE)\n"
+				"rk.show.html(%2)\n");
+			return command.arg(RObject::rQuote(infile), RObject::rQuote(outfile));
+		}
+	});
+	preview_mode_list.append(PreviewMode{
+		RKStandardIcons::getIcon(RKStandardIcons::WindowX11), i18n("Plot"), i18n("Preview of generated plot"),
+		i18n("Preview any onscreen graphics produced by running this script. This preview will be empty, if there is no call to <i>plot()</i> or other graphics commands."),
+		QLatin1String(".R"), QLatin1String(),
+		[](const QString& infile, const QString& /*outfile*/, const QString& preview_id) {
+			auto command = QLatin1String("olddev <- dev.cur()\n"
+				".rk.startPreviewDevice(%2)\n"
+				"try(source(%1, local=TRUE, print.eval=TRUE))\n"
+				"if (olddev != 1) dev.set(olddev)\n");
+			return command.arg(RObject::rQuote(infile), RObject::rQuote(preview_id));
+		}
+	});
+}
+
+void RKCommandEditorWindow::doRenderPreview () {
+	RK_TRACE (COMMANDEDITOR);
+
+	if (action_no_preview->isChecked()) return;
+	if (!preview_manager->needsCommand()) return;
+	int nmode = preview_modes->actions().indexOf(preview_modes->checkedAction());
+	const PreviewMode &mode = preview_mode_list.value(nmode-1);
+
+	if (!preview->findChild<RKMDIWindow*>()) {
+		// (lazily) initialize the preview window with _something_, as an RKMDIWindow is needed to display messages (important, if there is an error during the first preview)
+		RInterface::issueCommand(".rk.with.window.hints(rk.show.html(content=\"\"), \"\", " + RObject::rQuote(preview_manager->previewId()) + ", style=\"preview\")", RCommand::App | RCommand::Sync);
+	}
+
+	preview_io = RKScriptPreviewIO::init(preview_io, m_doc, nmode, mode.input_ext);
+	QString command = mode.command(preview_io->inpath(), preview_io->outpath("output" + mode.output_ext), preview_manager->previewId());
+	preview->setLabel(mode.previewlabel);
+	preview->show();
+
+	RCommand *rcommand = new RCommand(".rk.with.window.hints(local({\n" + command + QStringLiteral("}), \"\", ") + RObject::rQuote(preview_manager->previewId()) + ", style=\"preview\")", RCommand::App);
+	preview_manager->setCommand(rcommand);
+}
+
 void RKCommandEditorWindow::documentSaved () {
 	RK_TRACE (COMMANDEDITOR);
 
@@ -991,28 +1012,6 @@ void RKCommandEditorWindow::copyLinesToOutput () {
 	RKCommandHighlighter::copyLinesToOutput (m_view, RKCommandHighlighter::RScript);
 }
 
-void RKCommandEditorWindow::doRenderPreview () {
-	RK_TRACE (COMMANDEDITOR);
-
-	if (action_no_preview->isChecked()) return;
-	if (!preview_manager->needsCommand()) return;
-	int nmode = preview_modes->actions().indexOf(preview_modes->checkedAction());
-	const PreviewMode &mode = preview_mode_list.value(nmode-1);
-
-	if (!preview->findChild<RKMDIWindow*>()) {
-		// (lazily) initialize the preview window with _something_, as an RKMDIWindow is needed to display messages (important, if there is an error during the first preview)
-		RInterface::issueCommand(".rk.with.window.hints (rk.show.html(\"_nothing_.html\"), \"\", " + RObject::rQuote(preview_manager->previewId()) + ", style=\"preview\")", RCommand::App | RCommand::Sync);
-	}
-
-	preview_io = RKScriptPreviewIO::init(preview_io, m_doc, nmode, mode.input_ext);
-	QString command = mode.command(preview_io->inpath(), preview_io->outpath("output" + mode.output_ext), preview_manager->previewId());
-	preview->setLabel(mode.previewlabel);
-	preview->show();
-
-	RCommand *rcommand = new RCommand(".rk.with.window.hints(local({\n" + command + QStringLiteral("}), \"\", ") + RObject::rQuote(preview_manager->previewId()) + ", style=\"preview\")", RCommand::App);
-	preview_manager->setCommand(rcommand);
-}
-
 void RKCommandEditorWindow::runAll () {
 	RK_TRACE (COMMANDEDITOR);
 
diff --git a/rkward/windows/rkworkplace.cpp b/rkward/windows/rkworkplace.cpp
index 020fbf197..3084fedfe 100644
--- a/rkward/windows/rkworkplace.cpp
+++ b/rkward/windows/rkworkplace.cpp
@@ -503,13 +503,13 @@ RKMDIWindow* RKWorkplace::openHelpWindow (const QUrl &url, bool only_once) {
 	// if we're working with a window hint, try to _reuse_ the existing window, even if it did not get found, above
 	auto w = getNamedWindow<RKHTMLWindow>(window_name_override);
 	if (w) {
-		w->openURL (url);
+		if (!url.isEmpty()) w->openURL(url);
 //		w->activate ();   // HACK: Keep preview windows from stealing focus
 		return w;
 	}
 
 	RKHTMLWindow *hw = new RKHTMLWindow (view (), RKHTMLWindow::HTMLHelpWindow);
-	if (!url.isEmpty()) hw->openURL (url);
+	if (!url.isEmpty()) hw->openURL(url);
 	addWindow (hw);
 	return (hw);
 }



More information about the rkward-tracker mailing list