[prev in list] [next in list] [prev in thread] [next in thread] 

List:       kde-commits
Subject:    =?utf-8?q?=5Bpublictransport=5D_timetablemate/src=3A_Readd_sourc?=
From:       Friedrich_Karl_Tilman_Pülz <fpuelz () gmx ! de>
Date:       2011-03-03 12:47:54
Message-ID: 20110303124754.939E2A60C9 () git ! kde ! org
[Download RAW message or body]

Git commit fe1c7ba14bf092747a6bb355639ed83f14feeb85 by Friedrich Karl Tilman Pülz.
Committed on 03/03/2011 at 13:45.
Pushed by fkpulz into branch 'master'.

Readd source files for TimetableMate deleted by scripty

- Why did scripty delete them...?

A  +423  -0    timetablemate/src/accessorinfoxmlreader.cpp         [License: LGPL \
(v2+)] A  +271  -0    timetablemate/src/changelogwidget.cpp         [License: LGPL \
(v2+)] A  +725  -0    timetablemate/src/javascriptcompletionmodel.cpp         \
[License: LGPL (v2+)] A  +247  -0    timetablemate/src/javascriptmodel.cpp         \
[License: LGPL (v2+)] A  +828  -0    timetablemate/src/javascriptparser.cpp         \
[License: LGPL (v2+)] A  +67   -0    timetablemate/src/main.cpp         [License: \
LGPL (v2+)] A  +152  -0    timetablemate/src/publictransportpreview.cpp         \
[License: LGPL (v2+)] A  +70   -0    timetablemate/src/scripting.cpp         \
[License: LGPL (v2+)] A  +2434 -0    timetablemate/src/timetablemate.cpp         \
[License: LGPL (v2+)] A  +69   -0    timetablemate/src/timetablematehelper.cpp        \
[License: LGPL (v2+)] A  +654  -0    timetablemate/src/timetablemateview.cpp         \
[License: LGPL (v2+)]

http://commits.kde.org/publictransport/fe1c7ba14bf092747a6bb355639ed83f14feeb85

