Files
GoldenCheetah/src/RideEditor.cpp
Mark Liversedge 5f8907a2ff Fix RideEditor find dialog for 'between'
The find dialog expected the between values
to be small and high, this patch will find
values between regardless of whether the
search values are lo/hi or hi/lo.

Fixes #351.
2011-08-02 00:05:50 +01:00

2342 lines
78 KiB
C++

/*
* 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 "MainWindow.h"
#include "Settings.h"
#include <QtGui>
#include <QString>
// 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<int>(series));
}
static void unxsstring(QString val, int &x, RideFile::SeriesType &series)
{
QRegExp it("^([^:]*):([^:]*)$");
it.exactMatch(val);
x = it.cap(1).toDouble();
series = static_cast<RideFile::SeriesType>(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(MainWindow *main) : GcWindow(main), data(NULL), ride(NULL), main(main), inLUW(false), colMapper(NULL)
{
setInstanceName("Ride Editor");
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->setSpacing(0);
mainLayout->setContentsMargins(2,0,2,2);
//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->setStyleSheet("background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #CFCFCF, stop: 1.0 #A8A8A8);"
"border: 0px;");
QIcon saveIcon(":images/toolbar/save.png");
saveAct = new QAction(saveIcon, tr("Save"), this);
connect(saveAct, SIGNAL(triggered()), this, SLOT(saveFile()));
toolbar->addAction(saveAct);
//QIcon findIcon(":images/toolbar/search.png");
//searchAct = new QAction(findIcon, tr("Find"), this);
//connect(searchAct, SIGNAL(triggered()), this, SLOT(find()));
//toolbar->addAction(searchAct);
// *****************************************************
// REMOVED MANUALLY RUNNING A CHECK SINCE IT IS NOW
// PRETTY EFFICIENT AND UPDATES AUTOMATICALLY WHEN
// A COMMAND COMPLETES. IF THIS BECOMES TOO MUCH OF A
// PERFORMANCE OVERHEAD THEN WE CAN RE-ADD THIS
// *****************************************************
//QIcon checkIcon(":images/toolbar/splash green.png");
//checkAct = new QAction(checkIcon, tr("Check"), this);
//connect(checkAct, SIGNAL(triggered()), this, SLOT(check()));
//toolbar->addAction(checkAct);
// 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);
// 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(main, SIGNAL(intervalSelected()), this, SLOT(intervalSelected()));
//connect(main, SIGNAL(rideSelected()), this, SLOT(rideSelected()));
connect(this, SIGNAL(rideItemChanged(RideItem*)), this, SLOT(rideSelected()));
connect(main, SIGNAL(rideDirty()), this, SLOT(rideDirty()));
connect(main, SIGNAL(rideClean()), this, SLOT(rideClean()));
// put find tool and anomaly list in the controls
findTool = new FindDialog(this);
anomalyList = new QTableWidget(this);
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);
QSplitter *controlSplitter = new QSplitter(Qt::Vertical, main);
controlSplitter->setHandleWidth(1);
//controlSplitter->setStyleSheet("background-color: white; border: none;");
controlSplitter->setFrameStyle(QFrame::NoFrame);
controlSplitter->setAutoFillBackground(true);
controlSplitter->setOrientation(Qt::Vertical);
controlSplitter->addWidget(findTool);
controlSplitter->addWidget(anomalyList);
setControls(controlSplitter);
// allow us to jump to an anomaly
connect(anomalyList, SIGNAL(itemSelectionChanged()), this, SLOT(anomalySelected()));
}
void
RideEditor::configChanged() {}
//----------------------------------------------------------------------
// RideEditor table/model/rideFile utility functions
//----------------------------------------------------------------------
// what are the available columns? (used by insert column context menu)
QList<QString>
RideEditor::whatColumns()
{
QList<QString> 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("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<QModelIndex> 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<QModelIndex> 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()) {
main->saveRideSingleDialog(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::find()
{
// look for a value in a range and allow user to next/previous across
//RideEditorFindDialog finder(this, table);
//finder.exec();
//FindDialog *finder = new FindDialog(this);
// clear when a new ride is selected
//connect(main, SIGNAL(rideSelected()), finder, SLOT(clear()));
//finder->show();
}
void
RideEditor::check()
{
// run through all the available channels and find anomalies
data->anomalies.clear();
// clear the list
anomalyList->clear();
QStringList header;
header << "Id" << "Anomalies";
anomalyList->setHorizontalHeaderLabels(header);
QVector<double> power;
QVector<double> secs;
double lastdistance=9;
int count = 0;
foreach (RideFilePoint *point, 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] + ride->ride()->recIntSecs());
if (diff > (double)1.0 || diff < (double)-1.0 || secs[count] < secs[count-1]) {
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)
data->anomalies.insert(xsstring(count, RideFile::km),
tr("Distance goes backwards."));
}
lastdistance = point->km;
// suspicious values
if (point->cad > 150) {
data->anomalies.insert(xsstring(count, RideFile::cad),
tr("Suspiciously high cadence"));
}
if (point->hr > 200) {
data->anomalies.insert(xsstring(count, RideFile::hr),
tr("Suspiciously high heartrate"));
}
if (point->kph > 100) {
data->anomalies.insert(xsstring(count, RideFile::kph),
tr("Suspiciously high speed"));
}
if (point->lat > 90 || point->lat < -90) {
data->anomalies.insert(xsstring(count, RideFile::lat),
tr("Out of bounds value"));
}
if (point->lon > 180 || point->lon < -180) {
data->anomalies.insert(xsstring(count, RideFile::lon),
tr("Out of bounds value"));
}
if (ride->ride()->areDataPresent()->cad && point->nm && !point->cad) {
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 = model->headings().indexOf(tr("Power"));
if (column >= 0 && 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 = new LTMOutliers(secs.data(), power.data(), power.count(), 30, false);
// run through the ranked list
for (int i=0; i<secs.count(); i++) {
// is this over variance threshold?
if (outliers->getDeviationForRank(i) < variance) break;
// ok, so its highly variant but is it over
// the max value we are willing to accept?
if (outliers->getYForRank(i) < max) continue;
// which one is it
data->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(data->anomalies.count()); // <<< ZZZZ
int counter = 0;
QMapIterator<QString,QString> f(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++;
}
// 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.
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<QKeyEvent *>(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<QModelIndex> 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
bool changed = false;
QList<QModelIndex> selection = table->selectionModel()->selection().indexes();
if (selection.count() > 0) {
changed = true;
// 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
bool changed = false;
QList<QModelIndex> selection = table->selectionModel()->selection().indexes();
if (selection.count() > 0) {
changed = true;
// 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;
model->insertColumn(series);
}
void
RideEditor::paste()
{
QVector<QVector<double> > 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<QModelIndex> 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<cells.count(); i++) {
// just in case check booundary (i.e. truncate)
if (selectedrow+i > ride->ride()->dataPoints().count()-1) break;
for(int j=0; j<cells[i].count(); j++) {
// just in case check boundary (i.e. truncate)
if ((selectedcol+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<QVector<double> >&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);
cells[row][col] = token.toDouble();
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()
{
bool changed = false;
// Set the selected cells to zero
ride->ride()->command->startLUW("Clear cells");
foreach (QModelIndex current, table->selectionModel()->selection().indexes()) {
changed = true;
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<QDoubleSpinBox *>(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<QTimeEdit *>(editor);
timeEdit->setTime(value);
} else {
QDoubleSpinBox *valueEdit = qobject_cast<QDoubleSpinBox *>(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<QTimeEdit *>(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<QDoubleSpinBox *>(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 (main->currentRideItem() == ride) {
// clear all selections
table->selectionModel()->select(QItemSelection(model->index(0,0),
model->index(ride->ride()->dataPoints().count(),model->columnCount()-1)),
QItemSelectionModel::Clear);
// highlight selection and jump to last
foreach(QTreeWidgetItem *x, main->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()
{
RideItem *current = myRideItem;
if (!current || !current->ride()) {
model->setRide(NULL);
if (data) {
delete data;
data = NULL;
}
findTool->rideSelected();
return;
}
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<RideFileCommand> 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
check();
// update finder pane to show available channels
findTool->rideSelected();
}
void
RideEditor::anomalySelected()
{
if (anomalyList->currentRow() < 0) return;
// jump to the found item in the main table
int row;
RideFile::SeriesType series;
unxsstring(anomalyList->item(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 XXX still TODO...
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) 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 <QString, QString> a(anomalies);
QMap<QString,QString> 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 <QString, QString> r(found);
QMap<QString,QString> 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 <QString, QString> a(anomalies);
QMap<QString,QString> 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 <QString, QString> r(found);
QMap<QString,QString> 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 <QString, QString> a(anomalies);
QMap<QString,QString> 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 <QString, QString> r(found);
QMap<QString,QString> 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();
}
}
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<QString,QString> 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->setText(QString("%1").arg(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; i<resultsTable->rowCount(); i++) {
for (int j=0; j<resultsTable->columnCount(); 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);
#if 0
QDoubleSpinBox *atRow;
QComboBox *textDelimeter;
#endif
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() ? (i<sourceHeadings.count()) : (i<cells[0].count()); i++) {
if (hasHeader->isChecked() == 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 <struct RideFilePoint> 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("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<QModelIndex> 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<QModelIndex> 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<QModelIndex> 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]);
}
}