[education/rkward] rkward/windows: Wrap preview modes into helper class, enable according to highlighting

Thomas Friedrichsmeier null at kde.org
Sun Aug 24 16:08:55 BST 2025


Git commit 76e3400274dc3939d3540776671edf5ea4e3a82e by Thomas Friedrichsmeier.
Committed on 24/08/2025 at 15:06.
Pushed by tfry into branch 'master'.

Wrap preview modes into helper class, enable according to highlighting
mode

M  +122  -84   rkward/windows/rkcommandeditorwindow.cpp
M  +2    -10   rkward/windows/rkcommandeditorwindow.h

https://invent.kde.org/education/rkward/-/commit/76e3400274dc3939d3540776671edf5ea4e3a82e

diff --git a/rkward/windows/rkcommandeditorwindow.cpp b/rkward/windows/rkcommandeditorwindow.cpp
index a6aa41f89..a7078fd6f 100644
--- a/rkward/windows/rkcommandeditorwindow.cpp
+++ b/rkward/windows/rkcommandeditorwindow.cpp
@@ -259,7 +259,7 @@ RKCommandEditorWindow::RKCommandEditorWindow(QWidget *parent, const QUrl &_url,
 	if (use_r_highlighting) {
 		RKCommandHighlighter::setHighlighting(m_doc, RKCommandHighlighter::RScript);
 	} else {
-		RKCommandHighlighter::setHighlighting(m_doc, RKCommandHighlighter::Automatic);
+		RKCommandHighlighter::setHighlighting(m_doc, RKCommandHighlighter::AutomaticOrOther);
 	}
 	if (use_r_highlighting || RKSettingsModuleCommandEditor::completionSettings()->completionForAllFileTypes()) {
 		if (flags & RKCommandEditorFlags::UseCodeHinting) {
@@ -375,18 +375,12 @@ void RKCommandEditorWindow::initializeActions(KActionCollection *ac) {
 	preview_modes = new QActionGroup(this);
 	action_no_preview = preview_modes->addAction(RKStandardIcons::getIcon(RKStandardIcons::ActionDelete), i18n("No preview"));
 	action_no_preview->setToolTip(i18n("Disable preview"));
-	action_no_preview->setCheckable(true);
-	action_no_preview->setChecked(true);
-	actionmenu_preview->addAction(action_no_preview);
 	initPreviewModes();
-	for (int i = 0; i < preview_mode_list.size(); ++i) {
-		const PreviewMode &mode = preview_mode_list[i];
-		QAction *a = preview_modes->addAction(mode.icon, mode.actionlabel);
-		a->setWhatsThis(mode.tooltip);
-		a->setToolTip(mode.tooltip);
-		a->setCheckable(true);
+	const auto preview_actions = preview_modes->actions();
+	for (auto *a : preview_actions) {
 		actionmenu_preview->addAction(a);
 	}
+	action_no_preview->setChecked(true);
 	connect(preview, &RKXMLGUIPreviewArea::previewClosed, this, &RKCommandEditorWindow::discardPreview);
 	connect(preview_modes, &QActionGroup::triggered, this, &RKCommandEditorWindow::changePreviewMode);
 
@@ -538,22 +532,75 @@ void RKCommandEditorWindow::autoSaveHandlerModifiedChanged() {
 	}
 }
 
+static QString r_script_mode = QStringLiteral("R Script");
+static QString r_interactive_mode = QStringLiteral("R interactive session");
+static QString r_markdown_mode = QStringLiteral("R Markdown");
+
+static QString modeToString(RKCommandHighlighter::HighlightingMode mode, KTextEditor::Document *doc) {
+	if (mode == RKCommandHighlighter::RScript) return r_script_mode;
+	if (mode == RKCommandHighlighter::RInteractiveSession) return r_interactive_mode;
+	if (mode == RKCommandHighlighter::RMarkdown) return r_markdown_mode;
+	QString fn = doc->url().fileName().toLower();
+	if (fn.endsWith(QLatin1String(".pluginmap")) || fn.endsWith(QLatin1String(".rkh")) || fn.endsWith(QLatin1String(".xml")) || fn.endsWith(QLatin1String(".inc"))) {
+		return QStringLiteral("XML");
+	} else if (fn.endsWith(QLatin1String(".js"))) {
+		return QStringLiteral("JavaScript");
+	}
+	return QString();
+}
+
+static RKCommandHighlighter::HighlightingMode documentHighlightingMode(KTextEditor::Document *doc) {
+	const auto mode = doc->highlightingMode();
+	if (mode == r_script_mode) return RKCommandHighlighter::RScript;
+	if (mode == r_interactive_mode) return RKCommandHighlighter::RInteractiveSession;
+	if (mode == r_markdown_mode) return RKCommandHighlighter::RMarkdown;
+	// Let's hope, kate highlighting mode names remain stable for ever. But just in case,
+	// let's also add a fallback mechanism, if none of the above was a match
+	QString fn = doc->url().fileName().toLower();
+	if (RKSettingsModuleCommandEditor::matchesScriptFileFilter(fn)) return RKCommandHighlighter::RScript;
+	if (fn.endsWith(u".rmd"_s)) return RKCommandHighlighter::RMarkdown;
+	return RKCommandHighlighter::AutomaticOrOther;
+}
+
+class RKPreviewMode : public QAction {
+  public:
+	RKPreviewMode(KTextEditor::Document *doc, const QString &label, const QIcon &icon, const QString &input_ext, RKCommandHighlighter::HighlightingMode mode) :
+	  QAction(icon, label, doc),
+	  input_ext(input_ext),
+	  valid_mode(mode) {
+		setCheckable(true);
+		connect(doc, &KTextEditor::Document::highlightingModeChanged, this, [this, doc] { checkApplicable(doc); });
+		checkApplicable(doc);
+	};
+
+	void checkApplicable(KTextEditor::Document *doc) {
+		setEnabled((valid_mode == RKCommandHighlighter::AutomaticOrOther) || (documentHighlightingMode(doc) == valid_mode));
+	};
+
+	QString preview_label;
+	std::function<QString(const QString &, const QString &, const QString &)> command;
+	QList<QAction*> options();
+	QString input_ext;
+	RKCommandHighlighter::HighlightingMode valid_mode;
+  private:
+	QList<QAction*> _options;
+};
+
 class RKScriptPreviewIO {
 	QUrl url;
-	int preview_mode;
+	RKPreviewMode *mode;
 	QTemporaryDir out_dir;
 	QFile *infile;
-	QString extension;
-	RKScriptPreviewIO(const QString &extension, const QUrl &url) : url(url), preview_mode(-1), out_dir(), infile(nullptr), extension(extension) {
+	RKScriptPreviewIO(RKPreviewMode *mode, const QUrl &url) : url(url), mode(mode), out_dir(), infile(nullptr) {
 		const auto pattern = QLatin1String("tmp_rkward_preview");
 		if (url.isEmpty() || !url.isLocalFile()) {
 			// Not locally saved: save to tempdir
-			infile = new QFile(QDir(out_dir.path()).absoluteFilePath(pattern + extension));
+			infile = new QFile(QDir(out_dir.path()).absoluteFilePath(pattern + mode->input_ext));
 		} else {
 			// If the file is already saved, save the preview input as a temp file in the same folder.
 			// esp. .Rmd files might try to include other files by relative path.
 			QString tempfiletemplate = url.adjusted(QUrl::RemoveFilename).toLocalFile();
-			tempfiletemplate.append(pattern + QLatin1String("XXXXXX") + extension);
+			tempfiletemplate.append(pattern + QLatin1String("XXXXXX") + mode->input_ext);
 			infile = new QTemporaryFile(tempfiletemplate);
 		}
 	}
@@ -577,20 +624,18 @@ class RKScriptPreviewIO {
 		infile->remove();
 		delete infile;
 	}
-	static RKScriptPreviewIO *init(RKScriptPreviewIO *previous, KTextEditor::Document *doc, int preview_mode, const QString &extension) {
+	static RKScriptPreviewIO *init(RKScriptPreviewIO *previous, KTextEditor::Document *doc, RKPreviewMode  *preview_mode) {
 		// Whenever possible, we try to reuse an existing temporary file, because
 		// a) If build files do spill (as happens with rmarkdown::render()), it will not be quite as many
 		// b) Faster in some cases
-		if (previous && previous->preview_mode == preview_mode && previous->url == doc->url() && previous->extension == extension) {
+		if (previous && previous->mode == preview_mode && previous->url == doc->url()) {
 			// If re-using an existing filename, remove it first. Somehow, contrary to documentation, this does not happen in QFile::open(WriteOnly).
 			previous->infile->remove();
 			previous->write(doc);
 			return previous;
 		}
 		delete previous;
-		auto ret = new RKScriptPreviewIO(extension, doc->url());
-		ret->url = doc->url();
-		ret->preview_mode = preview_mode;
+		auto ret = new RKScriptPreviewIO(preview_mode, doc->url());
 		ret->write(doc);
 		return ret;
 	}
@@ -680,52 +725,57 @@ QString RmarkDownRender(const QString &infile, const QString &outdir, const QStr
 
 void RKCommandEditorWindow::initPreviewModes() {
 	RK_TRACE(COMMANDEDITOR);
-	preview_mode_list.append(PreviewMode{
-	    QIcon::fromTheme(u"preview_math"_s), i18n("R Markdown (HTML)"), i18n("Preview of rendered R Markdown"),
-	    i18n("Preview the script as rendered from RMarkdown format (.Rmd) to HTML."),
-	    u".Rmd"_s,
-	    [](const QString &infile, const QString &outdir, const QString & /*preview_id*/) {
-		    return RmarkDownRender(infile, outdir, u"output_format=\"html_document\", "_s);
-	    }});
-	preview_mode_list.append(PreviewMode{
-	    QIcon::fromTheme(u"preview_math"_s), 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."),
-	    u".Rmd"_s,
-	    [](const QString &infile, const QString &outdir, const QString & /*preview_id*/) {
-		    return RmarkDownRender(infile, outdir, 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."),
-	    u".R"_s,
-	    [](const QString &infile, const QString &outdir, const QString & /*preview_id*/) {
-		    auto command = QStringLiteral("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(outdir + u"/output.html"_s));
-	    }});
-	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"),
-	    u".R"_s,
-	    [](const QString &infile, const QString &outdir, const QString & /*preview_id*/) {
-		    auto command = QStringLiteral("rk.eval.as.preview(%1, %2)\n"
-		                                  "rk.show.html(%2)\n");
-		    return command.arg(RObject::rQuote(infile), RObject::rQuote(outdir + u"/output.html"_s));
-	    }});
-	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."),
-	    u".R"_s,
-	    [](const QString &infile, const QString & /*outdir*/, const QString &preview_id) {
-		    auto command = QStringLiteral("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));
-	    }});
+
+	auto m = new RKPreviewMode(m_doc, i18n("R Markdown (HTML)"), QIcon::fromTheme(u"preview_math"_s), u".Rmd"_s, RKCommandHighlighter::RMarkdown);
+	m->preview_label = i18n("Preview of rendered R Markdown");
+	m->setToolTip(i18n("Preview the script as rendered from RMarkdown format (.Rmd) to HTML."));
+	m->command = [](const QString &infile, const QString &outdir, const QString & /*preview_id*/) {
+		return RmarkDownRender(infile, outdir, u"output_format=\"html_document\", "_s);
+	};
+	preview_modes->addAction(m);
+
+	m = new RKPreviewMode(m_doc, i18n("R Markdown (auto)"), QIcon::fromTheme(u"preview_math"_s), u".Rmd"_s, RKCommandHighlighter::RMarkdown);
+	m->preview_label = i18n("Preview of rendered R Markdown");
+	m->setToolTip(i18n("Preview the script as rendered from RMarkdown format (.Rmd) to the format specified in the document header."));
+	m->command = [](const QString &infile, const QString &outdir, const QString & /*preview_id*/) {
+		return RmarkDownRender(infile, outdir, QString());
+	};
+	preview_modes->addAction(m);
+
+	m = new RKPreviewMode(m_doc, i18n("RKWard Output"), RKStandardIcons::getIcon(RKStandardIcons::WindowOutput), u".R"_s, RKCommandHighlighter::RScript);
+	m->preview_label = i18n("Preview of generated RKWard output");
+	m->setToolTip(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."));
+	m->command = [](const QString &infile, const QString &outdir, const QString & /*preview_id*/) {
+		auto command = QStringLiteral("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(outdir + u"/output.html"_s));
+	};
+	preview_modes->addAction(m);
+
+	m = new RKPreviewMode(m_doc, i18n("R Console"), RKStandardIcons::getIcon(RKStandardIcons::WindowConsole), u".R"_s, RKCommandHighlighter::RScript);
+	m->preview_label = i18n("Preview of script running in interactive R Console");
+	m->setToolTip(i18n("Preview the script as if it was run in the interactive R Console"));
+	m->command = [](const QString &infile, const QString &outdir, const QString & /*preview_id*/) {
+		auto command = QStringLiteral("rk.eval.as.preview(%1, %2)\n"
+		                              "rk.show.html(%2)\n");
+		return command.arg(RObject::rQuote(infile), RObject::rQuote(outdir + u"/output.html"_s));
+	};
+	preview_modes->addAction(m);
+
+	m = new RKPreviewMode(m_doc, i18n("Plot"), RKStandardIcons::getIcon(RKStandardIcons::WindowX11), u".R"_s, RKCommandHighlighter::RScript);
+	m->preview_label = i18n("Preview of generated plot");
+	m->setToolTip(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."));
+	m->command = [](const QString &infile, const QString & /*outdir*/, const QString &preview_id) {
+		auto command = QStringLiteral("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));
+	};
+	preview_modes->addAction(m);
 }
 
 void RKCommandEditorWindow::doRenderPreview() {
@@ -733,17 +783,16 @@ void RKCommandEditorWindow::doRenderPreview() {
 
 	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);
+	const auto mode = static_cast<RKPreviewMode *>(preview_modes->checkedAction());
 
 	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(u".rk.with.window.hints(rk.show.html(content=\"\"), \"\", "_s + RObject::rQuote(preview_manager->previewId()) + u", style=\"preview\")"_s, 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(QLatin1String("")), preview_manager->previewId());
-	preview->setLabel(mode.previewlabel);
+	preview_io = RKScriptPreviewIO::init(preview_io, m_doc, mode);
+	QString command = mode->command(preview_io->inpath(), preview_io->outpath(QLatin1String("")), preview_manager->previewId());
+	preview->setLabel(mode->preview_label.isEmpty() ? mode->text() : mode->preview_label);
 	preview->show();
 
 	RCommand *rcommand = new RCommand(u".rk.with.window.hints(local({\n"_s + command + u"}), \"\", "_s + RObject::rQuote(preview_manager->previewId()) + u", style=\"preview\")"_s, RCommand::App);
@@ -1240,19 +1289,8 @@ QString RKCommandHighlighter::commandToHTML(const QString &r_command, Highlighti
 void RKCommandHighlighter::setHighlighting(KTextEditor::Document *doc, HighlightingMode mode) {
 	RK_TRACE(COMMANDEDITOR);
 
-	QString mode_string;
-	if (mode == RScript) mode_string = QLatin1String("R Script");
-	else if (mode == RInteractiveSession) mode_string = QLatin1String("R interactive session");
-	else {
-		QString fn = doc->url().fileName().toLower();
-		if (fn.endsWith(QLatin1String(".pluginmap")) || fn.endsWith(QLatin1String(".rkh")) || fn.endsWith(QLatin1String(".xml")) || fn.endsWith(QLatin1String(".inc"))) {
-			mode_string = QLatin1String("XML");
-		} else if (fn.endsWith(QLatin1String(".js"))) {
-			mode_string = QLatin1String("JavaScript");
-		} else {
-			return;
-		}
-	}
+	QString mode_string = modeToString(mode, doc);
+	if (mode_string.isEmpty()) return;
 	if (!(doc->setHighlightingMode(mode_string) && doc->setMode(mode_string))) RK_DEBUG(COMMANDEDITOR, DL_ERROR, "R syntax highlighting definition ('%s')not found!", qPrintable(mode_string));
 }
 
diff --git a/rkward/windows/rkcommandeditorwindow.h b/rkward/windows/rkcommandeditorwindow.h
index dcb0cdcd5..46ab563ba 100644
--- a/rkward/windows/rkcommandeditorwindow.h
+++ b/rkward/windows/rkcommandeditorwindow.h
@@ -194,15 +194,6 @@ class RKCommandEditorWindow : public RKMDIWindow, public RKScriptContextProvider
 	QString _id;
 	static QMap<QString, KTextEditor::Document *> unnamed_documents;
 
-	struct PreviewMode {
-		QIcon icon;
-		QString actionlabel;
-		QString previewlabel;
-		QString tooltip;
-		QString input_ext;
-		std::function<QString(const QString &, const QString &, const QString &)> command;
-	};
-	QList<PreviewMode> preview_mode_list;
 	void initPreviewModes();
 
 	RKXMLGUIPreviewArea *preview;
@@ -219,7 +210,8 @@ class RKCommandHighlighter {
 	enum HighlightingMode {
 		RInteractiveSession,
 		RScript,
-		Automatic
+		RMarkdown,
+		AutomaticOrOther
 	};
 	static void copyLinesToOutput(KTextEditor::View *view, HighlightingMode mode);
 	static void setHighlighting(KTextEditor::Document *doc, HighlightingMode mode);



More information about the rkward-tracker mailing list