diff --git a/timetablemate/src/accessorinfoxmlreader.cpp \
b/timetablemate/src/accessorinfoxmlreader.cpp new file mode 100644
index 0000000..b01b237
--- /dev/null
+++ b/timetablemate/src/accessorinfoxmlreader.cpp
@@ -0,0 +1,423 @@
+/*
+*   Copyright 2010 Friedrich Pülz <fpuelz@gmx.de>
+*
+*   This program is free software; you can redistribute it and/or modify
+*   it under the terms of the GNU Library General Public License as
+*   published by the Free Software Foundation; either version 2 or
+*   (at your option) any later version.
+*
+*   This program is distributed in the hope that it will be useful,
+*   but WITHOUT ANY WARRANTY; without even the implied warranty of
+*   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+*   GNU General Public License for more details
+*
+*   You should have received a copy of the GNU Library General Public
+*   License along with this program; if not, write to the
+*   Free Software Foundation, Inc.,
+*   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+
+#include "accessorinfoxmlreader.h"
+
+#include <KLocalizedString>
+#include <KLocale>
+#include <KDebug>
+#include <QFile>
+#include <QFileInfo>
+#include <KGlobal>
+
+bool AccessorInfoXmlWriter::write( QIODevice* device, const TimetableAccessor& \
accessor ) { +    Q_ASSERT( device );
+    
+    bool closeAfterRead; // Only close after reading if it wasn't open before
+    if ( (closeAfterRead = !device->isOpen()) && !device->open(QIODevice::WriteOnly) \
) { +		return false;
+	}
+    setDevice( device );
+    setAutoFormatting( true );
+
+    writeStartDocument();
+    writeStartElement( "accessorInfo" );
+    writeAttribute( "fileVersion", accessor.fileVersion );
+    writeAttribute( "version", accessor.version );
+    writeAttribute( "type", accessor.type == XML ? "XML" : "HTML" );
+
+    bool enUsed = false;
+    for ( QHash<QString, QString>::const_iterator it = accessor.name.constBegin();
+	  it != accessor.name.constEnd(); ++it )
+    {
+		QString lang = it.key() == "en_US" ? "en" : it.key();
+		if ( lang == "en" ) {
+			if ( enUsed ) {
+				continue;
+			}
+			enUsed = true;
+		}
+		
+		writeStartElement( "name" );
+		writeAttribute( "lang", lang );
+		writeCharacters( it.value() );
+		writeEndElement();
+    }
+
+    enUsed = false;
+    for ( QHash<QString, QString>::const_iterator it = \
accessor.description.constBegin(); +	  it != accessor.description.constEnd(); ++it )
+    {
+		QString lang = it.key() == "en_US" ? "en" : it.key();
+		if ( lang == "en" ) {
+			if ( enUsed ) {
+				continue;
+			}
+			enUsed = true;
+		}
+		
+		writeStartElement( "description" );
+		writeAttribute( "lang", lang );
+		writeCharacters( it.value() );
+		writeEndElement();
+    }
+    
+    writeStartElement( "author" );
+    writeTextElement( "fullname", accessor.author );
+    writeTextElement( "short", accessor.shortAuthor );
+    writeTextElement( "email", accessor.email );
+    writeEndElement();
+
+    if ( accessor.useCityValue )
+		writeTextElement( "useSeperateCityValue", accessor.useCityValue ? "true" : "false" \
); +    if ( accessor.onlyUseCitiesInList )
+		writeTextElement( "onlyUseCitiesInList", accessor.onlyUseCitiesInList ? "true" : \
"false" ); +    if ( !accessor.url.isEmpty() )
+		writeTextElement( "url", accessor.url );
+    if ( !accessor.shortUrl.isEmpty() )
+		writeTextElement( "shortUrl", accessor.shortUrl );
+    if ( !accessor.credit.isEmpty() )
+		writeTextElement( "credit", accessor.credit );
+    if ( accessor.defaultVehicleType != "Unknown" )
+		writeTextElement( "defaultVehicleType", accessor.defaultVehicleType );
+    if ( accessor.minFetchWait > 2 )
+		writeTextElement( "minFetchWait", QString::number(accessor.minFetchWait) );
+    if ( !accessor.fallbackCharset.isEmpty() )
+		writeTextElement( "fallbackCharset", accessor.fallbackCharset );
+    if ( !accessor.charsetForUrlEncoding.isEmpty() )
+		writeTextElement( "charsetForUrlEncoding", accessor.charsetForUrlEncoding );
+    if ( !accessor.scriptFile.isEmpty() )
+		writeTextElement( "script", accessor.scriptFile );
+    if ( !accessor.cities.isEmpty() ) {
+		writeStartElement( "cities" );
+		foreach ( const QString &city, accessor.cities ) {
+			writeStartElement( "city" );
+			const QString lowerCity = city.toLower();
+			if ( accessor.cityNameReplacements.contains(lowerCity) ) {
+				writeAttribute( "replaceWith", accessor.cityNameReplacements[lowerCity] );
+			}
+			writeCharacters( city );
+			writeEndElement(); // city
+		}
+		writeEndElement(); // cities
+    }
+    
+    // Write raw urls
+    writeStartElement( "rawUrls" );
+    if ( !accessor.rawDepartureUrl.isEmpty() ) {
+		writeStartElement( "departures" );
+		writeCDATA( accessor.rawDepartureUrl );
+		writeEndElement();
+    }
+    if ( !accessor.rawJourneyUrl.isEmpty() ) {
+		writeStartElement( "journeys" );
+		writeCDATA( accessor.rawJourneyUrl );
+		writeEndElement();
+    }
+    if ( !accessor.rawStopSuggestionsUrl.isEmpty() ) {
+		writeStartElement( "stopSuggestions" );
+		writeCDATA( accessor.rawStopSuggestionsUrl );
+		writeEndElement();
+    }
+    writeEndElement(); // rawUrls
+	
+	// Write changelog
+	if ( !accessor.changelog.isEmpty() ) {
+		writeStartElement( "changelog" );
+		foreach ( const ChangelogEntry &entry, accessor.changelog ) {
+			writeStartElement( "entry" );
+			if ( !entry.author.isEmpty() && entry.author != accessor.shortAuthor ) {
+				writeAttribute( "author", entry.author );
+			}
+			writeAttribute( "since", entry.version );
+			if ( !entry.releasedWith.isEmpty() ) {
+				writeAttribute( "releasedWith", entry.releasedWith );
+			}
+			writeCharacters( entry.description );
+			writeEndElement(); // entry
+		}
+		writeEndElement(); // changelog
+	}
+    
+    writeEndElement(); // accessorInfo
+    writeEndDocument();
+
+    return true;
+}
+
+TimetableAccessor AccessorInfoXmlReader::read( QIODevice* device ) {
+    Q_ASSERT( device );
+    TimetableAccessor ret;
+
+    bool closeAfterRead; // Only close after reading if it wasn't open before
+    if ( (closeAfterRead = !device->isOpen()) && !device->open(QIODevice::ReadOnly) \
) { +		raiseError( "Couldn't open the file read only" );
+		return ret;
+    }
+    setDevice( device );
+    
+    while ( !atEnd() ) {
+		readNext();
+
+		if ( isStartElement() ) {
+			if ( name().compare("accessorInfo", Qt::CaseInsensitive) == 0 ) {
+				QString fileVersion = attributes().value( "fileVersion" ).toString();
+				if ( fileVersion != "1.0" ) {
+					// File won't be read by the data engine, because of a wrong
+					// file version
+					kDebug() << "The file is not a public transport accessor info "
+						"version 1.0 file.";
+				}
+				ret = readAccessorInfo();
+				ret.fileVersion = fileVersion;
+				break;
+			} else {
+				readUnknownElement();
+			}
+		}
+    }
+
+    if ( closeAfterRead ) {
+		device->close();
+	}
+
+    if ( error() ) {
+		ret.type = InvalidAccessor;
+	}
+    return ret;
+}
+
+void AccessorInfoXmlReader::readUnknownElement() {
+    Q_ASSERT( isStartElement() );
+
+    while ( !atEnd() ) {
+		readNext();
+
+		if ( isEndElement() ) {
+			break;
+		}
+
+		if ( isStartElement() ) {
+			readUnknownElement();
+		}
+    }
+}
+
+TimetableAccessor AccessorInfoXmlReader::readAccessorInfo() {
+    TimetableAccessor ret;
+    QString langRead;
+    QHash<QString,QString> l10nNames, l10nDescriptions;
+
+    if ( attributes().hasAttribute("version") ) {
+		ret.version = attributes().value( "version" ).toString();
+	} else {
+		ret.version = "1.0";
+	}
+
+    if ( attributes().hasAttribute("type") ) {
+		ret.type = attributes().value("type").toString()
+				.compare("XML", Qt::CaseInsensitive) == 0 ? XML : HTML;
+		if ( ret.type == InvalidAccessor ) { // TODO
+			kDebug() << "The type" << attributes().value("type").toString()
+				<< "is invalid. Currently there are two values allowed: "
+				"HTML and XML.";
+			return ret;
+		}
+    } else {
+		ret.type = HTML;
+	}
+
+    while ( !atEnd() ) {
+		readNext();
+
+		if ( isEndElement() && name().compare("accessorInfo", Qt::CaseInsensitive) == 0 ) \
{ +			break;
+		}
+
+		if ( isStartElement() ) {
+			if ( name().compare("name", Qt::CaseInsensitive) == 0 ) {
+				QString nameRead = readLocalizedTextElement( &langRead );
+				l10nNames.insert( langRead, nameRead );
+			} else if ( name().compare("description", Qt::CaseInsensitive) == 0 ) {
+				QString descriptionRead = readLocalizedTextElement( &langRead );
+				l10nDescriptions.insert( langRead, descriptionRead );
+			} else if ( name().compare("author", Qt::CaseInsensitive) == 0 ) {
+				readAuthor( &ret.author, &ret.shortAuthor, &ret.email );
+			} else if ( name().compare("cities", Qt::CaseInsensitive) == 0 ) {
+				readCities( &ret.cities, &ret.cityNameReplacements );
+			} else if ( name().compare("useSeperateCityValue", Qt::CaseInsensitive) == 0 ) {
+				ret.useCityValue = readBooleanElement();
+			} else if ( name().compare("onlyUseCitiesInList", Qt::CaseInsensitive) == 0 ) {
+				ret.onlyUseCitiesInList = readBooleanElement();
+			} else if ( name().compare("defaultVehicleType", Qt::CaseInsensitive) == 0 ) {
+				ret.defaultVehicleType = readElementText();
+			} else if ( name().compare("url", Qt::CaseInsensitive) == 0 ) {
+				ret.url = readElementText();
+			} else if ( name().compare("shortUrl", Qt::CaseInsensitive) == 0 ) {
+				ret.shortUrl = readElementText();
+			} else if ( name().compare("minFetchWait", Qt::CaseInsensitive) == 0 ) {
+				ret.minFetchWait = readElementText().toInt();
+			} else if ( name().compare("charsetForUrlEncoding", Qt::CaseInsensitive) == 0 ) {
+				ret.charsetForUrlEncoding = readElementText();
+			} else if ( name().compare("fallbackCharset", Qt::CaseInsensitive) == 0 ) {
+				ret.fallbackCharset = readElementText(); // TODO Implement as attributes in the \
url tags +			} else if ( name().compare("rawUrls", Qt::CaseInsensitive) == 0 ) {
+				readRawUrls( &ret.rawDepartureUrl, &ret.rawStopSuggestionsUrl, \
&ret.rawJourneyUrl ); +			} else if ( name().compare("script", Qt::CaseInsensitive) \
== 0 ) { +				ret.scriptFile = readElementText();
+			} else if ( name().compare("credit", Qt::CaseInsensitive) == 0 ) {
+				ret.credit = readElementText();
+			} else if ( name().compare("changelog", Qt::CaseInsensitive) == 0 ) {
+				ret.changelog = readChangelog();
+			} else {
+				readUnknownElement();
+			}
+		}
+    }
+
+    if ( ret.shortUrl.isEmpty() ) {
+		ret.shortUrl = ret.url;
+	}
+
+    ret.name = l10nNames;
+    ret.description = l10nDescriptions;
+    return ret;
+}
+
+QString AccessorInfoXmlReader::readLocalizedTextElement( QString *lang ) {
+    if ( attributes().hasAttribute("lang") ) {
+		*lang = attributes().value( "lang" ).toString();
+	} else {
+		*lang = "en";
+	}
+
+    return readElementText();
+}
+
+bool AccessorInfoXmlReader::readBooleanElement() {
+    QString content = readElementText().trimmed();
+    if ( content.compare("true", Qt::CaseInsensitive) == 0 || content == "1" ) {
+		return true;
+	} else {
+		return false;
+	}
+}
+
+void AccessorInfoXmlReader::readAuthor( QString *fullname, QString *shortName, \
QString *email ) { +    while ( !atEnd() ) {
+		readNext();
+
+		if ( isEndElement() && name().compare("author", Qt::CaseInsensitive) == 0 )
+			break;
+
+		if ( isStartElement() ) {
+			if ( name().compare("fullName", Qt::CaseInsensitive) == 0 ) {
+				*fullname = readElementText();
+			} else if ( name().compare("short", Qt::CaseInsensitive) == 0 ) {
+				*shortName = readElementText();
+			} else if ( name().compare("email", Qt::CaseInsensitive) == 0 ) {
+				*email = readElementText();
+			} else {
+				readUnknownElement();
+			}
+		}
+    }
+}
+
+void AccessorInfoXmlReader::readCities( QStringList *cities,
+					QHash< QString, QString > *cityNameReplacements ) {
+    while ( !atEnd() ) {
+		readNext();
+
+		if ( isEndElement() && name().compare("cities", Qt::CaseInsensitive) == 0 ) {
+			break;
+		}
+
+		if ( isStartElement() ) {
+			if ( name().compare("city", Qt::CaseInsensitive) == 0 ) {
+				if ( attributes().hasAttribute("replaceWith") ) {
+					QString replacement = attributes().value("replaceWith").toString().toLower();
+					QString city = readElementText();
+					cityNameReplacements->insert( city.toLower(), replacement );
+					cities->append( city );
+				} else {
+					QString city = readElementText();
+					cities->append( city );
+				}
+			} else {
+				readUnknownElement();
+			}
+		}
+    }
+}
+
+void AccessorInfoXmlReader::readRawUrls( QString* rawUrlDepartures,
+					 QString* rawUrlStopSuggestions,
+					 QString* rawUrlJourneys ) {
+    while ( !atEnd() ) {
+		readNext();
+
+		if ( isEndElement() && name().compare("rawUrls", Qt::CaseInsensitive) == 0 ) {
+			break;
+		}
+
+		if ( isStartElement() ) {
+			if ( name().compare("departures", Qt::CaseInsensitive) == 0 ) {
+				*rawUrlDepartures = readElementText();
+			} else if ( name().compare("stopSuggestions", Qt::CaseInsensitive) == 0 ) {
+				*rawUrlStopSuggestions = readElementText();
+			} else if ( name().compare("journeys", Qt::CaseInsensitive) == 0 ) {
+				*rawUrlJourneys = readElementText();
+			} else {
+				readUnknownElement();
+			}
+		}
+    }
+}
+
+QList<ChangelogEntry> AccessorInfoXmlReader::readChangelog()
+{
+	QList<ChangelogEntry> changelog;
+	while ( !atEnd() ) {
+		readNext();
+		if ( isEndElement() && name().compare("changelog", Qt::CaseInsensitive) == 0 ) {
+			break;
+		}
+
+		if ( isStartElement() ) {
+			if ( name().compare("entry", Qt::CaseInsensitive) == 0 ) {
+				ChangelogEntry currentEntry;
+				if ( attributes().hasAttribute(QLatin1String("since")) ) {
+					currentEntry.version = attributes().value( QLatin1String("since") ).toString();
+				}
+				if ( attributes().hasAttribute(QLatin1String("releasedWith")) ) {
+					currentEntry.releasedWith = attributes().value( QLatin1String("releasedWith") \
).toString(); +				}
+				if ( attributes().hasAttribute(QLatin1String("author")) ) {
+					currentEntry.author = attributes().value( QLatin1String("author") ).toString();
+				}
+				currentEntry.description = readElementText();
+				changelog.append( currentEntry );
+			} else {
+				readUnknownElement();
+			}
+		}
+	}
+	return changelog;
+}
diff --git a/timetablemate/src/changelogwidget.cpp \
b/timetablemate/src/changelogwidget.cpp new file mode 100644
index 0000000..326dcd4
--- /dev/null
+++ b/timetablemate/src/changelogwidget.cpp
@@ -0,0 +1,271 @@
+/*
+*   Copyright 2011 Friedrich Pülz <fpuelz@gmx.de>
+*
+*   This program is free software; you can redistribute it and/or modify
+*   it under the terms of the GNU Library General Public License as
+*   published by the Free Software Foundation; either version 2 or
+*   (at your option) any later version.
+*
+*   This program is distributed in the hope that it will be useful,
+*   but WITHOUT ANY WARRANTY; without even the implied warranty of
+*   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+*   GNU General Public License for more details
+*
+*   You should have received a copy of the GNU Library General Public
+*   License along with this program; if not, write to the
+*   Free Software Foundation, Inc.,
+*   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+
+#include "changelogwidget.h"
+#include "accessorinfoxmlreader.h"
+
+#include <QFormLayout>
+#include <QValidator>
+#include <QToolButton>
+#include <KLineEdit>
+#include <KLocalizedString>
+#include <KDebug>
+#include <QMenu>
+#include <KIcon>
+
+ChangelogEntryWidget::ChangelogEntryWidget(QWidget* parent, const ChangelogEntry \
&entry, +	const QString &shortAuthor ) : QWidget(parent)
+{
+	// Create layout
+	QFormLayout *layout = new QFormLayout( this );
+	
+	QWidget *authorVersionWidget = new QWidget( this );
+	QHBoxLayout *authorVersionLayout = new QHBoxLayout( authorVersionWidget );
+	authorVersionLayout->setContentsMargins( 0, 0, 0, 0 );
+	
+	m_author = new KLineEdit( entry.author, authorVersionWidget );
+	m_author->setClickMessage( shortAuthor );
+	m_author->setFixedWidth( 125 );
+	
+	m_version = new KLineEdit( entry.version, authorVersionWidget );
+	m_version->setFixedWidth( 75 );
+	
+	authorVersionLayout->addWidget( m_author );
+	authorVersionLayout->addWidget( m_version );
+	authorVersionLayout->addStretch();
+	
+	m_releasedWith = new KLineEdit( entry.releasedWith, this );
+	m_releasedWith->setFixedWidth( 75 );
+	
+	m_description = new KLineEdit( entry.description, this );
+	m_description->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Fixed );
+	
+	connect( m_author, SIGNAL(textChanged(QString)), this, SIGNAL(changed()) );
+	connect( m_version, SIGNAL(textChanged(QString)), this, SIGNAL(changed()) );
+	connect( m_releasedWith, SIGNAL(textChanged(QString)), this, SIGNAL(changed()) );
+	connect( m_description, SIGNAL(textChanged(QString)), this, SIGNAL(changed()) );
+	
+    // Set a validator for version line edits
+    QRegExpValidator *versionValidator = new QRegExpValidator( \
QRegExp("\\d+(\\.\\d+)*"), this ); +    m_version->setValidator( versionValidator );
+    m_releasedWith->setValidator( versionValidator );
+	
+	layout->addRow( i18nc("@info Label for the author of a changelog entry (short \
author name)",  +						  "Author, Version:"), authorVersionWidget );
+// 	layout->addRow( i18nc("@info Label for the version of a changelog entry", 
+// 						  "Version:"), m_version );
+	m_releasedWith->hide(); // Not used, but read and save it
+// 	layout->addRow( i18nc("@info Label for the publictransport version the changelog \
entry was released with",  +// 						  "Released With:"), m_releasedWith );
+	layout->addRow( i18nc("@info Label for the description of a changelog entry", 
+						  "Description:"), m_description );
+}
+
+ChangelogEntry ChangelogEntryWidget::changelogEntry() const
+{
+	ChangelogEntry entry;
+	entry.author = m_author->text();
+	entry.version = m_version->text();
+	entry.releasedWith = m_releasedWith->text();
+	entry.description = m_description->text();
+	return entry;
+}
+
+QString ChangelogEntryWidget::author() const
+{
+	return m_author->text();
+}
+
+void ChangelogEntryWidget::setAuthor(const QString& author)
+{
+	m_author->setText( author );
+}
+
+QString ChangelogEntryWidget::version() const
+{
+	return m_version->text();
+}
+
+void ChangelogEntryWidget::setVersion(const QString& version)
+{
+	m_version->setText( version );
+}
+
+QString ChangelogEntryWidget::releasedWith() const
+{
+	return m_releasedWith->text();
+}
+
+void ChangelogEntryWidget::setReleasedWith(const QString& releasedWith)
+{
+	m_releasedWith->setText( releasedWith );
+}
+
+QString ChangelogEntryWidget::description() const
+{
+	return m_description->text();
+}
+
+void ChangelogEntryWidget::setDescription(const QString& description)
+{
+	m_description->setText( description );
+}
+
+void ChangelogEntryWidget::setChangelogEntry( const ChangelogEntry& changelogEntry,
+	const QString &shortAuthor )
+{
+	setAuthor( changelogEntry.author );
+	if ( changelogEntry.author.isEmpty() ) {
+		m_author->setClickMessage( shortAuthor );
+	}
+	setVersion( changelogEntry.version );
+	setReleasedWith( changelogEntry.releasedWith );
+	setDescription( changelogEntry.description );
+}
+
+ChangelogWidget::ChangelogWidget( QWidget* parent,
+		AbstractDynamicWidgetContainer::RemoveButtonOptions removeButtonOptions,
+		AbstractDynamicWidgetContainer::AddButtonOptions addButtonOptions,
+		AbstractDynamicWidgetContainer::SeparatorOptions separatorOptions )
+		: AbstractDynamicWidgetContainer( parent, removeButtonOptions, addButtonOptions, 
+										  separatorOptions, AddWidgetsAtTop )
+{
+	QToolButton *btnAdd = addButton();
+	btnAdd->setToolButtonStyle( Qt::ToolButtonTextBesideIcon );
+	btnAdd->setText( i18nc("@action:button", "&Add Changelog Entry") );
+	
+	QMenu *addMenu = new QMenu( this );
+	addMenu->addAction( KIcon("list-add"), i18nc("@action:inmenu", "Add &Empty \
Changelog Entry"),  +						this, SLOT(createAndAddWidget()) );
+	addMenu->addAction( KIcon("list-add"), i18nc("@action:inmenu", "Add &Same Version \
Changelog Entry"),  +						this, SLOT(createAndAddWidgetSameVersion()) );
+	addMenu->addAction( KIcon("list-add"), i18nc("@action:inmenu", "Add &New Minor \
Version Changelog Entry"),  +						this, SLOT(createAndAddWidgetNewMinorVersion()) );
+	addMenu->addAction( KIcon("list-add"), i18nc("@action:inmenu", "Add New &Major \
Version Changelog Entry"),  +						this, SLOT(createAndAddWidgetNewMajorVersion()) );
+	
+	btnAdd->setPopupMode( QToolButton::MenuButtonPopup );
+	btnAdd->setMenu( addMenu );
+}
+
+void ChangelogWidget::createAndAddWidgetSameVersion()
+{
+	if ( widgetCount() == 0 ) {
+		createAndAddWidget();
+		return;
+	}
+	
+	ChangelogEntryWidget *lastEntry = \
dynamicWidgets().last()->contentWidget<ChangelogEntryWidget*>(); \
+	ChangelogEntryWidget *newEntry = qobject_cast< ChangelogEntryWidget* >( \
createNewWidget() ); +	newEntry->setVersion( lastEntry->version() );
+	addWidget( newEntry );
+}
+
+void ChangelogWidget::createAndAddWidgetNewMinorVersion()
+{
+	if ( widgetCount() == 0 ) {
+		createAndAddWidget();
+		return;
+	}
+	
+	ChangelogEntryWidget *lastEntry = \
dynamicWidgets().last()->contentWidget<ChangelogEntryWidget*>(); \
+	ChangelogEntryWidget *newEntry = qobject_cast< ChangelogEntryWidget* >( \
createNewWidget() ); +	QRegExp version( "(\\d+)\\.(\\d+)(\\.\\d+)*" );
+	if ( version.indexIn(lastEntry->version()) != -1 ) {
+		int major = version.cap(1).toInt();
+		int minor = version.cap(2).toInt();
+		QString rest = version.cap(3);
+		newEntry->setVersion( QString("%1.%2%3").arg(major).arg(minor + 1).arg(rest) );
+	} else {
+		newEntry->setVersion( lastEntry->version() );
+	}
+	addWidget( newEntry );
+}
+
+void ChangelogWidget::createAndAddWidgetNewMajorVersion()
+{
+	if ( widgetCount() == 0 ) {
+		createAndAddWidget();
+		return;
+	}
+	
+	ChangelogEntryWidget *lastEntry = \
dynamicWidgets().last()->contentWidget<ChangelogEntryWidget*>(); \
+	ChangelogEntryWidget *newEntry = qobject_cast< ChangelogEntryWidget* >( \
createNewWidget() ); +	QRegExp version( "(\\d+)\\.(\\d+)(\\.\\d+)*" );
+	if ( version.indexIn(lastEntry->version()) != -1 ) {
+		int major = version.cap(1).toInt();
+		int minor = 0; // Start with minor version number 0
+		QString rest = version.cap(3);
+		newEntry->setVersion( QString("%1.%2%3").arg(major + 1).arg(minor).arg(rest) );
+	} else {
+		newEntry->setVersion( lastEntry->version() );
+	}
+	addWidget( newEntry );
+}
+
+QWidget* ChangelogWidget::createNewWidget()
+{
+	return new ChangelogEntryWidget( this );
+}
+
+DynamicWidget* ChangelogWidget::addWidget( QWidget* widget )
+{
+	QString clickMessage;
+	if ( !dynamicWidgets().isEmpty() ) {
+		clickMessage = dynamicWidgets().first()->contentWidget<ChangelogEntryWidget*>()
+				->authorLineEdit()->clickMessage();
+	}
+	DynamicWidget *dynamicWidget = AbstractDynamicWidgetContainer::addWidget( widget );
+	KLineEdit *authorLineEdit = \
qobject_cast<ChangelogEntryWidget*>(widget)->authorLineEdit(); \
+	authorLineEdit->setClickMessage( clickMessage ); +	authorLineEdit->setFocus();
+	return dynamicWidget;
+}
+
+void ChangelogWidget::clear()
+{
+	removeAllWidgets();
+}
+
+void ChangelogWidget::addChangelog( const QList< ChangelogEntry >& changelog, 
+									const QString &shortAuthor )
+{
+	for ( int i = changelog.count() - 1; i >= 0; --i ) {
+		addChangelogEntry( changelog[i], shortAuthor );
+	}
+}
+
+void ChangelogWidget::addChangelogEntry( const ChangelogEntry& changelogEntry,
+	const QString &shortAuthor )
+{
+	ChangelogEntryWidget *widget = qobject_cast<ChangelogEntryWidget*>( \
createNewWidget() ); +	widget->setChangelogEntry( changelogEntry, shortAuthor );
+	connect( widget, SIGNAL(changed()), this, SIGNAL(changed()) );
+	addWidget( widget );
+}
+
+QList< ChangelogEntry > ChangelogWidget::changelog() const
+{
+	QList<ChangelogEntry> ret;
+	QList<ChangelogEntryWidget*> entryWidgets = widgets<ChangelogEntryWidget*>();
+	foreach ( const ChangelogEntryWidget *entryWidget, entryWidgets ) {
+		ret << entryWidget->changelogEntry();
+	}
+	return ret;
+}
diff --git a/timetablemate/src/javascriptcompletionmodel.cpp \
b/timetablemate/src/javascriptcompletionmodel.cpp new file mode 100644
index 0000000..6740ae9
--- /dev/null
+++ b/timetablemate/src/javascriptcompletionmodel.cpp
@@ -0,0 +1,725 @@
+/*
+*   Copyright 2010 Friedrich Pülz <fieti1983@gmx.de>
+*
+*   This program is free software; you can redistribute it and/or modify
+*   it under the terms of the GNU Library General Public License as
+*   published by the Free Software Foundation; either version 2 or
+*   (at your option) any later version.
+*
+*   This program is distributed in the hope that it will be useful,
+*   but WITHOUT ANY WARRANTY; without even the implied warranty of
+*   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+*   GNU General Public License for more details
+*
+*   You should have received a copy of the GNU Library General Public
+*   License along with this program; if not, write to the
+*   Free Software Foundation, Inc.,
+*   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+
+#include "javascriptcompletionmodel.h"
+
+#include <KTextEditor/View>
+#include <KTextEditor/Document>
+#include <KTextEditor/TemplateInterface>
+#include <KTextBrowser>
+#include <KActionCollection>
+#include <KIcon>
+#include <KLocale>
+
+JavaScriptCompletionModel::JavaScriptCompletionModel( const QString& \
completionShortcut, +						      QObject* parent )
+					: KTextEditor::CodeCompletionModel( parent ) {
+    m_completionShortcut = completionShortcut;
+    initGlobalFunctionCompletion();
+    initTimetableInfoCompletion();
+    initHelperCompletion();
+    initFunctionCallCompletion();
+}
+
+QVariant JavaScriptCompletionModel::data( const QModelIndex& index, int role ) const \
{ +    if ( index.column() == Icon && role == Qt::DecorationRole ) {
+        CompletionItem completion = m_completions.at( index.row() );
+        if ( completion.properties.testFlag(Function) )
+            return KIcon("code-function");
+        else if ( completion.properties.testFlag(Class) )
+            return KIcon("code-class");
+        else if ( completion.properties.testFlag(Const) )
+            return KIcon("code-variable");
+    }
+
+    if ( role == ItemSelected ) {
+        return m_completions.at( index.row() ).description;
+    } else if ( role == IsExpandable ) {
+        return true;
+    } else if ( role == ExpandingWidget ) {
+        QVariant v;
+        KTextBrowser *textBrowser = new KTextBrowser;
+        textBrowser->setText( m_completions.at(index.row()).description );
+        textBrowser->setGeometry( 0, 0, 100, 85 ); // Make the widget a bit bigger
+        textBrowser->setReadOnly( true );
+        textBrowser->setTextInteractionFlags( Qt::LinksAccessibleByKeyboard |
+                                              Qt::LinksAccessibleByMouse );
+        v.setValue< QWidget* >( textBrowser );
+        return v;
+    } else if ( role == CompletionRole ) {
+        return (int)m_completions.at( index.row() ).properties;
+    }
+
+    if ( role == Qt::DisplayRole ) {
+        if ( index.column() == Name ) {
+            return m_completions.at( index.row() ).name;
+        } else if ( index.column() == Prefix ) {
+            const QString prefix = m_completions.at( index.row() ).prefix;
+            return prefix.isEmpty() ? QVariant() : prefix;
+        } else if ( index.column() == Postfix ) {
+            const QString postfix = m_completions.at( index.row() ).postfix;
+            return postfix.isEmpty() ? QVariant() : postfix;
+        }
+    }
+
+    return QVariant();
+}
+
+void JavaScriptCompletionModel::executeCompletionItem( KTextEditor::Document* \
document, +					    const KTextEditor::Range& word, int row ) const {
+    kDebug() << "COMPLETION" << word << row;
+    CompletionItem completion = m_completions.at( row );
+    if ( completion.isTemplate ) {
+        KTextEditor::TemplateInterface *templateInterface =
+            qobject_cast<KTextEditor::TemplateInterface*>( document->activeView() );
+        if ( templateInterface ) {
+	    KTextEditor::Cursor cursor = word.start();
+            document->removeText( word );
+            templateInterface->insertTemplateText( cursor, completion.completion,
+                                                   QMap<QString, QString>() );
+        } else
+            kDebug() << "No template interface";
+    } else {
+        document->replaceText( word, completion.completion );
+    }
+}
+
+void JavaScriptCompletionModel::completionInvoked( KTextEditor::View* view,
+			    const KTextEditor::Range& range,
+			    KTextEditor::CodeCompletionModel::InvocationType /*invocationType*/ ) {
+    m_completions.clear();
+    setRowCount( 0 );
+
+    KTextEditor::Range leftRange( KTextEditor::Cursor(0, 0), range.start() );
+    QString leftText = stripComments( view->document()->text(leftRange) );
+    int blockLevel = leftText.count( '{' ) - leftText.count( '}' );
+    if ( blockLevel < 0 ) {
+        kDebug() << "More closing '}' found than opening '{' at line"
+		 << range.start().line();
+        return;
+    } else { // at root level or inside function
+        QString word = view->document()->text( range );
+
+        kDebug() << "COMPLETION WORD" << word;
+        QString text, textUntilWhiteSpace, textUntilLineBegin;
+        int col = range.start().column();
+        QString line = view->document()->line( range.end().line() );
+
+        textUntilLineBegin = line.left( col ).trimmed();
+        kDebug() << "Complete Start:" << textUntilLineBegin << col;
+
+        QRegExp rxWordBegin( "\\s" );
+        int pos = rxWordBegin.lastIndexIn( line, col - 1 );
+        if ( pos == -1 ) {
+            textUntilWhiteSpace = textUntilLineBegin;
+            kDebug() << "Using all left (pos is -1):" << textUntilWhiteSpace << col;
+        } else {
+            textUntilWhiteSpace = line.mid( pos + 1, col - pos - 1 ).trimmed();
+            kDebug() << "Word Start:" << pos << textUntilWhiteSpace << col;
+        }
+
+        if ( word.isEmpty() )
+            text = textUntilWhiteSpace;
+        else
+            text = textUntilWhiteSpace + word;
+
+        if ( blockLevel == 0 ) { // at root level
+            m_completions << m_completionsGlobalFunctions.values();
+        } else { // inside function
+            if ( text.startsWith("helper.") ) {
+                m_completions << m_completionsHelper.values();
+            } else if ( text.startsWith("timetableData.set( '")
+                        || textUntilLineBegin.startsWith("timetableData.set( '") ) {
+                // Add timetable info completions
+                m_completions << m_completionsTimetableInfo.values();
+            } else if ( text.startsWith("timetableData.") ) {
+                m_completions << m_completionsCalls["call:timetableData.set"];
+                m_completions << m_completionsCalls["call:timetableData.clear"];
+            } else if ( text.startsWith("result.") ) {
+                m_completions << m_completionsCalls["call:result.addData"];
+            } else {
+                m_completions << CompletionItem( Class | GlobalScope, "helper",
+			i18nc("@info The description for the 'helper' object",
+			    "The <emphasis>helper</emphasis> object contains some "
+			    "useful functions."),
+			"helper.", false, "object" );
+                m_completions << CompletionItem( Class | GlobalScope, \
"timetableData", +			i18nc("@info The description for the 'timetableData' object",
+			    "The <emphasis>timetableData</emphasis> object is used to "
+			    "put parsed timetable data into it.<nl/>"
+			    "<note>Once all data is stored inside <emphasis>"
+			    "timetableData</emphasis> you can call <emphasis>result" \
".addData()</emphasis>.</note>"), +			"timetableData.", false, "object" );
+                m_completions << CompletionItem(  Class | GlobalScope, "result",
+			i18nc("@info The description for the 'result' object",
+			    "The result object is used to store all parsed "
+			    "departure/arrival/journey items. Call <emphasis>"
+			    "result.addData( timetableData )</emphasis> to add the "
+			    "current item to the result set."),
+			"result.", false, "object" );
+            }
+        }
+    }
+
+    setRowCount( m_completions.count() );
+    reset();
+}
+
+QString JavaScriptCompletionModel::stripComments( const QString& text ) const {
+    QString ret = text;
+    QRegExp rx( "//.*\n|/\\*.*\\*/" );
+    rx.setMinimal( true );
+    return ret.remove( rx );
+}
+
+CompletionItem JavaScriptCompletionModel::completionItemFromId( const QString id ) {
+    CompletionItem item = m_completionsGlobalFunctions.value( id );
+    if ( item.isValid() )
+	return item;
+
+    item = m_completionsTimetableInfo.value( id );
+    if ( item.isValid() )
+	return item;
+
+    item = m_completionsCalls.value( id );
+    if ( item.isValid() )
+	return item;
+
+    QString simpleID = id;
+    QRegExp rxBraces( "\\([^\\)]*\\)" );
+    rxBraces.setMinimal( true );
+    simpleID.replace( rxBraces, "()" );
+    item = m_completionsHelper.value( simpleID );
+
+    return item;
+}
+
+void JavaScriptCompletionModel::initGlobalFunctionCompletion() {
+    m_completionsGlobalFunctions.insert( "func:usedTimetableInformations()",
+	    CompletionItem( Function | GlobalScope,
+            "usedTimetableInformations()",
+            i18nc("@info The description for the 'usedTimetableInformations' \
function", +                  "Should be implemented to tell which features the \
script supports.<nl/>" +                  "This function is called by the data \
engine."), +            "\n// This function returns a list of all features supported \
by this script.\n" +	    "function usedTimetableInformations() {\n"
+	    "\t// These strings are currently recognized as features:\n"
+	    "\t//   'Delay', 'DelayReason', 'Platform', 'JourneyNews', 'TypeOfVehicle',\n"
+	    "\t//   'StopID', 'Pricing', 'Changes', 'RouteStops', \
'RoutePlatformsDeparture',\n" +	    "\t//   'RoutePlatformsArrival', \
'RouteTimesDeparture', 'RoutePlatformsArrival',\n" +	    "\t//   \
'RouteTransportLines'.\n" +	    "\treturn [ '${cursor}' ];\n"
+	    "}\n",
+            true, "Implement string array", "                   ") ); // The spaces \
make the completion +		    // box wider, so that the code snipped can be read
+    
+    m_completionsGlobalFunctions.insert( "func:parseTimetable(html)",
+	    CompletionItem( Function | GlobalScope,
+            "parseTimetable( html )",
+            i18nc("@info The description for the 'parseTimetable' function",
+                  "Parses departure/arrival documents.<nl/>"
+                  "This function is called by the data engine. The parameter \
contains the " +                  "contents of the document body. Found \
departures/arrivals can be handed " +                  "over to the data engine like \
this:<nl/>" //<bcode>" +                  "<icode>  // First clear the old \
data</icode><nl/>" +                  "<icode>  timetableData.clear();</icode><nl/>"
+                  "<icode>  // Then set all read values</icode><nl/>"
+                  "<icode>  timetableData.set( 'TransportLine', '603' \
);</icode><nl/>" +                  "<icode>  timetableData.set( 'TypeOfVehicle', \
'bus' );</icode><nl/>" +                  "<icode>  timetableData.set( 'Target', \
'Samplestreet' );</icode><nl/>" +                  "<icode>  timetableData.set( \
'DepartureHour', 10 );</icode><nl/>" +                  "<icode>  timetableData.set( \
'DepartureMinute', 23 );</icode><nl/>" +                  "<icode>  \
timetableData.set( 'Delay', 4 );</icode><nl/>" +                  "<icode>  // Add \
timetable data to the result set</icode><nl/>" +                  "<icode>  \
result.addData( timetableData );</icode><nl/><nl/>" +                  "<note>You \
<emphasis>can</emphasis> return a string array with keywords " +                  \
"that affect all departures/arrivals. Currently only one such keyword is " +          \
"supported: <emphasis>'no delays'</emphasis>, used to indicate that " +               \
"there is no delay information for the given stop. The data engine can " +            \
"then use a higher timeout for the next data update. When delay " +                  \
"information is available updates are done more often, because delays " +             \
"may change.</note>"), +// 				      "</bcode>"),
+            "\n// This function parses a given HTML document for departure/arrival \
data.\n" +            "function parseTimetable( html ) {\n"
+            "\t// Find block of departures\n"
+            "\t// TODO: Adjust so that you get the block that contains\n"
+            "\t// the departures in the document\n"
+            "\tvar str = helper.extractBlock( html, "
+            "'<table ${departure_table}>', '</table>' );\n\n"
+            "\t// Initialize regular expressions\n"
+            "\t// TODO: Adjust the reg exp\n"
+            "\tvar departuresRegExp = /<tr>([\\s\\S]*?)<\\/tr>/ig;\n\n"
+            "\t// Go through all departure blocks\n"
+            "\twhile ( (departureRow = departuresRegExp.exec(str)) ) {\n"
+            "\t\t// This gets the current departure row\n"
+            "\t\tdepartureRow = departureRow[1];\n\n"
+            "\t\t// TODO: Parse the departure row for departure data\n"
+            "\t\t${cursor}\n\n"
+            "\t\t// Add departure to the result set\n"
+            "\t\t// TODO: Fill in parsed values instead of the sample strings.\n"
+            "\t\t// You can also add other information, use the code completion\n"
+            "\t\t// (" + m_completionShortcut + ") for more information.\n"
+            "\t\ttimetableData.clear();\n"
+            "\t\ttimetableData.set( 'TransportLine', 'Sample line 4' );\n"
+            "\t\ttimetableData.set( 'TypeOfVehicle', 'bus' );\n"
+            "\t\ttimetableData.set( 'Target', 'Sample target' );\n"
+            "\t\ttimetableData.set( 'DepartureHour', 10 );\n"
+            "\t\ttimetableData.set( 'DepartureMinute', 15 );\n"
+            "\t\tresult.addData( timetableData );\n"
+            "\t}\n"
+            "}\n",
+            true, "Implement string array", "                   " )); // The spaces \
make the completion +    // box wider, so that the code snipped can be read
+
+    m_completionsGlobalFunctions.insert( "func:parseJourneys(html)",
+	    CompletionItem( Function | GlobalScope,
+            "parseJourneys( html )",
+            i18nc("@info The description for the 'parseJourneys' function",
+                  "Parses journey documents.<nl/>"
+                  "This function is called by the data engine. The parameter "
+                  "contains the contents of the document body. Found journeys can "
+                  "be handed over to the data engine like this:<nl/>" //<bcode>"
+                  "<icode>  // First clear the old data</icode><nl/>"
+                  "<icode>  timetableData.clear();</icode><nl/>"
+                  "<icode>  // Then set all read values</icode><nl/>"
+                  "<icode>  timetableData.set( 'StartStopName', 'A' );</icode><nl/>"
+                  "<icode>  timetableData.set( 'TargetStopName', 'B' \
);</icode><nl/>" +                  "<icode>  timetableData.set( 'DepartureHour', 10 \
);</icode><nl/>" +                  "<icode>  timetableData.set( 'DepartureMinute', \
23 );</icode><nl/>" +                  "<icode>  timetableData.set( 'ArrivalHour', 11 \
);</icode><nl/>" +                  "<icode>  timetableData.set( 'ArrivalMinute', 05 \
);</icode><nl/>" +                  "<icode>  timetableData.set( 'Changes', 3 \
);</icode><nl/>" +                  "<icode>  timetableData.set( 'Pricing', '2,30 \
€' );</icode><nl/>" +                  "<icode>  // Add timetable data to the \
result set</icode><nl/>" +                  "<icode>  result.addData( timetableData \
);</icode>"), +// 				      "</bcode>"),
+            "\n// This function parses a given HTML document for journey data.\n"
+	    "function parseJourneys( html ) {\n"
+	    "\t${cursor}\n"
+	    "}\n",
+            true, "Implement void", "                   " )); // The spaces make the \
completion +    // box wider, so that the code snipped can be read
+
+    m_completionsGlobalFunctions.insert( "func:parsePossibleStops(html)",
+	    CompletionItem( Function | GlobalScope,
+            "parsePossibleStops( html )",
+            i18nc("@info The description for the 'parsePossibleStops' function",
+                  "Parses stop suggestion documents.<nl/>"
+                  "This function is called by the data engine. The parameter "
+                  "contains the contents of the document body. Found stop data "
+                  "can be handed over to the data engine like this:<nl/>" //<bcode>"
+                  "<icode>  // First clear the old data</icode><nl/>"
+                  "<icode>  timetableData.clear();</icode><nl/>"
+                  "<icode>  // Then set all read values</icode><nl/>"
+                  "<icode>  timetableData.set( 'StopName', 'Bremen Hbf' \
);</icode><nl/>" +                  "<icode>  // Add timetable data to the result \
set</icode><nl/>" +                  "<icode>  result.addData( timetableData \
);</icode>"), +// 				      "</bcode>"),
+            "\n// This function parses a given HTML document for stop \
suggestions.\n" +	    "function parsePossibleStops( html ) {\n"
+	    "\t${cursor}\n"
+	    "}\n",
+            true, "Implement void", "                   " )); // The spaces make the \
completion +    // box wider, so that the code snipped can be read
+
+    m_completionsGlobalFunctions.insert( "func:getUrlForLaterJourneyResults(html)",
+	    CompletionItem( Function | GlobalScope,
+            "getUrlForLaterJourneyResults( html )",
+            i18nc("@info The description for the 'getUrlForLaterJourneyResults' \
function", +                  "Parses a journey document for a link to a journey "
+                  "document containing later journeys.<nl/>"
+                  "This function is called by the data engine. The parameter "
+                  "contains the contents of the document body. The found link "
+                  "can be simply returned. If no link could be found, return \
null."), +            "\n// This function parses a given HTML document for a link to \
later journeys.\n" +	    "function getUrlForLaterJourneyResults( html ) {\n"
+	    "\treturn ${cursor};\n"
+	    "}\n",
+            true, "Implement string", "                   " )); // The spaces make \
the completion +    // box wider, so that the code snipped can be read
+
+    m_completionsGlobalFunctions.insert( \
"func:getUrlForDetailedJourneyResults(html)", +	    CompletionItem( Function | \
GlobalScope, +            "getUrlForDetailedJourneyResults( html )",
+            i18nc("@info The description for the 'getUrlForDetailedJourneyResults' \
function", +                  "Parses a journey document for a link to another \
journey " +                  "document containing more details about journeys.<nl/>"
+                  "This function is called by the data engine. The parameter "
+                  "contains the contents of the document body. "
+                  "The found link can be simply returned. If no link could be found, \
return null."), +            "\n// This function parses a given HTML document\n"
+            "// for a link to a more detailed journey document.\n"
+	    "function getUrlForLaterJourneyResults( html ) {\n"
+	    "\treturn ${cursor};\n"
+	    "}\n",
+            true, "Implement string", "                   " )); // The spaces make \
the completion +    // box wider, so that the code snipped can be read
+}
+
+void JavaScriptCompletionModel::initHelperCompletion() {
+    m_completionsHelper.insert( "call:extractBlock()", CompletionItem( Function,
+	    "extractBlock( string, string begin, string end )",
+	    i18nc("@info The description for the 'extractBlock' function",
+		    "Extracts the first block in the given string, that begins with "
+		    "<placeholder>begin</placeholder> and ends with <placeholder>end"
+		    "</placeholder>. Returns the found block or an empty string if the "
+		    "block couldn't be found."),
+	    "extractBlock( ${string}, ${begin}, ${end} );", true, "string" ));
+    m_completionsHelper.insert( "call:stripTags()", CompletionItem( Function, \
"stripTags( string )", +	    i18nc("@info The description for the 'stripTags' \
function", +		    "Strips all tags from a given string and returns the result."),
+	    "stripTags( ${string} );", true, "string" ));
+    m_completionsHelper.insert( "call:trim()", CompletionItem( Function, "trim( \
string )", +	    i18nc("@info The description for the 'trim' function",
+		    "Trims a string and returns the result."),
+	    "trim( ${string} );", true, "string" ));
+    m_completionsHelper.insert( "call:matchTime()", CompletionItem( Function,
+	    "matchTime( string time, string format = 'hh:mm' )",
+	    i18nc("@info The description for the 'matchTime' function",
+		    "Searches for a time with the given <emphasis>format</emphasis> in the "
+		    "given <emphasis>time</emphasis> string. Returns an integer array with "
+		    "two integers: The first one is the hour part, the second one the "
+		    "minute part."),
+	    "matchTime( ${timeString} );", true, "int array" ));
+    m_completionsHelper.insert( "call:matchDate()", CompletionItem( Function,
+	    "matchDate( string date, string format = 'yyyy-MM-dd' )",
+	    i18nc("@info The description for the 'matchDate' function",
+		    "Searches for a date with the given <emphasis>format</emphasis> in the "
+		    "given <emphasis>date</emphasis> string. Returns an integer array with "
+		    "three integers: The first one is the year part, the second one the "
+		    "month part and the third one the day part."),
+	    "matchDate( ${dateString} );", true, "int array" ));
+    m_completionsHelper.insert( "call:formatTime()", CompletionItem( Function,
+	    "formatTime( int hour, int minute, string format = 'hh:mm' )",
+	    i18nc("@info The description for the 'formatTime' function",
+		    "Formats a time given by it's <emphasis>hour</emphasis> and <emphasis>"
+		    "minute</emphasis> using the given <emphasis>format</emphasis>."),
+	    "formatTime( ${hour}, ${minute} );", true, "string" ));
+    m_completionsHelper.insert( "call:duration()", CompletionItem( Function,
+	    "duration( string time1, string time2, string format = 'hh:mm' )",
+	    i18nc("@info The description for the 'duration' function",
+		    "Computes the duration in minutes between the two times, given as strings. "
+		    "The time strings are parsed using the given <emphasis>format</emphasis>."),
+	    "duration( ${timeString1}, ${timeString2} );", true, "int" ));
+    m_completionsHelper.insert( "call:addMinsToTime()", CompletionItem( Function,
+	    "addMinsToTime( string time, int minsToAdd, string format = 'hh:mm' )",
+	    i18nc("@info The description for the 'addMinsToTime' function",
+		    "Adds <emphasis>minsToAdd</emphasis> minutes to the <emphasis>time"
+		    "</emphasis> given as a string. The time string is parsed using the "
+		    "given <emphasis>format</emphasis>. Returns a time string formatted "
+		    "using the given <emphasis>format</emphasis>"),
+	    "addMinsToTime( ${timeString}, ${minsToAdd} );", true, "string" ));
+    m_completionsHelper.insert( "call:addDaysToDate()", CompletionItem( Function,
+	    "addDaysToDate( int[] dateArray, int daysToAdd )",
+	    i18nc("@info The description for the 'addDaysToDate' function",
+		    "Adds <emphasis>daysToAdd</emphasis> days to the <emphasis>date"
+		    "</emphasis> given as an integer array (with three integers: year, month, \
day). " +		    "Returns an integer array with the new values"),
+	    "addDaysToDate( ${dateArray [year, month, day]}, ${daysToAdd} );", true, \
"string" )); +    m_completionsHelper.insert( "call:splitSkipEmptyParts()", \
CompletionItem( Function, +	    "splitSkipEmptyParts( string, string separator )",
+	    i18nc("@info The description for the 'splitSkipEmptyParts' function",
+		    "Splits the given <emphasis>string</emphasis> using the given "
+		    "<emphasis>separator</emphasis>. Returns an array of strings."),
+	    "splitSkipEmptyParts( ${string}, ${separator} );", true, "string array" ));
+    m_completionsHelper.insert( "call:error()", CompletionItem( Function,
+	    "error( string message, string data )",
+	    i18nc("@info The description for the 'error' function",
+		    "Logs the error message with the given data string, eg. the HTML code where \
parsing " +			"failed. The message gets also send to stdout with a short version of \
the data."), +	    "error( ${message}, ${data} );", true, "void" ));
+}
+
+void JavaScriptCompletionModel::initTimetableInfoCompletion() {
+    m_completionsTimetableInfo.insert( "str:DepartureHour", CompletionItem( Const, \
"DepartureHour", +            i18nc("@info The description for the 'DepartureHour' \
info", +                  "The hour of the departure time."),
+            "DepartureHour",
+            false, QString(), i18nc("@info/plain", "Needed for Departures/Journeys") \
)); +    m_completionsTimetableInfo.insert( "str:DepartureMinute", CompletionItem( \
Const, "DepartureMinute", +            i18nc("@info The description for the \
'DepartureMinute' info", +                  "The minute of the departure time."),
+            "DepartureMinute",
+            false, QString(), i18nc("@info/plain", "Needed for Departures/Journeys") \
)); +    m_completionsTimetableInfo.insert( "str:DepartureDate", CompletionItem( \
Const, "DepartureDate", +            i18nc("@info The description for the \
'DepartureDate' info", +                  "The date of the departure."),
+            "DepartureDate" ));
+    m_completionsTimetableInfo.insert( "str:TypeOfVehicle", CompletionItem( Const, \
"TypeOfVehicle", +            i18nc("@info The description for the 'TypeOfVehicle' \
info", "The type of vehicle."), +            "TypeOfVehicle" ));
+    m_completionsTimetableInfo.insert( "str:TransportLine", CompletionItem( Const, \
"TransportLine", +            i18nc("@info The description for the 'TransportLine' \
info", "The name of the " +                  "public transport line, e.g. '4', '6S', \
'S 5', 'RB 24122.'"), +            "TransportLine",
+            false, QString(), i18nc("@info/plain", "Needed for Departures") ));
+    m_completionsTimetableInfo.insert( "str:FlightNumber", CompletionItem( Const, \
"FlightNumber", +            i18nc("@info The description for the 'FlightNumber' \
info", +                  "Same as TransportLine, used for flights."),
+            "FlightNumber" ));
+    m_completionsTimetableInfo.insert( "str:Target", CompletionItem( Const, \
"Target", +            i18nc("@info The description for the 'Target' info",
+                  "The target of a journey / of a public transport line."),
+            "Target",
+            false, QString(), i18nc("@info/plain", "Needed for Departures") ));
+    m_completionsTimetableInfo.insert( "str:Platform", CompletionItem( Const, \
"Platform", +            i18nc("@info The description for the 'Platform' info",
+                  "The platform at which the vehicle departs/arrives."),
+            "Platform" ));
+    m_completionsTimetableInfo.insert( "str:Delay", CompletionItem( Const, "Delay",
+            i18nc("@info The description for the 'Delay' info",
+                  "The delay of a departure/arrival in minutes."),
+            "Delay" ));
+    m_completionsTimetableInfo.insert( "str:DelayReason", CompletionItem( Const, \
"DelayReason", +            i18nc("@info The description for the 'DelayReason' info",
+                  "The reason of a delay."),
+            "DelayReason" ));
+    m_completionsTimetableInfo.insert( "str:JourneyNews", CompletionItem( Const, \
"JourneyNews", +            i18nc("@info The description for the 'JourneyNews' info",
+                  "Can contain delay / delay reason / other news."),
+            "JourneyNews" ));
+    m_completionsTimetableInfo.insert( "str:JourneyNewsOther", CompletionItem( \
Const, "JourneyNewsOther", +            i18nc("@info The description for the \
'JourneyNewsOther' info", +                  "Other news (not delay / delay \
reason)."), +            "JourneyNewsOther" ));
+    m_completionsTimetableInfo.insert( "str:JourneyNewsLink", CompletionItem( Const, \
"JourneyNewsLink", +            i18nc("@info The description for the \
'JourneyNewsLink' info", +                  "A link to an html page with journey \
news.<nl/>" +                  "<note>The url of the accessor is prepended, if a \
relative path has been " +                  "matched (starting with '/').</note>"),
+            "JourneyNewsLink" ));
+    m_completionsTimetableInfo.insert( "str:DepartureHourPrognosis",
+	    CompletionItem( Const, "DepartureHourPrognosis",
+            i18nc("@info The description for the 'DepartureHourPrognosis' info",
+                  "The prognosis for the departure hour, which is the departure hour \
" +                  "plus the delay."),
+            "DepartureHourPrognosis" ));
+    m_completionsTimetableInfo.insert( "str:DepartureMinutePrognosis",
+	    CompletionItem( Const, "DepartureMinutePrognosis",
+            i18nc("@info The description for the 'DepartureMinutePrognosis' info",
+                  "The prognosis for the departure minute, which is the departure \
minute " +                  "plus the delay."),
+            "DepartureMinutePrognosis" ));
+    m_completionsTimetableInfo.insert( "str:Operator", CompletionItem( Const, \
"Operator", +            i18nc("@info The description for the 'Operator' info",
+                  "The company that is responsible for the journey."),
+            "Operator" ));
+    m_completionsTimetableInfo.insert( "str:DepartureAMorPM", CompletionItem( Const, \
"DepartureAMorPM", +            i18nc("@info The description for the \
'DepartureAMorPM' info", "'am' or 'pm' " +                  "for the departure \
time.<nl/>" +                  "<note>If not set, 24 hour format is \
assumed.</note>"), +            "DepartureAMorPM" ));
+    m_completionsTimetableInfo.insert( "str:DepartureAMorPMPrognosis",
+	    CompletionItem( Const, "DepartureAMorPMPrognosis",
+            i18nc("@info The description for the 'DepartureAMorPMPrognosis' info",
+                  "'am' or 'pm' for the prognosis departure time.<nl/>"
+                  "<note>If not set, 24 hour format is assumed.</note>"),
+            "DepartureAMorPMPrognosis" ));
+    m_completionsTimetableInfo.insert( "str:ArrivalAMorPM", CompletionItem( Const, \
"ArrivalAMorPM", +            i18nc("@info The description for the 'ArrivalAMorPM' \
info", +                  "'am' or 'pm' for the arrival time.<nl/>"
+                  "<note>If not set, 24 hour format is assumed.</note>"),
+            "ArrivalAMorPM" ));
+    m_completionsTimetableInfo.insert( "str:Status", CompletionItem( Const, \
"Status", +            i18nc("@info The description for the 'Status' info", "The \
current status of " +                  "the departure / arrival. Currently only used \
for planes."), +            "Status" ));
+    m_completionsTimetableInfo.insert( "str:DepartureYear", CompletionItem( Const, \
"DepartureYear", +            i18nc("@info The description for the 'DepartureYear' \
info", "The year of the " +                  "departure, to be used when the year is \
separated from the date."), +            "DepartureYear" ));
+    m_completionsTimetableInfo.insert( "str:IsNightLine", CompletionItem( Const, \
"IsNightLine", +            i18nc("@info The description for the 'IsNightLine' info", \
"A boolean indicating if " +				  "the transport line is a nightline or not."),
+            "IsNightLine" ));
+    m_completionsTimetableInfo.insert( "str:RouteStops", CompletionItem( Const, \
"RouteStops", +            i18nc("@info The description for the 'RouteStops' info",
+                  "A list of stops of the departure/arrival to it's destination stop \
or " +                  "a list of stops of the journey from it's start to it's \
destination " +                  "stop.<nl/>If <emphasis>RouteStops</emphasis> and \
<emphasis>RouteTimes" +                  "</emphasis> are both set, they should \
contain the same number of " +                  "elements. And elements with equal \
indices should be associated (the " +                  "times at which the vehicle is \
at the stops).<nl/>" +                  "<note>For journeys \
<emphasis>RouteTimesDeparture</emphasis> and " +                  \
"<emphasis>RouteTimesArrival</emphasis> should be used instead of " \
"<emphasis>RouteTimes</emphasis>.</note>"), +            "RouteStops" ));
+    m_completionsTimetableInfo.insert( "str:RouteTimes", CompletionItem( Const, \
"RouteTimes", +            i18nc("@info The description for the 'RouteTimes' info",
+                  "A list of times of the departure/arrival to it's destination \
stop.<nl/>" +                  "If <emphasis>RouteStops</emphasis> and \
<emphasis>RouteTimes</emphasis> " +                  "are both set, they should \
contain the same number of elements. And " +                  "elements with equal \
indices should be associated (the times at which " +                  "the vehicle is \
at the stops)."), +            "RouteTimes" ));
+
+    // Journey information
+    m_completionsTimetableInfo.insert( "str:RouteTimesDeparture",
+	    CompletionItem( Const, "RouteTimesDeparture",
+            i18nc("@info The description for the 'RouteTimesDeparture' info",
+                  "A list of departure times of the journey.<nl/>If \
<emphasis>RouteStops" +                  "</emphasis> and \
<emphasis>RouteTimesDeparture</emphasis> are both set, " +                  "the \
latter should contain one element less (because the last stop has " +                 \
"no departure, only an arrival time). Elements with equal indices should " +          \
"be associated (the times at which the vehicle departs from the stops)."), +          \
"RouteTimesDeparture" )); +    m_completionsTimetableInfo.insert( \
"str:RouteTimesArrival", +	    CompletionItem( Const, "RouteTimesArrival",
+            i18nc("@info The description for the 'RouteTimesArrival' info",
+                  "A list of arrival times of the journey.<nl/>If \
<emphasis>RouteStops" +                  "</emphasis> and \
<emphasis>RouteTimesArrival</emphasis> are both set, " +                  "the latter \
should contain one element less (because the first stop has " +                  "no \
arrival, only a departure time). Elements with equal indices should " +               \
"be associated (the times at which the vehicle arrives at the stops)."), +            \
"RouteTimesArrival" )); +    m_completionsTimetableInfo.insert( \
"str:RouteExactStops", CompletionItem( Const, "RouteExactStops", +            \
i18nc("@info The description for the 'RouteExactStops' info", +                  "The \
number of exact route stops.<nl/>The route stop list in <emphasis>" +                 \
"RouteStops</emphasis> isn't complete from the last exact route stop."), +            \
"RouteExactStops" )); +    m_completionsTimetableInfo.insert( \
"str:RouteTypesOfVehicles", +	    CompletionItem( Const, "RouteTypesOfVehicles",
+            i18nc("@info The description for the 'RouteTypesOfVehicles' info",
+                  "The types of vehicles used for each 'sub-journey' of a \
journey."), +            "RouteTypesOfVehicles" ));
+    m_completionsTimetableInfo.insert( "str:RouteTransportLines",
+	    CompletionItem( Const, "RouteTransportLines",
+            i18nc("@info The description for the 'RouteTransportLines' info",
+                  "The transport lines used for each 'sub-journey' of a journey."),
+            "RouteTransportLines" ));
+    m_completionsTimetableInfo.insert( "str:RoutePlatformsDeparture",
+	    CompletionItem( Const, "RoutePlatformsDeparture",
+            i18nc("@info The description for the 'RoutePlatformsDeparture' info",
+                  "The platforms of departures used for each 'sub-journey' of a \
journey.<nl/>" +                  "If <emphasis>RouteStops</emphasis> and \
<emphasis>RoutePlatformsDeparture" +                  "</emphasis> are both set, the \
latter should contain one element less (because " +                  "the last stop \
has no departure, only an arrival platform). Elements with " +                  \
"equal indices should be associated (the platforms from which the vehicle " +         \
"departs from the stops)."), +            "RoutePlatformsDeparture" ));
+    m_completionsTimetableInfo.insert( "str:RoutePlatformsArrival",
+	    CompletionItem( Const, "RoutePlatformsArrival",
+            i18nc("@info The description for the 'RoutePlatformsArrival' info",
+                  "The platforms of arrivals used for each 'sub-journey' of a \
journey.<nl/>" +                  "If <emphasis>RouteStops</emphasis> and \
<emphasis>RoutePlatformsArrival" +                  "</emphasis> are both set, the \
latter should contain one element less " +                  "(because the first stop \
has no arrival, only a departure platform). " +                  "Elements with equal \
indices should be associated (the platforms at which " +                  "the \
vehicle arrives at the stops)"), +            "RoutePlatformsArrival" ));
+    m_completionsTimetableInfo.insert( "str:RouteTimesDepartureDelay",
+	    CompletionItem( Const, "RouteTimesDepartureDelay",
+            i18nc("@info The description for the 'RouteTimesDepartureDelay' info",
+                  "A list of delays in minutes for each departure time of a route "
+                  "(see <emphasis>RouteTimesDeparture</emphasis>).<nl/>If set it \
should contain " +                  "the same number of elements as \
'RouteTimesDeparture'."), +            "RouteTimesDepartureDelay" ));
+    m_completionsTimetableInfo.insert( "str:RouteTimesArrivalDelay",
+	    CompletionItem( Const, "RouteTimesArrivalDelay",
+            i18nc("@info The description for the 'RouteTimesArrivalDelay' info",
+                  "A list of delays in minutes for each arrival time of a route "
+                  "(see <emphasis>RouteTimesArrival</emphasis>).<nl/>If set it \
should contain " +                  "the same number of elements as \
'RouteTimesArrival'."), +            "RouteTimesArrivalDelay" ));
+    m_completionsTimetableInfo.insert( "str:Duration", CompletionItem( Const, \
"Duration", +            i18nc("@info The description for the 'Duration' info",
+                  "The duration of a journey in minutes."),
+            "Duration" ));
+    m_completionsTimetableInfo.insert( "str:StartStopName", CompletionItem( Const, \
"StartStopName", +            i18nc("@info The description for the 'StartStopName' \
info", +                  "The name of the starting stop of a journey."),
+            "StartStopName",
+            false, QString(), i18nc("@info/plain", "Needed for Journeys") ));
+    m_completionsTimetableInfo.insert( "str:StartStopID", CompletionItem( Const, \
"StartStopID", +            i18nc("@info The description for the 'StartStopID' info",
+                  "The ID of the starting stop of a journey."),
+            "StartStopID" ));
+    m_completionsTimetableInfo.insert( "str:TargetStopName", CompletionItem( Const, \
"TargetStopName", +            i18nc("@info The description for the 'TargetStopName' \
info", +                  "The name of the target stop of a journey."),
+            "TargetStopName", false, QString(), i18nc("@info/plain", "Needed for \
Journeys") )); +    m_completionsTimetableInfo.insert( "str:TargetStopID", \
CompletionItem( Const, "TargetStopID", +            i18nc("@info The description for \
the 'TargetStopID' info", +                  "The ID of the target stop of a \
journey."), +            "TargetStopID" ));
+    m_completionsTimetableInfo.insert( "str:ArrivalDate", CompletionItem( Const, \
"ArrivalDate", +            i18nc("@info The description for the 'ArrivalDate' info",
+                  "The date of the arrival."),
+            "ArrivalDate" ));
+    m_completionsTimetableInfo.insert( "str:ArrivalHour", CompletionItem( Const, \
"ArrivalHour", +            i18nc("@info The description for the 'ArrivalHour' info",
+                  "The hour of the arrival time."),
+            "ArrivalHour", false, QString(), i18nc("@info/plain", "Needed for \
Journeys") )); +    m_completionsTimetableInfo.insert( "str:ArrivalMinute", \
CompletionItem( Const, "ArrivalMinute", +            i18nc("@info The description for \
the 'ArrivalMinute' info", +                  "The minute of the arrival time."),
+            "ArrivalMinute", false, QString(), i18nc("@info/plain", "Needed for \
Journeys") )); +    m_completionsTimetableInfo.insert( "str:Changes", CompletionItem( \
Const, "Changes", +            i18nc("@info The description for the 'Changes' info",
+                  "The number of changes between different vehicles in a journey."),
+            "Changes" ));
+    m_completionsTimetableInfo.insert( "str:TypesOfVehicleInJourney",
+	    CompletionItem( Const, "TypesOfVehicleInJourney",
+            i18nc("@info The description for the 'TypesOfVehicleInJourney' info",
+                  "A list of vehicle types used in a journey."),
+            "TypesOfVehicleInJourney" ));
+    m_completionsTimetableInfo.insert( "str:Pricing", CompletionItem( Const, \
"Pricing", +            i18nc("@info The description for the 'Pricing' info",
+                  "Information about the pricing of a journey."),
+            "Pricing" ));
+
+    // Stop suggestion information
+    m_completionsTimetableInfo.insert( "str:StopName", CompletionItem( Const, \
"StopName", +            i18nc("@info The description for the 'StopName' info",
+                  "The name of a stop/station."),
+            "StopName", false, QString(), i18nc("@info/plain", "Needed for Stop \
Suggestions") )); +    m_completionsTimetableInfo.insert( "str:StopID", \
CompletionItem( Const, "StopID", +            i18nc("@info The description for the \
'StopID' info", +                  "The ID of a stop/station."),
+            "StopID" ));
+    m_completionsTimetableInfo.insert( "str:StopWeight", CompletionItem( Const, \
"StopWeight", +            i18nc("@info The description for the 'StopWeight' info",
+                  "The weight of a stop suggestion."),
+            "StopWeight" ));
+    // TODO: range? Explain: passed through to eg. KCompletion
+}
+
+void JavaScriptCompletionModel::initFunctionCallCompletion() {
+    m_completionsCalls.insert( "call:timetableData.set",
+	    CompletionItem( Function, "timetableData.set( string infoName, variant value \
)", +			    i18nc("@info The description for the 'timetableData.set' function",
+				  "Saves the given <placeholder>value</placeholder> under the "
+				  "given <placeholder>infoName</placeholder>."),
+			    "set( '${infoName}', ${value} );", true, "void" ) );    
+    m_completionsCalls.insert( "call:timetableData.clear",
+	    CompletionItem( Function, "timetableData.clear()",
+			    i18nc("@info The description for the 'timetableData.clear' function",
+				  "Clears the current values of the "
+				  "<emphasis>timetableData</emphasis> object.<nl/>"
+				  "<note>You should call this method before setting values "
+				  "for the next item.</note>"),
+			    "clear();", false, "void" ) );
+    
+    m_completionsCalls.insert( "call:result.addData",
+	    CompletionItem( Function, "result.addData( timetableData )",
+			    i18nc("@info The description for the 'result.addData' function",
+				  "Adds the current <emphasis>timetableData</emphasis> object "
+				  "to the result set."),
+			    "addData( timetableData );", false, "void" ));
+}
diff --git a/timetablemate/src/javascriptmodel.cpp \
b/timetablemate/src/javascriptmodel.cpp new file mode 100644
index 0000000..936bde7
--- /dev/null
+++ b/timetablemate/src/javascriptmodel.cpp
@@ -0,0 +1,247 @@
+/*
+*   Copyright 2010 Friedrich Pülz <fieti1983@gmx.de>
+*
+*   This program is free software; you can redistribute it and/or modify
+*   it under the terms of the GNU Library General Public License as
+*   published by the Free Software Foundation; either version 2 or
+*   (at your option) any later version.
+*
+*   This program is distributed in the hope that it will be useful,
+*   but WITHOUT ANY WARRANTY; without even the implied warranty of
+*   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+*   GNU General Public License for more details
+*
+*   You should have received a copy of the GNU Library General Public
+*   License along with this program; if not, write to the
+*   Free Software Foundation, Inc.,
+*   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+
+#include "javascriptmodel.h"
+#include "javascriptparser.h"
+#include "javascriptcompletionmodel.h"
+
+#include <KDebug>
+#include <KLocalizedString>
+#include <KIcon>
+#include <KTextEditor/Cursor>
+
+JavaScriptModel::JavaScriptModel( QObject* parent ) : QAbstractItemModel( parent ),
+						m_javaScriptCompletionModel(0) {
+
+}
+
+JavaScriptModel::~JavaScriptModel() {
+    qDeleteAll( m_nodes );
+}
+
+void JavaScriptModel::needTextHint( const KTextEditor::Cursor &position, QString \
&text ) { +    if ( !m_javaScriptCompletionModel )
+	return;
+    
+    CodeNode *node = nodeFromLineNumber( position.line() + 1, position.column(), \
MatchChildren ); +    if ( !node || node->line() != position.line() + 1 )
+	return;
+    
+    CompletionItem item = m_javaScriptCompletionModel->completionItemFromId( \
node->id() ); +    if ( !item.isValid() || item.description.isEmpty() )
+	return;
+    
+    text = QString("<table style='margin: 3px;'><tr><td style='font-size:large;'>"
+		   "<nobr>%1<b>%2</b></nobr><hr></td></tr><tr><td>%3</td></tr>")
+	      .arg(node->type() == Function ? i18n("Function: ") : QString(),
+		   item.name, item.description);
+
+    // The KatePart only prints a debug message...
+    emit showTextHint( position, text );
+}
+
+QModelIndex JavaScriptModel::indexFromNode( CodeNode* node ) const {
+    if ( !m_nodes.contains(node) )
+	return QModelIndex();
+    else
+	return createIndex( m_nodes.indexOf(node), 0, node );
+}
+
+CodeNode *JavaScriptModel::nodeFromIndex( const QModelIndex& index ) const {
+    return static_cast<CodeNode*>( index.internalPointer() );
+}
+
+CodeNode *JavaScriptModel::nodeFromRow( int row ) const {
+    return m_nodes.at( row );
+}
+
+CodeNode *JavaScriptModel::nodeFromLineNumber( int lineNumber, int column,
+					       MatchOptions matchOptions ) {
+    foreach ( CodeNode *node, m_nodes ) {
+// 	if ( node->line() == lineNumber ) {
+// 	    if ( matchOptions.testFlag(MatchChildren) )
+// 		return node->childFromPosition( lineNumber, column );
+// 	    else
+// 		return node;
+// 	}
+
+	if ( /* !matchOptions.testFlag(MatchOnlyFirstRowOfSpanned) TODO REMOVE FLAG
+	    && */ node->isInRange(lineNumber, column) )
+	{
+	    if ( matchOptions.testFlag(MatchChildren) )
+		return node->childFromPosition( lineNumber, column );
+	    else
+		return node;
+	}
+    }
+
+    return createAndAddEmptyNode();
+}
+
+CodeNode *JavaScriptModel::nodeBeforeLineNumber( int lineNumber, NodeTypes nodeTypes \
) { +    CodeNode *lastFoundNode = NULL;
+    foreach ( CodeNode *node, m_nodes ) {
+	if ( node->type() == NoNodeType || !nodeTypes.testFlag(node->type()) )
+	    continue;
+	
+	if ( node->line() < lineNumber )
+	    lastFoundNode = node;
+	else if ( node->line() > lineNumber )
+	    break;
+
+	if ( lineNumber >= node->line() && lineNumber <= node->endLine() )
+	    return node;
+    }
+
+    return lastFoundNode ? lastFoundNode : createAndAddEmptyNode();
+}
+
+CodeNode *JavaScriptModel::nodeAfterLineNumber( int lineNumber, NodeTypes nodeTypes \
) { +    CodeNode *lastFoundNode = NULL;
+    for ( int i = m_nodes.count() - 1; i >= 0; --i ) {
+	CodeNode *node = m_nodes.at( i );
+	if ( node->type() == NoNodeType || !nodeTypes.testFlag(node->type()) )
+	    continue;
+	
+	if ( node->line() > lineNumber )
+	    lastFoundNode = node;
+	else if ( node->line() < lineNumber )
+	    break;
+
+	if ( lineNumber >= node->line() && lineNumber <= node->endLine() )
+	    return node;
+    }
+    return lastFoundNode ? lastFoundNode : createAndAddEmptyNode();
+}
+
+EmptyNode *JavaScriptModel::createAndAddEmptyNode() {
+    EmptyNode *node = NULL;
+    if ( !m_nodes.isEmpty() )
+	node = dynamic_cast<EmptyNode*>( m_nodes.first() );
+    if ( !node ) {
+	node = new EmptyNode;
+	beginInsertRows( QModelIndex(), 0, 0 );
+	m_nodes.insert( 0, node );
+	updateFirstEmptyNodeName();
+	endInsertRows();
+    }
+    
+    return node;
+}
+
+QModelIndex JavaScriptModel::index( int row, int column, const QModelIndex& parent ) \
const { +    Q_UNUSED( parent );
+    if ( !hasIndex(row, column, QModelIndex()) )
+	return QModelIndex();
+
+    if ( row >= 0 && row < m_nodes.count() )
+	return createIndex( row, column, m_nodes[row] );
+    else
+	return QModelIndex();
+}
+
+QVariant JavaScriptModel::data( const QModelIndex& index, int role ) const {
+    CodeNode *item = static_cast<CodeNode*>( index.internalPointer() );
+
+    if ( role == Qt::UserRole ) {
+	return item->type();
+    }
+
+    FunctionNode *function = dynamic_cast<FunctionNode*>( item );
+    if ( function ) {
+	switch ( role ) {
+	    case Qt::DisplayRole:
+		return function->toStringSignature();
+	    case Qt::DecorationRole:
+		return KIcon("code-function");
+	}
+    } else {
+	EmptyNode *empty = dynamic_cast<EmptyNode*>( item );
+	if ( empty ) {
+	    switch ( role ) {
+		case Qt::DisplayRole:
+		    return empty->text();
+	    }
+	}
+    }
+
+    return QVariant();
+}
+
+bool JavaScriptModel::removeRows( int row, int count, const QModelIndex& parent ) {
+    beginRemoveRows( parent, row, row + count - 1 );
+    for ( int i = 0; i < count; ++i ) {
+	CodeNode *item = m_nodes.takeAt( row );
+	delete item;
+    }
+    endRemoveRows();
+    
+    updateFirstEmptyNodeName();
+    return true;
+}
+
+void JavaScriptModel::clear() {
+    beginRemoveRows( QModelIndex(), 0, m_nodes.count() );
+    qDeleteAll( m_nodes );
+    m_nodes.clear();
+    endRemoveRows();
+}
+
+void JavaScriptModel::appendNodes( const QList< CodeNode* > nodes ) {
+    beginInsertRows( QModelIndex(), m_nodes.count(), m_nodes.count() + nodes.count() \
- 1 ); +    m_nodes << nodes;
+    endInsertRows();
+    
+    updateFirstEmptyNodeName();
+}
+
+void JavaScriptModel::setNodes( const QList< CodeNode* > nodes ) {
+    clear();
+
+    beginInsertRows( QModelIndex(), 0, nodes.count() - 1 );
+    m_nodes = nodes;
+    endInsertRows();
+
+    updateFirstEmptyNodeName();
+}
+
+QStringList JavaScriptModel::functionNames() const {
+    QStringList functions;
+    foreach ( CodeNode *node, m_nodes ) {
+	FunctionNode *function = dynamic_cast<FunctionNode*>( node );
+	if ( function )
+	    functions << function->text();
+    }
+    return functions;
+}
+
+void JavaScriptModel::updateFirstEmptyNodeName() {
+    if ( m_nodes.isEmpty() )
+	return;
+
+    EmptyNode *node = dynamic_cast<EmptyNode*>( m_nodes.first() );
+    if ( node ) {
+	if ( m_nodes.count() == 1 ) {
+	    node->setText( i18nc("@info/plain", "(no functions)") );
+	} else {
+	    node->setText( i18ncp("@info/plain", "%1 function:",
+				  "%1 functions:", m_nodes.count() - 1) );
+	}
+    }
+}
diff --git a/timetablemate/src/javascriptparser.cpp \
b/timetablemate/src/javascriptparser.cpp new file mode 100644
index 0000000..ee84b35
--- /dev/null
+++ b/timetablemate/src/javascriptparser.cpp
@@ -0,0 +1,828 @@
+/*
+ *   Copyright 2010 Friedrich Pülz <fieti1983@gmx.de>
+ *
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU Library General Public License as
+ *   published by the Free Software Foundation; either version 2 or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details
+ *
+ *   You should have received a copy of the GNU Library General Public
+ *   License along with this program; if not, write to the
+ *   Free Software Foundation, Inc.,
+ *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+#include "javascriptparser.h"
+
+#include <QHash>
+#include <KTextEditor/Cursor>
+#include <KLocalizedString>
+
+CodeNode::CodeNode( const QString& text, int line, int colStart, int colEnd ) : \
m_parent( 0 ) { +    m_text = text;
+    m_line = line;
+    m_col = colStart;
+    m_colEnd = colEnd;
+}
+
+CodeNode::~CodeNode() {
+    qDeleteAll( children() );
+}
+
+CodeNode* CodeNode::topLevelParent() const {
+    CodeNode *node = const_cast<CodeNode*>( this );
+    while ( node->parent() )
+	node = node->parent();
+    return node;
+}
+
+bool CodeNode::isInRange( int lineNumber, int column ) const {
+    if ( lineNumber == m_line && lineNumber == endLine() ) {
+	// lineNumber is in a one line node
+	return column == -1 || (column >= m_col && column <= m_colEnd);
+    } else if ( lineNumber == m_line ) {
+	// lineNumber is at the beginning of a multiline node
+	return column == -1 || column >= m_col;
+    } else if ( lineNumber == endLine() )  {
+	// lineNumber is at the end of a multiline node
+	return column == -1 || column <= m_colEnd;
+    } else if ( lineNumber >= m_line && lineNumber <= endLine() ) {
+	// lineNumber is inside a multiline node
+	return true;
+    } else {
+	return false;
+    }
+}
+
+CodeNode* CodeNode::childFromPosition( int lineNumber, int column ) const {
+    QList<CodeNode*> childList = children();
+    foreach ( CodeNode *child, childList ) {
+	if ( child->isInRange(lineNumber, column) )
+	    return child->childFromPosition( lineNumber, column );
+    }
+
+    if ( isInRange(lineNumber, column) )
+	return const_cast<CodeNode*>( this );
+    else
+	return NULL;
+}
+
+QString JavaScriptParser::Token::whitespacesBetween( const JavaScriptParser::Token* \
token1, +						     const JavaScriptParser::Token* token2 ) {
+    QString whitespaces;
+    int newLines = token2->line - token1->line;
+    if ( newLines > 0 ) {
+	while ( --newLines > 0 )
+	    whitespaces += '\n';
+// 	for ( int i = 0; i < token2->posStart; ++i )
+// 	    whitespaces += ' ';
+	return whitespaces;
+    }
+
+    int spaces = token2->posStart - token1->posEnd;
+    while ( --spaces > 0 )
+	whitespaces += ' ';
+    return whitespaces;
+}
+
+bool JavaScriptParser::tryMoveToNextToken() {
+    moveToNextToken();
+    if ( atEnd() ) {
+		setErrorState( i18nc("@info/plain", "Unexpected end of file."),
+				m_lastToken->line, m_lastToken->posEnd );
+		return false;
+    } else {
+		return true;
+    }
+}
+
+void JavaScriptParser::moveToNextToken() {
+    m_lastToken = currentToken();
+    ++m_it;
+}
+
+CommentNode* JavaScriptParser::parseComment() {
+    if ( atEnd() ) {
+		return NULL;
+	}
+    Token *curToken = currentToken();
+    if ( !curToken->isChar('/') || !tryMoveToNextToken() ) {
+		return NULL;
+	}
+
+    QString text;
+    if ( Token::whitespacesBetween(curToken, currentToken()).length() == 0 ) {
+		if ( currentToken()->isChar('/') ) {
+			for ( moveToNextToken(); !atEnd() && currentToken()->line == curToken->line;
+				moveToNextToken() ) {
+			text += whitespaceSinceLastToken();
+			text += currentToken()->text;
+			}
+			return new CommentNode( text.trimmed(), curToken->line, curToken->posStart,
+						m_lastToken->line, m_lastToken->posEnd );
+		} else if ( currentToken()->isChar('*') ) {
+			// TODO: Remove '*' from the beginning of comment lines for [text]
+			moveToNextToken();
+			while ( !atEnd() ) {
+			Token *nextCommentToken = currentToken();
+			if ( nextCommentToken->isChar('*') ) {
+				moveToNextToken();
+				if ( atEnd() ) {
+				text += Token::whitespacesBetween( m_lastToken, nextCommentToken );
+				text += '*';
+				setErrorState( i18nc("@info/plain", "Unclosed multiline comment"),
+						nextCommentToken->line, nextCommentToken->posEnd );
+				break;
+				} else if ( currentToken()->isChar('/') ) {
+				moveToNextToken();
+				return new CommentNode( text.trimmed(), curToken->line, curToken->posStart,
+							m_lastToken->line, m_lastToken->posEnd );
+				} else {
+				text += Token::whitespacesBetween( m_lastToken, nextCommentToken );
+				text += '*';
+				}
+			} else {
+				text += Token::whitespacesBetween( m_lastToken, currentToken() );
+				text += currentToken()->text;
+				moveToNextToken();
+			}
+			} // while ( it != token.constEnd() )
+		} else {
+			--m_it; // go back to the first '/' that wasn't part of a comment, ie. '//' or \
'/*' +		}
+    } else {
+		--m_it; // go back to the first '/' that wasn't part of a comment, ie. '//' or \
'/*' +    }
+
+    // No comment found
+    return NULL;
+}
+
+BracketedNode* JavaScriptParser::parseBracketed() {
+    if ( atEnd() ) {
+		return NULL;
+	}
+    Token *beginToken = currentToken();
+    m_lastToken = beginToken;
+    // '{' is catched by parseBlock
+    if ( !beginToken->isChar('(') && !beginToken->isChar('[') ) {
+		return NULL;
+	}
+
+    // Get the end character (closing bracket)
+    QChar endChar = beginToken->text.at( 0 ) == '(' ? ')' : ']';
+
+    Token *lastCommaToken = beginToken;
+    moveToNextToken();
+    QList<CodeNode*> children;
+    QString text, textSinceLastComma;
+    while ( !atEnd() ) {
+		CodeNode *node = NULL;
+		text += Token::whitespacesBetween( m_lastToken, currentToken() );
+		// Check for end character
+		if ( currentToken()->isChar(endChar) ) {
+			if ( !textSinceLastComma.isEmpty() ) {
+			children << new UnknownNode( textSinceLastComma, lastCommaToken->line,
+							lastCommaToken->posStart, m_lastToken->posEnd );
+			}
+			moveToNextToken();
+			return new BracketedNode( beginToken->text.at(0), text,
+						beginToken->line, beginToken->posStart,
+						m_lastToken->line, m_lastToken->posEnd, children );
+		} else if ( currentToken()->isChar('}') ) {
+			setErrorState( i18nc("@info/plain", "Unclosed bracket, expected '%1'.", endChar),
+				beginToken->line, beginToken->posEnd );
+			return NULL;
+		} else if ( (node = parseComment()) || (node = parseString()) || (node = \
parseBracketed()) +			|| (node = parseBlock()) || (node = parseFunction()) ) 
+		{
+			text += node->toString();
+			children << node;
+		} else if ( !atEnd() ) { // Could be at the end after the last else block \
(parse...) +			text += currentToken()->text;
+			if ( currentToken()->isChar(',') ) {
+				lastCommaToken = currentToken();
+				textSinceLastComma.clear();
+				children << new UnknownNode( ",", currentToken()->line,
+								currentToken()->posStart, currentToken()->posEnd );
+			} else {
+				textSinceLastComma += currentToken()->text;
+				children << new UnknownNode( textSinceLastComma, lastCommaToken->line,
+								lastCommaToken->posStart, currentToken()->posEnd );
+			}
+			moveToNextToken();
+		}
+    }
+
+    setErrorState( i18nc("@info/plain", "Unclosed bracket, expected '%1'.", \
endChar), +		   beginToken->line, beginToken->posEnd );
+    return NULL;
+}
+
+StringNode* JavaScriptParser::parseString() {
+    if ( atEnd() )
+	return NULL;
+    Token *beginToken = currentToken();
+    m_lastToken = beginToken;
+    bool isRegExp = false; // TODO match /.../ig.. at the end of the reg exp
+    if ( !beginToken->isChar('\"') && !beginToken->isChar('\'') ) {
+	if ( !beginToken->isChar('/') )
+	    return NULL; // Not a reg exp and not a string
+
+	// Get the previous token
+	if ( m_it != m_token.constBegin() ) {
+	    --m_it;
+	    Token *prevToken = currentToken();
+	    ++m_it;
+
+	    QString allowed("=(:?"); // If one of these comes before '/', it's the start of \
a reg exp +	    if ( prevToken->text.length() == 1 && \
allowed.contains(prevToken->text) ) { +		isRegExp = true;
+	    } else {
+		// Not a reg exp and not a string
+		return NULL;
+	    }
+	} else {
+	    return NULL;
+	}
+    }
+    // The end character is the same as the beginning character (", ' or / for reg \
exps) +    QChar endChar = beginToken->text.at( 0 );
+
+    moveToNextToken();
+    QString text;
+    while ( !atEnd() ) {
+	text += whitespaceSinceLastToken();
+	// Check for non-escaped end character
+	if ( currentToken()->isChar(endChar) && !m_lastToken->isChar('\\') ) {
+	    int columnEnd = currentToken()->posEnd;
+	    moveToNextToken();
+	    return new StringNode( text, beginToken->line, beginToken->posStart, columnEnd \
); +	} else if ( (*m_it)->line != beginToken->line ) {
+	    if ( endChar == '/' ) {
+		setErrorState( i18nc("@info/plain", "Unclosed regular expression, "
+				     "missing %1 at end.", endChar),
+			       m_lastToken->line, m_lastToken->posEnd );
+	    } else {
+		setErrorState( i18nc("@info/plain", "Unclosed string, "
+				     "missing %1 at end.", endChar),
+			       m_lastToken->line, m_lastToken->posEnd );
+	    }
+	    return NULL;
+	} else {
+	    text += currentToken()->text;
+	    moveToNextToken();
+	}
+    }
+
+    setErrorState( i18nc("@info/plain", "Unexpected end of file."),
+		   m_lastToken->line, m_lastToken->posEnd );
+    return NULL;
+}
+
+void JavaScriptParser::checkFunctionCall( const QString &object, const QString \
&function, +					  BracketedNode *bracketedNode, int line, int column ) {
+    if ( object == "timetableData" ) {
+		if ( function != "clear" && function != "set" ) {
+			setErrorState( i18nc("@info/plain", "The object '%1' has no function '%2'.",
+					object, function), line, column );
+		} else if ( function == "clear" ) {
+			if ( !bracketedNode->content().isEmpty() ) {
+			setErrorState( i18nc("@info/plain", "The function '%1.%2()' accepts no \
arguments.", +						object, function),
+					bracketedNode->line(), bracketedNode->column() );
+			}
+		} else if ( function == "set" ) {
+			if ( bracketedNode->commaSeparatedCount() != 2 ) {
+			setErrorState( i18nc("@info/plain", "The function timetableData.set() expects "
+						"two arguments."),
+					bracketedNode->line(), bracketedNode->column() );
+			} else {
+				StringNode *string = dynamic_cast<StringNode*>(
+				bracketedNode->commaSeparated(0).first() );
+				if ( !string ) {
+					// TODO the first child node isn't always the first argument, unknown text gets \
no node +					setErrorState( i18nc("@info/plain", "The first argument of \
timetableData.set() " +							"must be a string."), line, column );
+				} else {
+					QStringList timetableInfoStrings;
+					timetableInfoStrings << "DepartureDate" << "DepartureHour" << "DepartureMinute"
+					<< "TypeOfVehicle" << "TransportLine" << "FlightNumber" << "Target"
+					<< "Platform" << "Delay" << "DelayReason" << "JourneyNews"
+					<< "JourneyNewsOther" << "JourneyNewsLink" << "DepartureHourPrognosis"
+					<< "DepartureMinutePrognosis" << "Operator" << "DepartureAMorPM"
+					<< "DepartureAMorPMPrognosis" << "ArrivalAMorPM" << "Status"
+					<< "DepartureYear" << "RouteStops" << "RouteTimes" << "RouteTimesDeparture"
+					<< "RouteTimesArrival" << "RouteExactStops" << "RouteTypesOfVehicles"
+					<< "RouteTransportLines" << "RoutePlatformsDeparture" << "IsNightLine"
+					<< "RoutePlatformsArrival" << "RouteTimesDepartureDelay"
+					<< "RouteTimesArrivalDelay" << "Duration" << "StartStopName"
+					<< "StartStopID" << "TargetStopName" << "TargetStopID" << "ArrivalDate"
+					<< "ArrivalHour" << "ArrivalMinute" << "Changes"
+					<< "TypesOfVehicleInJourney" << "Pricing" << "StopName" << "StopID"
+					<< "StopWeight";
+
+					if ( !timetableInfoStrings.contains(string->content()) ) {
+						setErrorState( i18nc("@info/plain", "'%1' isn't a valid info name.",
+											string->content()), string->line(), string->column() );
+					}
+				}
+			}
+		}
+	} else if ( object == "result" ) {
+		if ( function != "addData" ) {
+			setErrorState( i18nc("@info/plain", "The object '%1' has no function '%2'.",
+					object, function), line, column );
+		} else if ( bracketedNode->commaSeparatedCount() != 1 ) {
+			setErrorState( i18nc("@info/plain", "The function %1.%2() expects one argument.",
+					object, function),
+				bracketedNode->line(), bracketedNode->column() );
+		} else {
+			CodeNode *node = bracketedNode->commaSeparated(0).first();
+			if ( node->text() != "timetableData" ) {
+			setErrorState( i18nc("@info/plain", "The argument of the function %1.%2() "
+						"must be 'timetableData'.", object, function),
+					node->line(), node->column() );
+			}
+		}
+    } else if ( object == "helper" ) {
+		if ( function != "addMinsToTime" && function != "addDaysToDate" && function != \
"duration"  +			&& function != "extractBlock" && function != "formatTime" && function \
!= "matchTime"  +			&& function != "matchDate" && function != "splitSkipEmptyParts" 
+			&& function != "stripTags" && function != "trim" && function != "error" ) 
+		{
+			setErrorState( i18nc("@info/plain", "The object '%1' has no function '%2'.",
+					object, function), line, column );
+		}
+    }
+}
+
+StatementNode* JavaScriptParser::parseStatement() {
+    if ( atEnd() ) {
+		return NULL;
+	}
+    Token *firstToken = currentToken();
+    m_lastToken = firstToken;
+
+    QString text;
+    QList<Token*> lastTokenList;
+    QList<CodeNode*> children;
+    while ( !atEnd() ) {
+		if ( currentToken()->isChar(';') ) {
+			// End of statement found
+			if ( !lastTokenList.isEmpty() ) {
+				text += Token::whitespacesBetween( lastTokenList.last(), currentToken() );
+			}
+			text += currentToken()->text;
+			moveToNextToken();
+			return new StatementNode( text, firstToken->line, firstToken->posStart,
+						m_lastToken->line, m_lastToken->posEnd, children );
+		} else if ( currentToken()->isChar('}') ) {
+			// '}' without previous '{' found in statement => ';' is missing
+			Token *errorToken = lastTokenList.isEmpty() ? currentToken() : \
lastTokenList.last(); +			setErrorState( i18nc("@info/plain", "Missing ';' at the end \
of the statement."), +				errorToken->line, errorToken->posEnd );
+			m_lastToken = errorToken;
+			if ( m_it != m_token.constBegin() ) {
+				--m_it; // Go before the '}'
+			}
+			return NULL;
+		}
+
+		CodeNode *node = NULL;
+		if ( (node = parseComment()) || (node = parseString()) ) {
+			text += node->toString();
+			children << node;
+			lastTokenList << m_lastToken;
+		} else if ( (node = parseBracketed()) ) {
+			text += node->toString();
+			BracketedNode *bracketedNode = static_cast<BracketedNode*>( node );
+			if ( bracketedNode->openingBracketChar() != '('
+			|| lastTokenList.count() != 3 || !lastTokenList.at(0)->isName
+			|| !lastTokenList.at(1)->isChar('.') || !lastTokenList.at(2)->isName )
+			{
+			children << node;
+			} else {
+			// Is a member function call "object.function(...)"
+			Token *objectToken = lastTokenList.at(0);
+			Token *pointToken = lastTokenList.at(1);
+			Token *functionToken = lastTokenList.at(2);
+
+			QString object = objectToken->text;
+			QString function = functionToken->text;
+			checkFunctionCall( object, function, bracketedNode,
+					objectToken->line, objectToken->posStart );
+			// TODO This UnknownNode could get an ObjectNode or VariableNode
+			CodeNode *objectNode = new UnknownNode( object + ".",
+				objectToken->line, objectToken->posStart, pointToken->posEnd );
+			children << objectNode;
+			node = new FunctionCallNode( object, function,
+							functionToken->line, functionToken->posStart,
+							currentToken()->posStart, bracketedNode );
+			children << node;
+			}
+			lastTokenList << m_lastToken;
+		} else if ( (node = parseBlock()) || (node = parseFunction()) ) {
+			text += node->toString();
+			children << node;
+			lastTokenList << m_lastToken;
+
+			if ( !atEnd() && currentToken()->isChar(';') ) {
+			lastTokenList << currentToken();
+			text += ';';
+			moveToNextToken();
+			}
+
+			return new StatementNode( text, firstToken->line, firstToken->posStart,
+						lastTokenList.last()->line, lastTokenList.last()->posEnd, children );
+		} else if ( atEnd() ) {
+			setErrorState( i18nc("@info/plain", "Unexpected end of file.") );
+		} else {
+			if ( !lastTokenList.isEmpty() )
+			text += Token::whitespacesBetween( lastTokenList.last(), currentToken() );
+			text += currentToken()->text;
+			lastTokenList << currentToken();
+			moveToNextToken();
+		}
+    }
+
+    m_lastToken = lastTokenList.last();
+    setErrorState( i18nc("@info/plain", "Unexpected end of file."),
+		   lastTokenList.last()->line, lastTokenList.last()->posEnd );
+    return NULL;
+}
+
+FunctionNode* JavaScriptParser::parseFunction() {
+    if ( atEnd() )
+	return NULL;
+    m_lastToken = currentToken();
+    if ( atEnd() )
+	return NULL;
+
+    Token *firstToken = currentToken();
+    if ( firstToken->text != "function" || !tryMoveToNextToken() )
+	return NULL;
+
+    // Parse function name if any
+    QString name;
+    if ( !currentToken()->isChar('(') ) {
+	name = currentToken()->text;
+	if ( !tryMoveToNextToken() )
+	    return NULL;
+    }
+
+    // Parse arguments
+    if ( currentToken()->isChar('(') ) {
+	QList< ArgumentNode* > arguments;
+	bool argumentNameExpected = true;
+	bool isComma = false;
+	if ( !tryMoveToNextToken() )
+	    return NULL;
+
+	// Parse until a ')' is read or EOF
+	while ( !atEnd() && !currentToken()->isChar(')') ) {
+	    isComma = currentToken()->isChar(',');
+	    if ( argumentNameExpected ) {
+		if ( isComma ) {
+		    setErrorState( i18nc("@info/plain", "Excepted argument or ')'."),
+				   currentToken()->line, currentToken()->posStart );
+		    break;
+		}
+
+		arguments << new ArgumentNode( currentToken()->text, currentToken()->line,
+					       currentToken()->posStart, currentToken()->posEnd );
+	    } else if ( !isComma ) {
+		setErrorState( i18nc("@info/plain", "Excepted ',' or ')'."),
+			       currentToken()->line, currentToken()->posStart );
+		break;
+	    }
+
+	    argumentNameExpected = !argumentNameExpected;
+	    if ( !tryMoveToNextToken() )
+		return NULL;
+	}
+
+	if ( isComma ) { // argument list ended with ','
+	    setErrorState( i18nc("@info/plain", "Excepted argument or ')'."),
+			    m_lastToken->line, m_lastToken->posStart );
+	}
+
+	// Read definition block
+	BlockNode *definition = NULL;
+	if ( !tryMoveToNextToken() )
+	    return NULL;
+	if ( /*tryMoveToNextToken() &&*/ !(definition = parseBlock()) ) {
+	    setErrorState( i18nc("@info/plain", "Function definition is missing."),
+			   m_lastToken->line, m_lastToken->posStart );
+	}
+
+	return new FunctionNode( name, firstToken->line, firstToken->posStart, \
m_lastToken->posEnd, +				 arguments, definition );
+    } else {
+	setErrorState( i18nc("@info/plain", "Expected '('."),
+		       currentToken()->line, currentToken()->posStart );
+	moveToNextToken();
+
+	return NULL;
+    }
+}
+
+BlockNode* JavaScriptParser::parseBlock() {
+    if ( atEnd() )
+	return NULL;
+
+    if ( currentToken()->isChar('{') ) {
+	Token *firstToken = currentToken();
+	QList<CodeNode*> children;
+// 	moveToNextToken();
+	if ( !tryMoveToNextToken() )
+	    return NULL;
+	while ( !atEnd() ) {
+	    CodeNode *node = NULL;
+	    if ( (node = parseComment())
+		    || (!m_hasError && (node = parseString()))
+		    || (!m_hasError && (node = parseBracketed()))
+		    || (!m_hasError && (node = parseFunction()))
+		    || (!m_hasError && (node = parseBlock())) ) {
+		children << node;
+	    } else if ( !atEnd() && currentToken()->isChar('}') ) {
+		moveToNextToken(); // Move to next token, ie. first token after the function \
definition (or EOF) +		return new BlockNode( firstToken->line, firstToken->posEnd,
+				      m_lastToken->line, m_lastToken->posEnd, children );
+	    } else if ( (node = parseStatement()) ) {
+		children << node;
+	    } else if ( !atEnd() ) {
+		moveToNextToken();
+	    }
+	}
+
+	setErrorState( i18nc("@info/plain",
+			    "Unclosed block, missing '}'. Block started at line %1.", firstToken->line),
+		    m_lastToken->line, m_lastToken->posEnd );
+    }
+
+    return NULL;
+}
+
+QList< CodeNode* > JavaScriptParser::parse() {
+    clearError();
+
+    // Get token from the code, with line number and column begin/end
+    m_token.clear();
+    QString alpha( "abcdefghijklmnopqrstuvwxyz" );
+    QRegExp rxTokenBegin( "[^\\s]" );
+    QRegExp rxTokenEnd( "\\s|[-=#!$%&~;:,<>^` \
´/\\.\\+\\*\\\\\\(\\)\\{\\}\\[\\]'\"\\?\\|]" ); +    QStringList lines2 = \
m_code.split( '\n' ); +    for ( int lineNr = 0; lineNr < lines2.count(); ++lineNr ) \
{ +		QString line = lines2.at( lineNr );
+		QStringList words;
+
+		int posStart = 0;
+		while ( (posStart = rxTokenBegin.indexIn(line, posStart)) != -1 ) {
+			int posEnd;
+			bool isName;
+			// Only words beginning with a letter ([a-z]) may be concatenated
+			if ( !alpha.contains(line.at(posStart).toLower()) ) {
+				posEnd = posStart + 1;
+				isName = false;
+			} else {
+				posEnd = rxTokenEnd.indexIn( line, posStart + 1 );
+				isName = true;
+			}
+			if ( posEnd == -1 ) {
+				posEnd = line.length();
+			}
+
+			QString word;
+			word = line.mid( posStart, posEnd - posStart );
+			if ( !word.isEmpty() ) {
+				m_token << new Token( word, lineNr + 1, posStart, posEnd - 1, isName );
+			}
+
+			// Set minimal next start position
+			posStart = posEnd;
+		}
+    }
+
+    // Get nodes from the token
+    QList< CodeNode* > nodes;
+    m_it = m_token.constBegin();
+    while ( !atEnd() ) {
+		CodeNode *node = NULL;
+		if ( (node = parseComment())
+			|| (!m_hasError && (node = parseString()))
+			|| (!m_hasError && (node = parseBracketed()))
+			|| (!m_hasError && (node = parseFunction()))
+			|| (!m_hasError && (node = parseBlock()))
+			|| (!m_hasError && (node = parseStatement())) ) 
+		{
+			nodes << node;
+
+			if ( m_hasError ) {
+				break;
+			}
+		} else if ( !atEnd() ) {
+			moveToNextToken();
+		}
+    } // for ( ...all tokens... )
+
+    // Done with the tokens, delete them
+    qDeleteAll( m_token );
+    m_token.clear();
+
+    // Check for multiple definitions
+    QHash<QString, FunctionNode*> functions;
+    foreach ( CodeNode *node, nodes ) {
+	FunctionNode *function = dynamic_cast<FunctionNode*>( node );
+		if ( function ) {
+			QString newFunctionName = function->toString( true );
+			if ( functions.contains(newFunctionName) ) {
+				FunctionNode *previousImpl = functions.value( newFunctionName );
+				setErrorState( i18nc("@info/plain", "Multiple definitions of function '%1', "
+									 "previously defined at line %2",
+									 function->text(), previousImpl->line()),
+							   function->line(), function->column(), previousImpl->line() );
+			} else {
+				functions.insert( newFunctionName, function );
+			}
+		}
+    }
+
+    return nodes;
+}
+
+KTextEditor::Cursor JavaScriptParser::errorCursor() const {
+    return KTextEditor::Cursor( m_errorLine - 1, m_errorColumn );
+}
+
+void JavaScriptParser::setErrorState( const QString& errorMessage, int errorLine,
+				      int errorColumn, int affectedLine ) {
+    if ( m_hasError )
+	return; // Don't override existing errors
+
+    m_hasError = true;
+    m_errorLine = errorLine;
+    m_errorAffectedLine = affectedLine;
+
+    m_errorColumn = errorColumn;
+    m_errorMessage = errorMessage;
+}
+
+void JavaScriptParser::clearError() {
+    m_hasError = false;
+    m_errorLine = -1;
+    m_errorAffectedLine = -1;
+    m_errorColumn = 0;
+    m_errorMessage.clear();
+}
+
+MultilineNode::MultilineNode( const QString& text, int line, int colStart,
+			      int lineEnd, int colEnd )
+			    : CodeNode( text, line, colStart, colEnd ) {
+    m_endLine = lineEnd;
+}
+
+ChildListNode::ChildListNode( const QString& text, int line, int colStart,
+			      int lineEnd, int colEnd, const QList< CodeNode* > &children )
+		: MultilineNode( text, line, colStart, lineEnd, colEnd ) {
+    m_children = children;
+    foreach ( CodeNode *child, children ) {
+	child->m_parent = this;
+    }
+}
+
+EmptyNode::EmptyNode() : CodeNode( QString(), -1, 0, 0 ) {
+}
+
+StatementNode::StatementNode( const QString &text, int line, int colStart, int \
lineEnd, int colEnd, +			      const QList<CodeNode*> &children )
+		: ChildListNode( text, line, colStart, lineEnd, colEnd, children ) {
+}
+
+BracketedNode::BracketedNode( const QChar &openingBracketChar, const QString &text,
+			      int line, int colStart, int lineEnd, int colEnd,
+			      const QList<CodeNode*> &children )
+		: ChildListNode( text, line, colStart, lineEnd, colEnd, children ) {
+    m_bracketChar = openingBracketChar;
+}
+
+QChar BracketedNode::closingBracketChar() const {
+    if ( m_bracketChar == '(' )
+	return ')';
+    else if ( m_bracketChar == '[' )
+	return ']';
+    else
+	return ' '; // should not happen
+}
+
+int BracketedNode::commaSeparatedCount() const {
+    int count = 1;
+    foreach ( CodeNode *child, m_children ) {
+	if ( child->type() == Unknown && child->text() == "," )
+	    ++count;
+    }
+    return count;
+}
+
+QList< CodeNode* > BracketedNode::commaSeparated( int pos ) const {
+    QList<CodeNode*> separated;
+
+    int curPos = 0;
+    foreach ( CodeNode *child, m_children ) {
+	if ( child->type() == Unknown && child->text() == "," ) {
+	    ++curPos;
+	    if ( curPos > pos )
+		return separated;
+	}
+
+	if ( curPos == pos )
+	    separated << child;
+    }
+
+    return separated;
+}
+
+FunctionCallNode::FunctionCallNode( const QString &object, const QString &function,
+				    int line, int colStart, int colEnd, BracketedNode *arguments )
+		    : CodeNode( object + '.' + function, line, colStart, colEnd ) {
+    m_object = object;
+    m_function = function;
+    m_arguments = arguments;
+}
+
+FunctionNode::FunctionNode( const QString& text, int line, int colStart, int colEnd,
+			    const QList< ArgumentNode* > &arguments, BlockNode *definition )
+		: MultilineNode( text, line, colStart,
+				 definition ? definition->endLine() : line, colEnd ) {
+    if ( m_text.isEmpty() ) {
+	m_text = i18nc("@info/plain Display name for anonymous JavaScript functions",
+		       "[anonymous]");
+    }
+    m_arguments = arguments;
+    m_definition = definition;
+
+    foreach ( CodeNode *child, arguments ) {
+	child->m_parent = this;
+    }
+    if ( definition )
+	definition->m_parent = this;
+}
+
+QList< CodeNode* > FunctionNode::children() const {
+    QList<CodeNode*> ret;
+    for ( QList<ArgumentNode*>::const_iterator it = m_arguments.constBegin();
+	    it != m_arguments.constEnd(); ++it ) {
+	ret << *it;
+    }
+    ret << m_definition;
+    return ret;
+}
+
+BlockNode::BlockNode( int line, int colStart, int lineEnd, int colEnd,
+		      const QList<CodeNode*> &children )
+		: ChildListNode( QString(), line, colStart, lineEnd, colEnd, children ) {
+}
+
+QString BlockNode::toString( bool shortString ) const {
+    QString s( '{' );
+    foreach ( CodeNode *child, m_children )
+	s += child->toString(shortString) + '\n';
+    return s + '}';
+}
+
+QString FunctionNode::id() const {
+    QStringList arguments;
+    foreach ( CodeNode *node, m_arguments ) {
+	ArgumentNode *argument = dynamic_cast<ArgumentNode*>( node );
+	if ( argument ) {
+	    arguments << argument->text();
+	}
+    }
+
+    return QString("func:%1(%2)").arg( m_text, arguments.join(",") );
+}
+
+QString FunctionNode::toStringSignature() const {
+    QStringList arguments;
+    foreach ( CodeNode *node, m_arguments ) {
+	ArgumentNode *argument = dynamic_cast<ArgumentNode*>( node );
+	if ( argument ) {
+	    arguments << argument->text();
+	}
+    }
+
+    return QString("%1( %2 )").arg( m_text
+, arguments.join(", ") );
+}
+
+QString FunctionNode::toString( bool shortString ) const {
+    if ( shortString )
+	return toStringSignature();
+    else
+	return toStringSignature() + ' ' + (m_definition ? m_definition->toString() : \
QString()); +}
diff --git a/timetablemate/src/main.cpp b/timetablemate/src/main.cpp
new file mode 100644
index 0000000..08cd121
--- /dev/null
+++ b/timetablemate/src/main.cpp
@@ -0,0 +1,67 @@
+/*
+*   Copyright 2010 Friedrich Pülz <fpuelz@gmx.de>
+*
+*   This program is free software; you can redistribute it and/or modify
+*   it under the terms of the GNU Library General Public License as
+*   published by the Free Software Foundation; either version 2 or
+*   (at your option) any later version.
+*
+*   This program is distributed in the hope that it will be useful,
+*   but WITHOUT ANY WARRANTY; without even the implied warranty of
+*   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+*   GNU General Public License for more details
+*
+*   You should have received a copy of the GNU Library General Public
+*   License along with this program; if not, write to the
+*   Free Software Foundation, Inc.,
+*   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+
+#include "timetablemate.h"
+#include <kapplication.h>
+#include <kaboutdata.h>
+#include <kcmdlineargs.h>
+#include <KDE/KLocale>
+
+static const char description[] =
+    I18N_NOOP("A helper application to add support for new service providers to "
+	      "the plasma data engine 'PublicTransport'");
+
+static const char version[] = "0.2.2";
+
+int main(int argc, char **argv)
+{
+    KAboutData about("timetablemate", 0, ki18n("TimetableMate"), version, \
ki18n(description), +                     KAboutData::License_GPL_V2, ki18n("(C) 2010 \
Friedrich Pülz"), +		     KLocalizedString(), 0, "fpuelz@gmx.de");
+    about.addAuthor( ki18n("Friedrich Pülz"), KLocalizedString(), "fpuelz@gmx.de" \
); +    KCmdLineArgs::init(argc, argv, &about);
+
+    KCmdLineOptions options;
+    options.add("+[URL]", ki18n( "Document to open" ));
+    KCmdLineArgs::addCmdLineOptions(options);
+    KApplication app;
+
+//     TimetableMate *widget = new TimetableMate;
+
+    // see if we are starting with session management
+    if (app.isSessionRestored()) {
+        RESTORE(TimetableMate);
+    } else {
+        // no session.. just start up normally
+        KCmdLineArgs *args = KCmdLineArgs::parsedArgs();
+        if (args->count() == 0) {
+            TimetableMate *widget = new TimetableMate;
+            widget->show();
+        } else {
+            for ( int i = 0; i < args->count(); i++ ) {
+                TimetableMate *widget = new TimetableMate;
+		widget->open( args->url(i) );
+                widget->show();
+            }
+        }
+        args->clear();
+    }
+
+    return app.exec();
+}
diff --git a/timetablemate/src/publictransportpreview.cpp \
b/timetablemate/src/publictransportpreview.cpp new file mode 100644
index 0000000..be5757f
--- /dev/null
+++ b/timetablemate/src/publictransportpreview.cpp
@@ -0,0 +1,152 @@
+/*
+*   Copyright 2010 Friedrich Pülz <fpuelz@gmx.de>
+*
+*   This program is free software; you can redistribute it and/or modify
+*   it under the terms of the GNU Library General Public License as
+*   published by the Free Software Foundation; either version 2 or
+*   (at your option) any later version.
+*
+*   This program is distributed in the hope that it will be useful,
+*   but WITHOUT ANY WARRANTY; without even the implied warranty of
+*   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+*   GNU General Public License for more details
+*
+*   You should have received a copy of the GNU Library General Public
+*   License along with this program; if not, write to the
+*   Free Software Foundation, Inc.,
+*   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+
+#include "publictransportpreview.h"
+
+#include <Plasma/Corona>
+#include <Plasma/DataEngine>
+#include <KPushButton>
+#include <KMessageBox>
+
+#include <QMetaClassInfo>
+#include <QGraphicsProxyWidget>
+#include <QGraphicsLinearLayout>
+#include <KInputDialog>
+
+PublicTransportPreview::PublicTransportPreview( QWidget* parent )
+		: QGraphicsView( parent ), m_containment(0), m_applet(0) {
+    setVerticalScrollBarPolicy( Qt::ScrollBarAlwaysOff );
+    setHorizontalScrollBarPolicy( Qt::ScrollBarAlwaysOff );
+    loadNoPlasmaScene();
+}
+
+void PublicTransportPreview::loadNoPlasmaScene() {
+    QGraphicsScene *newScene = new QGraphicsScene( this );
+
+    QGraphicsWidget *item = new QGraphicsWidget;
+    QGraphicsLinearLayout *l = new QGraphicsLinearLayout( item );
+    item->setLayout( l );
+
+    // Create a KPushButton in a proxy widget
+    KPushButton *btnShowPlasmaPreview =
+	    new KPushButton( i18nc("@action:button", "Show &Plasma Preview") );
+    connect( btnShowPlasmaPreview, SIGNAL(clicked(bool)), this, \
SLOT(loadPlasmaPreview()) ); +    
+    l->addItem( newScene->addWidget(btnShowPlasmaPreview) );
+//     l->addItem( scene->addText(i18nc("@info", "<para><note>You need to install \
your accessor to " +// 				     "show a preview of it.</note></para>")) );
+    newScene->addItem( item );
+    newScene->setSceneRect( item->boundingRect() );
+    
+    setAlignment( Qt::AlignCenter );
+    setScene( newScene );
+    setSceneRect( item->boundingRect()  );
+    
+    if ( m_containment ) {
+	delete m_containment;
+	m_containment = NULL;
+    }
+}
+
+bool PublicTransportPreview::loadPlasmaPreview() {
+    if ( isPlasmaPreviewShown() )
+	return true;
+    
+    // Add the desktop containment
+    m_containment = m_corona.addContainment( "desktop" );
+    if ( !m_containment ) {
+	KMessageBox::information( this, i18nc("@info", "The plasma desktop containment \
couldn't " +					      "be added. Ensure that you have plasma installed.") );
+	return false;
+    }
+    QGraphicsScene *oldScene = scene();
+    setScene( m_containment->scene() );
+    setSceneRect( m_containment->geometry() );
+    oldScene->deleteLater();
+
+    // Add the PublicTransport applet
+    m_applet = m_containment->addApplet( "publictransport" );
+    if ( !m_applet ) {
+	delete m_containment;
+	m_containment = NULL;
+	KMessageBox::information( this, i18nc("@info", "The PublicTransport applet couldn't \
be " +					      "added. Ensure that you have it installed.") );
+	return false;
+    }
+    m_applet->setFlag( QGraphicsItem::ItemIsMovable, false );
+    setAlignment( Qt::AlignLeft | Qt::AlignTop );
+
+    emit plasmaPreviewLoaded();
+    return true;
+}
+
+void PublicTransportPreview::closePlasmaPreview() {
+    if ( !isPlasmaPreviewShown() )
+	return;
+
+    // Remove applet
+    m_containment->clearApplets();
+    delete m_applet;
+    m_applet = 0;
+
+    // Load other scene
+    loadNoPlasmaScene();
+
+    // Remove containment
+    delete m_containment;
+    m_containment = 0;
+}
+
+void PublicTransportPreview::setSettings( const QString& serviceProviderID,
+					  const QString& stopName ) {
+    if ( !m_applet )
+	return;
+    
+    // Set settings of the PublicTransport applet using a specific slot
+    int index = m_applet->metaObject()->indexOfSlot( "setSettings(QString,QString)" \
); +    if ( index == -1 ) {
+	kDebug() << "Couldn't find slot with signarture setSettings(QString,QString) "
+		    "in the publicTransport applet.";
+	return;
+    }
+
+    bool success = m_applet->metaObject()->method( index ).invoke( m_applet,
+		    Q_ARG(QString, serviceProviderID), Q_ARG(QString, stopName) );
+    if ( !success ) {
+	kDebug() << "A call to setSettings in the publicTransport applet wasn't \
successful."; +    }
+
+    if ( stopName.isEmpty() ) {
+	m_applet->showConfigurationInterface();
+    }
+}
+
+void PublicTransportPreview::resizeEvent( QResizeEvent* event ) {
+    setUpdatesEnabled( false );
+    QGraphicsView::resizeEvent( event );
+
+    if ( m_containment ) {
+	m_containment->setMaximumSize( QWIDGETSIZE_MAX, QWIDGETSIZE_MAX );
+	m_containment->setMinimumSize( size() );
+	m_containment->setMaximumSize( size() );
+	m_containment->resize( size() );
+    }
+    setUpdatesEnabled( true );
+}
+
diff --git a/timetablemate/src/scripting.cpp b/timetablemate/src/scripting.cpp
new file mode 100644
index 0000000..6492b0b
--- /dev/null
+++ b/timetablemate/src/scripting.cpp
@@ -0,0 +1,70 @@
+/*
+*   Copyright 2010 Friedrich Pülz <fpuelz@gmx.de>
+*
+*   This program is free software; you can redistribute it and/or modify
+*   it under the terms of the GNU Library General Public License as
+*   published by the Free Software Foundation; either version 2 or
+*   (at your option) any later version.
+*
+*   This program is distributed in the hope that it will be useful,
+*   but WITHOUT ANY WARRANTY; without even the implied warranty of
+*   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+*   GNU General Public License for more details
+*
+*   You should have received a copy of the GNU Library General Public
+*   License along with this program; if not, write to the
+*   Free Software Foundation, Inc.,
+*   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+
+#include "scripting.h"
+
+void TimetableData::set( const QString &info, const QVariant& value ) {
+    static QStringList validDepartureStrings = QStringList()
+		<< "departuredate" << "departurehour" << "departureminute" << "typeofvehicle"
+		<< "transportline" << "flightnumber" << "target" << "platform" << "delay" << \
"delayreason" +		<< "journeynews" << "journeynewsother" << "journeynewslink" << \
"departurehourprognosis" +		<< "departureminuteprognosis" << "operator" << \
"departureamorpm" +		<< "departureamorpmprognosis" << "status" << "departureyear" << \
"routestops" +		<< "routetimes" << "routeexactstops" << "isnightline";
+    static QStringList validJourneyStrings = QStringList()
+		<< "departuredate" << "departurehour" << "departureminute" 
+		<< "duration" << "startstopname" << "startstopid" << "targetstopname" << \
"targetstopid" +		<< "arrivaldate" << "arrivalhour" << "arrivalminute" << "changes"
+		<< "typesofvehicleinjourney" << "pricing" << "routetransportlines" << \
"routetypesofvehicles" +		<< "routeplatformsdeparture" << "routeplatformsarrival" << \
"routetimesdeparturedelay" +		<< "routetimesarrivaldelay" << "routetimesdeparture" << \
"routetimesarrival" +		<< "routestops" << "journeynews" << "journeynewsother" << \
"journeynewslink" +		<< "departurehourprognosis" << "departureminuteprognosis" << \
"operator" << "departureamorpm" +		<< "departureamorpmprognosis" << "arrivalamorpm";
+    static QStringList validStopSuggestionStrings = QStringList()
+		<< "stopname" << "stopid" << "stopweight" << "stopcity" << "stopcountrycode";
+    
+    QString s = info.toLower();
+    if ( (m_mode == "departures" && !validDepartureStrings.contains(s))
+      || (m_mode == "journeys" && !validJourneyStrings.contains(s))
+      || (m_mode == "stopsuggestions" && !validStopSuggestionStrings.contains(s)) )
+    {
+		kDebug() << "Unknown timetable information" << s
+			<< "with value" << (value.isNull() ? "NULL" : value.toString());
+		m_unknownTimetableInformationStrings.insert( info, value );
+    } else if ( value.isNull() ) {
+		kDebug() << "Value is NULL for" << s;
+    } else {
+		// Valid data
+		if ( value.isValid() && value.canConvert(QVariant::String)
+			&& (s == "stopname" || s == "target"
+			|| s == "startStopName" || s == "targetstopname"
+			|| s == "operator" || s == "transportline"
+			|| s == "platform" || s == "delayreason"
+			|| s == "status" || s == "pricing") ) 
+		{
+			m_values[ s ] = /*TimetableAccessorScript::decodeHtmlEntities(*/ value.toString() \
/*)*/; // TODO +		} else if ( value.isValid() && value.canConvert(QVariant::List) && \
s == "departuredate" ) { +			QVariantList date = value.toList();
+			m_values[ s ] = date.length() == 3 ? QDate(date[0].toInt(), date[1].toInt(), \
date[2].toInt()) : value; +		} else {
+			m_values[ s ] = value;
+		}
+    }
+}
diff --git a/timetablemate/src/timetablemate.cpp \
b/timetablemate/src/timetablemate.cpp new file mode 100644
index 0000000..c00d675
--- /dev/null
+++ b/timetablemate/src/timetablemate.cpp
@@ -0,0 +1,2434 @@
+/*
+*   Copyright 2010 Friedrich Pülz <fpuelz@gmx.de>
+*
+*   This program is free software; you can redistribute it and/or modify
+*   it under the terms of the GNU Library General Public License as
+*   published by the Free Software Foundation; either version 2 or
+*   (at your option) any later version.
+*
+*   This program is distributed in the hope that it will be useful,
+*   but WITHOUT ANY WARRANTY; without even the implied warranty of
+*   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+*   GNU General Public License for more details
+*
+*   You should have received a copy of the GNU Library General Public
+*   License along with this program; if not, write to the
+*   Free Software Foundation, Inc.,
+*   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+
+#include "timetablemate.h"
+#include "timetablemateview.h"
+#include "settings.h"
+#include "publictransportpreview.h"
+#include "javascriptcompletionmodel.h"
+#include "javascriptmodel.h"
+#include "javascriptparser.h"
+#include "scripting.h"
+
+#include <QtGui/QDropEvent>
+#include <QtGui/QPainter>
+#include <QtGui/QPrinter>
+#include <QtGui/QSplitter>
+#include <QtGui/QToolTip>
+#include <QtGui/QSortFilterProxyModel>
+#include <QtWebKit/QWebFrame>
+#include <QtWebKit/QWebInspector>
+#include <QtCore/QBuffer>
+#include <QtCore/QTextCodec>
+#include <QtCore/QTimer>
+
+#include <KGlobalSettings>
+#include <KStandardDirs>
+#include <KDateTimeWidget>
+#include <KWebView>
+#include <KTabWidget>
+#include <KUrlComboBox>
+
+#include <KMenu>
+#include <KMenuBar>
+#include <KToolBar>
+#include <KStatusBar>
+
+#include <KConfigDialog>
+#include <KFileDialog>
+#include <KInputDialog>
+#include <KMessageBox>
+
+#include <KParts/PartManager>
+#include <KParts/MainWindow>
+#include <KWebKitPart>
+#include <KTextEditor/Document>
+#include <KTextEditor/View>
+#include <KTextEditor/CodeCompletionModel>
+#include <KTextEditor/CodeCompletionInterface>
+#include <KTextEditor/TemplateInterface>
+#include <KTextEditor/TextHintInterface>
+#include <KTextEditor/MarkInterface>
+#include <KLibFactory>
+#include <KLibLoader>
+
+#include <KAction>
+#include <KActionCollection>
+#include <KActionMenu>
+#include <KStandardAction>
+#include <KRecentFilesAction>
+
+#include <Kross/Action>
+#include <KAuth/Action>
+#include <KAuth/ActionReply>
+#include <KIO/NetAccess>
+#include <KDE/KLocale>
+
+TimetableMate::TimetableMate() : KParts::MainWindow( 0, \
Qt::WindowContextHelpButtonHint ), +        m_mainTabBar( new KTabWidget(this) ), \
m_view( new TimetableMateView(this) ), +        m_backgroundParserTimer(0) {
+    m_partManager = new KParts::PartManager( this );
+    m_accessorDocumentChanged = false;
+    m_accessorWidgetsChanged = false;
+    m_changed = false;
+    m_currentTab = AccessorTab;
+    m_mainTabBar->setDocumentMode( true );
+
+    setCentralWidget( m_mainTabBar );
+
+    // Create plasma preview widget
+    m_preview = new PublicTransportPreview( this );
+    m_preview->setWhatsThis( i18nc("@info:whatsthis", "<subtitle>Plasma \
Preview</subtitle>" +                                   "<para>This is a preview of \
the PublicTransport applet in a plasma desktop. " +                                   \
"The applet's settings are changed so that it always uses the currently opened " +    \
"timetable accessor.</para>" +                                   "<para><note>You \
have to install the accessor to use it in this preview. " +                           \
"Use <interface>File -&gt; Install</interface> to install the accessor locally " +    \
"or <interface>File -&gt; Install Globally</interface> to install the accessor " +    \
"globally, ie. for all users.</note></para>") ); +    connect( m_preview, \
SIGNAL(plasmaPreviewLoaded()), this, SLOT(plasmaPreviewLoaded()) ); +
+    // Create web view widget
+    KService::Ptr service = KService::serviceByDesktopPath( "kwebkitpart.desktop" );
+    if ( service ) {
+        m_webview = static_cast<KWebKitPart*>(
+				service->createInstance<KParts::ReadOnlyPart>(m_mainTabBar, this) );
+    } else {
+        // TODO: The webkit part shouldn't be a requirement
+        KMessageBox::error(this, "service kwebkitpart.desktop not found");
+        qApp->quit();
+        return;
+    }
+    m_webview->view()->settings()->setAttribute( \
QWebSettings::DeveloperExtrasEnabled, true ); +    m_webview->view()->pageAction( \
QWebPage::OpenLinkInNewWindow )->setVisible( false ); +    \
m_webview->view()->pageAction( QWebPage::OpenFrameInNewWindow )->setVisible( false ); \
+    m_webview->view()->pageAction( QWebPage::OpenImageInNewWindow )->setVisible( \
false ); +    m_webview->view()->setMinimumHeight( 150 );
+    m_webview->view()->setWhatsThis( i18nc("@info:whatsthis", "<subtitle>Web \
View</subtitle>" +                                           "<para>This is the web \
view. You can use it to check the URLs you have defined " +                           \
"in the <interface>Accessor</interface> settings or to get information about the " +  \
"structure of the documents that get parsed by the script.</para>" +                  \
"<para><note>You can select a web element in the <emphasis>inspector</emphasis> " +   \
"using the context menu.</note></para>") ); +
+    // Create a web inspector
+    QWebInspector *inspector = new QWebInspector( this );
+    inspector->setPage( m_webview->view()->page() );
+    inspector->setMinimumHeight( 150 );
+
+    QSplitter *webSplitter = new QSplitter( this );
+    webSplitter->setOrientation( Qt::Vertical );
+    webSplitter->addWidget( m_webview->widget() );
+    webSplitter->addWidget( inspector );
+
+    m_urlBar = new KUrlComboBox( KUrlComboBox::Both, true, this );
+    connect( m_webview, SIGNAL(setStatusBarText(QString)), this, \
SLOT(slotSetStatusBarText(QString)) ); +    connect( m_webview->view(), \
SIGNAL(urlChanged(QUrl)), this, SLOT(webUrlChanged(QUrl)) ); +    connect( m_urlBar, \
SIGNAL(returnPressed(QString)), this, SLOT(urlBarReturn(QString)) ); +
+    QWidget *webWidget = new QWidget( this );
+    QVBoxLayout *l = new QVBoxLayout( webWidget );
+    l->addWidget( m_urlBar );
+    l->addWidget( webSplitter );
+
+    setupActions();
+
+    // Add a status bar
+    statusBar()->show();
+
+    // a call to KXmlGuiWindow::setupGUI() populates the GUI
+    // with actions, using KXMLGUI.
+    // It also applies the saved mainwindow settings, if any, and ask the
+    // mainwindow to automatically save settings if changed: window size,
+    // toolbar position, icon size, etc.
+    setupGUI();
+
+    connect( m_view, SIGNAL(scriptAdded(QString)), this, SLOT(showScriptTab()) );
+    connect( m_view, SIGNAL(urlShouldBeOpened(QString,RawUrl)),
+             this, SLOT(showWebTab(QString,RawUrl)) );
+    connect( m_view, SIGNAL(changed()), this, SLOT(accessorWidgetsChanged()) );
+    connect( m_view, SIGNAL(scriptFileChanged(QString)),
+             this, SLOT(scriptFileChanged(QString)) );
+
+    // When the manager says the active part changes,
+    // the builder updates (recreates) the GUI
+    connect( m_partManager, SIGNAL(activePartChanged(KParts::Part*)),
+             this, SLOT(activePartChanged(KParts::Part*)) );
+    connect( m_mainTabBar, SIGNAL(currentChanged(int)),
+             this, SLOT(currentTabChanged(int)) );
+
+    // Query the .desktop file to load the requested Part
+    QWidget *accessorSourceWidget = 0, *scriptWidget = 0;
+    JavaScriptCompletionModel *completionModel = NULL;
+    service = KService::serviceByDesktopPath( "katepart.desktop" );
+    if ( service ) {
+        m_accessorDocument = static_cast<KTextEditor::Document*>(
+                                 \
service->createInstance<KParts::ReadWritePart>(m_mainTabBar) ); +        \
m_scriptDocument = static_cast<KTextEditor::Document*>( +                             \
service->createInstance<KParts::ReadWritePart>(m_mainTabBar) ); +
+        if ( !m_accessorDocument || !m_scriptDocument )
+            return;
+        connect( m_accessorDocument, SIGNAL(setStatusBarText(QString)),
+                 this, SLOT(slotSetStatusBarText(QString)) );
+        connect( m_scriptDocument, SIGNAL(setStatusBarText(QString)),
+                 this, SLOT(slotSetStatusBarText(QString)) );
+
+        connect( m_accessorDocument, SIGNAL(textChanged(KTextEditor::Document*)),
+                 this, SLOT(accessorDocumentChanged(KTextEditor::Document*)));
+        connect( m_scriptDocument, SIGNAL(textChanged(KTextEditor::Document*)),
+                 this, SLOT(scriptDocumentChanged(KTextEditor::Document*)));
+
+        m_accessorDocument->setHighlightingMode( "XML" );
+        m_scriptDocument->setHighlightingMode( "JavaScript" );
+
+        accessorSourceWidget = m_accessorDocument->widget();
+        scriptWidget = m_scriptDocument->widget();
+
+        accessorSourceWidget->setWhatsThis( i18nc("@info:whatsthis",
+                                            "<subtitle>Accessor Source</subtitle>"
+                                            "<para>This shows the XML source of the \
accessor settings. Normally you won't need " +                                        \
"this, because you can setup everything in the <interface>Accessor</interface> " +    \
"settings.</para>" +                                            "<para><note>Changes \
to <interface>Accessor</interface> and " +                                            \
"<interface>Accessor Source</interface> are synchronized automatically. " +           \
"Comments and unknown content in the source is removed when synchronizing." +         \
"</note></para>") ); +        scriptWidget->setWhatsThis( i18nc("@info:whatsthis",
+                                          "<subtitle>Script File</subtitle>"
+                                          "<para>This shows the script source code. \
Syntax completion is available for all " +                                          \
"functions and strings used by the data engine.</para>" +                             \
"<para>To try out the script functions just click one of the " +                      \
"<interface>Run '<placeholder>function</placeholder>'</interface> buttons.</para>") \
); +
+        KTextEditor::CodeCompletionInterface *iface =
+            qobject_cast<KTextEditor::CodeCompletionInterface*>( \
m_scriptDocument->activeView() ); +        if ( iface ) {
+            // Get the completion shortcut string
+            QString completionShortcut;
+            if ( !m_accessorDocument->views().isEmpty() ) {
+                KTextEditor::View *view = m_accessorDocument->views().first();
+                QAction *completionAction = \
view->action("tools_invoke_code_completion"); +                if ( completionAction \
) { +                    completionShortcut = completionAction->shortcut().toString(
+                                             QKeySequence::NativeText );
+                }
+            }
+            if ( completionShortcut.isEmpty() ) {
+                completionShortcut = "unknown"; // Should not happen
+            }
+
+            completionModel = new JavaScriptCompletionModel( completionShortcut, \
this ); +            iface->registerCompletionModel( completionModel );
+        }
+
+        KTextEditor::View *accessorView = m_accessorDocument->views().first();
+        KTextEditor::View *scriptView = m_scriptDocument->views().first();
+        connect( accessorView, \
SIGNAL(informationMessage(KTextEditor::View*,QString)), +                 this, \
SLOT(informationMessage(KTextEditor::View*,QString)) ); +        connect( scriptView, \
SIGNAL(informationMessage(KTextEditor::View*,QString)), +                 this, \
SLOT(informationMessage(KTextEditor::View*,QString)) ); +    } else {
+        // if we couldn't find our Part, we exit since the Shell by
+        // itself can't do anything useful
+        KMessageBox::error(this, "service katepart.desktop not found");
+        qApp->quit();
+        // we return here, cause qApp->quit() only means "exit the
+        // next time we enter the event loop...
+        return;
+    }
+
+    // Add parts
+    m_partManager->addPart( m_accessorDocument, false );
+    m_partManager->addPart( m_scriptDocument, false );
+    m_partManager->addPart( m_webview, false );
+
+    // Create script widgets/models
+    m_functions = new KComboBox( this );
+    m_javaScriptModel = new JavaScriptModel( this );
+    m_functionsModel = new QSortFilterProxyModel( this );
+    m_functionsModel->setSourceModel( m_javaScriptModel );
+    m_functionsModel->setFilterRole( Qt::UserRole ); // TODO
+    m_functionsModel->setFilterFixedString( QString::number(Function) );
+    m_functions->setModel( m_functionsModel );
+    connect( m_javaScriptModel, SIGNAL(showTextHint(KTextEditor::Cursor,QString&)),
+             this, SLOT(showTextHint(KTextEditor::Cursor,QString&)) );
+
+    KTextEditor::TextHintInterface *iface =
+        qobject_cast<KTextEditor::TextHintInterface*>( \
m_scriptDocument->activeView() ); +    if ( iface ) {
+        iface->enableTextHints( 250 );
+        connect( m_scriptDocument->activeView(), \
SIGNAL(needTextHint(KTextEditor::Cursor,QString&)), +                 \
m_javaScriptModel, SLOT(needTextHint(KTextEditor::Cursor,QString&)) ); +        \
m_javaScriptModel->setJavaScriptCompletionModel( completionModel ); +    }
+
+    QWidget *scriptTab = new QWidget( this );
+    QVBoxLayout *layoutScript = new QVBoxLayout( scriptTab );
+    QToolButton *btnPreviousFunction = new QToolButton( scriptTab );
+    btnPreviousFunction->setDefaultAction( action("script_previous_function") );
+    QToolButton *btnNextFunction = new QToolButton( scriptTab );
+    btnNextFunction->setDefaultAction( action("script_next_function") );
+    QHBoxLayout *layoutScriptTop = new QHBoxLayout();
+    layoutScriptTop->setSpacing( 0 );
+    layoutScriptTop->addWidget( btnPreviousFunction );
+    layoutScriptTop->addWidget( btnNextFunction );
+    layoutScriptTop->addWidget( m_functions );
+
+    layoutScript->addLayout( layoutScriptTop );
+    layoutScript->addWidget( scriptWidget );
+    connect( m_functions, SIGNAL(currentIndexChanged(int)),
+             this, SLOT(currentFunctionChanged(int)) );
+    connect( m_scriptDocument->views().first(),
+             SIGNAL(cursorPositionChanged(KTextEditor::View*,KTextEditor::Cursor)),
+             this, SLOT(scriptCursorPositionChanged(KTextEditor::View*,KTextEditor::Cursor)) \
); +
+    // Add tabs
+    m_mainTabBar->addTab( m_view, KIcon("public-transport-stop"),
+                          i18nc("@title:tab", "&Accessor") );
+    m_mainTabBar->addTab( accessorSourceWidget, KIcon("text-xml"),
+                          i18nc("@title:tab", "A&ccessor Source") );
+    m_mainTabBar->addTab( scriptTab, // The icon gets automatically set to the mime \
type of the script +                          i18nc("@title:tab", "&Script") );
+    m_mainTabBar->addTab( m_preview, KIcon("plasma"), i18nc("@title:tab", \
"&Preview") ); +    m_mainTabBar->addTab( webWidget, KIcon("applications-internet"),
+                          i18nc("@title:tab", "&Web View") );
+
+    m_mainTabBar->setTabEnabled( ScriptTab, false ); // Disable script tab
+
+//     loadTemplate();
+    writeScriptTemplate();
+
+    // This creates an XML document in the Accessor Source tab.
+    // First mark the Accessor tab changed and then sync with the Accessor Source \
tab. +    m_accessorWidgetsChanged = true;
+    syncAccessor();
+
+    // Accessor isn't modified (therefore file_save is disabled),
+    // but it's also not saved, so enable file_save.
+    action("file_save")->setEnabled( true );
+}
+
+TimetableMate::~TimetableMate() {
+    m_recentFilesAction->saveEntries( Settings::self()->config()->group(0) );
+}
+
+void TimetableMate::closeEvent( QCloseEvent *event ) {
+    if ( m_changed ) {
+        int result;
+        if ( m_openedPath.isEmpty() ) {
+            result = KMessageBox::warningYesNoCancel( this, i18nc("@info/plain",
+                     "The current accessor is unsaved.<nl/>"
+                     "Do you want to save or discard it?",
+                     m_currentServiceProviderID, m_openedPath),
+                     i18nc("@title:window", "Close Document"),
+                     KStandardGuiItem::save(), KStandardGuiItem::discard() );
+        } else {
+            result = KMessageBox::warningYesNoCancel( this, i18nc("@info/plain",
+                     "The accessor <resource>%1</resource> in \
<filename>%2</filename> " +                     "has been changed.<nl/>Do you want to \
save or discard the changes?", +                     m_currentServiceProviderID, \
m_openedPath), +                     i18nc("@title:window", "Close Document"),
+                     KStandardGuiItem::save(), KStandardGuiItem::discard() );
+        }
+        if ( result == KMessageBox::Yes ) {
+            // Save current document
+            fileSave();
+        } else if ( result == KMessageBox::Cancel ) {
+            // Cancel closing
+            event->setAccepted( false );
+        }
+    }
+}
+
+void TimetableMate::setChanged( bool changed ) {
+    if ( m_changed == changed )
+        return;
+
+    m_changed = changed;
+    action("file_save")->setEnabled( m_changed );
+    if ( !changed ) {
+        m_accessorDocument->setModified( false );
+        m_scriptDocument->setModified( false );
+    }
+
+    updateWindowTitle();
+}
+
+void TimetableMate::showTextHint( const KTextEditor::Cursor &position, QString &text \
) { +    QPoint pointInView = m_scriptDocument->activeView()->cursorToCoordinate( \
position ); +    QPoint pointGlobal = m_scriptDocument->activeView()->mapToGlobal( \
pointInView ); +    QToolTip::showText( pointGlobal, text );
+}
+
+void TimetableMate::updateWindowTitle() {
+    QString currentTab = KGlobal::locale()->removeAcceleratorMarker(
+                             m_mainTabBar->tabText(m_mainTabBar->currentIndex()) );
+    if ( m_currentServiceProviderID.isEmpty() )
+        setCaption( currentTab, m_changed );
+    else
+        setCaption( currentTab + " - " + m_currentServiceProviderID, m_changed );
+
+    // Set preview tab disabled when the accessor isn't saved
+    m_mainTabBar->setTabEnabled( PlasmaPreviewTab, !m_openedPath.isEmpty() );
+    if ( m_mainTabBar->currentIndex() == PlasmaPreviewTab
+            && !m_mainTabBar->isTabEnabled(PlasmaPreviewTab) )
+    {
+        m_mainTabBar->setCurrentIndex( AccessorTab );
+    }
+}
+
+void TimetableMate::writeScriptTemplate() {
+    // Get the template interface
+    KTextEditor::View *scriptView = m_scriptDocument->views().first();
+    KTextEditor::TemplateInterface *templateInterface =
+        qobject_cast<KTextEditor::TemplateInterface*>( scriptView );
+    if ( templateInterface ) {
+        // Insert a template with author information
+        templateInterface->insertTemplateText( KTextEditor::Cursor(),
+                                               QString::fromUtf8("/** Accessor for \
${Service Provider}\n" +                                                              \
"  *  © ${year}, ${Author} */\n\n" +                                                  \
"// TODO: Implement parsing functions, use syntax completion\n" +                     \
"${cursor}"), +                                               QMap<QString, \
QString>() ); +
+        setChanged( false );
+    }
+}
+
+void TimetableMate::activePartChanged( KParts::Part *part ) {
+    createGUI( part );
+
+    if ( part ) {
+        // Manually hide actions of the part
+        QStringList actionsToHide;
+        actionsToHide << "file_save" << "file_save_as" << "tools_mode"
+        << "tools_highlighting" << "tools_indentation";
+        foreach ( QAction *action, menuBar()->actions() ) {
+            KActionMenu *menuAction = static_cast<KActionMenu*>( action );
+            for ( int i = menuAction->menu()->actions().count() - 1; i >= 0; --i ) {
+                QAction *curAction = menuAction->menu()->actions().at( i );
+                if ( curAction->parent() == actionCollection() )
+                    continue; // Don't hide own actions
+
+                if ( actionsToHide.contains(curAction->objectName()) ) {
+                    curAction->setVisible( false );
+
+                    actionsToHide.removeAt( i );
+                    if ( actionsToHide.isEmpty() )
+                        break;
+                }
+            }
+
+            if ( actionsToHide.isEmpty() )
+                break;
+        }
+    }
+}
+
+void TimetableMate::informationMessage( KTextEditor::View*, const QString &message ) \
{ +    statusBar()->showMessage( message );
+}
+
+void TimetableMate::accessorWidgetsChanged() {
+    m_accessorWidgetsChanged = true;
+    setChanged( true );
+
+    TimetableAccessor accessor = m_view->accessorInfo();
+
+    // Enable/disable actions to open web pages
+    action( "web_load_homepage" )->setEnabled( !accessor.url.isEmpty() );
+    action( "web_load_departures" )->setEnabled( !accessor.rawDepartureUrl.isEmpty() \
); +    action( "web_load_stopsuggestions" )->setEnabled( \
!accessor.rawStopSuggestionsUrl.isEmpty() ); +    action( "web_load_journeys" \
)->setEnabled( !accessor.rawJourneyUrl.isEmpty() ); +
+    QStringList functions = m_javaScriptModel->functionNames();
+    action( "script_runParseTimetable" )->setEnabled(
+        !accessor.rawDepartureUrl.isEmpty() && functions.contains("parseTimetable") \
); +    action( "script_runParseStopSuggestions" )->setEnabled(
+        !accessor.rawStopSuggestionsUrl.isEmpty() && \
functions.contains("parsePossibleStops") ); +    action( "script_runParseJourneys" \
)->setEnabled( +        !accessor.rawJourneyUrl.isEmpty() && \
functions.contains("parseJourneys") ); +}
+
+void TimetableMate::accessorDocumentChanged( KTextEditor::Document */*document*/ ) {
+    m_accessorDocumentChanged = true;
+    setChanged( true );
+}
+
+void TimetableMate::updateNextPreviousFunctionActions() {
+    int count = m_functionsModel->rowCount();
+    int functionIndex = m_functions->currentIndex();
+    if ( functionIndex == -1 ) {
+        int currentLine = m_scriptDocument->activeView()->cursorPosition().line();
+        FunctionNode *previousNode = dynamic_cast<FunctionNode*>(
+                                         \
m_javaScriptModel->nodeBeforeLineNumber(currentLine, Function) ); +        \
FunctionNode *nextNode = dynamic_cast<FunctionNode*>( +                               \
m_javaScriptModel->nodeAfterLineNumber(currentLine, Function) ); +        \
action("script_previous_function")->setEnabled( previousNode ); +        \
action("script_next_function")->setEnabled( nextNode ); +    } else {
+        action("script_previous_function")->setEnabled( count > 1 && functionIndex > \
0 ); +        action("script_next_function")->setEnabled( count > 1 && functionIndex \
!= count - 1 ); +    }
+}
+
+void TimetableMate::beginScriptParsing() {
+    delete m_backgroundParserTimer;
+    m_backgroundParserTimer = NULL;
+
+    // Parse the script
+    JavaScriptParser parser( m_scriptDocument->text() );
+
+    KTextEditor::MarkInterface *iface =
+        qobject_cast<KTextEditor::MarkInterface*>( m_scriptDocument );
+    if ( iface )
+        iface->clearMarks();
+
+//     m_scriptDocument->activeView()->mar
+    if ( parser.hasError() ) {
+        if ( iface ) {
+            iface->addMark( parser.errorLine() - 1, \
KTextEditor::MarkInterface::Error ); +            if ( parser.errorAffectedLine() != \
-1 ) { +                iface->addMark( parser.errorAffectedLine() - 1,
+                                KTextEditor::MarkInterface::Warning );
+            }
+        }
+
+// 	m_scriptDocument->views().first()->setCursorPosition( parser.errorCursor() );
+        statusBar()->showMessage( i18nc("@info:status", "Syntax error in line %1, \
column %2: " +                                        "<message>%3</message>",
+                                        parser.errorLine(), parser.errorColumn(),
+                                        parser.errorMessage()), 10000 );
+    } else {
+        statusBar()->showMessage( i18nc("@info:status", "No syntax errors found."), \
5000 ); +    }
+
+    // Update the model with the parsed nodes
+    bool wasBlocked = m_functions->blockSignals( true );
+    m_javaScriptModel->setNodes( parser.nodes() );
+    m_functions->blockSignals( wasBlocked );
+
+    // Update selected function in the function combobox
+    scriptCursorPositionChanged( m_scriptDocument->views().first(),
+                                 m_scriptDocument->views().first()->cursorPosition() \
); +    // Update next/previous function actions enabled state
+    updateNextPreviousFunctionActions();
+
+    // Update script_run* action enabled state
+    TimetableAccessor accessor = m_view->accessorInfo();
+    QStringList functions = m_javaScriptModel->functionNames();
+    action( "script_runParseTimetable" )->setEnabled(
+        !accessor.rawDepartureUrl.isEmpty() && functions.contains("parseTimetable") \
); +    action( "script_runParseStopSuggestions" )->setEnabled(
+        !accessor.rawStopSuggestionsUrl.isEmpty() && \
functions.contains("parsePossibleStops") ); +    action( "script_runParseJourneys" \
)->setEnabled( +        !accessor.rawJourneyUrl.isEmpty() && \
functions.contains("parseJourneys") ); +}
+
+void TimetableMate::scriptDocumentChanged( KTextEditor::Document */*document*/ ) {
+    setChanged( true );
+
+    if ( !m_backgroundParserTimer ) {
+        m_backgroundParserTimer = new QTimer( this );
+        m_backgroundParserTimer->setSingleShot( true );
+        connect( m_backgroundParserTimer, SIGNAL(timeout()), this, \
SLOT(beginScriptParsing()) ); +    }
+
+    // Begin parsing after delay
+    m_backgroundParserTimer->start( 500 );
+}
+
+void TimetableMate::syncAccessor() {
+    bool wasChanged = m_changed;
+
+    if ( m_accessorDocumentChanged ) {
+        QTextCodec *codec = QTextCodec::codecForName( \
m_accessorDocument->encoding().isEmpty() +                            ? "UTF-8" : \
m_accessorDocument->encoding().toLatin1() ); +        QByteArray ba = \
codec->fromUnicode( m_accessorDocument->text() ); +        setAccessorValues( &ba );
+        m_accessorDocumentChanged = false;
+        m_accessorWidgetsChanged = false;
+    } else if ( m_accessorWidgetsChanged ) {
+        m_accessorDocument->setText( m_view->writeAccessorInfoXml() );
+        m_accessorDocument->setModified( false );
+        m_accessorDocumentChanged = false;
+        m_accessorWidgetsChanged = false;
+    }
+
+    if ( !wasChanged && m_changed )
+        setChanged( false );
+}
+
+void TimetableMate::showScriptTab( bool loadTemplateIfEmpty ) {
+    m_mainTabBar->setCurrentIndex( ScriptTab ); // go to script tab
+
+    if ( loadTemplateIfEmpty && m_scriptDocument->isEmpty() )
+        writeScriptTemplate();
+}
+
+void TimetableMate::showWebTab( const QString &url, RawUrl rawUrl ) {
+    if ( !url.isEmpty() ) {
+        KUrl kurl;
+        switch ( rawUrl ) {
+        case NormalUrl:
+            kurl = KUrl( url );
+            break;
+        case RawDepartureUrl:
+            kurl = getDepartureUrl();
+            break;
+        case RawStopSuggestionsUrl:
+            kurl = getStopSuggestionUrl();
+            break;
+        case RawJourneyUrl:
+            kurl = getJourneyUrl();
+            break;
+        }
+        if ( kurl.isEmpty() )
+            return;
+
+        m_webview->openUrl( kurl );
+    }
+    m_mainTabBar->setCurrentIndex( WebTab ); // go to web tab
+}
+
+void TimetableMate::currentTabChanged( int index ) {
+    // Clear status bar messages
+    statusBar()->showMessage( QString() );
+
+    // When leaving the "Accessor Source" tab with changes,
+    // reload accessor values into the widgets in the "Accessor" tab
+    syncAccessor();
+//     if ( m_currentTab == AccessorSourceTab && m_accessorDocumentChanged ) {
+// 	QTextCodec *codec = QTextCodec::codecForName( \
m_accessorDocument->encoding().isEmpty() +// 		? "UTF-8" : \
m_accessorDocument->encoding().toLatin1() ); +// 	QByteArray ba = codec->fromUnicode( \
m_accessorDocument->text() ); +// 	setAccessorValues( &ba );
+// 	m_accessorDocumentChanged = false;
+// 	m_accessorWidgetsChanged = false;
+//     }
+//     // When leaving the "Accessor" tab with changes,
+//     // recreate accessor source and set it in the "Accessor Source" tab
+//     else if ( m_currentTab == AccessorTab && m_accessorWidgetsChanged ) {
+// 	m_accessorDocument->setText( m_view->writeAccessorInfoXml() );
+// 	m_accessorDocumentChanged = false;
+// 	m_accessorWidgetsChanged = false;
+//     }
+
+    // Don't flicker while changing the active part
+    setUpdatesEnabled( false );
+
+    if ( index == AccessorSourceTab ) { // go to accessor source tab
+        m_partManager->setActivePart( m_accessorDocument, m_mainTabBar );
+        m_accessorDocument->activeView()->setFocus();
+    } else if ( index == ScriptTab ) { // go to script tab
+        m_partManager->setActivePart( m_scriptDocument, m_mainTabBar );
+        m_scriptDocument->activeView()->setFocus();
+    } else if ( index == WebTab ) { // go to web tab
+        m_partManager->setActivePart( m_webview, m_mainTabBar );
+        m_urlBar->setFocus();
+    } else {
+        m_partManager->setActivePart( 0 );
+    }
+
+    if ( m_currentTab == PlasmaPreviewTab ) { // left plasma preview tab
+        m_preview->closePlasmaPreview();
+    } else if ( m_currentTab == ScriptTab ) { // left script tab
+        if ( statusBar()->hasItem(1) )
+            statusBar()->removeItem( 1 );
+    }
+
+    if ( m_currentTab == ScriptTab ) { // left script tab
+        action("script_next_function")->setVisible( false );
+        action("script_previous_function")->setVisible( false );
+    } else if ( index == ScriptTab ) { // go to script tab
+        action("script_next_function")->setVisible( true );
+        action("script_previous_function")->setVisible( true );
+    }
+
+    if ( m_currentTab == WebTab ) { // left web tab
+        action("web_back")->setVisible( false ); // TODO: Make (in)visible with \
xmlgui-states +        action("web_forward")->setVisible( false );
+        action("web_stop")->setVisible( false );
+        action("web_reload")->setVisible( false );
+    } else if ( index == WebTab ) { // go to web tab
+        action("web_back")->setVisible( true );
+        action("web_forward")->setVisible( true );
+        action("web_stop")->setVisible( true );
+        action("web_reload")->setVisible( true );
+    }
+
+    // Update caption
+    updateWindowTitle();
+
+    // Reset updates
+    setUpdatesEnabled( true );
+
+    // Store last tab
+    m_currentTab = index;
+}
+
+void TimetableMate::setupActions() {
+    KStandardAction::openNew( this, SLOT(fileNew()), actionCollection() );
+    KStandardAction::open( this, SLOT(fileOpen()), actionCollection() );
+    KStandardAction::save( this, SLOT(fileSave()), actionCollection() );
+    KStandardAction::saveAs( this, SLOT(fileSaveAs()), actionCollection() );
+    KStandardAction::quit( qApp, SLOT(closeAllWindows()), actionCollection() );
+    KStandardAction::preferences( this, SLOT(optionsPreferences()), \
actionCollection() ); +    m_recentFilesAction = KStandardAction::openRecent( this, \
SLOT(open(KUrl)), actionCollection() ); +    m_recentFilesAction->loadEntries( \
Settings::self()->config()->group(0) ); +
+    KAction *openInstalled = new KAction( KIcon("document-open"), i18nc("@action",
+                                          "Open I&nstalled..."), this );
+    actionCollection()->addAction( QLatin1String("file_open_installed"), \
openInstalled ); +    connect( openInstalled, SIGNAL(triggered(bool)), this, \
SLOT(fileOpenInstalled()) ); +
+    KAction *install = new KAction( KIcon("run-build-install"), i18nc("@action", \
"&Install"), this ); +    actionCollection()->addAction( \
QLatin1String("file_install"), install ); +    connect( install, \
SIGNAL(triggered(bool)), this, SLOT(install()) ); +
+    KAction *installGlobal = new KAction( KIcon("run-build-install-root"),
+                                          i18nc("@action", "Install &Globally"), \
this ); +    actionCollection()->addAction( QLatin1String("file_install_global"), \
installGlobal ); +//     installGlobal->setEnabled( false ); // TODO Enable only if a \
valid xml with script is opened +    connect( installGlobal, SIGNAL(triggered(bool)), \
this, SLOT(installGlobal()) ); +
+    QAction *webBack = m_webview->view()->pageAction( QWebPage::Back );
+    webBack->setVisible( false );
+    actionCollection()->addAction( QLatin1String("web_back"), webBack );
+
+    QAction *webForward = m_webview->view()->pageAction( QWebPage::Forward );
+    webForward->setVisible( false );
+    actionCollection()->addAction( QLatin1String("web_forward"), webForward );
+
+    QAction *webStop = m_webview->view()->pageAction( QWebPage::Stop );
+    webStop->setVisible( false );
+    actionCollection()->addAction( QLatin1String("web_stop"), webStop );
+
+    QAction *webReload = m_webview->view()->pageAction( QWebPage::Reload );
+    webReload->setVisible( false );
+    actionCollection()->addAction( QLatin1String("web_reload"), webReload );
+
+    QAction *webLoadHomePage = new KAction( KIcon("document-open-remote"),
+                                            i18nc("@action", "Open &Provider Home \
Page"), this ); +    webLoadHomePage->setToolTip( i18nc("@info:tooltip",
+                                       "Opens the <emphasis>home page</emphasis> of \
the service provider.") ); +    webLoadHomePage->setEnabled( false );
+    actionCollection()->addAction( QLatin1String("web_load_homepage"), \
webLoadHomePage ); +    connect( webLoadHomePage, SIGNAL(triggered(bool)), this, \
SLOT(webLoadHomePage()) ); +
+    QAction *webLoadDepartures = new KAction( KIcon("document-open-remote"),
+            i18nc("@action", "Open &Departures Page"), this );
+    webLoadDepartures->setToolTip( i18nc("@info:tooltip",
+                                         "Opens the <emphasis>departures</emphasis> \
web page.") ); +    webLoadDepartures->setEnabled( false );
+    actionCollection()->addAction( QLatin1String("web_load_departures"), \
webLoadDepartures ); +    connect( webLoadDepartures, SIGNAL(triggered(bool)), this, \
SLOT(webLoadDepartures()) ); +
+    QAction *webLoadStopSuggestions = new KAction( KIcon("document-open-remote"),
+            i18nc("@action", "Open &Stop Suggestions Page"), this );
+    webLoadStopSuggestions->setToolTip( i18nc("@info:tooltip",
+                                        "Opens the <emphasis>stop \
suggestions</emphasis> web page.") ); +    webLoadStopSuggestions->setEnabled( false \
); +    actionCollection()->addAction( QLatin1String("web_load_stopsuggestions"), \
webLoadStopSuggestions ); +    connect( webLoadStopSuggestions, \
SIGNAL(triggered(bool)), this, SLOT(webLoadStopSuggestions()) ); +
+    QAction *webLoadJourneys = new KAction( KIcon("document-open-remote"),
+                                            i18nc("@action", "Open &Journeys Page"), \
this ); +    webLoadJourneys->setToolTip( i18nc("@info:tooltip",
+                                       "Opens the <emphasis>journeys</emphasis> web \
page.") ); +    webLoadJourneys->setEnabled( false );
+    actionCollection()->addAction( QLatin1String("web_load_journeys"), \
webLoadJourneys ); +    connect( webLoadJourneys, SIGNAL(triggered(bool)), this, \
SLOT(webLoadJourneys()) ); +
+    KActionMenu *webLoadPage = new KActionMenu( KIcon("document-open-remote"),
+            i18nc("@action", "Open &Page"), this );
+    webLoadPage->setToolTip( i18nc("@info:tooltip",
+                                   "Opens a web page defined in the \
<interface>Accessor</interface> tab.") ); +    webLoadPage->setDelayed( false );
+    webLoadPage->addAction( webLoadHomePage );
+    webLoadPage->addSeparator();
+    webLoadPage->addAction( webLoadDepartures );
+    webLoadPage->addAction( webLoadStopSuggestions );
+    webLoadPage->addAction( webLoadJourneys );
+    actionCollection()->addAction( QLatin1String("web_load_page"), webLoadPage );
+
+    QAction *runScriptTimetable = new KAction( KIcon("system-run"),
+            i18nc("@action", "Run 'parse&Timetable'"), this );
+    runScriptTimetable->setToolTip( i18nc("@info:tooltip",
+                                          "Runs the \
<emphasis>parseTimetable()</emphasis> function of the script.") ); +    \
actionCollection()->addAction( QLatin1String("script_runParseTimetable"), \
runScriptTimetable ); +    connect( runScriptTimetable, SIGNAL(triggered(bool)), \
this, SLOT(scriptRunParseTimetable()) ); +
+    QAction *runScriptStopSuggestions = new KAction( KIcon("system-run"),
+            i18nc("@action", "Run 'parse&StopSuggestions'"), this );
+    runScriptStopSuggestions->setToolTip( i18nc("@info:tooltip",
+                                          "Runs the \
<emphasis>parseStopSuggestions()</emphasis> function of the script.") ); +    \
actionCollection()->addAction( QLatin1String("script_runParseStopSuggestions"), +     \
runScriptStopSuggestions ); +    connect( runScriptStopSuggestions, \
SIGNAL(triggered(bool)), +             this, SLOT(scriptRunParseStopSuggestions()) );
+
+    QAction *runScriptJourneys = new KAction( KIcon("system-run"),
+            i18nc("@action", "Run 'parse&Journeys'"), this );
+    runScriptJourneys->setToolTip( i18nc("@info:tooltip",
+                                         "Runs the \
<emphasis>parseJourneys()</emphasis> function of the script.") ); +    \
actionCollection()->addAction( QLatin1String("script_runParseJourneys"), \
runScriptJourneys ); +    connect( runScriptJourneys, SIGNAL(triggered(bool)), this, \
SLOT(scriptRunParseJourneys()) ); +
+    KActionMenu *runScript = new KActionMenu( KIcon("system-run"),
+            i18nc("@action", "&Run Script"), this );
+    runScript->setToolTip( i18nc("@info:tooltip", "Runs a function of the script.") \
); +    runScript->setDelayed( false );
+    runScript->addAction( runScriptTimetable );
+    runScript->addAction( runScriptStopSuggestions );
+    runScript->addAction( runScriptJourneys );
+    actionCollection()->addAction( QLatin1String("script_run"), runScript );
+
+    QAction *toolsCheck = new KAction( KIcon("dialog-ok-apply"), i18nc("@action", \
"&Check"), this ); +    toolsCheck->setToolTip( i18nc("@info:tooltip", "Checks the \
accessor for error/features.") ); +    actionCollection()->addAction( \
QLatin1String("tools_check"), toolsCheck ); +    connect( toolsCheck, \
SIGNAL(triggered(bool)), this, SLOT(toolsCheck()) ); +
+    KAction *scriptPreviousFunction = new KAction( KIcon("go-previous"), // no icon \
for this so far +            i18nc("@action", "&Previous Function"), this );
+    scriptPreviousFunction->setToolTip( i18nc("@info:tooltip", "Selects the previous \
function.") ); +    scriptPreviousFunction->setVisible( false );
+    scriptPreviousFunction->setShortcut( KShortcut("Ctrl+Alt+PgUp") ); // Same as in \
KDevelop +    actionCollection()->addAction( \
QLatin1String("script_previous_function"), +                                   \
scriptPreviousFunction ); +    connect( scriptPreviousFunction, \
SIGNAL(triggered(bool)), +             this, SLOT(scriptPreviousFunction()) );
+
+    KAction *scriptNextFunction = new KAction( KIcon("go-next"), // no icon for this \
so far +            i18nc("@action", "&Next Function"), this );
+    scriptNextFunction->setToolTip( i18nc("@info:tooltip", "Selects the next \
function.") ); +    scriptNextFunction->setVisible( false );
+    scriptNextFunction->setShortcut( KShortcut("Ctrl+Alt+PgDown") ); // Same as in \
KDevelop +    actionCollection()->addAction( QLatin1String("script_next_function"), \
scriptNextFunction ); +    connect( scriptNextFunction, SIGNAL(triggered(bool)), \
this, SLOT(scriptNextFunction()) ); +}
+
+void TimetableMate::currentFunctionChanged( int index ) {
+    QModelIndex functionIndex = m_functionsModel->index( index, 0 );
+    CodeNode *node = m_javaScriptModel->nodeFromIndex( \
m_functionsModel->mapToSource(functionIndex) ); +    FunctionNode *function = \
dynamic_cast<FunctionNode*>( node ); +    if ( function ) {
+        KTextEditor::View *view = m_scriptDocument->activeView();
+        view->blockSignals( true );
+        view->setCursorPosition( KTextEditor::Cursor(function->line() - 1, 0) );
+        view->blockSignals( false );
+    }
+
+    updateNextPreviousFunctionActions();
+}
+
+void TimetableMate::scriptCursorPositionChanged( KTextEditor::View* view,
+        const KTextEditor::Cursor& cursor ) {
+    Q_UNUSED( view );
+    bool wasBlocked = m_functions->blockSignals( true );
+    CodeNode *node = m_javaScriptModel->nodeFromLineNumber( cursor.line() + 1 );
+    if ( node ) {
+        QModelIndex index = m_javaScriptModel->indexFromNode( node );
+        QModelIndex functionIndex = m_functionsModel->mapFromSource( index );
+        m_functions->setCurrentIndex( functionIndex.row() );
+        updateNextPreviousFunctionActions();
+    }
+    m_functions->blockSignals( wasBlocked );
+
+    QString posInfo = i18nc("@info:status", "Line: %1 Col: %2",
+                            cursor.line() + 1, cursor.column() + 1);
+    if ( statusBar()->hasItem(1) ) {
+        statusBar()->changeItem( posInfo, 1 );
+    } else {
+        statusBar()->insertPermanentItem( posInfo, 1 );
+    }
+}
+
+void TimetableMate::fileNew() {
+    // This slot is called whenever the File->New menu is selected,
+    // the New shortcut is pressed (usually CTRL+N) or the New toolbar
+    // button is clicked
+
+    // Create a new window
+    (new TimetableMate)->show();
+}
+
+void TimetableMate::open( const KUrl &url ) {
+    KUrl openedUrl( m_openedPath + '/' + m_currentServiceProviderID + ".xml" );
+    if ( url.equals(openedUrl, KUrl::CompareWithoutTrailingSlash) ) {
+        kDebug() << "The file" << m_openedPath << "was already opened";
+        return;
+    }
+
+    if ( !m_openedPath.isEmpty() || m_changed ) {
+        TimetableMate *newWindow = new TimetableMate;
+        newWindow->open( url );
+        newWindow->show();
+    } else
+        loadAccessor( url.path() );
+}
+
+void TimetableMate::fileOpen() {
+    QString fileName = KFileDialog::getOpenFileName( \
KGlobalSettings::documentPath(), +                       "??*_*.xml", this, \
i18nc("@title:window", "Open Accessor") ); +    if ( fileName.isNull() )
+        return; // Cancel clicked
+
+    open( KUrl(fileName) );
+}
+
+void TimetableMate::fileOpenInstalled() {
+    // Get a list of all script files in the directory of the XML file
+    QStringList accessorFiles = KGlobal::dirs()->findAllResources( "data",
+                                "plasma_engine_publictransport/accessorInfos/*.xml" \
); +    if ( accessorFiles.isEmpty() ) {
+        KMessageBox::information( this, i18nc("@info/plain", "There are no installed \
" +                                              "timetable accessors. You need to \
install the PublicTransport data engine.") ); +        return;
+    }
+
+    // Make filenames more pretty and create a hash to map from the pretty names to \
the full paths +    QHash< QString, QString > map;
+    for ( QStringList::iterator it = accessorFiles.begin(); it != \
accessorFiles.end(); ++it ) { +        QString prettyName;
+        if ( KStandardDirs::checkAccess(*it, W_OK) ) {
+            // File is writable, ie. locally installed
+            prettyName = KUrl( *it ).fileName();
+        } else {
+            // File isn't writable, ie. globally installed
+            prettyName = i18nc( "@info/plain This string is displayed instead of the \
full path for " +                                "globally installed timetable \
accessor xmls.", +                                "Global: %1", KUrl(*it).fileName() \
); +        }
+
+        map.insert( prettyName, *it );
+        *it = prettyName;
+    }
+
+    bool ok;
+    QString selectedPrettyName = KInputDialog::getItem( i18nc("@title:window",
+                                 "Open Installed Accessor"), i18nc("@info", \
"Installed timetable accessor"), +                                 accessorFiles, 0, \
false, &ok, this ); +    if ( ok ) {
+        QString selectedFilePath = map[ selectedPrettyName ];
+        loadAccessor( selectedFilePath );
+    }
+}
+
+void TimetableMate::fileSave() {
+    if ( m_openedPath.isEmpty() ) {
+        fileSaveAs();
+    } else {
+        syncAccessor();
+        m_accessorDocument->documentSave();
+        m_scriptDocument->documentSave();
+        setChanged( false );
+    }
+}
+
+void TimetableMate::fileSaveAs() {
+    TimetableAccessor accessor = m_view->accessorInfo();
+    QString fileName = KFileDialog::getSaveFileName(
+                           m_openedPath.isEmpty() ? KGlobalSettings::documentPath() \
: m_openedPath, +                           "??*_*.xml", this, i18nc("@title:window", \
"Save Accessor") ); +    if ( fileName.isNull() )
+        return; // Cancel clicked
+
+    KUrl url( fileName );
+    m_currentServiceProviderID = url.fileName().left( \
url.fileName().lastIndexOf('.') ); +    m_view->setCurrentServiceProviderID( \
m_currentServiceProviderID ); +    m_openedPath = url.directory();
+    syncAccessor();
+    m_view->writeAccessorInfoXml( fileName );
+
+    QString scriptFile = accessor.scriptFile;
+    if ( !scriptFile.isEmpty() ) {
+        QString scriptFilePath = m_openedPath + '/' + scriptFile;
+        if ( !m_scriptDocument->saveAs(scriptFilePath) ) {
+            KMessageBox::information( this, i18nc("@info", "Couldn't write the \
script file to" +                                                  \
"<filename>%1</filename>.", scriptFilePath) ); +        }
+    }
+
+    setChanged( false );
+}
+
+void TimetableMate::install() {
+    TimetableAccessor accessor = m_view->accessorInfo();
+    QString saveDir = KGlobal::dirs()->saveLocation( "data",
+                      "plasma_engine_publictransport/accessorInfos/" );
+    KUrl urlXml = KUrl( saveDir + m_currentServiceProviderID + ".xml" );
+    KUrl urlScript = KUrl( saveDir + accessor.scriptFile ); // TODO .py, .rb
+
+    syncAccessor();
+    bool ok = false;
+    ok = m_accessorDocument->saveAs( urlXml );
+    ok = ok && m_scriptDocument->saveAs( urlScript );
+
+    if ( ok ) {
+        // Installation successful
+        statusBar()->showMessage( i18nc("@info:status",
+                                        "Accessor successfully installed locally"), \
5000 ); +    } else {
+        KMessageBox::error( this, i18nc("@info", "Accessor couldn't be installed \
locally. Tried " +                                        "to save these files:<nl/>  \
%1<nl/>  %2", +                                        urlXml.prettyUrl(), \
urlScript.prettyUrl()) ); +    }
+}
+
+void TimetableMate::installGlobal() {
+    QStringList saveDirs = KGlobal::dirs()->findDirs( "data",
+                           "plasma_engine_publictransport/accessorInfos/" );
+    if ( saveDirs.isEmpty() ) {
+        kDebug() << "No save directory found. Is the PublicTransport data engine \
installed?"; +        return;
+    }
+    QString saveDir = saveDirs.last(); // Use the most global one
+    syncAccessor();
+    TimetableAccessor accessor = m_view->accessorInfo();
+
+    KAuth::Action action( "org.kde.timetablemate.install" );
+    action.setHelperID( "org.kde.timetablemate" );
+    QVariantMap args;
+    args["path"] = saveDir;
+    args["filenameAccessor"] = m_currentServiceProviderID + ".xml";
+    args["filenameScript"] = accessor.scriptFile;
+    args["contentsAccessor"] = m_accessorDocument->text();
+    args["contentsScript"] = m_scriptDocument->text();
+    action.setArguments( args );
+    KAuth::ActionReply reply = action.execute();
+    if ( reply.failed() ) {
+        kDebug() << reply.type() << reply.data();
+        kDebug() << reply.errorCode() << reply.errorDescription();
+        if ( reply.type() == KAuth::ActionReply::HelperError ) {
+            KMessageBox::error( this, i18nc("@info", "Accessor couldn't be installed \
globally: " +                                            "%1 <message>%2</message>", \
reply.errorCode(), reply.errorDescription()) ); +        } else {
+            switch ( reply.errorCode() ) {
+            case KAuth::ActionReply::UserCancelled:
+            case KAuth::ActionReply::AuthorizationDenied:
+// 		    UserCancelled is also AuthorizationDenied on X11 currently (PolicyKit \
limitation) +                // Do nothing
+// 		    KMessageBox::error( this, i18nc("@info", "Authentication denied: "
+// 			    "<message>%1</message>", reply.errorDescription()) );
+                break;
+
+            case KAuth::ActionReply::NoSuchAction:
+                KMessageBox::error( this, i18nc("@info", "Couldn't find the \
authentication " +                                                "action. If you \
just installed TimetableMate, you might need to " +                                   \
"restart DBus.") ); +                break;
+
+            case KAuth::ActionReply::HelperBusy:
+                KMessageBox::error( this, i18nc("@info", "The action is currently \
being " +                                                "performed. Please try again \
later.") ); +                break;
+
+            default:
+                KMessageBox::error( this, i18nc("@info", "Unable to authenticate the \
action: " +                                                "%1 \
<message>%2</message>", reply.errorCode(), reply.errorDescription()) ); +             \
break; +            }
+        }
+    } else {
+        // Installation successful
+        statusBar()->showMessage( i18nc("@info:status",
+                                        "Accessor successfully installed globally"), \
5000 ); +    }
+}
+
+bool TimetableMate::loadTemplate( const QString &fileName ) {
+    QString _fileName = fileName;
+    if ( _fileName.isNull() ) {
+        // Get a template file name
+        QStringList fileNames = KGlobal::dirs()->findAllResources( "data",
+                                QString("timetablemate/templates/*.xml") );
+        if ( fileNames.isEmpty() ) {
+            kDebug() << "Couldn't find a template";
+            return false;
+        }
+
+        // Use the first template found
+        _fileName = fileNames.first();
+    }
+
+    // Read template
+    QFile file( _fileName );
+    if ( !file.open(QIODevice::ReadOnly) ) {
+        kDebug() << "Coulnd't open file" << _fileName;
+        return false;
+    }
+    QByteArray ba = file.readAll();
+    file.close();
+
+    // Set template text to the Kate part in the "Accessor Source" tab
+    m_accessorDocument->closeUrl( false );
+    m_accessorDocument->setText( QString(ba) );
+    m_accessorDocument->setModified( false );
+
+    // Set values of widgets in the "Accessor" tab
+    QString error;
+    if ( !setAccessorValues(&ba, &error) ) {
+        KMessageBox::information( this, i18nc("@info", "The XML file <filename>"
+                                              "%1</filename> couldn't be read: \
<message>%2</message>", +                                              _fileName, \
error) ); +        return false;
+    }
+
+    m_accessorDocumentChanged = false;
+    m_accessorWidgetsChanged = false;
+    m_currentServiceProviderID.clear(); // window title gets updated on setChanged() \
below +    m_view->setCurrentServiceProviderID( m_currentServiceProviderID );
+    m_openedPath.clear();
+
+    // Load script file referenced by the XML
+    if ( !loadScriptForCurrentAccessor(QFileInfo(_fileName).path(), false) ) {
+        syncAccessor();
+        m_view->setScriptFile( QString() );
+        syncAccessor();
+        setChanged( false );
+        return false;
+    }
+
+    setChanged( false );
+    return true;
+}
+
+bool TimetableMate::setAccessorValues( QByteArray *text, QString *error,
+                                       const QString &fileName ) {
+    // Set values in the Accessor tab, text contains the XML document
+    QBuffer buffer( text, this );
+    return m_view->readAccessorInfoXml( &buffer, error, fileName );
+}
+
+bool TimetableMate::loadAccessor( const QString &fileName ) {
+    // Close old files without asking to save them
+    m_accessorDocument->closeUrl( false );
+    m_scriptDocument->closeUrl( false );
+
+    // Try to open the XML in the Kate part in the "Accessor Source" tab
+    KUrl url( fileName );
+    if ( !QFile::exists(fileName) ) {
+        KMessageBox::information( this, i18nc("@info", "The XML file <filename>"
+                                              "%1</filename> couldn't be found.", \
fileName) ); +        return false;
+    }
+    if ( !m_accessorDocument->openUrl(url) )
+        return false;
+    m_accessorDocument->setModified( false );
+
+    // Try to read the XML text into the widgets in the "Accessor" tab
+    QString error;
+    QTextCodec *codec = QTextCodec::codecForName( \
m_accessorDocument->encoding().isEmpty() +                        ? "UTF-8" : \
m_accessorDocument->encoding().toLatin1() ); +    QByteArray ba = codec->fromUnicode( \
m_accessorDocument->text() ); +    if ( !setAccessorValues(&ba, &error, fileName) ) {
+        KMessageBox::information( this, i18nc("@info", "The XML file <filename>"
+                                              "%1</filename> couldn't be read: \
<message>%2</message>", +                                              fileName, \
error) ); +        return false;
+    }
+    m_openedPath = url.directory();
+
+    // Set read only mode of the kate parts if the files aren't writable
+    QFile test( url.path() );
+    if ( test.open(QIODevice::ReadWrite) ) {
+        m_accessorDocument->setReadWrite( true );
+        m_scriptDocument->setReadWrite( true );
+        test.close();
+    } else {
+        m_accessorDocument->setReadWrite( false );
+        m_scriptDocument->setReadWrite( false );
+    }
+
+    m_accessorDocumentChanged = false;
+    m_accessorWidgetsChanged = false;
+    m_currentServiceProviderID = url.fileName().left( \
url.fileName().lastIndexOf('.') ); +    m_view->setCurrentServiceProviderID( \
m_currentServiceProviderID ); +
+    // Add to the recently used files action
+    m_recentFilesAction->addUrl( url );
+
+    // Load script file referenced by the XML
+    if ( !loadScriptForCurrentAccessor(url.directory()) ) {
+        syncAccessor();
+        m_view->setScriptFile( QString() );
+        syncAccessor();
+        setChanged( false );
+        return false;
+    }
+
+    setChanged( false );
+    return true;
+}
+
+void TimetableMate::scriptFileChanged( const QString &/*scriptFile*/ ) {
+    loadScriptForCurrentAccessor( m_openedPath );
+}
+
+void TimetableMate::plasmaPreviewLoaded() {
+    m_preview->setSettings( m_currentServiceProviderID, QString() );
+}
+
+bool TimetableMate::loadScriptForCurrentAccessor( const QString &path, bool openFile \
) { +    m_scriptDocument->closeUrl( false );
+    m_scriptDocument->setModified( false );
+    if ( path.isEmpty() ) {
+        kDebug() << "Cannot open script files when the path isn't given. "
+        "Save the accessor XML file first.";
+        m_mainTabBar->setTabEnabled( ScriptTab, false ); // Disable script tab
+        return false;
+    }
+
+    QString text = m_accessorDocument->text();
+    QString scriptFile = m_view->accessorInfo().scriptFile;
+    if ( scriptFile.isEmpty() ) {
+        m_mainTabBar->setTabEnabled( ScriptTab, false ); // Disable script tab
+        return false;
+    } else {
+        scriptFile = path + '/' + scriptFile;
+
+        if ( openFile ) {
+            if ( !QFile::exists(scriptFile) ) {
+                KMessageBox::information( this, i18nc("@info", "The script file \
<filename>" +                                                      "%1</filename> \
couldn't be found.", scriptFile) ); +                m_mainTabBar->setTabEnabled( \
ScriptTab, false ); // Disable script tab +                return false;
+            }
+            if ( !m_scriptDocument->openUrl(KUrl(scriptFile)) ) {
+                m_mainTabBar->setTabEnabled( ScriptTab, false ); // Disable script \
tab +                return false;
+            }
+        } else {
+            QFile file( scriptFile );
+            if ( !file.open(QIODevice::ReadOnly) ) {
+                kDebug() << "Coulnd't open file" << scriptFile << "read only";
+                m_mainTabBar->setTabEnabled( ScriptTab, false ); // Disable script \
tab +                return false;
+            }
+            QByteArray ba = file.readAll();
+            file.close();
+            m_scriptDocument->setText( QString(ba) );
+            m_scriptDocument->setModified( false );
+        }
+    }
+
+    m_mainTabBar->setTabEnabled( ScriptTab, true ); // Enable script tab
+    m_mainTabBar->setTabIcon( ScriptTab, \
KIcon(m_scriptDocument->mimeType().replace('/', '-')) ); +    return true;
+}
+
+void TimetableMate::optionsPreferences() {
+    KMessageBox::information( this, "There are currently no settings... But maybe \
later ;)" ); +    return;
+//     TODO
+    // The preference dialog is derived from prefs_base.ui
+    //
+    // compare the names of the widgets in the .ui file
+    // to the names of the variables in the .kcfg file
+    //avoid to have 2 dialogs shown
+    if ( KConfigDialog::showDialog( "settings" ) )  {
+        return;
+    }
+    KConfigDialog *dialog = new KConfigDialog( this, "settings", Settings::self() );
+    QWidget *generalSettingsDlg = new QWidget;
+    ui_prefs_base.setupUi(generalSettingsDlg);
+    dialog->addPage(generalSettingsDlg, i18n("General"), "package_setting");
+    connect(dialog, SIGNAL(settingsChanged(QString)), m_view, \
SLOT(settingsChanged())); +    dialog->setAttribute( Qt::WA_DeleteOnClose );
+    dialog->show();
+}
+
+void TimetableMate::toolsCheck() {
+    TimetableAccessor accessor = m_view->accessorInfo();
+    QStringList errors, inelegants;
+
+    bool nameOk = !accessor.name.isEmpty();
+    bool descriptionOk = !accessor.description.isEmpty();
+    bool versionOk = !accessor.version.isEmpty(); // Correct format is validated
+    bool fileVersionOk = accessor.fileVersion == "1.0"; // Correct format is \
validated +    bool authorOk = !accessor.author.isEmpty();
+    bool emailOk = !accessor.email.isEmpty(); // Correct format is validated
+    bool urlOk = !accessor.url.isEmpty();
+    bool shortUrlOk = !accessor.shortUrl.isEmpty();
+    bool departuresUrlOk = !accessor.rawDepartureUrl.isEmpty();
+    bool stopSuggestionsUrlOk = !accessor.rawStopSuggestionsUrl.isEmpty();
+    bool journeysUrlOk = !accessor.rawJourneyUrl.isEmpty();
+    bool scriptOk = !accessor.scriptFile.isEmpty();
+    QStringList scriptFunctions;
+
+    if ( !nameOk ) {
+        errors << i18nc("@info", "<emphasis>You need to specify a name for your \
accessor.</emphasis> " +                        "Applets show this name in a service \
provider selector widget."); +    }
+    if ( !descriptionOk ) {
+        inelegants << i18nc("@info", "<emphasis>You should give a description for \
your accessor." +                            "</emphasis> Describe what \
cities/countries/vehicles are supported and " +                            "what \
limitations there possibly are when using your accessor."); +    }
+    if ( !versionOk ) {
+        inelegants << i18nc("@info", "<emphasis>You should specify a version of your \
accessor." +                            "</emphasis> This helps to distinguish \
between different versions and " +                            "makes it possible to \
say for example: \"You need at least version " +                            "1.3 of \
that accessor for that feature to work\"."); +    }
+    if ( !fileVersionOk ) {
+        errors << i18nc("@info", "<emphasis>The PublicTransport data engine \
currently " +                        "only supports version '1.0'.</emphasis>");
+    }
+    if ( !authorOk ) {
+        inelegants << i18nc("@info", "<emphasis>You should give your \
name.</emphasis> " +                            "Applets may want to show the name as \
display text for email-links, " +                            "the result would be \
that nothing is shown."); +    }
+    if ( !emailOk ) {
+        inelegants << i18nc("@info", "<emphasis>You should give your email \
address.</emphasis> " +                            "You may create a new address if \
you don't want to use your private " +                            "one. Without an \
email address, no one can contact you if something " +                            "is \
wrong with your accessor."); +    }
+    if ( !urlOk ) {
+        inelegants << i18nc("@info", "<emphasis>You should give the URL to the home \
page of the " +                            "service provider.</emphasis> "
+                            "Since the service providers are running servers for the \
timetable " +                            "service they will want to get some credit. \
Applets (should) show " +                            "a link to the home page.");
+    }
+    if ( !shortUrlOk ) {
+        inelegants << i18nc("@info", "<emphasis>You should give a short version of \
the URL to " +                            "the home page of the service \
provider.</emphasis> " +                            "Applets may want to show the \
short URL as display text for the home " +                            "page link, to \
save space. The result would be that nothing is shown."); +    }
+
+    if ( scriptOk ) {
+        // First check the script using the own JavaScriptParser
+        JavaScriptParser parser( m_scriptDocument->text() );
+        if ( parser.hasError() ) {
+            // Go to error line
+            m_scriptDocument->activeView()->setCursorPosition(
+                KTextEditor::Cursor(parser.errorLine() - 1, parser.errorColumn()) );
+            errors << i18nc("@info", "<emphasis>Error in script (line \
%2):</emphasis> " +                            "<message>%1</message>", \
parser.errorMessage(), parser.errorLine()); +            scriptOk = false;
+        }
+
+        // Create a Kross::Action instance
+        Kross::Action script( this, "TimetableParser" );
+
+        // Set script code and type
+        TimetableAccessor accessor = m_view->accessorInfo();
+        if ( accessor.scriptFile.endsWith(".py") )
+            script.setInterpreter( "python" );
+        else if ( accessor.scriptFile.endsWith(".rb") )
+            script.setInterpreter( "ruby" );
+        else if ( accessor.scriptFile.endsWith(".js") )
+            script.setInterpreter( "javascript" );
+        else {
+            const QString scriptType = KInputDialog::getItem(
+                                           i18nc("@title:window", "Choose Script \
Type"), +                                           i18nc("@info", "Script type \
unknown, please choose one of these:"), +                                           \
QStringList() << "JavaScript" << "Ruby" << "Python", 0, false, 0, this ); +           \
script.setInterpreter( scriptType.toLower() ); +        }
+        script.setCode( m_scriptDocument->text().toUtf8() );
+
+        // Test the script
+        script.trigger();
+        if ( !script.hadError() ) {
+            scriptFunctions = script.functionNames();
+        } else {
+            if ( script.errorLineNo() != -1 ) {
+                // Go to error line
+                m_scriptDocument->activeView()->setCursorPosition(
+                    KTextEditor::Cursor(script.errorLineNo(), 0) );
+            }
+            errors << i18nc("@info", "<emphasis>Error in script:</emphasis> "
+                            "<message>%1</message>", script.errorMessage());
+            scriptOk = false;
+        }
+    } else {
+        errors << i18nc("@info", "<emphasis>No script file specified in the "
+                        "<interface>Accessor</interface> tab.</emphasis>");
+    }
+
+    if ( !scriptFunctions.contains("usedTimetableInformations") ) {
+        inelegants << i18nc("@info", "<emphasis>You should implement the "
+                            "'usedTimetableInformations' script function.</emphasis> \
This is used " +                            "to get the features supported by the \
accesor."); +    }
+    if ( !scriptFunctions.contains("parseTimetable") || !departuresUrlOk ) {
+        errors << i18nc("@info", "<emphasis>You need to specify both a raw departure \
URL and a " +                        "'parseTimetable' script function.</emphasis> \
<note>Accessors that only support " +                        "journeys are currently \
not accepted by the data engine, but that may " +                        \
"change</note>."); +    }
+    if ( scriptFunctions.contains("parsePossibleStops") && !stopSuggestionsUrlOk ) {
+        inelegants << i18nc("@info", "<emphasis>The script has a \
'parsePossibleStops' function, " +                            "but there's no raw URL \
for stop suggestions.</emphasis>"); +    }
+    if ( scriptFunctions.contains("parseJourneys") && !journeysUrlOk ) {
+        inelegants << i18nc("@info", "<emphasis>The script has a 'parseJourney' \
function, " +                            "but there's no raw URL for \
journeys.</emphasis>"); +    }
+
+    bool stopSuggestionsShouldWork = scriptFunctions.contains("parsePossibleStops") \
&& stopSuggestionsUrlOk; +    bool journeysShouldWork = \
scriptFunctions.contains("parseJourneys") && journeysUrlOk; +    QStringList working;
+    if ( stopSuggestionsShouldWork )
+        working << i18nc("@info", "Stop suggestions should work.");
+    if ( journeysShouldWork )
+        working << i18nc("@info", "Journeys should work.");
+
+    QString msg;
+    if ( errors.isEmpty() && inelegants.isEmpty() ) {
+        msg = i18nc("@info", "<para><emphasis>No errors found.</emphasis></para>"
+                    "<para>To ensure that your accessor is working correctly do \
these steps:<list>" +                    "<item>Check the home page link, eg. using \
<interface>View -> Open Page -> " +                    "Open Home \
Page</interface></item>" +                    "<item>Run each script function you \
have implemented using <interface>Tools -> " +                    "Run \
Script</interface></item>" +                    "<item>Try your accessor using the \
PublicTransport applet, eg. in the " +                    \
"<interface>Preview</interface> tab.</item>" +                    "<item>Try \
different stops, eg. with/without delay information.</item>" +                    \
"</list></para>"); +    } else if ( errors.isEmpty() && !inelegants.isEmpty() ) {
+        msg = i18nc("@info", "<para><emphasis>%1 errors found, but nothing \
severe.</emphasis> " +                    "You should try to fix these: \
<list>%2</list></para>" +                    "<para>To ensure that your accessor is \
working correctly do these steps:<list>" +                    "<item>Check the home \
page link, eg. using <interface>View -> Open Page -> " +                    "Open \
Home Page</interface></item>" +                    "<item>Run each script function \
you have implemented using <interface>Tools -> " +                    "Run \
Script</interface></item>" +                    "<item>Try your accessor using the \
PublicTransport applet, eg. in the " +                    \
"<interface>Preview</interface> tab.</item>" +                    "<item>Try \
different stops, eg. with/without delay information.</item>" +                    \
"</list></para>", inelegants.count(), +                    "<item>" + \
inelegants.join("</item><item>") + "</item>"); +    } else if ( !errors.isEmpty() && \
inelegants.isEmpty() ) { +        msg = i18nc("@info", "<para><warning>%1 severe \
errors found.</warning> You should try " +                    "to fix these: \
<list>%2</list></para>", errors.count(), +                    "<item>" + \
errors.join("</item><item>") + "</item>"); +    } else { // both not empty
+        msg = i18nc("@info", "<para><warning>%1 errors found, %2 of them are \
severe.</warning> " +                    "You need to fix these: \
<list>%3</list><nl/>" +                    "You should also try to fix these: \
<list>%4</list></para>", +                    errors.count() + inelegants.count(), \
errors.count(), +                    "<item>" + errors.join("</item><item>") + \
"</item>", +                    "<item>" + inelegants.join("</item><item>") + \
"</item>"); +    }
+
+    KMessageBox::information( this, msg, i18nc("@title:window", "Error Report") );
+}
+
+void TimetableMate::scriptNextFunction() {
+    if ( m_functions->currentIndex() == -1 ) {
+        FunctionNode *node = dynamic_cast<FunctionNode*>(
+                                 m_javaScriptModel->nodeAfterLineNumber(
+                                     \
m_scriptDocument->activeView()->cursorPosition().line(), Function) ); +        if ( \
node ) { +            QModelIndex index = m_javaScriptModel->indexFromNode( node );
+            m_functions->setCurrentIndex( \
m_functionsModel->mapFromSource(index).row() ); +            return;
+        }
+    }
+
+    m_functions->setCurrentIndex( m_functions->currentIndex() + 1 );
+}
+
+void TimetableMate::scriptPreviousFunction() {
+    if ( m_functions->currentIndex() == -1 ) {
+        FunctionNode *node = dynamic_cast<FunctionNode*>(
+                                 m_javaScriptModel->nodeBeforeLineNumber(
+                                     \
m_scriptDocument->activeView()->cursorPosition().line(), Function) ); +        if ( \
node ) { +            QModelIndex index = m_javaScriptModel->indexFromNode( node );
+            m_functions->setCurrentIndex( \
m_functionsModel->mapFromSource(index).row() ); +            return;
+        }
+    }
+
+    m_functions->setCurrentIndex( m_functions->currentIndex() - 1 );
+}
+
+void TimetableMate::urlBarReturn( const QString &text ) {
+    m_webview->openUrl( QUrl::fromUserInput(text) );
+}
+
+void TimetableMate::webUrlChanged( const QUrl& url ) {
+    m_urlBar->setEditUrl( url );
+    if ( !m_urlBar->contains(url.toString()) )
+        m_urlBar->addUrl( QWebSettings::iconForUrl(url), url );
+}
+
+void TimetableMate::webLoadHomePage() {
+    TimetableAccessor accessor = m_view->accessorInfo();
+    if ( !hasHomePageURL(accessor) )
+        return;
+
+    // Open URL
+    m_webview->openUrl( accessor.url );
+
+    // Go to web tab
+    m_mainTabBar->setCurrentIndex( WebTab );
+}
+
+void TimetableMate::webLoadDepartures() {
+    TimetableAccessor accessor = m_view->accessorInfo();
+    if ( !hasRawDepartureURL(accessor) )
+        return;
+
+    // Open URL
+    KUrl url = getDepartureUrl();
+    m_webview->openUrl( url );
+
+    // Go to web tab
+    m_mainTabBar->setCurrentIndex( WebTab );
+}
+
+void TimetableMate::webLoadStopSuggestions() {
+    TimetableAccessor accessor = m_view->accessorInfo();
+    if ( !hasRawStopSuggestionURL(accessor) )
+        return;
+
+    // Open URL
+    KUrl url = getStopSuggestionUrl();
+    m_webview->openUrl( url );
+
+    // Go to web tab
+    m_mainTabBar->setCurrentIndex( WebTab );
+}
+
+void TimetableMate::webLoadJourneys() {
+    TimetableAccessor accessor = m_view->accessorInfo();
+    if ( !hasRawJourneyURL(accessor) )
+        return;
+
+    // Open URL
+    KUrl url = getJourneyUrl();
+    m_webview->openUrl( url );
+
+    // Go to web tab
+    m_mainTabBar->setCurrentIndex( WebTab );
+}
+
+void TimetableMate::scriptRunParseTimetable() {
+    TimetableData timetableData;
+    ResultObject resultObject;
+    QVariant result;
+    DebugMessageList debugMessages;
+    if ( !scriptRun("parseTimetable", &timetableData, &resultObject, &result, \
&debugMessages) ) { +        return;
+    }
+
+    // Get global infos
+    QStringList globalInfos;
+    if ( result.isValid() && result.canConvert(QVariant::StringList) ) {
+        globalInfos = result.toStringList();
+    }
+
+    // Get result set
+    QList<TimetableData> data = resultObject.data();
+    int count = 0, countInvalid = 0;
+    QDate curDate;
+    QTime lastTime;
+    for ( int i = 0; i < data.count(); ++ i ) {
+        TimetableData timetableData = data.at( i );
+        QDate date = timetableData.value( "departuredate" ).toDate();
+        QTime departureTime = QTime( timetableData.value("departurehour").toInt(),
+                                     timetableData.value("departureminute").toInt() \
); +        if ( !date.isValid() ) {
+            if ( curDate.isNull() ) {
+                // First departure
+                if ( QTime::currentTime().hour() < 3 && departureTime.hour() > 21 )
+                    date = QDate::currentDate().addDays( -1 );
+                else if ( QTime::currentTime().hour() > 21 && departureTime.hour() < \
3 ) +                    date = QDate::currentDate().addDays( 1 );
+                else
+                    date = QDate::currentDate();
+            } else if ( lastTime.secsTo(departureTime) < -5 * 60 ) {
+                // Time too much ealier than last time, estimate it's tomorrow
+                date = curDate.addDays( 1 );
+            } else {
+                date = curDate;
+            }
+
+            timetableData.set( "departuredate", date );
+        }
+
+        curDate = date;
+        lastTime = departureTime;
+
+        QVariantHash values = timetableData.values();
+        bool isValid =  values.contains("transportline") && \
values.contains("target") +                        && \
values.contains("departurehour") && values.contains("departureminute"); +        if ( \
isValid ) +            ++count;
+        else
+            ++countInvalid;
+    }
+
+    QStringList departures;
+    for ( int i = 0; i < data.count(); ++i ) {
+        QVariantHash values = data[i].values();
+        QString departure;
+        departure = QString("\"%1\" to \"%2\" at %3:%4").arg( \
values["transportline"].toString(), +                    values["target"].toString() \
) +                    .arg( values["departurehour"].toInt(), 2, 10, QChar('0') )
+                    .arg( values["departureminute"].toInt(), 2, 10, QChar('0') );
+        if ( values.contains("departuredate") && \
!values["departuredate"].toList().isEmpty() ) { +			QList<QVariant> date = \
values["departuredate"].toList(); +			if ( date.count() >= 3 ) {
+				departure += QString(", %1").arg( QDate(date[0].toInt(), date[1].toInt(), \
date[2].toInt()).toString() ); +			}
+		}
+        if ( values.contains("typeofvehicle") && \
!values["typeofvehicle"].toString().isEmpty() ) +            departure += QString(", \
%1").arg( values["typeofvehicle"].toString() ); +        if ( \
values.contains("delay") && values["delay"].toInt() != -1 ) +            departure += \
QString(", delay: %1").arg( values["delay"].toInt() ); +        if ( \
values.contains("delayreason") && !values["delayreason"].toString().isEmpty() ) +     \
departure += QString(", delay reason: %1").arg( values["delayreason"].toString() ); + \
if ( values.contains("platform") && !values["platform"].toString().isEmpty() ) +      \
departure += QString(", platform: %1").arg( values["platform"].toString() ); +        \
if ( values.contains("operator") && !values["operator"].toString().isEmpty() ) +      \
departure += QString(", operator: %1").arg( values["operator"].toString() ); +        \
if ( values.contains("routestops") && !values["routestops"].toStringList().isEmpty() \
) { +            QStringList routeStops = values["routestops"].toStringList();
+            departure += QString(", %1 route stops").arg( routeStops.count() );
+
+            // Check if RouteTimes has the same number of elements as RouteStops (if \
set) +            if ( values.contains("routetimes") && \
!values["routetimes"].toStringList().isEmpty() ) { +                QStringList \
routeTimes = values["routetimes"].toStringList(); +                departure += \
QString(", %1 route times").arg( routeTimes.count() ); +
+                if ( routeTimes.count() != routeStops.count() ) {
+                    departure += QString(" - <emphasis strong='1'>'RouteTimes' \
should contain " +                                         "the same number of \
elements as 'RouteStops'</emphasis>"); +                }
+            }
+        }
+
+        departures << QString("<item><emphasis strong='1'>%1.</emphasis> %2</item>")
+			.arg( i + 1 ).arg( departure );
+    }
+
+    QStringList unknownTimetableInformations;
+    QMultiHash<QString, QVariant> unknown = \
timetableData.unknownTimetableInformationStrings(); +    kDebug() << unknown;
+    for ( QMultiHash<QString, QVariant>::const_iterator it = unknown.constBegin();
+            it != unknown.constEnd(); ++it )
+    {
+        unknownTimetableInformations << "<item>" +
+	    i18nc("@info", "'%1' with value '%2'", it.key(), it.value().toString()) + \
"</item>"; +    }
+
+    // Show results
+    QString resultItems = i18nc("@info", "Got %1 departures/arrivals.", \
data.count()); +    if ( countInvalid > 0 ) {
+        resultItems += "<br/>" + i18ncp("@info", "<warning>%1 departure/arrival is \
invalid</warning>", +                                        "<warning>%1 \
departures/arrivals are invalid</warning>", countInvalid); +    }
+    if ( globalInfos.contains("no delays", Qt::CaseInsensitive) ) {
+        // No delay information available for the given stop
+        resultItems += "<br/>" + i18nc("@info", "Got the information from the script \
that there " +                                       "is no delay information \
available for the given stop."); +    }
+    
+    QString resultText = i18nc("@info", "No syntax errors.") + "<br/>" + \
resultItems; +	
+	// Add departures
+	if ( !departures.isEmpty() ) {
+		resultText += i18nc("@info", "<para>Departures:<list>%1</list></para>",
+							departures.join(QString()));
+	}
+	
+    // Add debug messages
+	if ( debugMessages.isEmpty() ) {
+		resultText += i18nc("@info", "<para>No messages from the script \
(helper.error)</para>"); +	} else {
+		QString debugMessagesString;
+		foreach ( const DebugMessage &debugMessage, debugMessages ) {
+			QString message = debugMessage.message;
+			debugMessagesString.append( QString("<item>%1</item>")
+					.arg(message.replace("<", "&lt;").replace(">", "&gt;")) ); \
//.arg(debugMessage.context.left(200)) ); +		}
+		resultText += i18nc("@info", "<para>Messages from the script \
(helper.error):<list>%1</list></para>", +							debugMessagesString);
+	}
+	
+    if ( !unknownTimetableInformations.isEmpty() ) {
+        resultText += i18nc("@info", "<para>There were unknown strings used for "
+                            "<icode>timetableData.set( '<placeholder>unknown \
string</placeholder>', " +                            \
"<placeholder>value</placeholder> );</icode>" +                            \
"<list>%1</list></para>", +                            \
unknownTimetableInformations.join(QString())); +    }
+    KMessageBox::information( this, resultText, i18nc("@title:window", "Result") );
+}
+
+void TimetableMate::scriptRunParseStopSuggestions() {
+    TimetableData timetableData;
+    ResultObject resultObject;
+    QVariant result;
+    DebugMessageList debugMessages;
+    if ( !scriptRun("parsePossibleStops", &timetableData, &resultObject, &result, \
&debugMessages) ) +        return;
+
+    // Get global infos
+    QStringList globalInfos;
+    if ( result.isValid() && result.canConvert(QVariant::StringList) ) {
+        globalInfos = result.toStringList();
+    }
+
+    // Get result set
+    QStringList stops;
+    QHash<QString, QString> stopToStopId;
+    QHash<QString, int> stopToStopWeight;
+
+    QList<TimetableData> data = resultObject.data();
+    int count = 0, countInvalid = 0;
+    foreach ( const TimetableData &timetableData, data ) {
+        QString stopName = timetableData.value( "stopname" ).toString();
+        QString stopID;
+        int stopWeight = -1;
+
+        if ( stopName.isEmpty() ) {
+            ++countInvalid;
+            continue;
+        }
+
+        stops << stopName;
+
+        if ( timetableData.values().contains("stopid") ) {
+            stopID = timetableData.value( "stopid" ).toString();
+            stopToStopId.insert( stopName, stopID );
+        }
+
+        if ( timetableData.values().contains("stopweight") )
+            stopWeight = timetableData.value( "stopweight" ).toInt();
+
+        if ( stopWeight != -1 )
+            stopToStopWeight.insert( stopName, stopWeight );
+        ++count;
+    }
+
+    QStringList stopInfo;
+    for ( int i = 0; i < stops.count(); ++i ) {
+        QString stop = stops.at( i );
+        QString stopItem = QString("\"%1\"").arg( stop );
+
+        if ( stopToStopId.contains(stop) )
+            stopItem += QString(", ID: %1").arg( stopToStopId[stop] );
+        if ( stopToStopWeight.contains(stop) )
+            stopItem += QString(", weight: %1").arg( stopToStopWeight[stop] );
+
+        stopInfo << QString("<item><emphasis strong='1'>%1.</emphasis> %2</item>")
+        .arg( i + 1 ).arg( stopItem );
+    }
+
+    QStringList unknownTimetableInformations;
+    QMultiHash<QString, QVariant> unknown = \
timetableData.unknownTimetableInformationStrings(); +    kDebug() << unknown;
+    for ( QMultiHash<QString, QVariant>::const_iterator it = unknown.constBegin();
+            it != unknown.constEnd(); ++it )
+    {
+        unknownTimetableInformations << "<item>" +
+        i18nc("@info", "'%1' with value '%2'", it.key(), it.value().toString()) + \
"</item>"; +    }
+
+    // Show results
+    QString resultItems = i18nc("@info", "Got %1 stop suggestions.", data.count());
+    if ( countInvalid > 0 ) {
+        resultItems += "<br/>" + i18ncp("@info", "<warning>%1 stop suggestion is \
invalid</warning>", +                                        "<warning>%1 stop \
suggestions are invalid</warning>", countInvalid); +    }
+    
+    QString resultText = i18nc("@info", "No syntax errors.") + "<br/>" + \
resultItems; +    resultText += i18nc("@info", "<para>Stop \
suggestions:<list>%1</list></para>", +                        \
stopInfo.join(QString())); +	
+    // Add debug messages
+	if ( debugMessages.isEmpty() ) {
+		resultText += i18nc("@info", "<para>No messages from the script \
(helper.error)</para>"); +	} else {
+		QString debugMessagesString;
+		foreach ( const DebugMessage &debugMessage, debugMessages ) {
+			QString message = debugMessage.message;
+			debugMessagesString.append( QString("<item>%1</item>")
+					.arg(message.replace("<", "&lt;").replace(">", "&gt;")) ); \
//.arg(debugMessage.context.left(200)) ); +		}
+		resultText += i18nc("@info", "<para>Messages from the script \
(helper.error):<list>%1</list></para>", +							debugMessagesString);
+	}
+	
+    if ( !unknownTimetableInformations.isEmpty() ) {
+        resultText += i18nc("@info", "<para>There were unknown strings used for "
+                            "<icode>timetableData.set( '<placeholder>unknown \
string</placeholder>', " +                            \
"<placeholder>value</placeholder> );</icode>" +                            \
"<list>%1</list></para>", +                            \
unknownTimetableInformations.join(QString())); +    }
+    KMessageBox::information( this, resultText, i18nc("@title:window", "Result") );
+}
+
+void TimetableMate::scriptRunParseJourneys() {
+    TimetableData timetableData;
+    ResultObject resultObject;
+    QVariant result;
+    DebugMessageList debugMessages;
+    if ( !scriptRun("parseJourneys", &timetableData, &resultObject, &result, \
&debugMessages) ) +        return;
+
+    // Get global infos
+    QStringList globalInfos;
+    if ( result.isValid() && result.canConvert(QVariant::StringList) ) {
+        globalInfos = result.toStringList();
+    }
+
+    // Get result set
+    QList<TimetableData> data = resultObject.data();
+    int count = 0, countInvalid = 0;
+    QDate curDate;
+    QTime lastTime;
+    for ( int i = 0; i < data.count(); ++ i ) {
+        TimetableData timetableData = data.at( i );
+        QDate date = timetableData.value( "departuredate" ).toDate();
+        QTime departureTime = QTime( timetableData.value("departurehour").toInt(),
+                                     timetableData.value("departureminute").toInt() \
); +        if ( !date.isValid() ) {
+            if ( curDate.isNull() ) {
+                // First departure
+                if ( QTime::currentTime().hour() < 3 && departureTime.hour() > 21 )
+                    date = QDate::currentDate().addDays( -1 );
+                else if ( QTime::currentTime().hour() > 21 && departureTime.hour() < \
3 ) +                    date = QDate::currentDate().addDays( 1 );
+                else
+                    date = QDate::currentDate();
+            } else if ( lastTime.secsTo(departureTime) < -5 * 60 ) {
+                // Time too much ealier than last time, estimate it's tomorrow
+                date = curDate.addDays( 1 );
+            } else {
+                date = curDate;
+            }
+
+            timetableData.set( "departuredate", date );
+        }
+
+        curDate = date;
+        lastTime = departureTime;
+
+        QVariantHash values = timetableData.values();
+        bool isValid = values.contains("startstopname") && \
values.contains("targetstopname") +                       && \
values.contains("departurehour") && values.contains("departureminute") +              \
&& values.contains("arrivalhour") && values.contains("arrivalminute"); +        if ( \
isValid ) +            ++count;
+        else
+            ++countInvalid;
+    }
+
+    QStringList journeys;
+    for ( int i = 0; i < data.count(); ++i ) {
+        QVariantHash values = data[i].values();
+        QString journey;
+        journey = QString("From \"%1\" (%3:%4) to \"%2\" (%5:%6)")
+                  .arg( values["startstopname"].toString(), \
values["targetstopname"].toString() ) +                  .arg( \
values["departurehour"].toInt(), 2, 10, QChar('0') ) +                  .arg( \
values["departureminute"].toInt(), 2, 10, QChar('0') ) +                  .arg( \
values["arrivalhour"].toInt(), 2, 10, QChar('0') ) +                  .arg( \
values["arrivalminute"].toInt(), 2, 10, QChar('0') ); +        if ( \
values.contains("changes") && !values["changes"].toString().isEmpty() ) { +           \
journey += QString(",<br> changes: %1").arg( values["changes"].toString() ); +        \
} +        if ( values.contains("typeofvehicle") && \
!values["typeofvehicle"].toString().isEmpty() ) { +            journey += \
QString(",<br> %1").arg( values["typeofvehicle"].toString() ); +        }
+        if ( values.contains("operator") && !values["operator"].toString().isEmpty() \
) { +            journey += QString(",<br> operator: %1").arg( \
values["operator"].toString() ); +        }
+        if ( values.contains("routestops") && \
!values["routestops"].toStringList().isEmpty() ) { +            QStringList \
routeStops = values["routestops"].toStringList(); +            journey += \
QString(",<br> %1 route stops: %2") +                       .arg( routeStops.count() \
).arg( routeStops.join(", ") ); +
+            // Check if RouteTimesDeparture has one element less than RouteStops
+            // and if RouteTimesDepartureDelay has the same number of elements as \
RouteStops (if set) +            if ( values.contains("routetimesdeparture")
+                    && !values["routetimesdeparture"].toStringList().isEmpty() )
+            {
+                QStringList routeTimesDeparture = \
values["routetimesdeparture"].toStringList(); +                journey += \
QString(",<br> %1 route departure times: %2") +                           .arg( \
routeTimesDeparture.count() ).arg( routeTimesDeparture.join(", ") ); +
+                if ( routeTimesDeparture.count() != routeStops.count() - 1 ) {
+                    journey += QString("<br> - <emphasis \
strong='1'>'RouteTimesDeparture' should " +                                       \
"contain one element less than 'RouteStops', because the last stop " +                \
"has no departure, only an arrival time</emphasis>"); +                }
+
+                if ( values.contains("routetimesdeparturedelay")
+                        && \
!values["routetimesdeparturedelay"].toStringList().isEmpty() ) +                {
+                    QStringList routeTimesDepartureDelay =
+                        values["routetimesdeparturedelay"].toStringList();
+                    journey += QString(",<br> %1 route departure delays: %2")
+                               .arg( routeTimesDepartureDelay.count() ).arg( \
routeTimesDepartureDelay.join(", ") ); +
+                    if ( routeTimesDepartureDelay.count() != \
routeTimesDeparture.count() ) { +                        journey += QString("<br> - \
<emphasis strong='1'>'RouteTimesDepartureDelay' " +                                   \
"should contain the same number of elements as " +                                    \
"'RouteTimesDeparture'</emphasis>"); +                    }
+                }
+            }
+
+            // Check if RoutePlatformsDeparture has one element less than RouteStops
+            if ( values.contains("routeplatformsdeparture")
+                    && !values["routeplatformsdeparture"].toStringList().isEmpty() )
+            {
+                QStringList routePlatformsArrival = \
values["routeplatformsdeparture"].toStringList(); +                journey += \
QString(",<br> %1 route departure platforms: %2") +                           .arg( \
routePlatformsArrival.count() ).arg( routePlatformsArrival.join(", ") ); +
+                if ( routePlatformsArrival.count() != routeStops.count() - 1 ) {
+                    journey += QString("<br> - <emphasis \
strong='1'>'RoutePlatformsDeparture' should " +                                       \
"contain one element less than 'RouteStops', because the last stop has " +            \
"no departure, only an arrival platform</emphasis>"); +                }
+            }
+
+            // Check if RouteTimesArrival has one element less than RouteStops
+            // and if RouteTimesArrivalDelay has the same number of elements as \
RouteStops (if set) +            if ( values.contains("routetimesarrival")
+                    && !values["routetimesarrival"].toStringList().isEmpty() )
+            {
+                QStringList routeTimesArrival = \
values["routetimesarrival"].toStringList(); +                journey += \
QString(",<br> %1 route arrival times: %2") +                           .arg( \
routeTimesArrival.count() ).arg( routeTimesArrival.join(", ") ); +
+                if ( routeTimesArrival.count() != routeStops.count() - 1 ) {
+                    journey += QString("<br> - <emphasis \
strong='1'>'RouteTimesArrival' should " +                                       \
"contain one element less than 'RouteStops', because the first stop " +               \
"has no arrival, only a departure time</emphasis>"); +                }
+
+                if ( values.contains("routetimesarrivaldelay")
+                        && \
!values["routetimesarrivaldelay"].toStringList().isEmpty() ) +                {
+                    QStringList routeTimesArrivalDelay =
+                        values["routetimesarrivaldelay"].toStringList();
+                    journey += QString(",<br> %1 route arrival delays: %2")
+                               .arg( routeTimesArrivalDelay.count() ).arg( \
routeTimesArrivalDelay.join(", ") ); +
+                    if ( routeTimesArrivalDelay.count() != routeTimesArrival.count() \
) { +                        journey += QString("<br> - <emphasis \
strong='1'>'RouteTimesArrivalDelay' " +                                           \
"should contain the same number of elements as " +                                    \
"'RouteTimesArrival'</emphasis>"); +                    }
+                }
+            }
+
+            // Check if RoutePlatformsArrival has one element less than RouteStops
+            if ( values.contains("routeplatformssarrival")
+                    && !values["routeplatformssarrival"].toStringList().isEmpty() )
+            {
+                QStringList routePlatformsArrival = \
values["routeplatformssarrival"].toStringList(); +                journey += \
QString(",<br> %1 route arrival platforms: %2") +                           .arg( \
routePlatformsArrival.count() ).arg( routePlatformsArrival.join(", ") ); +
+                if ( routePlatformsArrival.count() != routeStops.count() - 1 ) {
+                    journey += QString("<br> - <emphasis \
strong='1'>'RoutePlatformsArrival' should " +                                       \
"contain one element less than 'RouteStops', because the first stop has " +           \
"no arrival, only a departure platform</emphasis>"); +                }
+            }
+        }
+
+        journeys << QString("<item><emphasis strong='1'>%1.</emphasis> %2</item>")
+        .arg( i + 1 ).arg( journey );
+    }
+
+    QStringList unknownTimetableInformations;
+    QMultiHash<QString, QVariant> unknown = \
timetableData.unknownTimetableInformationStrings(); +    kDebug() << unknown;
+    for ( QMultiHash<QString, QVariant>::const_iterator it = unknown.constBegin();
+            it != unknown.constEnd(); ++it )
+    {
+        unknownTimetableInformations << "<item>" +
+        i18nc("@info", "'%1' with value '%2'", it.key(), it.value().toString()) + \
"</item>"; +    }
+
+    // Show results
+    QString resultItems = i18nc("@info", "Got %1 journeys.", data.count());
+    if ( countInvalid > 0 ) {
+        resultItems += "<br/>" + i18ncp("@info", "<warning>%1 journey is \
invalid</warning>", +                                        "<warning>%1 journeys \
are invalid</warning>", countInvalid); +    }
+    QString resultText = i18nc("@info", "No syntax errors.") + "<br/>" + \
resultItems; +    resultText += i18nc("@info", \
"<para>Journeys:<list>%1</list></para>", +                        \
journeys.join(QString())); +	
+    // Add debug messages
+	if ( debugMessages.isEmpty() ) {
+		resultText += i18nc("@info", "<para>No messages from the script \
(helper.error)</para>"); +	} else {
+		QString debugMessagesString;
+		foreach ( const DebugMessage &debugMessage, debugMessages ) {
+			QString message = debugMessage.message;
+			debugMessagesString.append( QString("<item>%1</item>")
+					.arg(message.replace("<", "&lt;").replace(">", "&gt;")) ); \
//.arg(debugMessage.context.left(200)) ); +		}
+		resultText += i18nc("@info", "<para>Messages from the script \
(helper.error):<list>%1</list></para>", +							debugMessagesString);
+	}
+	
+    if ( !unknownTimetableInformations.isEmpty() ) {
+        resultText += i18nc("@info", "<para>There were unknown strings used for "
+                            "<icode>timetableData.set( '<placeholder>unknown \
string</placeholder>', " +                            \
"<placeholder>value</placeholder> );</icode>" +                            \
"<list>%1</list></para>", +                            \
unknownTimetableInformations.join(QString())); +    }
+    KMessageBox::information( this, resultText, i18nc("@title:window", "Result") );
+}
+
+bool TimetableMate::hasHomePageURL( const TimetableAccessor &accessor ) {
+    if ( accessor.url.isEmpty() ) {
+        KMessageBox::information( this, i18nc("@info",
+                                              "The <interface>Home Page \
URL</interface> is empty.<nl/>" +                                              \
"Please set it in the <interface>Accessor</interface> tab first.") ); +        return \
false; +    } else
+        return true;
+}
+
+bool TimetableMate::hasRawDepartureURL( const TimetableAccessor &accessor ) {
+    if ( accessor.rawDepartureUrl.isEmpty() ) {
+        KMessageBox::information( this, i18nc("@info",
+                                              "The <interface>Raw Departure \
URL</interface> is empty.<nl/>" +                                              \
"Please set it in the <interface>Accessor</interface> tab first.") ); +        return \
false; +    } else
+        return true;
+}
+
+bool TimetableMate::hasRawStopSuggestionURL( const TimetableAccessor &accessor ) {
+    if ( accessor.rawStopSuggestionsUrl.isEmpty() ) {
+        KMessageBox::information( this, i18nc("@info",
+                                              "The <interface>Raw StopSuggestions \
URL</interface> is empty.<nl/>" +                                              \
"Please set it in the <interface>Accessor</interface> tab first.") ); +        return \
false; +    } else
+        return true;
+}
+
+bool TimetableMate::hasRawJourneyURL( const TimetableAccessor &accessor ) {
+    if ( accessor.rawJourneyUrl.isEmpty() ) {
+        KMessageBox::information( this, i18nc("@info",
+                                              "The <interface>Raw Journey \
URL</interface> is empty.<nl/>" +                                              \
"Please set it in the <interface>Accessor</interface> tab first.") ); +        return \
false; +    } else
+        return true;
+}
+
+bool TimetableMate::scriptRun( const QString &functionToRun, TimetableData \
*timetableData, +                               ResultObject *resultObject, QVariant \
*result,  +			       DebugMessageList *debugMessageList ) {
+    // Create the Kross::Action instance
+    Kross::Action script( this, "TimetableParser" );
+
+    // Add global script objects
+    if ( functionToRun == "parseTimetable" ) {
+        timetableData->setMode( "departures" );
+    } else if ( functionToRun == "parsePossibleStops" ) {
+        timetableData->setMode( "stopsuggestions" );
+    } else if ( functionToRun == "parseJourneys" ) {
+        timetableData->setMode( "journeys" );
+    } else  {
+        kDebug() << "Unknown funtion to run:" << functionToRun;
+    }
+    
+    // NOTE Update documentation in scripting.h if the names change here, 
+    // eg. update the name in the documentation of the Helper class if the name \
"helper" changes. +    Helper *helper = new Helper(&script);
+    script.addQObject( helper, "helper" );
+    script.addQObject( timetableData, "timetableData" );
+    script.addQObject( resultObject, "result" );
+
+    // Set script code and type
+    TimetableAccessor accessor = m_view->accessorInfo();
+    if ( accessor.scriptFile.endsWith(".py") ) {
+        script.setInterpreter( "python" );
+    } else if ( accessor.scriptFile.endsWith(".rb") ) {
+        script.setInterpreter( "ruby" );
+    } else if ( accessor.scriptFile.endsWith(".js") ) {
+        script.setInterpreter( "javascript" );
+    } else {
+        const QString scriptType = KInputDialog::getItem(
+				i18nc("@title:window", "Choose Script Type"),
+				i18nc("@info", "Script type unknown, please choose one of these:"),
+				QStringList() << "JavaScript" << "Ruby" << "Python", 0, false, 0, this );
+        script.setInterpreter( scriptType.toLower() );
+    }
+    script.setCode( m_scriptDocument->text().toUtf8() );
+
+    // Run the script
+    script.trigger();
+    if ( script.hadError() ) {
+        if ( script.errorLineNo() != -1 ) {
+            // Go to error line
+            m_scriptDocument->activeView()->setCursorPosition(
+                KTextEditor::Cursor(script.errorLineNo(), 0) );
+        }
+        KMessageBox::information( this, i18nc("@info", 
+				"Error in script:<nl/><message>%1</message>", script.errorMessage()) );
+        return false;
+    }
+
+    // Check if the function is implemented by the script
+    if ( !script.functionNames().contains(functionToRun) ) {
+        KMessageBox::information( this, i18nc("@info/plain",
+				"The script doesn't implement <emphasis>%1</emphasis>.", functionToRun) );
+        return false;
+    }
+
+    // Get the HTML document
+    KUrl url;
+    if ( functionToRun == "parseTimetable" ) {
+        if ( !hasRawDepartureURL(accessor) ) {
+            return false;
+        }
+        url = getDepartureUrl();
+    } else if ( functionToRun == "parsePossibleStops" ) {
+        if ( !hasRawStopSuggestionURL(accessor) ) {
+            return false;
+        }
+        url = getStopSuggestionUrl();
+    } else if ( functionToRun == "parseJourneys" ) {
+        if ( !hasRawJourneyURL(accessor) ) {
+            return false;
+        }
+        url = getJourneyUrl();
+    } else {
+        kDebug() << "Unknown funtion to run:" << functionToRun; // Shouldn't happen
+    }
+    if ( !url.isValid() ) {
+        return false; // The url placeholder dialog was cancelled
+    }
+
+    QString tempFileName;
+    if ( !KIO::NetAccess::download(url, tempFileName,  this) ) {
+        KMessageBox::information( this, i18nc("@info/plain",
+				"Couldn't download from URL <emphasis>%1</emphasis>.", url.prettyUrl()) );
+        return false;
+    }
+
+    QFile file( tempFileName );
+    if ( !file.open(QIODevice::ReadOnly) ) {
+        kDebug() << "Temporary file couldn't be opened" << tempFileName;
+        return false;
+    }
+    QString doc = decodeHtml( file.readAll() );
+    file.close();
+
+    int pos = doc.indexOf( "<body>", 0, Qt::CaseInsensitive );
+    if ( pos != -1 ) {
+        doc = doc.mid( pos );
+    }
+
+    // Call script using Kross
+    *result = script.callFunction( functionToRun, QVariantList() << doc );
+
+    if ( script.hadError() || script.isFinalized() ) {
+        if ( script.errorLineNo() != -1 ) {
+            // Go to error line
+            m_scriptDocument->activeView()->setCursorPosition(
+                KTextEditor::Cursor(script.errorLineNo(), 0) );
+        }
+        
+        KMessageBox::information( this, i18nc("@info", 
+				"An error occured in the script:<nl/><message>%1</message>", \
script.errorMessage()) ); +        return false;
+    }
+	
+    if ( debugMessageList ) {
+		debugMessageList->append( helper->debugMessages );
+    }
+
+    return true;
+}
+
+QString TimetableMate::decodeHtml( const QByteArray& document, const QByteArray& \
fallbackCharset ) { +    // Get charset of the received document and convert it to a \
unicode QString +    // First parse the charset with a regexp to get a fallback \
charset +    // if QTextCodec::codecForHtml doesn't find the charset
+    QString sDocument = QString( document );
+    QTextCodec *textCodec;
+    QRegExp rxCharset( "(?:<head>.*<meta http-equiv=\"Content-Type\" "
+                       "content=\"text/html; charset=)([^\"]*)(?:\"[^>]*>)", \
Qt::CaseInsensitive ); +    rxCharset.setMinimal( true );
+    if ( rxCharset.indexIn(sDocument) != -1 && rxCharset.isValid() ) {
+        textCodec = QTextCodec::codecForName( rxCharset.cap(1).trimmed().toUtf8() );
+    } else if ( !fallbackCharset.isEmpty() ) {
+        textCodec = QTextCodec::codecForName( fallbackCharset );
+    } else {
+        textCodec = QTextCodec::codecForName( "UTF-8" );
+    }
+    sDocument = QTextCodec::codecForHtml( document, textCodec )->toUnicode( document \
); +
+    return sDocument;
+}
+
+QString TimetableMate::gethex( ushort decimal ) {
+    QString hexchars = "0123456789ABCDEFabcdef";
+    return QChar('%') + hexchars[decimal >> 4] + hexchars[decimal & 0xF];
+}
+
+QString TimetableMate::toPercentEncoding( const QString &str, const QByteArray \
&charset ) { +    QString unreserved = \
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_.~"; +    QString \
encoded; +
+    QByteArray ba = QTextCodec::codecForName(charset)->fromUnicode(str);
+    for ( int i = 0; i < ba.length(); ++i ) {
+        char ch = ba[i];
+        if ( unreserved.indexOf(ch) != -1 ) {
+            encoded += ch;
+        } else if ( ch < 0 ) {
+            encoded += gethex(256 + ch);
+        } else {
+            encoded += gethex(ch);
+        }
+    }
+
+    return encoded;
+}
+
+KUrl TimetableMate::getDepartureUrl() {
+    TimetableAccessor accessor = m_view->accessorInfo();
+
+    QPointer<KDialog> dialog = new KDialog( this );
+    QWidget *w = new QWidget( dialog );
+    QFormLayout *l = new QFormLayout( w );
+    KLineEdit *city = NULL;
+    KLineEdit *stop = new KLineEdit( w );
+    KComboBox *dataType = new KComboBox( w );
+    KDateTimeWidget *dateTime = new KDateTimeWidget( QDateTime::currentDateTime(), w \
); +    dataType->addItem( i18nc("@info/plain", "Departures"), "departures" );
+    dataType->addItem( i18nc("@info/plain", "Arrivals"), "arrivals" );
+    if ( accessor.useCityValue ) {
+        city = new KLineEdit( w );
+        l->addRow( i18nc("@info", "City:"), city );
+    }
+    l->addRow( i18nc("@info", "Stop Name:"), stop );
+    l->addRow( i18nc("@info", "Data Type:"), dataType );
+    l->addRow( i18nc("@info", "Time:"), dateTime );
+    dialog->setMainWidget( w );
+    stop->setFocus();
+
+    KUrl url;
+    if ( dialog->exec() == KDialog::Accepted ) {
+        url = getDepartureUrl( accessor, city ? city->text() : QString(), \
stop->text(), +                               dateTime->dateTime(), \
dataType->itemData(dataType->currentIndex()).toString() ); +    }
+    delete dialog;
+
+    return url;
+}
+
+KUrl TimetableMate::getStopSuggestionUrl() {
+    TimetableAccessor accessor = m_view->accessorInfo();
+
+    QPointer<KDialog> dialog = new KDialog( this );
+    QWidget *w = new QWidget( dialog );
+    QFormLayout *l = new QFormLayout( w );
+    KLineEdit *city = NULL;
+    KLineEdit *stop = new KLineEdit( w );
+    if ( accessor.useCityValue ) {
+        city = new KLineEdit( w );
+        l->addRow( i18nc("@info", "City:"), city );
+    }
+    l->addRow( i18nc("@info", "Partial Stop Name:"), stop );
+    dialog->setMainWidget( w );
+    stop->setFocus();
+
+    KUrl url;
+    if ( dialog->exec() == KDialog::Accepted ) {
+        url = getStopSuggestionUrl( accessor, city ? city->text() : QString(), \
stop->text() ); +    }
+    delete dialog;
+
+    return url;
+}
+
+KUrl TimetableMate::getJourneyUrl() {
+    TimetableAccessor accessor = m_view->accessorInfo();
+
+    QPointer<KDialog> dialog = new KDialog( this );
+    QWidget *w = new QWidget( dialog );
+    QFormLayout *l = new QFormLayout( w );
+    KLineEdit *city = NULL;
+    KLineEdit *startStop = new KLineEdit( w );
+    KLineEdit *targetStop = new KLineEdit( w );
+    KComboBox *dataType = new KComboBox( w );
+    KDateTimeWidget *dateTime = new KDateTimeWidget( QDateTime::currentDateTime(), w \
); +    dataType->addItem( i18nc("@info/plain", "Departing at Given Time"), "dep" );
+    dataType->addItem( i18nc("@info/plain", "Arriving at Given Time"), "arr" );
+    if ( accessor.useCityValue ) {
+        city = new KLineEdit( w );
+        l->addRow( i18nc("@info", "City:"), city );
+    }
+    l->addRow( i18nc("@info", "Start Stop Name:"), startStop );
+    l->addRow( i18nc("@info", "Target Stop Name:"), targetStop );
+    l->addRow( i18nc("@info", "Time:"), dateTime );
+    l->addRow( i18nc("@info", "Meaning of Time:"), dataType );
+    dialog->setMainWidget( w );
+    startStop->setFocus();
+
+    KUrl url;
+    if ( dialog->exec() == KDialog::Accepted ) {
+        url = getJourneyUrl( accessor, city ? city->text() : QString(),
+                             startStop->text(), targetStop->text(), \
dateTime->dateTime(), +                             \
dataType->itemData(dataType->currentIndex()).toString() ); +    }
+    delete dialog;
+
+    return url;
+}
+
+KUrl TimetableMate::getDepartureUrl( const TimetableAccessor &accessor, const \
QString& city, +                                     const QString& stop, const \
QDateTime& dateTime, +                                     const QString& dataType, \
bool useDifferentUrl ) const { +    int maxCount = 20;
+    QString sRawUrl = useDifferentUrl ? accessor.rawStopSuggestionsUrl : \
accessor.rawDepartureUrl; +    QString sTime = dateTime.time().toString("hh:mm");
+    QString sDataType;
+    QString sCity = city.toLower(), sStop = stop.toLower();
+    if ( dataType == "arrivals" ) {
+        sDataType = "arr";
+    } else if ( dataType == "departures" ) {
+        sDataType = "dep";
+    }
+
+//     sCity = accessor.mapCityNameToValue( sCity ); // TODO
+
+    // Encode city and stop
+    if ( accessor.charsetForUrlEncoding.isEmpty() ) {
+        sCity = QString::fromAscii(QUrl::toPercentEncoding(sCity));
+        sStop = QString::fromAscii(QUrl::toPercentEncoding(sStop));
+    } else {
+        sCity = toPercentEncoding( sCity, accessor.charsetForUrlEncoding.toUtf8() );
+        sStop = toPercentEncoding( sStop, accessor.charsetForUrlEncoding.toUtf8() );
+    }
+
+    // Construct the url from the "raw" url by replacing values
+    if ( accessor.useCityValue )
+        sRawUrl = sRawUrl.replace( "{city}", sCity );
+    sRawUrl = sRawUrl.replace( "{time}", sTime )
+              .replace( "{maxCount}", QString("%1").arg(maxCount) )
+              .replace( "{stop}", sStop )
+              .replace( "{dataType}", sDataType );
+
+    QRegExp rx = QRegExp("\\{date:([^\\}]*)\\}", Qt::CaseInsensitive);
+    if ( rx.indexIn(sRawUrl) != -1 ) {
+        sRawUrl.replace( rx, dateTime.date().toString(rx.cap(1)) );
+    }
+
+    return KUrl( sRawUrl );
+}
+
+KUrl TimetableMate::getStopSuggestionUrl( const TimetableAccessor &accessor,
+        const QString &city, const QString& stop ) {
+    QString sRawUrl = accessor.rawStopSuggestionsUrl;
+    QString sCity = city.toLower(), sStop = stop.toLower();
+
+    // Encode stop
+    if ( accessor.charsetForUrlEncoding.isEmpty() ) {
+        sCity = QString::fromAscii(QUrl::toPercentEncoding(sCity));
+        sStop = QString::fromAscii(QUrl::toPercentEncoding(sStop));
+    } else {
+        sCity = toPercentEncoding( sCity, accessor.charsetForUrlEncoding.toUtf8() );
+        sStop = toPercentEncoding( sStop, accessor.charsetForUrlEncoding.toUtf8() );
+    }
+
+    if ( accessor.useCityValue ) {
+        sRawUrl = sRawUrl.replace( "{city}", sCity );
+    }
+    sRawUrl = sRawUrl.replace( "{stop}", sStop );
+
+    return KUrl( sRawUrl );
+}
+
+KUrl TimetableMate::getJourneyUrl( const TimetableAccessor &accessor, const QString& \
city, +                                   const QString& startStopName, const \
QString& targetStopName, +                                   const QDateTime \
&dateTime, const QString& dataType ) const { +    int maxCount = 20;
+    QString sRawUrl = accessor.rawJourneyUrl;
+    QString sTime = dateTime.time().toString("hh:mm");
+    QString sDataType;
+    QString sCity = city.toLower(), sStartStopName = startStopName.toLower(),
+                    sTargetStopName = targetStopName.toLower();
+    if ( dataType == "arrivals" || dataType == "journeysArr" )
+        sDataType = "arr";
+    else if ( dataType == "departures" || dataType == "journeysDep" )
+        sDataType = "dep";
+
+//     sCity = accessor.mapCityNameToValue(sCity); TODO
+
+    // Encode city and stop
+    if ( accessor.charsetForUrlEncoding.isEmpty() ) {
+        sCity = QString::fromAscii(QUrl::toPercentEncoding(sCity));
+        sStartStopName = QString::fromAscii( QUrl::toPercentEncoding(sStartStopName) \
); +        sTargetStopName = QString::fromAscii( \
QUrl::toPercentEncoding(sTargetStopName) ); +    } else {
+        sCity = toPercentEncoding( sCity, accessor.charsetForUrlEncoding.toUtf8() );
+        sStartStopName = toPercentEncoding( sStartStopName, \
accessor.charsetForUrlEncoding.toUtf8() ); +        sTargetStopName = \
toPercentEncoding( sTargetStopName, accessor.charsetForUrlEncoding.toUtf8() ); +    }
+
+    // Construct the url from the "raw" url by replacing values
+    if ( accessor.useCityValue ) {
+        sRawUrl = sRawUrl.replace( "{city}", sCity );
+    }
+    sRawUrl = sRawUrl.replace( "{time}", sTime )
+              .replace( "{maxCount}", QString("%1").arg(maxCount) )
+              .replace( "{startStop}", sStartStopName )
+              .replace( "{targetStop}", sTargetStopName )
+              .replace( "{dataType}", sDataType );
+
+    // Fill in date with the given format
+    QRegExp rx = QRegExp( "\\{date:([^\\}]*)\\}", Qt::CaseInsensitive );
+    if ( rx.indexIn(sRawUrl) != -1 ) {
+        sRawUrl.replace( rx, dateTime.date().toString(rx.cap(1)) );
+    }
+
+    rx = QRegExp( "\\{dep=([^\\|]*)\\|arr=([^\\}]*)\\}", Qt::CaseInsensitive );
+    if ( rx.indexIn(sRawUrl) != -1 ) {
+        if ( sDataType == "arr" ) {
+            sRawUrl.replace( rx, rx.cap(2) );
+        } else {
+            sRawUrl.replace( rx, rx.cap(1) );
+        }
+    }
+
+    return KUrl( sRawUrl );
+}
+
+#include "timetablemate.moc"
diff --git a/timetablemate/src/timetablematehelper.cpp \
b/timetablemate/src/timetablematehelper.cpp new file mode 100644
index 0000000..5e86140
--- /dev/null
+++ b/timetablemate/src/timetablematehelper.cpp
@@ -0,0 +1,69 @@
+/*
+*   Copyright 2010 Friedrich Pülz <fieti1983@gmx.de>
+*
+*   This program is free software; you can redistribute it and/or modify
+*   it under the terms of the GNU Library General Public License as
+*   published by the Free Software Foundation; either version 2 or
+*   (at your option) any later version.
+*
+*   This program is distributed in the hope that it will be useful,
+*   but WITHOUT ANY WARRANTY; without even the implied warranty of
+*   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+*   GNU General Public License for more details
+*
+*   You should have received a copy of the GNU Library General Public
+*   License along with this program; if not, write to the
+*   Free Software Foundation, Inc.,
+*   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+
+#include "timetablematehelper.h"
+
+#include <KAuth/HelperSupport>
+#include <QFile>
+#include <KDebug>
+
+KAuth::ActionReply TimetableMateHelper::install( const QVariantMap& map ) {
+    kDebug() << "BEGIN" << map;
+    KAuth::ActionReply reply;
+    const QString saveDir = map["path"].toString();
+    const QString accessorFileName = map["filenameAccessor"].toString();
+    const QString accessorDocument = map["contentsAccessor"].toString();
+    const QString scriptFileName = map["filenameScript"].toString();
+    const QString scriptDocument = map["contentsScript"].toString();
+
+    QFile accessorFile( accessorFileName );
+    if ( !accessorFile.open(QIODevice::WriteOnly) ) {
+	reply = ActionReply::HelperErrorReply;
+	reply.setErrorCode( accessorFile.error() );
+	reply.setErrorDescription( accessorFile.errorString() );
+	return reply;
+    }
+    if ( accessorFile.write(accessorDocument.toUtf8()) == -1 ) {
+	reply = ActionReply::HelperErrorReply;
+	reply.setErrorCode( accessorFile.error() );
+	reply.setErrorDescription( accessorFile.errorString() );
+	return reply;
+    }
+    accessorFile.close();
+
+    QFile scriptFile( scriptFileName );
+    if ( !scriptFile.open(QIODevice::WriteOnly) ) {
+	reply = ActionReply::HelperErrorReply;
+	reply.setErrorCode( scriptFile.error() );
+	reply.setErrorDescription( scriptFile.errorString() );
+	return reply;
+    }
+    if ( scriptFile.write(scriptDocument.toUtf8()) == -1 ) {
+	reply = ActionReply::HelperErrorReply;
+	reply.setErrorCode( scriptFile.error() );
+	reply.setErrorDescription( scriptFile.errorString() );
+	return reply;
+    }
+    scriptFile.close();
+
+    kDebug() << "END";
+    return reply;
+}
+
+KDE4_AUTH_HELPER_MAIN( "org.kde.timetablemate", TimetableMateHelper )
diff --git a/timetablemate/src/timetablemateview.cpp \
b/timetablemate/src/timetablemateview.cpp new file mode 100644
index 0000000..1aea553
--- /dev/null
+++ b/timetablemate/src/timetablemateview.cpp
@@ -0,0 +1,654 @@
+/*
+*   Copyright 2010 Friedrich Pülz <fpuelz@gmx.de>
+*
+*   This program is free software; you can redistribute it and/or modify
+*   it under the terms of the GNU Library General Public License as
+*   published by the Free Software Foundation; either version 2 or
+*   (at your option) any later version.
+*
+*   This program is distributed in the hope that it will be useful,
+*   but WITHOUT ANY WARRANTY; without even the implied warranty of
+*   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+*   GNU General Public License for more details
+*
+*   You should have received a copy of the GNU Library General Public
+*   License along with this program; if not, write to the
+*   Free Software Foundation, Inc.,
+*   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+*/
+
+#include "timetablemateview.h"
+#include "settings.h"
+#include "accessorinfoxmlreader.h"
+
+#include <QtGui/QLabel>
+#include <QMenu>
+#include <QBuffer>
+#include <QFile>
+
+#include <KFileDialog>
+#include <KMessageBox>
+#include <KInputDialog>
+#include <KColorScheme>
+#include <KLocale>
+#include <QSignalMapper>
+#include "changelogwidget.h"
+#include <QScrollArea>
+
+TimetableMateView::TimetableMateView( QWidget *parent ) : QWidget( parent ) {
+    ui_accessor.setupUi( this );
+    settingsChanged();
+
+    // Initialize script file buttons
+    ui_accessor.btnBrowseForScriptFile->setIcon( KIcon("document-open") );
+    ui_accessor.btnCreateScriptFile->setIcon( KIcon("document-new") );
+    ui_accessor.btnDetachScriptFile->setIcon( KIcon("list-remove") );
+    ui_accessor.btnDetachScriptFile->setVisible( false );
+    connect( ui_accessor.btnBrowseForScriptFile, SIGNAL(clicked(bool)),
+	     this, SLOT(browseForScriptFile()) );
+    connect( ui_accessor.btnCreateScriptFile, SIGNAL(clicked(bool)),
+	     this, SLOT(createScriptFile()) );
+    connect( ui_accessor.btnDetachScriptFile, SIGNAL(clicked(bool)),
+	     this, SLOT(detachScriptFile()) );
+
+    // Initialize the language button
+    ui_accessor.currentLanguage->loadAllLanguages();
+    ui_accessor.currentLanguage->insertLanguage( "en", QString(), 0 );
+    ui_accessor.currentLanguage->insertSeparator( 1 );
+    connect( ui_accessor.currentLanguage, SIGNAL(activated(QString)),
+	     this, SLOT(languageActivated(QString)) );
+
+    // Initialize the KEditListWidget for predefined cities
+    QWidget *repWidget = new QWidget( this );
+    QHBoxLayout *customEditorLayout = new QHBoxLayout( repWidget );
+    m_cityName = new KLineEdit( this );
+    m_cityReplacement = new KLineEdit( this );
+    QLabel *lblCityReplacement = new QLabel( i18nc("@info", "Replace with:"), this \
); +    lblCityReplacement->setAlignment( Qt::AlignRight | Qt::AlignVCenter );
+    customEditorLayout->addWidget( m_cityName );
+    customEditorLayout->addWidget( lblCityReplacement );
+    customEditorLayout->addWidget( m_cityReplacement );
+    KLineEdit *defaultLineEdit = new KLineEdit();
+    m_predefinedCitiesCustomEditor.setLineEdit( defaultLineEdit );
+    defaultLineEdit->hide();
+    
+    m_predefinedCitiesCustomEditor.setRepresentationWidget( repWidget );
+    ui_accessor.predefinedCities->setCustomEditor( m_predefinedCitiesCustomEditor );
+    connect( m_cityName, SIGNAL(textChanged(QString)),
+	     this, SLOT(predefinedCityNameChanged(QString)) );
+    connect( m_cityReplacement, SIGNAL(textChanged(QString)),
+	     this, SLOT(predefinedCityReplacementChanged(QString)) );
+    connect( defaultLineEdit, SIGNAL(textChanged(QString)),
+	     this, SLOT(currentPredefinedCityChanged(QString)) );
+
+    // Use negative text color for the warning labels
+    ui_accessor.lblFileVersionWarning->hide();
+    QPalette pal = ui_accessor.lblFileVersionWarning->palette();
+    KColorScheme::adjustForeground( pal, KColorScheme::NegativeText,
+				    QPalette::WindowText, KColorScheme::Window );
+    ui_accessor.lblFileVersionWarning->setPalette( pal );
+
+    // Set a validator for version line edits
+    QRegExpValidator *versionValidator = new QRegExpValidator( \
QRegExp("\\d*\\.\\d*"), this ); +    ui_accessor.version->setValidator( \
versionValidator ); +    ui_accessor.fileVersion->setValidator( versionValidator );
+    
+    // Set a validator for the email line edit
+    // The reg exp is "inspired" by http://www.regular-expressions.info/email.html
+    QRegExp rx( "[a-z0-9!#$%&\\._-]+@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z]{2,4}", \
Qt::CaseInsensitive ); +    QRegExpValidator *emailValidator = new QRegExpValidator( \
rx, this ); +    ui_accessor.email->setValidator( emailValidator );
+
+    // Set icons and connections for the "open url buttons"
+    ui_accessor.btnUrlOpen->setIcon( KIcon("document-open-remote") );
+    ui_accessor.btnDepartureUrlOpen->setIcon( KIcon("document-open-remote") );
+    ui_accessor.btnStopUrlOpen->setIcon( KIcon("document-open-remote") );
+    ui_accessor.btnJourneyUrlOpen->setIcon( KIcon("document-open-remote") );
+    connect( ui_accessor.btnUrlOpen, SIGNAL(clicked(bool)),
+	     this, SLOT(openUrlClicked()) );
+    connect( ui_accessor.btnDepartureUrlOpen, SIGNAL(clicked(bool)),
+	     this, SLOT(openDepartureUrlClicked()) );
+    connect( ui_accessor.btnStopUrlOpen, SIGNAL(clicked(bool)),
+	     this, SLOT(openStopUrlClicked()) );
+    connect( ui_accessor.btnJourneyUrlOpen, SIGNAL(clicked(bool)),
+	     this, SLOT(openJourneyUrlClicked()) );
+    
+    // Set icons for "insert placeholder buttons"
+    ui_accessor.btnDepartureUrlInsertPlaceHolder->setIcon( KIcon("tools-wizard") );
+    ui_accessor.btnJourneyUrlInsertPlaceHolder->setIcon( KIcon("tools-wizard") );
+    ui_accessor.btnStopUrlInsertPlaceHolder->setIcon( KIcon("tools-wizard") );
+
+    // Create "Add ... Placeholder" actions for the departure raw url
+    QMenu *departureMenu = new QMenu( this );
+    QAction *action;
+    action = new QAction( KIcon("public-transport-stop"),
+			  i18n("Add &Stop Name Placeholder"), this );
+    action->setData( "{stop}" );
+    departureMenu->addAction( action );
+    
+    action = new QAction( i18n("Add &City Name Placeholder"), this );
+    action->setData( "{city}" );
+    departureMenu->addAction( action );
+    
+    action = new QAction( i18n("Add &Departure Date Placeholder"), this );
+    action->setData( "{date:dd.MM.yy}" );
+    departureMenu->addAction( action );
+    
+    action = new QAction( KIcon("chronometer"), i18n("Add &Departure Time \
Placeholder"), this ); +    action->setData( "{time}" );
+    departureMenu->addAction( action );
+    
+    action = new QAction( i18n("Add Departure/&Arrival Placeholder"), this );
+    action->setData( "{dataType}" );
+    departureMenu->addAction( action );
+    connect( departureMenu, SIGNAL(triggered(QAction*)),
+	     this, SLOT(departurePlaceHolder(QAction*)) );
+    ui_accessor.btnDepartureUrlInsertPlaceHolder->setMenu( departureMenu );
+    
+    // Create "Add ... Placeholder" actions for the journey raw url
+    QMenu *journeyMenu = new QMenu( this );
+    action = new QAction( KIcon("flag-green"), i18n("Add &Start Stop Name \
Placeholder"), this ); +    action->setData( "{startStop}" );
+    journeyMenu->addAction( action );
+    
+    action = new QAction( KIcon("flag-red"), i18n("Add &Target Stop Name \
Placeholder"), this ); +    action->setData( "{targetStop}" );
+    journeyMenu->addAction( action );
+
+    action = new QAction( KIcon("chronometer"), i18n("Add &Departure Time \
Placeholder"), this ); +    action->setData( "{time}" );
+    journeyMenu->addAction( action );
+    connect( journeyMenu, SIGNAL(triggered(QAction*)), this, \
SLOT(journeyPlaceHolder(QAction*)) ); +    \
ui_accessor.btnJourneyUrlInsertPlaceHolder->setMenu( journeyMenu ); +    
+    // Create "Add ... Placeholder" actions for the stop suggestions raw url
+    QMenu *stopMenu = new QMenu( this );
+    action = new QAction( KIcon("public-transport-stop"),
+			  i18n("Add &Stop Name Placeholder"), this );
+    action->setData( "{stop}" );
+    stopMenu->addAction( action );
+    connect( stopMenu, SIGNAL(triggered(QAction*)),
+			 this, SLOT(stopSuggestionsPlaceHolder(QAction*)) );
+    ui_accessor.btnStopUrlInsertPlaceHolder->setMenu( stopMenu );
+	
+	// Add changelog widget into a scroll area
+	QVBoxLayout *changelogAreaLayout = new QVBoxLayout( ui_accessor.tabChangelog );
+	QScrollArea *changelogArea = new QScrollArea( ui_accessor.tabChangelog );
+	changelogArea->setFrameStyle( QFrame::NoFrame );
+	changelogArea->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Expanding );
+	changelogArea->setWidgetResizable( true );
+	changelogAreaLayout->addWidget( changelogArea );
+	
+	QWidget *changelogAreaWidget = new QWidget( changelogArea );
+	changelogArea->setWidget( changelogAreaWidget );
+	QVBoxLayout *changelogLayout = new QVBoxLayout( changelogAreaWidget );
+	m_changelog = new ChangelogWidget( changelogAreaWidget );
+	m_changelog->clear();
+	changelogLayout->addWidget( m_changelog );
+	changelogLayout->addStretch();
+
+    // Add vehicle types with icons to the default vehicle type combo box
+    ui_accessor.defaultVehicleType->addItem(
+	    KIcon("status_unknown"), i18nc("@item:listbox", "Unknown"), "Unknown" );
+    ui_accessor.defaultVehicleType->addItem(
+	    KIcon("vehicle_type_tram"), i18nc("@item:listbox", "Tram"), "Tram" );
+    ui_accessor.defaultVehicleType->addItem(
+	    KIcon("vehicle_type_bus"), i18nc("@item:listbox", "Bus"), "Bus" );
+    ui_accessor.defaultVehicleType->addItem(
+	    KIcon("vehicle_type_subway"), i18nc("@item:listbox", "Subway"), "Subway" );
+    ui_accessor.defaultVehicleType->addItem(
+	    KIcon("vehicle_type_train_interurban"),
+	    i18nc("@item:listbox", "Interurban Train"), "TrainInterurban" );
+    ui_accessor.defaultVehicleType->addItem(
+	    KIcon("vehicle_type_metro"), i18nc("@item:listbox", "Metro"), "Metro" );
+    ui_accessor.defaultVehicleType->addItem(
+	    KIcon("vehicle_type_trolleybus"),
+	    i18nc("@item:listbox", "Trolley Bus"), "TrolleyBus" );
+    ui_accessor.defaultVehicleType->addItem(
+	    KIcon("vehicle_type_train_regional"), // TODO: Currently no special icon
+		i18nc("@item:listbox", "Regional Train"), "TrainRegional" );
+    ui_accessor.defaultVehicleType->addItem(
+	    KIcon("vehicle_type_train_regional"),
+	    i18nc("@item:listbox", "Regional Express Train"), "TrainRegionalExpress" );
+    ui_accessor.defaultVehicleType->addItem(
+	    KIcon("vehicle_type_train_interregional"),
+	    i18nc("@item:listbox", "Interregional Train"), "TrainInterregio" );
+    ui_accessor.defaultVehicleType->addItem(
+	    KIcon("vehicle_type_train_intercity"),
+	    i18nc("@item:listbox", "Intercity/Eurocity Train"), "TrainIntercityEurocity" );
+    ui_accessor.defaultVehicleType->addItem(
+	    KIcon("vehicle_type_train_highspeed"),
+	    i18nc("@item:listbox", "Intercity Express Train"), "TrainIntercityExpress" );
+    ui_accessor.defaultVehicleType->addItem(
+	    KIcon("vehicle_type_ferry"), i18nc("@item:listbox", "Ferry"), "Ferry" );
+    ui_accessor.defaultVehicleType->addItem(
+	    KIcon("vehicle_type_ferry"), i18nc("@item:listbox", "Ship"), "Ship" );
+    ui_accessor.defaultVehicleType->addItem(
+	    KIcon("vehicle_type_plane"), i18nc("@item:listbox", "Plane"), "Plane" );
+
+    // Connect all change signals of the widgets to the changed() signal
+    m_mapper = new QSignalMapper( this );
+    connect( ui_accessor.name, SIGNAL(textChanged(QString)), m_mapper, SLOT(map()) \
); +    connect( ui_accessor.description, SIGNAL(textChanged()), m_mapper, \
SLOT(map()) ); +    connect( ui_accessor.version, SIGNAL(textChanged(QString)), \
m_mapper, SLOT(map()) ); +    connect( ui_accessor.type, \
SIGNAL(currentIndexChanged(int)), m_mapper, SLOT(map()) ); +    connect( \
ui_accessor.useCityValue, SIGNAL(stateChanged(int)), m_mapper, SLOT(map()) ); +    \
connect( ui_accessor.onlyAllowPredefinedCities, SIGNAL(stateChanged(int)), m_mapper, \
SLOT(map()) ); +    connect( ui_accessor.url, SIGNAL(textChanged(QString)), m_mapper, \
SLOT(map()) ); +    connect( ui_accessor.shortUrl, SIGNAL(textChanged(QString)), \
m_mapper, SLOT(map()) ); +    connect( ui_accessor.rawDepartureUrl, \
SIGNAL(textChanged(QString)), m_mapper, SLOT(map()) ); +    connect( \
ui_accessor.rawJourneyUrl, SIGNAL(textChanged(QString)), m_mapper, SLOT(map()) ); +   \
connect( ui_accessor.rawStopSuggestionsUrl, SIGNAL(textChanged(QString)), m_mapper, \
SLOT(map()) ); +    connect( ui_accessor.minFetchWait, SIGNAL(valueChanged(int)), \
m_mapper, SLOT(map()) ); +    connect( ui_accessor.scriptFile, \
SIGNAL(textChanged(QString)), m_mapper, SLOT(map()) ); +    connect( \
ui_accessor.author, SIGNAL(textChanged(QString)), m_mapper, SLOT(map()) ); +    \
connect( ui_accessor.shortAuthor, SIGNAL(textChanged(QString)), m_mapper, SLOT(map()) \
); +    connect( ui_accessor.email, SIGNAL(textChanged(QString)), m_mapper, \
SLOT(map()) ); +    connect( ui_accessor.defaultVehicleType, \
SIGNAL(currentIndexChanged(int)), m_mapper, SLOT(map()) ); +    connect( \
ui_accessor.fileVersion, SIGNAL(textChanged(QString)), m_mapper, SLOT(map()) ); +    \
connect( ui_accessor.predefinedCities, SIGNAL(changed()), m_mapper, SLOT(map()) ); +  \
connect( m_changelog, SIGNAL(added(QWidget*)), m_mapper, SLOT(map()) ); +    connect( \
m_changelog, SIGNAL(removed(QWidget*,int)), m_mapper, SLOT(map()) ); +    connect( \
m_changelog, SIGNAL(changed()), m_mapper, SLOT(map()) ); +	// TODO Map changes in the \
changelog +    m_mapper->setMapping( ui_accessor.name, ui_accessor.name );
+    m_mapper->setMapping( ui_accessor.description, ui_accessor.description );
+    m_mapper->setMapping( ui_accessor.version, ui_accessor.version );
+    m_mapper->setMapping( ui_accessor.type, ui_accessor.type );
+    m_mapper->setMapping( ui_accessor.useCityValue, ui_accessor.useCityValue );
+    m_mapper->setMapping( ui_accessor.onlyAllowPredefinedCities, \
ui_accessor.onlyAllowPredefinedCities ); +    m_mapper->setMapping( ui_accessor.url, \
ui_accessor.url ); +    m_mapper->setMapping( ui_accessor.shortUrl, \
ui_accessor.shortUrl ); +    m_mapper->setMapping( ui_accessor.rawDepartureUrl, \
ui_accessor.rawDepartureUrl ); +    m_mapper->setMapping( ui_accessor.rawJourneyUrl, \
ui_accessor.rawJourneyUrl ); +    m_mapper->setMapping( \
ui_accessor.rawStopSuggestionsUrl, ui_accessor.rawStopSuggestionsUrl ); +    \
m_mapper->setMapping( ui_accessor.minFetchWait, ui_accessor.minFetchWait ); +    \
m_mapper->setMapping( ui_accessor.scriptFile, ui_accessor.scriptFile ); +    \
m_mapper->setMapping( ui_accessor.author, ui_accessor.author ); +    \
m_mapper->setMapping( ui_accessor.shortAuthor, ui_accessor.shortAuthor ); +    \
m_mapper->setMapping( ui_accessor.email, ui_accessor.email ); +    \
m_mapper->setMapping( ui_accessor.defaultVehicleType, ui_accessor.defaultVehicleType \
); +    m_mapper->setMapping( ui_accessor.fileVersion, ui_accessor.fileVersion );
+    m_mapper->setMapping( ui_accessor.predefinedCities, ui_accessor.predefinedCities \
); +    m_mapper->setMapping( m_changelog, m_changelog );
+    connect( m_mapper, SIGNAL(mapped(QWidget*)), this, SLOT(slotChanged(QWidget*)) \
); +
+    fillValuesFromWidgets();
+}
+
+TimetableMateView::~TimetableMateView() {
+
+}
+
+void TimetableMateView::slotChanged( QWidget* changedWidget ) {
+    if ( changedWidget == ui_accessor.scriptFile ) {
+		// Script file changed
+		const QString fileName = ui_accessor.scriptFile->text();
+		ui_accessor.btnCreateScriptFile->setVisible( fileName.isEmpty() );
+		ui_accessor.btnDetachScriptFile->setVisible( !fileName.isEmpty() );
+		emit scriptFileChanged( fileName );
+    } else if ( changedWidget == ui_accessor.fileVersion ) {
+		// File version changed
+		if ( ui_accessor.fileVersion->text() != "1.0" ) {
+			ui_accessor.lblFileVersionWarning->setText( i18nc("@info",
+				"The PublicTransport data engine currently only supports version '1.0'.") );
+			ui_accessor.lblFileVersionWarning->show();
+		} else {
+			ui_accessor.lblFileVersionWarning->hide();
+		}
+    } else if ( changedWidget == ui_accessor.url ) {
+		// Home page URL changed
+		ui_accessor.btnUrlOpen->setDisabled( ui_accessor.url->text().isEmpty() );
+    } else if ( changedWidget == ui_accessor.rawDepartureUrl ) {
+		// Raw departure URL changed
+		QString newUrl = ui_accessor.rawDepartureUrl->text();
+		ui_accessor.btnDepartureUrlOpen->setDisabled( newUrl.isEmpty() );
+
+		bool hasCityPlaceholder = newUrl.contains("{city}")
+			|| ui_accessor.rawJourneyUrl->text().contains("{city}");
+		ui_accessor.useCityValue->setChecked( hasCityPlaceholder );
+		ui_accessor.predefinedCities->setEnabled( hasCityPlaceholder );
+    } else if ( changedWidget == ui_accessor.rawStopSuggestionsUrl ) {
+		// Raw stop suggestions URL changed
+		ui_accessor.btnStopUrlOpen->setDisabled( \
ui_accessor.rawStopSuggestionsUrl->text().isEmpty() ); +    } else if ( changedWidget \
== ui_accessor.rawJourneyUrl ) { +		// Raw journey URL changed
+		QString newUrl = ui_accessor.rawJourneyUrl->text();
+		ui_accessor.btnJourneyUrlOpen->setDisabled( newUrl.isEmpty() );
+
+		bool hasCityPlaceholder = newUrl.contains("{city}")
+			|| ui_accessor.rawDepartureUrl->text().contains("{city}");
+		ui_accessor.useCityValue->setChecked( hasCityPlaceholder );
+		ui_accessor.predefinedCities->setEnabled( hasCityPlaceholder );
+    } else if ( changedWidget == ui_accessor.shortAuthor ) {
+		// Short author name changed, update changed log click messages
+		QList<ChangelogEntryWidget*> entryWidgets = m_changelog->entryWidgets();
+		foreach ( const ChangelogEntryWidget *entryWidget, entryWidgets ) {
+			entryWidget->authorLineEdit()->setClickMessage( m_accessor.shortAuthor );
+		}
+    }
+    
+    fillValuesFromWidgets();
+    emit changed();
+}
+
+void TimetableMateView::fillValuesFromWidgets() {
+    // Fill struct with current values of the widgets
+    QString lang = ui_accessor.currentLanguage->current();
+    if ( lang == "en_US" ) {
+		lang = "en";
+	}
+    m_accessor.name[lang] = ui_accessor.name->text();
+    m_accessor.description[lang] = ui_accessor.description->toPlainText();
+    m_accessor.version = ui_accessor.version->text();
+    m_accessor.type = static_cast<AccessorType>( ui_accessor.type->currentIndex() + \
1 ); +    m_accessor.useCityValue = ui_accessor.useCityValue->isChecked();
+    m_accessor.onlyUseCitiesInList = \
ui_accessor.onlyAllowPredefinedCities->isChecked(); +    m_accessor.url = \
ui_accessor.url->text(); +    m_accessor.shortUrl = ui_accessor.shortUrl->text();
+    m_accessor.rawDepartureUrl = ui_accessor.rawDepartureUrl->text();
+    m_accessor.rawJourneyUrl = ui_accessor.rawJourneyUrl->text();
+    m_accessor.rawStopSuggestionsUrl = ui_accessor.rawStopSuggestionsUrl->text();
+    m_accessor.minFetchWait = ui_accessor.minFetchWait->value();
+    m_accessor.scriptFile = ui_accessor.scriptFile->text();
+    m_accessor.author = ui_accessor.author->text();
+    m_accessor.shortAuthor = ui_accessor.shortAuthor->text();
+    m_accessor.email = ui_accessor.email->text();
+    m_accessor.defaultVehicleType = ui_accessor.defaultVehicleType->itemData(
+	    ui_accessor.defaultVehicleType->currentIndex() ).toString();
+    m_accessor.fileVersion = ui_accessor.fileVersion->text();
+	m_accessor.changelog = m_changelog->changelog();
+
+    m_accessor.cities.clear();
+    m_accessor.cityNameReplacements.clear();
+    const QStringList cityReplacements = ui_accessor.predefinedCities->items();
+    foreach ( const QString &cityReplacement, cityReplacements ) {
+		QStringList values = cityReplacement.split( "   ->   " );
+		if ( values.count() == 2 ) {
+			m_accessor.cities << values.at( 0 );
+			m_accessor.cityNameReplacements.insert( values.at(0).toLower(), values.at(1) );
+		} else {
+			m_accessor.cities << cityReplacement;
+		}
+    }
+}
+
+void TimetableMateView::currentPredefinedCityChanged( const QString& currentCityText \
) { +    QStringList values = currentCityText.split( "   ->   " );
+    if ( values.count() == 2 ) {
+		m_cityName->blockSignals( true );
+		m_cityReplacement->blockSignals( true );
+		
+		m_cityName->setText( values.at(0) );
+		m_cityReplacement->setText( values.at(1) );
+		
+		m_cityName->blockSignals( false );
+		m_cityReplacement->blockSignals( false );
+    } else {
+		m_cityName->blockSignals( true );
+		m_cityReplacement->blockSignals( true );
+		
+		m_cityName->setText( currentCityText );
+		m_cityReplacement->setText( QString() );
+		
+		m_cityName->blockSignals( false );
+		m_cityReplacement->blockSignals( false );
+    }
+}
+
+void TimetableMateView::predefinedCityNameChanged( const QString& newCityName ) {
+    QString text = newCityName;
+    if ( !m_cityReplacement->text().isEmpty() ) {
+		text += "   ->   " + m_cityReplacement->text();
+	}
+    
+    m_predefinedCitiesCustomEditor.lineEdit()->setText( text );
+}
+
+void TimetableMateView::predefinedCityReplacementChanged( const QString& \
newReplacement ) { +    QString text = m_cityName->text();
+    if ( !newReplacement.isEmpty() ) {
+		text += "   ->   " + newReplacement;
+	}
+
+    m_predefinedCitiesCustomEditor.lineEdit()->setText( text );
+}
+
+void TimetableMateView::languageActivated( const QString& languageCode ) {
+    QString code = languageCode == "en_US" ? "en" : languageCode;
+    
+    ui_accessor.name->blockSignals( true );
+    ui_accessor.name->setText( m_accessor.name.value(code) );
+    ui_accessor.name->blockSignals( false );
+
+    ui_accessor.description->blockSignals( true );
+    ui_accessor.description->setText( m_accessor.description.value(code) );
+    ui_accessor.description->blockSignals( false );
+}
+
+void TimetableMateView::openUrlClicked() {
+    emit urlShouldBeOpened( ui_accessor.url->text() );
+}
+
+void TimetableMateView::openDepartureUrlClicked() {
+    emit urlShouldBeOpened( ui_accessor.rawDepartureUrl->text(), RawDepartureUrl );
+}
+
+void TimetableMateView::openStopUrlClicked() {
+    emit urlShouldBeOpened( ui_accessor.rawStopSuggestionsUrl->text(),
+			    RawStopSuggestionsUrl );
+}
+
+void TimetableMateView::openJourneyUrlClicked() {
+    emit urlShouldBeOpened( ui_accessor.rawJourneyUrl->text(), RawJourneyUrl );
+}
+
+void TimetableMateView::createScriptFile() {
+    if ( m_openedPath.isEmpty() ) {
+		KMessageBox::information( this, i18nc("@info/plain", "Please save the "
+			"XML file first. The script file needs to be in the same folder.") );
+		return;
+    }
+
+    // Get a name for the new script file based on the current country code 
+    // and the current service provider ID
+    const QString scriptType = KInputDialog::getItem(
+	    i18nc("@title:window", "Choose Script Type"), i18nc("@info", "Script Type"),
+	    QStringList() << "JavaScript" << "Ruby" << "Python", 0, false, 0, this );
+    QString scriptFile = m_currentServiceProviderID;
+    if ( scriptType == "JavaScript" ) {
+		scriptFile += ".js";
+	} else if ( scriptType == "Ruby" ) {
+		scriptFile += ".rb";
+	} else if ( scriptType == "Python" ) {
+		scriptFile += ".py";
+	}
+
+    // Get fileName for the new script file
+    QString fullScriptFile = KUrl(m_openedPath).directory(KUrl::AppendTrailingSlash) \
+ scriptFile; +
+    // Check if the file already exists
+    QFile file( fullScriptFile );
+    if ( file.exists() ) {
+		int result = KMessageBox::questionYesNoCancel( this,
+			i18nc("@info/plain", "The script file <filename>%1</filename> already \
exists.<nl/>" +				"Do you want to overwrite it or open and use it as script file?", \
scriptFile), +			i18nc("@title:window", "File Already Exists"),
+			KStandardGuiItem::overwrite(), KStandardGuiItem::open() );
+		if ( result == KMessageBox::No ) { // open
+			ui_accessor.scriptFile->setText( scriptFile );
+			return;
+		} else if ( result == KMessageBox::Cancel ) {
+			return;
+		}
+    }
+    
+    // Create the file    
+    if ( !file.open(QIODevice::WriteOnly) ) {
+		KMessageBox::information( this, i18nc("@info/plain", "A new script file "
+			"with the name <filename>%1</filename> couldn't be created.",
+			fullScriptFile) );
+		return;
+    }
+    file.close();
+    
+    ui_accessor.scriptFile->setText( scriptFile );
+    emit scriptAdded( fullScriptFile );
+}
+
+void TimetableMateView::detachScriptFile() {
+    ui_accessor.scriptFile->setText( QString() );
+}
+
+void TimetableMateView::browseForScriptFile() {
+    if ( m_openedPath.isEmpty() ) {
+		KMessageBox::information( this, i18nc("@info/plain", "Please save the "
+			"XML file first. The script file needs to be in the same folder.") );
+		return;
+    }
+    
+    KUrl openedUrl( m_openedPath );
+
+    // Get a list of all script files in the directory of the XML file
+    QStringList scriptFiles;
+    int current = -1;
+    QDir dir( openedUrl.directory() );
+    QStringList fileNames = dir.entryList();
+    for ( int i = 0; i < fileNames.count(); ++i ) {
+		QString fileName = fileNames.at( i );
+		KMimeType::Ptr mimeType = KMimeType::findByUrl( KUrl(fileName) );
+		if ( mimeType->is("application/javascript")
+			|| mimeType->is("application/x-ruby") || mimeType->is("text/x-python") )
+		{
+			scriptFiles << fileName;
+			if ( fileName == ui_accessor.scriptFile->text() ) {
+				current = i;
+			}
+		}
+    }
+
+    bool ok;
+    QString selectedFile = KInputDialog::getItem( i18nc("@title:window", "Choose \
Script File"), +			   i18nc("@info", "Script File for Parsing Documents"),
+			   scriptFiles, current, false, &ok, this );
+    if ( ok ) {
+		ui_accessor.scriptFile->setText( selectedFile );
+    }
+}
+
+void TimetableMateView::departurePlaceHolder( QAction *action ) {
+    QString placeholder = action->data().toString();
+    ui_accessor.rawDepartureUrl->insert( placeholder );
+}
+
+void TimetableMateView::journeyPlaceHolder( QAction *action ) {
+    QString placeholder = action->data().toString();
+    ui_accessor.rawJourneyUrl->insert( placeholder );
+}
+
+void TimetableMateView::stopSuggestionsPlaceHolder( QAction *action ) {
+    QString placeholder = action->data().toString();
+    ui_accessor.rawStopSuggestionsUrl->insert( placeholder );
+}
+
+TimetableAccessor TimetableMateView::accessorInfo() const {
+    return m_accessor;
+}
+
+void TimetableMateView::setScriptFile( const QString& scriptFile ) {
+    ui_accessor.scriptFile->setText( scriptFile );
+}
+
+bool TimetableMateView::readAccessorInfoXml( const QString& fileName, QString *error \
) { +    QFile file( fileName );
+    return readAccessorInfoXml( &file, error, fileName );
+}
+
+bool TimetableMateView::readAccessorInfoXml( QIODevice* device, QString *error,
+					     const QString& fileName ) {
+    AccessorInfoXmlReader reader;
+    m_accessor = reader.read( device );
+    if ( !m_accessor.isValid() ) {
+		kDebug() << "Accessor is invalid" << reader.errorString() << fileName;
+		if ( error ) {
+			*error = reader.errorString();
+		}
+		return false;
+    }
+
+    // Disable changed signals from widgets while setting the read values
+    m_mapper->blockSignals( true );
+    
+    m_openedPath = fileName;
+    ui_accessor.currentLanguage->setCurrentItem( "en" );
+    ui_accessor.name->setText( m_accessor.name["en"] );
+    ui_accessor.description->setText( m_accessor.description["en"] );
+    ui_accessor.version->setText( m_accessor.version );
+    ui_accessor.type->setCurrentIndex( static_cast<int>(m_accessor.type) - 1 );
+    ui_accessor.useCityValue->setChecked( m_accessor.useCityValue );
+    ui_accessor.onlyAllowPredefinedCities->setChecked( \
m_accessor.onlyUseCitiesInList ); +    ui_accessor.url->setText( m_accessor.url );
+    ui_accessor.shortUrl->setText( m_accessor.shortUrl );
+    ui_accessor.rawDepartureUrl->setText( m_accessor.rawDepartureUrl );
+    ui_accessor.rawJourneyUrl->setText( m_accessor.rawJourneyUrl );
+    ui_accessor.rawStopSuggestionsUrl->setText( m_accessor.rawStopSuggestionsUrl );
+    ui_accessor.minFetchWait->setValue( m_accessor.minFetchWait );
+    ui_accessor.scriptFile->setText( m_accessor.scriptFile );
+    ui_accessor.author->setText( m_accessor.author );
+    ui_accessor.shortAuthor->setText( m_accessor.shortAuthor );
+    ui_accessor.email->setText( m_accessor.email );
+    int defaultVehicleTypeIndex =
+	    ui_accessor.defaultVehicleType->findData( m_accessor.defaultVehicleType );
+    ui_accessor.defaultVehicleType->setCurrentIndex(
+	    defaultVehicleTypeIndex > 0 ? defaultVehicleTypeIndex : 0 );
+    ui_accessor.fileVersion->setText( m_accessor.fileVersion );
+	m_changelog->clear();
+	m_changelog->addChangelog( m_accessor.changelog, m_accessor.shortAuthor );
+
+    ui_accessor.predefinedCities->clear();
+    foreach ( const QString &city, m_accessor.cities ) {
+		QString lowerCity = city.toLower();
+		if ( m_accessor.cityNameReplacements.contains(lowerCity) ) {
+			ui_accessor.predefinedCities->insertItem( city + "   ->   " +
+				m_accessor.cityNameReplacements[lowerCity] );
+		} else {
+			ui_accessor.predefinedCities->insertItem( city );
+		}
+    }
+
+    // Enable changed signals from widgets again and emit changed signals once
+    m_mapper->blockSignals( false );
+    emit changed();
+    emit scriptFileChanged( fileName );
+    
+    if ( error ) {
+		error->clear();
+	}
+    return true;
+}
+
+bool TimetableMateView::writeAccessorInfoXml( const QString& fileName ) {
+    AccessorInfoXmlWriter writer;
+    QFile file( fileName );
+    bool ret = writer.write( &file, m_accessor );
+    if ( ret ) {
+		m_openedPath = fileName;
+	}
+    return ret;
+}
+
+QString TimetableMateView::writeAccessorInfoXml() {
+    AccessorInfoXmlWriter writer;
+    QBuffer buffer;
+    if ( writer.write(&buffer, m_accessor) ) {
+		return QString::fromUtf8( buffer.data() );
+	} else {
+		return QString();
+	}
+}
+
+void TimetableMateView::settingsChanged() {
+    emit signalChangeStatusbar( i18n("Settings changed") );
+}
+
+#include "timetablemateview.moc"


[prev in list] [next in list] [prev in thread] [next in thread] 

Configure | About | News | Add a list | Sponsored by KoreLogic