[education/rkward/devel/workspace_output] /: Merge branch 'master' into work/workspace_output and fix compilation

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


Git commit d10c628a622ad501b018011224709fe9aa57198d by Thomas Friedrichsmeier.
Committed on 04/10/2020 at 18:36.
Pushed by tfry into branch 'devel/workspace_output'.

Merge branch 'master' into work/workspace_output and fix compilation

M  +70   -3    ChangeLog
M  +2    -1    rkward/dialogs/CMakeLists.txt
A  +6    -6    rkward/dialogs/rksavemodifieddialog.cpp     [License: GPL (v2+)]
M  +72   -18   rkward/misc/rkcommonfunctions.cpp
M  +8    -2    rkward/misc/rkcommonfunctions.h
M  +182  -71   rkward/rbackend/rkrbackend.cpp
M  +4    -4    rkward/rbackend/rkrbackend.h
M  +79   -84   rkward/rbackend/rkrinterface.cpp
M  +15   -7    rkward/rbackend/rkrinterface.h
M  +7    -2    rkward/rbackend/rpackages/rkward/NAMESPACE
M  +6    -4    rkward/rbackend/rpackages/rkward/R/base_overrides.R
M  +21   -24   rkward/rbackend/rpackages/rkward/R/internal.R
M  +18   -5    rkward/rbackend/rpackages/rkward/R/rk.filename-functions.R
M  +1    -1    rkward/rbackend/rpackages/rkward/R/rk.plugin-functions.R
M  +1    -1    rkward/rbackend/rpackages/rkward/R/rk.workspace-functions.R
M  +2    -3    rkward/rbackend/rpackages/rkward/man/rk.assign.preview.data.Rd
M  +36   -8    rkward/rbackend/rpackages/rkward/man/rk.get.tempfile.name.Rd
M  +8    -6    rkward/rbackend/rpackages/rkward/man/rk.load.pluginmaps.Rd
M  +121  -21   rkward/rkward.cpp
A  +1    -0    rkward/rkward.kdev4
A  +1    -0    rkward/rkward.sourcedir
M  +1    -0    rkward/windows/detachedwindowcontainer.cpp
M  +585  -0    rkward/windows/katepluginintegration.cpp
M  +304  -363  rkward/windows/rkcommandeditorwindow.cpp
M  +41   -79   rkward/windows/rkcommandeditorwindow.h
M  +1    -1    rkward/windows/rkdebugconsole.h
M  +336  -132  rkward/windows/rkhtmlwindow.cpp
M  +15   -29   rkward/windows/rkhtmlwindow.h
M  +18   -14   rkward/windows/rkmdiwindow.cpp
M  +2    -1    rkward/windows/rkmdiwindow.h
M  +36   -6    rkward/windows/rktoolwindowbar.cpp
M  +47   -14   rkward/windows/rktoplevelwindowgui.cpp
M  +7    -4    rkward/windows/rkwindowcatcher.cpp
M  +2    -1    rkward/windows/rkwindowcatcher.h
M  +44   -17   rkward/windows/rkworkplace.cpp
M  +32   -7    rkward/windows/rkworkplace.h
M  +2    -2    rkward/windows/rkworkplaceview.cpp

https://invent.kde.org/education/rkward/commit/d10c628a622ad501b018011224709fe9aa57198d

diff --cc ChangeLog
index acb81cb5,9ab2a9ae..5f84804b
--- a/ChangeLog
+++ b/ChangeLog
@@@ -1,6 -1,71 +1,73 @@@
- TODO: Check for duplicated factor levels in data editor, as these are no longer allowed in R
- 
 +- Hide or remove several purely internal functions (most can still be assessed from the rkward namespace as rkward:::xyz())
++
+ --- Version 0.7.2 - UNRELEASED
+ - Python development scripts have been ported to python3
+ - Fix some problems with plot preview in wizard-type plugins
+ - Add function rk.home() for retrieving applicaiton paths, similar to R.home()
+ - Remove direct dependency on libintl
+ - Add menu option to switch application language
+ - Fix several small memory leaks
+ - Clean some logged warnings and potential issues during R backend shutdown
+ - Add "Check installation" wizard to test for several common basic instation issues all in one place
+ - Add rkward internal package location to end of library search path, to make it accessible to help.search()
+ - Add menu action to open any supported file type, directly
+ - Support using QWebEngine instead of QtWebKit (still supported)
+ - <text> elements in plugins may now also contain clickable links, including rkward://-scheme links
+ - The new code hinting features from version 0.7.1 are now also available in the console
+ - On unix-systems, RKWard can now be run without installation
+ - Kate addons are now supported within RKWard. Intially, search-in-files, snippets, and projects are loaded by default
+   * TODO: Instruct packagers that they should add a dependency on kate, or at least kate plugins
+ - Fixed: Avoid shorcut clash with kate part by removing conflicting (default) shortcuts, automatically
+ 
+ --- Version 0.7.1b - May-25-2020
+ - Workaround to avoid bug in R 4.0.0 when using for loop on top level context
+ - Fixed: Hang when opening an empty workplace
+ - Fixed: Crash when mousing over code preview window with focus-follows-mouse enabled
+ 
+ --- Version 0.7.1 - Jan-23-2020
+ - Code hinting in script editor windows has been reworked, and now also completes argument names
+ - Instead of installing rkward R packages at build time, install them at runtime, when needed
+ - Add new commandline option "--r-executable=auto" for auto-detection of R installation
+ - Fixed: Underscore (_) was not accepted in names of newly created data.frames
+ - <browser> element in save file mode gains checkbox to control overwriting of existing files
+ - Fixed: <browser> element could not be set to not required
+ - Fixed: max.print option was not saved, correctly, when set from RKWard settings
+ - Add command-line option --autoreuse to avoid shipping two separate .desktop files
+ - <select> elements in plugin dialogs can be set to accept only a single selection
+ - New R functions rk.capture.output() and rk.end.capture.output()
+ - Allow to reference current script file in plugins
+ - Add various live-preview options for R scripts, including a preview of R markdown rendering
+ - Make it possible to "link" to specific settings pages from the internal documentation
+ - Slighlty less confusing output in case a package is installed from require(), such as in many plugins
+ - Fixed: RKWard icon was missing for the "enhances RKWard" column in package installation dialog
+ - Fixed a layout issue in plugins using <stretch>-elements that would sometimes also render top-left aligned UI-elements unsuable.
+ - Remove a bit of unused empty space around the main area of plugin dialogs
+ - Expand root level objects (esp. data.frames) by default in plugin object lists. Add button to toggle back to collapsed.
+ - Allow Tab-key to advance to the next row of data in data editor
+ - Fix highlighting of "trailing" rows and columns in data editor
+ - Fixed: Loading existing workspace from startup dialog failed silently.
+ - Support handling of help:/ pages (e.g. RKWard plugins documentation) inside the RKWard help window, again.
+ - Do not attempt object name hinting behind empty quoted strings or spaces.
+ - rk.show.question() gains a parameter to allow setting a default button other than "yes"
+ - new function rk.askYesNo() combines the functionality of askYesNo() (R >= 3.5.0) with rk.show.question() and is the default option for askYesNo() calls
+ - Compile in ui definitions, instead of providing them as separate files
+ - Fix printing of strings with R 3.5.0 on Windows in the console
+ - Offer R_LIBS_USER, instead of ~/.rkward/library as standard library installation location
+ - Support R version placeholder (%v) in custom library locations.
+ - File browser tool window follows changes in current working directory (while showing current working directory)
+ - Show focus indication in highlighted item color (usually blue), instead of hardcoded red
+ - On Mac, do not attempt to start DBus, if it is already running
+ - Fix most compiler warnings
+ - File tool window gains button to switch to current working directory
+ - Fix a crash when closing a plugin dialog with an active plot preview
+ - Do not throw an error on objects that return non-numeric/non-scalar dim()
+ 
+ --- Version 0.7.0b - Apr-16-2018
+ - Fix failure to install translations
+ 
+ --- Version 0.7.0 - Apr-16-2018
+ - Fix crash with R 3.5.x due to use to STRING_PTR
+ - Double click on object in Workspace browser tries to open reference page, not object viewer, if the object is outside of .GlobalEnv
  - Implement "split view" feature, allowing to partion the main window, and to hvae several views of the same files / data side-by-side
  - Fixed: Creating trellis on-screen plots, while package lattice is not on the search path would produce errors in plot history mechanism
  - Limit the number of debug log-files to keep (at most three, each, for frontend and backend)
diff --cc rkward/dialogs/CMakeLists.txt
index da6936cc,2c97eb90..74dfa46d
--- a/rkward/dialogs/CMakeLists.txt
+++ b/rkward/dialogs/CMakeLists.txt
@@@ -8,7 -8,7 +8,8 @@@ SET(dialogs_STAT_SRC
     rkselectlistdialog.cpp
     rkrecoverdialog.cpp
     rkerrordialog.cpp
 +   rksavemodifieddialog.cpp
+    rksetupwizard.cpp
     )
  
  ADD_LIBRARY(dialogs STATIC ${dialogs_STAT_SRCS})
diff --cc rkward/dialogs/rksavemodifieddialog.cpp
index 10b1111d,00000000..3ea71ea1
mode 100644,000000..100644
--- a/rkward/dialogs/rksavemodifieddialog.cpp
+++ b/rkward/dialogs/rksavemodifieddialog.cpp
@@@ -1,165 -1,0 +1,165 @@@
 +/***************************************************************************
 +                          rksavemodifieddialog  -  description
 +                             -------------------
 +    begin                : Wed Jul 12 2017
 +    copyright            : (C) 2017 by Thomas Friedrichsmeier
 +    email                : thomas.friedrichsmeier at kdemail.net
 + ***************************************************************************/
 +
 +/***************************************************************************
 + *                                                                         *
 + *   This program is free software; you can redistribute it and/or modify  *
 + *   it under the terms of the GNU General Public License as published by  *
 + *   the Free Software Foundation; either version 2 of the License, or     *
 + *   (at your option) any later version.                                   *
 + *                                                                         *
 + ***************************************************************************/ 
 +
 +#include "rksavemodifieddialog.h"
 +
 +#include <QTreeWidget>
 +#include <QDialogButtonBox>
 +#include <QVBoxLayout>
 +#include <QPushButton>
 +#include <QHeaderView>
 +#include <QLabel>
 +#include <QPointer>
 +
