diff --git a/src/Athlete.cpp b/src/Athlete.cpp index 73139c311..66406d4cb 100644 --- a/src/Athlete.cpp +++ b/src/Athlete.cpp @@ -47,6 +47,7 @@ #include "Route.h" #include "RouteWindow.h" #include "RideImportWizard.h" +#include "RideAutoImportConfig.h" #include "GcUpgrade.h" // upgrade wizard @@ -114,6 +115,9 @@ Athlete::Athlete(Context *context, const QDir &homeDir) } } + // read athlete's autoimport configuration + autoImportConfig = new RideAutoImportConfig(home->config()); + // read athlete's charts.xml and translate etc LTMSettings reader; reader.readChartXML(context->athlete->home->config(), context->athlete->useMetricUnits, presets); @@ -543,39 +547,12 @@ Athlete::configChanged() } void -Athlete::importFilesWithoutDialog() { +Athlete::importFilesWhenOpeningAthlete() { - int importSettings = appsettings->cvalue(context->athlete->cyclist, GC_IMPORTSETTINGS).toInt(); // default/unset = 0 - if (importSettings == 0) return; // no autoimport requested + // just do it if something is configured + if (autoImportConfig->hasRules()) { - QVariant importDirQV = appsettings->cvalue(context->athlete->cyclist, GC_IMPORTDIR, ""); - QString importDirectory = importDirQV.toString(); - if (importDirectory == "") return; // no implicit assumptions on the directory - only explicite is allowed - - // now get the files formats - const RideFileFactory &rff = RideFileFactory::instance(); - QStringList suffixList = rff.suffixes(); - suffixList.replaceInStrings(QRegExp("^"), "*."); - QStringList allFormats; - QFileInfoList fileInfos; - foreach(QString suffix, rff.suffixes()) - allFormats << QString("*.%1").arg(suffix); - - // and now the files for the GC formats / no SubDirs considered - QDir *importDir = new QDir (importDirectory); - if (!importDir->exists()) return; // directory might not be available (USB,..) - if (!importDir->isReadable()) return; // check if directory is readable, - - // now get the files with their full names - fileInfos = importDir->entryInfoList(allFormats, QDir::Files, QDir::NoSort); - if (!fileInfos.isEmpty()) { - QStringList fileNames; - foreach(QFileInfo f, fileInfos) { - fileNames.append(f.absoluteFilePath()); - } - RideImportWizard *import = new RideImportWizard(fileNames, context); - if (importSettings == 1) import->setDialogMode(RideImportWizard::allButDupFileErrors); - if (importSettings == 2) import->setDialogMode(RideImportWizard::allErrors); + RideImportWizard *import = new RideImportWizard(autoImportConfig, context); import->process(); } } diff --git a/src/Athlete.h b/src/Athlete.h index 07d50c5a2..ecec1a0ec 100644 --- a/src/Athlete.h +++ b/src/Athlete.h @@ -51,6 +51,7 @@ class PDEstimate; class LTMSettings; class Routes; class AthleteDirectoryStructure; +class RideAutoImportConfig; class Context; @@ -96,6 +97,9 @@ class Athlete : public QObject CalDAV *davCalendar; #endif + // Athlete's autoimport configuration + RideAutoImportConfig *autoImportConfig; + // ride metadata definitions RideMetadata *rideMetadata() { return rideMetadata_; } @@ -133,7 +137,7 @@ class Athlete : public QObject void notifyNamedSearchesChanged() { namedSearchesChanged(); } // import rides from athlete specific directory - void importFilesWithoutDialog(); + void importFilesWhenOpeningAthlete(); signals: void zonesChanged(); diff --git a/src/ConfigDialog.cpp b/src/ConfigDialog.cpp index 1046faf1c..bbade0046 100644 --- a/src/ConfigDialog.cpp +++ b/src/ConfigDialog.cpp @@ -265,6 +265,7 @@ AthleteConfig::AthleteConfig(QDir home, Zones *zones, Context *context) : zonePage = new ZonePage(context); hrZonePage = new HrZonePage(context); paceZonePage = new PaceZonePage(context); + autoImportPage = new AutoImportPage(context); setContentsMargins(0,0,0,0); QHBoxLayout *mainLayout = new QHBoxLayout(this); @@ -276,6 +277,7 @@ AthleteConfig::AthleteConfig(QDir home, Zones *zones, Context *context) : tabs->addTab(zonePage, tr("Power Zones")); tabs->addTab(hrZonePage, tr("Heartrate Zones")); tabs->addTab(paceZonePage, tr("Pace Zones")); + tabs->addTab(autoImportPage, tr("Auto Import")); mainLayout->addWidget(tabs); } @@ -286,6 +288,7 @@ void AthleteConfig::saveClicked() zonePage->saveClicked(); hrZonePage->saveClicked(); paceZonePage->saveClicked(); + autoImportPage->saveClicked(); } // APPEARANCE CONFIG diff --git a/src/ConfigDialog.h b/src/ConfigDialog.h index d2b1eeb3f..ab76ff30c 100644 --- a/src/ConfigDialog.h +++ b/src/ConfigDialog.h @@ -75,6 +75,7 @@ class AthleteConfig : public QWidget ZonePage *zonePage; HrZonePage *hrZonePage; PaceZonePage *paceZonePage; + AutoImportPage *autoImportPage; }; // APPEARANCE PAGE diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 43bef1e3c..af695d573 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -1453,7 +1453,7 @@ MainWindow::openTab(QString name) setUpdatesEnabled(true); // now do the automatic ride file import - context->athlete->importFilesWithoutDialog(); + context->athlete->importFilesWhenOpeningAthlete(); } void @@ -1979,7 +1979,7 @@ MainWindow::addIntervals() void MainWindow::ridesAutoImport() { - currentTab->context->athlete->importFilesWithoutDialog(); + currentTab->context->athlete->importFilesWhenOpeningAthlete(); } diff --git a/src/Pages.cpp b/src/Pages.cpp index 19049e7b8..67e4a084a 100644 --- a/src/Pages.cpp +++ b/src/Pages.cpp @@ -34,6 +34,7 @@ #include "SpecialFields.h" #include "DataProcessor.h" #include "OAuthDialog.h" +#include "RideAutoImportConfig.h" // // Main Config Page - tabs for each sub-page @@ -797,20 +798,6 @@ RiderPage::RiderPage(QWidget *parent, Context *context) : QWidget(parent), conte avatarButton->setFixedHeight(140); avatarButton->setFixedWidth(140); - QVariant importDir = appsettings->cvalue(context->athlete->cyclist, GC_IMPORTDIR, ""); - - importLabel = new QLabel(tr("Auto Import from")); - importDirectory = new QLineEdit; - importDirectory->setText(importDir.toString()); - importBrowseButton = new QPushButton(tr("Browse")); - importBrowseButton->setFixedWidth(120); - - importSetting = new QComboBox(); - importSetting->addItem("No auto import"); - importSetting->addItem("No duplicate file errors"); - importSetting->addItem("All errors"); - importSetting->setCurrentIndex(appsettings->cvalue(context->athlete->cyclist, GC_IMPORTSETTINGS).toInt()); // default/unset = 0 - Qt::Alignment alignment = Qt::AlignLeft|Qt::AlignVCenter; grid->addWidget(nicklabel, 0, 0, alignment); @@ -833,17 +820,11 @@ RiderPage::RiderPage(QWidget *parent, Context *context) : QWidget(parent), conte grid->addWidget(avatarButton, 0, 2, 4, 2, Qt::AlignRight|Qt::AlignVCenter); - grid->addWidget(importLabel, 8,0, alignment); - grid->addWidget(importDirectory, 8,1, alignment); - grid->addWidget(importBrowseButton, 8,2, alignment); - grid->addWidget(importSetting, 8,3, alignment); - all->addLayout(grid); all->addStretch(); connect (avatarButton, SIGNAL(clicked()), this, SLOT(chooseAvatar())); connect (unitCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(unitChanged(int))); - connect (importBrowseButton, SIGNAL(clicked()), this, SLOT(browseImportDir())); } void @@ -882,14 +863,6 @@ RiderPage::unitChanged(int currentIndex) } } -void -RiderPage::browseImportDir() -{ - QString dir = QFileDialog::getExistingDirectory(this, tr("Select Import Directory"), - "", QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); - if (dir != "") importDirectory->setText(dir); //only overwrite current dir, if a new was selected -} - void RiderPage::saveClicked() @@ -909,10 +882,6 @@ RiderPage::saveClicked() appsettings->setCValue(context->athlete->cyclist, GC_BIO, bio->toPlainText()); avatar.save(context->athlete->home->config().canonicalPath() + "/" + "avatar.png", "PNG"); - // save the import settings - appsettings->setCValue(context->athlete->cyclist, GC_IMPORTSETTINGS, importSetting->currentIndex()); - appsettings->setCValue(context->athlete->cyclist, GC_IMPORTDIR, importDirectory->text()); - } // @@ -1221,7 +1190,7 @@ ColorsPage::ColorsPage(QWidget *parent) : QWidget(parent) themes->setColumnWidth(0,240); themes->setSelectionMode(QAbstractItemView::SingleSelection); //colors->setEditTriggers(QAbstractItemView::SelectedClicked); // allow edit - themes->setUniformRowHeights(true); + themes->setUniformRowHeights(true); // causes height problems when adding - in case of non-text fields themes->setIndentation(0); //colors->header()->resizeSection(0,300); @@ -1232,7 +1201,7 @@ ColorsPage::ColorsPage(QWidget *parent) : QWidget(parent) colors->setColumnWidth(0,350); colors->setSelectionMode(QAbstractItemView::NoSelection); //colors->setEditTriggers(QAbstractItemView::SelectedClicked); // allow edit - colors->setUniformRowHeights(true); + colors->setUniformRowHeights(true); // causes height problems when adding - in case of non-text fields colors->setIndentation(0); //colors->header()->resizeSection(0,300); @@ -2234,7 +2203,7 @@ KeywordsPage::KeywordsPage(MetadataPage *parent, QListkeyword keywords->setColumnCount(3); keywords->setSelectionMode(QAbstractItemView::SingleSelection); keywords->setEditTriggers(QAbstractItemView::SelectedClicked); // allow edit - keywords->setUniformRowHeights(true); + //keywords->setUniformRowHeights(true); // causes height problems when adding - in case of non-text fields keywords->setIndentation(0); //keywords->header()->resizeSection(0,100); //keywords->header()->resizeSection(1,45); @@ -2454,7 +2423,7 @@ FieldsPage::FieldsPage(QWidget *parent, QListfieldDefinitions) fields->setColumnCount(5); fields->setSelectionMode(QAbstractItemView::SingleSelection); fields->setEditTriggers(QAbstractItemView::SelectedClicked); // allow edit - fields->setUniformRowHeights(true); + //fields->setUniformRowHeights(true); // causes height problems when adding - in case of non-text fields fields->setIndentation(0); SpecialFields specials; @@ -4523,7 +4492,7 @@ MeasuresPage::MeasuresPage(Context *context) : context(context) fields->setColumnCount(3); fields->setSelectionMode(QAbstractItemView::SingleSelection); fields->setEditTriggers(QAbstractItemView::SelectedClicked); // allow edit - fields->setUniformRowHeights(true); + //fields->setUniformRowHeights(true); fields->setIndentation(0); //fields->header()->resizeSection(0,130); //fields->header()->resizeSection(1,140); @@ -4923,3 +4892,202 @@ SeasonsPage::saveClicked() // re-read context->athlete->seasons->readSeasons(); } + + +AutoImportPage::AutoImportPage(Context *context) : context(context) +{ + QGridLayout *mainLayout = new QGridLayout(this); + + addButton = new QPushButton(tr("+")); + deleteButton = new QPushButton(tr("-")); + browseButton = new QPushButton(tr("Browse")); +#ifndef Q_OS_MAC + upButton = new QToolButton(this); + downButton = new QToolButton(this); + upButton->setArrowType(Qt::UpArrow); + downButton->setArrowType(Qt::DownArrow); + upButton->setFixedSize(20,20); + downButton->setFixedSize(20,20); + addButton->setFixedSize(20,20); + deleteButton->setFixedSize(20,20); +#else + addButton->setText(tr("Add")); + deleteButton->setText(tr("Delete")); + upButton = new QPushButton(tr("Up")); + downButton = new QPushButton(tr("Down")); +#endif + QHBoxLayout *actionButtons = new QHBoxLayout; + actionButtons->setSpacing(2); + actionButtons->addWidget(upButton); + actionButtons->addWidget(downButton); + actionButtons->addStretch(); + actionButtons->addWidget(browseButton); + actionButtons->addStretch(); + actionButtons->addWidget(addButton); + actionButtons->addWidget(deleteButton); + + fields = new QTreeWidget; + fields->headerItem()->setText(0, tr("Directory")); + fields->headerItem()->setText(1, tr("Import Rule")); + fields->setColumnWidth(0,400); + fields->setColumnWidth(1,100); + fields->setColumnCount(2); + fields->setSelectionMode(QAbstractItemView::SingleSelection); + //fields->setUniformRowHeights(true); + fields->setIndentation(0); + + fields->setCurrentItem(fields->invisibleRootItem()->child(0)); + + mainLayout->addWidget(fields, 0,0); + mainLayout->addLayout(actionButtons, 1,0); + + context->athlete->autoImportConfig->readConfig(); + QList rules = context->athlete->autoImportConfig->getConfig(); + int index = 0; + foreach (RideAutoImportRule rule, rules) { + QComboBox *comboButton = new QComboBox(this); + addRuleTypes(comboButton); + QTreeWidgetItem *add = new QTreeWidgetItem; + fields->invisibleRootItem()->insertChild(index, add); + add->setFlags(add->flags() | Qt::ItemIsEditable); + + add->setTextAlignment(0, Qt::AlignLeft | Qt::AlignVCenter); + add->setText(0, rule.getDirectory()); + + add->setTextAlignment(1, Qt::AlignHCenter | Qt::AlignVCenter); + comboButton->setCurrentIndex(rule.getImportRule()); + fields->setItemWidget(add, 1, comboButton); + index++; + } + + // connect up slots + connect(upButton, SIGNAL(clicked()), this, SLOT(upClicked())); + connect(downButton, SIGNAL(clicked()), this, SLOT(downClicked())); + connect(addButton, SIGNAL(clicked()), this, SLOT(addClicked())); + connect(deleteButton, SIGNAL(clicked()), this, SLOT(deleteClicked())); + connect(browseButton, SIGNAL(clicked()), this, SLOT(browseImportDir())); +} + +void +AutoImportPage::upClicked() +{ + if (fields->currentItem()) { + int index = fields->invisibleRootItem()->indexOfChild(fields->currentItem()); + if (index == 0) return; // its at the top already + + //movin on up! + QWidget *button = fields->itemWidget(fields->currentItem(),1); + QComboBox *comboButton = new QComboBox(this); + addRuleTypes(comboButton); + comboButton->setCurrentIndex(((QComboBox*)button)->currentIndex()); + QTreeWidgetItem* moved = fields->invisibleRootItem()->takeChild(index); + fields->invisibleRootItem()->insertChild(index-1, moved); + fields->setItemWidget(moved, 1, comboButton); + fields->setCurrentItem(moved); + } +} + +void +AutoImportPage::downClicked() +{ + if (fields->currentItem()) { + int index = fields->invisibleRootItem()->indexOfChild(fields->currentItem()); + if (index == (fields->invisibleRootItem()->childCount()-1)) return; // its at the bottom already + + QWidget *button = fields->itemWidget(fields->currentItem(),1); + QComboBox *comboButton = new QComboBox(this); + addRuleTypes(comboButton); + comboButton->setCurrentIndex(((QComboBox*)button)->currentIndex()); + QTreeWidgetItem* moved = fields->invisibleRootItem()->takeChild(index); + fields->invisibleRootItem()->insertChild(index+1, moved); + fields->setItemWidget(moved, 1, comboButton); + fields->setCurrentItem(moved); + } +} + + +void +AutoImportPage::addClicked() +{ + + int index = fields->invisibleRootItem()->indexOfChild(fields->currentItem()); + if (index < 0) index = 0; + + QComboBox *comboButton = new QComboBox(this); + addRuleTypes(comboButton); + + QTreeWidgetItem *add = new QTreeWidgetItem; + fields->invisibleRootItem()->insertChild(index, add); + add->setFlags(add->flags() | Qt::ItemIsEditable); + + add->setTextAlignment(0, Qt::AlignLeft | Qt::AlignVCenter); + add->setText(0, tr("Enter directory or press [Browse] to select")); + add->setTextAlignment(1, Qt::AlignHCenter | Qt::AlignVCenter); + fields->setItemWidget(add, 1, comboButton); + +} + +void +AutoImportPage::deleteClicked() +{ + if (fields->currentItem()) { + int index = fields->invisibleRootItem()->indexOfChild(fields->currentItem()); + + // zap! + delete fields->invisibleRootItem()->takeChild(index); + } +} + +void +AutoImportPage::saveClicked() { + + rules.clear(); + for(int i=0; iinvisibleRootItem()->childCount(); i++) { + + RideAutoImportRule rule; + rule.setDirectory(fields->invisibleRootItem()->child(i)->text(0)); + + QWidget *button = fields->itemWidget(fields->invisibleRootItem()->child(i),1); + rule.setImportRule(((QComboBox*)button)->currentIndex()); + rules.append(rule); + + } + + // write to disk + QString file = QString(context->athlete->home->config().canonicalPath() + "/autoimport.xml"); + RideAutoImportConfigParser::serialize(file, rules); + + // re-read + context->athlete->autoImportConfig->readConfig(); + +} + +void +AutoImportPage::addRuleTypes(QComboBox *p) { + + + p->addItem(tr("No autoimport")); + p->addItem(tr("Autoimport with dialog")); + +} + +void +AutoImportPage::browseImportDir() +{ + QStringList selectedDirs; + if (fields->currentItem()) { + QFileDialog fileDialog(this); + fileDialog.setFileMode(QFileDialog::Directory); + fileDialog.setOptions(QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); + if (fileDialog.exec()) { + selectedDirs = fileDialog.selectedFiles(); + } + if (selectedDirs.count() > 0) { + QString dir = selectedDirs.at(0); + if (dir != "") { + fields->currentItem()->setText(0, dir); + } + } + } +} + diff --git a/src/Pages.h b/src/Pages.h index d01ba19d7..5f1d92951 100644 --- a/src/Pages.h +++ b/src/Pages.h @@ -14,6 +14,7 @@ #include #include #include +#include #include "Zones.h" #include "HrZones.h" #include "PaceZones.h" @@ -30,6 +31,7 @@ #include "DataProcessor.h" #include "Season.h" #include "SeasonParser.h" +#include "RideAutoImportConfig.h" #ifdef GC_HAVE_LIBOAUTH extern "C" { @@ -113,7 +115,6 @@ class RiderPage : public QWidget public slots: void chooseAvatar(); void unitChanged(int currentIndex); - void browseImportDir(); private: Context *context; @@ -131,10 +132,6 @@ class RiderPage : public QWidget QTextEdit *bio; QPushButton *avatarButton; QPixmap avatar; - QLineEdit *importDirectory; - QPushButton *importBrowseButton; - QLabel *importLabel; - QComboBox *importSetting; }; @@ -890,4 +887,42 @@ class MeasuresPage : public QWidget QPushButton *addButton, *renameButton, *deleteButton; }; +class AutoImportPage : public QWidget +{ + Q_OBJECT + G_OBJECT + + + public: + + AutoImportPage(Context *); + void saveClicked(); + void addRuleTypes(QComboBox *p); + + public slots: + + void addClicked(); + void upClicked(); + void downClicked(); + void deleteClicked(); + void browseImportDir(); + + + private: + + Context *context; + QList rules; + + QTreeWidget *fields; + +#ifndef Q_OS_MAC + QToolButton *upButton, *downButton; +#else + QPushButton *upButton, *downButton; +#endif + QPushButton *addButton, *renameButton, *deleteButton, *browseButton; + +}; + + #endif diff --git a/src/RideAutoImportConfig.cpp b/src/RideAutoImportConfig.cpp new file mode 100644 index 000000000..327fd29d6 --- /dev/null +++ b/src/RideAutoImportConfig.cpp @@ -0,0 +1,221 @@ +/* + * Copyright (c) 2014 Joern Rischmueller (joern.rm@gmail.com) + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * 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 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 "RideAutoImportConfig.h" +#include "Context.h" +#include "Athlete.h" + + +// Class: RideAutoImportRule + + +RideAutoImportRule::RideAutoImportRule() { + _directory = ""; + _importRule = noImport; + + _ruleDescriptions.append(tr("No autoimport")); + _ruleDescriptions.append(tr("Autoimport with dialog")); + +} + +void +RideAutoImportRule::setDirectory(QString dir) { _directory = dir; } + +QString +RideAutoImportRule::getDirectory() { return _directory; } + +void +RideAutoImportRule::setImportRule(int rule) { _importRule = rule; } + +int +RideAutoImportRule::getImportRule() { return _importRule; } + +QList +RideAutoImportRule::getRuleDescriptions() { return _ruleDescriptions; } + + +// Class: RideAutoImportConfig + +void +RideAutoImportConfig::readConfig() +{ + QFile autoImportRulesFile(config.canonicalPath() + "/autoimport.xml"); + QXmlInputSource source( &autoImportRulesFile ); + QXmlSimpleReader xmlReader; + RideAutoImportConfigParser handler; + xmlReader.setContentHandler(&handler); + xmlReader.setErrorHandler(&handler); + xmlReader.parse(source); + + // go read them! + _configList = handler.getRules(); + + // let everyone know they have changed + changedConfig(); +} + +void +RideAutoImportConfig::writeConfig() +{ + // update seasons.xml + QString file = QString(config.canonicalPath() + "/autoimportrules.xml"); + RideAutoImportConfigParser::serialize(file, _configList); + + changedConfig(); // signal! +} + +// Class: RideAutoImportConfigParser + +bool +RideAutoImportConfigParser::startDocument() +{ + buffer.clear(); + return true; +} + +bool +RideAutoImportConfigParser::endElement( const QString&, const QString&, const QString &qName ) +{ + if(qName == "directory") { + rule.setDirectory(DecodeXML(buffer.trimmed())); + buffer.clear(); + } + else if(qName == "importrule") { + rule.setImportRule(buffer.trimmed().toInt()); + buffer.clear(); + } + else if(qName == "rule") { + rules.append(rule); + buffer.clear(); + } + return true; +} + +bool +RideAutoImportConfigParser::startElement( const QString&, const QString&, const QString &name, const QXmlAttributes ) +{ + buffer.clear(); + if(name == "rule") { + rule = RideAutoImportRule(); + } + + return true; +} + +bool +RideAutoImportConfigParser::characters( const QString& str ) +{ + buffer += str; + return true; +} + +QList +RideAutoImportConfigParser::getRules() +{ + return rules; +} + +bool +RideAutoImportConfigParser::endDocument() +{ + return true; +} + +bool +RideAutoImportConfigParser::serialize(QString filename, QList rules) +{ + // open file - truncate contents + QFile file(filename); + file.open(QFile::WriteOnly); + file.resize(0); + QTextStream out(&file); + out.setCodec("UTF-8"); + + // begin document + out << "\n"; + + // write out to file + foreach (RideAutoImportRule rule, rules) { + + QString dir = rule.getDirectory(); + + out<\n" + "\t\t%1\n" + "\t\t%2\n").arg(EncodeXML(dir)) + .arg(QString::number(rule.getImportRule())); + out <\n"); + } + + // end document + out << "\n"; + + // close file + file.close(); + + return true; // success +} + +QString RideAutoImportConfigParser::EncodeXML ( const QString& encodeMe ) +{ + QString temp; + + for (int index(0); index < encodeMe.size(); index++) + { + QChar character(encodeMe.at(index)); + + switch (character.unicode()) + { + case '&': + temp += "&"; break; + + case '\'': + temp += "'"; break; + + case '"': + temp += """; break; + + case '<': + temp += "<"; break; + + case '>': + temp += ">"; break; + + default: + temp += character; + break; + } + } + + return temp; +} + +QString RideAutoImportConfigParser::DecodeXML ( const QString& decodeMe ) +{ + QString temp(decodeMe); + + temp.replace("&", "&"); + temp.replace("'", "'"); + temp.replace(""", "\""); + temp.replace("<", "<"); + temp.replace(">", ">"); + + return temp; +} + + + diff --git a/src/RideAutoImportConfig.h b/src/RideAutoImportConfig.h new file mode 100644 index 000000000..4640f1df3 --- /dev/null +++ b/src/RideAutoImportConfig.h @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2014 Joern Rischmueller (joern.rm@gmail.com) + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * 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 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 + */ + +#ifndef _GC_RideAutoImportConfig_h +#define _GC_RideAutoImportConfig_h + +#include "GoldenCheetah.h" +#include +#include + + +class RideAutoImportRule { + + Q_DECLARE_TR_FUNCTIONS(RideAutoImportRule) + +public: + static QList rules; + enum ImportRule { noImport=0, ignoreDuplicateErrors=1, ignoreAllErrors=2 }; + + RideAutoImportRule(); + + void setDirectory(QString); + QString getDirectory(); + + void setImportRule(int); + int getImportRule(); + + QList getRuleDescriptions(); + +private: + QString _directory; + int _importRule; // enum + QList _ruleDescriptions; + + +}; + + +class RideAutoImportConfig : public QObject { + + Q_OBJECT + + public: + RideAutoImportConfig(QDir config) : config(config) { readConfig(); } + + void readConfig(); + void writeConfig(); + QList getConfig() { return _configList; } + bool hasRules() { return (_configList.count() > 0); } + + signals: + void changedConfig(); + + private: + QDir config; + QList _configList; +}; + + +class RideAutoImportConfigParser : public QXmlDefaultHandler +{ + +public: + // marshall + static bool serialize(QString, QList rules); + + // unmarshall + bool startDocument(); + bool endDocument(); + bool endElement( const QString&, const QString&, const QString &qName ); + bool startElement(const QString&, const QString&, const QString &name, const QXmlAttributes ); + bool characters( const QString& str ); + QList getRules(); + +private: + QString buffer; + RideAutoImportRule rule; + QList rules; + static QString EncodeXML ( const QString& ); + static QString DecodeXML ( const QString& ); + +}; + + +#endif diff --git a/src/RideImportWizard.cpp b/src/RideImportWizard.cpp index 6d82eaf24..0fb2935c5 100644 --- a/src/RideImportWizard.cpp +++ b/src/RideImportWizard.cpp @@ -31,33 +31,139 @@ #include "JsonRideFile.h" #include "TcxRideFile.h" #include "MetricAggregator.h" +#include "RideAutoImportConfig.h" // drag and drop passes urls ... convert to a list of files and call main constructor RideImportWizard::RideImportWizard(QList *urls, Context *context, QWidget *parent) : QDialog(parent), context(context) { - dialogMode = standardDialog; setAttribute(Qt::WA_DeleteOnClose); setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint); QList filenames; for (int i=0; icount(); i++) filenames.append(QFileInfo(urls->value(i).toLocalFile()).absoluteFilePath()); + autoImportMode = false; init(filenames, context); filenames.clear(); } RideImportWizard::RideImportWizard(QList files, Context *context, QWidget *parent) : QDialog(parent), context(context) { - dialogMode = standardDialog; + autoImportMode = false; init(files, context); } + +RideImportWizard::RideImportWizard(RideAutoImportConfig *dirs, Context *context, QWidget *parent) : QDialog(parent), context(context), importConfig(dirs) +{ + autoImportMode = true; + QList files; + + // get the directories + QList rules = importConfig->getConfig(); + + // prepare the widget to show the status of the directory + directoryWidget = new QTableWidget(rules.count(), 3, this); + + directoryWidget->verticalHeader()->setDefaultSectionSize(20); + + QTableWidgetItem *directoryHeading = new QTableWidgetItem; + directoryHeading->setText(tr("Directory")); + directoryWidget->setHorizontalHeaderItem(0, directoryHeading); + + QTableWidgetItem *importRuleHeading = new QTableWidgetItem; + importRuleHeading->setText(tr("Import Rule")); + directoryWidget->setHorizontalHeaderItem(1, importRuleHeading); + + QTableWidgetItem *statusHeading = new QTableWidgetItem; + statusHeading->setText(tr("Directory Status")); + directoryWidget->setHorizontalHeaderItem(2, statusHeading); + + // and get the allowed files formats + const RideFileFactory &rff = RideFileFactory::instance(); + QStringList suffixList = rff.suffixes(); + suffixList.replaceInStrings(QRegExp("^"), "*."); + QStringList allFormats; + foreach(QString suffix, rff.suffixes()) + allFormats << QString("*.%1").arg(suffix); + + // Fill in the directory names and importRuleStatus + int i=-1; + foreach (RideAutoImportRule rule, rules){ + i++; // do it here to allow "continue" - and start with "0" + QTableWidgetItem *t; + + // Directory + t = new QTableWidgetItem(); + t->setText(rule.getDirectory()); + t->setFlags(t->flags() & (~Qt::ItemIsEditable)); + directoryWidget->setItem(i,0,t); + + // Import Rule + QList descriptions = rule.getRuleDescriptions(); + t = new QTableWidgetItem(); + t->setText(descriptions.at(rule.getImportRule())); + t->setFlags(t->flags() & (~Qt::ItemIsEditable)); + directoryWidget->setItem(i,1,t); + + // Import Status + t = new QTableWidgetItem(); + t->setText(tr("")); + t->setFlags(t->flags() & (~Qt::ItemIsEditable)); + directoryWidget->setItem(i,2,t); + + // only add files if configured to do so + if (rule.getImportRule() == 0) { + directoryWidget->item(i,2)->setText(tr("No import")); + continue; + } + + // do some checks on the directory first + QString currentImportDirectory = rule.getDirectory(); + if (currentImportDirectory == "") { + directoryWidget->item(i,2)->setText(tr("No directory")); + continue; + } + QDir *importDir = new QDir (currentImportDirectory); + if (!importDir->exists()) { // directory might not be available (USB,..) + directoryWidget->item(i,2)->setText(tr("Directory not available")); + continue; + } + if (!importDir->isReadable()) { + directoryWidget->item(i,2)->setText(tr("Directory not readable")); + continue; + } + + // now get the files with their full names + QFileInfoList fileInfos = importDir->entryInfoList(allFormats, QDir::Files, QDir::NoSort); + if (!fileInfos.isEmpty()) { + int j = 0; + foreach(QFileInfo f, fileInfos) { + files.append(f.absoluteFilePath()); + j++; + } + directoryWidget->item(i,2)->setText(tr("%1 Files selected for import").arg(QString::number(j))); + } else { + directoryWidget->item(i,2)->setText(tr("No activity files found")); + continue; + } + } + + directoryWidget->setColumnWidth(0, 480); + directoryWidget->setColumnWidth(1, 150); + directoryWidget->setColumnWidth(2, 250); + + init(files, context); +} + + void RideImportWizard::init(QList files, Context * /*mainWindow*/) { // initialise dialog box tableWidget = new QTableWidget(files.count(), 6, this); + tableWidget->setItemDelegate(new RideDelegate(1)); // use a delegate for column 1 date tableWidget->verticalHeader()->setDefaultSectionSize(20); phaseLabel = new QLabel; @@ -127,12 +233,15 @@ RideImportWizard::init(QList files, Context * /*mainWindow*/) for (int i=0; i < files.count(); i++) { QTableWidgetItem *t; - filenames.append(QFileInfo(files[i]).absoluteFilePath()); + filenames.append(QFileInfo(files[i]).canonicalFilePath()); blanks.append(true); // by default editable // Filename t = new QTableWidgetItem(); - t->setText(QFileInfo(files[i]).fileName()); + if (autoImportMode) + t->setText(QFileInfo(files[i]).canonicalFilePath()); + else + t->setText(QFileInfo(files[i]).fileName()); t->setFlags(t->flags() & (~Qt::ItemIsEditable)); tableWidget->setItem(i,0,t); @@ -180,6 +289,10 @@ RideImportWizard::init(QList files, Context * /*mainWindow*/) buttons->addWidget(abortButton); QVBoxLayout *contents = new QVBoxLayout(this); + if (autoImportMode) { + contents->addWidget(directoryWidget); + + } contents->addWidget(tableWidget); contents->addWidget(progressBar); contents->addLayout(buttons); @@ -202,27 +315,23 @@ RideImportWizard::init(QList files, Context * /*mainWindow*/) resize(920 + ((files.count() > 16 ? 24 : 0) + ((files.count() > 9 && files.count() < 17) ? 8 : 0)), - 118 + (files.count() > 16 ? 17*20 : (files.count()+1) * 20)); + 118 + ((files.count() > 16 ? 17*20 : (files.count()+1) * 20) + + ((autoImportMode) ? 100 : 0))); // assume not more the 5 directory in average + if (autoImportMode) directoryWidget->adjustSize(); tableWidget->adjustSize(); + + // Refresh prior to running down the list & processing... + if (!isActiveWindow()) activateWindow(); + this->show(); + } int RideImportWizard::process() { - // Refresh prior to running down the list & processing... - // do it here and not in Constructor, since "dialogMode" might have been changed - if (dialogMode == standardDialog) show(); - - // set if any error occurs in one of the stages, for the "non-Dialog" modes - to make the widget visible - // if an error in one of the steps has happened - first error is set after the first error has occured - // and the widget has been made visible (in case of noDialog mode)- from that point on, no need - // to "show()" the widget again - bool firstError = false; - bool sectionError = false; - // set progress bar limits - for each file we // will make 5 passes over the files // 1. checking it is a file ane readable @@ -234,6 +343,8 @@ RideImportWizard::process() // So, therefore the progress bar runs from 0 to files*4. (since step 5 is not implemented yet) progressBar->setMinimum(0); progressBar->setMaximum(filenames.count()*4); + if (!isActiveWindow()) activateWindow(); + // Pass one - Is it valid? phaseLabel->setText(tr("Step 1 of 4: Check file permissions")); @@ -256,13 +367,11 @@ RideImportWizard::process() } else { tableWidget->item(i,5)->setText(tr("Error - Unknown file type")); - sectionError = true; } } else { // Cannot open tableWidget->item(i,5)->setText(tr("Error - Not a valid file")); - sectionError = true; } progressBar->setValue(progressBar->value()+1); @@ -270,11 +379,7 @@ RideImportWizard::process() } if (aborted) { done(0); } - if ((dialogMode == allErrors || dialogMode == allButDupFileErrors) && sectionError) { - firstError = true; - show(); // make the widget visible in case an error has happened errors - sectionError = false; // reset Error flag for next section - } + if (!isActiveWindow()) activateWindow(); repaint(); QApplication::processEvents(); @@ -295,6 +400,7 @@ RideImportWizard::process() QApplication::processEvents(); if (aborted) { done(0); } + if (!isActiveWindow()) activateWindow(); this->repaint(); QApplication::processEvents(); @@ -401,7 +507,6 @@ RideImportWizard::process() tableWidget->item(i,5)->setText(tr("Validated")); else { tableWidget->item(i,5)->setText(tr("Warning - ") + errors.join(tr(" "))); - sectionError = true; } // Set Date and Time @@ -411,7 +516,6 @@ RideImportWizard::process() blanks[i] = true; tableWidget->item(i,1)->setText(tr("")); tableWidget->item(i,2)->setText(tr("")); - sectionError = true; } else { @@ -456,17 +560,12 @@ RideImportWizard::process() } else { // nope - can't handle this file tableWidget->item(i,5)->setText(tr("Error - ") + errors.join(tr(" "))); - sectionError = true; } } progressBar->setValue(progressBar->value()+1); QApplication::processEvents(); if (aborted) { done(0); } - if ((dialogMode == allErrors || dialogMode == allButDupFileErrors) && sectionError && !firstError) { - firstError = true; - show(); // make the widget visible in case an error has happened errors - sectionError = false; // reset Error flag for next section - } + if (!isActiveWindow()) activateWindow(); this->repaint(); next:; @@ -489,13 +588,11 @@ RideImportWizard::process() // does nothing for the moment progressBar->setValue(progressBar->value()+1); + if (!isActiveWindow()) activateWindow(); progressBar->repaint(); } - // dialog has to show up for this import to complete the data - if (needdates != 0 && (dialogMode == allErrors || dialogMode == allButDupFileErrors) && !firstError) { - firstError = true; - show(); // make the widget visible in case an error has happened errors - } + // get it on top + activateWindow(); // Wait for user to press save abortButton->setText(tr("Save")); @@ -523,15 +620,6 @@ RideImportWizard::process() } connect(tableWidget, SIGNAL(itemChanged(QTableWidgetItem *)), this, SLOT(activateSave())); - // if running in any "dialog only in error cases mode" and no Error have occured until now, - // we need to trigger the final saving step - if ((dialogMode == allErrors || dialogMode == allButDupFileErrors) && !firstError) { - abortClicked(); // simulate user input - at this point "Abort" == "Save" - // in additional simulate the "Finish" user-click - context->athlete->isclean = false; - context->athlete->metricDB->refreshMetrics(); - } - return 0; } @@ -678,7 +766,9 @@ RideImportWizard::todayClicked(int index) } // phew! - repaint! QApplication::processEvents(); + if (!isActiveWindow()) activateWindow(); tableWidget->repaint(); + } void @@ -692,53 +782,10 @@ struct cpi_file_info { QString file, inname, outname; }; -static QStringList -findDuplicates(QString filename) -{ - // does this ride already exist? - // either the full name is a match - // or the same name but different - // filetype: e.g. xxx.gc matches xxx.tcx - QStringList duplicates; - - // get a list of possible duplicates (files we support) - QString basename = QFileInfo(filename).baseName(); - QStringList filters; - foreach (QString ext, RideFileFactory::instance().suffixes()) { - QString check = basename + "." + ext; - filters << check; - } - - // check if any matched (case insensitive) - QFlags spec = QDir::Files; -#ifdef Q_OS_WIN32 - spec |= QDir::Hidden; -#endif - // get list and convert to full path - foreach(QString name, QFileInfo(filename).dir().entryList(filters, spec, QDir::Name)) { - duplicates << QFileInfo(filename).dir().canonicalPath() + "/" + name; - } - - return duplicates; -} - -static void -removeDuplicate(QString filename) -{ - // rename to .bak, if that already exists - // then wipe it first - QString backup = filename + ".bak"; - QFile(backup).remove(); // wipe it, if it is there - QFile(filename).rename(backup); -} void RideImportWizard::abortClicked() { - // similar procedure for "non-Dialog" processing to handle errors like in "process()" here - bool sectionError = false; - bool fileExistsError = false; - // if done when labelled abort we kill off this dialog QString label = abortButton->text(); @@ -808,6 +855,7 @@ RideImportWizard::abortClicked() tableWidget->setCurrentCell(i,5); QApplication::processEvents(); if (aborted) { done(0); } + if (!isActiveWindow()) activateWindow(); this->repaint(); // serialize the file to .JSON format and copy the source file to the "/imports" directory @@ -842,10 +890,8 @@ RideImportWizard::abortClicked() // and by comparing "OriginalName+RideDateTime" for the /imports if (QFileInfo(activitiesFulltarget).exists()) { tableWidget->item(i,5)->setText(tr("Error - Activity file exists")); - fileExistsError = true; } else if (QFileInfo(importsFulltarget).exists()) { tableWidget->item(i,5)->setText(tr("Error - File already imported, but no activity found")); - fileExistsError = true; } else { // First copy of source then create .JSON (in case of error the last error will be shown) @@ -856,7 +902,6 @@ RideImportWizard::abortClicked() QFile source(filenames[i]); if (!source.copy(importsFulltarget)) { tableWidget->item(i,5)->setText(tr("Error - copy of %1 to import directory failed").arg(importsTarget)); - sectionError = true; } // serialize the file to .JSON @@ -878,7 +923,6 @@ RideImportWizard::abortClicked() tableWidget->rowCount() < 20 ? true : false); // don't signal if mass importing } else { tableWidget->item(i,5)->setText(tr("Error - .JSON creation failed")); - sectionError = true; } // clear delete ride; @@ -887,10 +931,7 @@ RideImportWizard::abortClicked() QApplication::processEvents(); if (aborted) { done(0); } progressBar->setValue(progressBar->value()+1); - if (((sectionError || fileExistsError) && (dialogMode == allErrors)) || - ((sectionError && !fileExistsError) && (dialogMode == allButDupFileErrors))) { - show(); // make the widget visible in case an error was logged - } + if (!isActiveWindow()) activateWindow(); this->repaint(); } @@ -912,11 +953,6 @@ RideImportWizard::abortClicked() } -void RideImportWizard::setDialogMode(int mode) { - - dialogMode = mode; -} - // clean up files RideImportWizard::~RideImportWizard() { diff --git a/src/RideImportWizard.h b/src/RideImportWizard.h index 62e5643de..729c2458f 100644 --- a/src/RideImportWizard.h +++ b/src/RideImportWizard.h @@ -31,6 +31,7 @@ #include #include #include "Context.h" +#include "RideAutoImportConfig.h" // Dialog class to show filenames, import progress and to capture user input // of ride date and time @@ -44,10 +45,10 @@ class RideImportWizard : public QDialog public: RideImportWizard(QList *urls, Context *context, QWidget *parent = 0); RideImportWizard(QList files, Context *context, QWidget *parent = 0); + RideImportWizard(RideAutoImportConfig *dirs, Context *context, QWidget *parent = 0); + ~RideImportWizard(); int process(); - void setDialogMode(int); // default is fullDialog - enum DialogMode { standardDialog, allErrors, allButDupFileErrors }; signals: @@ -65,9 +66,10 @@ private: QDir homeImports; // target directory for source files QDir homeActivities; // target directory for .JSON bool aborted; - int dialogMode; // see enum + bool autoImportMode; QLabel *phaseLabel; QTableWidget *tableWidget; + QTableWidget *directoryWidget; QProgressBar *progressBar; QPushButton *abortButton; // also used for save and finish QPushButton *cancelButton; // cancel when asking for dates @@ -75,6 +77,7 @@ private: // QCheckBox *overFiles; // chance to set overwrite when asking for dates // deprecate for this release... XXX // bool overwriteFiles; // flag to overwrite files from checkbox // deprecate for this release... XXX Context *context; // caller + RideAutoImportConfig *importConfig; QStringList deleteMe; // list of temp files created during import }; diff --git a/src/Settings.h b/src/Settings.h index a227251bf..5eb2ab976 100644 --- a/src/Settings.h +++ b/src/Settings.h @@ -59,8 +59,6 @@ #define GC_SEX "sex" #define GC_BIO "bio" #define GC_AVATAR "avatar" -#define GC_IMPORTDIR "importDir" -#define GC_IMPORTSETTINGS "importSettings" #define GC_SETTINGS_LAST_IMPORT_PATH "mainwindow/lastImportPath" #define GC_SETTINGS_LAST_WORKOUT_PATH "mainwindow/lastWorkoutPath" #define GC_LAST_DOWNLOAD_DEVICE "mainwindow/lastDownloadDevice" diff --git a/src/src.pro b/src/src.pro index 4bc181ada..578914fbb 100644 --- a/src/src.pro +++ b/src/src.pro @@ -385,6 +385,7 @@ HEADERS += \ ReferenceLineDialog.h \ ComputrainerController.h \ RealtimePlot.h \ + RideAutoImportConfig.h \ RideEditor.h \ RideFile.h \ RideFileCache.h \ @@ -600,6 +601,7 @@ SOURCES += \ RealtimePlot.cpp \ RealtimePlotWindow.cpp \ ReferenceLineDialog.cpp \ + RideAutoImportConfig.cpp \ RideEditor.cpp \ RideFile.cpp \ RideFileCache.cpp \