[education/rkward/devel/workspace_output] rkward: Start teaching the UI (RKHTMLWindow) about RKOutputDirectory.

Thomas Friedrichsmeier null at kde.org
Mon Feb 21 20:54:43 GMT 2022


Git commit 828565aec2a563454e56e9dfc49136c71f75cd31 by Thomas Friedrichsmeier.
Committed on 21/02/2022 at 20:53.
Pushed by tfry into branch 'devel/workspace_output'.

Start teaching the UI (RKHTMLWindow) about RKOutputDirectory.

M  +9    -9    rkward/dialogs/rksavemodifieddialog.cpp
M  +44   -16   rkward/misc/rkoutputdirectory.cpp
M  +11   -3    rkward/misc/rkoutputdirectory.h
M  +33   -23   rkward/windows/rkhtmlwindow.cpp
M  +11   -6    rkward/windows/rkhtmlwindow.h
M  +2    -2    rkward/windows/rktoplevelwindowgui.cpp
M  +8    -3    rkward/windows/rkworkplace.cpp

https://invent.kde.org/education/rkward/commit/828565aec2a563454e56e9dfc49136c71f75cd31

diff --git a/rkward/dialogs/rksavemodifieddialog.cpp b/rkward/dialogs/rksavemodifieddialog.cpp
index b28b9786..5eefe7dd 100644
--- a/rkward/dialogs/rksavemodifieddialog.cpp
+++ b/rkward/dialogs/rksavemodifieddialog.cpp
@@ -84,19 +84,19 @@ RKSaveModifiedDialog::RKSaveModifiedDialog (QWidget* parent, QList<RKMDIWindow*>
 	tree->header ()->hide ();
 
 	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 *header = makeHeaderItem(i18n("R Workspace (Data and Functions)"), tree);
+		QString url = RKWorkplace::mainWorkplace()->workspaceURL().toDisplayString();
+		if (url.isEmpty()) {
+			url = i18n("[Not saved]");
 		}
-		save_project_check = new QTreeWidgetItem (QStringList (url));
-		header->addChild (save_project_check);
-		save_project_check->setCheckState (0, Qt::Checked);
+		save_project_check = new QTreeWidgetItem(QStringList(url));
+		header->addChild(save_project_check);
+		save_project_check->setCheckState(0, Qt::Checked);
 
 		auto modified_outputs = RKOutputDirectory::modifiedOutputDirectories();
 		if (!modified_outputs.isEmpty()) {
-			QTreeWidgetItem *header = makeHeaderItem (i18n ("Output files"), tree);
-			for (int i = 0; i < modified_outputs.size (); ++i) {
+			QTreeWidgetItem *header = makeHeaderItem(i18n("Output files"), tree);
+			for (int i = 0; i < modified_outputs.size(); ++i) {
 				QTreeWidgetItem *item = new QTreeWidgetItem();
 				item->setText(0, modified_outputs[i]->caption());
 				item->setFirstColumnSpanned(true);
diff --git a/rkward/misc/rkoutputdirectory.cpp b/rkward/misc/rkoutputdirectory.cpp
index bb6364bc..725d4341 100644
--- a/rkward/misc/rkoutputdirectory.cpp
+++ b/rkward/misc/rkoutputdirectory.cpp
@@ -2,7 +2,7 @@
                           rkoutputdirectory  -  description
                              -------------------
     begin                : Mon Oct 12 2020
-    copyright            : (C) 2020 by Thomas Friedrichsmeier
+    copyright            : (C) 2020-2022 by Thomas Friedrichsmeier
     email                : thomas.friedrichsmeier at kdemail.net
  ***************************************************************************/
 
@@ -70,7 +70,7 @@ void RKOutputDirectoryCallResult::setDir(RKOutputDirectory *d) {
 
 QMap<QString, RKOutputDirectory*> RKOutputDirectory::outputs;
 
-RKOutputDirectory::RKOutputDirectory() : initialized(false) {
+RKOutputDirectory::RKOutputDirectory() : initialized(false), known_modified(false) {
 	RK_TRACE(APP);
 }
 
@@ -84,6 +84,16 @@ RKOutputDirectory* RKOutputDirectory::getOutputById(const QString& id) {
 	return outputs.value(id);
 }
 
+RKOutputDirectory* RKOutputDirectory::getOutputByWorkPath(const QString& workpath) {
+	RK_TRACE (APP);
+
+	if (workpath.endsWith("index.html")) {
+		QString wp = workpath;
+		return(outputs.value(wp.chopped(11)));  // index.html, including pathsep
+	}
+	return nullptr;
+}
+
 RKOutputDirectory* RKOutputDirectory::getOutputBySaveUrl(const QString& _dest) {
 	RK_TRACE (APP);
 
@@ -119,8 +129,9 @@ GenericRRequestResult RKOutputDirectory::save(const QString& _dest, RKOutputDire
 	}
 	GenericRRequestResult res = exportAs(dest, overwrite);
 	if (!res.failed()) {
-		updateSavedHash();
 		save_filename = res.ret.toString();  // might by different from dest, notably, if dest was empty
+		known_modified = true;  // dirty trick to ensure that updateSavedHash() will trigger a stateChange()->update caption in views, even if using SaveAs on an unmodified directory
+		updateSavedHash();
 	}
 	return res;
 }
@@ -231,7 +242,7 @@ GenericRRequestResult RKOutputDirectory::revert(OverwriteBehavior discard) {
 	if (save_filename.isEmpty()) {
 		return GenericRRequestResult::makeError(i18n("Output has not previously been saved. Cannot revert."));
 	}
-	if (!isModified()) return GenericRRequestResult(id, i18n("Output had no modifications. Nothing reverted."));
+	if (!isModifiedAccurate()) return GenericRRequestResult(id, i18n("Output had no modifications. Nothing reverted."));
 	if (discard == Ask) {
 		if (KMessageBox::warningContinueCancel(RKWardMainWindow::getMain(), i18n("Reverting will destroy any changes, since the last time you saved (%1). Are you sure you want to proceed?", save_timestamp.toString())) == KMessageBox::Continue) {
 			discard = Force;
@@ -259,7 +270,6 @@ RKOutputDirectory* RKOutputDirectory::createOutputDirectoryInternal() {
 	auto d = new RKOutputDirectory();
 	d->work_dir = QFileInfo(ddir.absoluteFilePath(destname)).canonicalFilePath();
 	d->id = d->work_dir;
-	d->initialized = false;
 	outputs.insert(d->id, d);
 	return d;
 }
@@ -269,11 +279,13 @@ GenericRRequestResult RKOutputDirectory::activate(RCommandChain* chain) {
 
 	QString index_file = work_dir + "/index.html";
 	RKGlobals::rInterface()->issueCommand(QStringLiteral("rk.set.output.html.file(\"") + RKCommonFunctions::escape(index_file) + QStringLiteral("\")\n"), RCommand::App, QString(), 0, 0, chain);
-	// when an output directory is first initialized, we don't want that to count as a "modification". Therefore, update the "saved hash" _after_ initialization
-	RCommand *command = new RCommand(QString(), RCommand::App | RCommand::Sync | RCommand::EmptyCommand);
-	connect(command->notifier(), &RCommandNotifier::commandFinished, this, &RKOutputDirectory::updateSavedHash);
-	RKGlobals::rInterface()->issueCommand(command, chain);
-	initialized = true;
+	if (!initialized) {
+		// when an output directory is first initialized, we don't want that to count as a "modification". Therefore, update the "saved hash" _after_ initialization
+		RCommand *command = new RCommand(QString(), RCommand::App | RCommand::Sync | RCommand::EmptyCommand);
+		connect(command->notifier(), &RCommandNotifier::commandFinished, this, &RKOutputDirectory::updateSavedHash);
+		RKGlobals::rInterface()->issueCommand(command, chain);
+		initialized = true;
+	}
 
 	return GenericRRequestResult(QVariant(index_file));
 }
@@ -290,6 +302,7 @@ GenericRRequestResult RKOutputDirectory::clear(OverwriteBehavior discard) {
 		if (discard != Force) {
 			return GenericRRequestResult::makeError(i18n("Output is not empty. Not clearing it."));
 		}
+		known_modified = true;
 	}
 
 	QDir dir(work_dir);
@@ -307,11 +320,11 @@ bool RKOutputDirectory::isEmpty() const {
 	if (!save_filename.isEmpty()) return false;  // we _could_ have saved an empty output, of course, but no worries about corner cases. In any doubt we return false.
 
 	if (!initialized) return true;
-	if (!isModified()) return true;   // because we have not saved/loaded this file, before, see above
+	if (!isModifiedAccurate()) return true;   // because we have not saved/loaded this file, before, see above
 	return false;
 }
 
-bool RKOutputDirectory::isModified() const {
+bool RKOutputDirectory::isModifiedAccurate() const {
 	RK_TRACE(APP);
 
 	return saved_hash != hashDirectoryState(work_dir);
@@ -320,13 +333,13 @@ bool RKOutputDirectory::isModified() const {
 QString RKOutputDirectory::caption() const {
 	RK_TRACE(APP);
 	if (!save_filename.isEmpty()) return QFileInfo(save_filename).fileName();
-	return i18n("Not previously saved");
+	return i18n("[Not saved]");
 }
 
 GenericRRequestResult RKOutputDirectory::purge(RKOutputDirectory::OverwriteBehavior discard, RCommandChain* chain, bool activate_other) {
 	RK_TRACE(APP);
 
-	if (isModified()) {
+	if (isModifiedAccurate()) {
 		if (discard == Fail) {
 			return GenericRRequestResult::makeError(i18n("Output has been modified. Not closing it."));
 		}
@@ -382,7 +395,7 @@ QList<RKOutputDirectory*> RKOutputDirectory::modifiedOutputDirectories() {
 
 	QList<RKOutputDirectory*> ret;
 	for (auto it = outputs.constBegin(); it != outputs.constEnd(); ++it) {
-		if (it.value()->isModified()) ret.append(it.value());
+		if (it.value()->isModifiedAccurate()) ret.append(it.value());
 	}
 	return ret;
 }
@@ -391,6 +404,7 @@ void RKOutputDirectory::updateSavedHash() {
 	RK_TRACE (APP);
 	saved_hash = hashDirectoryState(work_dir);
 	save_timestamp = QDateTime::currentDateTime();
+	setKnownModified(false);
 }
 
 QList<RKOutputDirectory *> RKOutputDirectory::allOutputs() {
@@ -463,8 +477,10 @@ GenericRRequestResult RKOutputDirectory::view(bool raise, RCommandChain* chain)
 			list[0]->activate();
 		}
 	} else {
+		if (!known_modified) known_modified = isModifiedAccurate();
 		RKWorkplace::mainWorkplace()->openOutputWindow(QUrl::fromLocalFile(workPath()));
 	}
+
 	return GenericRRequestResult(id);
 }
 
@@ -526,7 +542,7 @@ GenericRRequestResult RKOutputDirectory::handleRCall(const QStringList& params,
 		} else if (command == QStringLiteral("isEmpty")) {
 			return GenericRRequestResult(out->isEmpty());
 		} else if (command == QStringLiteral("isModified")) {
-			return GenericRRequestResult(out->isModified());
+			return GenericRRequestResult(out->isModifiedAccurate());
 		} else if (command == QStringLiteral("revert")) {
 			return out->revert(parseOverwrite(params.value(2)));
 		} else if (command == QStringLiteral("save")) {
@@ -548,5 +564,17 @@ GenericRRequestResult RKOutputDirectory::handleRCall(const QStringList& params,
 	return GenericRRequestResult::makeError(i18n("Unhandled output command '%1'", command));
 }
 
+void RKOutputDirectory::setKnownModified(bool modified) {
+	RK_TRACE(APP);
+	if (known_modified != modified) {
+		known_modified = modified;
+		stateChange(isActive(), modified);
+	}
+}
+
+bool RKOutputDirectory::isModifiedFast() const {
+	return known_modified;
+}
+
 
 #include "rkoutputdirectory.moc"
diff --git a/rkward/misc/rkoutputdirectory.h b/rkward/misc/rkoutputdirectory.h
index 1628a60f..7015f62e 100644
--- a/rkward/misc/rkoutputdirectory.h
+++ b/rkward/misc/rkoutputdirectory.h
@@ -2,7 +2,7 @@
                           rkoutputdirectory  -  description
                              -------------------
     begin                : Mon Oct 12 2020
-    copyright            : (C) 2020 by Thomas Friedrichsmeier
+    copyright            : (C) 2020-2022 by Thomas Friedrichsmeier
     email                : thomas.friedrichsmeier at kdemail.net
  ***************************************************************************/
 
@@ -59,7 +59,10 @@ public:
 	QString getId() const { return id; };
 	bool isEmpty() const;
 	bool isActive() const;
-	bool isModified() const;
+	/** This function is guaranteed to be accurate, but relatively slow. At least too slow to be called on every update of caption. */
+	bool isModifiedAccurate() const;
+	/** This function may not always be accurate, but is fast. It is fairly reliable as long as there is an active view, but should not be used when there is not.  */
+	bool isModifiedFast() const;
 	GenericRRequestResult view(bool raise, RCommandChain* chain=0);
 	QString filename() const { return save_filename; };
 	QString workDir() const { return work_dir; }
@@ -67,7 +70,7 @@ public:
 	QString caption() const;
 	static GenericRRequestResult handleRCall(const QStringList& params, RCommandChain *chain);
 	static RKOutputDirectory* getOutputById(const QString& id);
-	static RKOutputDirectory* getOutputByWorkPath(const QString& workpath) { return getOutputById(workpath); };
+	static RKOutputDirectory* getOutputByWorkPath(const QString& workpath);
 	static RKOutputDirectory* getOutputBySaveUrl(const QString& dest);
 	static RKOutputDirectory* getOutputByWindow(const RKMDIWindow* window);
 /** Return a list of all current output directories that have been modified. Used for asking for save during shutdown. */
@@ -81,6 +84,10 @@ public:
 	static RKOutputDirectoryCallResult get(const QString &filename=QString(), bool create=false, RCommandChain *chain=0);
 	static QList<RKOutputDirectory*> allOutputs();
 	static void purgeAllNoAsk();
+
+	void setKnownModified(bool modified);
+signals:
+	void stateChange(bool active, bool modified);
 private:
 	RKOutputDirectory();
 	~RKOutputDirectory();
@@ -94,6 +101,7 @@ private:
 	QString save_filename;
 	QString id;
 	bool initialized;
+	bool known_modified;  // TODO: needed?
 
 	/** map of outputs. */
 	static QMap<QString, RKOutputDirectory*> outputs;
diff --git a/rkward/windows/rkhtmlwindow.cpp b/rkward/windows/rkhtmlwindow.cpp
index 364f34fb..952e7bca 100644
--- a/rkward/windows/rkhtmlwindow.cpp
+++ b/rkward/windows/rkhtmlwindow.cpp
@@ -61,6 +61,7 @@
 #include "../misc/rkprogresscontrol.h"
 #include "../misc/rkmessagecatalog.h"
 #include "../misc/rkfindbar.h"
+#include "../misc/rkoutputdirectory.h"
 #include "../plugin/rkcomponentmap.h"
 #include "../windows/rkworkplace.h"
 #include "../windows/rkworkplaceview.h"
@@ -548,6 +549,8 @@ bool RKHTMLWindow::openURL (const QUrl &url) {
 
 			current_url = url;	// needs to be set before registering
 			RKOutputWindowManager::self ()->registerWindow (this);
+			dir = RKOutputDirectory::getOutputByWorkPath(url.toLocalFile());
+			connect(dir, &RKOutputDirectory::stateChange, this, &RKHTMLWindow::updateState);
 		}
 	}
 
@@ -671,10 +674,30 @@ void RKHTMLWindow::changeURL (const QUrl &url) {
 	}
 }
 
+void RKHTMLWindow::updateState(){
+	updateCaption(current_url);
+}
+
 void RKHTMLWindow::updateCaption (const QUrl &url) {
 	RK_TRACE (APP);
 
-	if (window_mode == HTMLOutputWindow) setCaption (i18n ("Output %1", url.fileName ()));
+	if (window_mode == HTMLOutputWindow) {
+		if (dir) {
+			QString name = QFileInfo(dir->filename()).fileName();
+			if (name.isEmpty()) name = i18n("New Output");
+			QString mods;
+			if (dir->isActive()) mods.append(i18n("[Active]"));
+			// TODO: use icon(s), instead
+			if (dir->isModifiedFast()) mods.append("(*)");
+			if (!mods.isEmpty()) {
+				name.append(' ');
+				name.append(mods);
+			}
+			setCaption(name);
+		} else {
+			setCaption (i18n ("Output %1", url.fileName ()));
+		}
+	}
 	else setCaption (url.fileName ());
 }
 
@@ -1296,24 +1319,10 @@ void RKOutputWindowManager::rewatchOutput () {
 	file_watcher->addFile (current_default_path);
 }
 
-QList<RKHTMLWindow*> RKOutputWindowManager::existingOutputWindows (const QString &path) const {
+QList<RKHTMLWindow*> RKOutputWindowManager::existingOutputWindows(const QString &path) const {
 	RK_TRACE (APP);
 
-	if (!path.isNull ()) return (windows.values (path));
-	return (windows.values (current_default_path));
-}
-
-RKHTMLWindow* RKOutputWindowManager::newOutputWindow (const QString& _path) {
-	RK_TRACE (APP);
-
-	QString path = _path;
-	if (path.isNull ()) path = current_default_path;
-
-	RKHTMLWindow* current_output = new RKHTMLWindow (RKWorkplace::mainWorkplace ()->view (), RKHTMLWindow::HTMLOutputWindow);
-	current_output->openURL (QUrl::fromLocalFile (path));
-	RK_ASSERT (current_output->url ().toLocalFile () == path);
-
-	return current_output;
+	return (windows.values(path));
 }
 
 void RKOutputWindowManager::fileChanged (const QString &path) {
@@ -1329,6 +1338,7 @@ void RKOutputWindowManager::fileChanged (const QString &path) {
 
 	if (w) {
 		if (RKSettingsModuleOutput::autoRaise ()) w->activate ();
+		if (w->outputDirectory()) w->outputDirectory()->setKnownModified(true);
 	} else {
 		RK_ASSERT (path == current_default_path);
 		if (RKSettingsModuleOutput::autoShow ()) RKWorkplace::mainWorkplace ()->openOutputWindow (QUrl::fromUserInput (path, QString (), QUrl::AssumeLocalFile));
@@ -1339,15 +1349,15 @@ void RKOutputWindowManager::windowDestroyed (QObject *window) {
 	RK_TRACE (APP);
 
 	// warning: Do not call any methods on the window. It is half-destroyed, already.
-	RKHTMLWindow *w = static_cast<RKHTMLWindow*> (window);
+	RKHTMLWindow *w = static_cast<RKHTMLWindow*>(window);
 
-	QString path = windows.key (w);
-	windows.remove (path, w);
+	QString path = windows.key(w);
+	windows.remove(path, w);
 
 	// if there are no further windows for this file, stop listening
-	if ((path != current_default_path) && (!windows.contains (path))) {
-		RK_DEBUG (APP, DL_DEBUG, "no longer watching %s for changes", qPrintable (path));
-		file_watcher->removeFile (path);
+	if ((path != current_default_path) && (!windows.contains(path))) {
+		RK_DEBUG(APP, DL_DEBUG, "no longer watching %s for changes", qPrintable(path));
+		file_watcher->removeFile(path);
 	}
 }
 
diff --git a/rkward/windows/rkhtmlwindow.h b/rkward/windows/rkhtmlwindow.h
index 56879d0b..98dc2331 100644
--- a/rkward/windows/rkhtmlwindow.h
+++ b/rkward/windows/rkhtmlwindow.h
@@ -39,6 +39,7 @@ class RKFindBar;
 class RCommandChain;
 class RKWebPage;
 class RKWebView;
+class RKOutputDirectory;
 
 /**
 	\brief Show html files.
@@ -77,6 +78,8 @@ public:
 	QUrl url () const { return current_url; };
 /** Return current url in a restorable way, i.e. for help pages, abstract the session specific part of the path */
 	QUrl restorableUrl ();
+/** Return the RKOutpuDirectory shown in this view (if any) */
+	RKOutputDirectory *outputDirectory() const { return dir; };
 
 	WindowMode mode () { return window_mode; };
 public slots:
@@ -93,6 +96,7 @@ public slots:
 	void zoomIn ();
 	void zoomOut ();
 	void setTextEncoding (QTextCodec* encoding);
+	void updateState();
 private slots:
 	void scrollToBottom ();
 	void mimeTypeDetermined (KIO::Job*, const QString& type);
@@ -133,7 +137,8 @@ friend class RKHTMLWindowPart;
 	void fileDoesNotExistMessage ();
 
 	void saveBrowserState (VisitedLocation *state);
-
+/** the RKOutpuDirectory viewed in this window (if any) */
+	RKOutputDirectory *dir;
 friend class RKWebPage;
 	static RKWebPage *new_window;
 };
@@ -197,7 +202,9 @@ private:
 
 #include <kdirwatch.h>
 
-/** Takes care of showing / refreshing output windows as needed. */
+/** Takes care of showing / refreshing output windows as needed.
+ *
+ *  For historical reasons, not all output windows refer to RKOutpuDirectories, which is why this separate class takes care of the mapping (for now). */
 class RKOutputWindowManager : public QObject {
 Q_OBJECT
 public:
@@ -207,10 +214,8 @@ public:
 /** 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);
 	QString currentOutputPath() const { return current_default_path; };
-/** 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 ());
+/** returns a list (possibly empty) of pointers to existing output windows for the given path. */
+	QList<RKHTMLWindow*> existingOutputWindows (const QString &path) const;
 private:
 	RKOutputWindowManager ();
 	~RKOutputWindowManager ();
diff --git a/rkward/windows/rktoplevelwindowgui.cpp b/rkward/windows/rktoplevelwindowgui.cpp
index 78318144..d8b834aa 100644
--- a/rkward/windows/rktoplevelwindowgui.cpp
+++ b/rkward/windows/rktoplevelwindowgui.cpp
@@ -227,9 +227,9 @@ void RKTopLevelWindowGUI::activateDocumentView () {
 }
 
 void RKTopLevelWindowGUI::slotOutputShow () {
-	RK_TRACE (APP);
+	RK_TRACE(APP);
 
-	RKWorkplace::mainWorkplace ()->openOutputWindow (QUrl ());
+	RKWorkplace::mainWorkplace()->openOutputWindow(QUrl());
 }
 
 void RKTopLevelWindowGUI::nextWindow () {
diff --git a/rkward/windows/rkworkplace.cpp b/rkward/windows/rkworkplace.cpp
index 4eaaa965..886a512c 100644
--- a/rkward/windows/rkworkplace.cpp
+++ b/rkward/windows/rkworkplace.cpp
@@ -510,6 +510,9 @@ RKMDIWindow* RKWorkplace::openHelpWindow (const QUrl &url, bool only_once) {
 RKMDIWindow* RKWorkplace::openOutputWindow(const QUrl &url) {
 	RK_TRACE (APP);
 
+	QString path = url.toLocalFile();
+	if (path.isNull()) path = RKOutputWindowManager::self()->currentOutputPath();
+
 	QList<RKHTMLWindow*> owins = RKOutputWindowManager::self()->existingOutputWindows(url.toLocalFile());
 	for (int i = 0; i < owins.size (); ++i) {
 		if (view()->windowInActivePane(owins[i])) {
@@ -518,9 +521,11 @@ RKMDIWindow* RKWorkplace::openOutputWindow(const QUrl &url) {
 		}
 	}
 
-	RKHTMLWindow* ret = RKOutputWindowManager::self()->newOutputWindow(url.toLocalFile());
-	addWindow(ret);
-	return ret;
+	RKHTMLWindow* win = new RKHTMLWindow(view(), RKHTMLWindow::HTMLOutputWindow);
+	win->openURL(QUrl::fromLocalFile(path));
+	RK_ASSERT(win->url().toLocalFile() == path);
+	addWindow(win);
+	return win;
 }
 
 void RKWorkplace::newX11Window (QWindow* window_to_embed, int device_number) {


More information about the rkward-tracker mailing list