/* * 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 "RideSummaryWindow.h" #include "Settings.h" #include #include // shorthand when looking up our ride via Q_PROPERTY #define ourRideItem meta->property("ride").value() /*---------------------------------------------------------------------- * Master widget for Metadata Entry "on" RideSummaryWindow *--------------------------------------------------------------------*/ RideMetadata::RideMetadata(MainWindow *parent, bool singlecolumn) : QWidget(parent), singlecolumn(singlecolumn), main(parent) { _ride = _connected = NULL; QVBoxLayout *mainLayout = new QVBoxLayout(this); mainLayout->setSpacing(0); mainLayout->setContentsMargins(0,0,0,0); QPalette palette; palette.setBrush(QPalette::Background, Qt::NoBrush); setAutoFillBackground(false); setPalette(palette); // setup the tabs widget tabs = new QTabWidget(this); tabs->setMovable(true); tabs->setPalette(palette); tabs->setAutoFillBackground(false); tabs->setStyleSheet("QTabBar { alignment: center; }" "QTabWidget::pane { top: 0px; }" ); mainLayout->addWidget(tabs); // read in metadata.xml and setup the tabs etc qRegisterMetaType("ride"); // XXX we're first here... bit of a hack extraForm = new Form(this); configUpdate(); // watch for config changes connect(main, SIGNAL(configChanged()), this, SLOT(configUpdate())); // Extra tab is expensive to update so we only update if it // is visible. In this case we need to trigger refresh when the // tab is selected -or- when the ride changes. Below is for tab. connect(tabs, SIGNAL(currentChanged(int)), this, SLOT(setExtraTab())); } void RideMetadata::setRideItem(RideItem *ride) { static QPointer _connected = NULL; if (_connected) { disconnect(_connected, SIGNAL(rideMetadataChanged()), this, SLOT(metadataChanged())); } _connected=_ride=ride; if (ride) { connect (_connected, SIGNAL(rideMetadataChanged()), this, SLOT(metadataChanged())); setExtraTab(); metadataChanged(); } } // the extra tab has all the fields that are in the current // ride that are not shown on the tabs void RideMetadata::setExtraTab() { // wipe out the form & re-initialise -- special case for extras extraForm->clear(); extraForm->initialise(); // no ride available... if (!extraForm->isVisible() || !rideItem()) { return; } // look for metadata not in config QMapIterator tags(rideItem()->ride()->tags()); tags.toFront(); while (tags.hasNext()) { tags.next(); bool configured = false; foreach (FieldDefinition x, fieldDefinitions) { if (x.tab != "" && x.name == tags.key()) { configured = true; break; } } // Try and guess the field type, for now we assume everything // is a string; if its short make it a shorttext // if it contains newlines make it a textbox // Otherwise make it a text // if (!configured) { int type; if (tags.value().contains("\n")) type = FIELD_TEXTBOX; else if (tags.value().length() < 30) type = FIELD_SHORTTEXT; else type = FIELD_TEXT; extraForm->addField(FieldDefinition("Extra", tags.key(), type, false)); } } // now arrange the form - but only if we have anything to display! if (extraForm->fields.count()) { // arrange them extraForm->arrange(); // fetch values foreach(FormField *field, extraForm->fields) { field->metadataChanged(); // since we show EVERYTHING, don't let the user edit them // we might get more selective later? field->widget->setEnabled(false); } } } RideItem* RideMetadata::rideItem() const { return _ride; } void RideMetadata::metadataChanged() { // loop through each tab QMapIterator d(tabList); d.toFront(); while (d.hasNext()) { d.next(); foreach(FormField *field, d.value()->fields) // keep track so we can destroy field->metadataChanged(); } } 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(this); 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(); } // add extra back tabs->addTab(extraForm, "Extra"); // set Extra tab for current ride setExtraTab(); // when constructing we have not registered // the properties nor selected a ride metadataChanged(); // re-read the values! } /*---------------------------------------------------------------------- * Forms (one per tab) *--------------------------------------------------------------------*/ Form::Form(RideMetadata *meta) : meta(meta) { initialise(); } Form::~Form() { clear(); } void Form::clear() { // wipe the fields foreach (FormField *current, fields) { delete current; } fields.clear(); // wipe away the layouts foreach (QHBoxLayout *h, overrides) delete h; overrides.clear(); // clear out and set back to NULL to indicate not allocated if (grid1) { delete grid1; grid1 = NULL; } if (grid2) { delete grid2; grid2 = NULL; } if (vlayout1) { delete vlayout1; vlayout1 = NULL; } if (vlayout2) { delete vlayout2; vlayout2 = NULL; } if (hlayout) { delete hlayout; hlayout = NULL; } if (contents) { delete contents; contents = NULL; } } void Form::initialise() { QPalette palette; palette.setBrush(QPalette::Background, Qt::NoBrush); setPalette(palette); setAutoFillBackground(false); palette.setBrush(QPalette::Background, Qt::NoBrush); contents = new QWidget; contents->setPalette(palette); contents->setAutoFillBackground(false); 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); } 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 && meta->singlecolumn==false) { 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 if (fields[i]->definition.type == FIELD_TEXTBOX) { // here->addWidget(fields[i]->widget, ++y, 0, 2, 2, alignment); // y++; } else { here->addWidget(fields[i]->widget, y, 1, alignment); } y++; } } /*---------------------------------------------------------------------- * Form fields *--------------------------------------------------------------------*/ FormField::FormField(FieldDefinition field, RideMetadata *meta) : definition(field), meta(meta), active(false) { QFont font; font.setPointSize(font.pointSize()-2); QString units; if (sp.isMetric(field.name)) { field.type = FIELD_DOUBLE; // whatever they say, we want a double! QVariant unit = appsettings->value(this, 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); //label->setFont(font); //label->setFixedHeight(18); switch(field.type) { case FIELD_TEXT : // text case FIELD_SHORTTEXT : // shorttext if (field.name == "Keywords") widget = new KeywordField(this); else widget = new QLineEdit(this); //widget->setFixedHeight(18); connect (widget, SIGNAL(textChanged(const QString&)), this, SLOT(dataChanged())); connect (widget, SIGNAL(editingFinished()), this, SLOT(editFinished())); break; case FIELD_TEXTBOX : // textbox widget = new QTextEdit(this); if (field.name == "Change History") { dynamic_cast(widget)->setReadOnly(true); // pick up when ride saved - since it gets updated then // XXX ? connect (main, SIGNAL(rideClean()), this, SLOT(rideSelected())); } else { connect (widget, SIGNAL(textChanged()), this, SLOT(editFinished())); } break; case FIELD_INTEGER : // integer widget = new QSpinBox(this); //widget->setFixedHeight(18); ((QSpinBox*)widget)->setSingleStep(1); ((QSpinBox*)widget)->setMinimum(-99999999); ((QSpinBox*)widget)->setMaximum(99999999); connect (widget, SIGNAL(valueChanged(int)), this, SLOT(dataChanged())); connect (widget, SIGNAL(editingFinished()), this, SLOT(editFinished())); break; case FIELD_DOUBLE : // double widget = new QDoubleSpinBox(this); //widget->setFixedHeight(18); ((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); //widget->setFixedHeight(18); 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); //widget->setFixedHeight(18); ((QTimeEdit*)widget)->setDisplayFormat("hh:mm:ss AP"); connect (widget, SIGNAL(timeChanged(const QTime)), this, SLOT(dataChanged())); connect (widget, SIGNAL(editingFinished()), this, SLOT(editFinished())); break; } //widget->setFont(font); //connect(main, SIGNAL(rideSelected()), this, SLOT(rideSelected())); } FormField::~FormField() { delete label; switch (definition.type) { case FIELD_TEXT: case FIELD_SHORTTEXT: if (definition.name == "Keywords") delete (KeywordField*)widget; else delete (QLineEdit*)widget; break; case FIELD_TEXTBOX : if (definition.name == "Summary") delete ((RideSummaryWindow *)widget); else 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 || ourRideItem == NULL || (edited == false && definition.type != FIELD_TEXTBOX) || ourRideItem == 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") { ourRideItem->ride()->setDeviceType(text); ourRideItem->notifyRideMetadataChanged(); } else if (definition.name == "Identifier") { ourRideItem->ride()->setId(text); ourRideItem->notifyRideMetadataChanged(); } else if (definition.name == "Recording Interval") { ourRideItem->ride()->setRecIntSecs(text.toDouble()); ourRideItem->notifyRideMetadataChanged(); } else if (definition.name == "Start Date") { QDateTime current = ourRideItem->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()); ourRideItem->setStartTime(update); ourRideItem->notifyRideMetadataChanged(); } else if (definition.name == "Start Time") { QDateTime current = ourRideItem->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); ourRideItem->setStartTime(update); ourRideItem->notifyRideMetadataChanged(); } else if (definition.name != "Summary") { if (sp.isMetric(definition.name) && enabled->isChecked()) { QVariant unit = appsettings->value(this, 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); ourRideItem->ride()->metricOverrides.insert(sp.metricSymbol(definition.name), override); // get widgets updated with new override ourRideItem->notifyRideMetadataChanged(); } else { // just update the tags QMap! ourRideItem->ride()->setTag(definition.name, text); } } // Construct the summary text used on the calendar QString calendarText; foreach (FieldDefinition field, meta->getFields()) { if (field.diary == true) { calendarText += QString("%1\n") .arg(ourRideItem->ride()->getTag(field.name, "")); } } ourRideItem->ride()->setTag("Calendar Text", calendarText); // rideFile is now dirty! ourRideItem->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 (ourRideItem && ourRideItem->ride()) override = ourRideItem->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 (ourRideItem && ourRideItem->ride()) { // update overrides for this metric in the main QMap ourRideItem->ride()->metricOverrides.insert(sp.metricSymbol(definition.name), override); // rideFile is now dirty! ourRideItem->setDirty(true); // get refresh done, coz overrides state has changed ourRideItem->notifyRideMetadataChanged(); } } void FormField::metadataChanged() { active = true; edited = false; QString value; if (ourRideItem && ourRideItem->ride()) { // Handle "Special" fields if (definition.name == "Device") value = ourRideItem->ride()->deviceType(); else if (definition.name == "Recording Interval") value = QString("%1").arg(ourRideItem->ride()->recIntSecs()); else if (definition.name == "Start Date") value = ourRideItem->ride()->startTime().date().toString("dd.MM.yyyy"); else if (definition.name == "Start Time") value = ourRideItem->ride()->startTime().time().toString("hh:mm:ss.zzz"); else if (definition.name == "Identifier") value = ourRideItem->ride()->id(); else { if (sp.isMetric(definition.name)) { // get from metric overrides const QMap override = ourRideItem->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) { QVariant unit = appsettings->value(this, 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 = ourRideItem->ride()->getTag(definition.name, ""); } } } else { value =""; } 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 if (definition.name != "Summary") ((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 { if (value == "") value = " "; 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 { if (value == "") value = " "; 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); out.setCodec("UTF-8"); // 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<%1\n").arg(field.diary ? 1 : 0); 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(); // now auto append special fields, in case // the user wiped them or we have introduced // them in this release. This is to ensure // they get written to metricDB bool hasCalendarText = false; foreach (FieldDefinition field, fieldDefinitions) { if (field.name == "Calendar Text") { hasCalendarText = true; } // other fields here... } if (!hasCalendarText) { FieldDefinition add; add.name = "Calendar Text"; add.type = 1; add.diary = false; add.tab = ""; fieldDefinitions.append(add); } } // 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(); if (field.tab != "" && field.type < 3 && field.name != "Filename" && field.name != "Change History") field.diary = true; // default! } else if (qName == "fielddiary") field.diary = (buffer.trimmed().toInt() != 0); // // 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<