/* * Copyright (c) 2011 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 "SplitActivityWizard.h" // Minimum gap in recording to find a natural break to split static const double defaultMinimumGap = 15; // 15 minutes // Minimum size of segment to identify as a new activity static const double defaultMinimumSegmentSize = 20; // 20 minutes // Main wizard SplitActivityWizard::SplitActivityWizard(MainWindow *main) : QWizard(main), main(main) { // delete when done setAttribute(Qt::WA_DeleteOnClose); // Minimum 600x500 for when selecting intervals setMinimumHeight(500); setMinimumWidth(600); // title setWindowTitle(tr("Split Activity")); // set ride - unconst since we will wipe it away eventually rideItem = const_cast(main->currentRideItem()); // Set sensible defaults keepOriginal = false; minimumGap = defaultMinimumGap; minimumSegmentSize = defaultMinimumSegmentSize; usedMinimumSegmentSize = usedMinimumGap = -1; // set initial intervals list, will be adjusted // if the user modifies the default paramters intervals = new QTreeWidget; intervals->headerItem()->setText(0, tr("")); intervals->headerItem()->setText(1, tr("Start")); intervals->headerItem()->setText(2, tr("")); intervals->headerItem()->setText(3, tr("Stop")); intervals->headerItem()->setText(4, tr("Duration")); intervals->headerItem()->setText(5, tr("Distance")); intervals->headerItem()->setText(6, tr("Interval Name")); intervals->setColumnCount(7); intervals->setColumnWidth(0,30); intervals->setColumnWidth(1,80); intervals->setColumnWidth(2,30); intervals->setColumnWidth(3,80); intervals->setColumnWidth(4,80); intervals->setColumnWidth(5,80); intervals->setSelectionMode(QAbstractItemView::NoSelection); intervals->setEditTriggers(QAbstractItemView::SelectedClicked); // allow edit intervals->setUniformRowHeights(true); intervals->setIndentation(0); files = new QTreeWidget; files->headerItem()->setText(0, tr("Filename")); files->headerItem()->setText(1, tr("Date")); files->headerItem()->setText(2, tr("Time")); files->headerItem()->setText(3, tr("Duration")); files->headerItem()->setText(4, tr("Distance")); files->headerItem()->setText(5, tr("Action")); files->setColumnCount(6); files->setColumnWidth(0, 190); // filename files->setColumnWidth(1, 95); // date files->setColumnWidth(2, 90); // time files->setColumnWidth(3, 75); // duration files->setColumnWidth(4, 75); // distance files->setSelectionMode(QAbstractItemView::SingleSelection); files->setEditTriggers(QAbstractItemView::SelectedClicked); // allow edit files->setUniformRowHeights(true); files->setIndentation(0); // just the hr and power as a plot smallPlot = new SmallPlot(this); smallPlot->setFixedHeight(100); smallPlot->setData(rideItem); bg = new SplitBackground(this); bg->attach(smallPlot); // 5 step process, although Conflict may be skipped addPage(new SplitWelcome(this)); addPage(new SplitKeep(this)); addPage(new SplitParameters(this)); addPage(new SplitSelect(this)); addPage(new SplitConfirm(this)); done = false; } void SplitActivityWizard::setIntervalsList(SplitSelect *selector) { // didn't change so no need to rebuild if (usedMinimumGap == minimumGap && usedMinimumSegmentSize == minimumSegmentSize) return; // clear the table intervals->clear(); // convert to seconds int minimumGap = this->minimumGap * 60; int minimumSegmentSize = this->minimumSegmentSize * 60; // remember the last ones we used usedMinimumSegmentSize = this->minimumSegmentSize; usedMinimumGap = this->minimumGap; // find segments where gap is greater than minimumGap // and segment size is > minimumSize. If a segment is shorter // than minimumSize then ignore it (i.e. treat is as part of the gap) QList segments; double segmentStart = 0; double segmentEnd = 0; double lastSecs = 0; bool first = true; int counter = 0; foreach (RideFilePoint *p, rideItem->ride()->dataPoints()) { if (first == true) { segmentStart = segmentEnd = lastSecs = p->secs; first = false; } else { if ((p->secs - segmentEnd) >= minimumGap) { if ((segmentEnd-segmentStart) >= minimumSegmentSize) { // we have a candidate segments.append(RideFileInterval(segmentStart, segmentEnd, QString("Activity Segment #%1").arg(++counter))); } segmentEnd = segmentStart = p->secs; } else { // keep accumulating segmentEnd = p->secs; } } lastSecs = p->secs; } // we got to the end, is there a segment here? if ((segmentEnd-segmentStart) >= minimumSegmentSize) { // we have a candidate segments.append(RideFileInterval(segmentStart, segmentEnd, QString("Activity Segment #%1").arg(++counter))); } // now look at the segments and add any gaps in recording // because these MUST be honoured (i.e. user cannot unmark // these points, otherwise gaps in recording are retained // in the resulting file foreach(RideFileInterval *p, gaps) { delete p; } gaps.clear(); // wip old ones double lastsecs = rideItem->ride()->dataPoints().first()->secs; int gapnum = 0; foreach(RideFileInterval activity, segments) { if (activity.start > lastsecs) { // we have a gap gapnum++; // add to gap list RideFileInterval *gap = new RideFileInterval(lastsecs, activity.start, QString("Gap in recording #%1").arg(gapnum)); gaps.append(gap); // add to interval list segments.append(RideFileInterval(*gap)); } lastsecs = activity.stop; } if (lastsecs < rideItem->ride()->dataPoints().last()->secs) { // gap at the end gapnum++; // add to gap list RideFileInterval *gap = new RideFileInterval(lastsecs, rideItem->ride()->dataPoints().last()->secs, QString("Gap in recording #%1").arg(gapnum)); gaps.append(gap); // add to interval list segments.append(RideFileInterval(*gap)); } // first entry in list should always be entire file // so we can mark the start and stop for splitting segments.insert(0, RideFileInterval(rideItem->ride()->dataPoints().first()->secs, rideItem->ride()->dataPoints().last()->secs, "Entire Activity")); // now fold in the ride intervals segments.append(rideItem->ride()->intervals()); // now lets sort the segments in start order qSort(segments.begin(), segments.end()); // first just add all the current ride intervals counter = 0; QChar zero = QLatin1Char('0'); foreach (RideFileInterval interval, segments) { // DO NOT skip intervals that are too short //if (interval.stop - interval.start < minimumSegmentSize) continue; QTreeWidgetItem *add = new QTreeWidgetItem(intervals->invisibleRootItem()); add->setFlags(add->flags() | Qt::ItemIsEditable); // we set these intervals as checked by default bool checkit = (interval.name.startsWith("Gap in recording") || interval.name == "Entire Activity"); // disable checkbox editing (i.e. mandatory split) at gaps in recording bool disableit = (interval.name.startsWith("Gap in recording")); // selector start QCheckBox *checkBox = new QCheckBox("", this); checkBox->setChecked(checkit); checkBox->setEnabled(!disableit); intervals->setItemWidget(add, 0, checkBox); connect(checkBox, SIGNAL(stateChanged(int)), selector, SLOT(refreshMarkers())); // interval start int secs = interval.start; add->setText(1, QString("%1:%2:%3") .arg(secs/3600,2,10,zero) .arg(secs%3600/60,2,10,zero) .arg(secs%60,2,10,zero)); // selector stop checkBox = new QCheckBox("", this); checkBox->setChecked(checkit); checkBox->setEnabled(!disableit); intervals->setItemWidget(add, 2, checkBox); connect(checkBox, SIGNAL(stateChanged(int)), selector, SLOT(refreshMarkers())); // interval start secs = interval.stop; add->setText(3, QString("%1:%2:%3") .arg(secs/3600,2,10,zero) .arg(secs%3600/60,2,10,zero) .arg(secs%60,2,10,zero)); // interval duration secs = interval.stop - interval.start; add->setText(4, QString("%1:%2:%3") .arg(secs/3600,2,10,zero) .arg(secs%3600/60,2,10,zero) .arg(secs%60,2,10,zero)); // interval distance double distance = rideItem->ride()->timeToDistance(interval.stop) - rideItem->ride()->timeToDistance(interval.start); add->setText(5, QString("%1 %2") .arg(distance * (main->useMetricUnits ? 1 : MILES_PER_KM), 0, 'f', 2) .arg(main->useMetricUnits ? "km" : "mi")); // interval name add->setText(6, interval.name); // hiddden columns with dataPoint from/to add->setText(7, QString("%1").arg(rideItem->ride()->timeIndex(interval.start))); add->setText(8, QString("%1").arg(rideItem->ride()->timeIndex(interval.stop))); counter++; } smallPlot->replot(); } void SplitActivityWizard::setFilesList() { // clear the table files->clear(); // fold in current ride -- if we are removing if (keepOriginal == false) { // we will wipe the original file QTreeWidgetItem *add = new QTreeWidgetItem(files->invisibleRootItem()); add->setFlags(add->flags() | Qt::ItemIsEditable); add->setText(0, rideItem->fileName); add->setText(1, rideItem->ride()->startTime().toString(tr("dd MMM yyyy"))); add->setText(2, rideItem->ride()->startTime().toString(tr("hh:mm:ss ap"))); // get duration and distance, yuk, dup of code below, and from RideImportWizard int secs=0; double km=0; if (!rideItem->ride()->dataPoints().isEmpty() && rideItem->ride()->dataPoints().last() != NULL) { if (!secs) secs = rideItem->ride()->dataPoints().last()->secs; if (!km) km = rideItem->ride()->dataPoints().last()->km; } // set duration QChar zero = QLatin1Char ( '0' ); QString time = QString("%1:%2:%3").arg(secs/3600,2,10,zero) .arg(secs%3600/60,2,10,zero) .arg(secs%60,2,10,zero); add->setText(3, time); // set distance QString dist = main->useMetricUnits ? QString ("%1 km").arg(km, 0, 'f', 1) : QString ("%1 mi").arg(km * MILES_PER_KM, 0, 'f', 1); add->setText(4, dist); // interval action add->setText(5, "Remove"); } // create a row for each file and action QChar zero = QLatin1Char('0'); foreach (RideFile *ride, activities) { QTreeWidgetItem *add = new QTreeWidgetItem(files->invisibleRootItem()); add->setFlags(add->flags() | Qt::ItemIsEditable); QString filename = QString ("%1_%2_%3_%4_%5_%6.json") .arg(ride->startTime().date().year(), 4, 10, zero) .arg(ride->startTime().date().month(), 2, 10, zero) .arg(ride->startTime().date().day(), 2, 10, zero) .arg(ride->startTime().time().hour(), 2, 10, zero) .arg(ride->startTime().time().minute(), 2, 10, zero) .arg(ride->startTime().time().second(), 2, 10, zero); // filename add->setText(0, filename); // date and time add->setText(1, ride->startTime().toString(tr("dd MMM yyyy"))); add->setText(2, ride->startTime().toString(tr("hh:mm:ss ap"))); // get duration and distance int secs=0; double km=0; if (!ride->dataPoints().isEmpty() && ride->dataPoints().last() != NULL) { if (!secs) secs = ride->dataPoints().last()->secs; if (!km) km = ride->dataPoints().last()->km; } // set duration QChar zero = QLatin1Char ( '0' ); QString time = QString("%1:%2:%3").arg(secs/3600,2,10,zero) .arg(secs%3600/60,2,10,zero) .arg(secs%60,2,10,zero); add->setText(3, time); // set distance QString dist = main->useMetricUnits ? QString ("%1 km").arg(km, 0, 'f', 1) : QString ("%1 mi").arg(km * MILES_PER_KM, 0, 'f', 1); add->setText(4, dist); // interval action add->setText(5, "Create"); } // tidy up column widths files->resizeColumnToContents(5); } // if the file in question has a backup file already then // return the filename (without the path), otherwise return // an empty string QString SplitActivityWizard::hasBackup(QString filename) { QString backupFilename = main->home.absolutePath() + "/" + filename + ".bak"; if (QFile(backupFilename).exists()) { return QString(filename + ".bak"); } else { return ""; } } QStringList SplitActivityWizard::conflicts(QDateTime datetime) { QStringList returning; // Check if an existing ride has the same starttime QChar zero = QLatin1Char('0'); QString targetnosuffix = QString ("%1_%2_%3_%4_%5_%6") .arg(datetime.date().year(), 4, 10, zero) .arg(datetime.date().month(), 2, 10, zero) .arg(datetime.date().day(), 2, 10, zero) .arg(datetime.time().hour(), 2, 10, zero) .arg(datetime.time().minute(), 2, 10, zero) .arg(datetime.time().second(), 2, 10, zero); // now make a regexp for all know ride types foreach(QString suffix, RideFileFactory::instance().suffixes()) { QString conflict = main->home.absolutePath() + "/" + targetnosuffix + "." + suffix; if (QFile(conflict).exists()) returning << conflict; } return returning; } /*---------------------------------------------------------------------- * Wizard Pages *--------------------------------------------------------------------*/ // welcome SplitWelcome::SplitWelcome(SplitActivityWizard *parent) : QWizardPage(parent), wizard(parent) { setTitle(tr("Split Activity")); setSubTitle(tr("Lets get started")); QVBoxLayout *layout = new QVBoxLayout; setLayout(layout); QLabel *label = new QLabel("This wizard will help you split the current activity " "into multiple activities\n\n" "The wizard will identify segments of uninterrupted " "activity and allow you to select which ones to " "save as new activities. You will also be able to " "select any currently defined intervals too.\n\n" "If the newly created activity clashes with an existing " "activity (same date and time) then the wizard will adjust " "the start time by one or more seconds to avoid losing or " "overwriting any existing activities."); label->setWordWrap(true); layout->addWidget(label); layout->addStretch(); } // Keep original? SplitKeep::SplitKeep(SplitActivityWizard *parent) : QWizardPage(parent), wizard(parent) { setTitle(tr("Keep original")); setSubTitle(tr("Do you want to keep the original activity?")); QVBoxLayout *layout = new QVBoxLayout; setLayout(layout); QLabel *label = new QLabel("If you want to keep the current activity then you " "should ensure you have clicked on the \"Keep original " "activity\" check box below.\n\n" "If you do not choose to keep the original activity " "it will be backed up before removing it from the " "activity history.\n\n"); label->setWordWrap(true); keepOriginal = new QCheckBox("Keep original activity", this); keepOriginal->setChecked(wizard->keepOriginal); warning = new QLabel(this); warning->setWordWrap(true); QFont font; font.setWeight(QFont::Bold); warning->setFont(font); setWarning(); layout->addWidget(label); layout->addWidget(keepOriginal); layout->addStretch(); layout->addWidget(warning); connect(keepOriginal, SIGNAL(stateChanged(int)), this, SLOT(keepOriginalChanged())); } // paramters SplitParameters::SplitParameters(SplitActivityWizard *parent) : QWizardPage(parent), wizard(parent) { setTitle(tr("Split Parameters")); setSubTitle(tr("Configure how segments are found")); QVBoxLayout *layout = new QVBoxLayout; setLayout(layout); QLabel *label = new QLabel("This wizard will find segments of activity to save " "by looking for gaps in recording. \n\n" "You can define the minimum length, in time, a gap " "in recording should be in order to mark the end of " "one segment and the beginning of another.\n\n" "In addition, you can set a minimum segment size. " "Any segment smaller than this limit will be ignored.\n\n"); label->setWordWrap(true); layout->addWidget(label); QGridLayout *grid = new QGridLayout; QLabel *minGap = new QLabel("Minimum Gap (minutes)", this); QLabel *minSize = new QLabel("Minimum Segment Size (minutes)", this); minimumGap = new QDoubleSpinBox(this); minimumGap->setDecimals(0); minimumGap->setSingleStep(1.0); minimumGap->setValue(wizard->minimumGap); minimumSegmentSize = new QDoubleSpinBox(this); minimumSegmentSize->setDecimals(0); minimumSegmentSize->setSingleStep(1.0); minimumSegmentSize->setValue(wizard->minimumSegmentSize); grid->addWidget(minGap,0,0); grid->addWidget(minimumGap,0,1,Qt::AlignLeft); grid->addWidget(minSize,1,0); grid->addWidget(minimumSegmentSize,1,1,Qt::AlignLeft); layout->addLayout(grid); layout->addStretch(); connect (minimumGap, SIGNAL(valueChanged(double)), this, SLOT(valueChanged())); connect (minimumSegmentSize, SIGNAL(valueChanged(double)), this, SLOT(valueChanged())); } void SplitParameters::valueChanged() { wizard->minimumGap = minimumGap->value(); wizard->minimumSegmentSize = minimumSegmentSize->value(); } void SplitKeep::keepOriginalChanged() { wizard->keepOriginal = keepOriginal->isChecked(); setWarning(); } void SplitKeep::setWarning() { if (!keepOriginal->isChecked()) { if (wizard->hasBackup(wizard->rideItem->fileName) != "") { warning->setText("WARNING: The current ride will be backed up and " "removed, but a backup already exists. The existing " "backup will therefore be overwritten."); return; } } warning->setText(""); } // Select SplitSelect::SplitSelect(SplitActivityWizard *parent) : QWizardPage(parent), wizard(parent) { setTitle(tr("Select Split Markers")); setSubTitle(tr("Activity will be split between marker points selected")); QVBoxLayout *layout = new QVBoxLayout; setLayout(layout); layout->addWidget(wizard->smallPlot); layout->addWidget(wizard->intervals); } void SplitSelect::initializePage() { wizard->setIntervalsList(this); refreshMarkers(); } void SplitSelect::refreshMarkers() { // update the markers on the plot foreach (QwtPlotMarker *m, wizard->markers) { // remove from the plot and delete m->detach(); delete m; } wizard->markers.clear(); wizard->marks.clear(); // dataPoint indexes // now refresh them for(int i=0; iintervals->invisibleRootItem()->childCount(); i++) { QTreeWidgetItem *current = wizard->intervals->invisibleRootItem()->child(i); // add marker for start? if (static_cast(wizard->intervals->itemWidget(current,0))->isChecked()) { long index = current->text(7).toInt(); double point = wizard->rideItem->ride()->dataPoints().at(index)->secs; wizard->marks.append(index); QwtPlotMarker *add = new QwtPlotMarker; wizard->markers.append(add); // vertical line will do for now add->setLineStyle(QwtPlotMarker::VLine); add->setLabelAlignment(Qt::AlignRight | Qt::AlignTop); add->setLinePen(QPen(GColor(CPLOTMARKER), 0, Qt::DashDotLine)); add->setValue(point / 60.0, 0.0); add->attach(wizard->smallPlot); } // add marker for stop? if (static_cast(wizard->intervals->itemWidget(current,2))->isChecked()) { long index = current->text(8).toInt(); double point = wizard->rideItem->ride()->dataPoints().at(index)->secs; wizard->marks.append(index); QwtPlotMarker *add = new QwtPlotMarker; wizard->markers.append(add); // vertical line will do for now add->setLineStyle(QwtPlotMarker::VLine); add->setLabelAlignment(Qt::AlignRight | Qt::AlignTop); add->setLinePen(QPen(GColor(CPLOTMARKER), 0, Qt::DashDotLine)); add->setValue(point / 60.0, 0.0); add->attach(wizard->smallPlot); } } wizard->smallPlot->replot(); } // Confirm SplitConfirm::SplitConfirm(SplitActivityWizard *parent) : QWizardPage(parent), wizard(parent) { setTitle(tr("Confirm")); setSubTitle(tr("Split Activity cannot be undone")); setCommitPage(true); setButtonText(QWizard::CommitButton, "Confirm"); QVBoxLayout *layout = new QVBoxLayout; setLayout(layout); layout->addWidget(wizard->files); } // create an array of rides void SplitConfirm::initializePage() { // clear the current array foreach(RideFile *ride, wizard->activities) { delete ride; } wizard->activities.clear(); // create a sorted list of markers, since we // may have duplicates and the sequence is // not guaranteed to be ordered QList points; foreach(long mark, wizard->marks) points.append(mark); // marks are indexes to ensure absolute accuracy qSort(points.begin(), points.end()); // Create a new activity for each marked segment long lastmark = -1; foreach(long mark, points) { if (mark == lastmark) continue; if (lastmark != -1) { // ignore gaps! if (!wizard->bg->isGap(wizard->rideItem->ride()->dataPoints().at(lastmark)->secs/60.0, wizard->rideItem->ride()->dataPoints().at(mark)->secs/60.0)) { RideFile *add = createRideFile(lastmark, mark); wizard->activities.append(add); } } lastmark = mark; } // now lets adjust the starttime to avoid conflicts // and record adjustment in a metadata field for transparency // it will always conflict with current ride, so we pick that // up as a special case. // we check against existing rides AND the rides we WILL create QString originalFileName = wizard->main->home.absolutePath() + "/" + wizard->rideItem->fileName; QList toBeCreated; foreach(RideFile *ride, wizard->activities) { int adjust = 0; QStringList conflicts = wizard->conflicts(ride->startTime()); while (conflicts.count() || toBeCreated.contains(ride->startTime())) { // if the only conflict is with the original filename // and it will be wiped then don't worry about it // we also check conflicts of rides the WILl be created if (conflicts.count() == 1 && conflicts.at(0) == originalFileName && wizard->keepOriginal == false && !toBeCreated.contains(ride->startTime())) { break; } adjust++; ride->setStartTime(ride->startTime().addSecs(1)); conflicts = wizard->conflicts(ride->startTime()); } // record the fact we adjusted... if (adjust) { ride->setTag("Start Time Adjust", QString("%1 seconds").arg(adjust)); } toBeCreated.append(ride->startTime()); } wizard->setFilesList(); } //create a new ride file from the current ride file //by using datapoints from index start to stop RideFile * SplitConfirm::createRideFile(long start, long stop) { RideFile *returning = new RideFile; // target RideFile *ride = wizard->rideItem->ride(); // source // set offset in seconds, make sure in bounds too double offset = 0; double distanceoffset = 0; if (start < ride->dataPoints().count() && start >= 0) { offset = ride->dataPoints().at(start)->secs; distanceoffset = ride->dataPoints().at(start)->km; } // copy first class variables (adjust starttime to include offset) returning->setStartTime(ride->startTime().addSecs(offset)); returning->setRecIntSecs(ride->recIntSecs()); returning->setDeviceType(ride->deviceType()); // lets keep the metadata too const_cast&>(returning->tags()) = QMap(ride->tags()); // now the dataPoints, check in bounds too! for(long i=start; idataPoints().count(); i++) { RideFilePoint *p = ride->dataPoints().at(i); returning->appendPoint(p->secs - offset, // start from zero! p->cad, p->hr, p->km - distanceoffset, p->kph, p->nm, p->watts, p->alt, p->lon, p->lat, p->headwind, p->interval); } // lets keep intervals that start in our section truncating them // if neccessary (some folks want to keep lap markers) double startTime = wizard->rideItem->ride()->dataPoints().at(start)->secs; double stopTime = wizard->rideItem->ride()->dataPoints().at(stop)->secs; foreach (RideFileInterval interval, wizard->rideItem->ride()->intervals()) { if (interval.start >= startTime && interval.start <= stopTime) { if (interval.stop > stopTime) returning->addInterval(interval.start - offset, stopTime, interval.name); else returning->addInterval(interval.start - offset, interval.stop - offset, interval.name); } } return returning; } bool SplitConfirm::validatePage() { if(QMessageBox::question(this, "Confirm", QString("%1 file(s) will be created.\n\nAre you sure you wish to proceed?") .arg(wizard->activities.count()), QMessageBox::Ok|QMessageBox::Cancel, QMessageBox::Cancel) == QMessageBox::Ok) { // LETS DO IT NOW! // first do we need to remove the current ride? if (wizard->keepOriginal == false) { wizard->main->removeCurrentRide(); QTreeWidgetItem *current = wizard->files->invisibleRootItem()->child(0); current->setText(5, "Removed"); } // whizz through and create each new activity int off = wizard->keepOriginal ? 0 : 1; // skip first line to remove or not? for(int i=0; iactivities.count(); i++) { QTreeWidgetItem *current = wizard->files->invisibleRootItem()->child(i+off); QString target = wizard->main->home.absolutePath() + "/" + current->text(0); JsonFileReader reader; QFile out(target); reader.writeRideFile(wizard->activities.at(i), out); current->setText(5, "Saved"); wizard->main->addRide(QFileInfo(out).fileName(), true); } // now make this page the last (so we can see what was done) setTitle("Completed"); setSubTitle("Split Activity Completed"); wizard->done = true; return true; } else return false; } bool SplitConfirm::isComplete() const { return !wizard->done; }