[rkward-cvs] SF.net SVN: rkward: [900] trunk/rkward

tfry at users.sourceforge.net tfry at users.sourceforge.net
Mon Oct 30 12:29:37 UTC 2006


Revision: 900
          http://svn.sourceforge.net/rkward/?rev=900&view=rev
Author:   tfry
Date:     2006-10-30 04:29:26 -0800 (Mon, 30 Oct 2006)

Log Message:
-----------
some small improvements to completion and arg hinting
add argument hinting in script editor
console and editor use common mechanism for argument hinting

Modified Paths:
--------------
    trunk/rkward/ChangeLog
    trunk/rkward/rkward/core/renvironmentobject.h
    trunk/rkward/rkward/core/robjectlist.cpp
    trunk/rkward/rkward/misc/rkcommonfunctions.cpp
    trunk/rkward/rkward/rkconsole.cpp
    trunk/rkward/rkward/rkconsole.h
    trunk/rkward/rkward/windows/rkcommandeditorwindow.cpp
    trunk/rkward/rkward/windows/rkcommandeditorwindow.h

Modified: trunk/rkward/ChangeLog
===================================================================
--- trunk/rkward/ChangeLog	2006-10-22 18:52:08 UTC (rev 899)
+++ trunk/rkward/ChangeLog	2006-10-30 12:29:26 UTC (rev 900)
@@ -5,8 +5,8 @@
 - fix for compilation on 64bit architectures
 - remove misleading "About KDE" dialog, and replace report Bug dialog with correct information
 - don't crash when closing a detached window
+- function argument hinting in the console and script editor
 - as you type completion of R symbol names in the script editor
-- function argument hinting in the console
 - tab completion of R symbol names in the console
 - added R function rk.edit (x) to open object x for editing in rkward
 - fixed: an empty table created before the object list was updated (esp. at startup) would be thought to have been removed in the workspace

Modified: trunk/rkward/rkward/core/renvironmentobject.h
===================================================================
--- trunk/rkward/rkward/core/renvironmentobject.h	2006-10-22 18:52:08 UTC (rev 899)
+++ trunk/rkward/rkward/core/renvironmentobject.h	2006-10-30 12:29:26 UTC (rev 900)
@@ -39,6 +39,7 @@
 	QString makeChildBaseName (const QString &short_child_name);
 /** reimplemented from RContainerObject: If this is an environment var, call RContainerObject::writeMetaData (). Else, do nothing. An environment has no meta data. */
 	void writeMetaData (RCommandChain *chain);
+	QString namespaceName () { return namespace_name; };
 protected:
 	bool updateStructure (RData *new_data);
 /** reimplemented from RContainerObject to raise an assert if this is not the isGlobalEnv (). Otherwise calls "remove (objectname)" instead of objectname <- NULL" */

Modified: trunk/rkward/rkward/core/robjectlist.cpp
===================================================================
--- trunk/rkward/rkward/core/robjectlist.cpp	2006-10-22 18:52:08 UTC (rev 899)
+++ trunk/rkward/rkward/core/robjectlist.cpp	2006-10-30 12:29:26 UTC (rev 900)
@@ -174,10 +174,15 @@
 		QString env = canonified.section ("::", 0, 0);
 		QString remainder = canonified.section ("::", 1);
 
-		RObjectMap::iterator it = childmap.find (env);
-		if (it == childmap.end ()) return 0;
+		RObject *found = 0;
+		for (unsigned int i = 0; i < num_toplevel_environments; ++i) {
+			if (toplevel_environments[i]->namespaceName () == env) {
+				found = toplevel_environments[i];
+				break;
+			}
+		}
+		if (!found) return 0;
 
-		RObject *found = it.data ();
 		return (found->findObject (remainder, true));
 	}
 
@@ -201,10 +206,15 @@
 		QString env = canonified.section ("::", 0, 0);
 		QString remainder = canonified.section ("::", 1);
 
-		RObjectMap::iterator it = childmap.find (env);
-		if (it == childmap.end ()) return;
-
-		RObject *found = it.data ();
+		RObject *found = 0;
+		for (unsigned int i = 0; i < num_toplevel_environments; ++i) {
+			if (toplevel_environments[i]->namespaceName () == env) {
+				found = toplevel_environments[i];
+				break;
+			}
+		}
+		if (!found) return;
+		
 		found->findObjectsMatching (remainder, current_list, true);
 		return;
 	}

