[education/rkward/devel/workspace_output] rkward: More work on output directories

Thomas Friedrichsmeier null at kde.org
Sat Oct 24 15:34:54 BST 2020


Git commit 9bee71d32e2ca694f90d8f006b81ce98f2427b2e by Thomas Friedrichsmeier.
Committed on 24/10/2020 at 14:34.
Pushed by tfry into branch 'devel/workspace_output'.

More work on output directories

M  +1    -1    rkward/dialogs/rksavemodifieddialog.cpp
M  +155  -29   rkward/misc/rkoutputdirectory.cpp
M  +16   -7    rkward/misc/rkoutputdirectory.h
M  +6    -17   rkward/rbackend/rkrinterface.cpp
M  +11   -13   rkward/rbackend/rpackages/rkward/R/rk.output.R
M  +0    -8    rkward/windows/rkhtmlwindow.h

https://invent.kde.org/education/rkward/commit/9bee71d32e2ca694f90d8f006b81ce98f2427b2e

diff --git a/rkward/dialogs/rksavemodifieddialog.cpp b/rkward/dialogs/rksavemodifieddialog.cpp
index fa990668..6d523f60 100644
--- a/rkward/dialogs/rksavemodifieddialog.cpp
+++ b/rkward/dialogs/rksavemodifieddialog.cpp
@@ -158,7 +158,7 @@ void RKSaveModifiedDialog::saveSelected () {
 
 	for (auto it = outputdir_checklist.constBegin (); it != outputdir_checklist.constEnd (); ++it) {
 		if (it.key ()->checkState (0) != Qt::Checked) continue;
-		RKOutputDirectory *dir = RKOutputDirectory::getOutputById(it.value(), nullptr);
+		RKOutputDirectory *dir = RKOutputDirectory::getOutputById(it.value());
 		if (dir) dir->save();
 		else RK_ASSERT(dir);
 	}
diff --git a/rkward/misc/rkoutputdirectory.cpp b/rkward/misc/rkoutputdirectory.cpp
index 7b3e04e2..b2be6be2 100644
--- a/rkward/misc/rkoutputdirectory.cpp
+++ b/rkward/misc/rkoutputdirectory.cpp
@@ -85,6 +85,8 @@ bool copyDirRecursively(const QString& _source_dir, const QString& _dest_dir) {
 	return ok;
 }
 
+QMap<QString, RKOutputDirectory*> RKOutputDirectory::outputs;
+
 RKOutputDirectory::RKOutputDirectory() : initialized(false), window(nullptr) {
 	RK_TRACE(APP);
 }
@@ -93,38 +95,23 @@ RKOutputDirectory::~RKOutputDirectory() {
 	RK_TRACE(APP);
 }
 
-RKOutputDirectory* RKOutputDirectory::getOutputById(const QString& id, GenericRCallResult* result) {
+RKOutputDirectory* RKOutputDirectory::getOutputById(const QString& id) {
 	RK_TRACE (APP);
 
-	if (!outputs.contains(id)) {
-		if (result) result->error = i18n("The output identified by '%1' is not loaded in this session.", id);
-		return 0;
-	}
-	return outputs[id];
+	return outputs.value(id);
 }
 
-RKOutputDirectory* RKOutputDirectory::getOutputBySaveUrl(const QString& _dest, bool create) {
+RKOutputDirectory* RKOutputDirectory::getOutputBySaveUrl(const QString& _dest) {
 	RK_TRACE (APP);
 
-	if (_dest.isEmpty()) {
-		if (!create) return RKOutputWindowManager::self()->getCurrentOutput();
-		return createOutputDirectoryInternal();
-	}
-
 	QString dest = QFileInfo(_dest).canonicalFilePath();
-
 	auto it = outputs.constBegin();
 	while (it != outputs.constEnd()) {
 		if (it.value()->save_dir == dest) {
 			return(it.value());
 		}
 	}
-
-# error move to handleRCall
-	// not returned, yet? We need to create and import an output
-	auto ret = createOutputDirectoryInternal();
-	ret->import(_dest);
-	return ret;
+	return nullptr;
 }
 
 RKOutputDirectory::GenericRCallResult RKOutputDirectory::save(const QString& dest, RKOutputDirectory::OverwriteBehavior overwrite) {
@@ -284,6 +271,8 @@ RKOutputDirectory::GenericRCallResult RKOutputDirectory::clear(OverwriteBehavior
 	dir.mkpath(".");
 	initialized = false;
 	if (isActive()) activate();
+
+	return GenericRCallResult();
 }
 
 bool RKOutputDirectory::isEmpty() const {
@@ -296,17 +285,23 @@ bool RKOutputDirectory::isEmpty() const {
 	return false;
 }
 
-RKOutputDirectory::GenericRCallResult RKOutputDirectory::purge(RKOutputDirectory::OverwriteBehavior discard) {
+bool RKOutputDirectory::isModified() const {
+	RK_TRACE(APP);
+
+	return saved_hash == hashDirectoryState(work_dir);
+}
+
+RKOutputDirectory::GenericRCallResult RKOutputDirectory::purge(RKOutputDirectory::OverwriteBehavior discard, RCommandChain* chain) {
 	RK_TRACE(APP);
 
 	if (isModified()) {
 		if (discard == Fail) {
-			return GenericRCallResult::nakeError(i18n("Output has been modified. Not closing it."));
+			return GenericRCallResult::makeError(i18n("Output has been modified. Not closing it."));
 		}
 		if (discard == Ask) {
 			auto res = KMessageBox::questionYesNoCancel(RKWardMainWindow::getMain(), i18n("The output has been modified, and closing it will discard all changes. What do you want to do?"), i18n("Discard unsaved changes?"), KStandardGuiItem::discard(), KStandardGuiItem::save(), KStandardGuiItem::cancel());
 			if (res == KMessageBox::Cancel) {
-				return GenericRCallResult::nakeError(i18n("User cancelled"));
+				return GenericRCallResult::makeError(i18n("User cancelled"));
 			}
 			if (res == KMessageBox::No) {
 				auto ret = save();
@@ -317,25 +312,33 @@ RKOutputDirectory::GenericRCallResult RKOutputDirectory::purge(RKOutputDirectory
 
 	QDir dir(work_dir);
 	dir.removeRecursively();
-	if (isActive()) {
+	bool active = isActive();
+	outputs.remove(id);
+	deleteLater();
+	if (active) {
 		if (RKQuitAgent::quittingInProgress()) {
-			RK_DEBUG(APP, DEBUG, "Skipping activation of new output file: quitting in progress");
+			RK_DEBUG(APP, DL_DEBUG, "Skipping activation of new output file: quitting in progress");
 		} else {
-			activateDefault();
+			getCurrentOutput(chain)->activate(chain);
 		}
 	}
-	outputs.remove(id);
-	deleteLater();
 	return GenericRCallResult();
 }
 
+QString RKOutputDirectory::workPath() const {
+	RK_TRACE(APP);
+
+	// hardcoded for now, might be made to support several files in the future
+	return (QDir(work_dir).absoluteFilePath("index.html"));
+}
+
 bool RKOutputDirectory::isActive() const {
 	RK_TRACE(APP);
 
-	return RKOutputWindowManager::self()->currentDefaultPath().startsWith(work_dir);
+	return RKOutputWindowManager::self()->currentOutputPath() == workPath();
 }
 
-QStringList RKOutputDirectory::modifiedOutputDirectories() const {
+QStringList RKOutputDirectory::modifiedOutputDirectories() {
 	RK_TRACE (APP);
 
 	QStringList ret;
@@ -358,4 +361,127 @@ void RKOutputDirectory::updateSavedHash() {
 	save_timestamp = QDateTime::currentDateTime();
 }
 
+QList<RKOutputDirectory *> RKOutputDirectory::allOutputs() {
+	RK_TRACE(APP);
+
+	QList<RKOutputDirectory*> ret;
+	for (auto it = outputs.constBegin (); it != outputs.constEnd (); ++it) {
+		ret.append(it.value());
+	}
+	return ret;
+}
+
+RKOutputDirectory* RKOutputDirectory::getCurrentOutput(RCommandChain* chain) {
+	RK_TRACE(APP);
+
+	if (outputs.isEmpty()) {
+		auto n = createOutputDirectoryInternal();
+		n->activate(chain);
+		return n;
+	}
+
+	RKOutputDirectory* candidate = nullptr;
+	for (auto it = outputs.constBegin(); it != outputs.constEnd(); ++it) {
+		if (it.value()->isActive()) return it.value();
+		if (it.value()->filename().isEmpty()) candidate = it.value();
+	}
+#warning generate a warning, when a new output is created or activated
+	if (!candidate) candidate = outputs[0];
+	candidate->activate(chain);
+	return candidate;
+}
+
+RKOutputDirectory::OverwriteBehavior parseOverwrite(const QString &param) {
+	if (param == QStringLiteral("ask")) return RKOutputDirectory::Ask;
+	if (param == QStringLiteral("force")) return RKOutputDirectory::Force;
+	return RKOutputDirectory::Fail;
+}
+
+RKOutputDirectory::GenericRCallResult RKOutputDirectory::view(bool raise) {
+	RK_TRACE(APP);
+
+	auto list = RKOutputWindowManager::self()->existingOutputWindows(workPath());
+	if (!list.isEmpty()) {
+		auto w = list[0];
+		if (!w->isActiveWindow() || raise) {
+			list[0]->activate();
+		}
+	} else {
+		RKOutputWindowManager::self()->newOutputWindow(workPath());
+	}
+	return GenericRCallResult(id);
+}
+
+RKOutputDirectory::GenericRCallResult RKOutputDirectory::handleRCall(const QStringList& params, RCommandChain *chain) {
+	RK_TRACE(APP);
+
+	QString command = params.value(1);
+	if (command == QStringLiteral("get")) {
+		if (params.value(2) == QStringLiteral("all")) {
+			QStringList ret;
+			auto all = allOutputs();
+			for (int i=0; i < all.size(); ++i) {
+				ret.append(all[i]->getId());
+			}
+			return GenericRCallResult(ret);
+		}
+
+		QString filename = params.value(4);
+		bool create = params.value(3) == QStringLiteral("create");
+		RKOutputDirectory *out = nullptr;
+		if (filename.isEmpty()) {
+			if (create) {
+				out = createOutputDirectoryInternal();
+			} else {
+				out = getCurrentOutput(chain);
+			}
+		} else {
+			filename = QFileInfo(filename).canonicalFilePath();
+			out = getOutputBySaveUrl(filename);
+			if (create) {
+				if (out) return GenericRCallResult::makeError(i18n("Output '1%' is already loaded in this session. Cannot create it.", filename));
+				if (QFileInfo(filename).exists()) return GenericRCallResult::makeError(i18n("As file named '1%' already exists. Cannot create it.", filename));
+				out = createOutputDirectoryInternal();
+				return (out->import(filename));
+			} else {
+				if (!out) return (out->import(filename));
+			}
+		}
+		return GenericRCallResult(out->getId());
+	} else {
+		// all other commands pass the output id as second parameter. Look that up, first
+		QString id = params.value(2);
+		auto out = getOutputById(params.value(2));
+		if (!out) {
+			return GenericRCallResult::makeError(i18n("The output identified by '%1' is not loaded in this session.", id));
+		}
+
+		if (command == QStringLiteral("activate")) {
+			return out->activate(chain);
+		} else if (command == QStringLiteral("isEmpty")) {
+			return GenericRCallResult(out->isEmpty());
+		} else if (command == QStringLiteral("isModified")) {
+			return GenericRCallResult(out->isModified());
+		} else if (command == QStringLiteral("revert")) {
+			return out->revert(parseOverwrite(params.value(3)));
+		} else if (command == QStringLiteral("save")) {
+			return out->save(params.value(3), parseOverwrite(params.value(4)));
+		} else if (command == QStringLiteral("export")) {
+			return out->exportAs(params.value(3), parseOverwrite(params.value(4)));
+		} else if (command == QStringLiteral("clear")) {
+			return out->clear(parseOverwrite(params.value(3)));
+		} else if (command == QStringLiteral("close")) {
+			return out->purge(parseOverwrite(params.value(3)), chain);
+		} else if (command == QStringLiteral("view")) {
+			return out->view(params.value(3) == QStringLiteral("raise"));
+		} else if (command == QStringLiteral("workingDir")) {
+			return GenericRCallResult(out->workDir());
+		} else if (command == QStringLiteral("filename")) {
+			return GenericRCallResult(out->filename());
+		}
+	}
+	return GenericRCallResult::makeError(i18n("Unhandled output command '%1'", command));
+}
+
+
 #include "rkoutputdirectory.moc"
diff --git a/rkward/misc/rkoutputdirectory.h b/rkward/misc/rkoutputdirectory.h
index e56b42ba..56caa447 100644
--- a/rkward/misc/rkoutputdirectory.h
+++ b/rkward/misc/rkoutputdirectory.h
@@ -58,19 +58,28 @@ public:
 	GenericRCallResult save(const QString& dest=QString(), OverwriteBehavior overwrite=Ask);
 	GenericRCallResult exportAs(const QString& dest=QString(), OverwriteBehavior overwrite=Ask);
 	GenericRCallResult clear(OverwriteBehavior discard=Ask);
-	GenericRCallResult purge(OverwriteBehavior discard=Ask);
+	GenericRCallResult purge(OverwriteBehavior discard=Ask, RCommandChain* chain=0);
 	QString getId() const { return id; };
 	bool isEmpty() const;
 	bool isActive() const;
 	bool isModified() const;
-	void view(bool raise);
-	QString filename() const;
-	QString workDir() const;
-	static GenericRCallResult handleRCall(const QStringList& params);
-	static RKOutputDirectory* getOutputById(const QString& id, GenericRCallResult* result);
-	static RKOutputDirectory* getOutputBySaveUrl(const QString& dest, bool create=false);
+	GenericRCallResult view(bool raise);
+	QString filename() const { return save_dir; };
+	QString workDir() const { return work_dir; }
+	QString workPath() const;
+	static GenericRCallResult handleRCall(const QStringList& params, RCommandChain *chain);
+	static RKOutputDirectory* getOutputById(const QString& id);
+	static RKOutputDirectory* getOutputBySaveUrl(const QString& dest);
 /** Return a list of all current output directories that have been modified. Used for asking for save during shutdown. */
 	static QStringList modifiedOutputDirectories();
+
+	static RKOutputDirectory::GenericRCallResult R_rk_Output(const QString& filename=QString(), bool create=false, bool all=false);
+/** Returns the active output (in case there is one).
+ *  If no output is active, find an activate the next output without a save url.
+ *  If that does not exist, activate and return the next existing output.
+ *  If that does not exist, create a new output, activate and return it. */
+	static RKOutputDirectory* getCurrentOutput(RCommandChain *chain=0);
+	static QList<RKOutputDirectory*> allOutputs();
 private:
 	RKOutputDirectory();
 	~RKOutputDirectory();
diff --git a/rkward/rbackend/rkrinterface.cpp b/rkward/rbackend/rkrinterface.cpp
index 9618d438..a1b71fda 100644
--- a/rkward/rbackend/rkrinterface.cpp
+++ b/rkward/rbackend/rkrinterface.cpp
@@ -317,7 +317,7 @@ void RInterface::rCommandDone (RCommand *command) {
 			issueCommand (*it, RCommand::App | RCommand::Sync, QString (), this, SET_RUNTIME_OPTS, chain);
 		}
 		// initialize output file
-		RKOutputWindowManager::self ()->createOutputDirectory (chain);
+		RKOutputDirectory::getCurrentOutput(chain);
 
 #ifdef Q_OS_MACOS
 		// On MacOS, the backend is started from inside R home to allow resolution of dynamic libs. Re-set to frontend wd, here.
@@ -745,23 +745,12 @@ void RInterface::processHistoricalSubstackRequest (const QStringList &calllist,
 			RK_ASSERT (false);
 		}
 	} else if (call == QStringLiteral ("output")) {
-		const QString &subcall = calllist.value (1);
-		QString error;
-		if (subcall == QStringLiteral ("export")) {
-			error = RKOutputWindowManager::self()->exportOutputDirectoryAs(calllist.value(2), calllist.value(3), calllist.value(4) == QStringLiteral ("TRUE"), in_chain);
-		} else if (subcall == QStringLiteral ("import")) {
-			RKOutputWindowManager::ImportMode mode = RKOutputWindowManager::Ask;
-			if (calllist.value (4) == "integrate") mode = RKOutputWindowManager::Integrate;
-			else if (calllist.value (4) == "keep.separate") mode = RKOutputWindowManager::KeepSeparate;
-#warning TODO mode
-			error = RKOutputWindowManager::self()->importOutputDirectory(calllist.value(2), calllist.value(3), calllist.value(5) == QStringLiteral ("TRUE"), in_chain);
-		} else if (subcall == QStringLiteral ("create")) {
-			RKOutputWindowManager::self ()->createOutputDirectory (in_chain);
+		RKOutputDirectory::GenericRCallResult res = RKOutputDirectory::handleRCall(calllist.mid(1), in_chain);
+#warning TODO
+		if (res.failed()) {
+			request->params["return"] = QVariant(QStringList(QStringLiteral("error")) << res.error);
 		} else {
-			RK_ASSERT (false);
-		}
-		if (!error.isEmpty ()) {
-			request->params["return"] = QVariant (QStringList (QStringLiteral ("error")) << error);
+			request->params["return"] = QVariant(res.ret);
 		}
 	} else {
 		request->params["return"] = QVariant (QStringList (QStringLiteral ("error")) << i18n ("Unrecognized call '%1'", call));
diff --git a/rkward/rbackend/rpackages/rkward/R/rk.output.R b/rkward/rbackend/rpackages/rkward/R/rk.output.R
index 4950459e..5e7c979a 100644
--- a/rkward/rbackend/rpackages/rkward/R/rk.output.R
+++ b/rkward/rbackend/rpackages/rkward/R/rk.output.R
@@ -23,7 +23,7 @@
 #'
 #' @param create If \code{TRUE}, create a new output directory. The parameter \code{filename}, if specified, is the target save file/directory, in this case. Should this already exist,
 #'               an error will be raised. If \code{create=FALSE}, load or re-use an existing output directory. If the parameter \code{filename} is left \code{NULL}, \code{rk.output} will
-#'               return the currently active output.
+#'               return the currently active output in this case (creating and/or activating an output file, in case all outputs have been closed or deactivated).
 #'
 #' @param all If \code{TRUE}, return a list of all currently loaded output directories.
 #'
@@ -32,8 +32,6 @@
 #'
 #' @param discard See \code{overwrite} for the meaning.
 #'
-#' @param ask Prompt the user for confirmation before potentially desctrucitve operations such as closing or reverting a modified output.
-#'
 #' @param raise Raise the output window, if it is already visble.
 #'
 #' @returns NULL
@@ -55,32 +53,32 @@ RK.Output <- setRefClass(Class="RK.Output", fields=list(id="character"),
 "Returns TRUE, if this output has any changes that may need saving."
 			isTRUE(.rk.do.call("output", c ("isModified", .checkId())))
 		},
-		revert=function(ask=TRUE) {
+		revert=function(discard=NULL) {
 "Revert this output to the last saved state. If no previous state is available (never saved, before), clears the output."
-			.rk.do.call("output", c ("revert", .checkId(), isTRUE(ask)))
+			.rk.do.call("output", c ("revert", .checkId(), if (is.Null(discard)) "ask" else isTRUE(discard) ? "force" : "fail"))
 		},
 		save=function(filename, overwrite=NULL) {
 "Save this output, either to the last known save location (if no filename is specified) or to a new location (\"save as\")."
-			.rk.do.call("output", c ("save", .checkId(), filename, if (is.Null(overwrite)) "ask" else isTRUE(overwrite)))
+			.rk.do.call("output", c ("save", .checkId(), filename, if (is.Null(overwrite)) "ask" else isTRUE(overwrite) ? "force" : "fail"))
 		},
 		export=function(filename, overwrite=NULL) {
 "Save this output, to the specified location, but keep it associated with the previous location (\"save a copy\")."
 			if (missing(filename)) stop("No file name specified")
-			.rk.do.call("output", c ("export", .checkId(), filename, if (is.Null(overwrite)) "ask" else isTRUE(overwrite)))
+			.rk.do.call("output", c ("export", .checkId(), filename, if (is.Null(overwrite)) "ask" else isTRUE(overwrite) ? "force" : "fail"))
 		},
-		clear=function(ask=TRUE) {
+		clear=function(discard=NULL) {
 "Clear all content from this output. As with any function in this class, this affects the working copy, only, until you call save. Therefore, by default, the user will be prompted for confirmation
 if and only if there are unsaved changes pending."
-			.rk.do.call("output", c ("clear", .checkId(), isTRUE(ask)))
+			.rk.do.call("output", c ("clear", .checkId(), if (is.Null(discard)) "ask" else isTRUE(discard) ? "force" : "fail"))
 		},
 		close=function(discard=NULL) {
 "Forget about this output file, also closing any open views. Note: Trying to call any further methods on this object will fail."
-			.rk.do.call("output", c ("close", .checkId(), if (is.Null(discard)) "ask" else isTRUE(discard)))
+			.rk.do.call("output", c ("close", .checkId(), if (is.Null(discard)) "ask" else isTRUE(discard) ? "force" : "fail"))
 			id=NULL
 		},
 		view=function(raise=TRUE) {
 "Open this output for viewing in the frontend."
-			.rk.do.call("output", c ("view", .checkId(), isTRUE(raise)))
+			.rk.do.call("output", c ("view", .checkId(), isTRUE(raise) ? "raise" : ""))
 		},
 		.workingDir=function() {
 "The path of the working copy of this object. Please don't use this except for automated tests. The internals may be subject to change."
@@ -91,7 +89,7 @@ if and only if there are unsaved changes pending."
 Do not write anything to the target filename, directly! This is purely for information."
 			.rk.do.call("output", c ("filename", .checkId()))
 		},
-		.checkId=function() {
+		.checkId=function(id) {
 "For internal use: Throws an error, if the id parameter is NULL or too long, returns a length one character vector otherwise."
 			i <- as.character(id)
 			if (length(i) != 1) stop ("Invalid output id. Use rk.output() to obtain a valid output handle.")
@@ -103,5 +101,5 @@ Do not write anything to the target filename, directly! This is purely for infor
 #' @rdname RK.Output
 "rk.output" <- function(filename=NULL, create=FALSE, all=FALSE) {
 	if(all && (!is.Null(filename) || create)) stop("'all' cannot be combined with 'create' or 'filename'")
-	.rk.do.call("output", c ("get", as.character(isTRUE(create)), as.character(isTRUE(all)), as.character(load)))
+	.rk.do.call("output", c ("get", isTRUE(all) ? "all" : "one", isTRUE(create) ? "create" : "get", as.character(filename)))
 }
diff --git a/rkward/windows/rkhtmlwindow.h b/rkward/windows/rkhtmlwindow.h
index 9959c20f..59496ea8 100644
--- a/rkward/windows/rkhtmlwindow.h
+++ b/rkward/windows/rkhtmlwindow.h
@@ -213,14 +213,8 @@ public:
 /** 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 ());
 
-	RKOutputDirectory::GenericRCallResult R_rk_Output(const QString& filename=QString(), bool create=false, bool all=false);
-	RKOutputDirectory* getCurrentOutput(const QString& filename=QString(), bool create=false);
-	QList<RKOutputDirectory*> allOutputs();
-
 /** Return the name / caption of the given output directory */
 	QString outputCaption (const QString &dir) const;
-/** Use with care! Purges all current output directories, saved or not. You should query modifiedOutputDirectories (), and make sure to prompt for saving, before calling this. For use during shutdown. */
-	void purgeAllOututputDirectories ();
 private:
 	RKOutputWindowManager ();
 	~RKOutputWindowManager ();
@@ -233,8 +227,6 @@ 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