[education/rkward/devel/workspace_output] rkward: Some more work on output directories.

Thomas Friedrichsmeier null at kde.org
Sat Oct 10 09:18:45 BST 2020


Git commit 46e55233360e79e6b6e67eb7e416b859b5ebafbc by Thomas Friedrichsmeier.
Committed on 09/07/2017 at 19:29.
Pushed by tfry into branch 'devel/workspace_output'.

Some more work on output directories.

Basic worker functions are (mostly) implemented. Next steps: UI, tests.

M  +15   -0    rkward/rbackend/rkrinterface.cpp
M  +3    -0    rkward/rbackend/rpackages/rkward/NAMESPACE
M  +12   -14   rkward/rbackend/rpackages/rkward/R/rk.filename-functions.R
M  +13   -2    rkward/rbackend/rpackages/rkward/man/rk.get.tempfile.name.Rd
M  +165  -21   rkward/windows/rkhtmlwindow.cpp
M  +23   -5    rkward/windows/rkhtmlwindow.h

https://invent.kde.org/education/rkward/commit/46e55233360e79e6b6e67eb7e416b859b5ebafbc

diff --git a/rkward/rbackend/rkrinterface.cpp b/rkward/rbackend/rkrinterface.cpp
index 6995ca9b..285236f5 100644
--- a/rkward/rbackend/rkrinterface.cpp
+++ b/rkward/rbackend/rkrinterface.cpp
@@ -758,6 +758,21 @@ void RInterface::processHistoricalSubstackRequest (const QStringList &calllist,
 		} else {
 			RK_ASSERT (false);
 		}
+	} else if (call == QStringLiteral ("output")) {
+		const QString &subcall = calllist.value (1);
+		QString error;
+		if (subcall == QStringLiteral ("export")) {
+			error = RKOutputWindowManager::self ()->saveOutputDirectoryAs (calllist.value (2), calllist.value (3), calllist.value (4) == QStringLiteral ("TRUE"), in_chain);
+		} else if (subcall == QStringLiteral ("import")) {
+			error = RKOutputWindowManager::self ()->importOutputDirectory (calllist.value (2), calllist.value (3), calllist.value (4) == QStringLiteral ("TRUE"), in_chain);
+		} else if (subcall == QStringLiteral ("create")) {
+			RKOutputWindowManager::self ()->createOutputDirectory (in_chain);
+		} else {
+			RK_ASSERT (false);
+		}
+		if (!error.isEmpty ()) {
+			request->params["return"] = QVariant (QStringList (QStringLiteral ("error")) << error);
+		}
 	} else {
 		request->params["return"] = QVariant (QStringList (QStringLiteral ("error")) << i18n ("Unrecognized call '%1'", call));
 	}
diff --git a/rkward/rbackend/rpackages/rkward/NAMESPACE b/rkward/rbackend/rpackages/rkward/NAMESPACE
index c2e5e0f9..cc04a744 100644
--- a/rkward/rbackend/rpackages/rkward/NAMESPACE
+++ b/rkward/rbackend/rpackages/rkward/NAMESPACE
@@ -51,6 +51,7 @@ export(require)
 export(rk.assign.preview.data)
 export(rk.call.plugin)
 export(rk.clear.plot.history)
+export(rk.create.output.dir)
 export(rk.demo)
 export(rk.describe.alternative)
 export(rk.discard.preview.data)
@@ -58,6 +59,7 @@ export(rk.duplicate.device)
 export(rk.edit)
 export(rk.edit.files)
 export(rk.embed.device)
+export(rk.export.output.dir)
 export(rk.first.plot)
 export(rk.flush.output)
 export(rk.force.append.plot)
@@ -72,6 +74,7 @@ export(rk.goto.plot)
 export(rk.graph.off)
 export(rk.graph.on)
 export(rk.header)
+export(rk.import.output.dir)
 export(rk.last.plot)
 export(rk.list)
 export(rk.list.labels)