Modified: trunk/rkward/rkward/misc/rkcommonfunctions.cpp
===================================================================
--- trunk/rkward/rkward/misc/rkcommonfunctions.cpp	2006-10-22 18:52:08 UTC (rev 899)
+++ trunk/rkward/rkward/misc/rkcommonfunctions.cpp	2006-10-30 12:29:26 UTC (rev 900)
@@ -106,7 +106,7 @@
 	
 		// if both return the same position, we're on a non-word.
 		if (current_word_start == current_word_end) return (QString ());
-	
+
 		return (context_line.mid (current_word_start, current_word_end - current_word_start));
 	}
 

Modified: trunk/rkward/rkward/rkconsole.cpp
===================================================================
--- trunk/rkward/rkward/rkconsole.cpp	2006-10-22 18:52:08 UTC (rev 899)
+++ trunk/rkward/rkward/rkconsole.cpp	2006-10-30 12:29:26 UTC (rev 900)
@@ -22,9 +22,6 @@
 #include <qapplication.h>
 #include <qobjectlist.h>
 #include <qevent.h>
-#include <qregexp.h>
-#include <qvbox.h>
-#include <qlabel.h>
 #include <qtimer.h>
 
 #include <klocale.h>
@@ -120,6 +117,8 @@
 	view->installEventFilter(this);
 
 	doc->setModified (false);
+
+	hinter = new RKFunctionArgHinter (this, view);
 	
 	setCaption (i18n ("R Console"));
 	
@@ -137,19 +136,12 @@
 
 	current_command = 0;
 	tab_key_pressed_before = false;
-
-	arghints_popup = new QVBox (0, 0, WType_Popup);
-	arghints_popup->setFrameStyle (QFrame::Box | QFrame::Plain);
-	arghints_popup->setLineWidth (1);
-	arghints_popup_text = new QLabel (arghints_popup);
-	arghints_popup->hide ();
-	arghints_popup->setFocusProxy (this);
 }
 
 RKConsole::~RKConsole () {
 	RK_TRACE (APP);
 
-	delete arghints_popup;
+	delete hinter;
 	RKSettingsModuleConsole::saveCommandHistory (commands_history);
 }
 
@@ -203,12 +195,11 @@
 	}
 
 	if (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return) {
-		arghints_popup->hide ();
+		hinter->hideArgHint ();
 		submitCommand ();
 		return true;
 	}
 	else if (e->state () == Qt::ShiftButton && e->key () == Qt::Key_Home){
-		arghints_popup->hide ();
 		if(hasSelectedText())
 			pos=selectionInterfaceExt(doc)->selEndCol (); //There is already a selection, we take it into account.
 		selectionInterface(doc)->setSelection(doc->numLines()-1,prefix.length (),doc->numLines()-1, pos);
@@ -216,7 +207,6 @@
 		return TRUE;
 	}
 	else if (e->state () == Qt::ShiftButton && e->key () == Qt::Key_Left){
-		arghints_popup->hide ();
 		if(pos<=prefix.length ()){
 			return TRUE;
 		} else {
@@ -225,17 +215,14 @@
 		}
 	}
 	else if (e->key () == Qt::Key_Up) {
-		arghints_popup->hide ();
 		commandsListUp ();
 		return true;
 	}
 	else if (e->key () == Qt::Key_Down) {
-		arghints_popup->hide ();
 		commandsListDown ();
 		return true;
 	}
 	else if (e->key () == Qt::Key_Left){
-		arghints_popup->hide ();
 		if(pos<=prefix.length ()){
 			return TRUE;
 		} else {
@@ -244,7 +231,6 @@
 		}
 	}
 	else if (e->key () == Qt::Key_Backspace){
-		tryShowFunctionArgHints ();
 		if(pos<=prefix.length ()){
 			return TRUE;
 		} else {
@@ -257,87 +243,31 @@
 		return TRUE;
 	}
 	else if (e->key () == Qt::Key_Home){
-		arghints_popup->hide ();
 		cursorAtTheBeginning ();
 		return TRUE;
 	}
 	else if (e->key() == Qt::Key_Delete) {
-		tryShowFunctionArgHints ();
 		view->keyDelete();
 		return TRUE;
-	} else {
-		QString text = e->text ();
-		if (text == "(") {
-			tryShowFunctionArgHints ();
-		} else if (text == ")") {
-			tryShowFunctionArgHints ();
-		}
 	}
 
 	return FALSE;
 }
 
