/* * Copyright (c) 2010 Mark Liversedge (liversedge@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 "RideMetadata.h" #include "SpecialFields.h" #include "MainWindow.h" #include "Settings.h" #include #include /*---------------------------------------------------------------------- * Master widget for Metadata Entry "on" RideSummaryWindow *--------------------------------------------------------------------*/ RideMetadata::RideMetadata(MainWindow *parent) : QWidget(parent), main(parent) { QVBoxLayout *mainLayout = new QVBoxLayout(this); // setup the tabs widget tabs = new QTabWidget(this); tabs->setMovable(true); mainLayout->addWidget(tabs); // read in metadata.xml and setup the tabs etc configUpdate(); // watch for config changes connect(main, SIGNAL(configChanged()), this, SLOT(configUpdate())); } void RideMetadata::configUpdate() { // read metadata.xml QString filename = main->home.absolutePath()+"/metadata.xml"; if (!QFile(filename).exists()) filename = ":/xml/metadata.xml"; readXML(filename, keywordDefinitions, fieldDefinitions); // wipe existing tabs QMapIterator d(tabList); d.toFront(); while (d.hasNext()) { d.next(); tabs->removeTab(tabs->indexOf(d.value())); delete d.value(); } tabList.clear(); // create forms and populate with fields foreach(FieldDefinition field, fieldDefinitions) { if (field.tab == "") continue; // not to be shown! Form *form; if ((form = tabList.value(field.tab, NULL)) == NULL) { form = new Form(main); tabs->addTab(form, field.tab); tabList.insert(field.tab, form); } form->addField(field); } // get forms arranged QMapIterator i(tabList); i.toFront(); while (i.hasNext()) { i.next(); i.value()->arrange(); } main->notifyRideSelected(); // refresh } /*---------------------------------------------------------------------- * Forms (one per tab) *--------------------------------------------------------------------*/ Form::Form(MainWindow *main) : main(main) { contents = new QWidget; QVBoxLayout *mainLayout = new QVBoxLayout(contents); hlayout = new QHBoxLayout; vlayout1 = vlayout2 = NULL; grid1 = grid2 = NULL; mainLayout->addLayout(hlayout); //mainLayout->addStretch(); contents->setLayout(mainLayout); setFrameStyle(QFrame::NoFrame); setWidgetResizable(true); setWidget(contents); } Form::~Form() { // wipe the fields foreach (FormField *current, fields) { delete current; } // wipe away the layouts foreach (QHBoxLayout *h, overrides) delete h; if (grid1) delete grid1; if (grid2) delete grid2; if (vlayout1) delete vlayout1; if (vlayout2) delete vlayout2; delete hlayout; delete contents; } void Form::arrange() { QGridLayout *here; int x=0; int y=0; // special case -- a textbox and its the only field on the tab needs no label // this is how the "Notes" tab is created if (fields.count() == 1 && fields[0]->definition.type == FIELD_TEXTBOX) { hlayout->addWidget(fields[0]->widget, 0, 0); return; } else { vlayout1 = new QVBoxLayout; here = grid1 = new QGridLayout; vlayout1->addLayout(here); vlayout1->addStretch(); hlayout->addLayout(vlayout1); } int rows = ceil(fields.count() / 2.0); if (rows < 5) rows = 5; // minimum of 5 rows seems pleasing for (int i=0; i= rows) { x+=1; y=0; vlayout2 = new QVBoxLayout; here = grid2 = new QGridLayout; vlayout2->addLayout(here); vlayout2->addStretch(); hlayout->addLayout(vlayout2); } here->setColumnStretch(0,0); here->setColumnStretch(1,3); Qt::Alignment labelalignment = Qt::AlignLeft|Qt::AlignTop; Qt::Alignment alignment = Qt::AlignLeft|Qt::AlignTop; if (fields[i]->definition.type < FIELD_SHORTTEXT) alignment = 0; // text types here->addWidget(fields[i]->label, y, 0, labelalignment); if (sp.isMetric(fields[i]->definition.name)) { QHBoxLayout *override = new QHBoxLayout; overrides.append(override); override->setContentsMargins(0,0,0,0); override->addWidget(fields[i]->enabled); override->addWidget(fields[i]->widget); here->addLayout(override, y, 1, alignment); } else { here->addWidget(fields[i]->widget, y, 1, alignment); } y++; } } /*---------------------------------------------------------------------- * Form fields *--------------------------------------------------------------------*/ FormField::FormField(FieldDefinition field, MainWindow *main) : definition(field), main(main), active(false) { QString units; if (sp.isMetric(field.name)) { field.type = FIELD_DOUBLE; // whatever they say, we want a double! boost::shared_ptr settings = GetApplicationSettings(); QVariant unit = settings->value(GC_UNIT); bool useMetricUnits = (unit.toString() == "Metric"); units = sp.rideMetric(field.name)->units(useMetricUnits); if (units != "") units = QString(" (%1)").arg(units); } label = new QLabel(QString("%1%2").arg(field.name).arg(units), this); switch(field.type) { case FIELD_TEXT : // text case FIELD_SHORTTEXT : // shorttext if (field.name == "Keywords") widget = new KeywordField(this); else widget = new QLineEdit(this); connect (widget, SIGNAL(textChanged(const QString&)), this, SLOT(dataChanged())); connect (widget, SIGNAL(editingFinished()), this, SLOT(editFinished())); break; case FIELD_TEXTBOX : // textbox if (field.name == "Notes") { widget = main->rideNotesWidget(); } else { widget = new QTextEdit(this); if (field.name == "Change History") { dynamic_cast(widget)->setReadOnly(true); // pick up when ride saved - since it gets updated then connect (main, SIGNAL(rideClean()), this, SLOT(rideSelected())); } else { connect (widget, SIGNAL(textChanged()), this, SLOT(editFinished())); } } break; case FIELD_INTEGER : // integer widget = new QSpinBox(this); ((QSpinBox*)widget)->setSingleStep(1); connect (widget, SIGNAL(valueChanged(int)), this, SLOT(dataChanged())); connect (widget, SIGNAL(editingFinished()), this, SLOT(editFinished())); break; case FIELD_DOUBLE : // double widget = new QDoubleSpinBox(this); ((QDoubleSpinBox*)widget)->setSingleStep(0.01); ((QDoubleSpinBox*)widget)->setMaximum(999999.99); if (sp.isMetric(field.name)) { enabled = new QCheckBox(this); connect(enabled, SIGNAL(stateChanged(int)), this, SLOT(stateChanged(int))); } connect (widget, SIGNAL(valueChanged(double)), this, SLOT(dataChanged())); connect (widget, SIGNAL(editingFinished()), this, SLOT(editFinished())); break; case FIELD_DATE : // date widget = new QDateEdit(this); connect (widget, SIGNAL(dateChanged(const QDate)), this, SLOT(dataChanged())); connect (widget, SIGNAL(editingFinished()), this, SLOT(editFinished())); break; case FIELD_TIME : // time widget = new QTimeEdit(this); ((QTimeEdit*)widget)->setDisplayFormat("hh:mm:ss AP"); connect (widget, SIGNAL(timeChanged(const QTime)), this, SLOT(dataChanged())); connect (widget, SIGNAL(editingFinished()), this, SLOT(editFinished())); break; } connect(main, SIGNAL(rideSelected()), this, SLOT(rideSelected())); } FormField::~FormField() { delete label; if (definition.name == "Notes") { ((QTextEdit*)widget)->setParent(main->rideMetadata()); // change parent to stop it // being zapped by Qt } else { switch (definition.type) { case FIELD_TEXT: case FIELD_SHORTTEXT: if (definition.name == "Keywords") delete (KeywordField*)widget; else delete (QLineEdit*)widget; break; case FIELD_TEXTBOX : delete ((QTextEdit*)widget); break; case FIELD_INTEGER : delete ((QSpinBox*)widget); break; case FIELD_DOUBLE : delete ((QDoubleSpinBox*)widget); break; case FIELD_DATE : delete ((QDateEdit*)widget); break; case FIELD_TIME : delete ((QTimeEdit*)widget); break; } if (sp.isMetric(definition.name)) delete enabled; } } void FormField::dataChanged() { if (active) return; edited = true; } void FormField::editFinished() { if (active || main->rideItem() == NULL || (edited == false && definition.type != FIELD_TEXTBOX) || main->rideItem()->ride() == NULL) return; // get values from the widgets QString text; // get the current value into a string switch (definition.type) { case FIELD_TEXT : case FIELD_SHORTTEXT : if (definition.name == "Keywords") text = ((KeywordField*)widget)->text(); else text = ((QLineEdit*)widget)->text(); break; case FIELD_TEXTBOX : { text = ((QTextEdit*)widget)->document()->toPlainText(); break; } case FIELD_INTEGER : text = QString("%1").arg(((QSpinBox*)widget)->value()); break; case FIELD_DOUBLE : text = QString("%1").arg(((QDoubleSpinBox*)widget)->value()); break; case FIELD_DATE : text = ((QDateEdit*)widget)->date().toString("dd.MM.yyyy"); break; case FIELD_TIME : text = ((QTimeEdit*)widget)->time().toString("hh:mm:ss.zzz"); break; } // Update special field if (definition.name == "Device") { main->rideItem()->ride()->setDeviceType(text); main->notifyRideSelected(); } else if (definition.name == "Recording Interval") { main->rideItem()->ride()->setRecIntSecs(text.toDouble()); main->notifyRideSelected(); } else if (definition.name == "Start Date") { QDateTime current = main->rideItem()->ride()->startTime(); QDate date(/* year*/text.mid(6,4).toInt(), /* month */text.mid(3,2).toInt(), /* day */text.mid(0,2).toInt()); QDateTime update = QDateTime(date, current.time()); main->rideItem()->setStartTime(update); main->notifyRideSelected(); } else if (definition.name == "Start Time") { QDateTime current = main->rideItem()->ride()->startTime(); QTime time(/* hours*/ text.mid(0,2).toInt(), /* minutes */ text.mid(3,2).toInt(), /* seconds */ text.mid(6,2).toInt(), /* milliseconds */ text.mid(9,3).toInt()); QDateTime update = QDateTime(current.date(), time); main->rideItem()->setStartTime(update); main->notifyRideSelected(); } else { if (sp.isMetric(definition.name) && enabled->isChecked()) { boost::shared_ptr settings = GetApplicationSettings(); QVariant unit = settings->value(GC_UNIT); bool useMetricUnits = (unit.toString() == "Metric"); // convert from imperial to metric if needed if (!useMetricUnits) { double value = text.toDouble() * (1/ sp.rideMetric(definition.name)->conversion()); text = QString("%1").arg(value); } // update metric override QMap! QMap override; override.insert("value", text); main->rideItem()->ride()->metricOverrides.insert(sp.metricSymbol(definition.name), override); // get widgets updated with new override main->rideItem()->computeMetricsTime = QDateTime(); // force refresh main->notifyRideSelected(); } else { // just update the tags QMap! main->rideItem()->ride()->setTag(definition.name, text); } } // rideFile is now dirty! main->rideItem()->setDirty(true); } void FormField::stateChanged(int state) { if (active) return; // being updated programmatically widget->setEnabled(state ? true : false); widget->setHidden(state ? false : true); // remove from overrides if neccessary QMap override; if (main->rideItem() && main->rideItem()->ride()) override = main->rideItem()->ride()->metricOverrides.value (sp.metricSymbol(definition.name)); // setup initial override value if (state) { // clear and reset override value for this metric override.insert("value", QString("%1").arg(0.0)); // add metric value ((QDoubleSpinBox *)widget)->setValue(0.0); } else if (override.contains("value")) // clear override value override.remove("value"); if (main->rideItem() && main->rideItem()->ride()) { // update overrides for this metric in the main QMap main->rideItem()->ride()->metricOverrides.insert(sp.metricSymbol(definition.name), override); // rideFile is now dirty! main->rideItem()->setDirty(true); // get refresh done, coz overrides state has changed main->rideItem()->computeMetricsTime = QDateTime(); main->notifyRideSelected(); } } void FormField::rideSelected() { active = true; edited = false; if (main->rideItem() && main->rideItem()->ride()) { QString value; // Handle "Special" fields if (definition.name == "Notes") return; else if (definition.name == "Device") value = main->rideItem()->ride()->deviceType(); else if (definition.name == "Recording Interval") value = QString("%1").arg(main->rideItem()->ride()->recIntSecs()); else if (definition.name == "Start Date") value = main->rideItem()->ride()->startTime().date().toString("dd.MM.yyyy"); else if (definition.name == "Start Time") value = main->rideItem()->ride()->startTime().time().toString("hh:mm:ss.zzz"); else { if (sp.isMetric(definition.name)) { // get from metric overrides const QMap override = main->rideItem()->ride()->metricOverrides.value(sp.metricSymbol(definition.name)); if (override.contains("value")) { enabled->setChecked(true); widget->setEnabled(true); widget->setHidden(false); value = override.value("value"); // does it need conversion from metric? if (sp.rideMetric(definition.name)->conversion() != 1.0) { boost::shared_ptr settings = GetApplicationSettings(); QVariant unit = settings->value(GC_UNIT); bool useMetricUnits = (unit.toString() == "Metric"); // do we want imperial? if (!useMetricUnits) { double newvalue = value.toDouble() * sp.rideMetric(definition.name)->conversion(); value = QString("%1").arg(newvalue); } } } else { value = "0.0"; enabled->setChecked(false); widget->setEnabled(false); widget->setHidden(true); } } else { value = main->rideItem()->ride()->getTag(definition.name, ""); } } switch (definition.type) { case FIELD_TEXT : // text case FIELD_SHORTTEXT : // shorttext if (definition.name == "Keywords") ((KeywordField*)widget)->setText(value); else ((QLineEdit*)widget)->setText(value); break; case FIELD_TEXTBOX : // textbox ((QTextEdit*)widget)->setText(value); break; case FIELD_INTEGER : // integer ((QSpinBox*)widget)->setValue(value.toInt()); break; case FIELD_DOUBLE : // double ((QDoubleSpinBox*)widget)->setValue(value.toDouble()); break; case FIELD_DATE : // date { QDate date(/* year*/value.mid(6,4).toInt(), /* month */value.mid(3,2).toInt(), /* day */value.mid(0,2).toInt()); ((QDateEdit*)widget)->setDate(date); } break; case FIELD_TIME : // time { QTime time(/* hours*/ value.mid(0,2).toInt(), /* minutes */ value.mid(3,2).toInt(), /* seconds */ value.mid(6,2).toInt(), /* milliseconds */ value.mid(9,3).toInt()); ((QTimeEdit*)widget)->setTime(time); } break; } } active = false; } /*---------------------------------------------------------------------- * Read / Write metadata.xml file *--------------------------------------------------------------------*/ // static helper to protect special xml characters // ideally we would use XMLwriter to do this but // the file format is trivial and this implementation // is easier to follow and modify... for now. static QString xmlprotect(QString string) { QTextEdit trademark("™"); // process html encoding of(TM) QString tm = trademark.toPlainText(); QString s = string; s.replace( tm, "™" ); s.replace( "&", "&" ); s.replace( ">", ">" ); s.replace( "<", "<" ); s.replace( "\"", """ ); s.replace( "\'", "'" ); return s; } void RideMetadata::serialize(QString filename, QListkeywordDefinitions, QListfieldDefinitions) { // open file - truncate contents QFile file(filename); file.open(QFile::WriteOnly); file.resize(0); QTextStream out(&file); // begin document out << "\n"; // // First we write out the keywords // out << "\t\n"; // write out to file foreach (KeywordDefinition keyword, keywordDefinitions) { // chart name out<\n"); out<\"%1\"\n").arg(xmlprotect(keyword.name)); out<\n") .arg(keyword.color.red()) .arg(keyword.color.green()) .arg(keyword.color.blue()); foreach (QString token, keyword.tokens) out<\"%1\"\n").arg(xmlprotect(token)); out<\n"); } out <<"\t\n"; // // Last we write out the field definitions // out << "\t\n"; // write out to file foreach (FieldDefinition field, fieldDefinitions) { // chart name out<\n"); out<\"%1\"\n").arg(xmlprotect(field.tab)); out<\"%1\"\n").arg(xmlprotect(field.name)); out<%1\n").arg(field.type); out<\n"); } out <<"\t\n"; // end document out << "\n"; // close file file.close(); } static QString unprotect(QString buffer) { // get local TM character code QTextEdit trademark("™"); // process html encoding of(TM) QString tm = trademark.toPlainText(); // remove quotes QString t = buffer.trimmed(); QString s = t.mid(1,t.length()-2); // replace html (TM) with local TM character s.replace( "™", tm ); // html special chars are automatically handled // other special characters will not work // cross-platform but will work locally, so not a biggie // i.e. if thedefault charts.xml has a special character // in it it should be added here return s; } void RideMetadata::readXML(QString filename, QList&keywordDefinitions, QList&fieldDefinitions) { QFile metadataFile(filename); QXmlInputSource source( &metadataFile ); QXmlSimpleReader xmlReader; MetadataXMLParser handler; xmlReader.setContentHandler(&handler); xmlReader.setErrorHandler(&handler); xmlReader.parse( source ); keywordDefinitions = handler.getKeywords(); fieldDefinitions = handler.getFields(); } // to see the format of the charts.xml file, look at the serialize() // function at the bottom of this source file. bool MetadataXMLParser::endElement( const QString&, const QString&, const QString &qName) { // // Single Attribute elements // if(qName == "keywordname") keyword.name = unprotect(buffer); else if(qName == "keywordtoken") keyword.tokens << unprotect(buffer); else if(qName == "keywordcolor") { // the r,g,b values are in red="xx",green="xx" and blue="xx" attributes // of this element and captured in startelement below keyword.color = QColor(red,green,blue); } else if(qName == "fieldtab") field.tab = unprotect(buffer); else if(qName == "fieldname") field.name = unprotect(buffer); else if(qName == "fieldtype") field.type = buffer.trimmed().toInt(); // // Complex Elements // else if(qName == "keyword") // block keywordDefinitions.append(keyword); else if (qName == "field") // block fieldDefinitions.append(field); return TRUE; } bool MetadataXMLParser::startElement( const QString&, const QString&, const QString &name, const QXmlAttributes &attrs ) { buffer.clear(); if(name == "keywords") keywordDefinitions.clear(); else if (name == "fields") fieldDefinitions.clear(); else if (name == "field") field = FieldDefinition(); else if (name == "keyword") keyword = KeywordDefinition(); else if (name == "keywordcolor") { // red="x" green="x" blue="x" attributes for pen/brush color for(int i=0; i=0; i--) returnList<