diff --git a/rkward/rbackend/rpackages/rkward/R/rk.filename-functions.R b/rkward/rbackend/rpackages/rkward/R/rk.filename-functions.R
index da4a7564..b1c06bd5 100644
--- a/rkward/rbackend/rpackages/rkward/R/rk.filename-functions.R
+++ b/rkward/rbackend/rpackages/rkward/R/rk.filename-functions.R
@@ -105,7 +105,7 @@
 	stopifnot (is.character (x))
 	style <- match.arg (style)
 	oldfile <- rk.get.output.html.file ()
-	dir.create (dirname (x, showWarnings=FALSE, recursive=TRUE))
+	dir.create (dirname (x), showWarnings=FALSE, recursive=TRUE)
 	stopifnot (dir.exists (dirname (x)))
 	assign (".rk.output.html.file", x, .rk.variables)
 
@@ -272,23 +272,21 @@
 
 #' @export
 #' @rdname rk.get.tempfile.name
-rk.export.output.dir <- function (target.dir, source.dir=basename (rk.get.output.html.file ()), ask=TRUE) {
+rk.export.output.dir <- function (source.dir=basename (rk.get.output.html.file ()), target.dir, ask=TRUE) {
 # This is not terribly complex, but we need an implementation in the frontend, anyway, so we use that.
-	x <- .rk.plain.call ("output.export", c (target.dir, source.dir, as.character (isTRUE (ask))))
-	if (!is.null (x)) stop (x)
-# 	.rk.purge.target.dir (target.dir, ask)
-# 	file.copy (source.dir, target.dir, recursive = TRUE, copy.date = TRUE)
+	x <- .rk.do.call ("output", c ("export", source.dir, target.dir, as.character (isTRUE (ask))))
 }
 
 #' @export
 #' @rdname rk.get.tempfile.name
-rk.import.output.dir <- function (source.dir, target.dir=file.path (rk.tempdir (), "output"), ask=TRUE, activate="index.html") {
+rk.import.output.dir <- function (source.dir, activate="index.html", ask=TRUE) {
 # This is not terribly complex, but we need an implementation in the frontend, anyway, so we use that.
-	x <- .rk.plain.call ("output.import", c (target.dir, source.dir, as.character (isTRUE (ask)), activate))
-	if (!is.null (x)) stop (x)
-# 	.rk.purge.target.dir (target.dir, ask)
-# 	file.copy (source.dir, target.dir, recursive = TRUE, copy.date = TRUE)
-# 	if (nzchar (activate)) {
-# 		rk.set.output.html.file (file.path (target.dir, activate))
-# 	}
+	x <- .rk.do.call ("output", c ("import", source.dir, activate, as.character (isTRUE (ask))))
+}
+
+#' @export
+#' @rdname rk.get.tempfile.name
+rk.create.output.dir <- function () {
+# This is not terribly complex, but we need an implementation in the frontend, anyway, so we use that.
+	x <- .rk.do.call ("output", c ("create"))
 }
diff --git a/rkward/rbackend/rpackages/rkward/man/rk.get.tempfile.name.Rd b/rkward/rbackend/rpackages/rkward/man/rk.get.tempfile.name.Rd
index 810763bd..9b8a615b 100644
--- a/rkward/rbackend/rpackages/rkward/man/rk.get.tempfile.name.Rd
+++ b/rkward/rbackend/rpackages/rkward/man/rk.get.tempfile.name.Rd
@@ -1,15 +1,19 @@
 % Generated by roxygen2: do not edit by hand
 % Please edit documentation in R/rk.filename-functions.R
 \name{rk.get.tempfile.name}
+\alias{rk.create.output.dir}
+\alias{rk.export.output.dir}
 \alias{rk.flush.output}
 \alias{rk.get.output.html.file}
 \alias{rk.get.tempfile.name}
 \alias{rk.get.workspace.url}
+\alias{rk.import.output.dir}
 \alias{rk.set.output.html.file}
 \alias{rk.tempdir}
 \title{RKWard file names}
 \usage{
-rk.get.tempfile.name(prefix = "image", extension = ".jpg", directory = "")
+rk.get.tempfile.name(prefix = "image", extension = ".jpg",
+  directory = dirname(rk.get.output.html.file()))
 
 rk.get.workspace.url()
 
@@ -21,6 +25,13 @@ rk.set.output.html.file(x,
 
 rk.flush.output(x = rk.get.output.html.file(), flush.images = TRUE,
   ask = TRUE, ...)
+
+rk.export.output.dir(source.dir = basename(rk.get.output.html.file()),
+  target.dir, ask = TRUE)
+
+rk.import.output.dir(source.dir, activate = "index.html", ask = TRUE)
+
+rk.create.output.dir()
 }
 \arguments{
 \item{prefix}{a string, used as a filename prefix when saving images to the
@@ -67,7 +78,7 @@ the current (or specified) html file, and re-initialize it.
 \details{
 \code{rk.get.tempfile.name} returns a non-existing filename inside the
 specified directory (or the directory of the current output file, if the parameter is
-omitted / left empty). The filename is returned as an absolute path,
+omitted). The filename is returned as an absolute path,
 but the relative path with respect to the base directory can be obtained via
 \code{names()}. It is mainly used by \link{rk.graph.on} to
 create filenames suitable for storing images in the output. The filenames of
diff --git a/rkward/windows/rkhtmlwindow.cpp b/rkward/windows/rkhtmlwindow.cpp
index 3533303a..a1cfc7a3 100644
--- a/rkward/windows/rkhtmlwindow.cpp
+++ b/rkward/windows/rkhtmlwindow.cpp
@@ -37,6 +37,7 @@
 #include <QWebFrame>
 #include <QPrintDialog>
 #include <QMenu>
+#include <QFileDialog>
 #include <QTextCodec>
 #include <QFontDatabase>
 #include <QTemporaryFile>
@@ -1157,7 +1158,7 @@ void listDirectoryState (const QString& _dir, QString *list) {
 	RK_TRACE (APP);
 
 	QDir dir (_dir);
-	QFileInfoList entries = dir.entryInfoList (QDir::NoFilter, QDir::Name | QDir::DirsLast);
+	QFileInfoList entries = dir.entryInfoList (QDir::NoDotAndDotDot, QDir::Name | QDir::DirsLast);
 	for (int i = 0; i < entries.size (); ++i) {
 		const QFileInfo fi = entries[i];
 		if (fi.isDir ()) {
@@ -1179,43 +1180,186 @@ QString hashDirectoryState (const QString& dir) {
 	return QCryptographicHash::hash (list.toUtf8 (), QCryptographicHash::Md5);
 }
 
-QString RKOutputWindowManager::importOutputDirectory (const QString& _dir, const QString& index_file) {
+bool copyDirRecursively (const QString& _source_dir, const QString& _dest_dir) {
+	RK_TRACE (APP);
+
+	QDir dest_dir (_dest_dir);
+	QDir source_dir (_source_dir);
+	if (!QDir ().mkpath (_dest_dir)) return false;
+	if (!source_dir.exists ()) return false;
+
+	bool ok = true;
+	QFileInfoList entries = source_dir.entryInfoList (QDir::NoDotAndDotDot);
+	for (int i = 0; i < entries.size (); ++i) {
+		const QFileInfo fi = entries[i];
+		if (fi.isDir ()) {
+			ok = ok && copyDirRecursively (fi.absoluteFilePath (), dest_dir.absoluteFilePath (fi.fileName ()));
+		} else {
+			// NOTE: this does not overwrite existing target files, but in our use case, we're always writing into empty targets
+			ok = ok && QFile::copy (fi.absoluteFilePath (), dest_dir.absoluteFilePath (fi.fileName ()));
+		}
+	}
+
+	return ok;
+}
+
+QString RKOutputWindowManager::saveOutputDirectory (const QString& _dir, RCommandChain* chain) {
+	RK_TRACE (APP);
+
+	const QString dir = QFileInfo (_dir).canonicalFilePath ();
+	return saveOutputDirectoryAs (_dir, outputs.value (dir).save_dir, true, chain);  // pass raw "dir" parameter b/c of error handling.
+}
+
+QString RKOutputWindowManager::saveOutputDirectoryAs (const QString& dir, const QString& _dest, bool ask_overwrite, RCommandChain* chain) {
+	RK_TRACE (APP);
+
+	const QString work_dir = QFileInfo (dir).canonicalFilePath ();
+	if (!outputs.contains (work_dir)) {
+		return i18n ("The directory %1 does not correspond to to any output directory loaded in this session.", dir);
+	}
+
+	QString dest = _dest;
+	if (dest.isEmpty ()) {
+		QFileDialog dialog (RKWardMainWindow::getMain (), i18n ("Specify directory where to save output"), outputs.value (work_dir).save_dir);
+		dialog.setFileMode (QFileDialog::Directory);
+		dialog.setOption (QFileDialog::ShowDirsOnly, true);
+		dialog.setAcceptMode (QFileDialog::AcceptSave);
+		dialog.setOption (QFileDialog::DontConfirmOverwrite, true);  // custom handling below
+
+		int result = dialog.exec ();
+		const QString cancelled = i18n ("File selection cancelled");
+		if (result != QDialog::Accepted) return cancelled;
+
+		dest = QDir::cleanPath (dialog.selectedFiles ().value (0));
+		if (ask_overwrite && QFileInfo (dest).exists ()) {
+#warning: TODO: We really need some marker to make extra sure that we are not overwriting just _any_ directory, only ones that appear to be RKWard outputs.
+			const QString warning = i18n ("Are you sure you want to overwrite the existing directory '%1'? All current contents, including subdirectories will be lost.", dest);
+			KMessageBox::ButtonCode res = KMessageBox::warningContinueCancel (RKWardMainWindow::getMain (), warning, i18n ("Overwrite Directory?"), KStandardGuiItem::overwrite (),
+			                                                                 KStandardGuiItem::cancel (), QString (), KMessageBox::Options (KMessageBox::Notify | KMessageBox::Dangerous));
+			if (KMessageBox::Continue != res) {
+				return cancelled;
+			}
+		}
+	}
+
+	// If destination already exists, rename it before copying, so we can restore the save in case of an error
+	QString tempname;
+	if (QFileInfo (dest).exists ()) {
+		tempname = dest + '~';
+		while (QFileInfo (tempname).exists ()) {
+			tempname.append ('~');
+		}
+		if (!QDir ().rename (dest, tempname)) {
+			return i18n ("Failed to create temporary backup file %1.", tempname);
+		}
+	}
+
+	bool error = copyDirRecursively (work_dir, dest);
+	if (error) {
+		if (!tempname.isEmpty ()) {
+			QDir ().rename (tempname, dest);
+		}
+		return i18n ("Error while copying %1 to %2", work_dir, dest);
+	}
+	if (!tempname.isEmpty ()) {
+		QDir (tempname).removeRecursively ();
+	}
+
+	OutputDirectory& od = outputs[work_dir];
+	od.save_timestamp = QDateTime::currentDateTime ();
+	od.save_dir = dest;
+	od.saved_hash = hashDirectoryState (work_dir);
+
+	return QString ();
+}
+
+QString RKOutputWindowManager::importOutputDirectory (const QString& _dir, const QString& index_file, bool ask_revert, RCommandChain* chain) {
 	RK_TRACE (APP);
 
 	QFileInfo fi (_dir);
+	if (!fi.isDir ()) {
+		return i18n ("The path %1 does not exist or is not a directory.", _dir);
+	}
+
 	QString dir = fi.canonicalFilePath ();
-	for (int i = 0; i < outputs.count (); ++i) {
-		if (outputs[i].save_dir == dir) {
-			if (KMessageBox::warningContinueCancel (RKWardMainWindow::getMain (), i18n ("The output directory %1 is already imported (use Window->Show Output to show it). Importing it again will revert any unsaved changes made to the output directory during the past %2 minutes. Are you sure you want to revert?", dir, outputs[i].save_timestamp.secsTo (QDateTime::currentDateTime ()) / 60), i18n ("Reset output directory?"), KStandardGuiItem::reset ()) != KMessageBox::Continue) {
-				return QString ();
+	QMap<QString, OutputDirectory>::const_iterator i = outputs.constBegin ();
+	while (i != outputs.constEnd ()) {
+		if (i.value ().save_dir == dir) {
+			if (ask_revert && (KMessageBox::warningContinueCancel (RKWardMainWindow::getMain (), i18n ("The output directory %1 is already imported (use Window->Show Output to show it). Importing it again will revert any unsaved changes made to the output directory during the past %2 minutes. Are you sure you want to revert?", dir, i.value ().save_timestamp.secsTo (QDateTime::currentDateTime ()) / 60), i18n ("Reset output directory?"), KStandardGuiItem::reset ()) != KMessageBox::Continue)) {
+				return i18n ("Cancelled");
 			} else {
-				outputs.removeAt (i);
+				outputs.remove (i.key ());
 				break;
 			}
 		}
 	}
 
+	QString dest = createOutputDirectoryInternal ();
+
+	if (!copyDirRecursively (dir, dest)) {
+		QDir (dest).removeRecursively ();
+		return i18n ("The path %1 could not be imported (copy failure).", _dir);
+	}
+
+	OutputDirectory od;
+	od.index_file = index_file;
+	od.save_dir = dir;
+	od.saved_hash = hashDirectoryState (dest);
+	od.save_timestamp = QDateTime::currentDateTime ();
+	outputs.insert (dest, od);
+
+	backendActivateOutputDirectory (dest, chain);
+
+	return (QString ());
+}
+
+QString RKOutputWindowManager::createOutputDirectory (RCommandChain* chain) {
+	RK_TRACE (APP);
+
+	QString dest = createOutputDirectoryInternal ();
+
+	OutputDirectory od;
+	od.index_file = QStringLiteral ("index.html");
+	outputs.insert (dest, od);
+
+	backendActivateOutputDirectory (dest, chain);
+
+	// the state of the output directory cannot be hashed until the backend has created the index file (via backendActivateOutputDirectory())
+	RCommand *command = new RCommand (QString (), RCommand::App | RCommand::Sync | RCommand::EmptyCommand);
+	command->notifier ()->setProperty ("path", dest);
+	connect (command->notifier (), &RCommandNotifier::commandFinished, this, &RKOutputWindowManager::updateOutputSavedHash);
+	RKGlobals::rInterface ()->issueCommand (command, chain);
+
+	return dest;
+}
+
+QString RKOutputWindowManager::createOutputDirectoryInternal () {
+	RK_TRACE (APP);
+
 	QString prefix = QStringLiteral ("unsaved_output");
 	QString destname = prefix;
 	QDir ddir (RKSettingsModuleGeneral::filesPath ());
-	int i = 0;
+	int x = 0;
 	while (true) {
 		if (!ddir.exists (destname)) break;
-		destname = prefix + QString::number (i);
+		destname = prefix + QString::number (x++);
 	}
-	QString dest = ddir.absoluteFilePath (destname);
+	ddir.mkpath (destname);
+	return ddir.absoluteFilePath (destname);
+}
 
-	KIO::CopyJob *j = KIO::copyAs (QUrl::fromLocalFile (dir), QUrl::fromLocalFile (dest));
-#warning No good, just for testing.
-	j->exec ();
+void RKOutputWindowManager::backendActivateOutputDirectory (const QString& dir, RCommandChain* chain) {
+	RK_TRACE (APP);
 
-	OutputDirectory od;
-	od.index_file = index_file;
-	od.save_dir = dir;
-	od.work_dir = dest;
-	od.saved_hash = hashDirectoryState (dest);
-	od.save_timestamp = QDateTime::currentDateTime ();
-	outputs.append (od);
+	RKGlobals::rInterface ()->issueCommand (QStringLiteral ("rk.set.output.html.file (\"") + RKCommonFunctions::escape (dir + '/' + outputs.value (dir).index_file) + QStringLiteral ("\")\n"), RCommand::App, QString (), 0, 0, chain);
+}
+
+void RKOutputWindowManager::updateOutputSavedHash (RCommand* command) {
+	RK_TRACE (APP);
 
-	return (dest + '/' + index_file);
+	QString path = command->notifier ()->property ("path").toString ();
+	RK_ASSERT (outputs.contains (path));
+	OutputDirectory &output = outputs[path];
+	output.saved_hash = hashDirectoryState (path);
+	output.save_timestamp = QDateTime::currentDateTime ();
 }
diff --git a/rkward/windows/rkhtmlwindow.h b/rkward/windows/rkhtmlwindow.h
index 29b19d4a..9a5ff586 100644
--- a/rkward/windows/rkhtmlwindow.h
+++ b/rkward/windows/rkhtmlwindow.h
@@ -37,6 +37,7 @@ class KWebView;
 class QTemporaryFile;
 class RKHTMLWindow;
 class RKFindBar;
+class RCommandChain;
 
 class RKWebPage : public KWebPage {
 	Q_OBJECT
@@ -225,9 +226,22 @@ public:
 	QList<RKHTMLWindow*> existingOutputWindows (const QString &path = QString ()) const;
 /** Create (and show) a new output window (for the current output path, unless path is specified), and @return the pointer */
 	RKHTMLWindow* newOutputWindow (const QString& path = QString ());
-/** Import an existing output directory. @Returns the file name of the index_file in the writeable location (the "output path"), or an empty string, if an error occured. */
-	QString importOutputDirectory (const QString& dir, const QString& index_file);
-	QString createOutputDirectory ();
+/** Import an existing output directory. @Returns error message, if any, and empty string in case of success */
+	QString importOutputDirectory (const QString& dir, const QString& index_file, bool ask_revert = true, RCommandChain* chain = 0);
+/** Save the given output directory to the locaiton it was last saved to / imported from. If the output directory has not been saved / imported, yet, prompt the user for a destination.
+    @param index_path output directory to save
+    @returns error message, if any, an empty string in case of success */
+	QString saveOutputDirectory (const QString& dir, RCommandChain* chain = 0);
+/** Save the given output directory. @see saveOutputDirectory ().
+    @param index_path the output directory to save
+    @param dest destination directory. May be left empty, in which case the user will be prompted for a destination.
+    @returns error message, if any, an empty string in case of success */
+	QString saveOutputDirectoryAs (const QString& dir, const QString& dest = QString (), bool ask_overwrite = true, RCommandChain* chain = 0);
+/** Create a new empty output directory.
+    @returns path of the new directory */
+	QString createOutputDirectory (RCommandChain* chain = 0);
+/** Drop the given output directory. If it was the active directory, activate another output file. Return the new active output file. */
+	QString dropOutputDirectory (const QString& index_path, bool ask=true, RCommandChain* chain = 0);
 private:
 	RKOutputWindowManager ();
 	~RKOutputWindowManager ();
@@ -238,17 +252,21 @@ private:
 	QMultiHash<QString, RKHTMLWindow *> windows;
 
 	struct OutputDirectory {
-		QString work_dir;
 		QString index_file;
 		QString saved_hash;
 		QDateTime save_timestamp;
 		QString save_dir;
 	};
-	QList<OutputDirectory> outputs;
+	/** map of outputs. Key is the working directory of the output */
+	QMap<QString, OutputDirectory> outputs;
+	void backendActivateOutputDirectory (const QString& dir, RCommandChain* chain);
+	QString createOutputDirectoryInternal ();
 private slots:
 	void fileChanged (const QString &path);
 	void windowDestroyed (QObject *window);
 	void rewatchOutput ();
+
+	void updateOutputSavedHash (RCommand *command);
 };
 
 #endif




More information about the rkward-tracker mailing list