-void RKConsole::tryShowFunctionArgHints () {
-	// wait for the keypress to become effective first
-	QTimer::singleShot (0, this, SLOT (realTryShowFunctionArgHints ()));
-}
+bool RKConsole::provideContext (unsigned int line_rev, QString *context, int *cursor_position) {
+	RK_TRACE (COMMANDEDITOR);
 
-void RKConsole::realTryShowFunctionArgHints () {
-	RK_TRACE (APP);
+	if (line_rev > 1) return false;
 
-	QString current_context = currentCommand ();
-	int cursor_pos = currentCursorPositionInCommand ();
-	if (command_incomplete) {
-		current_context.prepend (incomplete_command);
-		cursor_pos += incomplete_command.length ();
+	if (line_rev == 0) {
+		*cursor_position = currentCursorPositionInCommand ();
+		*context = currentCommand ();
+	} else {
+		*cursor_position = -1;
+		*context = incomplete_command;
 	}
-	int matching_left_brace_pos;
 
-	// find the corrresponding opening brace
-	int brace_level = 1;
-	int i;
-	for (i = cursor_pos; i >= 0; --i) {
-		if (current_context.at (i) == QChar (')')) {
-			brace_level++;
-		} else if (current_context.at (i) == QChar ('(')) {
-			brace_level--;
-			if (!brace_level) break;
-		}
-	}
-	if (!brace_level) matching_left_brace_pos = i;
-	else {
-		arghints_popup->hide ();
-		return;
-	}
-
-	// now find where the symbol to the left ends
-	int potential_symbol_end = matching_left_brace_pos - 1;
-	while ((potential_symbol_end >= 0) && current_context.at (potential_symbol_end).isSpace ()) {
-		--potential_symbol_end;
-	}
-	if (current_context.at (potential_symbol_end).isSpace ()) {
-		arghints_popup->hide ();
-		return;
-	}
-
-	// now identify the symbol and object (if any)
-	QString effective_symbol = RKCommonFunctions::getCurrentSymbol (current_context, potential_symbol_end);
-	if (effective_symbol.isEmpty ()) {
-		arghints_popup->hide ();
-		return;
-	}
-
-	RObject *object = RObjectList::getObjectList ()->findObject (effective_symbol);
-	if ((!object) || (!object->isType (RObject::Function))) {
-		arghints_popup->hide ();
-		return;
-	}
-
-	// initialize and show popup
-	arghints_popup_text->setText (effective_symbol + " (" + static_cast<RFunctionObject*> (object)->printArgs () + ")");
-	arghints_popup->resize (arghints_popup_text->sizeHint () + QSize (2, 2));
-	arghints_popup->move (mapToGlobal (view->cursorCoordinates () + QPoint (0, arghints_popup->height ())));
-	arghints_popup->show ();
+	return true;
 }
 
 void RKConsole::doTabCompletion () {

Modified: trunk/rkward/rkward/rkconsole.h
===================================================================
--- trunk/rkward/rkward/rkconsole.h	2006-10-22 18:52:08 UTC (rev 899)
+++ trunk/rkward/rkward/rkconsole.h	2006-10-30 12:29:26 UTC (rev 900)
@@ -26,13 +26,11 @@
 #include <qptrlist.h>
 
 #include "rbackend/rcommandreceiver.h"
+#include "windows/rkcommandeditorwindow.h"
 
 class QStringList;
-class QVBox;
-class QLabel;
 class KAction;
 class RCommand;
-class KateCodeCompletion;
 
 /**
 ** 	\brief Provides an R-like console.
@@ -47,7 +45,7 @@
 ** @author Pierre Ecochard
 **/
 
-class RKConsole : public QWidget, public RCommandReceiver {
+class RKConsole : public QWidget, public RCommandReceiver, public RKScriptContextProvider {
 Q_OBJECT
 public:
 /** Submits a batch of commands, line by line.
@@ -64,6 +62,7 @@
 /** interrupt the current incomplete command (if any) */
 	void resetIncompleteCommand ();
 	void doTabCompletion ();
+	bool provideContext (unsigned int line_rev, QString *context, int *cursor_position);
 protected:
 /** Constructor. Protected. Construct an RKConsolePart instead */
 	RKConsole ();
@@ -83,7 +82,7 @@
 	void fetchPopupMenu (QPopupMenu **menu);
 private:
 friend class RKConsolePart;
-bool eventFilter( QObject *o, QEvent *e );
+	bool eventFilter (QObject *o, QEvent *e);
 /** set syntax-highlighting for R */
 	void setRHighlighting ();
 	QString incomplete_command;
@@ -127,23 +126,19 @@
 \param ac the action collection from which to retrieve the KAction*/
 	void unplugAction (QString action, KActionCollection* ac);
 
-	void tryShowFunctionArgHints ();
-
 	bool output_continuation;
 
 	RCommand *current_command;
 	Kate::Document *doc;
 	Kate::View *view;
+	RKFunctionArgHinter *hinter;
 
 	bool tab_key_pressed_before;
-	QVBox *arghints_popup;
-	QLabel *arghints_popup_text;
 public slots:
 /** We intercept paste commands and get them executed through submitBatch.
 @sa submitBatch */
 	void paste ();
 	void copy ();
-	void realTryShowFunctionArgHints ();
 };
 
 /** A part interface to RKConsole. Provides the context-help functionality

Modified: trunk/rkward/rkward/windows/rkcommandeditorwindow.cpp
===================================================================
--- trunk/rkward/rkward/windows/rkcommandeditorwindow.cpp	2006-10-22 18:52:08 UTC (rev 899)
+++ trunk/rkward/rkward/windows/rkcommandeditorwindow.cpp	2006-10-30 12:29:26 UTC (rev 900)
@@ -87,13 +87,19 @@
 	completion_timer = new QTimer (this);
 	connect (completion_timer, SIGNAL (timeout ()), this, SLOT (tryCompletion()));
 
-	if (use_r_highlighting) setRHighlighting ();
+	if (use_r_highlighting) {
+		setRHighlighting ();
+		hinter = new RKFunctionArgHinter (this, m_view);
+	} else {
+		hinter = 0;
+	}
 
 	updateCaption ();	// initialize
 }
 
 RKCommandEditorWindow::~RKCommandEditorWindow () {
 	RK_TRACE (COMMANDEDITOR);
+	delete hinter;
 	delete m_doc;
 }
 
@@ -236,9 +242,11 @@
 	}
 }
 
-void RKCommandEditorWindow::fixCompletion (KTextEditor::CompletionEntry *, QString *) {
+void RKCommandEditorWindow::fixCompletion (KTextEditor::CompletionEntry *entry, QString *string) {
 	RK_TRACE (COMMANDEDITOR);
 
+	*string = entry->text;	// why, oh, why, isn't this always the case?
+
 	uint current_line_num=0; uint cursor_pos=0;
 	m_view->cursorPosition (&current_line_num, &cursor_pos);
 	QString current_line = getLine ();
@@ -251,4 +259,165 @@
 	m_doc->removeText (current_line_num, word_start, current_line_num, word_end);
 }
 
+bool RKCommandEditorWindow::provideContext (unsigned int line_rev, QString *context, int *cursor_position) {
+	RK_TRACE (COMMANDEDITOR);
+
+	uint current_line_num=0; uint cursor_pos=0;
+	m_view->cursorPosition (&current_line_num, &cursor_pos);
+
+	if (line_rev > current_line_num) return false;
+
+	if (line_rev == 0) {
+		*cursor_position = cursor_pos;
+	} else {
+		*cursor_position = -1;
+	}
+	*context = m_doc->textLine (current_line_num - line_rev);
+
+	return true;
+}
+
+//////////////////////// RKFunctionArgHinter //////////////////////////////
+
+#include <qobjectlist.h>
+#include <qvbox.h>
+
+#include "../core/rfunctionobject.h"
+
+RKFunctionArgHinter::RKFunctionArgHinter (RKScriptContextProvider *provider, Kate::View* view) {
+	RK_TRACE (COMMANDEDITOR);
+
+	RKFunctionArgHinter::provider = provider;
+	RKFunctionArgHinter::view = view;
+
+	const QObjectList *children = view->children ();
+	QObjectListIt it (*children);
+	QObject *obj;
+	while ((obj = it.current()) != 0) {
+		++it;
+		obj->installEventFilter (this);
+	}
+
+	arghints_popup = new QVBox (0, 0, WType_Popup);
+	arghints_popup->setFrameStyle (QFrame::Box | QFrame::Plain);
+	arghints_popup->setLineWidth (1);
+	arghints_popup_text = new QLabel (arghints_popup);
+	arghints_popup->hide ();
+	arghints_popup->setFocusProxy (view);
+}
+
+RKFunctionArgHinter::~RKFunctionArgHinter () {
+	RK_TRACE (COMMANDEDITOR);
+}
+
+void RKFunctionArgHinter::tryArgHint () {
+	RK_TRACE (COMMANDEDITOR);
+
+	// do this in the next event cycle to make sure any inserted characters have truely been inserted
+	QTimer::singleShot (0, this, SLOT (tryArgHintNow ()));
+}
+
+void RKFunctionArgHinter::tryArgHintNow () {
+	RK_TRACE (COMMANDEDITOR);
+
+	int line_rev;
+	int cursor_pos;
+	QString current_context;
+	QString current_line;
+
+	// fetch the most immediate context line. More will be fetched later, if appropriate
+	bool have_context = provider->provideContext (line_rev = 0, &current_line, &cursor_pos);
+	RK_ASSERT (have_context);
+	RK_ASSERT (cursor_pos >= 0);
+	current_context = current_line;
+
+	// find the corrresponding opening brace
+	int matching_left_brace_pos;
+	int brace_level = 1;
+	int i = cursor_pos;
+
+	while (true) {
+		if (current_context.at (i) == QChar (')')) {
+			brace_level++;
+		} else if (current_context.at (i) == QChar ('(')) {
+			brace_level--;
+			if (!brace_level) break;
+		}
+
+		--i;
+		if (i < 0) {
+			bool have_context = provider->provideContext (++line_rev, &current_line, &cursor_pos);
+			if (!have_context) break;
+
+			RK_ASSERT (cursor_pos < 0);
+			current_context.prepend (current_line);
+			i = current_line.length () - 1;
+		}
+	}
+
+	if (!brace_level) matching_left_brace_pos = i;
+	else {
+		hideArgHint ();
+		return;
+	}
+
+	// now find where the symbol to the left ends
+	// there cannot be a line-break between the opening brace, and the symbol name (or can there?), so no need to fetch further context
+	int potential_symbol_end = matching_left_brace_pos - 1;
+	while ((potential_symbol_end > 0) && current_context.at (potential_symbol_end).isSpace ()) {
+		--potential_symbol_end;
+	}
+	if (current_context.at (potential_symbol_end).isSpace ()) {
+		hideArgHint ();
+		return;
+	}
+
+	// now identify the symbol and object (if any)
+	QString effective_symbol = RKCommonFunctions::getCurrentSymbol (current_context, potential_symbol_end+1);
+	if (effective_symbol.isEmpty ()) {
+		hideArgHint ();
+		return;
+	}
+
+	RObject *object = RObjectList::getObjectList ()->findObject (effective_symbol);
+	if ((!object) || (!object->isType (RObject::Function))) {
+		hideArgHint ();
+		return;
+	}
+
+	// initialize and show popup
+	arghints_popup_text->setText (effective_symbol + " (" + static_cast<RFunctionObject*> (object)->printArgs () + ")");
+	arghints_popup->resize (arghints_popup_text->sizeHint () + QSize (2, 2));
+	arghints_popup->move (view->mapToGlobal (view->cursorCoordinates () + QPoint (0, arghints_popup->height ())));
+	arghints_popup->show ();
+}
+
+void RKFunctionArgHinter::hideArgHint () {
+	RK_TRACE (COMMANDEDITOR);
+	arghints_popup->hide ();
+}
+
+bool RKFunctionArgHinter::eventFilter (QObject *, QEvent *e) {
+	RK_TRACE (COMMANDEDITOR);
+
+	if (e->type () == QEvent::KeyPress || e->type () == QEvent::AccelOverride) {
+		QKeyEvent *k = static_cast<QKeyEvent *> (e);
+
+		if (k->key() == Qt::Key_Enter || k->key() == Qt::Key_Return || k->key () == Qt::Key_Up || k->key () == Qt::Key_Down || k->key () == Qt::Key_Left || k->key () == Qt::Key_Right || k->key () == Qt::Key_Home || k->key () == Qt::Key_Tab) {
+			hideArgHint ();
+		} else if (k->key () == Qt::Key_Backspace || k->key () == Qt::Key_Delete){
+			tryArgHint ();
+		} else {
+			QString text = k->text ();
+			if (text == "(") {
+				tryArgHint ();
+			} else if (text == ")") {
+				tryArgHint ();
+			}
+		}
+	}
+
+	return FALSE;
+}
+
 #include "rkcommandeditorwindow.moc"

Modified: trunk/rkward/rkward/windows/rkcommandeditorwindow.h
===================================================================
--- trunk/rkward/rkward/windows/rkcommandeditorwindow.h	2006-10-22 18:52:08 UTC (rev 899)
+++ trunk/rkward/rkward/windows/rkcommandeditorwindow.h	2006-10-30 12:29:26 UTC (rev 900)
@@ -26,16 +26,49 @@
 
 #include "../windows/rkmdiwindow.h"
 
+class QVBox;
+class QLabel;
+
+/** classes wishing to use RKFunctionArgHinter should derive from this, and implement provideContext () */
+class RKScriptContextProvider {
+public:
+	RKScriptContextProvider () {};
+	~RKScriptContextProvider () {};
+
+	virtual bool provideContext (unsigned int line_rev, QString *context, int *cursor_position) = 0;
+};
+
+/** function argument hinting for RKCommandEditorWindow and RKConsole */
+class RKFunctionArgHinter : public QObject {
+	Q_OBJECT
+public:
+	RKFunctionArgHinter (RKScriptContextProvider *provider, Kate::View* view);
+	~RKFunctionArgHinter ();
+
+	void tryArgHint ();
+	void hideArgHint ();
+public slots:
+	void tryArgHintNow ();
+protected:
+	bool eventFilter (QObject *o, QEvent *e);
+private:
+	RKScriptContextProvider *provider;
+	Kate::View *view;
+
+	QVBox *arghints_popup;
+	QLabel *arghints_popup_text;
+};
+
 class QTimer;
 
 /**
 	\brief Provides an editor window for R-commands, as well as a text-editor window in general.
 
-While being called RKCommandEditorWindow, this class handles all sort of text-files, both read/write and read-only. It is an MDI child that is added to the main window, based on KatePart.
+While being called RKCommandEditorWindow, this class handles all sorts of text-files, both read/write and read-only. It is an MDI window that is added to the main window, based on KatePart.
 
 @author Pierre Ecochard
 */
-class RKCommandEditorWindow : public RKMDIWindow {
+class RKCommandEditorWindow : public RKMDIWindow, public RKScriptContextProvider {
 // we need the Q_OBJECT thing for some inherits ("RKCommandEditorWindow")-calls in rkward.cpp.
 	Q_OBJECT
 public:
@@ -67,11 +100,16 @@
 
 	QString getRDescription ();
 	KParts::Part *getPart () { return m_doc; };
+
+	bool provideContext (unsigned int line_rev, QString *context, int *cursor_position);
 public slots:
 /** update Tab caption according to the current url. Display the filename-component of the URL, or - if not available - a more elaborate description of the url. Also appends a "[modified]" if approriate */
 	void updateCaption ();
+/** called whenever it might be appropriate to show a code completion box. The box is not shown immediately, but only after a timeout (if at all) */
 	void tryCompletionProxy ();
+/** show a code completion box if appropriate. Use tryCompletionProxy () instead, which will call this function after a timeout */
 	void tryCompletion ();
+/** called by the Kate part, if an entry was selected from the code completion box. Will remove the current symbol, as the kate part is about to re-add it (in completed form) */
 	void fixCompletion (KTextEditor::CompletionEntry *, QString *);
 protected:
 /** reimplemented from KMdiChildView: give the editor window a chance to object to being closed (if unsaved) */
@@ -79,6 +117,7 @@
 private:
 	Kate::Document *m_doc;
 	Kate::View *m_view;
+	RKFunctionArgHinter *hinter;
 
 	QTimer *completion_timer;
 /** set syntax highlighting-mode to R syntax */


This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site.




More information about the rkward-tracker mailing list