- #include <klocale.h>
++#include <KLocalizedString>
 +
 +#include "../windows/rkworkplace.h"
 +#include "../windows/rkhtmlwindow.h"
 +
 +#include "../debug.h"
 +
 +bool RKSaveModifiedDialog::askSaveModified (QWidget* parent, QList <RKMDIWindow*> windows, bool project) {
 +	RK_TRACE (APP);
 +
- 	save_project_check = 0;
- 
 +	QList<RKMDIWindow*> modified_wins;
 +	for (int i = 0; i < windows.size (); ++i) {
 +		if (windows[i]->isModified ()) {
 +			modified_wins.append (windows[i]);
 +		}
 +	}
 +
 +	if (project || !modified_wins.isEmpty ()) {
 +		RKSaveModifiedDialog dialog (parent, modified_wins, project);
 +		dialog.exec ();
 +		if (dialog.result () == QDialog::Rejected) return false;
 +	}
 +
 +	return true;
 +}
 +
 +QTreeWidgetItem* makeHeaderItem (const QString &label, QTreeWidget* tree) {
 +	RK_TRACE (APP);
 +
 +	QTreeWidgetItem* header = new QTreeWidgetItem (QStringList (label));
 +	header->setFirstColumnSpanned (true);
 +	header->setFlags (Qt::ItemIsEnabled);
 +	QFont f = tree->font ();
 +	f.setBold (true);
 +	header->setFont (0, f);
 +	tree->addTopLevelItem (header);
 +	header->setExpanded (true);
 +
 +	return header;
 +}
 +
 +RKSaveModifiedDialog::RKSaveModifiedDialog (QWidget* parent, QList<RKMDIWindow*> modified_wins, bool project) : QDialog (parent) {
 +	RK_TRACE (APP);
 +
 +	setWindowTitle (i18n ("Save modified"));
 +
 +	QVBoxLayout* v_layout = new QVBoxLayout (this);
 +	QLabel *label = new QLabel (i18n ("The following items have been modified. Do you want to save them before closing?"));
 +	v_layout->addWidget (label);
 +
 +	QTreeWidget *tree = new QTreeWidget ();
 +	v_layout->addWidget (tree);
 +
++	save_project_check = 0;
 +	tree->header ()->hide ();
 +#warning TODO: remove me
 +project = true;
 +	if (project) {
 +		QTreeWidgetItem *header = makeHeaderItem (i18n ("R Workspace (Data and Functions)"), tree);
 +		QString url = RKWorkplace::mainWorkplace ()->workspaceURL ().toDisplayString ();
 +		if (url.isEmpty ()) {
 +			url = i18n ("Not previously saved");
 +		}
 +		QTreeWidgetItem *save_project_check = new QTreeWidgetItem (QStringList (url));
 +		header->addChild (save_project_check);
 +		save_project_check->setCheckState (0, Qt::Checked);
 +
- 		QStringList modified_outputs = RKOutputWindowManager::modifiedOutputDirectories ();
++		QStringList modified_outputs = RKOutputWindowManager::self()->modifiedOutputDirectories();
 +		if (!modified_outputs.isEmpty ()) {
 +			QTreeWidgetItem *header = makeHeaderItem (i18n ("Output files"), tree);
 +			for (int i = 0; i < modified_outputs.size (); ++i) {
- 				QTreeWidgetItem *item = new QTreeWidgetItem (RKOutputWindowManager::outputCaption (modified_outputs[i]));
++				QTreeWidgetItem *item = new QTreeWidgetItem();
++				item->setText(0, RKOutputWindowManager::self()->outputCaption(modified_outputs[i]));
 +				item->setFirstColumnSpanned (true);
 +				header->addChild (item);
 +				item->setCheckState (0, Qt::Checked);
 +				outputdir_checklist.insert (item, modified_outputs[i]);
 +			}
 +		}
 +	}
 +	if (!modified_wins.isEmpty ()) {
 +		QTreeWidgetItem* header = makeHeaderItem (i18n ("Scripts"), tree);
 +		for (int i = 0; i < modified_wins.size (); ++i) {
 +			QTreeWidgetItem *item = new QTreeWidgetItem (QStringList (modified_wins[i]->fullCaption ()));
 +			item->setFirstColumnSpanned (true);
 +			header->addChild (item);
 +			item->setCheckState (0, Qt::Checked);
 +			window_checklist.insert (item, QPointer<RKMDIWindow> (modified_wins[i]));
 +		}
 +	}
 +
 +	QDialogButtonBox *buttonbox = new QDialogButtonBox (this);
 +	v_layout->addWidget (buttonbox);
 +
 +	buttonbox->setStandardButtons (QDialogButtonBox::Save | QDialogButtonBox::Discard | QDialogButtonBox::Cancel);
 +	buttonbox->button (QDialogButtonBox::Save)->setDefault (true);
 +	buttonbox->button (QDialogButtonBox::Save)->setText (i18nc ("Save the selected items", "Save selected"));
 +	buttonbox->button (QDialogButtonBox::Discard)->setText (i18n ("Discard all"));
 +	buttonbox->button (QDialogButtonBox::Cancel)->setText (i18n ("Do not close"));
 +
 +	connect (buttonbox->button (QDialogButtonBox::Save), &QPushButton::clicked, this, &RKSaveModifiedDialog::saveSelected);
 +	connect (buttonbox->button (QDialogButtonBox::Discard), &QPushButton::clicked, this, &QDialog::accept);
 +	connect (buttonbox->button (QDialogButtonBox::Cancel), &QPushButton::clicked, this, &QDialog::reject);
 +
 +	setModal (true);
 +}
 +
 +RKSaveModifiedDialog::~RKSaveModifiedDialog () {
 +	RK_TRACE (APP);
 +}
 +
 +void RKSaveModifiedDialog::saveWorkplaceChanged () {
 +	RK_TRACE (APP);
 +// TODO: enable / disable "save with workplace" option for Output windows
 +}
 +
 +void RKSaveModifiedDialog::saveSelected () {
 +	RK_TRACE (APP);
 +
 +	bool all_ok = true;
 +	for (QMap<QTreeWidgetItem *, QPointer<RKMDIWindow>>::const_iterator it = window_checklist.constBegin (); it != window_checklist.constEnd (); ++it) {
 +		if (it.key ()->checkState (0) != Qt::Checked) continue;
 +		if (!it.value ()) continue;
 +		if (!it.value ()->save ()) all_ok = false; // but we proceed with the others
 +	}
 +
 +	if (save_project_check && save_project_check->checkState (0) == Qt::Checked) {
 +#warning TODO
 +	}
 +
 +	for (QMap<QTreeWidgetItem *, QString>::const_iterator it = outputdir_checklist.constBegin (); it != outputdir_checklist.constEnd (); ++it) {
 +		if (it.key ()->checkState (0) != Qt::Checked) continue;
- 		RKOutputWindowManager::saveOutputDirectory (it.value ());
++		RKOutputWindowManager::self()->saveOutputDirectory(it.value());
 +	}
 +
 +	if (all_ok) accept ();
 +	else reject ();
 +}
