[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