[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 -> Install</interface> to install the accessor locally " + \
"or <interface>File -> 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("<", "<").replace(">", ">")) ); \
//.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("<", "<").replace(">", ">")) ); \
//.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("<", "<").replace(">", ">")) ); \
//.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