/* * 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 "RideEditor.h" #include "LTMOutliers.h" #include "IntervalItem.h" #include "Athlete.h" #include "MainWindow.h" #include "Context.h" #include "Settings.h" #include "Colors.h" #include #include // for strtod #include // used to make a lookup string for row/col anomalies static QString xsstring(int x, RideFile::SeriesType series) { return QString("%1:%2").arg((int)x).arg(static_cast(series)); } static void unxsstring(QString val, int &x, RideFile::SeriesType &series) { QRegExp it("^([^:]*):([^:]*)$"); it.exactMatch(val); x = it.cap(1).toDouble(); series = static_cast(it.cap(2).toInt()); } static void secsMsecs(double value, int &secs, int &msecs) { // split into secs and msecs from a double // tried modf, floor, round and a host of others but // they all had difference problems. In the end // I've resorted to rounding to 100ths of a second. // I acknowledge that this is horrid, but its ok // for Powertaps but maybe more precise devices will // come along? secs = floor(value); // assume it is positive!! .. it is a time field! msecs = round((value - secs) * 100) * 10; } RideEditor::RideEditor(Context *context) : GcChartWindow(context), data(NULL), ride(NULL), context(context), inLUW(false), colMapper(NULL) { setControls(NULL); QVBoxLayout *mainLayout = new QVBoxLayout; mainLayout->setSpacing(0); mainLayout->setContentsMargins(2,0,2,2); setChartLayout(mainLayout); //Left in the code to display a title, but //its a waste of screen estate, maybe uncomment //it if someone finds it useful //title = new QLabel(tr("No ride selected")); //QFont font; //font.setWeight(Qt::black); //title->setFont(font); //title->setAlignment(Qt::AlignHCenter); // setup the toolbar toolbar = new QToolBar(this); toolbar->setToolButtonStyle(Qt::ToolButtonTextUnderIcon); toolbar->setFloatable(true); toolbar->setIconSize(QSize(18,18)); QIcon saveIcon(":images/toolbar/save.png"); saveAct = new QAction(saveIcon, tr("Save"), this); connect(saveAct, SIGNAL(triggered()), this, SLOT(saveFile())); toolbar->addAction(saveAct); toolbar->addSeparator(); // undo and redo deliberately at a distance from the // save icon, since accidentally hitting the wrong // icon in that instance would be horrible QIcon undoIcon(":images/toolbar/undo.png"); undoAct = new QAction(undoIcon, tr("Undo"), this); connect(undoAct, SIGNAL(triggered()), this, SLOT(undo())); toolbar->addAction(undoAct); QIcon redoIcon(":images/toolbar/redo.png"); redoAct = new QAction(redoIcon, tr("Redo"), this); connect(redoAct, SIGNAL(triggered()), this, SLOT(redo())); toolbar->addAction(redoAct); toolbar->addSeparator(); QIcon findIcon(":images/toolbar/search.png"); searchAct = new QAction(findIcon, tr("Find"), this); connect(searchAct, SIGNAL(triggered()), this, SLOT(find())); toolbar->addAction(searchAct); QIcon checkIcon(":images/toolbar/splash green.png"); checkAct = new QAction(checkIcon, tr("Anomalies"), this); connect(checkAct, SIGNAL(triggered()), this, SLOT(anomalies())); toolbar->addAction(checkAct); // empty model model = new RideFileTableModel(NULL); // set up the table table = new QTableView(); table->setItemDelegate(new CellDelegate(this)); table->verticalHeader()->setDefaultSectionSize(20); table->setModel(model); table->setContextMenuPolicy(Qt::CustomContextMenu); table->setSelectionMode(QAbstractItemView::ContiguousSelection); table->installEventFilter(this); // prettify (and make anomalies more visible) QPen gridStyle; gridStyle.setColor(Qt::lightGray); table->setGridStyle(Qt::DotLine); connect(table, SIGNAL(customContextMenuRequested(const QPoint&)), this, SLOT(cellMenu(const QPoint&))); // layout the widget //mainLayout->addWidget(title); mainLayout->addWidget(toolbar); mainLayout->addWidget(table); // trap GC signals connect(context, SIGNAL(intervalSelected()), this, SLOT(intervalSelected())); //connect(main, SIGNAL(rideSelected()), this, SLOT(rideSelected())); connect(this, SIGNAL(rideItemChanged(RideItem*)), this, SLOT(rideSelected())); connect(context, SIGNAL(rideDirty(RideItem*)), this, SLOT(rideDirty())); connect(context, SIGNAL(rideClean(RideItem*)), this, SLOT(rideClean())); connect(context, SIGNAL(configChanged()), this, SLOT(configChanged())); // put find tool and anomaly list in the controls findTool = new FindDialog(this); findTool->hide(); anomalyTool = new AnomalyDialog(this); anomalyTool->hide(); // allow us to jump to an anomaly connect(anomalyTool->anomalyList, SIGNAL(itemSelectionChanged()), this, SLOT(anomalySelected())); // set initial color config configChanged(); } void RideEditor::configChanged() { setProperty("color", GColor(CPLOTBACKGROUND)); QPalette palette; palette.setBrush(QPalette::Window, QBrush(GColor(CPLOTBACKGROUND))); palette.setBrush(QPalette::Background, QBrush(GColor(CPLOTBACKGROUND))); palette.setBrush(QPalette::Base, QBrush(GColor(CPLOTBACKGROUND))); palette.setColor(QPalette::WindowText, GCColor::invertColor(GColor(CPLOTBACKGROUND))); palette.setColor(QPalette::Text, GCColor::invertColor(GColor(CPLOTBACKGROUND))); palette.setColor(QPalette::Normal, QPalette::Window, GCColor::invertColor(GColor(CPLOTBACKGROUND))); setPalette(palette); table->setPalette(palette); table->setStyleSheet(QString("QTableView QTableCornerButton::section { background-color: %1; color: %2; border: %1 }") .arg(GColor(CPLOTBACKGROUND).name()) .arg(GCColor::invertColor(GColor(CPLOTBACKGROUND)).name())); table->horizontalHeader()->setStyleSheet(QString("QHeaderView::section { background-color: %1; color: %2; border: 0px }") .arg(GColor(CPLOTBACKGROUND).name()) .arg(GCColor::invertColor(GColor(CPLOTBACKGROUND)).name())); table->verticalHeader()->setStyleSheet(QString("QHeaderView::section { background-color: %1; color: %2; border: 0px }") .arg(GColor(CPLOTBACKGROUND).name()) .arg(GCColor::invertColor(GColor(CPLOTBACKGROUND)).name())); toolbar->setStyleSheet(QString("background: %1; border: 0px;").arg(GColor(CPLOTBACKGROUND).name())); } //---------------------------------------------------------------------- // RideEditor table/model/rideFile utility functions //---------------------------------------------------------------------- // what are the available columns? (used by insert column context menu) QList RideEditor::whatColumns() { QList what; what << tr("Time") << tr("Distance") << tr("Power") << tr("Heartrate") << tr("Cadence") << tr("Speed") << tr("Torque") << tr("Altitude") << tr("Latitude") << tr("Longitude") << tr("Headwind") << tr("Slope") << tr("Temperature") << tr("Interval"); return what; } double RideEditor::getValue(int row, int column) { if (row < 0 || column < 0) return 0.0; return ride->ride()->getPointValue(row, model->columnType(column)); } void RideEditor::setModelValue(int row, int col, double value) { model->setValue(row, col, value); } bool RideEditor::isAnomaly(int row, int col) { if (row < 0 || col < 0) return false; if (data->anomalies.value(xsstring(row,model->columnType(col)), "") != "") return true; return false; } bool RideEditor::isFound(int row, int col) { if (row < 0 || col < 0) return false; if (data->found.value(xsstring(row,model->columnType(col)), "") != "") return true; return false; } bool RideEditor::isTooPrecise(int row, int column) { if (row < 0 || column < 0) return false; RideFile::SeriesType what = model->columnType(column); int dp; QString value = QString("%1").arg(getValue(row, column), 0, 'g', 10); if ((dp = value.indexOf(".")) >= 0) if (value.length()-(dp+1) > RideFile::decimalsFor(what)) return true; return false; } bool RideEditor::isRowSelected() { QList selection = table->selectionModel()->selection().indexes(); if (selection.count() > 0 && selection[0].column() == 0 && selection[selection.count()-1].column() == (model->columnCount()-1)) return true; return false; } bool RideEditor::isColumnSelected() { QList selection = table->selectionModel()->selection().indexes(); if (selection.count() > 0 && selection[0].row() == 0 && selection[selection.count()-1].row() == (ride->ride()->dataPoints().count()-1)) return true; return false; } //---------------------------------------------------------------------- // Toolbar functions //---------------------------------------------------------------------- void RideEditor::saveFile() { if (ride && ride->isDirty()) { context->mainWindow->saveRideSingleDialog(context, ride); } } void RideEditor::undo() { if (ride && ride->ride() && ride->ride()->command) ride->ride()->command->undoCommand(); } void RideEditor::redo() { if (ride && ride->ride() && ride->ride()->command) ride->ride()->command->redoCommand(); } void RideEditor::hideEvent(QHideEvent *) { findTool->hide(); anomalyTool->hide(); } void RideEditor::find() { // find only valid if we have data points to search across if (ride && ride->ride() && ride->ride()->dataPoints().count()) findTool->show(); } void RideEditor::anomalies() { // show anomalies list .. hidden when tab changed or ride selected if (ride && ride->ride() && ride->ride()->dataPoints().count()) anomalyTool->show(); } void AnomalyDialog::check() { // run through all the available channels and find anomalies rideEditor->data->anomalies.clear(); // clear the list anomalyList->clear(); //QStringList header; //header << "Id" << "Anomalies"; //anomalyList->setHorizontalHeaderLabels(header); anomalyList->horizontalHeader()->hide(); QVector power; QVector secs; double lastdistance=9; int count = 0; foreach (RideFilePoint *point, rideEditor->ride->ride()->dataPoints()) { power.append(point->watts); secs.append(point->secs); if (count) { // whilst we are here we might as well check for gaps in recording // anything bigger than a second is of a material concern // and we assume time always flows forward ;-) double diff = secs[count] - (secs[count-1] + rideEditor->ride->ride()->recIntSecs()); if (diff > (double)1.0 || diff < (double)-1.0 || secs[count] < secs[count-1]) { rideEditor->data->anomalies.insert(xsstring(count, RideFile::secs), tr("Invalid recording gap")); } // and on the same theme what about distance going backwards? if (point->km < lastdistance) rideEditor->data->anomalies.insert(xsstring(count, RideFile::km), tr("Distance goes backwards.")); } lastdistance = point->km; // suspicious values if (point->cad > 150) { rideEditor->data->anomalies.insert(xsstring(count, RideFile::cad), tr("Suspiciously high cadence")); } if (point->hr > 200) { rideEditor->data->anomalies.insert(xsstring(count, RideFile::hr), tr("Suspiciously high heartrate")); } if (point->kph > 100) { rideEditor->data->anomalies.insert(xsstring(count, RideFile::kph), tr("Suspiciously high speed")); } if (point->lat > 90 || point->lat < -90) { rideEditor->data->anomalies.insert(xsstring(count, RideFile::lat), tr("Out of bounds value")); } if (point->lon > 180 || point->lon < -180) { rideEditor->data->anomalies.insert(xsstring(count, RideFile::lon), tr("Out of bounds value")); } if (rideEditor->ride->ride()->areDataPresent()->cad && point->nm && !point->cad) { rideEditor->data->anomalies.insert(xsstring(count, RideFile::nm), tr("Non-zero torque but zero cadence")); } count++; } // lets look at the Power Column if its there and has enough data int column = rideEditor->model->headings().indexOf(tr("Power")); if (column >= 0 && rideEditor->ride->ride()->dataPoints().count() >= 30) { // get spike config double max = appsettings->value(this, GC_DPFS_MAX, "1500").toDouble(); double variance = appsettings->value(this, GC_DPFS_VARIANCE, "1000").toDouble(); LTMOutliers outliers(secs.data(), power.data(), power.count(), 30, false); // run through the ranked list for (int i=0; idata->anomalies.insert(xsstring(outliers.getIndexForRank(i), RideFile::watts), tr("Data spike candidate")); } } // now fill in the anomaly list anomalyList->setRowCount(0); // <<< fixes crash at ZZZZ anomalyList->setRowCount(rideEditor->data->anomalies.count()); // <<< ZZZZ int counter = 0; QMapIterator f(rideEditor->data->anomalies); while (f.hasNext()) { f.next(); QTableWidgetItem *t = new QTableWidgetItem; t->setText(f.key()); t->setFlags(t->flags() & (~Qt::ItemIsEditable)); anomalyList->setItem(counter, 0, t); t = new QTableWidgetItem; t->setText(f.value()); t->setFlags(t->flags() & (~Qt::ItemIsEditable)); t->setForeground(QBrush(Qt::red)); anomalyList->setItem(counter, 1, t); counter++; } // enable the toolbar / disable for anomalies found if (counter) rideEditor->checkAct->setEnabled(true); else rideEditor->checkAct->setEnabled(false); // redraw - even if no anomalies were found since // some may have been highlighted previouslt. This is // an expensive operation, but then so is the check() // function. rideEditor->model->forceRedraw(); } //---------------------------------------------------------------------- // handle TableView signals / events //---------------------------------------------------------------------- bool RideEditor::eventFilter(QObject *object, QEvent *e) { // not for the table? if (object != (QObject *)table) return false; // what happenned? switch(e->type()) { case QEvent::ContextMenu: borderMenu(((QMouseEvent *)e)->pos()); return true; // I'll take that thanks break; case QEvent::KeyPress: { QKeyEvent *keyEvent = static_cast(e); if (keyEvent->modifiers() & Qt::ControlModifier) { switch (keyEvent->key()) { case Qt::Key_C: // defacto standard for copy copy(); return true; case Qt::Key_V: // defacto standard for paste paste(); return true; case Qt::Key_X: // defacto standard for cut cut(); return true; case Qt::Key_Y: // emerging standard for redo redo(); return true; case Qt::Key_Z: // common standard for undo undo(); return true; case Qt::Key_0: clear(); return true; default: return false; } } break; } default: break; } return false; } void RideEditor::stdContextMenu(QMenu *menu, const QPoint &pos) { int row = table->indexAt(pos).row(); int column = table->indexAt(pos).column(); QIcon undoIcon(":images/toolbar/undo.png"); QIcon redoIcon(":images/toolbar/redo.png"); QIcon cutIcon(":images/toolbar/cut.png"); QIcon pasteIcon(":images/toolbar/paste.png"); QIcon copyIcon(":images/toolbar/copy.png"); bool pastable = QApplication::clipboard()->text() == "" ? false : true; // setup all the actions QAction *undoAct = new QAction(undoIcon, tr("Undo"), table); undoAct->setShortcut(QKeySequence("Ctrl+Z")); undoAct->setEnabled(ride->ride()->command->undoCount() > 0); menu->addAction(undoAct); connect(undoAct, SIGNAL(triggered()), this, SLOT(undo())); QAction *redoAct = new QAction(redoIcon, tr("Redo"), table); redoAct->setShortcut(QKeySequence("Ctrl+Y")); redoAct->setEnabled(ride->ride()->command->redoCount() > 0); menu->addAction(redoAct); connect(redoAct, SIGNAL(triggered()), this, SLOT(redo())); menu->addSeparator(); QAction *cutAct = new QAction(cutIcon, tr("Cut"), table); cutAct->setShortcut(QKeySequence("Ctrl+X")); cutAct->setEnabled(isRowSelected() || isColumnSelected()); menu->addAction(cutAct); connect(cutAct, SIGNAL(triggered()), this, SLOT(cut())); QAction *copyAct = new QAction(copyIcon, tr("Copy"), table); copyAct->setShortcut(QKeySequence("Ctrl+C")); copyAct->setEnabled(true); menu->addAction(copyAct); connect(copyAct, SIGNAL(triggered()), this, SLOT(copy())); QAction *pasteAct = new QAction(pasteIcon, tr("Paste"), table); pasteAct->setShortcut(QKeySequence("Ctrl+V")); pasteAct->setEnabled(pastable); menu->addAction(pasteAct); connect(pasteAct, SIGNAL(triggered()), this, SLOT(paste())); QAction *specialAct = new QAction(tr("Paste Special..."), table); specialAct->setEnabled(pastable); menu->addAction(specialAct); connect(specialAct, SIGNAL(triggered()), this, SLOT(pasteSpecial())); QAction *clearAct = new QAction(tr("Clear Contents"), table); clearAct->setShortcut(QKeySequence("Ctrl+0")); clearAct->setEnabled(true); menu->addAction(clearAct); connect(clearAct, SIGNAL(triggered()), this, SLOT(clear())); currentCell.row = row; currentCell.column = column; } void RideEditor::cellMenu(const QPoint &pos) { int row = table->indexAt(pos).row(); int column = table->indexAt(pos).column(); bool anomaly = isAnomaly(row, column); QMenu menu(table); stdContextMenu(&menu, pos); menu.addSeparator(); QAction *smoothAnomaly = new QAction(tr("Smooth Anomaly"), table); smoothAnomaly->setEnabled(anomaly); menu.addAction(smoothAnomaly); connect(smoothAnomaly, SIGNAL(triggered(void)), this, SLOT(smooth())); currentCell.row = row < 0 ? 0 : row; currentCell.column = column < 0 ? 0 : column; menu.exec(table->mapToGlobal(QPoint(pos.x(), pos.y()+20))); } void RideEditor::borderMenu(const QPoint &pos) { int column=0, row=0; // but we need to set the row or column to zero since // we are in the border, this seems an easy and quick way // to do this (the indexAt function assumes pos starts from // 0 for row/col 0 and does not include the vertical // or horizontal header width (which is what we go passed) if (pos.y() <= table->horizontalHeader()->height()) { row = 0; QPoint tickle(pos.x() - table->verticalHeader()->width(), pos.y()); column = table->indexAt(tickle).column(); } if (pos.x() <= table->verticalHeader()->width()) { column = 0; QPoint tickle(pos.x(), pos.y() - table->horizontalHeader()->height()); row = table->indexAt(tickle).row(); } // avoid crash when pos translation failed // just return with no menu options added if (row < 0 && column < 0) return; QMenu menu(table); stdContextMenu(&menu, pos); menu.addSeparator(); if (column <= 0) { QAction *delAct = new QAction(tr("Delete Row"), table); delAct->setEnabled(isRowSelected()); menu.addAction(delAct); connect(delAct, SIGNAL(triggered()), this, SLOT(delRow())); QAction *insAct = new QAction(tr("Insert Row"), table); insAct->setEnabled(true); menu.addAction(insAct); connect(insAct, SIGNAL(triggered()), this, SLOT(insRow())); } else if (row <= 0){ QAction *delAct = new QAction(tr("Remove Column"), table); delAct->setEnabled(isColumnSelected()); menu.addAction(delAct); connect(delAct, SIGNAL(triggered()), this, SLOT(delColumn())); QMenu *insCol = new QMenu(tr("Add Column"), table); insCol->setEnabled(true); // add menu options for each column if (colMapper) delete colMapper; colMapper = new QSignalMapper(this); connect(colMapper, SIGNAL(mapped(const QString &)), this, SLOT(insColumn(const QString &))); foreach(QString heading, whatColumns()) { QAction *insColAct = new QAction(heading, table); connect(insColAct, SIGNAL(triggered()), colMapper, SLOT(map())); insColAct->setEnabled(!model->headings().contains(heading)); insCol->addAction(insColAct); // map action to column heading colMapper->setMapping(insColAct, heading); } menu.addMenu(insCol); } currentCell.row = row < 0 ? 0 : row; currentCell.column = column < 0 ? 0 : column; menu.exec(table->mapToGlobal(QPoint(pos.x(), pos.y()))); } //---------------------------------------------------------------------- // Context menu actions //---------------------------------------------------------------------- void RideEditor::copy() { QString copy; QList selection = table->selectionModel()->selection().indexes(); if (selection.count() > 0) { QString text; for (int row = selection[0].row(); row <= selection[selection.count()-1].row(); row++) { for (int column = selection[0].column(); column <= selection[selection.count()-1].column(); column++) { if (column == selection[selection.count()-1].column()) text += QString("%1").arg(getValue(row,column)); else text += QString("%1\t").arg(getValue(row,column)); } text += "\n"; } QApplication::clipboard()->setText(text); // remember the headings we copied, so we can // default them if we paste special them back copyHeadings.clear(); for (int column = selection[0].column(); column <= selection[selection.count()-1].column(); column++) copyHeadings << model->headings()[column]; } } void RideEditor::cut() { copy(); if (isRowSelected()) delRow(); else if (isColumnSelected()) delColumn(); } void RideEditor::delRow() { // run through the selected rows and zap them QList selection = table->selectionModel()->selection().indexes(); if (selection.count() > 0) { // delete from table - we do in one hit since row-by-row is VERY slow ride->ride()->command->startLUW("Delete Rows"); model->removeRows(selection[0].row(), selection[selection.count()-1].row() - selection[0].row() + 1, QModelIndex()); ride->ride()->command->endLUW(); } } void RideEditor::delColumn() { // run through the selected columns and "zap" them QList selection = table->selectionModel()->selection().indexes(); if (selection.count() > 0) { // Delete each column by its SeriesType ride->ride()->command->startLUW("Delete Columns"); for(int column = selection.first().column(), count = selection.last().column() - selection.first().column() + 1; count > 0 ; count--) model->removeColumn(model->columnType(column)); ride->ride()->command->endLUW(); } } void RideEditor::insRow() { // add to model model->insertRow(currentCell.row, QModelIndex()); } void RideEditor::insColumn(QString name) { // update state data RideFile::SeriesType series; if (name == tr("Time")) series = RideFile::secs; if (name == tr("Distance")) series = RideFile::km; if (name == tr("Power")) series = RideFile::watts; if (name == tr("Cadence")) series = RideFile::cad; if (name == tr("Speed")) series = RideFile::kph; if (name == tr("Torque")) series = RideFile::nm; if (name == tr("Latitude")) series = RideFile::lat; if (name == tr("Longitude")) series = RideFile::lon; if (name == tr("Altitude")) series = RideFile::alt; if (name == tr("Headwind")) series = RideFile::headwind; if (name == tr("Interval")) series = RideFile::interval; if (name == tr("Heartrate")) series = RideFile::hr; if (name == tr("Slope")) series = RideFile::slope; if (name == tr("Temperature")) series = RideFile::temp; model->insertColumn(series); } void RideEditor::paste() { QVector > cells; QStringList seps, head; seps << "\t"; getPaste(cells, seps, head, false); // empty paste buffer if (cells.count() == 0 || cells[0].count() == 0) return; // if selected range is not the same // size as the copy buffer then barf // unless just a single cell selected QList selection = table->selectionModel()->selection().indexes(); // is anything selected? if (selection.count() == 0) { // wrong size QMessageBox oops(QMessageBox::Critical, tr("Paste error"), tr("Please select target cell or cells to paste values into.")); oops.exec(); return; } int selectedrow = selection[0].row(); int selectedcol = selection[0].column(); int selectedrows = selection[selection.count()-1].row() - selectedrow + 1; int selectedcols = selection[selection.count()-1].column() - selectedcol + 1; if (selection.count() > 1 && (selectedrows != cells.count() || selectedcols != cells[0].count())) { // wrong size QMessageBox oops(QMessageBox::Critical, tr("Paste error"), tr("Copy buffer and selected area are diffferent sizes.")); oops.exec(); return; } // overrun cols? if (selection.count() == 1 && (selectedcol + cells[0].count()) > model->columnCount()) { QMessageBox oops(QMessageBox::Critical, tr("Paste error"), tr("Copy buffer has more columns than available.")); oops.exec(); return; } // overrun rows? if (selection.count() == 1 && (selectedrow + cells.count()) > ride->ride()->dataPoints().count()) { QMessageBox oops(QMessageBox::Critical, tr("Paste error"), tr("Copy buffer has more rows than available.")); oops.exec(); return; } // go paste! ride->ride()->command->startLUW("Paste Cells"); for (int i=0; i ride->ride()->dataPoints().count()-1) break; for(int j=0; j model->columnCount()-1)) break; // set table setModelValue(selectedrow+i, selectedcol+j, cells[i][j]); } } ride->ride()->command->endLUW(); } // get clipboard into a 2-dim array of doubles void RideEditor::getPaste(QVector >&cells, QStringList &seps, QStringList &head, bool hasHeader) { QString text = QApplication::clipboard()->text(); int row = 0; int col = 0; bool first = true; QString regexpStr; regexpStr = "["; foreach (QString sep, seps) regexpStr += sep; regexpStr += "]"; QRegExp sep(regexpStr); // RegExp for seperators QRegExp ELine(("\n|\r|\r\n")); //RegExp for line endings foreach(QString line, text.split(ELine)) { if (line == "") continue; if (hasHeader && first == true) { foreach (QString token, line.split(sep)) { head << token; } } else { cells.resize(row+1); foreach (QString token, line.split(sep)) { cells[row].resize(col+1); // use strtod to get better precision char *p; cells[row][col] = strtod(token.toLatin1(), &p); col++; // if there are more cols than in the // heading row then set to unknown while (hasHeader && (col+1) > head.count()) head << "unknown"; } row++; col = 0; } first = false; } } void RideEditor::pasteSpecial() { // paste clipboard but with a dialog to // choose field columns and insert rather // than overwrite or append rather than insert PasteSpecialDialog *paster = new PasteSpecialDialog(this); // center the dialog QDesktopWidget *desktop = QApplication::desktop(); int x = (desktop->width() - paster->size().width()) / 2; int y = ((desktop->height() - paster->size().height()) / 2) -50; // move window to desired coordinates paster->move(x,y); paster->exec(); } void RideEditor::clear() { // Set the selected cells to zero ride->ride()->command->startLUW("Clear cells"); foreach (QModelIndex current, table->selectionModel()->selection().indexes()) { setModelValue(current.row(), current.column(), (double)0.0); } ride->ride()->command->endLUW(); } void RideEditor::smooth() { QString xs = xsstring(currentCell.row, model->columnType(currentCell.column)); // calculate smoothed value double left = 0.0; double right = 0.0; if (currentCell.row > 0) left = getValue(currentCell.row-1, currentCell.column); if (currentCell.row < (ride->ride()->dataPoints().count()-1)) right = getValue(currentCell.row+1, currentCell.column); double value = (left+right) / 2; // update model setModelValue(currentCell.row, currentCell.column, value); } //---------------------------------------------------------------------- // Cell item delegate //---------------------------------------------------------------------- // Cell editor - item delegate CellDelegate::CellDelegate(RideEditor *rideEditor, QObject *parent) : QItemDelegate(parent), rideEditor(rideEditor) {} // setup editor for edit of field!! QWidget *CellDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &, const QModelIndex &index) const { // what are we editing? RideFile::SeriesType what = rideEditor->model->columnType(index.column()); if (what == RideFile::secs) { QTimeEdit *timeEdit = new QTimeEdit(parent); timeEdit->setDisplayFormat ("hh:mm:ss.zzz"); connect(timeEdit, SIGNAL(editingFinished()), this, SLOT(commitAndCloseEditor())); return timeEdit; } else { QDoubleSpinBox *valueEdit = new QDoubleSpinBox(parent); valueEdit->setDecimals(RideFile::decimalsFor(what)); valueEdit->setMaximum(RideFile::maximumFor(what)); valueEdit->setMinimum(RideFile::minimumFor(what)); connect(valueEdit, SIGNAL(editingFinished()), this, SLOT(commitAndCloseEditor())); return valueEdit; } } // user hit tab or return so save away the data to our model void CellDelegate::commitAndCloseEditor() { QDoubleSpinBox *editor = qobject_cast(sender()); emit commitData(editor); emit closeEditor(editor); } // We don't set anything because the data is saved within the view not the model! void CellDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const { // what are we editing? RideFile::SeriesType what = rideEditor->model->columnType(index.column()); if (what == RideFile::secs) { int seconds, msecs; secsMsecs(index.model()->data(index, Qt::DisplayRole).toDouble(), seconds, msecs); QTime value = QTime(0,0,0,0).addSecs(seconds).addMSecs(msecs); QTimeEdit *timeEdit = qobject_cast(editor); timeEdit->setTime(value); } else { QDoubleSpinBox *valueEdit = qobject_cast(editor); double value = index.model()->data(index, Qt::DisplayRole).toString().toDouble(); valueEdit->setValue(value); } } void CellDelegate::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &/*index*/) const { if (editor) editor->setGeometry(option.rect); } // We don't set anything because the data is saved within the view not the model! void CellDelegate::setModelData(QWidget *editor, QAbstractItemModel *, const QModelIndex &index) const { // what are we editing? RideFile::SeriesType what = rideEditor->model->columnType(index.column()); if (what == RideFile::secs) { double seconds; QTime midnight(0,0,0,0); QTimeEdit *timeEdit = qobject_cast(editor); seconds = (double)midnight.secsTo(timeEdit->time()) + (double)timeEdit->time().msec() / (double)1000.00; rideEditor->setModelValue(index.row(), index.column(), seconds); } else { QDoubleSpinBox *valueEdit = qobject_cast(editor); QString value = QString("%1").arg(valueEdit->value()); rideEditor->setModelValue(index.row(), index.column(), valueEdit->value()); } } // anomalies are underlined in red, otherwise straight paintjob void CellDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { // what are we editing? RideFile::SeriesType what = rideEditor->model->columnType(index.column()); QString value; if (what == RideFile::secs) { int seconds, msecs; secsMsecs(index.model()->data(index, Qt::DisplayRole).toDouble(), seconds, msecs); value = QTime(0,0,0,0).addSecs(seconds).addMSecs(msecs).toString("hh:mm:ss.zzz"); } else value = index.model()->data(index, Qt::DisplayRole).toString(); // best place to update the tooltip is here, rather than whenever we update the editor // data, since this is just before it is used... rideEditor->model->setToolTip(index.row(), rideEditor->model->columnType(index.column()), rideEditor->data->anomalies.value(xsstring(index.row(), rideEditor->model->columnType(index.column())),"")); // found items in yellow if (rideEditor->isFound(index.row(), index.column()) == true) { painter->fillRect(option.rect, QBrush(QColor(255,255,0))); } if (rideEditor->isAnomaly(index.row(), index.column())) { // wavy line is a pain! QTextDocument *meh = new QTextDocument(QString(value)); QTextCharFormat wavy; wavy.setUnderlineStyle(QTextCharFormat::WaveUnderline); wavy.setUnderlineColor(Qt::red); QTextCursor cur = meh->find(value); cur.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor); cur.selectionStart(); cur.movePosition(QTextCursor::End, QTextCursor::KeepAnchor); cur.selectionEnd(); cur.setCharFormat(wavy); // only red background if not selected //if (rideEditor->table->selectionModel()->isSelected(index) == false) // painter->fillRect(option.rect, QBrush(QColor(255,230,230))); painter->save(); painter->translate(option.rect.x(), option.rect.y()); meh->drawContents(painter); painter->restore(); delete meh; } else { // normal render QStyleOptionViewItem myOption = option; myOption.displayAlignment = Qt::AlignLeft | Qt::AlignVCenter; drawDisplay(painter, myOption, myOption.rect, value); drawFocus(painter, myOption, myOption.rect); } // warning triangle - for high precision numbers if (rideEditor->isTooPrecise(index.row(), index.column())) { QPolygon triangle(3); triangle.putPoints(0, 3, option.rect.x(), option.rect.y(), option.rect.x()+4, option.rect.y(), option.rect.x(), option.rect.y()+4); painter->setBrush(QBrush(QColor(Qt::darkGreen))); painter->setPen(QPen(QColor(Qt::darkGreen))); painter->drawPolygon(triangle); } } //---------------------------------------------------------------------- // handle GC Signals //---------------------------------------------------------------------- void RideEditor::intervalSelected() { // is it for the ride item we are editing? if (ride && context->currentRideItem() == ride) { // clear all selections table->selectionModel()->clear(); // highlight selection and jump to last foreach(QTreeWidgetItem *x, context->athlete->allIntervalItems()->treeWidget()->selectedItems()) { IntervalItem *current = (IntervalItem*)x; if (current != NULL && current->isSelected() == true) { // what is the first dataPoint index for this interval? int start = ride->ride()->timeIndex(current->start); int end = ride->ride()->timeIndex(current->stop); // select all the rows table->selectionModel()->clearSelection(); table->selectionModel()->setCurrentIndex(model->index(start,0), QItemSelectionModel::Select); table->selectionModel()->select(QItemSelection(model->index(start,0), model->index(end,model->columnCount()-1)), QItemSelectionModel::Select); } } } } void RideEditor::rideSelected() { findTool->hide(); // hide the dialog! anomalyTool->hide(); RideItem *current = myRideItem; if (!current || !current->ride() || !current->ride()->dataPoints().count()) { model->setRide(NULL); setIsBlank(true); findTool->rideSelected(); return; } else { setIsBlank(false); } ride = current; // get/or setup ridefile state data if (ride->ride()->editorData() == NULL) { data = new EditorData; ride->ride()->setEditorData(data); } else { data = ride->ride()->editorData(); data->found.clear(); // search is not active, so clear } model->setRide(ride->ride()); // reset the save icon on the toolbar if (ride->isDirty()) saveAct->setEnabled(true); else saveAct->setEnabled(false); // connect the ride command signals so we can // set/reset undo/redo static QPointer connection = NULL; if (connection) { disconnect(connection, SIGNAL(endCommand(bool,RideCommand*)), this, SLOT(endCommand(bool,RideCommand*))); disconnect(connection, SIGNAL(beginCommand(bool,RideCommand*)), this, SLOT(beginCommand(bool,RideCommand*))); } connection = ride->ride()->command; connect(connection, SIGNAL(endCommand(bool,RideCommand*)), this, SLOT(endCommand(bool,RideCommand*))); connect(connection, SIGNAL(beginCommand(bool,RideCommand*)), this, SLOT(beginCommand(bool,RideCommand*))); // lets set them anyway if (ride->ride()->command->redoCount() == 0) redoAct->setEnabled(false); else redoAct->setEnabled(true); if (ride->ride()->command->undoCount() == 0) undoAct->setEnabled(false); else undoAct->setEnabled(true); // look for anomalies anomalyTool->check(); // update finder pane to show available channels findTool->rideSelected(); } void RideEditor::anomalySelected() { if (anomalyTool->anomalyList->currentRow() < 0) return; // jump to the found item in the main table int row; RideFile::SeriesType series; unxsstring(anomalyTool->anomalyList->item(anomalyTool->anomalyList->currentRow(), 0)->text(), row, series); table->setCurrentIndex(model->index(row,model->columnFor(series))); } // We update the current selection on the table view // to reflect the actions performed on the data. This // is especially relevant when 'redo'ing a command // since it signposts the user to the change that has // been applied and makes the UI feel more 'natural' // void RideEditor::beginCommand(bool, RideCommand *cmd) { // when executing a Logical Unit of Work we // highlight sells as we go, rather than // clearing the current selection and highlighting // only those cells updated in the current command // we set inLUW to let endCommand (below) know that // we are in an LUW if (cmd->type == RideCommand::LUW) { inLUW = true; // redo needs to clear the current selection // since we do not clear selections during // a LUW in endCommand below. We do not clear // for the first 'do' because the current // selection is being used to identify which // cells to operate upon. if (cmd->docount) { itemselection.clear(); table->selectionModel()->clearSelection(); } } } // Once the command has been executed we update the // selection model to highlight the changes to the // user. We also need to update the EditorData maps // to reflect changes when rows are added or removed void RideEditor::endCommand(bool undo, RideCommand *cmd) { // Update the undo/redo toolbar icons if (ride->ride()->command->redoCount() == 0) redoAct->setEnabled(false); else redoAct->setEnabled(true); if (ride->ride()->command->undoCount() == 0) undoAct->setEnabled(false); else undoAct->setEnabled(true); // update the selection model when a command has been executed switch (cmd->type) { case RideCommand::SetPointValue: { SetPointValueCommand *spv = (SetPointValueCommand*)cmd; // move cursor to point updated QModelIndex cursor = model->index(spv->row, model->columnFor(spv->series)); // NOTE: This is to circumvent a performance issue with multiple calls to setCurrentIndex if (inLUW) { // remember and do it at the end -- otherwise major performance impact!! itemselection << cursor; } else { table->selectionModel()->select(cursor, QItemSelectionModel::SelectCurrent); table->selectionModel()->setCurrentIndex(cursor, inLUW ? QItemSelectionModel::Select : QItemSelectionModel::SelectCurrent); } break; } case RideCommand::InsertPoint: { InsertPointCommand *ip = (InsertPointCommand *)cmd; if (undo) { // deleted this row... data->deleteRows(ip->row, 1); } else { data->insertRows(ip->row, 1); } break; } case RideCommand::DeletePoint: { DeletePointCommand *dp = (DeletePointCommand *)cmd; if (undo) { // clear current if (!inLUW) table->selectionModel()->clearSelection(); // undo delete brings in a row to highlight QModelIndex topleft = model->index(dp->row, 0); QItemSelection highlight(topleft, model->index(dp->row, model->headings().count()-1)); // highlight the rows brought back in table->selectionModel()->setCurrentIndex(topleft, QItemSelectionModel::Select); table->selectionModel()->select(highlight, inLUW ? QItemSelectionModel::Select : QItemSelectionModel::SelectCurrent); // update the EditorData maps data->insertRows(dp->row, 1); } else { // update the EditorData maps data->deleteRows(dp->row, 1); } break; } case RideCommand::DeletePoints: { DeletePointsCommand *dp = (DeletePointsCommand *)cmd; if (undo) { // clear current if (!inLUW) table->selectionModel()->clearSelection(); // highlight the rows brought back in // undo delete brings in a row to highlight QModelIndex topleft = model->index(dp->row, 0); QItemSelection highlight(topleft, model->index(dp->row+dp->count-1, model->headings().count()-1)); // highlight the rows brought back in table->selectionModel()->setCurrentIndex(topleft, QItemSelectionModel::Select); table->selectionModel()->select(highlight, inLUW ? QItemSelectionModel::Select : QItemSelectionModel::SelectCurrent); // update the EditorData maps data->insertRows(dp->row, dp->count); } else { // update the EditorData maps data->deleteRows(dp->row, dp->count); } break; } case RideCommand::AppendPoints: if (!undo) { // clear current if (!inLUW) table->selectionModel()->clearSelection(); // show the user where the rows went AppendPointsCommand *ap = (AppendPointsCommand*)cmd; QModelIndex topleft = model->index(ap->row, 0); QItemSelection highlight(topleft, model->index(ap->row+ap->count-1, model->headings().count()-1)); // move cursor and highligth all the rows table->selectionModel()->setCurrentIndex(topleft, QItemSelectionModel::Select); table->selectionModel()->select(highlight, inLUW ? QItemSelectionModel::Select : QItemSelectionModel::SelectCurrent); } break; case RideCommand::SetDataPresent: { // clear current if (!inLUW) table->selectionModel()->clearSelection(); // show the user where the rows went SetDataPresentCommand *sp = (SetDataPresentCommand*)cmd; // highlight row just arrived if ((sp->oldvalue == true && undo == true) || (sp->oldvalue == false && undo == false)) { QModelIndex top = model->index(0, model->columnFor(sp->series)); QModelIndex bottom = model->index(ride->ride()->dataPoints().count()-1, model->columnFor(sp->series)); QItemSelection highlight(top,bottom); // move cursor and highligth all the rows if (!inLUW) { table->selectionModel()->clearSelection(); table->selectionModel()->setCurrentIndex(top, QItemSelectionModel::Select); } table->selectionModel()->select(highlight, inLUW ? QItemSelectionModel::Select : QItemSelectionModel::SelectCurrent); } } break; case RideCommand::LUW: { inLUW = false; // kinda crap, but QItemSelection::merge was painfully slow // and we cannot guarantee that a LUW will be in a contiguous // range since it collects lots of atomic actions int top=99999999, left=99999999, right=-9999999, bottom=-999999; foreach (QModelIndex index, itemselection) { if (index.row() < top) top = index.row(); if (index.row() > bottom) bottom = index.row(); if (index.column() < left) left = index.column(); if (index.column() > right) right = index.column(); } itemselection.clear(); table->selectionModel()->select(QItemSelection(model->index(top,left), model->index(bottom,right)), QItemSelectionModel::Select); } break; default: break; } if (!inLUW) anomalyTool->check(); // refresh the anomalies... } void RideEditor::rideClean() { // the file was saved / reverted saveAct->setEnabled(false); } void RideEditor::rideDirty() { // the file was updated saveAct->setEnabled(true); } //---------------------------------------------------------------------- // EditorData functions //---------------------------------------------------------------------- void EditorData::deleteRows(int row, int count) { // anomalies if (anomalies.count()) { QMutableMapIterator a(anomalies); QMap newa; // updated QMap while (a.hasNext()) { a.next(); int crow; RideFile::SeriesType series; unxsstring(a.key(), crow, series); if (crow >= row && crow <= (row+count-1)) { // do nothing - i.e. don't copy across - it is zapped ; } else if (crow > (row+count-1)) { crow -= count; newa.insert(xsstring(crow,series), a.value()); } else { newa.insert(a.key(), a.value()); } } anomalies = newa; // replace with resynced values } // found if (found.count()) { QMutableMapIterator r(found); QMap newr; // updated QMap while (r.hasNext()) { r.next(); int crow; RideFile::SeriesType series; unxsstring(r.key(), crow, series); if (crow >= row && crow <= (row+count-1)) { // do nothing - i.e. don't copy across ; } else if (crow > (row+count-1)) { crow -= count; newr.insert(xsstring(crow,series), r.value()); } else { newr.insert(r.key(), r.value()); } } found = newr; // replace with resynced values } } void EditorData::deleteSeries(RideFile::SeriesType series) { // anomalies if (anomalies.count()) { QMutableMapIterator a(anomalies); QMap newa; // updated QMap while (a.hasNext()) { a.next(); int crow; RideFile::SeriesType cseries; unxsstring(a.key(), crow, cseries); if (cseries == series) { // do nothing - i.e. don't copy across - it is zapped ; } else { newa.insert(a.key(), a.value()); } } anomalies = newa; // replace with resynced values } // found if (found.count()) { QMutableMapIterator r(found); QMap newr; // updated QMap while (r.hasNext()) { r.next(); int crow; RideFile::SeriesType cseries; unxsstring(r.key(), crow, cseries); if (cseries == series) { // do nothing - i.e. don't copy across ; } else { newr.insert(r.key(), r.value()); } } found = newr; // replace with resynced values } } void EditorData::insertRows(int row, int count) { // anomalies if (anomalies.count()) { QMutableMapIterator a(anomalies); QMap newa; // updated QMap while (a.hasNext()) { a.next(); int crow; RideFile::SeriesType series; unxsstring(a.key(), crow, series); if (crow > row) { crow += count; newa.insert(xsstring(crow,series), a.value()); } else { newa.insert(a.key(), a.value()); } } anomalies = newa; // replace with resynced values } // found if (found.count()) { QMutableMapIterator r(found); QMap newr; // updated QMap while (r.hasNext()) { r.next(); int crow; RideFile::SeriesType series; unxsstring(r.key(), crow, series); if (crow > row) { crow += count; newr.insert(xsstring(crow,series), r.value()); } else { newr.insert(r.key(), r.value()); } } found = newr; // replace with resynced values } } //---------------------------------------------------------------------- // Toolbar dialogs //---------------------------------------------------------------------- // // Find Dialog // FindDialog::FindDialog(RideEditor *rideEditor) : rideEditor(rideEditor) { // setup the basic window settings; nonmodal, ontop and delete on close setWindowTitle("Search"); //setAttribute(Qt::WA_DeleteOnClose); setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint | Qt::Tool); // create UI components QLabel *look = new QLabel(tr("Find values"), this); type = new QComboBox(this); type->addItem(tr("Between")); type->addItem(tr("Not Between")); type->addItem(tr("Greater Than")); type->addItem(tr("Less Than")); type->addItem(tr("Equal To")); type->addItem(tr("Not Equal To")); andLabel = new QLabel(tr("and"), this); from = new QDoubleSpinBox(this); from->setMinimum(-9999999); from->setMaximum(9999999); from->setDecimals(5); from->setSingleStep(0.00001); to = new QDoubleSpinBox(this); to->setMinimum(-9999999); to->setMaximum(9999999); to->setDecimals(5); to->setSingleStep(0.00001); // buttons findButton = new QPushButton(tr("Find")); clearButton = new QPushButton(tr("Clear")); // results resultsTable = new QTableWidget(this); resultsTable->setColumnCount(4); resultsTable->setColumnHidden(3, true); resultsTable->setSortingEnabled(true); QStringList header; header << "Time" << "Column" << "Value"; resultsTable->setHorizontalHeaderLabels(header); resultsTable->verticalHeader()->hide(); resultsTable->setShowGrid(false); resultsTable->setSelectionMode(QAbstractItemView::SingleSelection); resultsTable->setSelectionBehavior(QAbstractItemView::SelectRows); // layout the widget QVBoxLayout *mainLayout = new QVBoxLayout(this); QGridLayout *criteria = new QGridLayout; criteria->setColumnStretch(0,1); criteria->setColumnStretch(1,2); criteria->addWidget(look, 0,0, Qt::AlignLeft|Qt::AlignTop); criteria->addWidget(type, 0,1, Qt::AlignLeft|Qt::AlignTop); criteria->addWidget(from, 1,1, Qt::AlignLeft|Qt::AlignTop); criteria->addWidget(andLabel, 2,0, Qt::AlignRight|Qt::AlignTop); criteria->addWidget(to, 2,1, Qt::AlignLeft|Qt::AlignTop); mainLayout->addLayout(criteria); chans = new QGridLayout; mainLayout->addLayout(chans); QHBoxLayout *execute = new QHBoxLayout; execute->addStretch(); execute->addWidget(findButton); mainLayout->addLayout(execute); mainLayout->addWidget(resultsTable); QHBoxLayout *closer = new QHBoxLayout; closer->addStretch(); closer->addWidget(clearButton); mainLayout->addLayout(closer); setLayout(mainLayout); connect(type, SIGNAL(currentIndexChanged(int)), this, SLOT(typeChanged(int))); connect(findButton, SIGNAL(clicked()), this, SLOT(find())); connect(clearButton, SIGNAL(clicked()), this, SLOT(clear())); connect(resultsTable, SIGNAL(itemSelectionChanged()), this, SLOT(selection())); // refresh when data changes... if (rideEditor->ride && rideEditor->ride->ride()) connect(rideEditor->ride->ride()->command, SIGNAL(endCommand(bool,RideCommand*)), this, SLOT(dataChanged())); } FindDialog::~FindDialog() { } void FindDialog::typeChanged(int index) { // 0 and 1 are range based the rest are single value if (index < 2) { from->show(); to->show(); andLabel->show(); } else { to->hide(); andLabel->hide(); } } // close doesnt really close, just hides void FindDialog::closeEvent(QCloseEvent* event) { event->ignore(); rideSelected(); // reset the search... hide(); } void FindDialog::reject() { hide(); } void FindDialog::find() { if (rideEditor->data == NULL) return; // are we looking anywhere? bool search = false; foreach (QCheckBox *c, channels) if (c->isChecked()) search = true; if (search == false) return; // ok something to do then... rideEditor->data->found.clear(); clearResultsTable(); for (int i=0; i< rideEditor->ride->ride()->dataPoints().count(); i++) { // for each selected channel, get the value and // see if it matches foreach (QCheckBox *c, channels) { if (c->isChecked()) { // which Column? int col = rideEditor->model->headings().indexOf(c->text()); if (col >= 0) { double value = rideEditor->getValue(i, col); bool match = false; switch(type->currentIndex()) { case 0 : // between if ((value >= from->value() && value <= to->value()) || (value <= from->value() && value >= to->value())) match = true; break; case 1 : // not between if (!(value >= from->value() && value <= to->value())) match = true; break; case 2 : // greater than if (value > from->value()) match = true; break; case 3 : // less than if (value < from->value()) match = true; break; case 4 : // matches if (value == from->value()) match = true; break; case 5 : // not equal if (value != from->value()) match = true; break; } if (match == true) { // highlight on the table rideEditor->data->found.insert(xsstring(i,rideEditor->model->columnType(col)), QString("%1").arg(value)); } } } } } dataChanged(); // update results table and redraw rideEditor->model->forceRedraw(); } void FindDialog::dataChanged() { // a column or row was inserted/deleted // we need to refresh the results table // to reflect the new values clearResultsTable(); // do not be tempted to removed the following two lines! // by setting the row count to zero the item selection // is cleared properly and fixes a crash in the line // marked ZZZZ below. This only occurs when the current item // selected is greater than the new row count. resultsTable->setRowCount(0); // <<< fixes crash at ZZZZ resultsTable->setColumnCount(0); resultsTable->setRowCount(rideEditor->data->found.count()); // <<< ZZZZ resultsTable->setColumnCount(4); resultsTable->setColumnHidden(3, true); // has start xystring QMapIterator f(rideEditor->data->found); resultsTable->setSortingEnabled(false);// see QT Bug QTBUG-7483 int counter =0; while (f.hasNext()) { f.next(); int row; RideFile::SeriesType series; unxsstring(f.key(), row, series); // time -- format correctly... held as a double in the model int seconds, msecs; secsMsecs(rideEditor->model->getValue(row,0), seconds, msecs); QString value = QTime(0,0,0,0).addSecs(seconds).addMSecs(msecs).toString("hh:mm:ss.zzz"); QTableWidgetItem *t = new QTableWidgetItem; t->setText(value); t->setFlags(t->flags() & (~Qt::ItemIsEditable)); resultsTable->setItem(counter, 0, t); // channel t = new QTableWidgetItem; t->setText(RideFile::seriesName(series)); t->setFlags(t->flags() & (~Qt::ItemIsEditable)); resultsTable->setItem(counter, 1, t); // value t = new QTableWidgetItem; t->setData(Qt::DisplayRole, QVariant(rideEditor->ride->ride()->getPointValue(row,series))); t->setFlags(t->flags() & (~Qt::ItemIsEditable)); resultsTable->setItem(counter, 2, t); // xs for selection t = new QTableWidgetItem; t->setText(f.key()); t->setFlags(t->flags() & (~Qt::ItemIsEditable)); resultsTable->setItem(counter, 3, t); resultsTable->setRowHeight(counter, 20); counter++; } resultsTable->setSortingEnabled(true);// see QT Bug QTBUG-7483 QStringList header; header << "Time" << "Column" << "Value"; resultsTable->setHorizontalHeaderLabels(header); } void FindDialog::clear() { if (rideEditor->data) { rideEditor->data->found.clear(); clearResultsTable(); rideEditor->model->forceRedraw(); } } void FindDialog::rideSelected() { // Update the channels we can search // wipe old ones foreach(QWidget *x, channels) { chans->removeWidget(x); delete x; } channels.clear(); // add new ones foreach (QString heading, rideEditor->model->headings()) { QCheckBox *add = new QCheckBox(heading); if (heading == tr("Power")) add->setChecked(true); else add->setChecked(false); channels << add; } int row =0; int col =0; foreach (QCheckBox *check, channels) { chans->addWidget(check, row,col); if (++col > 2) { col =0; row++; } } // clear old search results etc clear(); } void FindDialog::selection() { if (resultsTable->currentRow() < 0) return; // jump to the found item in the main table int row; RideFile::SeriesType series; unxsstring(resultsTable->item(resultsTable->currentRow(), 3)->text(), row, series); rideEditor->table->setCurrentIndex(rideEditor->model->index(row,rideEditor->model->columnFor(series))); } void FindDialog::clearResultsTable() { resultsTable->selectionModel()->clearSelection(); // zap the 3 main cols and two hidden ones for (int i=0; irowCount(); i++) { for (int j=0; jcolumnCount(); j++) delete resultsTable->takeItem(i,j); } } // // Paste Special Dialog // PasteSpecialDialog::PasteSpecialDialog(RideEditor *rideEditor, QWidget *parent) : QDialog(parent), rideEditor(rideEditor) { // setup the basic window settings; nonmodal, ontop and delete on close setWindowTitle("Paste Special"); setAttribute(Qt::WA_DeleteOnClose); setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint | Qt::Tool); QVBoxLayout *mainLayout = new QVBoxLayout(this); // create the widgets mode = new QGroupBox(tr("Paste mode")); separators = new QGroupBox(tr("Separator options")); contents = new QGroupBox(tr("Columns")); append = new QRadioButton(tr("Append")); overwrite = new QRadioButton(tr("Overwrite")); QVBoxLayout *modeLayout = new QVBoxLayout; modeLayout->addWidget(append); modeLayout->addWidget(overwrite); mode->setLayout(modeLayout); mode->setFlat(false); append->setChecked(true); hasHeader = new QCheckBox(tr("First line has headings")); tab = new QCheckBox(tr("Tab")); tab->setChecked(true); comma = new QCheckBox(tr("Comma")); semi = new QCheckBox(tr("Semi-colon")); space = new QCheckBox(tr("Space")); other = new QCheckBox(tr("Other")); otherText = new QLineEdit; QHBoxLayout *otherLayout = new QHBoxLayout; otherLayout->addWidget(other); otherLayout->addWidget(otherText); otherLayout->addStretch(); QGridLayout *sepLayout = new QGridLayout; sepLayout->addWidget(hasHeader, 0,0); sepLayout->addWidget(tab, 1,0); sepLayout->addWidget(comma, 1,1); sepLayout->addWidget(semi, 1,2); sepLayout->addWidget(space, 1,3); sepLayout->addLayout(otherLayout, 2,0,1,4); separators->setLayout(sepLayout); separators->setFlat(false); // what we got? seps << "\t"; rideEditor->getPaste(cells, seps, sourceHeadings, false); resultsTable = new QTableView(this); resultsTable->setSelectionMode(QAbstractItemView::SingleSelection); resultsTable->setSelectionBehavior(QAbstractItemView::SelectColumns); QFont font; font.setPointSize(font.pointSize()-2); // smaller please resultsTable->setFont(font); resultsTable->verticalHeader()->setDefaultSectionSize(QFontMetrics(font).height()+2); model = new QStandardItemModel; resultsTable->setModel(model); setResultsTable(); // column selector QHBoxLayout *selectorLayout = new QHBoxLayout; QLabel *selectLabel = new QLabel(tr("Column Type")); columnSelect = new QComboBox; columnSelect->addItem("Ignore"); foreach (QString name, rideEditor->model->headings()) columnSelect->addItem(name); selectorLayout->addWidget(selectLabel); selectorLayout->addWidget(columnSelect); selectorLayout->addStretch(); // contents layout QVBoxLayout *contentsLayout = new QVBoxLayout; contentsLayout->addLayout(selectorLayout); contentsLayout->addWidget(resultsTable); contents->setLayout(contentsLayout); contents->setFlat(false); okButton = new QPushButton(tr("OK")); cancelButton = new QPushButton(tr("Cancel")); QHBoxLayout *buttons = new QHBoxLayout; buttons->addStretch(); buttons->addWidget(cancelButton); buttons->addWidget(okButton); // layout the widgets QGridLayout *widgetLayout = new QGridLayout; widgetLayout->addWidget(mode, 0,0); widgetLayout->addWidget(separators,0,1); widgetLayout->addWidget(contents,1,0,1,2); widgetLayout->setRowStretch(0,1); widgetLayout->setRowStretch(1,4); mainLayout->addLayout(widgetLayout); mainLayout->addLayout(buttons); // set size hint setMinimumHeight(300); setMinimumWidth(500); connect(okButton, SIGNAL(clicked()), this, SLOT(okClicked())); connect(cancelButton, SIGNAL(clicked()), this, SLOT(cancelClicked())); connect(columnSelect, SIGNAL(currentIndexChanged(int)), this, SLOT(columnChanged())); connect(resultsTable->selectionModel(), SIGNAL(selectionChanged(const QItemSelection &, const QItemSelection &)), this, SLOT(setColumnSelect())); connect(hasHeader, SIGNAL(stateChanged(int)), this, SLOT(sepsChanged())); connect(tab, SIGNAL(stateChanged(int)), this, SLOT(sepsChanged())); connect(comma, SIGNAL(stateChanged(int)), this, SLOT(sepsChanged())); connect(semi, SIGNAL(stateChanged(int)), this, SLOT(sepsChanged())); connect(space, SIGNAL(stateChanged(int)), this, SLOT(sepsChanged())); connect(other, SIGNAL(stateChanged(int)), this, SLOT(sepsChanged())); } void PasteSpecialDialog::clearResultsTable() { model->clear(); } void PasteSpecialDialog::setResultsTable() { model->clear(); headings.clear(); // is it like what was just copied and no headings line? if (hasHeader->isChecked() == false && cells[0].count() == rideEditor->copyHeadings.count()) { headings = rideEditor->copyHeadings; } else { for (int i=0; hasHeader->isChecked() ? (iisChecked() == true) { // have we mapped this before? QString lookup = "colmap/" + sourceHeadings[i]; QString mapto = appsettings->value(this, lookup, "Ignore").toString(); // is this an available heading tho? if (columnSelect->findText(mapto) != -1) { headings << mapto; } else { headings << "Ignore"; } } else { headings << "Ignore"; } } } model->setRowCount(cells.count() < 50 ? cells.count() : 50); model->setColumnCount(cells[0].count()); model->setHorizontalHeaderLabels(headings); // just setup the first 50 rows for (int row=0; row < 50 && row < cells.count(); row++) { for (int col=0; col < cells[row].count(); col++) { // add value model->setItem(row, col, new QStandardItem(QString("%1").arg(cells[row][col]))); } } } PasteSpecialDialog::~PasteSpecialDialog() { clearResultsTable(); rideEditor->model->forceRedraw(); } void PasteSpecialDialog::okClicked() { // headings has the headings for each column // with "Ignore" set if we are to igmore it // cells contains all the actual data from the // buffer. // We have three modes; (1) insert means add new // samples at the top of the current selection // which will mean all time/distance offsets for // the remaining rows will be increased and the // time/distance for the inserted rows will start // after the previous row time/distance // (2) append will add all samples to the tail of // the ride file with time/distance offset from // the last sample currently in the ride // (3) overwrite will change the current samples to // use the non-ignored values, including time/distance // which may well create out-of-sync values. Which // is why we do an anomaly check at the end. // add these to the pasted rows double timeOffset=0, distanceOffset=0; int where = rideEditor->ride->ride()->dataPoints().count(); if (append->isChecked() && rideEditor->ride->ride()->dataPoints().count()) { timeOffset = rideEditor->ride->ride()->dataPoints().last()->secs; distanceOffset = rideEditor->ride->ride()->dataPoints().last()->km; } // if we are inserting or appending lets create an array of rows to insert if (append->isChecked()) { QVector newRows; for (int row=0; row < cells.count(); row++) { struct RideFilePoint newrow; newrow.secs = timeOffset; // in case it is being ignore or not available newrow.km = distanceOffset; // in case it is being ignored or not available for (int col=0; col < cells[row].count(); col++) { if (headings[col] == tr("Ignore")) continue; double value; if (headings[col] == tr("Time")) value = cells[row][col] + timeOffset; else if (headings[col] == tr("Distance")) value = cells[row][col] + distanceOffset; else value = cells[row][col]; // update the relevant value in the new dataPoint based upon the heading... if (headings[col] == tr("Time")) newrow.secs = value; if (headings[col] == tr("Distance")) newrow.km = value; if (headings[col] == tr("Speed")) newrow.kph = value; if (headings[col] == tr("Cadence")) newrow.cad = value; if (headings[col] == tr("Power")) newrow.watts = value; if (headings[col] == tr("Heartrate")) newrow.hr = value; if (headings[col] == tr("Torque")) newrow.nm = value; if (headings[col] == tr("Latitude")) newrow.lat = value; if (headings[col] == tr("Longitude")) newrow.lon = value; if (headings[col] == tr("Altitude")) newrow.alt = value; if (headings[col] == tr("Headwind")) newrow.headwind = value; if (headings[col] == tr("Slope")) newrow.slope = value; if (headings[col] == tr("Temperature")) newrow.temp = value; if (headings[col] == tr("Interval")) newrow.interval = value; } // add to the list newRows << newrow; } // ok. we are good to go, so overwrite target with source rideEditor->ride->ride()->command->startLUW("Paste Special"); // now we have an array add to dataPoints rideEditor->model->appendRows(newRows); // highlight the affected cells -- good UI for paste, since it might // update offscreen (esp. true for our paste append) QItemSelection highlight = QItemSelection(rideEditor->model->index(where, 0), rideEditor->model->index(where+newRows.count()-1, rideEditor->model->headings().count()-1)); // our job is done. rideEditor->ride->ride()->command->endLUW(); } else { // *** The paste special overwrite function is somewhat // *** "clever", it will only overwrite columns that were // *** selected in the dialog box amd will only overwrite // *** existing rows. This makes the paste operation quite // *** "complicated". We do quite a lot of checks to make // *** sure the user really mean't to do this... // get the target selection QList selection = rideEditor->table->selectionModel()->selection().indexes(); if (selection.count() == 0) { // wrong size QMessageBox oops(QMessageBox::Critical, tr("Paste error"), tr("Please select target cell or cells to paste values into.")); oops.exec(); accept(); return; } // to make code more readable, and with an eye to refactoring // use these vars to describe the range selected in the table // the target we will paste to (i.e. we may truncate) and the // range available in the clipboard struct range { int row, column, rows, columns; } selected, target, source; bool norange = selection.count() == 1 ? true : false; bool truncate = false, partial = false; // what is selected in the selection model? selected.row = selection.first().row(); selected.column = selection.first().column(); selected.rows = selection.last().row() - selection.first().row() + 1; selected.columns = selection.last().column() - selection.first().column() + 1; if (norange) { // Single cell selected for paste means 'from here' not // 'into here'. So from here to end of the row is selected // because we check by series type not column number when // the paste is performed we will not go out of bounds selected.columns = rideEditor->model->headings().count() - selected.column; } // what is in the clipboard? source.row = 0; // not defined, obviously source.column = 0; source.rows = cells.count(); source.columns = 0; foreach(QString heading, headings) if (heading != tr("Ignore")) source.columns++; // so what is the target? target.row = selected.row; target.column = selected.column; if (norange) { target.rows = source.rows; target.columns = source.columns; } else { target.rows = selected.rows; target.columns = selected.columns; } // out of bounds for ride? if (target.row + target.rows > rideEditor->ride->ride()->dataPoints().count()) { truncate = true; target.rows = rideEditor->ride->ride()->dataPoints().count() - target.row; } // selection smaller than clipboard? if (source.rows > target.rows) { truncate = true; } // partially fill selected rows ? if (source.rows < target.rows) { partial = true; target.rows = source.rows; } // out of bounds for columns? if (target.column + target.columns - 1 > rideEditor->model->headings().count()) { truncate = true; target.columns = rideEditor->model->headings().count() - target.column; } // selection smaller than clipboard? if (source.columns > target.columns) { truncate = true; } // partially fill columnss ? if (source.columns < target.columns) { partial = true; target.columns = source.columns; } // // Now we have calculated the source and target, lets // make sure the user agrees ... // if (truncate || partial) { // we are going to truncate QMessageBox confirm(QMessageBox::Question, tr("Copy/Paste Mismatch"), tr("The selected range and available data have " "different sizes, some data may be lost.\n\n" "Do you want to continue?"), QMessageBox::Ok | QMessageBox::Cancel); if ((confirm.exec() & QMessageBox::Cancel) != 0) { accept(); return; // accept doesn't return. } } // ok. we are good to go, so overwrite target with source rideEditor->ride->ride()->command->startLUW("Paste Special"); for (int i = 0; i < target.rows; i++) { for (int j = 0; j < target.columns; j++) { // target column type... RideFile::SeriesType what = rideEditor->model->columnType(target.column + j); // do we have that? int sourceSeries = headings.indexOf(RideFile::seriesName(what)); if (sourceSeries != -1) // YES, we have some rideEditor->ride->ride()->command->setPointValue(target.row+i, what, cells[i][sourceSeries]); } } // highlight what we did QItemSelection highlight = QItemSelection(rideEditor->model->index(target.row,target.column), rideEditor->model->index(target.row+target.rows-1, target.column+target.columns-1)); // all done. rideEditor->ride->ride()->command->endLUW(); } accept(); } void PasteSpecialDialog::cancelClicked() { reject(); } void PasteSpecialDialog::sepsChanged() { seps.clear(); cells.clear(); sourceHeadings.clear(); if (tab->isChecked()) seps << "\t"; if (comma->isChecked()) seps << ","; if (semi->isChecked()) seps << ";"; if (space->isChecked()) seps << " "; if (other->isChecked()) seps << otherText->text(); rideEditor->getPaste(cells, seps, sourceHeadings, hasHeader->isChecked()); setResultsTable(); } void PasteSpecialDialog::setColumnSelect() { QList selection = resultsTable->selectionModel()->selection().indexes(); if (selection.count() == 0) return; int column = selection[0].column(); // which column? active = true; columnSelect->setCurrentIndex(columnSelect->findText(headings[column])); active = false; } void PasteSpecialDialog::columnChanged() { if (active) return; // is anything selected? QList selection = resultsTable->selectionModel()->selection().indexes(); if (selection.count() == 0) return; // set column heading int column = selection[0].column(); QString text = columnSelect->itemText(columnSelect->currentIndex()); // set the headings string headings[column] = text; // now update the results table model->setHorizontalHeaderLabels(headings); // lets remember this mapping if its to a source header if (hasHeader->isChecked() && headings[column] != "Ignore") { QString lookup = "colmap/" + sourceHeadings[column]; appsettings->setValue(lookup, headings[column]); } } AnomalyDialog::AnomalyDialog(RideEditor *rideEditor) : rideEditor(rideEditor) { // setup the basic window settings; nonmodal, ontop and delete on close setWindowTitle("Anomalies"); //setAttribute(Qt::WA_DeleteOnClose); setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint | Qt::Tool); QVBoxLayout *main = new QVBoxLayout(this); main->setContentsMargins(0,0,0,0); main->setSpacing(0); anomalyList = new QTableWidget(this); main->addWidget(anomalyList); anomalyList->setColumnCount(2); anomalyList->setColumnHidden(0, true); anomalyList->setSortingEnabled(false); QStringList header; header << "Id" << "Anomalies"; anomalyList->setHorizontalHeaderLabels(header); anomalyList->horizontalHeader()->setStretchLastSection(true); anomalyList->verticalHeader()->hide(); anomalyList->setShowGrid(false); anomalyList->setSelectionMode(QAbstractItemView::SingleSelection); anomalyList->setSelectionBehavior(QAbstractItemView::SelectRows); } AnomalyDialog::~AnomalyDialog() { } // close doesnt really close, just hides void AnomalyDialog::closeEvent(QCloseEvent* event) { event->ignore(); hide(); } void AnomalyDialog::reject() { hide(); }