mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-02-14 16:39:57 +00:00
.. the alpha channel was too agressive for light backgrounds and was washed out and unpleasant on the eye. .. reduced the alpha blend to make it flatter and cleaner.
1141 lines
35 KiB
C++
1141 lines
35 KiB
C++
/*
|
|
* Copyright (c) 2015 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 "WorkoutWidgetItems.h"
|
|
#include "Context.h"
|
|
#include "Athlete.h"
|
|
#include "Zones.h"
|
|
#include "Colors.h"
|
|
|
|
#include <QDebug>
|
|
|
|
#define POWERSCALEWIDTH (5 *dpiXFactor)
|
|
#define WBALSCALEWIDTH (5 *dpiXFactor)
|
|
static bool GRIDLINES = true;
|
|
#define SPACING (4 *dpiXFactor)
|
|
|
|
static int MINTOOLHEIGHT = 350; // don't do lots of decoration on "mini" view
|
|
|
|
WWPowerScale::WWPowerScale(WorkoutWidget *w, Context *c) : WorkoutWidgetItem(w), context(c)
|
|
{
|
|
w->addItem(this);
|
|
}
|
|
|
|
void
|
|
WWPowerScale::paint(QPainter *painter)
|
|
{
|
|
// if too small paint nothing
|
|
if (workoutWidget()->height() < MINTOOLHEIGHT) return;
|
|
|
|
// fill transparent to deminish "over painted" curves
|
|
// that are shown in borders to give sense of scroll
|
|
QColor deminish(GColor(CTRAINPLOTBACKGROUND));
|
|
deminish.setAlpha(175);
|
|
painter->fillRect(workoutWidget()->left(), deminish);
|
|
|
|
int rnum = -1;
|
|
|
|
// CP etc are not available so draw nothing
|
|
if (context->athlete->zones(false) == NULL || (rnum = context->athlete->zones(false)->whichRange(QDate::currentDate())) == -1) return;
|
|
|
|
// lets get the zones, CP and PMAX
|
|
int CP = context->athlete->zones(false)->getCP(rnum);
|
|
int Pmax = context->athlete->zones(false)->getPmax(rnum);
|
|
Q_UNUSED(Pmax); // for now ........
|
|
int numZones = context->athlete->zones(false)->numZones(rnum);
|
|
|
|
QFontMetrics fontMetrics(workoutWidget()->markerFont);
|
|
QFontMetrics bfontMetrics(workoutWidget()->bigFont);
|
|
|
|
for(int i=0; i<numZones; i++) {
|
|
|
|
// zoneinfo here
|
|
QString name, description;
|
|
int low, high;
|
|
|
|
// get zoneinfo for a given range and zone
|
|
context->athlete->zones(false)->zoneInfo(rnum, i, name, description, low, high);
|
|
|
|
// draw coordinates
|
|
int ylow=workoutWidget()->transform(0,low).y();
|
|
int yhigh=workoutWidget()->transform(0,high).y();
|
|
|
|
// stay within left block
|
|
if (yhigh < workoutWidget()->canvas().y()) yhigh = workoutWidget()->canvas().y();
|
|
int ymid = (yhigh + ylow) / 2;
|
|
|
|
// bounding rect for zone color
|
|
QRect bound(QPoint(workoutWidget()->left().topRight().x()-POWERSCALEWIDTH, yhigh),
|
|
QPoint(workoutWidget()->left().topRight().x(), ylow));
|
|
|
|
// draw rect
|
|
painter->fillRect(bound, QBrush(zoneColor(i,numZones)));
|
|
|
|
// HIGH % LABELS
|
|
if (i<(numZones-1)) {
|
|
|
|
// percent labels for high only - but skip the last zone its off to infinity..
|
|
QString label = QString("%1%").arg(int(double(high) / double(CP) * 100.00f));
|
|
QRect textBound = fontMetrics.boundingRect(label);
|
|
painter->setFont(workoutWidget()->markerFont);
|
|
painter->setPen(workoutWidget()->markerPen);
|
|
|
|
painter->drawText(QPoint(workoutWidget()->left().right()-SPACING-textBound.width()-POWERSCALEWIDTH,
|
|
yhigh+(fontMetrics.ascent()/2)), // we use ascent not height to line up numbers
|
|
label);
|
|
}
|
|
|
|
// if we have gridlines enabled and don't paint on canvas border
|
|
if (GRIDLINES && yhigh > workoutWidget()->canvas().y()) {
|
|
|
|
QColor color(zoneColor(i,numZones));
|
|
color.setAlpha(128);
|
|
painter->setPen(QPen(color));
|
|
|
|
// zone high
|
|
painter->drawLine(QPoint(workoutWidget()->canvas().x(), yhigh),
|
|
QPoint(workoutWidget()->canvas().x()+workoutWidget()->canvas().width(), yhigh));
|
|
|
|
// ZONE LABELS MID CANVAS AND FEINT
|
|
QRect boundText = bfontMetrics.boundingRect(name);
|
|
painter->setFont(workoutWidget()->bigFont);
|
|
|
|
painter->drawText(QPoint(workoutWidget()->canvas().center().x() - (boundText.width()/2),
|
|
ymid+(bfontMetrics.ascent()/2)), // we use ascent not height to line up numbers
|
|
name);
|
|
}
|
|
}
|
|
|
|
// CP !
|
|
QPen cppen(GColor(CPLOTMARKER));
|
|
cppen.setStyle(Qt::DashLine);
|
|
painter->setPen(cppen);
|
|
|
|
double CPy = workoutWidget()->transform(0, CP).y();
|
|
|
|
// zone high
|
|
painter->drawLine(QPoint(workoutWidget()->canvas().x(), CPy),
|
|
QPoint(workoutWidget()->canvas().x()+workoutWidget()->canvas().width(), CPy));
|
|
|
|
}
|
|
|
|
WWWBalScale::WWWBalScale(WorkoutWidget *w, Context *c) : WorkoutWidgetItem(w), context(c)
|
|
{
|
|
w->addItem(this);
|
|
}
|
|
|
|
void
|
|
WWWBalScale::paint(QPainter *painter)
|
|
{
|
|
// if too small paint nothing
|
|
if (workoutWidget()->height() < MINTOOLHEIGHT) return;
|
|
|
|
// fill transparent to deminish "over painted" curves
|
|
// that are shown in borders to give sense of scroll
|
|
QColor deminish(GColor(CTRAINPLOTBACKGROUND));
|
|
deminish.setAlpha(175);
|
|
painter->fillRect(workoutWidget()->right(), deminish);
|
|
|
|
int rnum = -1;
|
|
|
|
// CP etc are not available so draw nothing
|
|
if (context->athlete->zones(false) == NULL || (rnum = context->athlete->zones(false)->whichRange(QDate::currentDate())) == -1) return;
|
|
|
|
// lets get the zones, CP and PMAX
|
|
int WPRIME = context->athlete->zones(false)->getWprime(rnum);
|
|
|
|
QFontMetrics fontMetrics(workoutWidget()->markerFont);
|
|
QFontMetrics bfontMetrics(workoutWidget()->bigFont);
|
|
|
|
// wprime zones are simply 25% 50% 75% 100%
|
|
for(int i=0; i<4; i++) {
|
|
|
|
// scale height
|
|
int height = workoutWidget()->canvas().height();
|
|
|
|
// bounding rect for zone color
|
|
QPointF tl = workoutWidget()->right().topLeft();
|
|
QRect bound(QPoint(tl.x(), tl.y() + (i * (height/4))),
|
|
QPoint(tl.x()+WBALSCALEWIDTH, tl.y() + ((i+1) * (height/4))));
|
|
|
|
// draw rect
|
|
QColor wbal = GColor(CWBAL);
|
|
wbal.setAlpha((255/4) * (i+1));
|
|
painter->fillRect(bound, QBrush(wbal));
|
|
|
|
// HIGH LABEL % (100,75,50,25)
|
|
|
|
// percent labels for high only - but skip the last zone its off to infinity..
|
|
QString label = QString("%1%").arg(100-(i*25));
|
|
QRect textBound = fontMetrics.boundingRect(label);
|
|
painter->setFont(workoutWidget()->markerFont);
|
|
painter->setPen(workoutWidget()->markerPen);
|
|
|
|
painter->drawText(QPoint(tl.x()+SPACING+POWERSCALEWIDTH,
|
|
tl.y() + (i * (height/4)) +(fontMetrics.ascent()/2)),
|
|
label);
|
|
|
|
// absolute labels for high WPRIME - (i*WPRIME/4)
|
|
label = QString("%1 kJ").arg(double((WPRIME - (i*(WPRIME/4)))/1000.00f), 0, 'f', 1);
|
|
textBound = fontMetrics.boundingRect(label);
|
|
|
|
painter->drawText(QPoint(tl.x()-SPACING-textBound.width(),
|
|
tl.y() + (i * (height/4)) +(fontMetrics.ascent()/2)),
|
|
label);
|
|
|
|
}
|
|
}
|
|
|
|
// the warning bar at bottom for TTE efforts
|
|
void
|
|
WWTTE::paint(QPainter *painter)
|
|
{
|
|
// don't show when recording
|
|
if (workoutWidget()->recording()) return;
|
|
|
|
QRectF gap = workoutWidget()->bottomgap();
|
|
|
|
// if no efforts/TTE found then its green by default
|
|
// but just way too colorful for my eyes
|
|
//XXX QRectF all(gap.left(), gap.top(), gap.width(), 2);
|
|
//XXX painter->fillRect(all, QBrush(Qt::green));
|
|
|
|
foreach(WWEffort m, workoutWidget()->efforts) {
|
|
|
|
// top left of rectangle
|
|
QPoint tl = workoutWidget()->transform(m.start, 0);
|
|
tl.setY(gap.top());
|
|
|
|
QPoint br = workoutWidget()->transform(m.start + m.duration, 0);
|
|
br.setY(gap.top()+2); // thin line
|
|
|
|
// paint a red rectange
|
|
if (m.quality >= 1.00f)
|
|
painter->fillRect(QRect(tl,br), QBrush(Qt::red));
|
|
else
|
|
painter->fillRect(QRect(tl,br), QBrush(QColor(255,152,0)));
|
|
}
|
|
}
|
|
|
|
// lap markers
|
|
void
|
|
WWLap::paint(QPainter *painter)
|
|
{
|
|
if (workoutWidget()->laps().count() == 0) return;
|
|
|
|
QRectF top = workoutWidget()->top();
|
|
|
|
QFontMetrics fontMetrics(workoutWidget()->bigFont);
|
|
painter->setFont(workoutWidget()->bigFont);
|
|
painter->setPen(GColor(CPLOTMARKER));
|
|
|
|
for(int i=0; i<workoutWidget()->laps().count(); i++) {
|
|
|
|
QString name = QString("%1").arg(workoutWidget()->laps()[i].LapNum);
|
|
|
|
// where to paint?
|
|
QPointF here = workoutWidget()->transform(workoutWidget()->laps()[i].x/1000.0f, 0);
|
|
here.setY(top.top() + fontMetrics.height());
|
|
|
|
// end ..
|
|
int end = workoutWidget()->maxWX();
|
|
if (i < workoutWidget()->laps().count()-1) end=workoutWidget()->laps()[i+1].x/1000.f;
|
|
double endx = workoutWidget()->transform(end,0).x();
|
|
|
|
// red rectangle for selected intervals
|
|
if (workoutWidget()->laps()[i].selected) {
|
|
QColor color(255,0,0,64); // lighten whatever it is
|
|
painter->fillRect(QRectF(here.x(), top.top(), endx - here.x(),
|
|
top.height()+workoutWidget()->canvas().height()),
|
|
QBrush(color));
|
|
}
|
|
|
|
// hover over show full length markers
|
|
painter->drawLine(QPointF(here.x(), top.top()), QPointF(here.x(), top.bottom()));
|
|
|
|
// and the text
|
|
painter->drawText(here, name);
|
|
}
|
|
}
|
|
|
|
// a dot
|
|
void
|
|
WWPoint::paint(QPainter *painter)
|
|
{
|
|
// transform
|
|
QPoint center = workoutWidget()->transform(x,y);
|
|
|
|
// don't show when recording, or not on main canvas
|
|
if (!workoutWidget()->canvas().contains(center) || workoutWidget()->recording()) return;
|
|
|
|
// if too small paint nothing
|
|
if (workoutWidget()->height() < MINTOOLHEIGHT) return;
|
|
|
|
painter->setPen(Qt::NoPen);
|
|
|
|
// highlight hovered
|
|
if (hover) {
|
|
painter->setBrush(Qt::gray);
|
|
painter->drawEllipse(QPointF(center.x(), center.y()), 10.0f*dpiXFactor, 10.0f*dpiXFactor);
|
|
}
|
|
|
|
// selected!
|
|
if (selecting || selected) {
|
|
|
|
// selected change color
|
|
painter->setBrush(Qt::red);
|
|
painter->drawEllipse(QPointF(center.x(), center.y()), 3.0f*dpiXFactor, 3.0f*dpiXFactor);
|
|
|
|
} else {
|
|
|
|
// draw point
|
|
painter->setBrush(GColor(CPOWER));
|
|
painter->drawEllipse(QPointF(center.x(), center.y()), 3.0f*dpiXFactor, 3.0f*dpiXFactor);
|
|
}
|
|
|
|
// set bound so we can be moused over etc
|
|
bound = QRectF(QPointF(center.x()-(3.0f*dpiXFactor), center.y()-(3.0f*dpiXFactor)),QPointF(center.x()+(3.0f*dpiXFactor), center.y()+(3.0f*dpiXFactor)));
|
|
}
|
|
|
|
void
|
|
WWLine::paint(QPainter *painter)
|
|
{
|
|
// thin ?
|
|
QPen linePen(workoutWidget()->recording() ? GColor(CTPOWER) : GColor(CPOWER));
|
|
linePen.setWidth(1 *dpiXFactor);
|
|
painter->setPen(linePen);
|
|
|
|
QPoint origin = workoutWidget()->transform(0,0);
|
|
QPainterPath path(QPointF(origin.x(), origin.y()));
|
|
|
|
// join the dots and make a path as you go
|
|
QPointF last(0,0);
|
|
|
|
foreach(WWPoint *p, workoutWidget()->points()) {
|
|
|
|
// might be better to always use float?
|
|
QPoint center = workoutWidget()->transform(p->x,p->y);
|
|
QPointF dot(center.x(), center.y());
|
|
|
|
// path...
|
|
if (last.x() && last.y()) painter->drawLine(last, dot);
|
|
path.lineTo(dot);
|
|
|
|
// moving on
|
|
last = dot;
|
|
}
|
|
|
|
// fill beneath
|
|
if (last.x() && last.y()) {
|
|
|
|
// drop down to baseline then back to origin
|
|
path.lineTo(QPointF(last.x(), origin.y()));
|
|
path.lineTo(origin);
|
|
|
|
// now fill
|
|
painter->setPen(Qt::NoPen);
|
|
|
|
QColor brush_color1 = QColor(GColor(CTPOWER));
|
|
brush_color1.setAlpha(240);
|
|
QColor brush_color2 = QColor(GColor(CTPOWER));
|
|
brush_color2.setAlpha(200);
|
|
|
|
QLinearGradient linearGradient(0, 0, 0, workoutWidget()->transform(0,0).y());
|
|
linearGradient.setColorAt(0.0, brush_color1);
|
|
linearGradient.setColorAt(1.0, brush_color2);
|
|
linearGradient.setSpread(QGradient::PadSpread);
|
|
|
|
painter->fillPath(path, QBrush(linearGradient));
|
|
}
|
|
}
|
|
|
|
void
|
|
WWTelemetry::paint(QPainter *painter)
|
|
{
|
|
// only when recording
|
|
if (!workoutWidget()->recording()) return;
|
|
|
|
// Draw POWER
|
|
if (workoutWidget()->shouldPlotPwr())
|
|
{
|
|
updateAvg(workoutWidget()->watts, workoutWidget()->pwrAvg, workoutWidget()->pwrPlotAvgLength());
|
|
paintSampleList(painter, GColor(CPOWER), workoutWidget()->pwrAvg, WorkoutWidget::POWER);
|
|
}
|
|
|
|
// Draw HR
|
|
if (workoutWidget()->shouldPlotHr())
|
|
{
|
|
updateAvg(workoutWidget()->hr, workoutWidget()->hrAvg, workoutWidget()->hrPlotAvgLength());
|
|
paintSampleList(painter, GColor(CHEARTRATE), workoutWidget()->hrAvg, WorkoutWidget::HEARTRATE);
|
|
}
|
|
// Draw Speed
|
|
if (workoutWidget()->shouldPlotSpeed())
|
|
{
|
|
updateAvg(workoutWidget()->speed, workoutWidget()->speedAvg, workoutWidget()->speedPlotAvgLength());
|
|
paintSampleList(painter, GColor(CSPEED), workoutWidget()->speedAvg, WorkoutWidget::SPEED);
|
|
}
|
|
// Draw Cadence
|
|
if (workoutWidget()->shouldPlotCadence())
|
|
{
|
|
updateAvg(workoutWidget()->cadence, workoutWidget()->cadenceAvg, workoutWidget()->cadencePlotAvgLength());
|
|
paintSampleList(painter, GColor(CCADENCE), workoutWidget()->cadenceAvg, WorkoutWidget::CADENCE);
|
|
}
|
|
// Draw VO2
|
|
if (workoutWidget()->shouldPlotVo2())
|
|
{
|
|
updateAvg(workoutWidget()->vo2, workoutWidget()->vo2Avg, workoutWidget()->vo2PlotAvgLength());
|
|
paintSampleList(painter, GColor(CVO2), workoutWidget()->vo2Avg, WorkoutWidget::VO2);
|
|
}
|
|
// Draw Ventilation
|
|
if (workoutWidget()->shouldPlotVentilation())
|
|
{
|
|
updateAvg(workoutWidget()->ventilation, workoutWidget()->ventilationAvg, workoutWidget()->ventilationPlotAvgLength());
|
|
paintSampleList(painter, GColor(CVENTILATION), workoutWidget()->ventilationAvg, WorkoutWidget::VENTILATION);
|
|
}
|
|
|
|
//
|
|
// W'bal last, if not zones return
|
|
//
|
|
if (workoutWidget()->shouldPlotWbal())
|
|
{
|
|
// lets get the zones, CP and PMAX, if none we're done
|
|
int rnum = -1;
|
|
|
|
// CP etc are not available so draw nothing
|
|
if (context->athlete->zones(false) == NULL ||
|
|
(rnum = context->athlete->zones(false)->whichRange(QDate::currentDate())) == -1) return;
|
|
|
|
// lets get the zones, CP and PMAX
|
|
int WPRIME = context->athlete->zones(false)->getWprime(rnum);
|
|
|
|
// full color
|
|
QColor color = GColor(CWBAL);
|
|
QPen wlinePen(color);
|
|
wlinePen.setWidth(1 *dpiXFactor);
|
|
painter->setPen(wlinePen);
|
|
|
|
// top left origin
|
|
QPointF tl = workoutWidget()->canvas().topLeft();
|
|
|
|
// pixels per WPRIME value
|
|
double ratio = workoutWidget()->canvas().height() / WPRIME;
|
|
|
|
// join the dots
|
|
QPointF last = QPointF(tl.x(),tl.y());
|
|
|
|
// run through the wpBal values...
|
|
int index = 0;
|
|
foreach(int b, workoutWidget()->wbal) {
|
|
// this dot...
|
|
if (b < 0) b=0;
|
|
|
|
// x and y pixel location
|
|
double now = workoutWidget()->sampleTimes.at(index) / 1000.0f;
|
|
double px = workoutWidget()->transform(now, 0).x();
|
|
double py = tl.y() + ((WPRIME-b) * ratio);
|
|
|
|
QPointF dot(px,py);
|
|
painter->drawLine(last, dot);
|
|
last = dot;
|
|
|
|
// next sample
|
|
index++;
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
WWRect::paint(QPainter *painter)
|
|
{
|
|
// don't show when recording
|
|
if (workoutWidget()->recording()) return;
|
|
|
|
QPointF onRect = workoutWidget()->onRect;
|
|
QPointF atRect = workoutWidget()->atRect;
|
|
|
|
// draw a selection rectangle
|
|
if (onRect != QPointF(-1,-1) && atRect != QPointF(-1,-1) && onRect != atRect) {
|
|
|
|
// thin ?
|
|
QPen linePen(GColor(CPLOTMARKER));
|
|
linePen.setWidth(1 *dpiXFactor);
|
|
painter->setPen(linePen);
|
|
|
|
painter->drawRect(QRectF(onRect,atRect));
|
|
}
|
|
}
|
|
|
|
// block cursos
|
|
void
|
|
WWBlockCursor::paint(QPainter *painter)
|
|
{
|
|
// don't show when recording
|
|
if (workoutWidget()->recording()) return;
|
|
|
|
// if were in a selection block don't draw a cursos
|
|
if (workoutWidget()->selectionBlock.contains(workoutWidget()->mapFromGlobal(QCursor::pos()))) return;
|
|
|
|
// are we busy resizing and stuff and is the block cursor valid?
|
|
if (workoutWidget()->state == WorkoutWidget::none &&
|
|
workoutWidget()->cursorBlock != QPainterPath()) {
|
|
|
|
QColor darken = Qt::black;
|
|
darken.setAlpha(125);
|
|
painter->fillPath(workoutWidget()->cursorBlock, QBrush(darken));
|
|
|
|
// cursor block duration text
|
|
QFontMetrics fontMetrics(workoutWidget()->bigFont);
|
|
QRect textBound = fontMetrics.boundingRect(workoutWidget()->cursorBlockText);
|
|
painter->setFont(workoutWidget()->bigFont);
|
|
painter->setPen(GColor(CPLOTMARKER));
|
|
|
|
QPointF where(workoutWidget()->cursorBlock.boundingRect().center().x()-(textBound.width()/2),
|
|
workoutWidget()->cursorBlock.boundingRect().bottom()-10); //XXX 10 is hardcoded space from bottom
|
|
|
|
painter->drawText(where, workoutWidget()->cursorBlockText);
|
|
|
|
QRect textBound2 = fontMetrics.boundingRect(workoutWidget()->cursorBlockText2);
|
|
QPointF where2(workoutWidget()->cursorBlock.boundingRect().center().x()-(textBound2.width()/2),
|
|
where.y()-textBound.height()); //XXX 4 is hardcoded space between labels
|
|
|
|
painter->drawText(where2, workoutWidget()->cursorBlockText2);
|
|
|
|
}
|
|
}
|
|
|
|
void
|
|
WWBlockSelection::paint(QPainter *painter)
|
|
{
|
|
// don't show when recording
|
|
if (workoutWidget()->recording()) return;
|
|
|
|
// draw the block selection
|
|
if (workoutWidget()->selectionBlock == QPainterPath()) return;
|
|
|
|
// set pen
|
|
painter->setPen(GColor(CPLOTMARKER));
|
|
|
|
// now draw the path
|
|
painter->drawPath(workoutWidget()->selectionBlock);
|
|
|
|
// and fill it
|
|
QColor darken = Qt::red;
|
|
darken.setAlpha(125);
|
|
painter->fillPath(workoutWidget()->selectionBlock, QBrush(darken));
|
|
|
|
// cursor block duration text
|
|
QFontMetrics fontMetrics(workoutWidget()->bigFont);
|
|
QRect textBound = fontMetrics.boundingRect(workoutWidget()->selectionBlockText);
|
|
painter->setFont(workoutWidget()->bigFont);
|
|
painter->setPen(GColor(CPLOTMARKER));
|
|
|
|
QPointF where(workoutWidget()->selectionBlock.boundingRect().center().x()-(textBound.width()/2),
|
|
workoutWidget()->selectionBlock.boundingRect().bottom()-10); //XXX 10 is hardcoded space from bottom
|
|
|
|
painter->drawText(where, workoutWidget()->selectionBlockText);
|
|
|
|
QRect textBound2 = fontMetrics.boundingRect(workoutWidget()->selectionBlockText2);
|
|
QPointF where2(workoutWidget()->selectionBlock.boundingRect().center().x()-(textBound2.width()/2),
|
|
where.y()-textBound.height()); //XXX 4 is hardcoded space between labels
|
|
|
|
painter->drawText(where2, workoutWidget()->selectionBlockText2);
|
|
}
|
|
|
|
// locate me on the parent widget in paint coordinates
|
|
QRectF
|
|
WWBlockSelection::bounding()
|
|
{
|
|
return QRectF();
|
|
}
|
|
|
|
//W'bal curve paint
|
|
void
|
|
WWWBLine::paint(QPainter *painter)
|
|
{
|
|
int rnum = -1;
|
|
|
|
// CP etc are not available so draw nothing
|
|
if (context->athlete->zones(false) == NULL || (rnum = context->athlete->zones(false)->whichRange(QDate::currentDate())) == -1) return;
|
|
|
|
// lets get the zones, CP and PMAX
|
|
int WPRIME = context->athlete->zones(false)->getWprime(rnum);
|
|
|
|
// should be translucent if recording, as will be "overwritten"
|
|
// by the actual W'bal value
|
|
QColor color = GColor(CWBAL);
|
|
if (workoutWidget()->recording()) color.setAlpha(64);
|
|
|
|
// set pen
|
|
QPen linePen(color);
|
|
linePen.setWidth(1 *dpiXFactor);
|
|
painter->setPen(linePen);
|
|
|
|
// top left origin
|
|
QPointF tl = workoutWidget()->canvas().topLeft();
|
|
|
|
// pixels per WPRIME value
|
|
double ratio = workoutWidget()->canvas().height() / WPRIME;
|
|
|
|
// join the dots
|
|
QPointF last(tl.x(),tl.y());
|
|
|
|
// run through the wpBal values...
|
|
int secs=0;
|
|
foreach(double y, workoutWidget()->wprime().ydata()) {
|
|
|
|
// next second
|
|
secs++;
|
|
|
|
// this dot...
|
|
if (y < 0) y=0;
|
|
|
|
// x and y pixel location
|
|
double px = workoutWidget()->transform(secs,0).x();
|
|
double py = tl.y() + ((WPRIME-y) * ratio);
|
|
|
|
QPointF dot(px,py);
|
|
painter->drawLine(last, dot);
|
|
last = dot;
|
|
}
|
|
}
|
|
|
|
//MMP Curve
|
|
void
|
|
WWMMPCurve::paint(QPainter *painter)
|
|
{
|
|
// don't show when recording
|
|
if (workoutWidget()->recording()) return;
|
|
|
|
// thin ?
|
|
QPen linePen(GColor(CCP));
|
|
linePen.setWidth(1);
|
|
painter->setPen(linePen);
|
|
|
|
// join the dots
|
|
QPointF last(-1,-1);
|
|
|
|
// run through the wpBal values...
|
|
int secs=0;
|
|
foreach(int watts, workoutWidget()->mmpArray) {
|
|
|
|
// skip zero
|
|
if (watts == 0) { secs++; continue; }
|
|
|
|
// x and y pixel location
|
|
QPointF point = workoutWidget()->transform(secs,watts);
|
|
|
|
// use a log scale?
|
|
if (workoutWidget()->logScale()) point.setX(workoutWidget()->logX(secs));
|
|
|
|
if (last.x() >= 0) painter->drawLine(last, point);
|
|
|
|
// move on
|
|
last = point;
|
|
secs++;
|
|
|
|
}
|
|
}
|
|
|
|
void
|
|
WWSmartGuide::paint(QPainter *painter)
|
|
{
|
|
// don't show when recording
|
|
if (workoutWidget()->recording()) return;
|
|
|
|
// if too small paint nothing
|
|
if (workoutWidget()->height() < MINTOOLHEIGHT) return;
|
|
|
|
QPointF tl(-1,-1);
|
|
QPointF br(-1,-1);
|
|
int selected=0;
|
|
|
|
//
|
|
// FIND SCOPE TO MARK GUDIES FOR
|
|
//
|
|
// Currently we add guides when items are selected
|
|
// or you are dragging a block or point around
|
|
//
|
|
|
|
// If not dragging points or blocks just mark any selected items
|
|
if (workoutWidget()->state == WorkoutWidget::none) {
|
|
|
|
// get the boundary of the current selection
|
|
foreach(WWPoint *p, workoutWidget()->points()) {
|
|
|
|
// don't show guides whilst selecting, too noisy
|
|
if (!p->selected) continue;
|
|
|
|
selected++;
|
|
|
|
// top left
|
|
if (tl.x() == -1 || tl.x() > p->x) tl.setX(p->x);
|
|
if (tl.y() == -1 || tl.y() < p->y) tl.setY(p->y);
|
|
|
|
// bottom right
|
|
if (br.x() == -1 || br.x() < p->x) br.setX(p->x);
|
|
if (br.y() == -1 || br.y() > p->y) br.setY(p->y);
|
|
}
|
|
}
|
|
|
|
// if dragging a block mark it
|
|
if (workoutWidget()->state == WorkoutWidget::dragblock) {
|
|
|
|
foreach(PointMemento m, workoutWidget()->cr8block) {
|
|
|
|
// how many points
|
|
selected ++;
|
|
|
|
// top left
|
|
if (tl.x() == -1 || tl.x() > m.x) tl.setX(m.x);
|
|
if (tl.y() == -1 || tl.y() < m.y) tl.setY(m.y);
|
|
|
|
// bottom right
|
|
if (br.x() == -1 || br.x() < m.x) br.setX(m.x);
|
|
if (br.y() == -1 || br.y() > m.y) br.setY(m.y);
|
|
}
|
|
}
|
|
|
|
// if dragging a point mark that
|
|
if (workoutWidget()->state == WorkoutWidget::drag && workoutWidget()->dragging) {
|
|
|
|
// how many points
|
|
selected=1;
|
|
|
|
tl.setX(workoutWidget()->dragging->x);
|
|
br.setX(workoutWidget()->dragging->x);
|
|
tl.setY(workoutWidget()->dragging->y);
|
|
br.setY(workoutWidget()->dragging->y);
|
|
}
|
|
|
|
// set the boundary
|
|
QRectF boundary (workoutWidget()->transform(tl.x(), tl.y()),
|
|
workoutWidget()->transform(br.x(), br.y()));
|
|
|
|
|
|
QFontMetrics fontMetrics(workoutWidget()->markerFont);
|
|
painter->setFont(workoutWidget()->markerFont);
|
|
|
|
//
|
|
// X-AXIS GUIDES
|
|
//
|
|
if (selected > 0) {
|
|
|
|
// for now just paint the boundary tics on the x-axis
|
|
QPen linePen(GColor(CPLOTMARKER));
|
|
linePen.setWidthF(0);
|
|
painter->setPen(linePen);
|
|
|
|
QRectF bottom = workoutWidget()->bottom();
|
|
|
|
// top line width indicators
|
|
painter->drawLine(boundary.bottomLeft().x(), workoutWidget()->bottom().y(),
|
|
boundary.bottomLeft().x(), workoutWidget()->bottom().y()+bottom.height());
|
|
|
|
painter->drawLine(boundary.bottomRight().x(), bottom.y(),
|
|
boundary.bottomRight().x(), bottom.y()+bottom.height());
|
|
|
|
// now the text - but only if we have a gap!
|
|
if (br.x() > tl.x()) {
|
|
|
|
// paint time at very bottom of bottom in the middle
|
|
// of the elongated tic marks
|
|
QString text = time_to_string(br.x()-tl.x());
|
|
QRect bound = fontMetrics.boundingRect(text);
|
|
QPoint here(boundary.center().x()-(bound.width()/2), bottom.bottom());
|
|
|
|
painter->drawText(here, text);
|
|
}
|
|
|
|
// find next and previous points, so we can mark them too
|
|
// this is useful when shifting left and right
|
|
int prev=-1, next=-1;
|
|
for(int i=0; i < workoutWidget()->points().count(); i++) {
|
|
WWPoint *p = workoutWidget()->points()[i];
|
|
if (p->x < tl.x()) prev=i; // to left
|
|
if (p->x > br.x()) {
|
|
next=i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// in seconds
|
|
int left = prev >= 0 ? workoutWidget()->points()[prev]->x : 0;
|
|
int right = next >= 0 ? workoutWidget()->points()[next]->x : workoutWidget()->maxWX();
|
|
|
|
// in plot coordinates
|
|
QPointF leftpx = workoutWidget()->transform(left,0);
|
|
QPointF rightpx = workoutWidget()->transform(right,0);
|
|
leftpx.setY(bottom.y());
|
|
rightpx.setY(bottom.y());
|
|
|
|
// left indicator
|
|
painter->drawLine(leftpx, leftpx + QPointF(0, bottom.height()));
|
|
painter->drawLine(rightpx, rightpx + QPointF(0, bottom.height()));
|
|
|
|
// now the left text - but only if we have a gap!
|
|
if (left < tl.x()) {
|
|
|
|
// paint time at very bottom of bottom in the middle
|
|
// of the elongated tic marks
|
|
QString text = time_to_string(tl.x() - left);
|
|
QRect bound = fontMetrics.boundingRect(text);
|
|
QPointF here(leftpx.x() + ((boundary.left() - leftpx.x())/2) - (bound.width()/2), bottom.bottom());
|
|
|
|
painter->drawText(here, text);
|
|
}
|
|
|
|
// now the right text - but only if we have a gap!
|
|
if (right > br.x()) {
|
|
|
|
// paint time at very bottom of bottom in the middle
|
|
// of the elongated tic marks
|
|
QString text = time_to_string(right - br.x());
|
|
QRect bound = fontMetrics.boundingRect(text);
|
|
QPointF here(boundary.right() + ((rightpx.x()-boundary.right())/2) - (bound.width()/2), bottom.bottom());
|
|
painter->drawText(here, text);
|
|
}
|
|
|
|
#if 0 // doesn't add much
|
|
// side line width indicators
|
|
painter->drawLine(boundary.topLeft()-QPointF(10,0), boundary.topLeft()-QPointF(30,0));
|
|
painter->drawLine(boundary.bottomLeft()-QPointF(10,0), boundary.bottomLeft()-QPointF(30,0));
|
|
painter->drawLine(boundary.bottomLeft()-QPointF(20,0), boundary.topLeft()-QPointF(20,0));
|
|
#endif
|
|
}
|
|
|
|
//
|
|
// Y-AXIS GUIDES
|
|
//
|
|
if (selected > 0) {
|
|
|
|
// for now just paint the boundary tics on the y-axis
|
|
QPen linePen(GColor(CPLOTMARKER));
|
|
linePen.setWidthF(0);
|
|
painter->setPen(linePen);
|
|
|
|
// top line width indicators
|
|
painter->drawLine(workoutWidget()->left().left(), boundary.topRight().y(),
|
|
workoutWidget()->left().right(), boundary.topRight().y());
|
|
|
|
painter->drawLine(workoutWidget()->left().left(), boundary.bottomLeft().y(),
|
|
workoutWidget()->left().right(), boundary.bottomLeft().y());
|
|
|
|
|
|
// now the texts
|
|
// paint the bottom value
|
|
QString text = QString("%1w").arg(double(tl.y()), 0, 'f', 0);
|
|
QRect bound = fontMetrics.boundingRect(text);
|
|
painter->drawText(workoutWidget()->left().left(), boundary.top()-SPACING, text);
|
|
if (br.y() < tl.y()) {
|
|
|
|
// paint the bottom value UNDERNEATH the line, just in case they are really close!
|
|
text = QString("%1w").arg(double(br.y()), 0, 'f', 0);
|
|
bound = fontMetrics.boundingRect(text);
|
|
painter->drawText(workoutWidget()->left().left(), boundary.bottom()+SPACING+fontMetrics.ascent(), text);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
WWNow::paint(QPainter *painter)
|
|
{
|
|
// only when recording
|
|
if (!workoutWidget()->recording()) return;
|
|
|
|
// get now
|
|
int px = workoutWidget()->transform(context->getNow()/1000.0f,0).x();
|
|
|
|
QPen linePen(GColor(CPLOTMARKER));
|
|
linePen.setWidthF(2 *dpiXFactor);
|
|
painter->setPen(linePen);
|
|
|
|
// horizontal bar
|
|
painter->drawLine(px, workoutWidget()->canvas().top(), px, workoutWidget()->canvas().bottom());
|
|
|
|
}
|
|
|
|
//
|
|
// COMMANDS
|
|
//
|
|
|
|
// Create a new point
|
|
CreatePointCommand::CreatePointCommand(WorkoutWidget *w, double x, double y, int index)
|
|
: WorkoutWidgetCommand(w), x(x), y(y), index(index) { }
|
|
|
|
// undo create point
|
|
void
|
|
CreatePointCommand::undo()
|
|
{
|
|
// wipe it from the array
|
|
WWPoint *p = NULL;
|
|
if (index >= 0) p = workoutWidget()->points().takeAt(index);
|
|
else p = workoutWidget()->points().takeAt(workoutWidget()->points().count()-1);
|
|
delete p;
|
|
}
|
|
|
|
// do it again
|
|
void
|
|
CreatePointCommand::redo()
|
|
{
|
|
// create a new WWPoint
|
|
WWPoint *p = new WWPoint(workoutWidget(), x,y, false);
|
|
|
|
// -1 means append
|
|
if (index < 0)
|
|
workoutWidget()->points().append(p);
|
|
else
|
|
workoutWidget()->points().insert(index, p);
|
|
}
|
|
|
|
// create a block
|
|
CreateBlockCommand::CreateBlockCommand(WorkoutWidget *w, QList<PointMemento>&points)
|
|
: WorkoutWidgetCommand(w), points(points) { }
|
|
|
|
// undo create block
|
|
void
|
|
CreateBlockCommand::undo()
|
|
{
|
|
// delete the points in reverse
|
|
for (int i=points.count()-1; i>=0; i--) {
|
|
WWPoint *p = NULL;
|
|
if (points[i].index >= 0) p = workoutWidget()->points().takeAt(points[i].index);
|
|
else p = workoutWidget()->points().takeAt(workoutWidget()->points().count()-1);
|
|
delete p;
|
|
}
|
|
}
|
|
|
|
// do it again
|
|
void
|
|
CreateBlockCommand::redo()
|
|
{
|
|
// add the points forward
|
|
foreach(PointMemento m, points) {
|
|
|
|
// create a new WWPoint
|
|
WWPoint *p = new WWPoint(workoutWidget(), m.x,m.y, false);
|
|
|
|
// -1 means append
|
|
if (m.index < 0)
|
|
workoutWidget()->points().append(p);
|
|
else
|
|
workoutWidget()->points().insert(m.index, p);
|
|
}
|
|
}
|
|
|
|
MovePointsCommand::MovePointsCommand(WorkoutWidget *w, QList<PointMemento> before, QList<PointMemento> after)
|
|
: WorkoutWidgetCommand(w), before(before), after(after) { }
|
|
|
|
void
|
|
MovePointsCommand::undo()
|
|
{
|
|
foreach(PointMemento m, before) {
|
|
WWPoint *p = workoutWidget()->points()[m.index];
|
|
p->x = m.x;
|
|
p->y = m.y;
|
|
}
|
|
}
|
|
|
|
void
|
|
MovePointsCommand::redo()
|
|
{
|
|
foreach(PointMemento m, after) {
|
|
WWPoint *p = workoutWidget()->points()[m.index];
|
|
p->x = m.x;
|
|
p->y = m.y;
|
|
}
|
|
}
|
|
|
|
MovePointCommand::MovePointCommand(WorkoutWidget *w, QPointF before, QPointF after, int index)
|
|
: WorkoutWidgetCommand(w), before(before), after(after), index(index) { }
|
|
|
|
void
|
|
MovePointCommand::undo()
|
|
{
|
|
WWPoint *p = workoutWidget()->points()[index];
|
|
|
|
p->x = before.x();
|
|
p->y = before.y();
|
|
}
|
|
|
|
void
|
|
MovePointCommand::redo()
|
|
{
|
|
WWPoint *p = workoutWidget()->points()[index];
|
|
|
|
p->x = after.x();
|
|
p->y = after.y();
|
|
}
|
|
|
|
ScaleCommand::ScaleCommand(WorkoutWidget *w, double up, double down, bool scaleup)
|
|
: WorkoutWidgetCommand(w), up(up), down(down), scaleup(scaleup) { }
|
|
|
|
void
|
|
ScaleCommand::undo()
|
|
{
|
|
double factor = scaleup ? down : up;
|
|
foreach(WWPoint *p, workoutWidget()->points())
|
|
p->y *= factor;
|
|
}
|
|
|
|
void
|
|
ScaleCommand::redo()
|
|
{
|
|
double factor = scaleup ? up : down;
|
|
foreach(WWPoint *p, workoutWidget()->points())
|
|
p->y *= factor;
|
|
}
|
|
|
|
DeleteWPointsCommand::DeleteWPointsCommand(WorkoutWidget*w, QList<PointMemento>points)
|
|
: WorkoutWidgetCommand(w), points(points) { }
|
|
|
|
void
|
|
DeleteWPointsCommand::redo()
|
|
{
|
|
// delete backward
|
|
for (int i=points.count()-1; i>=0; i--) {
|
|
PointMemento m = points[i];
|
|
WWPoint *rm = workoutWidget()->points().takeAt(m.index);
|
|
delete rm;
|
|
}
|
|
}
|
|
|
|
void
|
|
DeleteWPointsCommand::undo()
|
|
{
|
|
// add forward
|
|
foreach(PointMemento m, points) {
|
|
WWPoint *add = new WWPoint(workoutWidget(), m.x, m.y, false);
|
|
workoutWidget()->points().insert(m.index, add);
|
|
}
|
|
}
|
|
|
|
CutCommand::CutCommand(WorkoutWidget*w, QList<PointMemento>copyIndexes, QList<PointMemento> deleteIndexes, double shift)
|
|
: WorkoutWidgetCommand(w), copyIndexes(copyIndexes), deleteIndexes(deleteIndexes), shift(shift) { }
|
|
|
|
void
|
|
CutCommand::redo()
|
|
{
|
|
// delete again
|
|
int last = -1;
|
|
foreach (PointMemento m, deleteIndexes) {
|
|
WWPoint *remove = workoutWidget()->points().takeAt(m.index);
|
|
delete remove;
|
|
last = m.index;
|
|
}
|
|
|
|
// shift in
|
|
for (int i=last; i<workoutWidget()->points().count(); i++) {
|
|
workoutWidget()->points()[i]->x -= shift;
|
|
}
|
|
|
|
// update clipboard
|
|
workoutWidget()->setClipboard(copyIndexes);
|
|
}
|
|
|
|
void
|
|
CutCommand::undo()
|
|
{
|
|
// undelete
|
|
int last=-1;
|
|
// re-add backwards
|
|
for (int i=deleteIndexes.count()-1; i >=0; i--) {
|
|
|
|
PointMemento m = deleteIndexes[i];
|
|
WWPoint *add = new WWPoint(workoutWidget(), m.x, m.y, false);
|
|
workoutWidget()->points().insert(m.index, add);
|
|
last = m.index;
|
|
}
|
|
|
|
// shift out
|
|
for (int i=last+1; i<workoutWidget()->points().count(); i++) {
|
|
workoutWidget()->points()[i]->x += shift;
|
|
}
|
|
}
|
|
|
|
PasteCommand::PasteCommand(WorkoutWidget*w, int here, double offset, double shift, QList<PointMemento> points)
|
|
: WorkoutWidgetCommand(w), here(here), offset(offset), shift(shift), points(points) {}
|
|
|
|
|
|
void PasteCommand::undo()
|
|
{
|
|
// remove the added points
|
|
for(int i=points.count(); i>0; i--) {
|
|
WWPoint *take = workoutWidget()->points().takeAt(here == -1 ? (workoutWidget()->points().count()-1) : here);
|
|
delete take;
|
|
}
|
|
|
|
// unshift
|
|
if (here != -1) {
|
|
for(int i=here; i<workoutWidget()->points().count(); i++) {
|
|
workoutWidget()->points()[i]->x -= shift;
|
|
}
|
|
}
|
|
|
|
// reduce maxWX if needed, never less than an hour
|
|
if (workoutWidget()->points().count() && workoutWidget()->points().last()->x < workoutWidget()->maxWX() &&
|
|
workoutWidget()->maxWX()>3600) {
|
|
if (workoutWidget()->points().last()->x<3600)
|
|
workoutWidget()->setMaxWX(3600);
|
|
else
|
|
workoutWidget()->setMaxWX(workoutWidget()->points().last()->x);
|
|
}
|
|
}
|
|
|
|
void PasteCommand::redo()
|
|
{
|
|
// if its the last point append!
|
|
if (here != -1) {
|
|
for(int i=here; i<workoutWidget()->points().count(); i++) {
|
|
workoutWidget()->points()[i]->x += shift;
|
|
}
|
|
}
|
|
|
|
// here is either the index to append after
|
|
// or we add to the end of the workout
|
|
foreach(PointMemento m, points) {
|
|
|
|
if (here == -1) {
|
|
new WWPoint(workoutWidget(), m.x+offset, m.y);
|
|
} else {
|
|
|
|
WWPoint *add = new WWPoint(workoutWidget(), m.x+offset, m.y, false);
|
|
workoutWidget()->points().insert(here + m.index, add);
|
|
}
|
|
}
|
|
|
|
// increase maxWX if needed?
|
|
if (workoutWidget()->points().count() && workoutWidget()->points().last()->x > workoutWidget()->maxWX())
|
|
workoutWidget()->setMaxWX(workoutWidget()->points().last()->x);
|
|
|
|
}
|
|
|
|
QWKCommand::QWKCommand(WorkoutWidget *w, QString before, QString after)
|
|
: WorkoutWidgetCommand(w), before(before), after(after) {}
|
|
|
|
void
|
|
QWKCommand::redo()
|
|
{
|
|
workoutWidget()->apply(after);
|
|
}
|
|
|
|
void
|
|
QWKCommand::undo()
|
|
{
|
|
workoutWidget()->apply(before);
|
|
}
|