Files
GoldenCheetah/src/AnalysisSidebar.cpp
Mark Liversedge d2901c3119 Move intervals to AnalysisSidebar
Further MainWindow updates, this time moving the code
for working with intervals to the AnalysisSidebar and the
associate menus.

Ultimately, the functions for working with Activities and
their Intervals will move from MainWindow and Sidebars to
the ActivityCollection classes.

This is a step as we strip out MainWindow to just GUI
aspects.
2013-07-18 08:00:35 +01:00

585 lines
22 KiB
C++

/*
* Copyright (c) 2012 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 "AnalysisSidebar.h"
#include "MainWindow.h"
#include "Context.h"
#include "Athlete.h"
#include "Settings.h"
#include "Units.h"
#include <QtGui>
// metadata support
#include "RideMetadata.h"
#include "SpecialFields.h"
// working with intervals
#include "IntervalItem.h"
#include "AddIntervalDialog.h"
#include "BestIntervalDialog.h"
AnalysisSidebar::AnalysisSidebar(Context *context) : QWidget(context->mainWindow), context(context)
{
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(0,0,0,0);
mainLayout->setSpacing(0);
setContentsMargins(0,0,0,0);
splitter = new GcSplitter(Qt::Vertical);
mainLayout->addWidget(splitter);
// calendar
calendarWidget = new GcMultiCalendar(context);
calendarItem = new GcSplitterItem(tr("Calendar"), iconFromPNG(":images/sidebar/calendar.png"), this);
calendarItem->addWidget(calendarWidget);
// Activity History
rideNavigator = new RideNavigator(context, true);
rideNavigator->setProperty("nomenu", true);
// retrieve settings (properties are saved when we close the window)
if (appsettings->cvalue(context->athlete->cyclist, GC_NAVHEADINGS, "").toString() != "") {
rideNavigator->setSortByIndex(appsettings->cvalue(context->athlete->cyclist, GC_SORTBY).toInt());
rideNavigator->setSortByOrder(appsettings->cvalue(context->athlete->cyclist, GC_SORTBYORDER).toInt());
rideNavigator->setGroupBy(appsettings->cvalue(context->athlete->cyclist, GC_NAVGROUPBY).toInt());
rideNavigator->setColumns(appsettings->cvalue(context->athlete->cyclist, GC_NAVHEADINGS).toString());
rideNavigator->setWidths(appsettings->cvalue(context->athlete->cyclist, GC_NAVHEADINGWIDTHS).toString());
}
QWidget *activityHistory = new QWidget(this);
activityHistory->setContentsMargins(0,0,0,0);
#ifndef Q_OS_MAC // not on mac thanks
activityHistory->setStyleSheet("padding: 0px; border: 0px; margin: 0px;");
#endif
QVBoxLayout *activityLayout = new QVBoxLayout(activityHistory);
activityLayout->setSpacing(0);
activityLayout->setContentsMargins(0,0,0,0);
activityLayout->addWidget(rideNavigator);
activityItem = new GcSplitterItem(tr("Activities"), iconFromPNG(":images/sidebar/folder.png"), this);
QAction *activityAction = new QAction(iconFromPNG(":images/sidebar/extra.png"), tr("Menu"), this);
activityItem->addAction(activityAction);
connect(activityAction, SIGNAL(triggered(void)), this, SLOT(analysisPopup()));
activityItem->addWidget(activityHistory);
// INTERVALS
intervalSummaryWindow = new IntervalSummaryWindow(context);
intervalSplitter = new QSplitter(this);
intervalSplitter->setHandleWidth(1);
intervalSplitter->setOrientation(Qt::Vertical);
intervalSplitter->addWidget(context->athlete->intervalWidget);
activeInterval = NULL;
intervalSplitter->addWidget(intervalSummaryWindow);
intervalSplitter->setFrameStyle(QFrame::NoFrame);
intervalSplitter->setCollapsible(0, false);
intervalSplitter->setCollapsible(1, false);
intervalItem = new GcSplitterItem(tr("Intervals"), iconFromPNG(":images/mac/stop.png"), this);
QAction *moreIntervalAct = new QAction(iconFromPNG(":images/sidebar/extra.png"), tr("Menu"), this);
intervalItem->addAction(moreIntervalAct);
connect(moreIntervalAct, SIGNAL(triggered(void)), this, SLOT(intervalPopup()));
intervalItem->addWidget(intervalSplitter);
splitter->addWidget(calendarItem);
splitter->addWidget(activityItem);
splitter->addWidget(intervalItem);
splitter->prepare(context->athlete->cyclist, "analysis");
// GC signal
connect(context, SIGNAL(configChanged()), this, SLOT(configChanged()));
// right click menus...
connect(rideNavigator,SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(showActivityMenu(const QPoint &)));
connect(context->athlete->intervalWidget,SIGNAL(customContextMenuRequested(const QPoint &)), this, SLOT(showIntervalMenu(const QPoint &)));
}
void
AnalysisSidebar::setRide(RideItem*ride)
{
calendarWidget->setRide(ride);
}
void
AnalysisSidebar::close()
{
appsettings->setCValue(context->athlete->cyclist, GC_SORTBY, rideNavigator->sortByIndex());
appsettings->setCValue(context->athlete->cyclist, GC_SORTBYORDER, rideNavigator->sortByOrder());
appsettings->setCValue(context->athlete->cyclist, GC_NAVGROUPBY, rideNavigator->groupBy());
appsettings->setCValue(context->athlete->cyclist, GC_NAVHEADINGS, rideNavigator->columns());
appsettings->setCValue(context->athlete->cyclist, GC_NAVHEADINGWIDTHS, rideNavigator->widths());
}
void
AnalysisSidebar::configChanged()
{
}
void
AnalysisSidebar::setFilter(QStringList filter)
{
rideNavigator->searchStrings(filter);
calendarWidget->setFilter(filter);
}
void
AnalysisSidebar::clearFilter()
{
rideNavigator->clearSearch();
calendarWidget->clearFilter();
}
/***********************************************************************
* Sidebar Menus
**********************************************************************/
void
AnalysisSidebar::analysisPopup()
{
// set the point for the menu and call below
showActivityMenu(this->mapToGlobal(QPoint(activityItem->pos().x()+activityItem->width()-20, activityItem->pos().y())));
}
void
AnalysisSidebar::showActivityMenu(const QPoint &pos)
{
if (context->athlete->treeWidget->selectedItems().size() == 0) return; //none selected!
RideItem *rideItem = (RideItem *)context->athlete->treeWidget->selectedItems().first();
if (rideItem != NULL && rideItem->text(0) != tr("All Activities")) {
QMenu menu(context->athlete->treeWidget);
QAction *actSaveRide = new QAction(tr("Save Changes"), context->athlete->treeWidget);
connect(actSaveRide, SIGNAL(triggered(void)), context->mainWindow, SLOT(saveRide()));
QAction *revertRide = new QAction(tr("Revert to Saved version"), context->athlete->treeWidget);
connect(revertRide, SIGNAL(triggered(void)), context->mainWindow, SLOT(revertRide()));
QAction *actDeleteRide = new QAction(tr("Delete Activity"), context->athlete->treeWidget);
connect(actDeleteRide, SIGNAL(triggered(void)), context->mainWindow, SLOT(deleteRide()));
QAction *actSplitRide = new QAction(tr("Split Activity"), context->athlete->treeWidget);
connect(actSplitRide, SIGNAL(triggered(void)), context->mainWindow, SLOT(splitRide()));
if (rideItem->isDirty() == true) {
menu.addAction(actSaveRide);
menu.addAction(revertRide);
}
menu.addAction(actDeleteRide);
menu.addAction(actSplitRide);
#ifdef GC_HAVE_ICAL
QAction *actUploadCalendar = new QAction(tr("Upload Activity to Calendar"), context->athlete->treeWidget);
connect(actUploadCalendar, SIGNAL(triggered(void)), context->mainWindow, SLOT(uploadCalendar()));
menu.addAction(actUploadCalendar);
#endif
menu.addSeparator();
// ride navigator stuff
//XXX QAction *colChooser = new QAction(tr("Show Column Chooser"), context->athlete->treeWidget);
//XXX connect(colChooser, SIGNAL(triggered(void)), listView, SLOT(showColumnChooser()));
//XXX menu.addAction(colChooser);
#if 0 //XXX
if (listView->groupBy() >= 0) {
// already grouped lets ungroup
QAction *nogroups = new QAction(tr("Do Not Show In Groups"), context->athlete->treeWidget);
connect(nogroups, SIGNAL(triggered(void)), listView, SLOT(noGroups()));
menu.addAction(nogroups);
} else {
QMenu *groupByMenu = new QMenu(tr("Group By"), context->athlete->treeWidget);
groupByMenu->setEnabled(true);
menu.addMenu(groupByMenu);
// add menu options for each column
if (groupByMapper) delete groupByMapper;
groupByMapper = new QSignalMapper(this);
connect(groupByMapper, SIGNAL(mapped(const QString &)), listView, SLOT(setGroupByColumnName(QString)));
foreach(QString heading, listView->columnNames()) {
if (heading == "*") continue; // special hidden column
QAction *groupByAct = new QAction(heading, context->athlete->treeWidget);
connect(groupByAct, SIGNAL(triggered()), groupByMapper, SLOT(map()));
groupByMenu->addAction(groupByAct);
// map action to column heading
groupByMapper->setMapping(groupByAct, heading);
}
}
#endif
menu.exec(pos);
}
}
void
AnalysisSidebar::intervalPopup()
{
// always show the 'find best' 'find peaks' options
QMenu menu(intervalItem);
RideItem *rideItem = (RideItem *)context->athlete->treeWidget->selectedItems().first();
if (rideItem != NULL && rideItem->ride() && rideItem->ride()->dataPoints().count()) {
QAction *actFindPeak = new QAction(tr("Find Peak Intervals"), intervalItem);
QAction *actFindBest = new QAction(tr("Find Best Intervals"), intervalItem);
connect(actFindPeak, SIGNAL(triggered(void)), this, SLOT(findPowerPeaks(void)));
connect(actFindBest, SIGNAL(triggered(void)), this, SLOT(addIntervals(void)));
menu.addAction(actFindPeak);
menu.addAction(actFindBest);
// sort but only if 2 or more intervals
if (context->athlete->allIntervals->childCount() > 1) {
QAction *actSort = new QAction(tr("Sort Intervals"), intervalItem);
connect(actSort, SIGNAL(triggered(void)), this, SLOT(sortIntervals(void)));
menu.addAction(actSort);
}
if (context->athlete->intervalWidget->selectedItems().count()) menu.addSeparator();
}
if (context->athlete->intervalWidget->selectedItems().count() == 1) {
// we can zoom, rename etc if only 1 interval is selected
QAction *actZoomInt = new QAction(tr("Zoom to interval"), context->athlete->intervalWidget);
QAction *actEditInt = new QAction(tr("Edit interval"), context->athlete->intervalWidget);
QAction *actDeleteInt = new QAction(tr("Delete interval"), context->athlete->intervalWidget);
connect(actZoomInt, SIGNAL(triggered(void)), this, SLOT(zoomIntervalSelected(void)));
connect(actEditInt, SIGNAL(triggered(void)), this, SLOT(editIntervalSelected(void)));
connect(actDeleteInt, SIGNAL(triggered(void)), this, SLOT(deleteIntervalSelected(void)));
menu.addAction(actZoomInt);
menu.addAction(actEditInt);
menu.addAction(actDeleteInt);
}
if (context->athlete->intervalWidget->selectedItems().count() > 1) {
QAction *actRenameInt = new QAction(tr("Rename selected intervals"), context->athlete->intervalWidget);
connect(actRenameInt, SIGNAL(triggered(void)), this, SLOT(renameIntervalsSelected(void)));
QAction *actDeleteInt = new QAction(tr("Delete selected intervals"), context->athlete->intervalWidget);
connect(actDeleteInt, SIGNAL(triggered(void)), this, SLOT(deleteIntervalSelected(void)));
menu.addAction(actRenameInt);
menu.addAction(actDeleteInt);
}
menu.exec(mapToGlobal((QPoint(intervalItem->pos().x()+intervalItem->width()-20, intervalItem->pos().y()))));
}
void
AnalysisSidebar::showIntervalMenu(const QPoint &pos)
{
QTreeWidgetItem *trItem = context->athlete->intervalWidget->itemAt(pos);
if (trItem != NULL && trItem->text(0) != tr("Intervals")) {
activeInterval = (IntervalItem *)trItem;
QMenu menu(context->athlete->intervalWidget);
QAction *actEditInt = new QAction(tr("Edit interval"), context->athlete->intervalWidget);
QAction *actDeleteInt = new QAction(tr("Delete interval"), context->athlete->intervalWidget);
QAction *actZoomInt = new QAction(tr("Zoom to interval"), context->athlete->intervalWidget);
QAction *actFrontInt = new QAction(tr("Bring to Front"), context->athlete->intervalWidget);
QAction *actBackInt = new QAction(tr("Send to back"), context->athlete->intervalWidget);
connect(actEditInt, SIGNAL(triggered(void)), this, SLOT(editInterval(void)));
connect(actDeleteInt, SIGNAL(triggered(void)), this, SLOT(deleteInterval(void)));
connect(actZoomInt, SIGNAL(triggered(void)), this, SLOT(zoomInterval(void)));
connect(actFrontInt, SIGNAL(triggered(void)), this, SLOT(frontInterval(void)));
connect(actBackInt, SIGNAL(triggered(void)), this, SLOT(backInterval(void)));
menu.addAction(actZoomInt);
menu.addAction(actEditInt);
menu.addAction(actDeleteInt);
menu.exec(context->athlete->intervalWidget->mapToGlobal(pos));
}
}
/*----------------------------------------------------------------------
* Intervals
*--------------------------------------------------------------------*/
void
AnalysisSidebar::addIntervals()
{
if (context->ride && context->ride->ride() && context->ride->ride()->dataPoints().count()) {
AddIntervalDialog *p = new AddIntervalDialog(context);
p->setWindowModality(Qt::ApplicationModal); // don't allow select other ride or it all goes wrong!
p->exec();
} else {
if (!context->ride || !context->ride->ride())
QMessageBox::critical(this, tr("Find Best Intervals"), tr("No activity selected"));
else
QMessageBox::critical(this, tr("Find Best Intervals"), tr("Current activity contains no data"));
}
}
void
AnalysisSidebar::addIntervalForPowerPeaksForSecs(RideFile *ride, int windowSizeSecs, QString name)
{
QList<BestIntervalDialog::BestInterval> results;
BestIntervalDialog::findBests(ride, windowSizeSecs, 1, results);
if (results.isEmpty()) return;
const BestIntervalDialog::BestInterval &i = results.first();
QTreeWidgetItem *peak =
new IntervalItem(ride, name+tr(" (%1 watts)").arg((int) round(i.avg)),
i.start, i.stop,
ride->timeToDistance(i.start),
ride->timeToDistance(i.stop),
context->athlete->allIntervals->childCount()+1);
peak->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled);
context->athlete->allIntervals->addChild(peak);
}
void
AnalysisSidebar::findPowerPeaks()
{
if (!context->ride) return;
QTreeWidgetItem *which = context->athlete->treeWidget->selectedItems().first();
if (which->type() != RIDE_TYPE) {
return;
}
if (context->ride && context->ride->ride() && context->ride->ride()->dataPoints().count()) {
addIntervalForPowerPeaksForSecs(context->ride->ride(), 5, "Peak 5s");
addIntervalForPowerPeaksForSecs(context->ride->ride(), 10, "Peak 10s");
addIntervalForPowerPeaksForSecs(context->ride->ride(), 20, "Peak 20s");
addIntervalForPowerPeaksForSecs(context->ride->ride(), 30, "Peak 30s");
addIntervalForPowerPeaksForSecs(context->ride->ride(), 60, "Peak 1min");
addIntervalForPowerPeaksForSecs(context->ride->ride(), 120, "Peak 2min");
addIntervalForPowerPeaksForSecs(context->ride->ride(), 300, "Peak 5min");
addIntervalForPowerPeaksForSecs(context->ride->ride(), 600, "Peak 10min");
addIntervalForPowerPeaksForSecs(context->ride->ride(), 1200, "Peak 20min");
addIntervalForPowerPeaksForSecs(context->ride->ride(), 1800, "Peak 30min");
addIntervalForPowerPeaksForSecs(context->ride->ride(), 3600, "Peak 60min");
// now update the RideFileIntervals
context->athlete->updateRideFileIntervals();
} else {
if (!context->ride || !context->ride->ride())
QMessageBox::critical(this, tr("Find Power Peaks"), tr("No activity selected"));
else
QMessageBox::critical(this, tr("Find Power Peaks"), tr("Current activity contains no data"));
}
}
bool
lessItem(const IntervalItem *s1, const IntervalItem *s2) {
return s1->start < s2->start;
}
void
AnalysisSidebar::sortIntervals()
{
// sort them chronologically
QList<IntervalItem*> intervals;
// set string to first interval selected
for (int i=0; i<context->athlete->allIntervals->childCount();i++)
intervals.append((IntervalItem*)(context->athlete->allIntervals->child(i)));
// now sort them into start time order
qStableSort(intervals.begin(), intervals.end(), lessItem);
// empty context->athlete->allIntervals
context->athlete->allIntervals->takeChildren();
// and put em back in chronological sequence
foreach(IntervalItem* item, intervals) {
context->athlete->allIntervals->addChild(item);
}
// now update the ridefile
context->athlete->updateRideFileIntervals(); // will emit intervalChanged() signal
}
// rename multiple intervals
void
AnalysisSidebar::renameIntervalsSelected()
{
QString string;
// set string to first interval selected
for (int i=0; i<context->athlete->allIntervals->childCount();i++) {
if (context->athlete->allIntervals->child(i)->isSelected()) {
string = context->athlete->allIntervals->child(i)->text(0);
break;
}
}
// type in a name and we will renumber all the intervals
// in the same fashion -- esp if the last characters are
RenameIntervalDialog dialog(string, this);
dialog.setFixedWidth(320);
if (dialog.exec()) {
int number = 1;
// does it end in a number?
// if so we use that to renumber from
QRegExp ends("^(.*[^0-9])([0-9]+)$");
if (ends.exactMatch(string)) {
string = ends.cap(1);
number = ends.cap(2).toInt();
} else if (!string.endsWith(" ")) string += " ";
// now go and renumber from 'number' with prefix 'string'
for (int i=0; i<context->athlete->allIntervals->childCount();i++) {
if (context->athlete->allIntervals->child(i)->isSelected())
context->athlete->allIntervals->child(i)->setText(0, QString("%1%2").arg(string).arg(number++));
}
context->athlete->updateRideFileIntervals(); // will emit intervalChanged() signal
}
}
void
AnalysisSidebar::deleteIntervalSelected()
{
// delete the intervals that are selected (from the menu)
// the normal delete intervals does that already
deleteInterval();
}
void
AnalysisSidebar::deleteInterval()
{
// now delete highlighted!
for (int i=0; i<context->athlete->allIntervals->childCount();) {
if (context->athlete->allIntervals->child(i)->isSelected()) delete context->athlete->allIntervals->takeChild(i);
else i++;
}
context->athlete->updateRideFileIntervals(); // will emit intervalChanged() signal
}
void
AnalysisSidebar::renameIntervalSelected()
{
// go edit the name
for (int i=0; i<context->athlete->allIntervals->childCount();) {
if (context->athlete->allIntervals->child(i)->isSelected()) {
context->athlete->allIntervals->child(i)->setFlags(context->athlete->allIntervals->child(i)->flags() | Qt::ItemIsEditable);
context->athlete->intervalWidget->editItem(context->athlete->allIntervals->child(i), 0);
break;
} else i++;
}
context->athlete->updateRideFileIntervals(); // will emit intervalChanged() signal
}
void
AnalysisSidebar::renameInterval() {
// go edit the name
activeInterval->setFlags(activeInterval->flags() | Qt::ItemIsEditable);
context->athlete->intervalWidget->editItem(activeInterval, 0);
}
void
AnalysisSidebar::editIntervalSelected()
{
// go edit the interval
for (int i=0; i<context->athlete->allIntervals->childCount();) {
if (context->athlete->allIntervals->child(i)->isSelected()) {
activeInterval = (IntervalItem*)context->athlete->allIntervals->child(i);
editInterval();
break;
} else i++;
}
}
void
AnalysisSidebar::editInterval()
{
IntervalItem temp = *activeInterval;
EditIntervalDialog dialog(this, temp);
if (dialog.exec()) {
*activeInterval = temp;
context->athlete->updateRideFileIntervals(); // will emit intervalChanged() signal
context->athlete->intervalWidget->update();
}
}
void
AnalysisSidebar::zoomIntervalSelected()
{
// zoom the one interval that is selected via popup menu
for (int i=0; i<context->athlete->allIntervals->childCount();) {
if (context->athlete->allIntervals->child(i)->isSelected()) {
context->notifyIntervalZoom((IntervalItem*)(context->athlete->allIntervals->child(i)));
break;
} else i++;
}
}
void
AnalysisSidebar::zoomInterval() {
// zoom into this interval on allPlot
context->notifyIntervalZoom(activeInterval);
}
void
AnalysisSidebar::frontInterval()
{
int oindex = activeInterval->displaySequence;
for (int i=0; i<context->athlete->allIntervals->childCount(); i++) {
IntervalItem *it = (IntervalItem *)context->athlete->allIntervals->child(i);
int ds = it->displaySequence;
if (ds > oindex)
it->setDisplaySequence(ds-1);
}
activeInterval->setDisplaySequence(context->athlete->allIntervals->childCount());
// signal!
context->notifyIntervalsChanged();
}
void
AnalysisSidebar::backInterval()
{
int oindex = activeInterval->displaySequence;
for (int i=0; i<context->athlete->allIntervals->childCount(); i++) {
IntervalItem *it = (IntervalItem *)context->athlete->allIntervals->child(i);
int ds = it->displaySequence;
if (ds < oindex)
it->setDisplaySequence(ds+1);
}
activeInterval->setDisplaySequence(1);
// signal!
context->notifyIntervalsChanged();
}