diff --cc rkward/misc/rkcommonfunctions.cpp
index e12bd970,ecf8d5ba..2aa05905
--- a/rkward/misc/rkcommonfunctions.cpp
+++ b/rkward/misc/rkcommonfunctions.cpp
@@@ -155,9 -173,45 +173,32 @@@ namespace RKCommonFunctions 
  	}
  
  	QString getRKWardDataDir () {
- 		return (QStandardPaths::locate (QStandardPaths::GenericDataLocation, "rkward/resource.ver").replace ("resource.ver", QString ()));
+ 		static QString rkward_data_dir;
+ 		if (rkward_data_dir.isNull ()) {
+ 			QString inside_build_tree = QCoreApplication::applicationDirPath() + "/rkwardinstall/";
+ 			if (QFileInfo(inside_build_tree).isReadable()) {
+ 				rkward_data_dir = inside_build_tree;
+ 				return rkward_data_dir;
+ 			}
+ #if (QT_VERSION >= QT_VERSION_CHECK(5, 4, 0))
+ 			QStringList candidates = QStandardPaths::locateAll (QStandardPaths::AppDataLocation, "resource.ver");
+ 			candidates += QStandardPaths::locateAll (QStandardPaths::AppDataLocation, "rkward/resource.ver");  // Well, isn't this just silly? AppDataLocation may or may not contain the application name (on Mac)
+ #else
+ 			QStringList candidates = QStandardPaths::locateAll (QStandardPaths::GenericDataLocation, "resource.ver");  // Fails on Mac with unpatched Qt 5.10 (and before). See https://mail.kde.org/pipermail/kde-frameworks-devel/2018-May/063151.html
+ #endif
+ 			for (int i = 0; i < candidates.size (); ++i) {
+ 				QFile resource_ver (candidates[i]);
+ 				if (resource_ver.open (QIODevice::ReadOnly) && (resource_ver.read (100).trimmed () == RKWARD_VERSION)) {
+ 					rkward_data_dir = candidates[i].replace ("resource.ver", QString ());
+ 					return rkward_data_dir;
+ 				}
+ 			}
+ 			rkward_data_dir = "";   // prevents checking again
+ 			RK_DEBUG(APP, DL_WARNING, "resource.ver not found. Data path(s): %s", qPrintable (QStandardPaths::standardLocations (QStandardPaths::AppDataLocation).join (':')));
+ 		}
+ 		return rkward_data_dir;
  	}
  
 -	QString getUseableRKWardSavefileName (const QString &prefix, const QString &postfix) {
 -		QDir dir (RKSettingsModuleGeneral::filesPath ());
 -
 -		int i=0;
 -		while (true) {
 -			QString candidate = prefix + QString::number (i) + postfix;
 -			if (!dir.exists (candidate)) {
 -				return dir.filePath (candidate);
 -			}
 -			i++;
 -		}
 -	}
 -
  	QString escape (const QString &in) {
  		QString out;
  
diff --cc rkward/rbackend/rkrbackend.cpp
index bea3de91,a16936c7..4fb3e86d
--- a/rkward/rbackend/rkrbackend.cpp
+++ b/rkward/rbackend/rkrbackend.cpp
@@@ -844,10 -899,10 +899,10 @@@ extern "C" int R_interrupts_pending
  #else
  LibExtern int R_interrupts_pending;
  #endif
 -SEXP doError (SEXP call) {
 +void doError (QString callstring) {
  	RK_TRACE (RBACKEND);
  
- 	if ((RKRBackend::repl_status.eval_depth == 0) && (!RKRBackend::repl_status.browser_context) && (!RKRBackend::this_pointer->isKilled ()) && (RKRBackend::repl_status.user_command_status != RKRBackend::RKReplStatus::ReplIterationKilled) && (!RKRBackend::repl_status.user_command_status == RKRBackend::RKReplStatus::NoUserCommand)) {
+ 	if ((RKRBackend::repl_status.eval_depth == 0) && (!RKRBackend::repl_status.browser_context) && (!RKRBackend::this_pointer->isKilled ()) && (RKRBackend::repl_status.user_command_status != RKRBackend::RKReplStatus::ReplIterationKilled) && (RKRBackend::repl_status.user_command_status != RKRBackend::RKReplStatus::NoUserCommand)) {
  		RKRBackend::repl_status.user_command_status = RKRBackend::RKReplStatus::UserCommandFailed;
  	}
  	if (RKRBackend::repl_status.interrupted) {
@@@ -862,11 -917,21 +917,19 @@@
  			}
  		}
  	} else if (RKRBackend::repl_status.user_command_status != RKRBackend::RKReplStatus::ReplIterationKilled) {
 -		QString string = RKRSupport::SEXPToString (call);
 -		RKRBackend::this_pointer->handleOutput (string, string.length (), ROutput::Error);
 -		RK_DEBUG (RBACKEND, DL_DEBUG, "error '%s'", qPrintable (string));
 +		RKRBackend::this_pointer->handleOutput (callstring, callstring.length (), ROutput::Error);
 +		RK_DEBUG (RBACKEND, DL_DEBUG, "error '%s'", qPrintable (callstring));
  	}
 -	return R_NilValue;
  }
  
+ SEXP doWs (SEXP name) {
+ 	if ((!RKRBackend::this_pointer->current_command) || (RKRBackend::this_pointer->current_command->type & RCommand::ObjectListUpdate) || (!(RKRBackend::this_pointer->current_command->type & RCommand::Sync))) {		// ignore Sync commands that are not flagged as ObjectListUpdate
+ 		QString sym = RKRSupport::SEXPToString(name);
+ 		if (!RKRBackend::this_pointer->changed_symbol_names.contains (sym)) RKRBackend::this_pointer->changed_symbol_names.append (sym);  // schedule symbol update for later
+ 	}
+ 	return R_NilValue;
+ }
+ 
  SEXP doSubstackCall (SEXP call) {
  	RK_TRACE (RBACKEND);
  
@@@ -898,51 -962,6 +961,44 @@@ SEXP doPlainGenericRequest (SEXP call, 
  	return RKRSupport::StringListToSEXP (ret);
  }
  
 +// Function to handle several simple calls from R code, that do not need any special arguments, or interaction with the frontend process.
 +SEXP doSimpleBackendCall (SEXP _call) {
 +	RK_TRACE (RBACKEND);
 +
 +	QStringList list = RKRSupport::SEXPToStringList (_call);
 +	QString call = list[0];
 +
- 	// symbol updates
- 	if (list.count () == 2 && call == QStringLiteral ("ws")) {		// schedule symbol update for later
- 		// always keep in mind: No current command can happen for tcl/tk events.
- 		if ((!RKRBackend::this_pointer->current_command) || (RKRBackend::this_pointer->current_command->type & RCommand::ObjectListUpdate) || (!(RKRBackend::this_pointer->current_command->type & RCommand::Sync))) {		// ignore Sync commands that are not flagged as ObjectListUpdate
- 			if (!RKRBackend::this_pointer->changed_symbol_names.contains (list[1])) RKRBackend::this_pointer->changed_symbol_names.append (list[1]);
- 		}
- 		return R_NilValue;
- 	} else if (call == QStringLiteral ("unused.filename")) {
++	if (call == QStringLiteral ("unused.filename")) {
 +		QString prefix = list.value (1);
 +		QString extension = list.value (2);
 +		QString dirs = list.value (3);
 +		QDir  dir (dirs);
 +		if (dirs.isEmpty ()) {
 +			dir = QDir (RKRBackendProtocolBackend::dataDir ());
 +		}
 +
 +		int i = 0;
 +		while (true) {
 +			QString candidate = prefix + QString::number (i) + extension;
 +			if (!dir.exists (candidate)) {
 +				return (RKRSupport::StringListToSEXP (QStringList (candidate) << dir.absoluteFilePath (candidate))); // return as c (relpath, abspath)
 +			}
 +			i++;
 +		}
 +	} else if (call == QStringLiteral ("error")) {  // capture error message
 +		doError (list.value (1));
 +		return R_NilValue;
 +	} else if (call == QStringLiteral ("locale.name")) {
 +		RK_ASSERT (QTextCodec::codecForLocale());
 +		return (RKRSupport::StringListToSEXP (QStringList (QTextCodec::codecForLocale()->name ().data ())));
 +	} else if (call == QStringLiteral ("tempdir")) {
 +		return (RKRSupport::StringListToSEXP (QStringList (RKRBackendProtocolBackend::dataDir ())));
 +	}
 +
 +	RK_ASSERT (false);  // Unhandled call.
 +	return R_NilValue;
 +}
 +
  void R_CheckStackWrapper (void *) {
  	R_CheckStack ();
  }
@@@ -1065,18 -1114,22 +1139,21 @@@ bool RKRBackend::startR () 
  
  // register our functions
  	R_CallMethodDef callMethods [] = {
+ 		// NOTE: Intermediate cast to void* to avoid compiler warning
+ 		{ "ws", (DL_FUNC) (void*) &doWs, 1 },
 -		{ "rk.do.error", (DL_FUNC) (void*) &doError, 1 },
 +		{ "rk.simple", (DL_FUNC) &doSimpleBackendCall, 1},
- 		{ "rk.do.command", (DL_FUNC) &doSubstackCall, 1 },
- 		{ "rk.do.generic.request", (DL_FUNC) &doPlainGenericRequest, 2 },
- 		{ "rk.get.structure", (DL_FUNC) &doGetStructure, 4 },
- 		{ "rk.get.structure.global", (DL_FUNC) &doGetGlobalEnvStructure, 3 },
- 		{ "rk.copy.no.eval", (DL_FUNC) &doCopyNoEval, 3 },
- 		{ "rk.edit.files", (DL_FUNC) &doEditFiles, 4 },
- 		{ "rk.show.files", (DL_FUNC) &doShowFiles, 5 },
- 		{ "rk.dialog", (DL_FUNC) &doDialog, 6 },
- 		{ "rk.update.locale", (DL_FUNC) &doUpdateLocale, 0 },
- 		{ "rk.graphics.device", (DL_FUNC) &RKStartGraphicsDevice, 7},
- 		{ "rk.graphics.device.resize", (DL_FUNC) &RKD_AdjustSize, 1},
+ 		{ "rk.do.command", (DL_FUNC) (void*) &doSubstackCall, 1 },
+ 		{ "rk.do.generic.request", (DL_FUNC) (void*) &doPlainGenericRequest, 2 },
+ 		{ "rk.get.structure", (DL_FUNC) (void*) &doGetStructure, 4 },
+ 		{ "rk.get.structure.global", (DL_FUNC) (void*) &doGetGlobalEnvStructure, 3 },
+ 		{ "rk.copy.no.eval", (DL_FUNC) (void*) &doCopyNoEval, 4 },
+ 		{ "rk.edit.files", (DL_FUNC) (void*) &doEditFiles, 4 },
+ 		{ "rk.show.files", (DL_FUNC) (void*) &doShowFiles, 5 },
+ 		{ "rk.dialog", (DL_FUNC) (void*) &doDialog, 7 },
+ 		{ "rk.update.locale", (DL_FUNC) (void*) &doUpdateLocale, 0 },
 -		{ "rk.locale.name", (DL_FUNC) (void*) &doLocaleName, 0 },
+ 		{ "rk.capture.output", (DL_FUNC) (void*) &doCaptureOutput, 5 },
+ 		{ "rk.graphics.device", (DL_FUNC) (void*) &RKStartGraphicsDevice, 7},
+ 		{ "rk.graphics.device.resize", (DL_FUNC) (void*) &RKD_AdjustSize, 1},
  		{ 0, 0, 0 }
  	};
  	R_registerRoutines (R_getEmbeddingDllInfo(), NULL, callMethods, NULL, NULL);
@@@ -1545,9 -1607,12 +1631,13 @@@ QVariant RKRBackend::handleHistoricalSu
  	request.params["call"] = list;
  	request.command = current_command;
  	handleRequest (&request);
 +	return request.params.value ("return");
  }
  
+ QString getLibLoc() {
+ 	return RKRBackendProtocolBackend::dataDir () + "/.rkward_packages/" + QString::number (RKRBackend::this_pointer->r_version / 10);
+ }
+ 
  QStringList RKRBackend::handlePlainGenericRequest (const QStringList &parameters, bool synchronous) {
  	RK_TRACE (RBACKEND);
  
diff --cc rkward/rbackend/rkrinterface.cpp
index 032ae1e4,1ba560f8..9618d438
--- a/rkward/rbackend/rkrinterface.cpp
+++ b/rkward/rbackend/rkrinterface.cpp
@@@ -311,8 -317,16 +317,17 @@@ void RInterface::rCommandDone (RComman
  			issueCommand (*it, RCommand::App | RCommand::Sync, QString (), this, SET_RUNTIME_OPTS, chain);
  		}
  		// initialize output file
 -		issueCommand ("rk.set.output.html.file (\"" + RKSettingsModuleGeneral::filesPath () + "/rk_out.html\")\n", RCommand::App | RCommand::Sync, QString (), this, SET_RUNTIME_OPTS, chain);
 +		RKOutputWindowManager::self ()->createOutputDirectory (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.
+ 		issueCommand ("setwd (" + RKRSharedFunctionality::quote (QDir::currentPath ()) + ")\n", RCommand::App | RCommand::Sync, QString (), this, SET_RUNTIME_OPTS, chain);
+ #endif
+ 		// Workaround for https://bugs.kde.org/show_bug.cgi?id=421958
+ 		if (RKSessionVars::compareRVersion("4.0.0") < 1 && RKSessionVars::compareRVersion("4.0.1") > 0) {
+ 			issueCommand ("if(compiler::enableJIT(-1) > 2) compiler::enableJIT(2)\n", RCommand::App | RCommand::Sync, QString (), this, SET_RUNTIME_OPTS, chain);
+ 		}
+ 
  		closeChain (chain);
  	} else if (command->getFlags () == GET_R_VERSION) {
  		RK_ASSERT (command->getDataType () == RData::StringVector);
@@@ -383,8 -399,9 +400,9 @@@ void RInterface::handleRequest (RBacken
  				break;
  			}
  		}
+ 		delete cproxy;
  		command_requests.append (request);
 -		processHistoricalSubstackRequest (request->params["call"].toStringList (), parent);
 +		processHistoricalSubstackRequest (request->params["call"].toStringList (), parent, request);
  	} else if (request->type == RBackendRequest::PlainGenericRequest) {
  		request->params["return"] = QVariant (processPlainGenericRequest (request->params["call"].toStringList ()));
  		RKRBackendProtocolFrontend::setRequestCompleted (request);
@@@ -739,10 -727,14 +725,10 @@@ void RInterface::processHistoricalSubst
  		QStringList object_list = calllist.mid (1);
  		new RKEditObjectAgent (object_list, in_chain);
  	} else if (call == "require") {
 -		if (calllist.count () >= 2) {
 -			QString lib_name = calllist[1];
 -			KMessageBox::information (0, 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 (0, in_chain, QStringList(lib_name));
 -			issueCommand (".rk.set.reply (\"\")", RCommand::App | RCommand::Sync, QString (), 0, 0, in_chain);
 -		} else {
 -			issueCommand (".rk.set.reply (\"Too few arguments in call to require.\")", RCommand::App | RCommand::Sync, QString (), 0, 0, in_chain);
 -		}
 +		RK_ASSERT (calllist.count () == 2);
 +		QString lib_name = calllist.value (1);
 +		KMessageBox::information (0, 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 (0, in_chain, lib_name);
++		RKLoadLibsDialog::showInstallPackagesModal (0, in_chain, QStringList(lib_name));
  	} else if (call == "doPlugin") {
  		if (calllist.count () >= 3) {
  			QString message;
@@@ -758,26 -752,8 +744,27 @@@
  		} 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);
++			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;
- 			error = RKOutputWindowManager::self ()->importOutputDirectory (calllist.value (2), calllist.value (3), mode, calllist.value (5) == QStringLiteral ("TRUE"), in_chain);
++#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);
 +		} else {
 +			RK_ASSERT (false);
 +		}
 +		if (!error.isEmpty ()) {
 +			request->params["return"] = QVariant (QStringList (QStringLiteral ("error")) << error);
 +		}
  	} else {
 -		issueCommand ("stop (\"Unrecognized call '" + call + "'. Ignoring\")", RCommand::App | RCommand::Sync, QString (), 0, 0, in_chain);
 +		request->params["return"] = QVariant (QStringList (QStringLiteral ("error")) << i18n ("Unrecognized call '%1'", call));
  	}
  	
  	closeChain (in_chain);
diff --cc rkward/rbackend/rpackages/rkward/NAMESPACE
index cc04a744,cd5cd845..c0f89d43
--- a/rkward/rbackend/rpackages/rkward/NAMESPACE
+++ b/rkward/rbackend/rpackages/rkward/NAMESPACE
@@@ -13,9 -14,11 +13,9 @@@ export(.rk.do.error
  export(.rk.find.package.pluginmaps)
  export(.rk.fix.assignments)
  export(.rk.fix.assignments.graphics)
 -export(.rk.get.environment.children)
  export(.rk.get.installed.packages)
  export(.rk.get.meta)
- export(.rk.get.package.intallation.state)
+ export(.rk.get.package.installation.state)
  export(.rk.get.search.results)
  export(.rk.get.slots)
  export(.rk.get.structure)
@@@ -48,10 -52,13 +47,14 @@@ export(q
  export(quartz)
  export(quit)
  export(require)
+ export(rk.adjust.system.path)
+ export(rk.askYesNo)
  export(rk.assign.preview.data)
  export(rk.call.plugin)
+ export(rk.capture.output)
+ export(rk.check.for.pandoc)
  export(rk.clear.plot.history)
 +export(rk.create.output.dir)
  export(rk.demo)
  export(rk.describe.alternative)
  export(rk.discard.preview.data)
@@@ -59,7 -66,7 +62,8 @@@ export(rk.duplicate.device
  export(rk.edit)
  export(rk.edit.files)
  export(rk.embed.device)
 +export(rk.export.output.dir)
+ export(rk.end.capture.output)
  export(rk.first.plot)
  export(rk.flush.output)
  export(rk.force.append.plot)
@@@ -74,7 -81,7 +78,8 @@@ export(rk.goto.plot
  export(rk.graph.off)
  export(rk.graph.on)
  export(rk.header)
 +export(rk.import.output.dir)
+ export(rk.home)
  export(rk.last.plot)
  export(rk.list)
  export(rk.list.labels)
diff --cc rkward/rbackend/rpackages/rkward/R/base_overrides.R
index b333f73b,1a9878ba..5918fa93
--- a/rkward/rbackend/rpackages/rkward/R/base_overrides.R
+++ b/rkward/rbackend/rpackages/rkward/R/base_overrides.R
@@@ -19,8 -21,7 +21,8 @@@
  	if (!character.only) {
  		package <- as.character(substitute(package))
  	}
- 	if (!base::require(as.character(package), quietly = quietly, character.only = TRUE, ...)) {
+ 	if (!suppressWarnings(base::require(as.character(package), quietly = quietly, character.only = TRUE, ...))) {
 +		if (missing (package)) stop ("No package name given")
  		.rk.do.call("require", as.character(package))
  		invisible(base::require(as.character(package), quietly = TRUE, character.only = TRUE, ...))
  	} else {
diff --cc rkward/rbackend/rpackages/rkward/R/internal.R
index 2e388d29,1601bc13..24d85d99
mode 100644,100755..100755
--- a/rkward/rbackend/rpackages/rkward/R/internal.R
+++ b/rkward/rbackend/rpackages/rkward/R/internal.R
@@@ -223,10 -222,10 +223,9 @@@
  #	.Internal (.addCondHands (c ("message", "warning", "error"), list (function (m) { .Call ("rk.do.condition", c ("m", conditionMessage (m))) }, function (w) { .Call ("rk.do.condition", c ("w", conditionMessage (w))) }, function (e) { .Call ("rk.do.condition", c ("e", conditionMessage (e))) }), globalenv (), NULL, TRUE))
  #}
  
- # these functions can be used to track assignments to R objects. The main interfaces are .rk.watch.symbol (k) and .rk.unwatch.symbol (k). This works by copying the symbol to a backup environment, removing it, and replacing it by an active binding to the backup location
- #' @export
+ # these functions can be used to track assignments to R objects. The main interfaces are .rk.watch.symbol (k) and .rk.unwatch.symbol (k). This works by copying the symbol to a local environment, removing it, and replacing it by an active binding to the backup location
  ".rk.watched.symbols" <- new.env ()
  
 -#' @export
  ".rk.make.watch.f" <- function (k) {
  	# we need to make sure, the functions we use are *not* looked up as symbols in .GlobalEnv.
  	# else, for instance, if the user names a symbol "missing", and we try to resolve it in the
diff --cc rkward/rbackend/rpackages/rkward/R/rk.filename-functions.R
index 0da7a9c8,9051898c..fe7d5e8a
--- a/rkward/rbackend/rpackages/rkward/R/rk.filename-functions.R
+++ b/rkward/rbackend/rpackages/rkward/R/rk.filename-functions.R
@@@ -26,16 -19,18 +26,22 @@@
  #' is note affected by running \code{load} in R, only by loading R workspaces
  #' via the RKWard GUI.
  #' 
+ #' \code{rk.home} returns the filename of the specified component similar to
+ #' \link{R.home}.
+ #'
  #' @aliases rk.get.tempfile.name rk.get.workspace.url rk.get.output.html.file
- #'   rk.set.output.html.file rk.tempdir
 -#'   rk.set.output.html.file rk.home
++#'   rk.set.output.html.file rk.tempdir rk.home
  #' @param prefix a string, used as a filename prefix when saving images to the
 -#'   output file
 +#'   output file. This is usually just a plain file name, but can also be a relative or absolute
 +#'   path. Relative paths are resolved with the default output directory as base, absolute paths
 +#'   are kept as is.
  #' @param extension a string, used as a filename extension when saving images
  #'   to the output file
 +#' @param directory a string, The base directory for the file. If left empty, this will default to the
 +#'   write directory of the current output file (usually "~.rkward)
+ #' @param component a string specifying the desired path. "home" (the default value) means to
+ #'        return the generic data dir used by RKWard. "lib" means to return the directory where
+ #'        the rkward R library is installed.
  #' @param x a string, giving the filename of the of the output file
  #' @param additional.header.contents NULL or an additional string to add to the HTML header section.
  #'        This could be scripts or additional CSS definitions, for example. Note that
diff --cc rkward/rbackend/rpackages/rkward/man/rk.get.tempfile.name.Rd
index 9b8a615b,79a43a01..78b7e0a3
--- a/rkward/rbackend/rpackages/rkward/man/rk.get.tempfile.name.Rd
+++ b/rkward/rbackend/rpackages/rkward/man/rk.get.tempfile.name.Rd
@@@ -1,37 -1,36 +1,55 @@@
  % 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.get.output.html.file}
 +\alias{rk.import.output.dir}
  \alias{rk.set.output.html.file}
 +\alias{rk.tempdir}
+ \alias{rk.home}
 -\alias{rk.flush.output}
  \title{RKWard file names}
  \usage{
 -rk.get.tempfile.name(prefix = "image", extension = ".jpg")
 +rk.get.tempfile.name(prefix = "image", extension = ".jpg",
 +  directory = dirname(rk.get.output.html.file()))
  
  rk.get.workspace.url()
  
+ rk.home(component = "home")
+ 
  rk.get.output.html.file()
  
- rk.set.output.html.file(x,
+ rk.set.output.html.file(
+   x,
    additional.header.contents = getOption("rk.html.header.additions"),
++<<<<<<< HEAD
 +  style = c("regular", "preview"), css = getOption("rk.output.css.file"))
 +
 +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()
++=======
+   style = c("regular", "preview"),
+   css = getOption("rk.output.css.file"),
+   silent = FALSE
+ )
+ 
+ rk.flush.output(
+   x = rk.get.output.html.file(),
+   flush.images = TRUE,
+   ask = TRUE,
+   ...
+ )
++>>>>>>> master
  }
  \arguments{
  \item{prefix}{a string, used as a filename prefix when saving images to the
@@@ -42,8 -39,9 +60,14 @@@ are kept as is.
  \item{extension}{a string, used as a filename extension when saving images
  to the output file}
  
++<<<<<<< HEAD
 +\item{directory}{a string, The base directory for the file. If left empty, this will default to the
 +write directory of the current output file (usually "~.rkward)}
++=======
+ \item{component}{a string specifying the desired path. "home" (the default value) means to
+ return the generic data dir used by RKWard. "lib" means to return the directory where
+ the rkward R library is installed.}
++>>>>>>> master
  
  \item{x}{a string, giving the filename of the of the output file}
  
diff --cc rkward/rkward.kdev4
index e9cfb7f4,00000000..a2b625a0
mode 100644,000000..100755
--- a/rkward/rkward.kdev4
+++ b/rkward/rkward.kdev4
@@@ -1,3 -1,0 +1,4 @@@
 +[Project]
++CreatedFrom=CMakeLists.txt
 +Manager=KDevCMakeManager
 +Name=rkward
diff --cc rkward/rkward.sourcedir
index 00000000,00000000..7b738261
new file mode 100644
--- /dev/null
+++ b/rkward/rkward.sourcedir
@@@ -1,0 -1,0 +1,1 @@@
++/home/thomas/develop/rkward/rkward
diff --cc rkward/windows/katepluginintegration.cpp
index 00000000,66dc2e8c..e6a4179c
mode 000000,100644..100644
--- a/rkward/windows/katepluginintegration.cpp
+++ b/rkward/windows/katepluginintegration.cpp
@@@ -1,0 -1,585 +1,585 @@@
+ /***************************************************************************
+                           katepluginintegration  -  description
+                              -------------------
+     begin                : Mon Jun 12 2017
+     copyright            : (C) 2017-2020 by Thomas Friedrichsmeier
+     email                : thomas.friedrichsmeier at kdemail.net
+  ***************************************************************************/
+ 
+ /***************************************************************************
+  *                                                                         *
+  *   This program is free software; you can redistribute it and/or modify  *
+  *   it under the terms of the GNU General Public License as published by  *
+  *   the Free Software Foundation; either version 2 of the License, or     *
+  *   (at your option) any later version.                                   *
+  *                                                                         *
+  ***************************************************************************/
+ 
+ #include "katepluginintegration.h"
+ 
+ #include <QWidget>
+ #include <QFileInfo>
+ #include <QVBoxLayout>
+ #include <QChildEvent>
+ #include <QComboBox>
+ 
+ #include <KPluginFactory>
+ #include <KPluginLoader>
+ #include <KPluginMetaData>
+ #include <KTextEditor/Editor>
+ #include <KTextEditor/Application>
+ #include <KTextEditor/Plugin>
+ #include <KTextEditor/SessionConfigInterface>
+ #include <KSharedConfig>
+ #include <KConfigGroup>
+ #include <KXMLGUIFactory>
+ #include <KLocalizedString>
+ 
+ #include "../rkward.h"
+ #include "rkworkplace.h"
+ #include "rkworkplaceview.h"
+ #include "rkcommandeditorwindow.h"
+ #include "../misc/rkdummypart.h"
+ #include "../misc/rkcommonfunctions.h"
+ #include "../settings/rksettingsmodulecommandeditor.h"
+ 
+ #include "../debug.h"
+ 
+ ///  BEGIN  KTextEditor::Application interface
+ 
+ KatePluginIntegrationApp::KatePluginIntegrationApp(QObject *parent) : QObject (parent) {
+ 	RK_TRACE (APP);
+ 
+ 	dummy_view = 0;
+ 	window = new KatePluginIntegrationWindow(this);
+ 	app = new KTextEditor::Application(this);
+ 	KTextEditor::Editor::instance()->setApplication(app);
+ 
+ 	// enumerate all available kate plugins
+ 	QVector<KPluginMetaData> plugins = KPluginLoader::findPlugins(QStringLiteral ("ktexteditor"), [](const KPluginMetaData &md) { return md.serviceTypes().contains(QLatin1String("KTextEditor/Plugin")); });
+     for (int i = plugins.size() -1; i >= 0; --i) {
+ 		PluginInfo info;
+ 		info.plugin = 0;
+ 		info.data = plugins[i];
+ 		// Note: creates a lookup-table *and* eliminates potential dupes later in the search path
+ 		known_plugins.insert(idForPlugin(info.data), info);
+ 	}
+ 	// NOTE: Destructor is too late for this, esp. As plugin destructors will try to unregister from the guiFactory(), and such.
+ 	connect(RKWardMainWindow::getMain(), &RKWardMainWindow::aboutToQuitRKWard, this, &KatePluginIntegrationApp::saveConfigAndUnload);
+ }
+ 
+ KatePluginIntegrationApp::~KatePluginIntegrationApp() {
+ 	RK_TRACE (APP);
+ }
+ 
+ KTextEditor::View *KatePluginIntegrationApp::dummyView() {
+ 	if (!dummy_view) {
+ 		RK_TRACE (APP);
+ 		KTextEditor::Document *doc = KTextEditor::Editor::instance()->createDocument (this);
+ 		dummy_view = doc->createView(0);
+ 		dummy_view->hide();
+ 		// Make sure it does not accumulate cruft.
+ 		connect(doc, &KTextEditor::Document::textChanged, doc, &KTextEditor::Document::clear);
+ 	}
+ 	return dummy_view;
+ }
+ 
+ QString KatePluginIntegrationApp::idForPlugin(const KTextEditor::Plugin *plugin) const {
+ 	for (auto it = known_plugins.constBegin(); it != known_plugins.constEnd(); ++it) {
+ 		if (it.value().plugin == plugin) return it.key();
+ 	}
+ 	return QString();
+ }
+ 
+ QString KatePluginIntegrationApp::idForPlugin(const KPluginMetaData &plugin) const {
+ 	return QFileInfo(plugin.fileName()).baseName();
+ }
+ 
+ QObject* KatePluginIntegrationApp::loadPlugin (const QString& identifier) {
+ 	RK_TRACE (APP);
+ 
+ 	if (!known_plugins.contains (identifier)) {
+ 		RK_DEBUG (APP, DL_WARNING, "Plugin %s is not known", qPrintable (identifier));
+ 		return 0;
+ 	}
+ 
+ 	KPluginFactory *factory = KPluginLoader(known_plugins[identifier].data.fileName ()).factory ();
+ 	if (factory) {
+ 		KTextEditor::Plugin *plugin = factory->create<KTextEditor::Plugin>(this, QVariantList () << identifier);
+ 		if (plugin) {
+ 			known_plugins[identifier].plugin = plugin;
+ 			emit KTextEditor::Editor::instance()->application()->pluginCreated(identifier, plugin);
+ 			QObject* created = mainWindow()->createPluginView(plugin);
+ 			if (created) {
+ 				emit mainWindow()->main->pluginViewCreated(identifier, created);
+ 				KTextEditor::SessionConfigInterface *interface = qobject_cast<KTextEditor::SessionConfigInterface *>(created);
+ 				if (interface) {
+ 					// NOTE: Some plugins (noteably the Search in files plugin) will misbehave, unless readSessionConfig has been called!
+ 					KConfigGroup group = KSharedConfig::openConfig()->group(QStringLiteral("KatePlugin:%1:").arg(identifier));
+ 					interface->readSessionConfig(group);
+ 				}
+ 			}
+ 			return plugin;
+ 		}
+ 	}
+ 
+ 	return 0;
+ }
+ 
+ void KatePluginIntegrationApp::loadPlugins(const QStringList& plugins) {
+ 	RK_TRACE (APP);
+ 
+ 	bool changes = false;
+ 	foreach (const QString &key, known_plugins.keys()) {
+ 		auto info = known_plugins.value(key);
+ 		if (plugins.contains(key)) {
+ 			if (!info.plugin) {
+ 				loadPlugin(key);
+ 				changes = true;
+ 			}
+ 		} else {
+ 			if (info.plugin) {
+ 				unloadPlugin(key);
+ 				changes = true;
+ 			}
+ 		}
+ 	}
+ 
+ 	if (!changes) return;
+ 	RKWardMainWindow::getMain()->factory()->removeClient(mainWindow());
+ 	RKWardMainWindow::getMain()->factory()->addClient(mainWindow());
+ }
+ 
+ void KatePluginIntegrationApp::unloadPlugin(const QString &identifier) {
+ 	RK_TRACE (APP);
+ 
+ 	if (!known_plugins.contains(identifier)) return;
+ 	PluginInfo &info = known_plugins[identifier];
+ 	if (!info.plugin) return;
+ 
+ 	QObject* view = mainWindow()->pluginView(identifier);
+ 	if (view) {
+ 		KTextEditor::SessionConfigInterface* interface = qobject_cast<KTextEditor::SessionConfigInterface *>(view);
+ 		if (interface) {
+ 			KConfigGroup group = KSharedConfig::openConfig()->group(QStringLiteral("KatePlugin:%1:").arg(identifier));
+ 			interface->writeSessionConfig(group);
+ 		}
+ 		emit mainWindow()->main->pluginViewDeleted(identifier, view);
+ 		delete view;
+ 	}
+ 	emit app->pluginDeleted(identifier, info.plugin);
+ 	delete info.plugin;
+ 	info.plugin = 0;
+ }
+ 
+ void KatePluginIntegrationApp::saveConfigAndUnload() {
+ 	RK_TRACE (APP);
+ 
+ 	for (auto it = known_plugins.constBegin(); it != known_plugins.constEnd(); ++it) {
+ 		unloadPlugin (it.key());
+ 	}
+ 	known_plugins.clear();
+ }
+ 
+ QList<KTextEditor::MainWindow *> KatePluginIntegrationApp::mainWindows() {
+ 	RK_TRACE (APP);
+ 	QList<KTextEditor::MainWindow *> ret;
+ 	ret.append (window->main);
+ 	return ret;
+ }
+ 
+ KTextEditor::MainWindow *KatePluginIntegrationApp::activeMainWindow() {
+ 	RK_TRACE (APP);
+ 	return window->main;
+ }
+ 
+ RKCommandEditorWindow* findWindowForView(KTextEditor::View *view) {
+ 	RK_TRACE (APP);
+ 
+ 	QList<RKMDIWindow*> w = RKWorkplace::mainWorkplace()->getObjectList(RKMDIWindow::CommandEditorWindow);
+ 	for (int i = 0; i < w.size(); ++i) {
+ 		KTextEditor::View *v = static_cast<RKCommandEditorWindow*>(w[i])->getView();
+ 		if (v && (v == view)) {
+ 			return static_cast<RKCommandEditorWindow*>(w[i]);
+ 		}
+ 	}
+ 	return 0;
+ }
+ 
+ RKCommandEditorWindow* findWindowForDocument(KTextEditor::Document *document) {
+ 	RK_TRACE (APP);
+ 
+ 	QList<RKMDIWindow*> w = RKWorkplace::mainWorkplace()->getObjectList(RKMDIWindow::CommandEditorWindow);
+ 	for (int i = 0; i < w.size(); ++i) {
+ 		KTextEditor::View *v = static_cast<RKCommandEditorWindow*>(w[i])->getView();
+ 		if (v && (v->document() == document)) {
+ 			return static_cast<RKCommandEditorWindow*>(w[i]);
+ 		}
+ 	}
+ 	return 0;
+ }
+ 
+ QList<KTextEditor::Document *> KatePluginIntegrationApp::documents() {
+ 	RK_TRACE (APP);
+ 
+ 	QList<RKMDIWindow*> w = RKWorkplace::mainWorkplace()->getObjectList(RKMDIWindow::CommandEditorWindow);
+ 	QList<KTextEditor::Document*> ret;
+ 	for (int i = 0; i < w.size (); ++i) {
+ 		KTextEditor::View *v = static_cast<RKCommandEditorWindow*>(w[i])->getView();
+ 		if (v) ret.append(v->document());
+ 	}
+ 	if (ret.isEmpty()) {
+ 		// See the NOTE in KatePluginIntegrationWindow::activeView()
+ 		ret.append(dummyView()->document());
+ 	}
+ 	return ret;
+ }
+ 
+ KTextEditor::Document *KatePluginIntegrationApp::findUrl(const QUrl &url) {
+ 	RK_TRACE (APP);
+ 
+ 	QUrl _url = url.adjusted(QUrl::NormalizePathSegments);  // Needed?
+ 	QList<RKMDIWindow*> w = RKWorkplace::mainWorkplace()->getObjectList(RKMDIWindow::CommandEditorWindow);
+ 	for (int i = 0; i < w.size (); ++i) {
+ 		if (_url == static_cast<RKCommandEditorWindow*>(w[i])->url().adjusted(QUrl::NormalizePathSegments)) {
+ 			KTextEditor::View *v = static_cast<RKCommandEditorWindow*>(w[i])->getView();
+ 			if (v) return v->document();
+ 		}
+ 	}
+ 	return 0;
+ }
+ 
+ KTextEditor::Document *KatePluginIntegrationApp::openUrl(const QUrl &url, const QString &encoding) {
+ 	RK_TRACE (APP);
+ 
+ 	KTextEditor::View *v = window->openUrl(url, encoding);
+ 	if (v) return v->document();
+ 	return 0;
+ }
+ 
+ bool KatePluginIntegrationApp::closeDocument(KTextEditor::Document *document) {
+ 	RK_TRACE (APP);
+ 
+ 	RKMDIWindow *w = findWindowForDocument(document);
+ 	if (w) return RKWorkplace::mainWorkplace()->closeWindow(w); // NOTE: Closes only a single view of the document
+ 	return false;
+ }
+ 
+ bool KatePluginIntegrationApp::closeDocuments(const QList<KTextEditor::Document *> &documents) {
+ 	RK_TRACE (APP);
+ 
+ 	bool allfound = true;
+ 	QList<RKMDIWindow*> w = RKWorkplace::mainWorkplace()->getObjectList(RKMDIWindow::CommandEditorWindow);
+ 	QList<RKMDIWindow*> toclose;
+ 	for (int i = 0; i < documents.size(); ++i) {
+ 		bool found = false;
+ 		for (int j = 0; j < w.size(); ++j) {
+ 			KTextEditor::View *v = static_cast<RKCommandEditorWindow*>(w[j])->getView();
+ 			if (v && v->document() == documents[i]) {
+ 				toclose.append(w[i]);
+ 				found = true;
+ 				break;
+ 			}
+ 		}
+ 		if (!found) allfound = false;
+ 	}
+ 
+ 	return RKWorkplace::mainWorkplace()->closeWindows(toclose) && allfound;
+ }
+ 
+ KTextEditor::Plugin *KatePluginIntegrationApp::plugin(const QString &name) {
+ 	RK_TRACE (APP);
+ 
+ 	if (known_plugins.contains(name)) {
+ 		return known_plugins[name].plugin;
+ 	}
+ 	return 0;
+ }
+ 
+ ///  END  KTextEditor::Application interface
+ ///  BEGIN  KTextEditor::MainWindow interface
+ 
+ 
+ KatePluginIntegrationWindow::KatePluginIntegrationWindow (KatePluginIntegrationApp *parent) : QObject (parent), KXMLGUIClient () {
+ 	RK_TRACE (APP);
+ 
+ 	// This one is passed to each created plugin
+ 	main = new KTextEditor::MainWindow(this);
+ 	// While this one may be accessed from plugins via KTextEditor::Editor::instance()->application()
+ 	app = parent;
+ 	active_plugin = 0;
+ 
+ 	connect(RKWorkplace::getHistory(), &RKMDIWindowHistory::activeWindowChanged, this, &KatePluginIntegrationWindow::activeWindowChanged);
+ }
+ 
+ class KatePluginToolWindow : public RKMDIWindow {
+ 	Q_OBJECT
+ public:
+ 	KatePluginToolWindow(QWidget *parent) : RKMDIWindow(parent, RKMDIWindow::KatePluginWindow, true) {
+ 		RK_TRACE (APP);
+ 
+ 		QVBoxLayout *layout = new QVBoxLayout(this);
+ 		layout->setContentsMargins(0, 0, 0, 0);
+ 		setPart(new RKDummyPart(this, this));
+ 		initializeActivationSignals();
+ 		setFocusPolicy(Qt::ClickFocus);
+ 	}
+ 	~KatePluginToolWindow() {
+ 		RK_TRACE (APP);
+ 	}
+ 
+ /** This is a bit lame, but the plugin does not add itself to the parent widget's layout by itself. So we need this override
+  *  to do that. Where did the good old KVBox go? */
+ 	void childEvent(QChildEvent *ev) override {
+ 		if ((ev->type() == QEvent::ChildAdded) && qobject_cast<QWidget *>(ev->child())) {
+ 			QWidget *widget = qobject_cast<QWidget *>(ev->child());
+ 			layout()->addWidget(widget);
+ 			setFocusProxy(widget);
+ 		}
+ 		QWidget::childEvent(ev);
+ 	}
+ };
+ 
+ QWidget* KatePluginIntegrationWindow::createToolView (KTextEditor::Plugin *plugin, const QString &identifier, KTextEditor::MainWindow::ToolViewPosition pos, const QIcon &icon, const QString &text) {
+ 	RK_TRACE (APP);
+ 
+ 	RK_DEBUG(APP, DL_DEBUG, "createToolView for %p, %s, position %d, %s", plugin, qPrintable(identifier), pos, qPrintable(text));
+ 	// TODO: Set proper RKMDIWindow:type
+ 	KatePluginToolWindow *window = new KatePluginToolWindow(RKWorkplace::mainWorkplace()->view());
+ 	window->setCaption(text);
+ 	window->setWindowIcon(icon);
+ 	RKToolWindowList::registerToolWindow(window, identifier, (RKToolWindowList::Placement) pos, 0);
+ 	RKWorkplace::mainWorkplace()->placeInToolWindowBar(window, pos);
+ 	plugin_resources[plugin].windows.append(window);
+ 
+ 	return window;
+ }
+ 
+ bool KatePluginIntegrationWindow::showToolView (QWidget *widget) {
+ 	RK_TRACE (APP);
+ 	RKMDIWindow *w = qobject_cast<RKMDIWindow*>(widget);
+ 	if (w) w->activate();
+ 	else {
+ 		RK_DEBUG(APP, DL_ERROR, "Failed to find mdi window for (plugin) tool view %p", widget);
+ 		RK_ASSERT(w);
+ 		widget->show();
+ 	}
+ 	return true;
+ }
+ 
+ KXMLGUIFactory *KatePluginIntegrationWindow::guiFactory () {
+ 	RK_TRACE (APP);
+ 
+ 	// NOTE: We'd rather like to add the plugin to our own RKMDIWindows, rather than
+ 	// allowing it direct access to the guiFactory()
+ 	// See the HACK in createPluginView().
+ 	return factory ();
+ }
+ 
+ QWidget *KatePluginIntegrationWindow::window() {
+ 	RK_TRACE (APP);
+ 
+ 	return RKWorkplace::mainWorkplace()->view()->window();
+ }
+ 
+ QList<KTextEditor::View *> KatePluginIntegrationWindow::views() {
+ 	RK_TRACE (APP);
+ 
+ 	QList<RKMDIWindow*> w = RKWorkplace::mainWorkplace()->getObjectList(RKMDIWindow::CommandEditorWindow);
+ 	QList<KTextEditor::View*> ret;
+ 	for (int i = 0; i < w.size (); ++i) {
+ 		ret.append (static_cast<RKCommandEditorWindow*>(w[i])->getView());
+ 	}
+ 	return ret;
+ }
+ 
+ void KatePluginIntegrationWindow::activeWindowChanged(RKMDIWindow* window) {
+ 	RK_TRACE (APP);
+ 
+ 	if (window->isType(RKMDIWindow::CommandEditorWindow)) {
+ 		emit main->viewChanged(static_cast<RKCommandEditorWindow *>(window)->getView());
+ 	}
+ }
+ 
+ KTextEditor::View *KatePluginIntegrationWindow::activeView() {
+ 	RK_TRACE (APP);
+ 
+ 	RKMDIWindow *w = RKWorkplace::mainWorkplace()->activeWindow(RKMDIWindow::AnyWindowState);
+ 	if (w && w->isType (RKMDIWindow::CommandEditorWindow)) {
+ 		return static_cast<RKCommandEditorWindow*>(w)->getView();
+ 	}
+ 	// NOTE: As far as RKWard is concerned, the active window will most likely be the tool window at this point, while the
+ 	//       intention will be to get an active window that the tool should operate on. So get the last used window from
+ 	//       history. (Another option would be to check which window is on top in the view area, but this will be difficult
+ 	//       for split views.
+ 	RKMDIWindow* candidate =  RKWorkplace::getHistory()->previousDocumentWindow();
+ 	if (candidate && candidate->isType(RKMDIWindow::CommandEditorWindow)) return static_cast<RKCommandEditorWindow*>(candidate)->getView();
+ 	// NOTE: It looks like some plugins assume this cannot return 0. That's a bug in the plugin, but still one that could
+ 	//       be quite prevalent, as in kate, that assumption holds. So, to be safe, we create a dummy window on the fly.
+ 	return app->dummyView();
+ }
+ 
+ KTextEditor::View *KatePluginIntegrationWindow::activateView(KTextEditor::Document *document) {
+ 	RK_TRACE (APP);
+ 
+ 	RKCommandEditorWindow* w = findWindowForDocument(document);
+ 	if (w) {
+ 		w->activate();
+ 		return w->getView();
+ 	}
+ 	if (app->dummy_view && document == app->dummy_view->document()) return app->dummy_view;
+ 	return 0;
+ }
+ 
+ KTextEditor::View *KatePluginIntegrationWindow::openUrl(const QUrl &url, const QString &encoding) {
+ 	RK_TRACE (APP);
+ 	RKMDIWindow *w = RKWorkplace::mainWorkplace()->openScriptEditor(url, encoding);
+ 	if (w) return static_cast<RKCommandEditorWindow*>(w)->getView();
+ 
+ 	RK_ASSERT(w);  // should not happen
+ 	return 0;
+ }
+ 
+ QObject *KatePluginIntegrationWindow::pluginView(const QString &name) {
+ 	RK_TRACE (APP);
+ 
+ 	return plugin_resources.value(app->plugin(name)).view;
+ }
+ 
+ bool KatePluginIntegrationWindow::closeSplitView(KTextEditor::View* view) {
+ 	RK_TRACE (APP);
+ 
+ 	// TODO: This should close the area that this view is in, not necessarily the view itself. However, if the same doc
+ 	//       is also present in the area to merge into, then close this view, keeping the other.
+ 	return closeView(view);
+ }
+ 
+ bool KatePluginIntegrationWindow::closeView(KTextEditor::View* view) {
+ 	RK_TRACE (APP);
+ 
+ 	RKMDIWindow *w = findWindowForView(view);
+ 	if (w) return RKWorkplace::mainWorkplace()->closeWindow(w);
+ 	return false;
+ }
+ 
+ 
+ bool KatePluginIntegrationWindow::hideToolView(QWidget* widget) {
+ 	RK_TRACE (APP);
+ 
+ 	RKMDIWindow *w = qobject_cast<RKMDIWindow*>(widget);
 -	if (w) w->close(false);
++	if (w) w->close(RKMDIWindow::NoAskSaveModified);
+ 	else {
+ 		RK_ASSERT(w);
+ 		widget->hide();
+ 	}
+ 	return true;
+ }
+ 
+ /* These appear to be truly optional, so let's disable them for now.
+ void KatePluginIntegrationWindow::hideViewBar(KTextEditor::View* view) {}
+ void KatePluginIntegrationWindow::showViewBar(KTextEditor::View* view) {}
+ void KatePluginIntegrationWindow::deleteViewBar(KTextEditor::View* view) {}
+ void KatePluginIntegrationWindow::addWidgetToViewBar(KTextEditor::View* view, QWidget* bar) {}
+ QWidget *KatePluginIntegrationWindow::createViewBar(KTextEditor::View *view) {} */
+ 
+ bool KatePluginIntegrationWindow::moveToolView(QWidget* widget, KTextEditor::MainWindow::ToolViewPosition pos) {
+ 	RK_TRACE (APP);
+ 
+ 	RKMDIWindow *w = qobject_cast<RKMDIWindow*>(widget);
+ 	if (w) {
+ 		RKWorkplace::mainWorkplace ()->placeInToolWindowBar (w, pos);
+ 		return true;
+ 	}
+ 	return false;
+ }
+ 
+ void KatePluginIntegrationWindow::splitView(Qt::Orientation orientation) {
+ 	RK_TRACE (APP);
+ 	RKWorkplace::mainWorkplace()->view()->splitView(orientation);
+ }
+ 
+ bool KatePluginIntegrationWindow::viewsInSameSplitView(KTextEditor::View* view1, KTextEditor::View* view2) {
+ 	RK_TRACE (APP);
+ 	// TODO not sure what the semantics of this really are. The two views are in the same view area (not visible, simultaneously), or in two areas split side-by-side?
+ 	// However, this is essentially unused in kate.
+ 	return false;
+ }
+ 
+ void KatePluginIntegrationWindow::fixUpPluginUI(const QString &id, const PluginResources &resources) {
+ 	RK_TRACE (APP);
+ 
+ 	KXMLGUIClient* hacked_parent = this;
+ 	// KF6 TODO: In KF6, plugins will probably be limited to one UI client, in the first place.
+ 	for (int i = 0; i < resources.clients.size(); ++i) {
+ 		KXMLGUIClient* client = resources.clients[i];
+ 		RKMDIWindow* window = resources.windows.value(i);
+ 		if (window) {
+ 			hacked_parent = window->getPart();;
+ 		}
+ 		factory()->removeClient(client);
+ 
+ 		if (i == 0 && id == QStringLiteral("katesearchplugin")) {
+ 			// Try to avoid confusion regarding where the plugin will search:
+ 			// https://mail.kde.org/pipermail/rkward-devel/2020-January/005393.html
+ 			// window->setCaption(i18nc("Tab title", "Search in Scripts"));
+ 			if (!resources.windows.isEmpty()) {
+ 				// I wonder how long this HACK will work...
+ 				QComboBox *box = resources.windows.first()->findChild<QComboBox*>("searchPlaceCombo");
+ 				if (box && (box->count() > 1)) {
+ 					box->setItemText(0, i18nc("where to search", "in Current Script"));
+ 					box->setItemText(1, i18nc("where to search", "in Open Scripts"));
+ 				}
+ 			}
+ 			RKCommonFunctions::removeContainers(client, QStringList() << "search_in_files", true);
+ 			// TODO: Rename "Search more" to something sensible. These actions should still be accessible, globally.
+ 		} else if (i == 0 && id == QStringLiteral("kateprojectplugin")) {
+ 			RKCommonFunctions::moveContainer(client, "Menu", "projects", "view", true, false);
+ 		}
+ 
+ 		RKCommonFunctions::moveContainer(client, "Menu", "tools", "edit", true, true);
+ 		hacked_parent->insertChildClient(client);
+ 	}
+ 
+ /* TODO: Ok, I guess we need even more specialization.
+ kateprojectplugin:
+  - Actions should probably be accessible, globally
+ katesearchplugin:
+  - should go to next / previous match be accessible, globally?
+ katesnippetsplugin:
+  - ok as is, I think
+ */
+ 	// TODO: If child clients were added to the window, itself, we need to tell the main window to rebuild.
+ 	//       Right now, this is handled during startup, only.
+ 
+ }
+ 
+ QObject* KatePluginIntegrationWindow::createPluginView(KTextEditor::Plugin* plugin) {
+ 	RK_TRACE (APP);
+ 
+ 	// HACK: Currently, plugins will add themselves to the main window's UI, without asking. We don't want that, as
+ 	//       our MDI windows are enabled / disabled on activation. To hack around this, we catch the added clients,
+ 	//       and put them, where they belong.
+ 	connect(factory(), &KXMLGUIFactory::clientAdded, this, &KatePluginIntegrationWindow::catchXMLGUIClientsHack);
+ 	active_plugin = plugin;
+ 	PluginResources& resources = plugin_resources.insert(plugin, PluginResources()).value();
+ 	resources.view = plugin->createView(main);
+ 	active_plugin = 0;
+ 	disconnect(factory(), &KXMLGUIFactory::clientAdded, this, &KatePluginIntegrationWindow::catchXMLGUIClientsHack);
+ 	fixUpPluginUI(app->idForPlugin(plugin), resources);
+ 	connect(plugin, &QObject::destroyed, [&]() { plugin_resources.remove(plugin); });
+ 	return resources.view;
+ }
+ 
+ void KatePluginIntegrationWindow::catchXMLGUIClientsHack(KXMLGUIClient* client) {
+ 	RK_TRACE (APP);
+ 	if (active_plugin) {
+ 		RK_ASSERT(plugin_resources.contains(active_plugin));
+ 		plugin_resources[active_plugin].clients.append(client);
+ 	} else {
+ 		RK_DEBUG(APP, DL_INFO, "XML client created by unknown kate plugin");
+ 	}
+ }
+ 
+ ///  END  KTextEditor::MainWindow interface
+ 
+ #include "katepluginintegration.moc"
diff --cc rkward/windows/rkcommandeditorwindow.cpp
index 2d1e203a,a682fba5..950a95b3
--- a/rkward/windows/rkcommandeditorwindow.cpp
+++ b/rkward/windows/rkcommandeditorwindow.cpp
@@@ -248,12 -303,19 +303,20 @@@ RKCommandEditorWindow::~RKCommandEditor
  		m_view->writeSessionConfig (viewconf);
  	}
  
- 	delete hinter;
+ 	discardPreview ();
  	delete m_view;
 +	m_doc->waitSaveComplete ();
  	QList<KTextEditor::View*> views = m_doc->views ();
  	if (views.isEmpty ()) {
+ 		if (visible_to_kateplugins) {
+ 			emit KTextEditor::Editor::instance()->application()->documentWillBeDeleted(m_doc);
+ 			emit KTextEditor::Editor::instance()->application()->aboutToDeleteDocuments(QList<KTextEditor::Document*>() << m_doc);
+ 		}
  		delete m_doc;
+ 		if (visible_to_kateplugins) {
+ 			emit KTextEditor::Editor::instance()->application()->documentDeleted(m_doc);
+ 			emit KTextEditor::Editor::instance()->application()->documentsDeleted(QList<KTextEditor::Document*>() << m_doc);
+ 		}
  		if (!delete_on_close.isEmpty ()) KIO::del (delete_on_close)->start ();
  		unnamed_documents.remove (_id);
  	}
diff --cc rkward/windows/rkdebugconsole.h
index c29f3667,ff43e519..cb4077a1
--- a/rkward/windows/rkdebugconsole.h
+++ b/rkward/windows/rkdebugconsole.h
@@@ -35,7 -35,7 +35,7 @@@ public
  	static RKDebugConsole *instance () { return _instance; };
  
  	// reimplemented to refuse closing while inside the debugger
- 	bool close (CloseWindowMode aks_save) override;
 -	bool close (bool auto_delete) override;
++	bool close (CloseWindowMode ask_save) override;
  public slots:
  	void newDebugState ();
  private slots:
diff --cc rkward/windows/rkhtmlwindow.cpp
index d5039c16,2024b5d5..9ac7e402
--- a/rkward/windows/rkhtmlwindow.cpp
+++ b/rkward/windows/rkhtmlwindow.cpp
@@@ -33,11 -32,8 +32,9 @@@
  #include <qdir.h>
  #include <QHBoxLayout>
  #include <QHostInfo>
- #include <QNetworkRequest>
- #include <QWebFrame>
  #include <QPrintDialog>
  #include <QMenu>
 +#include <QFileDialog>
  #include <QTextCodec>
  #include <QFontDatabase>
  #include <QTemporaryFile>
@@@ -1153,333 -1346,4 +1355,335 @@@ void RKOutputWindowManager::windowDestr
  	}
  }
  
 +/** much like `ls -Rl`. List directory contents including timestamps and sizes, recursively. Used to detect whether an output directory has any changes. */
 +void listDirectoryState (const QString& _dir, QString *list) {
 +	RK_TRACE (APP);
 +
 +	QDir dir (_dir);
 +	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 ()) {
 +			list->append (QStringLiteral ("dir: ") + fi.fileName () + '\n');
 +			listDirectoryState (fi.absolutePath (), list);
 +			list->append (QStringLiteral ("enddir\n"));
 +		} else {
 +			list->append (fi.fileName () + '\n');
 +			list->append (fi.lastModified ().toString ("dd.hh.mm.ss.zzz") + '\n');
 +			list->append (QString::number (fi.size ()) + '\n');
 +		}
 +	}
 +}
 +
 +QString hashDirectoryState (const QString& dir) {
 +	RK_TRACE (APP);
 +	QString list;
 +	listDirectoryState (dir, &list);
 +	return QCryptographicHash::hash (list.toUtf8 (), QCryptographicHash::Md5);
 +}
 +
 +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::exportOutputDirectory (const QString& _dir, RCommandChain* chain) {
 +	RK_TRACE (APP);
 +
 +	const QString dir = QFileInfo (_dir).canonicalFilePath ();
 +	return exportOutputDirectoryAs (_dir, outputs.value (dir).export_dir, true, chain);  // pass raw "dir" parameter b/c of error handling.
 +}
 +
 +QString RKOutputWindowManager::exportOutputDirectoryAs (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 export output"), outputs.value (work_dir).export_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 ()) {
 +			if (!isRKWwardOutputDirectory (dest)) {
 +				return i18n ("The directory %1 exists, but does not appear to be an RKWard output directory. Refusing to overwrite it.", dest);
 +			}
 +
 +			const QString warning = i18n ("Are you sure you want to overwrite the existing directory '%1'? All current contents, <b>including subdirectories</b> 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 ()) {
 +		if (!isRKWwardOutputDirectory (dest)) {
 +			return i18n ("The directory %1 exists, but does not appear to be an RKWard output directory. Refusing to overwrite it.", dest);
 +		}
 +
 +		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.export_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 ();
 +	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.remove (i.key ());
 +				break;
 +			}
 +		}
 +	}
 +
 +	QString index_file = _index_file;
 +	if (index_file.isEmpty ()) {
 +		QStringList html_files = QDir (dir).entryList (QStringList () << "*.html" << "*.htm", QDir::Files | QDir::Readable);
 +		if (html_files.isEmpty ()) {
 +			return i18n ("The directory %1 does not appear to contain any (readable) HTML-files.", dir);
 +		} else if (html_files.contains ("rk_out.html")) {
 +			index_file = "rk_out.html";
 +		} else if (html_files.contains ("index.html")) {
 +			index_file = "index.html";
 +		} else {
 +			index_file = html_files.first ();
 +		}
 +	}
 +
 +	bool was_rkward_ouput = isRKWwardOutputDirectory (dir);
 +	if ((!was_rkward_ouput) && ask_revert) {
 +		if (KMessageBox::warningContinueCancel (RKWardMainWindow::getMain (), i18n ("The directory %1 does not appear to be an existing RKWard output directory. RKWard can try to import it, anyway (and this will usually work for output files created by RKWard < 0.7.0), but it may not be possible to append to the existing output, and for safety concerns RKWard will refuse to save the modified output back to this directory.\nAre you sure you want to continue?", dir)) != KMessageBox::Continue) {
 +			return i18n ("Cancelled");
 +		}
 +	}
 +
 +	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;
 +	if (was_rkward_ouput) 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 x = 0;
 +	while (true) {
 +		if (!ddir.exists (destname)) break;
 +		destname = prefix + QString::number (x++);
 +	}
 +	ddir.mkpath (destname);
 +
 +	QFile marker (destname + QStringLiteral ("/rkward_output_marker.txt"));
 +	marker.open (QIODevice::WriteOnly);
 +	marker.write (i18n ("This file is used to indicate that this directory is an ouptut directory created by RKWard (http://rkward.kde.org). Do not place any other contents in this directory, as the entire directory will be purged if/when overwriting the output.\nRKWard will ask you before purging this directory (unless explicitly told otherwise), but if you remove this file, RKWard will not purge this directory.\n").toLocal8Bit ());
 +	marker.close ();
 +
 +	return ddir.absoluteFilePath (destname);
 +}
 +
 +QString RKOutputWindowManager::dropOutputDirectory (const QString& dir, bool ask, 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);
 +	}
 +
 +	OutputDirectory od = outputs[work_dir];
 +	if (ask) {
 +		if (od.saved_hash != hashDirectoryState (work_dir)) {
 +			if (KMessageBox::warningContinueCancel (RKWardMainWindow::getMain (), i18n ("The output directory %1 has unsaved changes, which will be lost by dropping it. Are you sure you want to proceed?", work_dir)) != KMessageBox::Continue) return i18n ("Cancelled");
 +		}
 +	}
 +
 +	QString error = dropOutputDirectoryInternal (work_dir);
 +	if (!error.isEmpty ()) return error;
 +
 +	if (current_default_path.startsWith (work_dir)) {
 +		if (!outputs.isEmpty ()) {
 +			backendActivateOutputDirectory (outputs.constBegin ().key (), chain);
 +		} else {
 +			createOutputDirectory (chain);
 +		}
 +	}
 +
 +	return QString ();
 +}
 +
 +QString RKOutputWindowManager::dropOutputDirectoryInternal (const QString& dir) {
 +	RK_TRACE (APP);
 +
 +	if (!isRKWwardOutputDirectory (dir)) {
 +		RK_ASSERT (false); // this should not happen unless user messes with the temporary file, but we better play it safe.
 +		return (i18n ("The directory %1 does not appear to be an RKWard output directory. Refusing to remove it.", dir));
 +	}
 +	outputs.remove (dir);
 +	QDir (dir).removeRecursively ();
 +
 +	QStringList paths = windows.keys ();
 +	for (int i = 0; i < paths.size (); ++i) {
 +		if (paths[i].startsWith (dir)) {
 +//			RKWorkplace::closeWindows (windows.values (paths[i]));  // NOTE: Won't work with current compilers
 +			QList<RKHTMLWindow*> wins = windows.values (paths[i]);
 +			for (int j = 0; j < wins.size (); ++j) {
 +				RKWorkplace::mainWorkplace ()->closeWindow (wins[j]);
 +			}
 +		}
 +	}
 +
 +	return QString ();
 +}
 +
 +void RKOutputWindowManager::purgeAllOututputDirectories () {
 +	RK_TRACE (APP);
 +
 +	QStringList output_dirs = outputs.keys ();
 +	for (int i = output_dirs.size () - 1; i >= 0; --i) {
 +		if (i > 0) dropOutputDirectoryInternal (output_dirs[i]);
 +		else dropOutputDirectory (output_dirs[i], false);
 +	}
 +}
 +
 +QStringList RKOutputWindowManager::modifiedOutputDirectories() const {
 +	RK_TRACE (APP);
 +
 +	QStringList ret;
 +	for (QMap<QString, OutputDirectory>::const_iterator it = outputs.constBegin (); it != outputs.constEnd (); ++it) {
 +		if (it.value ().saved_hash != hashDirectoryState (it.key ())) ret.append (it.key ());
 +	}
 +	return ret;
 +}
 +
 +QString RKOutputWindowManager::outputCaption (const QString& dir) const {
 +	RK_TRACE (APP);
 +
 +	// TODO: real implementation!
 +	return (dir);
 +}
 +
 +bool RKOutputWindowManager::isRKWwardOutputDirectory (const QString& dir) {
 +	RK_TRACE (APP);
 +
 +	return (QDir (dir).exists (QStringLiteral ("rkward_output_marker.txt")));
 +}
 +
 +void RKOutputWindowManager::backendActivateOutputDirectory (const QString& dir, RCommandChain* chain) {
 +	RK_TRACE (APP);
 +
 +	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);
 +
 +	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 ();
 +}
++
+ #include "rkhtmlwindow.moc"
diff --cc rkward/windows/rkhtmlwindow.h
index 72e3f970,048880ab..25f5a30c
--- a/rkward/windows/rkhtmlwindow.h
+++ b/rkward/windows/rkhtmlwindow.h
@@@ -36,26 -36,8 +36,9 @@@ class RKHTMLWindowPart
  class QTemporaryFile;
  class RKHTMLWindow;
  class RKFindBar;
 +class RCommandChain;
- 
- class RKWebPage : public KWebPage {
- 	Q_OBJECT
- public:
- 	explicit RKWebPage (RKHTMLWindow* window);
- 	void load (const QUrl& url);
- signals:
- 	void pageInternalNavigation (const QUrl& url);
- protected:
- /** reimplemented to always emit linkClicked() for pages that need special handling (importantly, rkward://-urls). */
- 	bool acceptNavigationRequest (QWebFrame* frame, const QNetworkRequest& request, NavigationType type) override;
- /** reimplemented to schedule new window creation for the next page to load */
- 	QWebPage* createWindow (WebWindowType type) override;
- private:
- 	RKHTMLWindow *window;
- 	bool new_window;
- 	bool direct_load;
- };
+ class RKWebPage;
+ class RKWebView;
  
  /**
  	\brief Show html files.
@@@ -222,42 -205,10 +206,44 @@@ public
  	void registerWindow (RKHTMLWindow *window);
  /** R may produce output while no output window is active. This allows to set the file that should be monitored for such changes (called from within rk.set.html.output.file()). */
  	void setCurrentOutputPath (const QString &path);
 -/** returns a list (possibly empty) of pointers to existing output windows (for the current output path, only). */
 -	QList<RKHTMLWindow*> existingOutputWindows () const;
 -/** Create (and show) a new output window, and @return the pointer */
 -	RKHTMLWindow* newOutputWindow ();
 +/** returns a list (possibly empty) of pointers to existing output windows for the given path (for the current output path, if no path given). */
 +	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 ());
 +	enum ImportMode {
 +		Ask,           /// Ask whether to integrate imported output into workplace
 +		Integrate,     /// Integrate imported output (forget original source, save with workplace)
 +		KeepSeparate   /// Keep output separate (save back to original source)
 +	};
 +/** 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=QString (), bool ask_revert = true, RCommandChain* chain = 0);
 +/** Export the given output directory to the location it was last exported to / imported from. If the output directory has not been exported / imported, yet, prompt the user for a destination.
 +    @param dir output directory to export
 +    @returns error message, if any, an empty string in case of success */
 +	QString exportOutputDirectory (const QString& dir, RCommandChain* chain = 0);
 +/** Export the given output directory. @see exportOutputDirectory ().
 +    @param dir the output directory to export
 +    @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 exportOutputDirectoryAs (const QString& dir, const QString& dest = QString (), bool ask_overwrite = true, RCommandChain* chain = 0);
 +/** Save the given output directory to the given location. Does not ask for overwrite confirmation.
 +    @param dir output directory to save
 +    @returns error message, if any, an empty string in case of success */
- 	QString saveOutputDirectory (const QString& dir, const QString& dest, RCommandChain* chain = 0);
++	QString saveOutputDirectory (const QString& dir, const QString& dest=QString(), RCommandChain* chain = 0) {
++		return exportOutputDirectoryAs (dir, dest, false, chain);
++	}
 +/** 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 error message if any, an empty string in case of success. */
 +	QString dropOutputDirectory (const QString& dir, bool ask=true, RCommandChain* chain = 0);
 +
 +/** Return a list of all current output directories that have been modified. Used for asking for save during shutdown. */
 +	QStringList modifiedOutputDirectories () const;
 +/** Return the name / caption of the given output directory */
 +	QString outputCaption (const QString &dir) const;
 +/** Use with case! 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 ();
diff --cc rkward/windows/rkwindowcatcher.cpp
index aa949bd2,5a801809..f4867d31
--- a/rkward/windows/rkwindowcatcher.cpp
+++ b/rkward/windows/rkwindowcatcher.cpp
@@@ -356,7 -357,8 +357,9 @@@ RKCaughtX11Window::~RKCaughtX11Window (
  	RK_ASSERT (device_windows.contains (device_number));
  	device_windows.remove (device_number);
  
+ 	in_destructor = true;
 -	close (false);
 +	close (NoAskSaveModified);
++
  	if (embedded) RKWindowCatcher::instance ()->unregisterWatcher (embedded->winId ());
  	error_dialog->autoDeleteWhenDone ();
  }
diff --cc rkward/windows/rkworkplace.cpp
index 383506ef,46a66535..c2d804ed
--- a/rkward/windows/rkworkplace.cpp
+++ b/rkward/windows/rkworkplace.cpp
@@@ -611,14 -632,15 +633,15 @@@ void RKWorkplace::flushAllData () 
  	}
  }
  
- void RKWorkplace::closeWindow (RKMDIWindow *window, RKMDIWindow::CloseWindowMode ask_save) {
 -bool RKWorkplace::closeWindow (RKMDIWindow *window) {
++bool RKWorkplace::closeWindow (RKMDIWindow *window, RKMDIWindow::CloseWindowMode ask_save) {
  	RK_TRACE (APP);
  	RK_ASSERT (windows.contains (window));
  
  	bool tool_window = window->isToolWindow ();
- 	window->close (ask_save);		// all the rest should happen in removeWindow ()
- 	
- 	if (tool_window) windowRemoved ();	// for regular windows, this happens in removeWindow(), already
 -	bool closed = window->close (true);		// all the rest should happen in removeWindow ()
++	bool closed = window->close (ask_save);		// all the rest should happen in removeWindow ()
+ 
+ 	if (closed && tool_window) windowRemoved ();	// for regular windows, this happens in removeWindow(), already
+ 	return closed;
  }
  
  void RKWorkplace::closeActiveWindow () {
@@@ -649,16 -671,13 +672,18 @@@ bool RKWorkplace::closeAll (int type, i
  bool RKWorkplace::closeWindows (QList<RKMDIWindow*> windows) {
  	RK_TRACE (APP);
  
+ 	bool allclosed = true;
  	RKWardMainWindow::getMain ()->lockGUIRebuild (true);
 -	for (int i = windows.size () - 1; i >= 0; --i) {
 -		if (!closeWindow (windows[i])) allclosed = false;
++
 +	bool ok = RKSaveModifiedDialog::askSaveModified (this, windows, false);
 +	if (ok) {
 +		for (int i = windows.size () - 1; i >= 0; --i) {
- 			closeWindow (windows[i], RKMDIWindow::NoAskSaveModified);
++			RK_ASSERT(closeWindow (windows[i], RKMDIWindow::NoAskSaveModified));
 +			// TODO: Do not ask for saving _again_
 +		}
  	}
  	RKWardMainWindow::getMain ()->lockGUIRebuild (false);
 -	return allclosed;
 +	return ok;
  }
  
  void RKWorkplace::removeWindow (QObject *object) {
diff --cc rkward/windows/rkworkplace.h
index 4dafae1c,6eeddce4..0eb26afe
--- a/rkward/windows/rkworkplace.h
+++ b/rkward/windows/rkworkplace.h
@@@ -136,11 -155,12 +155,12 @@@ public
  /** Close the active (attached) window. Safe to call even if there is no current active window (no effect in that case) */
  	void closeActiveWindow ();
  /** Close the given window, whether it is attached or detached.
- @param window window to close */
- 	void closeWindow (RKMDIWindow *window, RKMDIWindow::CloseWindowMode ask_save = RKMDIWindow::AutoAskSaveModified);
+ @param window window to close
+ @returns true, if the window was actually closed (not cancelled) */
 -	bool closeWindow (RKMDIWindow *window);
 -/** Close the given windows, whether they are attached or detached. TODO: Be smart about asking what to save.
++	bool closeWindow (RKMDIWindow *window, RKMDIWindow::CloseWindowMode ask_save = RKMDIWindow::AutoAskSaveModified);
 +/** Close the given windows, whether they are attached or detached.
  @param windows list windows to close
- @returns false if cancelled by user (user was prompted for saving, and chose cancel) */
+ @returns true, if _all_ windows were actually closed. */
  	bool closeWindows (QList<RKMDIWindow*> windows);
  /** Closes all windows of the given type(s). Default call (no arguments) closes all windows
  @param type: A bitwise OR of RKWorkplaceObjectType




More information about the rkward-tracker mailing list